Подтвердить что ты не робот

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

В настоящее время я использую следующий оператор для создания таблицы в базе данных 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 );

Конфликт-предложение в конце приводит к тому, что строки отбрасываются, когда новые вставки выполняются с одинаковыми координатами. Документация 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);
}

Когда данные поступают из бэкэнд, все, что я делаю, вставляет данные следующим образом.

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

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

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

4b9b3361

Ответ 1

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

Ответ 2

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

Вид:

CREATE VIEW locations_view AS SELECT * FROM locations;

Trigger:

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. К сожалению, они не работают с таблицами - нам нужно создать представление, чтобы использовать его.

Ответ 3

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

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

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

Если вы все еще хотите сохранить свою начальную строку, это означает, что вы хотите сохранить один и тот же _id, вам нужно будет сделать два запроса, сначала один для вставки строки, второй для обновления строки, если вставка не выполнена (или наоборот) Versa). Чтобы сохранить согласованность, вы должны выполнять запросы в транзакции.

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

Ответ 5

Используйте ВСТАВИТЬ ИЛИ ЗАМЕНИТЬ.

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