为什么insertWithOnConflict(...,CONFLICT_IGNORE)返回-1(错误)?

时间:2012-11-15 05:17:27

标签: android android-sqlite

我有一个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 documentation(强调我的)陈述:

  

当发生适用的约束违规时,IGNORE解析算法会跳过包含约束违规的一行,并继续处理SQL语句的后续行,就像没有出错一样。包含约束违例的行之前和之后的其他行将正常插入或更新。 使用IGNORE冲突解决算法时不会返回错误。

Android insertWithOnConflict() documentation州:

  

返回   如果输入参数'conflictAlgorithm'= CONFLICT_IGNORE则为新插入行的行ID或现有行的主键,如果有任何错误则为-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()抛出错误?我需要调用什么函数才能获得有效的行ID?

3 个答案:

答案 0 :(得分:31)

遗憾的是,您的问题的答案是文档完全错误,并且没有此类功能。

an open bug from 2010正好解决了这个问题,即使已有80多人加注,但Android团队也没有正式回复。

问题是also discussed on SO here

如果您的用例存在冲突(即大多数情况下您希望找到现有记录并希望返回该ID),您建议的解决方法似乎就要走了。另一方面,如果您的用例大多数情况下您希望没有现有记录,那么以下解决方法可能更合适:

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();
        }
    }
}

答案 1 :(得分:3)

虽然您对insertWithOnConflict行为的期望似乎完全合理(您应该获得碰撞行的pk),但这不是它的工作原理。实际发生的是你:尝试插入,它无法插入行但是没有错误信号,框架计算插入的行数,发现数字为0,并且显式地返回-1。

已编辑添加:

顺便说一下,这个答案是基于最终实现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(如果有)。

编辑回答第二个问题:

重新阅读问题之后,我认为您正在寻找的是一种方法:

  • 如果可能的话,将新行插入数据库
  • 如果无法插入行,则会失败并返回冲突的现有行的rowid(不更改该rowid)

替换的整个讨论看起来像红鲱鱼。

第二个问题的答案是,没有这样的功能。

答案 2 :(得分:-2)

问题已经解决,但这可能是解决我的问题的一个选择。只需将最后一个参数更改为CONFLICT_REPLACE。

long regionId = 
db.insertWithOnConflict("regions", null, cv, SQLiteDatabase.CONFLICT_REPLACE);

希望它有所帮助。