Мне нужно выполнить UPSERT/INSERT или UPDATE для базы данных SQLite.
Существует команда INSERT OR REPLACE, которая во многих случаях может быть полезна. Но если вы хотите сохранить свой идентификатор с автоинкрементами на месте из-за внешних ключей, он не работает, так как он удаляет строку, создает новую, и, следовательно, эта новая строка имеет новый идентификатор.
Это будет таблица:
игроки - (первичный ключ по идентификатору, уникальное имя пользователя)
| id | user_name | age |
------------------------------
| 1982 | johnny | 23 |
| 1983 | steven | 29 |
| 1984 | pepee | 40 |
Я сделаю вам лучше, не требуя "игнорирования" грубой силы, которая будет работать только в случае нарушения ключа. Этот способ работает на основе любых условий, указанных в обновлении.
Попробуйте это...
-- 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
теперь будет ложным и, следовательно, никакая вставка не будет выполнена.
Не требуется грубой силы.
Хорошо, после нескольких часов исследования и борьбы с этой проблемой, я узнал, что есть два способа сделать это, в зависимости от структуры вашей таблицы, и если у вас есть ограничения на внешние ключи, чтобы поддерживать целостность. Я хотел бы поделиться этим в чистом формате, чтобы сэкономить время людям, которые могут быть в моей ситуации.
Другими словами, у вас нет внешнего ключа, или если у вас есть, ваш 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", если он есть, и в противном случае он возвращает новый свежий идентификатор.
После обезвреживания с предыдущим решением я понял, что в моем случае это может привести к уничтожению данных, так как этот идентификатор работает как внешний ключ для другой таблицы. Кроме того, я создал таблицу с предложением 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);
Проблема со всеми представленными ответами заключается в отсутствии необходимости принимать триггеры (и, возможно, другие побочные эффекты). Решение, подобное
INSERT OR IGNORE ...
UPDATE ...
приводит к выполнению обоих триггеров (для вставки, а затем для обновления), когда строка не существует.
Правильное решение
UPDATE OR IGNORE ...
INSERT OR IGNORE ...
в этом случае выполняется только один оператор (когда строка существует или нет).
UPDATE OR IGNORE
необходимо, так как обновление не приведет к сбою, если строки не найдены.
Чтобы иметь чистый 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);
Если вам нравится избегать как 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';
Примечание. Я не тестировал его, чтобы увидеть, какие триггеры вызывается, но я предполагаю следующее:
таким образом вы можете иметь одну команду 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.