【问题标题】:How to conditionally insert a record in table B during insert into table A in PostgreSQL?如何在PostgreSQL中插入表A期间有条件地在表B中插入一条记录?
【发布时间】:2017-03-31 11:45:58
【问题描述】:

给定以下结构:

表 A (aliases):

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

user_idusers 中引用id

表 B (users):

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

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

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

  • 如果aliases中存在alias,则更新users中对应的password
  • 如果alias 不存在,使用给定密码在users 中创建一个新用户,并在aliases 中插入一行指向这条新记录。

如何在 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}

【问题讨论】:

    标签: sql postgresql upsert


    【解决方案1】:

    这假定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;
    

    【讨论】:

    • 那么,您是说在 CTE 边界处不检查直接约束?在这种情况下会很方便。
    • 我不明白 - 哪个约束?
    • 是的。我假设aliases.user_id 上有一个外键(应该指向users.id)。那个,至少在我的例子中。
    • 你有什么理由不能在 CTE 部分只做 ON CONFLICT DO NOTHING RETURNING user_id 吗?
    • 是的,因为这样就没有RETURNING 数据。它不漂亮,但不应该成为问题。
    【解决方案2】:

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

    很遗憾,由于唯一列 (alias) 不在目标表(UPSERT)上,因此您无法使用单个 INSERT ... ON CONFLICT ... 语句来执行此操作。

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

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

    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

    【讨论】:

    • 下面的答案更简洁,所以我会接受它,但延迟约束检查是一个我不知道存在的非常有趣的功能。
    • @IvanPoliakov 另一个更简洁的答案,因为它会在序列中留下空白。而且,它并不总是成功,因为insert into ... on duplicate do update ... returning ... 在有更新(而不是插入)时实际上不会返回。请参阅:rextester.com/XOJ87761 - 添加这些将得到我的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多