Android/SQLite:插入 - 更新表列以保留标识符

JJD*_*JJD 25 sqlite android insert-update android-contentprovider

目前,我使用以下语句在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 );
Run Code Online (Sandbox Code Playgroud)

最后的conflict-clause导致在完成具有相同坐标的新插入时删除行.在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);
}
Run Code Online (Sandbox Code Playgroud)

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

getContentResolver.insert(CustomContract.Locations.CONTENT_URI, contentValues);
Run Code Online (Sandbox Code Playgroud)

我在弄清楚如何在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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Dev*_*red 19

我可以理解在SQL中使用所有这些逻辑最好的性能,但在这种情况下,最简单的(最少代码)解决方案可能是最好的解决方案吗?为什么不首先尝试更新,然后使用insertWithOnConflict()with CONFLICT_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);
}
Run Code Online (Sandbox Code Playgroud)

一个更简单的解决方案是检查返回值,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;
}
Run Code Online (Sandbox Code Playgroud)

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


bla*_*oni 5

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

视图:

CREATE VIEW locations_view AS SELECT * FROM locations;
Run Code Online (Sandbox Code Playgroud)

触发:

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;
Run Code Online (Sandbox Code Playgroud)

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

您可能希望检查子选择对性能的影响程度,并将其与可能进行的其他可能的更改进行比较,但它确实允许您将此逻辑保留在代码之外.

我尝试了一些其他涉及基于INSERT或IGNORE的表自身触发器的解决方案,但似乎BEFORE和AFTER触发器仅在它实际插入表时触发.

您可能会发现此答案很有用,这是触发器的基础.

编辑:由于BEFORE和AFTER触发器在忽略插入时没有触发(可能已经更新了),我们需要使用INSTEAD OF触发器重写插入.不幸的是,那些不适用于表 - 我们必须创建一个视图来使用它.