25

目前,我正在使用以下语句在 Android 设备上的 SQLite 数据库中创建表。

CREATE TABLE IF NOT EXISTS 'locations' (
  '_id' INTEGER PRIMARY KEY AUTOINCREMENT, 'name' TEXT, 
  'latitude' REAL, 'longitude' REAL, 
  UNIQUE ( 'latitude',  'longitude' ) 
ON CONFLICT REPLACE );

最后的冲突子句导致在完成具有相同坐标的新插入时删除行。SQLite 文档包含有关冲突子句的更多信息。

相反,我想保留以前的行并只更新它们的列。在 Android/SQLite 环境中执行此操作的最有效方法是什么?

  • 作为CREATE TABLE声明中的冲突条款。
  • 作为INSERT触发器。
  • 作为ContentProvider#insert方法中的条件子句。
  • ...任何更好的你可以考虑

我认为在数据库中处理此类冲突会更高效。此外,我发现很难重写该ContentProvider#insert方法以考虑插入更新方案。这是该insert方法的代码:

public Uri insert(Uri uri, ContentValues values) {
    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    long id = db.insert(DatabaseProperties.TABLE_NAME, null, values);
    return ContentUris.withAppendedId(uri, id);
}

当数据从后端到达时,我所做的就是按如下方式插入数据。

getContentResolver.insert(CustomContract.Locations.CONTENT_URI, contentValues);

我在弄清楚如何在ContentProvider#update此处应用替代调用时遇到问题。此外,无论如何,这不是我最喜欢的解决方案。


编辑:

@CommonsWare:我尝试实施您的建议以使用INSERT OR REPLACE. 我想出了这段丑陋的代码。

private static long insertOrReplace(SQLiteDatabase db, ContentValues values, String tableName) {
    final String COMMA_SPACE = ", ";
    StringBuilder columnsBuilder = new StringBuilder();
    StringBuilder placeholdersBuilder = new StringBuilder();
    List<Object> pureValues = new ArrayList<Object>(values.size());
    Iterator<Entry<String, Object>> iterator = values.valueSet().iterator();
    while (iterator.hasNext()) {
        Entry<String, Object> pair = iterator.next();
        String column = pair.getKey();
        columnsBuilder.append(column).append(COMMA_SPACE);
        placeholdersBuilder.append("?").append(COMMA_SPACE);
        Object value = pair.getValue();
        pureValues.add(value);
    }
    final String columns = columnsBuilder.substring(0, columnsBuilder.length() - COMMA_SPACE.length());
    final String placeholders = placeholderBuilder.substring(0, placeholdersBuilder.length() - COMMA_SPACE.length());
    db.execSQL("INSERT OR REPLACE INTO " + tableName + "(" + columns + ") VALUES (" + placeholders + ")", pureValues.toArray());

    // The last insert id retrieved here is not safe. Some other inserts can happen inbetween.
    Cursor cursor = db.rawQuery("SELECT * from SQLITE_SEQUENCE;", null);
    long lastId = INVALID_LAST_ID;
    if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
        lastId = cursor.getLong(cursor.getColumnIndex("seq"));
    }
    cursor.close();
    return lastId;
}

但是,当我检查 SQLite 数据库时,仍会删除相等的列并插入新的 ids 。我不明白为什么会发生这种情况,并认为原因是我的冲突条款。但文档说明相反。

在 INSERT 或 UPDATE 的 OR 子句中指定的算法将 覆盖在 CREATE TABLE 中指定的任何算法。如果任何地方都没有指定算法,则使用 ABORT 算法。

这种尝试的另一个缺点是您丢失了插入语句返回的 id 的值。为了弥补这一点,我终于找到了一个选项来要求last_insert_rowid. 正如dtmilano 和 swiz 的帖子中所解释的那样。但是,我不确定这是否安全,因为中间可能会发生另一个插入。

4

6 回答 6

20

我可以理解这样一种观念,即在 SQL 中执行所有这些逻辑对性能来说是最好的,但在这种情况下,也许最简单(最少代码)的解决方案是最好的解决方案?为什么不先尝试更新,然后使用insertWithOnConflict()withCONFLICT_IGNORE进行插入(如有必要)并获取您需要的行 ID:

public Uri insert(Uri uri, ContentValues values) {
    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    String selection = "latitude=? AND longitude=?"; 
    String[] selectionArgs = new String[] {values.getAsString("latitude"),
                values.getAsString("longitude")};

    //Do an update if the constraints match
    db.update(DatabaseProperties.TABLE_NAME, values, selection, null);

    //This will return the id of the newly inserted row if no conflict
    //It will also return the offending row without modifying it if in conflict
    long id = db.insertWithOnConflict(DatabaseProperties.TABLE_NAME, null, values, CONFLICT_IGNORE);        

    return ContentUris.withAppendedId(uri, id);
}

一个更简单的解决方案是检查的返回值,update()并且仅在受影响的计数为零时才进行插入,但是在这种情况下,如果没有额外的选择,您将无法获得现有行的 id。这种形式的插入将始终返回给您正确的 id 以传回Uri,并且不会过多地修改数据库。

