web-dev-qa-db-ja.com

Android / SQLite:識別子を保持するためのInsert-Updateテーブル列

現在、私は次のステートメントを使用して、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により、同じ座標を持つ新しい挿入が行われたときに、行がdroppedされます。 SQLiteドキュメント には、conflict-clauseに関する詳細情報が含まれています。

代わりに、以前の行をkeepにしてupdateしたいだけです列。 Android/SQLite環境でこれを行う最も効率的な方法は何ですか?

  • CREATE TABLEステートメントの競合句として。
  • INSERTトリガーとして。
  • ContentProvider#insertメソッドの条件節として。
  • ...あなたが考えることができるより良い

データベース内でこのような競合を処理する方がパフォーマンスが高いと思います。また、ContentProvider#insertメソッドを書き換えてinsert-updateシナリオを検討するのは難しいと思います。 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データベースを確認すると、等しい列がまだ削除され、新しいIDで挿入されています。なぜこれが起こるのか理解できず、その理由は私の紛争条項だと思いました。しかし documentation は反対を述べています。

INSERTまたはUPDATEのOR句で指定されたアルゴリズムは、CREATE TABLEで指定されたアルゴリズムをオーバーライドします。アルゴリズムはどこでも指定され、ABORTアルゴリズムが使用されます。

この試みのもう1つの欠点は、挿入ステートメントによって返されるIDの値が失われることです。これを補うために、last_insert_rowidを要求するオプションをようやく見つけました。説明したとおりです dtmilanoとswizの投稿 。ただし、その間に別の挿入が発生する可能性があるため、これが安全かどうかはわかりません。

25
JJD

SQLでこのすべてのロジックを実行することがパフォーマンスにとって最良であるという認識された概念を理解できますが、おそらくこの場合、最も単純な(最小のコード)ソリューションが最良のソリューションですか?最初に更新を試み、次に_CONFLICT_IGNORE_でinsertWithOnConflict()を使用して挿入を行い(必要な場合)、必要な行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を渡すことができるプロバイダーに。

19
Devunwired

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トリガーを使用して挿入を書き直す必要があります。残念ながら、これらはテーブルでは機能しません。それを使用するにはビューを作成する必要があります。

5
blazeroni

INSERT OR REPLACEON CONFLICT REPLACEと同じように機能します。一意の列を持つ行が既に存在し、挿入が行われる場合は、行が削除されます。更新されることはありません。

現在の解決策を守ることをお勧めします。ON CONFLICTクラスを使用してテーブルを作成しますが、行を挿入して制約違反が発生するたびに、Originの行と同じように、新しい行に新しい_idが含まれます。削除されました。

または、ON CONFLICTクラスを使用せずにテーブルを作成し、INSERT OR REPLACEを使用することもできます。そのためには insertWithOnConflict() メソッドを使用できますが、APIレベル8以降で使用できるため、より多くのコーディングとON CONFLICT clausuleを使用して、テーブルと同じソリューションを導きます。

それでもOriginの行を保持したい場合は、同じ_idを保持する必要があることを意味します。1つは行を挿入するためのクエリ、もう1つは挿入が失敗した場合に行を更新するためのクエリ(またはその逆)を行う必要があります。 )。一貫性を維持するには、トランザクションでクエリを実行する必要があります。

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

insertWithOnConflictを使用して、最後のパラメーター(conflictAlgorithm)をCONFLICT_REPLACEに設定します。

次のリンクで詳細をご覧ください。

insertWithOnConflictドキュメント

CONFLICT_REPLACEフラグ

0
Muzikant