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

最後の conflict-clause により、同じ座標を持つ新しい挿入が行われたときに行が削除されます。SQLiteのドキュメントには、conflict-clause に関する詳細情報が含まれています。

代わりに、以前の行を保持し、それらの列を更新したいと思います。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 アルゴリズムが使用されます。

この試行のもう 1 つの欠点は、insert ステートメントによって返される ID の値が失われることです。これを補うために、最終的にlast_insert_rowid. dtmilano と swiz の投稿で説明されているとおりです。ただし、間に別の挿入が発生する可能性があるため、これが安全かどうかはわかりません。

4

6 に答える 6

20

このすべてのロジックを SQL で実行するのがパフォーマンスにとって最善であるという認識は理解できますが、この場合、おそらく最も単純な (最小限のコード) ソリューションが最適なソリューションでしょうか? 最初に更新を試みてから、with を使用insertWithOnConflict()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);
}

より簡単な解決策は、戻り値を確認しupdate()、影響を受けるカウントがゼロの場合にのみ挿入を行うことですが、選択を追加しないと既存の行の ID を取得できない場合があります。この形式の挿入では、常に正しい ID が返されて に返され、Uri必要以上にデータベースが変更されることはありません。

If you want to do a large number of these at once, you might look at the bulkInsert() method on your provider, where you can run multiple inserts inside a single transaction. In this case, since you don't need to return the id of the updated record, the "simpler" solution should work just fine:

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

In truth, the transaction code is what makes things faster by minimizing the number of times the database memory is written to the file, bulkInsert() just allows multiple ContentValues to be passed in with a single call to the provider.

于 2012-08-02T15:54:55.643 に答える
5

1 つの解決策は、ビューに INSTEAD OF トリガーを使用してテーブルのビューを作成し、ビューlocationsに挿入することです。これは次のようになります。

意見:

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トリガーは、サブセレクトを使用して正しい値を提供します。何らかの理由で、挿入にすでに が含まれている場合は、_idそれCOALESCEを保持し、テーブル内の既存のものを上書きします。

おそらく、サブセレクトがパフォーマンスにどの程度影響するかを確認し、それを他の可能な変更と比較する必要がありますが、これにより、このロジックをコードから除外できます。

INSERT OR IGNOREに基づいてテーブル自体にトリガーを含む他のソリューションをいくつか試しましたが、BEFOREおよびAFTERトリガーは、実際にテーブルに挿入される場合にのみトリガーされるようです。

トリガーの基礎となるこの回答が役立つ場合があります。

編集: 挿入が無視されたときに BEFORE および AFTER トリガーが起動しないため (代わりに更新された可能性があります)、INSTEAD OF トリガーを使用して挿入を書き直す必要があります。残念ながら、これらはテーブルでは機能しません。使用するにはビューを作成する必要があります。

于 2012-08-02T00:38:51.977 に答える
3

INSERT OR REPLACE works just like ON CONFLICT REPLACE. It will delete the row if the row with the unique column already exists and than it does an insert. It never does update.

I would recommend you stick with your current solution, you create table with ON CONFLICT clausule, but every time you insert a row and the constraint violation occurs, your new row will have new _id as origin row will be deleted.

Or you can create table without ON CONFLICT clausule and use INSERT OR REPLACE, you can use insertWithOnConflict() method for that, but it is available since API level 8, requires more coding and leads to the same solution as table with ON CONFLICT clausule.

If you still want to keep your origin row, it means you want to keep the same _id you will have to make two queries, first one for inserting a row, second to update a row if insertion failed (or vice versa). To preserve consistency, you have to execute queries in a transaction.

    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 に答える
-2

INSERT OR REPLACEを使用します。

これが正しい方法です。

于 2012-08-03T18:29:39.330 に答える