Почему insertWithOnConflict (..., CONFLICT_IGNORE) возвращает -1 (ошибка)?
У меня есть таблица SQLite:
CREATE TABLE regions (_id INTEGER PRIMARY KEY, name TEXT, UNIQUE(name));
И некоторые Android-коды:
Validate.notBlank(region);
ContentValues cv = new ContentValues();
cv.put(Columns.REGION_NAME, region);
long regionId =
db.insertWithOnConflict("regions", null, cv, SQLiteDatabase.CONFLICT_IGNORE);
Validate.isTrue(regionId > -1,
"INSERT ON CONFLICT IGNORE returned -1 for region name '%s'", region);
В повторяющихся строках insertWithOnConflict() возвращает -1, указывая на ошибку, и Validate затем бросает с:
INSERT ON CONFLICT IGNORE returned -1 for region name 'Overseas'
Документация SQLite ON CONFLICT (акцент мой) гласит:
При наличии соответствующего нарушения ограничений алгоритм разрешения IGNORE пропускает одну строку, которая содержит нарушение ограничений, и продолжает обрабатывать последующие строки инструкции SQL, как будто ничего не получилось. Другие строки до и после строки, содержащей нарушение ограничения, вставляются или обновляются нормально. При использовании алгоритма разрешения конфликта IGNORE ошибка не возвращается.
Документация Android insertWithOnConflict() сообщает:
Возвраты идентификатор строки вновь вставленной строки ИЛИ первичный ключ существующей строки, если входной параметр "конфликтAlgorithm" = CONFLICT_IGNORE ИЛИ -1, если какая-либо ошибка
CONFLICT_REPLACE не является вариантом, поскольку замена строк изменит их первичный ключ вместо того, чтобы просто вернуть существующий ключ:
sqlite> INSERT INTO regions (name) VALUES ("Southern");
sqlite> INSERT INTO regions (name) VALUES ("Overseas");
sqlite> SELECT * FROM regions;
1|Southern
2|Overseas
sqlite> INSERT OR REPLACE INTO regions (name) VALUES ("Overseas");
sqlite> SELECT * FROM regions;
1|Southern
3|Overseas
sqlite> INSERT OR REPLACE INTO regions (name) VALUES ("Overseas");
sqlite> SELECT * FROM regions;
1|Southern
4|Overseas
Я думаю, что insertWithOnConflict() должен, в повторяющихся строках, вернуть мне первичный ключ (столбец _id) дублированной строки — поэтому я никогда не должен получать ошибку для этой вставки. Почему insertWithOnConflict() выдает ошибку? Какую функцию мне нужно вызвать, чтобы я всегда возвращал действительный идентификатор строки?
Ответы
Ответ 1
Ответ на ваш вопрос, к сожалению, заключается в том, что документы просто неправильны и нет такой функциональности.
Существует открытая ошибка с 2010 года, которая затрагивает именно эту проблему, и даже несмотря на то, что 80+ человек сняли эту заметку, официального ответа нет из команды Android.
Проблема также обсуждается здесь здесь.
Если ваш прецедент тяжело конфликт (т.е. большую часть времени вы ожидаете найти существующую запись и хотите вернуть этот идентификатор), предлагаемое вами решение обходного пути похоже на путь. Если, с другой стороны, ваш вариант использования таков, что большую часть времени вы ожидаете, что там не будет существующей записи, тогда может быть более целесообразным следующее обходное решение:
try {
insertOrThrow(...)
} catch(SQLException e) {
// Select the required record and get primary key from it
}
Вот автономная реализация этого обходного пути:
public static long insertIgnoringConflict(SQLiteDatabase db,
String table,
String idColumn,
ContentValues values) {
try {
return db.insertOrThrow(table, null, values);
} catch (SQLException e) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT ");
sql.append(idColumn);
sql.append(" FROM ");
sql.append(table);
sql.append(" WHERE ");
Object[] bindArgs = new Object[values.size()];
int i = 0;
for (Map.Entry<String, Object> entry: values.valueSet()) {
sql.append((i > 0) ? " AND " : "");
sql.append(entry.getKey());
sql.append(" = ?");
bindArgs[i++] = entry.getValue();
}
SQLiteStatement stmt = db.compileStatement(sql.toString());
for (i = 0; i < bindArgs.length; i++) {
DatabaseUtils.bindObjectToProgram(stmt, i + 1, bindArgs[i]);
}
try {
return stmt.simpleQueryForLong();
} finally {
stmt.close();
}
}
}
Ответ 2
Хотя ваши ожидания относительно поведения insertWithOnConflict кажутся вполне разумными (вы должны получить pk для сталкивающейся строки), это просто не так, как это работает. Фактически случается, что вы: пытаетесь вставить, не вставляете строку, но не сигнализируете об ошибке, фреймворк подсчитывает количество вставленных строк, обнаруживает, что число равно 0, и явно возвращает -1.
Отредактировано для добавления:
Btw, этот ответ основан на коде, который, в конце концов, реализует insertWithOnConflict:
int err = executeNonQuery(env, connection, statement);
return err == SQLITE_DONE && sqlite3_changes(connection->db) > 0
? sqlite3_last_insert_rowid(connection->db) : -1;
SQLITE_DONE
- хороший статус; sqlite3_changes
- количество вставок в последнем вызове, а sqlite3_last_insert_rowid
- это rowid для вновь вставленной строки, если таковая имеется.
Отредактировано для ответа на второй вопрос:
После повторного чтения вопроса, я думаю, что то, что вы ищете, это метод, который делает это:
- вставляет новую строку в db, если это возможно
- если он не может вставить строку, не удается и возвращает rowid для существующей строки, которая конфликтует (без изменения этой строки)
Вся дискуссия о замене выглядит как красная селедка.
Таким образом, ответ на ваш второй вопрос заключается в том, что такой функции нет.
Ответ 3
Проблема уже решена, но это может быть вариант, который разрешил мою проблему. Просто изменив последний параметр на CONFLICT_REPLACE.
long regionId =
db.insertWithOnConflict("regions", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
Надеюсь, что это поможет.