Kev*_*rke 28 postgresql database-design constraint referential-integrity ddl
假设我们有用户,每个用户可以有多个电子邮件地址
CREATE TABLE emails (
user_id integer,
email_address text,
is_active boolean
)
Run Code Online (Sandbox Code Playgroud)
一些示例行
user_id | email_address | is_active
1 | foo@bar.com | t
1 | baz@bar.com | f
1 | bar@foo.com | f
2 | ccc@ddd.com | t
Run Code Online (Sandbox Code Playgroud)
我想强制执行一个约束,即每个用户都只有一个活动地址。我怎样才能在 Postgres 中做到这一点?我可以这样做:
CREATE UNIQUE INDEX "user_email" ON emails(user_id) WHERE is_active=true;
Run Code Online (Sandbox Code Playgroud)
这可以防止用户拥有多个活动地址,但我相信不会防止他们的所有地址都设置为 false。
如果可能的话,我更愿意避免使用触发器或 pl/pgsql 脚本,因为我们目前没有这些脚本,而且设置起来会很困难。但我很感激知道“唯一的方法是使用触发器或 pl/pgsql”,如果是这样的话。
Erw*_*ter 21
您根本不需要触发器或 PL/pgSQL。
你甚至不需要 DEFERRABLE约束。
而且您不需要冗余存储任何信息。
在users表中包含活动电子邮件的 ID ,从而导致相互引用。有人可能认为我们需要一个DEFERRABLE约束来解决插入用户及其活动电子邮件的先有鸡还是先有蛋的问题,但使用数据修改 CTE 我们甚至不需要它。
这始终对每个用户强制执行一封活动电子邮件:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
, email_id int NOT NULL -- FK to active email, constraint added below
);
CREATE TABLE email (
email_id serial PRIMARY KEY
, user_id int NOT NULL REFERENCES users ON DELETE CASCADE ON UPDATE CASCADE
, email text NOT NULL
, CONSTRAINT email_fk_uni UNIQUE(user_id, email_id) -- for FK constraint below
);
ALTER TABLE users ADD CONSTRAINT active_email_fkey
FOREIGN KEY (user_id, email_id) REFERENCES email(user_id, email_id);
Run Code Online (Sandbox Code Playgroud)
删除NOT NULL约束users.email_id以使其“最多一封活动电子邮件”。(您仍然可以为每个用户存储多封电子邮件,但它们都不是“活动的”。)
你可以让active_email_fkey DEFERRABLE让更多的回旋余地(插入用户和电子邮件中的单独的命令相同的事务),但是这是没有必要的。
我user_id首先在UNIQUE约束email_fk_uni中优化索引覆盖率。细节:
可选视图:
CREATE VIEW user_with_active_email AS
SELECT * FROM users JOIN email USING (user_id, email_id);
Run Code Online (Sandbox Code Playgroud)
以下是使用活动电子邮件插入新用户的方法(根据需要):
WITH new_data(username, email) AS (
VALUES
('usr1', 'abc@d.com') -- new users with *1* active email
, ('usr2', 'def3@d.com')
, ('usr3', 'ghi1@d.com')
)
, u AS (
INSERT INTO users(username, email_id)
SELECT n.username, nextval('email_email_id_seq'::regclass)
FROM new_data n
RETURNING *
)
INSERT INTO email(email_id, user_id, email)
SELECT u.email_id, u.user_id, n.email
FROM u
JOIN new_data n USING (username);
Run Code Online (Sandbox Code Playgroud)
具体的困难在于我们既没有user_id也没有email_id开始。两者都是由各自提供的序列号SEQUENCE。它不能用一个RETURNING子句解决(另一个鸡和蛋的问题)。解决方案nextval()在下面的链接答案中详细解释。
如果您不知道serial列的附加序列的名称,email.email_id您可以替换:
nextval('email_email_id_seq'::regclass)
Run Code Online (Sandbox Code Playgroud)
和
nextval(pg_get_serial_sequence('email', 'email_id'))
Run Code Online (Sandbox Code Playgroud)
以下是添加新“活动”电子邮件的方法:
WITH e AS (
INSERT INTO email (user_id, email)
VALUES (3, 'new_active@d.com')
RETURNING *
)
UPDATE users u
SET email_id = e.email_id
FROM e
WHERE u.user_id = e.user_id;
Run Code Online (Sandbox Code Playgroud)
如果某些头脑简单的 ORM 不够聪明,无法处理这个问题,您可以将 SQL 命令封装在服务器端函数中。
密切相关,并有充分的解释:
还相关:
关于DEFERRABLE约束:
关于nextval()和pg_get_serial_sequence():
如果您可以向表中添加一列,则以下方案几乎可以1工作:
CREATE TABLE emails
(
UserID integer NOT NULL,
EmailAddress varchar(254) NOT NULL,
IsActive boolean NOT NULL,
-- New column
ActiveAddress varchar(254) NOT NULL,
-- Obvious PK
CONSTRAINT PK_emails_UserID_EmailAddress
PRIMARY KEY (UserID, EmailAddress),
-- Validate that the active address row exists
CONSTRAINT FK_emails_ActiveAddressExists
FOREIGN KEY (UserID, ActiveAddress)
REFERENCES emails (UserID, EmailAddress),
-- Validate the IsActive value makes sense
CONSTRAINT CK_emails_Validate_IsActive
CHECK
(
(IsActive = true AND EmailAddress = ActiveAddress)
OR
(IsActive = false AND EmailAddress <> ActiveAddress)
)
);
-- Enforce maximum of one active address per user
CREATE UNIQUE INDEX UQ_emails_One_IsActive_True_PerUser
ON emails (UserID, IsActive)
WHERE IsActive = true;
Run Code Online (Sandbox Code Playgroud)
在a_horse_with_no_name 的帮助下,从我的本地 SQL Server 翻译而来
正如ypercube在评论中提到的,您甚至可以更进一步:
UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)效果是一样的,但可以说是更简单、更整洁。
1问题在于现有约束仅确保存在被另一行称为“活动”的行,而不是它实际上也是活动的。我不太了解 Postgres,无法自己实现额外的约束(至少现在不是),但在 SQL Server 中,可以这样做:
CREATE TABLE Emails
(
EmailID integer NOT NULL UNIQUE,
UserID integer NOT NULL,
EmailAddress varchar(254) NOT NULL,
IsActive bit NOT NULL,
-- New columns
ActiveEmailID integer NOT NULL,
ActiveIsActive AS CONVERT(bit, 'true') PERSISTED,
-- Obvious PK
CONSTRAINT PK_emails_UserID_EmailAddress
PRIMARY KEY (UserID, EmailID),
CONSTRAINT UQ_emails_UserID_EmailAddress_IsActive
UNIQUE (UserID, EmailID, IsActive),
-- Validate that the active address exists and is active
CONSTRAINT FK_emails_ActiveAddressExists_And_IsActive
FOREIGN KEY (UserID, ActiveEmailID, ActiveIsActive)
REFERENCES emails (UserID, EmailID, IsActive),
-- Validate the IsActive value makes sense
CONSTRAINT CK_emails_Validate_IsActive
CHECK
(
(IsActive = 'true' AND EmailID = ActiveEmailID)
OR
(IsActive = 'false' AND EmailID <> ActiveEmailID)
)
);
-- Enforce maximum of one active address per user
CREATE UNIQUE INDEX UQ_emails_One_IsActive_PerUser
ON emails (UserID, IsActive)
WHERE IsActive = 'true';
Run Code Online (Sandbox Code Playgroud)
通过使用代理而不是复制完整的电子邮件地址,这项工作在原来的基础上有所改进。
在不更改架构的情况下执行上述任一操作的唯一方法是使用 PL/PgSQL 触发器。
对于“恰好一个”的情况,您可以使引用相互引用,其中一个是DEFERRABLE INITIALLY DEFERRED。所以A.b_id(FK)引用B.b_id(PK)和B.a_id(FK)引用A.a_id(PK)。但许多 ORM 等无法应对可延迟的约束。因此,在这种情况下,您需要在列上添加从用户到地址的可延迟 FK active_address_id,而不是active在 上使用标志address。