2016-03-18 6 views
8

Для таблицы, как этот:Получить Id от условного INSERT

CREATE TABLE Users(
    id SERIAL PRIMARY KEY, 
    name TEXT UNIQUE 
); 

Что бы правильно вставить один-запроса для следующей операции:

Учитывая пользователь name, вставить новую запись и вернуть новый номер id. Но если name уже существует, просто верните id.

Я знаю новый синтаксис в PostgreSQL 9.5 для ON CONFLICT(column) DO UPDATE/NOTHING, но я не могу понять, как он может помочь, если мне нужно вернуть id.

Кажется, что RETURNING id и ON CONFLICT не относятся друг к другу.

+0

Эта логика действительно должна быть в базе данных? – Krismorte

+0

@ErwinBrandstetter это кажется более сложным, чем я думал. Я пытался выяснить, стоит ли искать решение с одним запросом в приложении Node.js или просто придерживаться 'SELECT id FROM users WHERE name = 'text'', проверить, найден ли, а если нет, INSERT ... ', который представляет собой 2 запроса, но теперь это выглядит проще, чем вариант с одним запросом. Я не ожидал, что это будет неудобно. Я надеялся, что новый «ПО КОНФЛИКТУ» поможет, но, увы, это не так. Спасибо за ссылки! –

+0

Вопрос очень сложный, как только вступает в игру одновременный доступ к записи. Я добавлю еще один ответ. –

ответ

7

Реализация UPSERT является чрезвычайно сложным, чтобы быть в безопасности от одновременного доступа к записи. Взгляните на this Postgres Wiki, который служил журналом во время первоначальной разработки. Хакеры Postgres решили не включать «исключенные» строки в предложение RETURNING для первого выпуска в Postgres 9.5. Они могут создать что-то для следующего выпуска.

Это важно утверждение в руководстве, чтобы объяснить ситуацию:

Синтаксис RETURNING списка идентичен тому, что выходной список SELECT.Будут возвращены только строки, которые были успешно вставлены или обновлены . Например, если строка была заблокирована, но не обновлена ​​ , поскольку условие ON CONFLICT DO UPDATE ... WHERE условие не было удовлетворено , строка не будет возвращена.

Смелый акцент мой.

Для один ряд для вставки:

WITH ins AS (
    INSERT INTO users(name) 
    VALUES ('new_usr_name')   -- input value 
    ON  CONFLICT(name) DO UPDATE 
    SET name = name WHERE FALSE -- never executed, just to lock row 
    RETURNING users.id 
    ) 
SELECT id FROM ins 
UNION ALL 
SELECT id FROM users   -- 2nd SELECT never executed if INSERT successful 
WHERE name = 'new_usr_name' -- input value a 2nd time 
LIMIT 1; 

Или обернуть в функцию, чтобы только обеспечить новое имя раз. Как продемонстрировано здесь (также рассмотреть объяснение LIMIT 1):

Возможная гонка: параллельная транзакция может изменить/удалить существующую строку между INSERT попытки и SELECT. Очень маловероятно, но возможно.

Если у вас нет (возможно) одновременный доступ на запись (или просто не волнует), упрощать:

... 
ON  CONFLICT(name) DO NOTHING 
... 

Чтобы вставить набор строк:

+0

Вы видите какую-либо проблему в более раннем ответе, данном Clodoaldo Neto? Что мне нравится в этом, он не требует работы PostgreSQL 9.5. –

+0

@ vitaly-t: Это прочное заявление, но не охватывает упомянутое состояние гонки. Это также, вероятно, дороже. Вы можете добавить 'LIMIT 1' из моего заявления для производительности. –

+0

Спасибо за ваш ответ. Мое предыдущее исследование было сформулировано здесь: https://github.com/vitaly-t/pg-promise/blob/master/examples/select-insert.md, и я надеялся, что смогу улучшить его с помощью альтернативы с одним запросом. Но теперь, учитывая всю его сложность, я не уверен, можно ли считать это улучшением. Но, по крайней мере, хорошо знать, что это возможно. –

2

Для одной вставки строк и без обновления:

with i as (
    insert into users (name) 
    select 'the name' 
    where not exists (
     select 1 
     from users 
     where name = 'the name' 
    ) 
    returning id 
) 
select id 
from users 
where name = 'the name' 

union all 

select id from i 

The manual относительно первичного и with подзапросов части:

Первичного запроса и с запросами все (умозрительно) выполняются в то же время

Хотя это ounds для меня "тот же снимок" Я не уверен, так как не знаю, что , по определению, означает в этом контексте.

Но there is also:

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

Если я правильно понимаю, что же снимок бит предотвращает условия гонки. Но опять же я не уверен, что в все утверждения ссылаются только на утверждения в подзапросах with, исключая основной запрос. Для того, чтобы избежать каких-либо сомнений Перемещение курсора в предыдущем запросе к with подзапроса:

with s as (
    select id 
    from users 
    where name = 'the name' 
), i as (
    insert into users (name) 
    select 'the name' 
    where not exists (select 1 from s) 
    returning id 
) 
select id from s 
union all 
select id from i 
+0

Тот факт, что CTE основаны на том же снимке, не может препятствовать условиям гонки. Один пример. Представьте себе две транзакции, начинающиеся практически в тот же момент. Оба обнаруживают, что «имя» еще не существует и пытается «ВСТАВИТЬ». Вы получаете уникальное нарушение. Есть веские причины для нового 'INSERT ... ON CONFLICT ...' –

+0

@ErwinBrandstetter Я не уверен, что то, что вы описываете, является точным, когда вы используете слово 'transaction', что подразумевает использование' begin' и ' commit'. В таком случае вторая транзакция будет заблокирована до тех пор, пока первая не завершится, по самому определению транзакций. –

+0

@ Erwin: Теперь я добираюсь до твоего мышления. Как я вижу, это две транзакции: транзакции. Один будет терпеть неудачу с надлежащим сообщением о уникальном нарушении, которое будет улавливаться и обрабатываться приложением. Это не похоже на вводящий в заблуждение результат. Я думаю, что попытка избежать второй транзакции из-за сбоя связана с разработкой. Не значительная прибыль по сравнению с добавленной сложностью. –

 Смежные вопросы

  • Нет связанных вопросов^_^