Подтвердить что ты не робот

Почему 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() выдает ошибку? Какую функцию мне нужно вызвать, чтобы я всегда возвращал действительный идентификатор строки?

4b9b3361

Ответ 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);

Надеюсь, что это поможет.