如何在 PostgreSQL 中插入 table A 期间有条件地在 table B 中插入一条记录?

How to conditionally insert a record in table B during insert into table A in PostgreSQL?

给定以下结构:

Table一个(aliases):

user_id | alias 
---------------
   1      john
   2      peter

user_idusers.

中引用了 id

Table B (users):

  id | password_hash | ...
---------------------------
   1        ...        ...
   2        ...        ...

(想法是用户可以有多个别名,所有这些别名都指向同一个主用户帐户记录)

我想做以下操作:给定一个 alias, password, ... 记录:

如何在 Postgres 中的单个查询中执行此操作?

类似于

WITH (
  INSERT INTO users(id, password, ...) VALUES(DEFAULT, password, ...) RETURNING id
)
INSERT INTO aliases(user_id, alias) VALUES(id, alias)
  ON CONFLICT {delete the temp row in users and update the one with the 
               known user_id instead}

注意:我假设aliasaliases的主键(但至少是唯一键)。

不幸的是,因为唯一列 (alias) 不在目标 table 上(属于 UPSERT),您无法使用单个 INSERT ... ON CONFLICT ... 执行此操作声明。

首先,您需要将 aliases.user_id(指的是 users.id 列)上的外键定义为 DEFERRABLE(可以是 INITIALLY IMMEDIATE虽然)。

之后,这些语句应该能够 运行(尽管对这些 table 进行了任何并发修改):

set constraints fk_aliases_user_id deferred;

with params(alias, pwd) as (
  values ('john', 'pass3'),
         ('jane', 'pass4')
),
inserted_alias as (
  insert into aliases(alias, user_id)
  select      alias, coalesce((select user_id
                               from   aliases a
                               where  a.alias = p.alias),
                              nextval('users_id_seq'))
  from        params p
  on conflict (alias) do nothing
  returning   *
)
insert into users(id, password_hash)
select      coalesce(i.user_id, a.user_id),
            crypt(p.pwd, gen_salt('bf'))
from        params p
left join   inserted_alias i using (alias)
left join   aliases a using (alias)
on conflict (id) do update
set         password_hash = excluded.password_hash;

set constraints fk_aliases_user_id immediate;

备注:

  • 我使用 pgcrypto 模块中的 crypt() 函数从纯密码生成 password_hash。我希望你也在做类似的事情。
  • 当并发性高时,这可能会导致 users_id_seq 中出现间隙,但应该总是成功(并且我通过第一个插入的 coalesce() 部分将这种可能性降至最低)。
  • 如果你的外键是INITIALLY DEFERRED,你可以保留set constraints语句。

http://rextester.com/YDY89070

您的另一个选择是使用 PL/pgSQL 和重试循环(添加 ON CONFLICT 支持之前的 official recommendation 是什么)。

编辑:似乎没有在 CTE 边界之间检查直接约束(但是,我还没有在文档中找到任何证据),所以 set constraints 声明 & 使外键可延迟是不需要的。

http://rextester.com/IUSM65192

这假设 users_id_seq 是用于 users.id 的序列并且 aliases.alias 上有一个 UNIQUE 约束:

WITH a AS (INSERT INTO aliases (user_id, alias)
              VALUES (nextval('users_id_seq'), p_alias)
           ON CONFLICT (alias)
              /* this does nothing, but is needed for RETURNING */
              DO UPDATE
                 SET user_id = aliases.user_id
           RETURNING user_id
          )
INSERT INTO users (id, password_hash, ...)
   SELECT user_id, p_password, ...
      FROM a
ON CONFLICT (id)
   DO UPDATE
      SET password_hash = EXCLUDED.password_hash;