Thi*_*ter 5 postgresql foreign-key database-design constraint unique-constraint
我有一个包含用户的表。每个用户都有一个主电子邮件和一个指示该用户是否被删除的标志(我们从不硬删除用户)。
但是,每个用户还可以拥有额外的电子邮件。
无论如何,电子邮件地址必须是唯一的,我想在数据库级别强制执行这一点。对于主要电子邮件地址来说这很简单。我可以简单地添加一个带有WHERE not is_deleted
限制的 UNIQUE 索引,以便可以重新使用已删除的用户的电子邮件。
然而,对于辅助电子邮件来说,这就更加棘手。
is_deleted
用户的标志复制到该表,这也很丑陋,但允许我简单地使用 UNIQUE 索引与WHERE not is_deleted
.有没有更好的解决方案来实现我想要做的事情?
为了强制使用唯一的电子邮件地址,我将删除所有竞争电子邮件列,并将它们存储在email
所有活动电子邮件的一个中央表中。还有另一个表用于删除电子邮件:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text UNIQUE NOT NULL
, email text UNIQUE -- FK added below -- can also be NOT NULL
);
CREATE TABLE email (
email text PRIMARY KEY
, user_id int NOT NULL REFERENCES users ON DELETE CASCADE
, UNIQUE (user_id, email) -- seems redundant, but required for FK
);
ALTER TABLE users ADD CONSTRAINT users_primary_email_fkey
FOREIGN KEY (user_id, email) REFERENCES email (user_id, email);
CREATE TABLE email_deleted (
email_id serial PRIMARY KEY
, email text NOT NULL -- not necessarily unique
, user_id int NOT NULL REFERENCES users ON DELETE CASCADE
);
Run Code Online (Sandbox Code Playgroud)
这边走:
email
。email
移至email_deleted
.
users_primary_email_fkey
为 span (user_id, email)
,乍一看这似乎是多余的。但这样主电子邮件只能是同一用户实际拥有的电子邮件。MATCH SIMPLE
FK 约束的默认行为,您仍然可以输入没有主要电子邮件的用户,因为如果任何列为空,则不会强制执行 FK 约束。UNIQUE
对这个解决方案的约束是users.email
多余的,但出于其他原因它可能很有用。自动创建的索引应该会派上用场(例如对于此答案中的最后一个查询)。
唯一不以这种方式强制执行的是每个用户都有一个主要电子邮件。你也可以这样做。添加NOT NULL
约束到users.email
UNIQUE (user_id, email)
FK 约束需要:
您无疑已经发现了上述模型中的循环引用。与人们的预期相反,这确实有效。
只要users.email
可以NULL
,这都是微不足道的:
INSERT
没有电子邮件的用户。INSERT
引用拥有者的电子邮件user_id
。UPDATE
用户设置其主要电子邮件(如果适用)。它甚至可以与users.email
set to一起使用NOT NULL
。不过,您必须同时插入用户和电子邮件:
WITH u AS (
INSERT INTO users(username, email)
VALUES ('user_foo', 'foo@mail.com')
RETURNING email, user_id
)
INSERT INTO email (email, user_id)
SELECT email, user_id
FROM u;
Run Code Online (Sandbox Code Playgroud)
IMMEDIATE
FK 约束(默认)在每个语句的末尾进行检查。以上是一种说法。这就是为什么它可以在两个单独的语句失败的情况下起作用。详细解释:
要将用户的所有电子邮件作为数组获取,首先是主电子邮件:
SELECT u.*, e.emails
FROM users u
, LATERAL (
SELECT ARRAY (
SELECT email
FROM email
WHERE user_id = u.user_id
ORDER BY (email <> u.email) -- sort primary email first
) AS emails
) e
WHERE user_id = 1;
Run Code Online (Sandbox Code Playgroud)
您可以用VIEW
它创建一个以便于使用。
LATERAL
需要 Postgres 9.3。在 pg 9.2 中使用相关子查询:
SELECT *, ARRAY (
SELECT email
FROM email
WHERE user_id = u.user_id
ORDER BY (email <> u.email) -- sort primary email first
) AS emails
FROM users u
WHERE user_id = 1;
Run Code Online (Sandbox Code Playgroud)
要软删除电子邮件:
WITH del AS (
DELETE FROM email
WHERE email = 'spam@mail.com'
RETURNING email, user_id
)
INSERT INTO email_deleted (email, user_id)
SELECT email, user_id FROM del;
Run Code Online (Sandbox Code Playgroud)
要软删除给定用户的主电子邮件:
WITH upd AS (
UPDATE users u
SET email = NULL
FROM (SELECT user_id, email FROM users WHERE user_id = 123 FOR UPDATE) old
WHERE old.user_id = u.user_id
AND u.user_id = 1
RETURNING old.*
)
, del AS (
DELETE FROM email
USING upd
WHERE email.email = upd.email
)
INSERT INTO email_deleted (email, user_id)
SELECT email, user_id FROM upd;
Run Code Online (Sandbox Code Playgroud)
细节:
快速测试上述所有内容:SQL Fiddle。
考虑这个问题的一种方法是,您有两类用户,它们的规则略有不同:已删除和未删除。已删除用户的电子邮件可能会发生冲突,未删除用户的电子邮件必须是唯一的。
因为这两个类具有不同的规则(即约束),所以我不会使用标志来指示用户是否被删除,而是复制这些表:一组用于已删除的用户,一组用于未删除的用户。然后,我只需在表上为未删除的用户的电子邮件创建唯一约束。
优点:
is_deleted
行的来源生成一个标志)。CREATE INDEX...WHERE
. 我提到这一点的原因是部分索引并未得到普遍支持(例如MS-SQL 和DB2)。显然 PostgreSQL 没有任何问题,但避免未来潜在的迁移障碍总没有坏处。缺点:
INSERT..SELECT FROM
这样应该可以使将条目从未删除到已删除(反之亦然)变得简单。 归档时间: |
|
查看次数: |
6460 次 |
最近记录: |