Android/ SQLite: Insert-обновление столбцов таблицы для сохранения идентификатора

В настоящее время я использую следующую инструкцию для создания таблицы в базе данных SQLite на устройстве Android.

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

предложение conflict в конце приводит к тому, что строки за когда новые вставки сделаны которые приходят с такими же координатами. The документация SQLite содержит дополнительную информацию о предложении conflict -.

вместо этого, я хотел бы keep бывшие строки и просто обновление их колонны. Каков наиболее эффективный способ сделать это в среде 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 здесь. Кроме того, это не мое любимое решение в любом случае.


Edit:

@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, однако, равными столбцами являются все еще удаляется и вставляется с новыми идентификаторами. Я не понимаю, почему это происходит, и думал, что причина в моей коллизионной статье. Но ... --54-->документация обратное.

алгоритм, указанный в предложении OR вставки или обновления переопределяет любой алгоритм, указанный в таблице CREATE. Если нет алгоритма указывается в любом месте, используется алгоритм ABORT.

другим недостатком этой попытки является то, что вы теряете значение id, которое возвращается инструкцией insert. Чтобы компенсировать это, я, наконец, нашел вариант попросить last_insert_rowid. Это, как пояснил в сообщениях dtmilano и swiz. Однако я не уверен, что это безопасно, так как между ними может произойти другая вставка.

5 ответов


я могу понять воспринимаемое представление о том, что для производительности лучше всего делать всю эту логику в SQL, но, возможно, самое простое (наименьший код) решение является лучшим в этом случае? Почему бы сначала не попробовать обновление, а затем использовать insertWithOnConflict() С CONFLICT_IGNORE чтобы сделать вставку (при необходимости) и получить идентификатор строки, вам нужно:

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() и только вставьте, если затронутый счетчик был равен нулю, но тогда будет если вы не смогли получить идентификатор существующей строки без дополнительного выбора. Эта форма вставки всегда будет возвращать вам правильный идентификатор для передачи обратно в 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 передается с одного звонка к провайдеру.


одним из решений является создание представления для locations таблица с A вместо триггера в представлении, затем вставьте в представление. Вот как это будет выглядеть:

вид:

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 или IGNORE, но кажется, что до и после триггеров только триггер, если он будет фактически вставлять в таблица.

вы можете найти ответ полезно, что является основой для триггера.

Edit: из-за того, что триггеры BEFORE и AFTER не срабатывают, когда вставка игнорируется (которая могла бы быть обновлена вместо этого), нам нужно переписать вставку с помощью триггера вместо триггера. К сожалению, они не работают с таблицами - мы должны создать представление для его использования.


INSERT OR REPLACE работает так же, как ON CONFLICT REPLACE. Он удалит строку, если строка с уникальным столбцом уже существует и чем она делает insert. Он никогда не обновляется.

Я бы порекомендовал вам придерживаться вашего текущего решения, вы создаете таблицу с ON CONFLICT clausule, но каждый раз, когда вы вставляете строку и происходит нарушение ограничений, Ваша новая строка будет иметь новый _id как исходная строка будет удалена.

или вы можете создать таблицу без ON CONFLICT clausule и использовать INSERT OR REPLACE, вы можете использовать insertWithOnConflict () метод для этого, но он доступен с уровня API 8, требует большего кодирования и приводит к тому же решению, что и таблица с ON CONFLICT clausule.

если вы все еще хотите сохранить исходную строку, это означает, что вы хотите сохранить то же самое _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();
    }

использовать insertWithOnConflict и установите последний параметр (conflictAlgorithm) в CONFLICT_REPLACE.

Подробнее читайте по следующим ссылкам:

insertWithOnConflict документация

флаг CONFLICT_REPLACE


использовать ВСТАВИТЬ ИЛИ ЗАМЕНИТЬ.

Это правильный способ сделать это.