SQLite UPSERT / ОБНОВЛЕНИЕ ИЛИ ВСТАВКА

104

Мне нужно выполнить UPSERT / INSERT OR UPDATE для базы данных SQLite.

Есть команда INSERT OR REPLACE, которая во многих случаях может быть полезной. Но если вы хотите сохранить свой идентификатор с автоинкрементом из-за внешних ключей, он не работает, поскольку он удаляет строку, создает новую и, следовательно, эта новая строка имеет новый идентификатор.

Это будет таблица:

игроки - (первичный ключ по id, имя_пользователя уникальное)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |
Bgusach
источник

Ответы:

53

Это поздний ответ. Начиная с версии SQLIte 3.24.0, выпущенной 4 июня 2018 г., наконец, появилась поддержка предложения UPSERT , следующего за синтаксисом PostgreSQL.

INSERT INTO players (user_name, age)
  VALUES('steven', 32) 
  ON CONFLICT(user_name) 
  DO UPDATE SET age=excluded.age;

Примечание. Для тех, кто использует версию SQLite ранее, чем 3.24.0, см. Этот ответ ниже (опубликованный мной, @MarqueIV).

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

прапин
источник
На данный момент этого релиза в репозитории Ubuntu нет.
bl79 09
Почему я не могу использовать это на Android? Я пробовал db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Выдает мне синтаксическую ошибку в слове «on»
Bastian Voigt
1
@BastianVoigt Поскольку библиотеки SQLite3, установленные в различных версиях Android, старше 3.24.0. См .: developer.android.com/reference/android/database/sqlite/… К сожалению, если вам нужна новая функция SQLite3 (или любой другой системной библиотеки) на Android или iOS, вам необходимо связать определенную версию SQLite в своем приложение вместо того, чтобы полагаться на установленное системой.
prapin
Разве это не UPSERT, а скорее INDATE, поскольку он сначала пытается вставить вставку? ;)
Марк А. Донохо
@BastianVoigt, пожалуйста, посмотрите мой ответ ниже (ссылка на вопрос выше), который относится к версиям до 3.24.0.
Марк А. Донохо
107

Стиль вопросов и ответов

Что ж, после нескольких часов изучения проблемы и борьбы с ней я обнаружил, что есть два способа решить эту проблему, в зависимости от структуры вашей таблицы и от того, активированы ли у вас ограничения внешних ключей для поддержания целостности. Я хотел бы поделиться этим в чистом формате, чтобы сэкономить время людям, которые могут оказаться в моей ситуации.


Вариант 1. Вы можете позволить себе удалить строку

Другими словами, у вас нет внешнего ключа, или, если он у вас есть, ваш механизм SQLite настроен так, что нет исключений целостности. Путь - ВСТАВИТЬ ИЛИ ЗАМЕНИТЬ . Если вы пытаетесь вставить / обновить игрока, чей идентификатор уже существует, механизм SQLite удалит эту строку и вставит предоставленные вами данные. Теперь возникает вопрос: что делать, чтобы старый идентификатор оставался связанным?

Допустим, мы хотим выполнить UPSERT с данными user_name = 'steven' и age = 32.

Взгляните на этот код:

INSERT INTO players (id, name, age)

VALUES (
    coalesce((select id from players where user_name='steven'),
             (select max(id) from drawings) + 1),
    32)

Хитрость заключается в слиянии. Он возвращает идентификатор пользователя «steven», если таковой имеется, а в противном случае возвращает новый свежий идентификатор.


Вариант 2. Вы не можете позволить себе удалить строку

Попробовав предыдущее решение, я понял, что в моем случае это может привести к уничтожению данных, поскольку этот идентификатор работает как внешний ключ для другой таблицы. Кроме того, я создал таблицу с предложением ON DELETE CASCADE , что означало бы, что он будет удалять данные без уведомления. Опасно.

