У меня есть следующая таблица счетчиков:
CREATE TABLE cache (
key text PRIMARY KEY,
generation int
);
Я хотел бы увеличить один из счетчиков или установить его на ноль, если соответствующая строка еще не существует. Есть ли способ сделать это без проблем concurrency в стандартном SQL? Операция иногда является частью транзакции, иногда отдельной.
SQL должен запускаться без изменений в SQLite, PostgreSQL и MySQL, если это возможно.
Поиск дал несколько идей, которые либо страдают от проблем concurrency, либо специфичны для базы данных:
Попробуйте INSERT
новую строку и UPDATE
, если произошла ошибка. К сожалению, ошибка в INSERT
прерывает текущую транзакцию.
UPDATE
строка, и если строки не были изменены, INSERT
новая строка.
MySQL имеет предложение ON DUPLICATE KEY UPDATE
.
EDIT: Спасибо за все замечательные ответы. Похоже, что Пол прав, и нет ни одного, портативного способа сделать это. Это удивительно для меня, поскольку это звучит как очень простая операция.
MySQL (и впоследствии SQLite) также поддерживает синтаксис REPLACE INTO:
REPLACE INTO my_table (pk_id, col1) VALUES (5, '123');
Это автоматически идентифицирует первичный ключ и находит соответствующую строку для обновления, вставляя новую, если ни один не найден.
SQLite поддерживает замену строки, если она уже существует:
INSERT OR REPLACE INTO [...blah...]
Вы можете сократить это до
REPLACE INTO [...blah...]
Этот ярлык был добавлен для совместимости с выражением MySQL REPLACE INTO
.
PRAMARY KEY
в ваших значениях.
Я бы сделал что-то вроде следующего:
INSERT INTO cache VALUES (key, generation)
ON DUPLICATE KEY UPDATE (key = key, generation = generation + 1);
Установка значения генерации в 0 в коде или в sql, но с использованием ON DUP... для увеличения значения. Я думаю, что синтаксис в любом случае.
Я не знаю, что вы найдете нейтральное для платформы решение.
Это обычно называется "UPSERT".
См. некоторые связанные обсуждения:
предложение ON DUPLICATE KEY UPDATE является лучшим решением, потому что: REPLACE делает DELETE, за которым следует INSERT, поэтому в течение столь незначительного периода запись удаляется, создавая настолько небольшую вероятность, что запрос может вернуться, если пропустить это, если страница просматривалась во время запроса REPLACE.
Я предпочитаю INSERT... ON DUPLICATE UPDATE... по этой причине.
Решение jmoz - лучшее: хотя я предпочитаю синтаксис SET в круглые скобки
INSERT INTO cache
SET key = 'key', generation = 'generation'
ON DUPLICATE KEY
UPDATE key = 'key', generation = (generation + 1)
;
В PostgreSQL нет команды слияния, и на самом деле писать ее нетривиально - на самом деле есть странные крайние случаи, которые делают задачу "интересной".
Лучшим (как в: работе в наиболее возможных условиях) подходом является использование функции - например, показанной в manual (merge_db).
Если вы не хотите использовать функцию, вы обычно можете уйти с:
updated = db.execute(UPDATE ... RETURNING 1)
if (!updated)
db.execute(INSERT...)
Просто помните, что это не ошибка, и она будет терпеть неудачу.
Стандартный SQL предоставляет оператор MERGE для этой задачи. Не все СУБД поддерживают оператор MERGE.
Если вы в порядке с использованием библиотеки, которая пишет SQL для вас, вы можете использовать Upsert (в настоящее время Ruby и Python только):
Pet.upsert({:name => 'Jerry'}, :breed => 'beagle')
Pet.upsert({:name => 'Jerry'}, :color => 'brown')
Это работает через MySQL, Postgres и SQLite3.
Он записывает хранимую процедуру или пользовательскую функцию (UDF) в MySQL и Postgres. Он использует INSERT OR REPLACE
в SQLite3.
Не могли бы вы использовать триггер insert? Если это не удается, выполните обновление.
Если у вас нет общего способа атомарного обновления или вставки (например, через транзакцию), вы можете вернуться к другой схеме блокировки. 0-байтовый файл, системный мьютекс, именованный канал и т.д.