SQLite UPSERT / UPDATE OR INSERT

72

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

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

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

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

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |
Теги:
database
upsert
sqlite3

5 ответов

42
Лучший ответ

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

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

-- Try to update any existing row
UPDATE players
SET user_name='steven', 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);

Как это работает

"Магия" здесь вы используете предложение Where (Select Changes() = 0), чтобы определить, есть ли какие-либо строки для вставки, и поскольку это основано на вашем собственном предложении Where, оно может быть для всего, что вы определяете, а не просто ключевые нарушения.

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

Если Update обновил существующую строку, тогда Changes()= 1, поэтому предложение "Where" в Insert теперь будет ложным и, следовательно, никакая вставка не будет выполнена.

Не требуется грубой силы.

  • 1
    Человеку, который проголосовал за это, не могли бы вы объяснить, почему?
  • 0
    Это отлично сработало для меня. Я не видел этого решения нигде, кроме всех примеров INSERT OR REPLACE, он гораздо более гибкий для моего случая использования.
Показать ещё 5 комментариев
91

Q & A Стиль

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


Вариант 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, что означало бы, что она будет удалять данные молча. Dangerous.

Итак, я сначала подумал о предложении IF, но SQLite имеет только CASE. И этот CASE нельзя использовать (или, по крайней мере, я его не использовал) для выполнения одного UPDATE-запроса, если EXISTS (выберите id из игроков, где user_name = 'steven), и INSERT, если это не так. Нет.

И затем, наконец, я с успехом использовал грубую силу. Логика заключается в том, что для каждого UPSERT, который вы хотите выполнить, сначала выполните INSERT ИЛИ 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 user_name='steven', age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 
  • 9
    То же самое ... вариант 2 отличный. За исключением того, что я сделал это наоборот: попробуйте обновить, проверьте, если rowActed> 0, если нет, то сделайте вставку.
  • 0
    Это тоже довольно хороший подход, единственный маленький недостаток в том, что у вас нет только одного SQL для «upsert».
Показать ещё 1 комментарий
22

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

INSERT OR IGNORE ...
UPDATE ...

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

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

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

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

  • 1
    Я понимаю вашу точку зрения. Я обновлю свой вопрос. Кстати, я не знаю, почему UPDATE OR IGNORE необходимо, так как обновление не приведет к сбою, если строки не найдены.
  • 1
    читаемость? Я сразу вижу, что делает код Энди. Ваш bgusach Я должен был изучить минуту, чтобы выяснить.
4

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

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

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

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

Вариант 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 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.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню