22

我有一个 SQLite 表:

CREATE TABLE regions (_id INTEGER PRIMARY KEY, name TEXT, UNIQUE(name));

还有一些安卓代码:

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 () 文档指出:

如果输入参数 '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?

4

3 回答 3

31

不幸的是,您的问题的答案是文档完全错误并且没有这样的功能。

从 2010 年开始,有一个开放的错误正好解决了这个问题,尽管 80 多人已经为它加注了星标,但 Android 团队没有官方回应。

这个问题也在 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();
        }
    }
}
于 2013-02-25T10:26:25.180 回答
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)

对替换的整个讨论似乎是红鲱鱼。

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

于 2013-02-22T16:02:32.903 回答
-1

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

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

希望能帮助到你。

于 2015-09-30T05:35:46.487 回答