如果您想一次执行大量这些操作,您可以查看bulkInsert()提供程序上的方法,您可以在单个事务中运行多个插入。在这种情况下,由于您不需要返回id更新记录的,“更简单”的解决方案应该可以正常工作:

public int bulkInsert(Uri uri, ContentValues[] values) {
    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    String selection = "latitude=? AND longitude=?";
    String[] selectionArgs = null;

    int rowsAdded = 0;
    long rowId;
    db.beginTransaction();
    try {
        for (ContentValues cv : values) {
            selectionArgs = new String[] {cv.getAsString("latitude"),
                cv.getAsString("longitude")};

            int affected = db.update(DatabaseProperties.TABLE_NAME, 
                cv, selection, selectionArgs);
            if (affected == 0) {
                rowId = db.insert(DatabaseProperties.TABLE_NAME, null, cv);
                if (rowId > 0) rowsAdded++;
            }
        }
        db.setTransactionSuccessful();
    } catch (SQLException ex) {
        Log.w(TAG, ex);
    } finally {
        db.endTransaction();
    }

    return rowsAdded;
}

事实上,事务代码通过最小化数据库内存写入文件的次数来使事情变得更快,bulkInsert()只允许ContentValues通过一次调用提供者来传递多个。

于 2012-08-02T15:54:55.643 回答
5

一种解决方案是使用视图上locations的 INSTEAD OF 触发器为表创建视图,然后插入到视图中。这就是它的样子:

看法:

CREATE VIEW locations_view AS SELECT * FROM locations;

扳机:

CREATE TRIGGER update_location INSTEAD OF INSERT ON locations_view FOR EACH ROW 
  BEGIN 
    INSERT OR REPLACE INTO locations (_id, name, latitude, longitude) VALUES ( 
       COALESCE(NEW._id, 
         (SELECT _id FROM locations WHERE latitude = NEW.latitude AND longitude = NEW.longitude)),
       NEW.name, 
       NEW.latitude, 
       NEW.longitude
    );
  END;

不是插入到locations表中,而是插入到locations_view视图中。触发器将_id通过使用子选择来提供正确的值。如果由于某种原因,插入已经包含一个_idCOALESCE则将保留它并覆盖表中现有的。

您可能想要检查子选择对性能的影响程度,并将其与您可以进行的其他可能更改进行比较,但它确实允许您将此逻辑排除在代码之外。

我尝试了一些其他解决方案,这些解决方案涉及基于 INSERT OR IGNORE 对表本身的触发器,但似乎 BEFORE 和 AFTER 触发器只有在它实际插入表时才会触发。

您可能会发现此答案很有帮助,这是触发器的基础。

编辑:由于忽略插入时 BEFORE 和 AFTER 触发器不会触发(然后可能已经更新),我们需要使用 INSTEAD OF 触发器重写插入。不幸的是,这些不适用于表格 - 我们必须创建一个视图才能使用它。

于 2012-08-02T00:38:51.977 回答
3

INSERT OR REPLACE就像ON CONFLICT REPLACE. 如果具有唯一列的行已经存在并且它会插入,它将删除该行。它永远不会更新。

我建议您坚持使用当前的解决方案,使用ON CONFLICTclausule 创建表,但是每次插入一行并且发生约束冲突时,您的新行都会有新_id的原始行将被删除。

或者您可以创建不带分句的表ON CONFLICT并使用INSERT OR REPLACE,您可以为此使用insertWithOnConflict()方法,但它从 API 级别 8 开始可用,需要更多编码并导致与带分句的表相同的解决方案ON CONFLICT

如果您仍想保留原始行,则意味着您想保持不变_id,您将必须进行两次查询,第一个用于插入行,第二个用于在插入失败时更新行(反之亦然)。为了保持一致性,您必须在事务中执行查询。

    db.beginTransaction();
    try {
        long rowId = db.insert(table, null, values);
        if (rowId == -1) {
            // insertion failed
            String whereClause = "latitude=? AND longitude=?"; 
            String[] whereArgs = new String[] {values.getAsString("latitude"),
                    values.getAsString("longitude")};
            db.update(table, values, whereClause, whereArgs);
            // now you have to get rowId so you can return correct Uri from insert()
            // method of your content provider, so another db.query() is required
        }
        db.setTransactionSuccessful();
    } finally {
        db.endTransaction();
    }
于 2012-07-31T00:37:46.290 回答
0

使用insertWithOnConflict并将最后一个参数 ( conflictAlgorithm) 设置为CONFLICT_REPLACE

在以下链接中阅读更多信息:

insertWithOnConflict 文档

CONFLICT_REPLACE 标志

于 2013-06-18T12:48:46.883 回答
0

对我来说,如果我没有“_id”,这些方法都不起作用

您应该首先调用更新,如果受影响的行为零,然后将其插入忽略:

 String selection = MessageDetailTable.SMS_ID+" =?";
 String[] selectionArgs =  new String[] { String.valueOf(md.getSmsId())};

 int affectedRows = db.update(MessageDetailTable.TABLE_NAME, values, selection,selectionArgs);

 if(affectedRows<=0) {
     long id = db.insertWithOnConflict(MessageDetailTable.TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
 }
于 2020-05-25T22:47:12.807 回答
-2

使用插入或替换

这是正确的做法。

于 2012-08-03T18:29:39.330 回答