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


102

Мені потрібно виконати UPSERT / INSERT АБО ОНОВЛЕННЯ проти бази даних SQLite.

Існує команда ВСТАВИТЬ АБО ЗАМОВИТИ, яка у багатьох випадках може бути корисною. Але якщо ви хочете зберегти свій ідентифікатор з автоматичним збільшенням через сторонні ключі, він не працює, оскільки він видаляє рядок, створює новий і, отже, цей новий рядок має новий ідентифікатор.

Це буде таблиця:

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

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |

Відповіді:


51

Це пізня відповідь. Починаючи з 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

Чому я не можу використовувати це на Android? Я спробував db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Подає мені синтаксичну помилку слова "на"
Bastian Voigt

1
@BastianVoigt Тому що бібліотеки SQLite3, встановлені на різних версіях Android, старші 3.24.0. Дивіться: developer.android.com/reference/android/database/sqlite/… На жаль, вам потрібна нова функція SQLite3 (або будь-яка інша системна бібліотека) на Android або iOS, вам потрібно зв’язати конкретну версію SQLite у вашому додаток замість того, щоб покладатися на встановлену систему.
prapin

Замість UPSERT, це не більше ПОСЛУГИ, оскільки він спробує вставити спочатку? ;)
Марк А.

@BastianVoigt, будь ласка, дивіться мою відповідь нижче (пов’язану з питанням вище), що стосується версій, які раніше, ніж 3.24.0.
Марк А.

105

Q&A Стиль

Ну, вивчивши проблему та боровшись із проблемою протягом декількох годин, я з’ясував, що це можна досягти двома способами, залежно від структури вашої таблиці та якщо у вас активовані обмеження на зовнішні ключі для підтримки цілісності. Я хотів би поділитися цим в чистому форматі, щоб заощадити час людям, які можуть бути в моїй ситуації.


Варіант 1: Ви можете дозволити видалення рядка

Іншими словами, у вас немає зовнішнього ключа, або якщо у вас є, ваш двигун SQLite налаштований так, щоб не було винятків з цілісності. Шлях пройти - ВСТАВИТИ АБО ЗАМОВИТИ . Якщо ви намагаєтеся вставити / оновити програвач, ідентифікатор якого вже існує, движок SQLite видалить цей рядок і вставить дані, які ви надаєте. Тепер виникає питання: що робити, щоб зберегти старий ідентифікатор?

Скажімо, ми хочемо UPSERT з даними user_name = 'steven' та віком = 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 . І цей CASE не може бути використаний (або, принаймні, я не керував ним) для виконання одного UPDATE- запиту, якщо існує (виберіть ідентифікатор від гравців, де user_name = 'steven'), і INSERT, якщо він не був. Не йдіть.

І тоді, нарешті, я застосував грубу силу, з успіхом. Логіка полягає в тому, що для кожного UPSERT, який ви хочете виконати, спочатку виконайте INSERT OR IGNORE, щоб переконатися, що є ряд з нашим користувачем, а потім виконати запит UPDATE з точно тими ж даними, які ви намагалися вставити.

Ті ж дані, що і раніше: user_name = 'steven' та вік = 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'; 

І це все!

EDIT

Як прокоментував Енді, спроба вставити спочатку, а потім оновити, може призвести до запуску тригерів частіше, ніж очікувалося. На мою думку, це питання безпеки даних не є, але це правда, що звільнення непотрібних подій мало сенсу. Тому вдосконаленим рішенням було б:

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

10
Дітто ... варіант 2 чудовий. Крім того, я зробив це навпаки: спробуйте оновити, перевірте, чи rowsAffected> 0, якщо ні, то зробіть вставку.
Том Спенсер

Це теж непоганий підхід. Єдиним невеликим недоліком є ​​те, що у вас немає лише одного SQL для "програми".
bgusach

2
вам не потрібно повторно встановлювати ім'я користувача в заяві оновлення в останньому зразку коду. Досить встановити вік.
Серг Стецюк

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 (або точніше, не нуль, якщо оновлено більше одного рядка), тож пункт «Де» в Insertтеперішній час оцінюється як хибний, і тому вставка не відбудеться.

Краса цього полягає в тому, що не потрібно ні грубої сили, ні зайвого видалення, а потім повторне вставлення даних, що може призвести до переплутування ключів вниз за течією у відносинах із зовнішніми ключами.

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


1
Це спрацювало для мене чудово. Я не бачив цього рішення більше ніде, окрім усіх прикладів ВСТАВИТЬ АБО ЗАМІНУ, він набагато гнучкіший для мого використання.
csab

@MarqueIV, а як бути, якщо два елементи потрібно оновити чи вставити? наприклад, ялинки були оновлені, а другого не існує. у такому випадку Changes() = 0повернеться помилково, і два ряди зроблять ВСТАВИТЬ АБО ЗАМОВИТЬ
Андрій Антонов

Зазвичай UPSERT повинен діяти на одній записи. Якщо ви кажете, що точно знаєте, що він діє на більш ніж одній записи, то відповідно змініть перевірку підрахунку.
Марк А. Донохое

Погано в тому, що якщо рядок існує, метод оновлення повинен бути виконаний незалежно від того, змінився чи ні.
Джімі

1
Чому це погано? І якщо дані не змінилися, чому ви дзвоните UPSERTв першу чергу? Але навіть незважаючи на це, добре , що оновлення відбувається, встановивши, Changes=1інакше INSERTоператор буде неправильно запускати, чого ви цього не хочете.
Марк А.

25

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

INSERT OR IGNORE ...
UPDATE ...

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

Правильне рішення є

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

у цьому випадку виконується лише одне твердження (коли рядок існує чи ні).


1
Я бачу вашу думку. Я оновлю своє запитання. До речі, мені це не UPDATE OR IGNOREпотрібно, оскільки оновлення не завершиться, якщо не знайдеться рядків.
bgusach

1
читабельність? Я можу побачити, що робить Енді код, з першого погляду. З повагою, мені довелося вивчити хвилину, щоб розібратися.
Брандан

6

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

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

SELECT зміни () повернуть кількість оновлень, зроблених в останньому запиті. Потім перевірте, чи повертається значення змін () 0, якщо так виконувати:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 

Це еквівалентно тому, що @fiznool запропонував у своєму коментарі (хоча я б пішов на його рішення). Це все добре і насправді працює нормально, але у вас немає унікального оператора SQL. UPSERT, що не базується на ПК або інших унікальних клавішах, мало для мене сенсу.
bgusach

4

Ви також можете просто додати пункт ON CONFLICT REPLACE до унікального обмеження свого користувача, а потім просто ВСТУПИТИ, залишаючи його SQLite, щоб зрозуміти, що робити у випадку конфлікту. Дивіться: https://sqlite.org/lang_conflict.html .

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


1

Варіант 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';

Щодо тригерів

Зверніть увагу: я не перевіряв його, щоб побачити, які тригери викликаються, але я припускаю наступне:

якщо рядок не існує

  • ПЕРЕД ВСТУП
  • ВСТАВИТИ, використовуючи 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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.