Получить идентификатор из условного 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 не принадлежат друг другу.

Ответ 1

Реализация UPSERT чрезвычайно сложна, чтобы быть в безопасности от одновременного доступа к записи. Взгляните на эту 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
...

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

Ответ 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

Руководство о первичных и with частях подзапросов:

Основной запрос и запросы WITH - это все (условно), выполняемые одновременно

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

Но также есть:

Подзапросы в 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