Итак, я сначала подумал о предложении IF, но в SQLite есть только CASE . И этот СЛУЧАЙ нельзя использовать (или, по крайней мере, я не справился с этим) для выполнения одного запроса UPDATE, если СУЩЕСТВУЕТ (выберите идентификатор из игроков, где user_name = 'steven'), и INSERT, если это не так. Нет.

И вот, наконец, я успешно применил грубую силу. Логика заключается в том, что для каждого UPSERT, который вы хотите выполнить, сначала выполните INSERT OR IGNORE, чтобы убедиться, что есть строка с нашим пользователем, а затем выполните запрос UPDATE с точно такими же данными, которые вы пытались вставить.

Те же данные, что и раньше: user_name = 'steven' и age = 32.

-- make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

-- make sure it has the right data
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven'; 

И это все!

РЕДАКТИРОВАТЬ

Как прокомментировал Энди, попытка сначала вставить, а затем обновить может привести к срабатыванию триггеров чаще, чем ожидалось. На мой взгляд, это не проблема безопасности данных, но действительно, запуск ненужных событий не имеет смысла. Следовательно, улучшенное решение будет:

-- Try to update any existing row
UPDATE players SET age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 
Bgusach
источник
10
То же самое ... вариант 2 великолепен. За исключением того, что я сделал наоборот: попробуйте обновить, проверьте, если rowsAffected> 0, если нет, то сделайте вставку.
Tom Spencer
Это тоже неплохой подход, единственный небольшой недостаток в том, что у вас нет только одного SQL для «upsert».
bgusach 05
2
вам не нужно повторно устанавливать user_name в операторе обновления в последнем примере кода. Достаточно указать возраст.
Серг Стецук
72

Вот подход, который не требует «игнорирования» грубой силы, который будет работать только в случае нарушения ключа. Этот способ работает при любых условиях, указанных вами в обновлении.

Попробуй это...

-- Try to update any existing row
UPDATE players
SET age=32
WHERE user_name='steven';

