web-dev-qa-db-fra.com

Android/SQLite: colonnes de table Insert-Update pour conserver l'identifiant

Actuellement, j'utilise l'instruction suivante pour créer une table dans une base de données SQLite sur un périphérique Android.

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

La clause de conflit à la fin fait que les lignes sont dropées lorsque de nouvelles insertions sont effectuées avec les mêmes coordonnées. La Documentation SQLite contient des informations supplémentaires sur la clause de conflit.

Au lieu de cela, j'aimerais conserver les anciennes lignes et simplement mettre à jour leurs colonnes. Quel est le moyen le plus efficace de le faire dans un environnement Android/SQLite?

  • En tant que clause de conflit dans l'instruction CREATE TABLE.
  • En tant que déclencheur INSERT.
  • En tant que clause conditionnelle dans la méthode ContentProvider#insert.
  • ... mieux vous pouvez penser

Je pense qu'il est plus performant de gérer de tels conflits au sein de la base de données. De plus, j'ai du mal à réécrire la méthode ContentProvider#insert pour prendre en compte le scénario insert-update. Voici le code de la méthode 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);
}

Lorsque les données arrivent du serveur, tout ce que je fais est de les insérer comme suit.

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

J'ai du mal à comprendre comment appliquer un autre appel à ContentProvider#update ici. De plus, ce n’est de toute façon pas ma solution préférée.


Modifier:

@CommonsWare: j'ai essayé de mettre en œuvre votre suggestion d'utiliser INSERT OR REPLACE. Je suis venu avec ce morceau de code laid.

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

Lorsque je vérifie la base de données SQLite, cependant, des colonnes identiques sont toujours supprimées et insérées avec les nouveaux identifiants. Je ne comprends pas pourquoi cela se produit et je pense que la raison en est ma clause de conflit. Mais le documentation indique le contraire.

L'algorithme spécifié dans la clause OR d'un INSERT ou UPDATE remplace tout algorithme spécifié dans CREATE TABLE. Si aucun algorithme est spécifié n'importe où, l'algorithme ABORT est utilisé.

Un autre inconvénient de cette tentative est que vous perdez la valeur de l'id qui est renvoyée par une instruction insert. Pour compenser cela, j'ai finalement trouvé une option pour demander le last_insert_rowid. C'est comme expliqué dans les messages de dtmilano et swiz . Je ne suis toutefois pas sûr que ce soit sans danger puisqu'un autre insert peut se produire entre les deux.

25
JJD

Je peux comprendre la notion perçue selon laquelle il est préférable que les performances fassent toute cette logique en SQL, mais peut-être que la solution la plus simple (le moins codé) est la meilleure dans ce cas? Pourquoi ne pas essayer d'abord la mise à jour, puis utiliser insertWithOnConflict() avec CONFLICT_IGNORE pour effectuer l'insertion (si nécessaire) et obtenir l'id de la ligne dont vous avez besoin:

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

Une solution plus simple consisterait à vérifier la valeur de retour de update() et à ne faire l'insertion que si le nombre affecté était égal à zéro, mais il pourrait arriver que vous ne puissiez pas obtenir l'ID de la ligne existante sans sélection supplémentaire. Cette forme d’insert vous retournera toujours l’id correct à renvoyer dans la variable Uri et ne modifiera pas la base de données plus que nécessaire.

Si vous souhaitez effectuer un grand nombre de ces opérations simultanément, vous pouvez consulter la méthode bulkInsert() sur votre fournisseur, dans laquelle vous pouvez exécuter plusieurs insertions dans une même transaction. Dans ce cas, puisqu'il n'est pas nécessaire de renvoyer la id de l'enregistrement mis à jour, la solution "plus simple" devrait fonctionner correctement:

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

En réalité, le code de transaction est ce qui accélère les choses en réduisant le nombre d'écritures de la base de données dans le fichier. bulkInsert() autorise simplement la transmission de plusieurs ContentValues avec un seul appel au fournisseur.

19
Devunwired

Une solution consiste à créer une vue pour la table locations avec un déclencheur INSTEAD OF dans la vue, puis à l'insérer dans la vue. Voici à quoi cela ressemblerait:

Vue:

CREATE VIEW locations_view AS SELECT * FROM locations;

Déclencheur:

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;

Au lieu d'insérer dans la table locations, vous insérez dans la vue locations_view. Le déclencheur se chargera de fournir la valeur _id correcte en utilisant la sous-sélection. Si, pour une raison quelconque, l'insertion contient déjà un _id, la COALESCE le conservera et remplacera un existant dans la table.

Vous voudrez probablement vérifier dans quelle mesure la sous-sélection affecte les performances et la comparer aux autres modifications que vous pourriez apporter, mais cela vous permet de garder cette logique hors de votre code.

J'ai essayé d'autres solutions impliquant des déclencheurs sur la table elle-même en fonction de INSERT OR IGNORE, mais il semble que les déclencheurs BEFORE et AFTER ne se déclenchent que s'ils sont réellement insérés dans la table.

Vous pourriez trouver cette réponse utile, qui est la base du déclencheur.

Edit: En raison des déclencheurs BEFORE et AFTER qui ne se déclenchent pas lorsqu'une insertion est ignorée (qui aurait alors pu être mise à jour à la place), nous devons réécrire l'insertion avec un déclencheur INSTEAD OF. Malheureusement, ceux-ci ne fonctionnent pas avec les tables - nous devons créer une vue pour l'utiliser.

5
blazeroni

INSERT OR REPLACE fonctionne exactement comme ON CONFLICT REPLACE. Il supprimera la ligne si la ligne avec la colonne unique existe déjà et qu’il s’agit d’une insertion. Il ne met jamais à jour.

Je vous recommande de vous en tenir à votre solution actuelle, vous créez une table avec ON CONFLICT clausule, mais chaque fois que vous insérez une ligne et que la violation de contrainte se produit, votre nouvelle ligne aura le nouveau _id car la ligne d'origine sera supprimée.

Ou vous pouvez créer une table sans ON CONFLICT clausule et utiliser INSERT OR REPLACE, vous pouvez utiliser la méthode insertWithOnConflict () , mais elle est disponible car l’API de niveau 8 nécessite davantage de codage et conduit à la même solution que table avec ON CONFLICT clausule.

Si vous souhaitez toujours conserver votre ligne d'origine, cela signifie que vous souhaitez conserver le même _id. Vous devrez alors effectuer deux requêtes, la première pour l'insertion d'une ligne, la seconde pour mettre à jour une ligne si l'insertion échoue (ou vice versa). Pour préserver la cohérence, vous devez exécuter des requêtes dans une 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();
    }
3
biegleux

Utilisez insertWithOnConflict et définissez le dernier paramètre (conflictAlgorithm) sur CONFLICT_REPLACE.

En savoir plus sur les liens suivants:

Documentation insertWithOnConflict

Drapeau CONFLICT_REPLACE

0
Muzikant