如何在 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_id
在 users
.
中引用了 id
Table 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}
注意:我假设alias
是aliases
的主键(但至少是唯一键)。
不幸的是,因为唯一列 (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
语句。
您的另一个选择是使用 PL/pgSQL 和重试循环(添加 ON CONFLICT
支持之前的 official recommendation 是什么)。
编辑:似乎没有在 CTE 边界之间检查直接约束(但是,我还没有在文档中找到任何证据),所以 set constraints
声明 & 使外键可延迟是不需要的。
这假设 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;
给定以下结构:
Table一个(aliases
):
user_id | alias
---------------
1 john
2 peter
user_id
在 users
.
id
Table B (users
):
id | password_hash | ...
---------------------------
1 ... ...
2 ... ...
(想法是用户可以有多个别名,所有这些别名都指向同一个主用户帐户记录)
我想做以下操作:给定一个 alias, password, ...
记录:
- 如果
aliases
中存在alias
,更新users
中对应的 - 如果
alias
不存在,使用给定的密码在users
中创建一个新用户,并在aliases
中插入一行指向这个新记录。
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}
注意:我假设alias
是aliases
的主键(但至少是唯一键)。
不幸的是,因为唯一列 (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
语句。
您的另一个选择是使用 PL/pgSQL 和重试循环(添加 ON CONFLICT
支持之前的 official recommendation 是什么)。
编辑:似乎没有在 CTE 边界之间检查直接约束(但是,我还没有在文档中找到任何证据),所以 set constraints
声明 & 使外键可延迟是不需要的。
这假设 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;