-- If no update happened (i.e. the row didn't exist) then insert one
INSERT INTO players (user_name, age)
SELECT 'steven', 32
WHERE (Select Changes() = 0);

Как это устроено

«Волшебный соус» здесь используется Changes()в Whereпредложении. Changes()представляет количество строк, затронутых последней операцией, которая в данном случае является обновлением.

В приведенном выше примере, если после обновления нет никаких изменений (т. Е. Запись не существует), то Changes()= 0, поэтому Whereпредложение в Insertоператоре оценивается как истинное, и новая строка вставляется с указанными данными.

Если Update действительноChanges() обновил существующую строку, то = 1 (или, точнее, не ноль, если было обновлено более одной строки), поэтому предложение Where в Insertnow оценивается как false, и, таким образом, вставка не выполняется .

Прелесть этого заключается в том, что нет необходимости в грубой силе или излишнем удалении, а затем повторной вставке данных, что может привести к нарушению вышестоящих ключей в отношениях внешнего ключа.

Кроме того, поскольку это просто стандартное Whereпредложение, оно может быть основано на чем угодно, а не только на ключевых нарушениях. Точно так же вы можете использовать Changes()в сочетании с любыми другими выражениями, которые вам нужны / разрешены.

Марк А. Донохью
источник
1
Это отлично сработало для меня. Я не встречал этого решения где-либо еще вместе со всеми примерами INSERT OR REPLACE, оно намного более гибкое для моего варианта использования.
csab
@MarqueIV, а что делать, если нужно обновить или вставить два элемента? например, первая обновилась, а вторая не существует. в таком случае Changes() = 0вернет false, а две строки сделают INSERT OR REPLACE
Андрей Антонов
Обычно предполагается, что UPSERT действует с одной записью. Если вы говорите, что точно знаете, что он действует более чем на одну запись, измените проверку счетчика соответствующим образом.
Марк А. Донохо
Плохо то, что если строка существует, метод обновления должен выполняться независимо от того, изменилась строка или нет.
Jimi
1
Почему это плохо? И если данные не изменились, зачем вы вообще звоните UPSERT? Но даже в этом случае это хорошо , что обновление происходит, настройка, Changes=1иначе INSERTоператор будет неправильно запускаться, чего вы не хотите.
Марк А. Донохо
25

Проблема со всеми представленными ответами заключается в полном отсутствии учета триггеров (и, возможно, других побочных эффектов). Решение вроде

INSERT OR IGNORE ...
UPDATE ...

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

Правильное решение

UPDATE OR IGNORE ...
INSERT OR IGNORE ...

в этом случае выполняется только один оператор (если строка существует или нет).

Энди
источник
1
Я понимаю вашу точку зрения. Я обновлю свой вопрос. Кстати, я не знаю, зачем UPDATE OR IGNOREэто нужно, поскольку обновление не выйдет из строя, если строки не найдены.
bgusach
1
читаемость? Я могу сразу увидеть, что делает код Энди. Твой бгусач Пришлось минутку изучить, чтобы разобраться.
Brandan
6

Чтобы иметь чистый UPSERT без дыр (для программистов), не использующих уникальные и другие ключи:

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();

SELECT changes () вернет количество обновлений, выполненных в последнем запросе. Затем проверьте, равно ли возвращаемое значение от changes () значение 0, если да, выполните:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 
Gilco
источник
Это эквивалентно тому, что @fiznool предложил в своем комментарии (хотя я бы выбрал его решение). Все в порядке и на самом деле работает нормально, но у вас нет уникального оператора SQL. UPSERT, не основанный на PK или других уникальных ключах, для меня практически не имеет смысла.
bgusach 01
4

Вы также можете просто добавить предложение ON CONFLICT REPLACE к уникальному ограничению user_name, а затем просто INSERT прочь, предоставив SQLite выяснить, что делать в случае конфликта. См. Https://sqlite.org/lang_conflict.html .

Также обратите внимание на предложение о триггерах удаления: когда стратегия разрешения конфликтов REPLACE удаляет строки, чтобы удовлетворить ограничение, триггеры удаления срабатывают тогда и только тогда, когда рекурсивные триггеры включены.

Максимилиан Тиртания
источник
2

Вариант 1: Вставить -> Обновить

Если вам нравится избегать и того, changes()=0и другого, и INSERT OR IGNOREдаже если вы не можете позволить себе удаление строки - вы можете использовать эту логику;

Сначала вставьте (если не существует), а затем обновите , отфильтровав уникальный ключ.

пример

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Insert if NOT exists
INSERT INTO players (user_name, age)
SELECT 'johnny', 20
WHERE NOT EXISTS (SELECT 1 FROM players WHERE user_name='johnny' AND age=20);

-- Update (will affect row, only if found)
-- no point to update user_name to 'johnny' since it's unique, and we filter by it as well
UPDATE players 
SET age=20 
WHERE user_name='johnny';

Что касается триггеров

Примечание: я не тестировал, какие триггеры вызываются, но предполагаю следующее:

если строка не существует

  • ПЕРЕД ВСТАВКОЙ
  • INSERT, используя INSTEAD OF
  • ПОСЛЕ ВСТАВКИ
  • ПЕРЕД ОБНОВЛЕНИЕМ
  • ОБНОВЛЕНИЕ с использованием INSTEAD OF
  • ПОСЛЕ ОБНОВЛЕНИЯ

если строка существует

  • ПЕРЕД ОБНОВЛЕНИЕМ
  • ОБНОВЛЕНИЕ с использованием INSTEAD OF
  • ПОСЛЕ ОБНОВЛЕНИЯ

Вариант 2. Вставьте или замените - оставьте свой идентификатор

таким образом у вас может быть одна команда SQL

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Single command to insert or update
INSERT OR REPLACE INTO players 
(id, user_name, age) 
VALUES ((SELECT id from players WHERE user_name='johnny' AND age=20),
        'johnny',
        20);

Изменить: добавлен вариант 2.

Itsho
источник