在 PostgreSQL 中交叉 table 约束
Cross table constraints in PostgreSQL
使用 PostgreSQL 9.2.4,我有一个 table users
与 table user_roles
的 1:many 关系。 users
table 存储员工和其他类型的用户。
Table "public.users"
Column | Type | Modifiers
-----------------+-------------------+-----------------------------------------------------
uid | integer | not null default nextval('users_uid_seq'::regclass)
employee_number | character varying |
name | character varying |
Indexes:
"users_pkey" PRIMARY KEY, btree (uid)
Referenced by:
TABLE "user_roles" CONSTRAINT "user_roles_uid_fkey" FOREIGN KEY (uid) REFERENCES users(uid)
Table "public.user_roles"
Column | Type | Modifiers
-----------+-------------------+------------------------------------------------------------------
id | integer | not null default nextval('user_roles_id_seq'::regclass)
uid | integer |
role | character varying | not null
Indexes:
"user_roles_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"user_roles_uid_fkey" FOREIGN KEY (uid) REFERENCES users(uid)
如果有相关行 user_roles.role_name
包含员工角色名称,我想确保列 users.employee_number
不能是 NULL
。也就是说,我希望数据库强制执行约束,即对于某些角色,users.employee_number
必须有一个值,但对于其他角色则没有。
我怎样才能做到这一点,最好不要使用用户定义的函数或触发器?我发现 (blog post, SO Answer) SQL 服务器支持索引视图,这听起来可以满足我的目的。但是,我假设物化视图在我的情况下不起作用,因为它们不是动态更新的。
新答案:
这个(SQL Sub queries in check constraint ) seems to answer your question, and the language is still in the 9.4 documentation( http://www.postgresql.org/docs/9.4/interactive/sql-createtable.html).
旧答案:
SELECT
User.*
, UserRole1.*
FROM
User
LEFT JOIN UserRole UserRole1
ON User.id = UserRole1.UserId
AND (
(
User.employee_number IS NOT NULL AND UserRole1.role_name IN (enumerate employee role names here)
)
OR
(User.employee_number IS NULL)
)
以上查询选择了 User
中的所有字段和 UserRole
中的所有字段(别名为 UserRole1)。我假设这两个字段之间的关键字段称为 User.id 和 UserRole1.UserId
,请将它们更改为实际值。
在查询的 JOIN
部分有一个 OR,在左侧要求员工编号不是 NULL
在用户 table 中 UserRole1.role_name 位于您必须向 IN ()
运算符提供 的列表中。
JOIN
右边的部分是相反的,它要求User.employee_number
是NULL
(这应该是你的非员工集)。
如果您需要更精确的解决方案,请提供有关您的 table 结构以及必须为员工选择哪些角色的更多详细信息。
首先,你可以使用触发器来解决这个问题。
但是,我认为你可以使用约束来解决这个问题,只是有点奇怪:
create table UserRoles (
UserRoleId int not null primary key,
. . .
NeedsEmployeeNumber boolean not null,
. . .
);
create table Users (
. . .
UserRoleId int,
NeedsEmployeeNumber boolean,
EmployeeNumber,
foreign key (UserRoleId, NeedsEmployeeNumber) references UserRoles(UserRoleId, NeedsEmployeeNumber),
check ((NeedsEmployeeNumber and EmployeeNumber is not null) or
(not NeedsEmployeeNumber and EmployeeNumber is null)
)
);
这应该可行,但这是一个笨拙的解决方案:
- 为员工添加角色时,需要在添加角色的同时添加标志。
- 如果更新角色以更改标志,则需要将其传播到现有记录——并且传播不能自动进行,因为您还可能需要设置
EmployeeNumber
.
澄清
此要求的表述留有解释空间:
其中 UserRole.role_name
包含员工角色名称。
我的解读:
UserRole
中的条目具有 role_name = 'employee'
。
您的命名约定是有问题(现已更新)。 User
是标准 SQL 和 Postgres 中的保留字。除非用双引号引起来,否则作为标识符是非法的——这是不明智的。用户合法名称,因此您不必双引号。
我在实施中使用了无故障标识符。
问题
FOREIGN KEY
和 CHECK
约束是经过验证的可靠工具,可确保关系完整性。触发器是强大、有用和通用的功能,但更复杂、更不严格,并且有更多设计错误和极端情况的空间。
你的情况很困难,因为 FK 约束起初似乎是不可能的:它需要一个 PRIMARY KEY
或 UNIQUE
约束来引用——两者都不允许 NULL 值。没有部分 FK 约束,由于默认 MATCH SIMPLE
,referencing 列中的 NULL 值是避免严格参照完整性的唯一方法FK 约束的行为。 Per documentation:
MATCH SIMPLE
allows any of the foreign key columns to be null; if any
of them are null, the row is not required to have a match in the referenced table.
dba.SE 上的相关回答更多:
解决方法是引入一个布尔标志is_employee
来标记两边的员工,在users
中定义为NOT NULL
,但在[=中允许为NULL
26=]:
解决方案
这会完全执行您的要求,同时将噪音和开销保持在最低限度:
CREATE TABLE users (
users_id serial PRIMARY KEY
, employee_nr int
, is_employee bool NOT NULL DEFAULT false
, CONSTRAINT role_employee CHECK (employee_nr IS NOT NULL = is_employee)
, UNIQUE (is_employee, users_id) -- required for FK (otherwise redundant)
);
CREATE TABLE user_role (
user_role_id serial PRIMARY KEY
, users_id int NOT NULL REFERENCES users
, role_name text NOT NULL
, is_employee bool CHECK(is_employee)
, CONSTRAINT role_employee
CHECK (role_name <> 'employee' OR is_employee IS TRUE)
, CONSTRAINT role_employee_requires_employee_nr_fk
FOREIGN KEY (is_employee, users_id) REFERENCES users(is_employee, users_id)
);
就这些了。
这些 触发器 是可选的,但为了方便起见,建议自动设置添加的标签 is_employee
并且您不必执行任何操作 任何事情额外:
-- users
CREATE OR REPLACE FUNCTION trg_users_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = (NEW.employee_nr IS NOT NULL);
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF employee_nr ON users
FOR EACH ROW
EXECUTE PROCEDURE trg_users_insup_bef();
-- user_role
CREATE OR REPLACE FUNCTION trg_user_role_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = true;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF role_name ON user_role
FOR EACH ROW
WHEN (NEW.role_name = 'employee')
EXECUTE PROCEDURE trg_user_role_insup_bef();
再次强调,废话,优化,只在需要时调用。
SQL Fiddle Postgres 9.3 演示。应与 Postgres 9.1+ 一起使用。
要点
现在,如果我们要设置user_role.role_name = 'employee'
,那么必须先有一个匹配的user.employee_nr
。
您仍然可以将 employee_nr
添加到 任何 用户,并且您(然后)仍然可以使用 is_employee
,不管实际的 role_name
。如果需要,很容易禁止,但此实现不会引入比要求更多的限制。
users.is_employee
只能是 true
或 false
并且被迫反映 [= 的存在30=] 由 CHECK
约束。触发器自动使列保持同步。您可以允许 false
额外用于其他目的,只需对设计进行少量更新。
user_role.is_employee
的规则略有不同:如果role_name = 'employee'
则必须为真。由 CHECK
约束强制执行并再次由触发器自动设置。但允许将 role_name
更改为其他内容并仍然保留 is_employee
。没有人说 employee_nr
的用户 需要 在 user_role
中有相应的条目,恰恰相反!同样,如果需要,可以轻松实施。
如果有其他触发因素可能会干扰,请考虑:
How To Avoid Looping Trigger Calls In PostgreSQL 9.2.1
但我们不必担心可能会违反规则,因为上述触发器只是为了方便。规则本身是通过 CHECK
和 FK 约束强制执行的,不允许有任何例外。
旁白:出于某种原因,我将列 is_employee
放在约束 UNIQUE (is_employee, users_id)
的第一位。 users_id
已经在PK中覆盖了,所以这里可以排第二:
DB associative entities and indexing
使用 PostgreSQL 9.2.4,我有一个 table users
与 table user_roles
的 1:many 关系。 users
table 存储员工和其他类型的用户。
Table "public.users"
Column | Type | Modifiers
-----------------+-------------------+-----------------------------------------------------
uid | integer | not null default nextval('users_uid_seq'::regclass)
employee_number | character varying |
name | character varying |
Indexes:
"users_pkey" PRIMARY KEY, btree (uid)
Referenced by:
TABLE "user_roles" CONSTRAINT "user_roles_uid_fkey" FOREIGN KEY (uid) REFERENCES users(uid)
Table "public.user_roles"
Column | Type | Modifiers
-----------+-------------------+------------------------------------------------------------------
id | integer | not null default nextval('user_roles_id_seq'::regclass)
uid | integer |
role | character varying | not null
Indexes:
"user_roles_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"user_roles_uid_fkey" FOREIGN KEY (uid) REFERENCES users(uid)
如果有相关行 user_roles.role_name
包含员工角色名称,我想确保列 users.employee_number
不能是 NULL
。也就是说,我希望数据库强制执行约束,即对于某些角色,users.employee_number
必须有一个值,但对于其他角色则没有。
我怎样才能做到这一点,最好不要使用用户定义的函数或触发器?我发现 (blog post, SO Answer) SQL 服务器支持索引视图,这听起来可以满足我的目的。但是,我假设物化视图在我的情况下不起作用,因为它们不是动态更新的。
新答案:
这个(SQL Sub queries in check constraint ) seems to answer your question, and the language is still in the 9.4 documentation( http://www.postgresql.org/docs/9.4/interactive/sql-createtable.html).
旧答案:
SELECT
User.*
, UserRole1.*
FROM
User
LEFT JOIN UserRole UserRole1
ON User.id = UserRole1.UserId
AND (
(
User.employee_number IS NOT NULL AND UserRole1.role_name IN (enumerate employee role names here)
)
OR
(User.employee_number IS NULL)
)
以上查询选择了 User
中的所有字段和 UserRole
中的所有字段(别名为 UserRole1)。我假设这两个字段之间的关键字段称为 User.id 和 UserRole1.UserId
,请将它们更改为实际值。
在查询的 JOIN
部分有一个 OR,在左侧要求员工编号不是 NULL
在用户 table 中 UserRole1.role_name 位于您必须向 IN ()
运算符提供 的列表中。
JOIN
右边的部分是相反的,它要求User.employee_number
是NULL
(这应该是你的非员工集)。
如果您需要更精确的解决方案,请提供有关您的 table 结构以及必须为员工选择哪些角色的更多详细信息。
首先,你可以使用触发器来解决这个问题。
但是,我认为你可以使用约束来解决这个问题,只是有点奇怪:
create table UserRoles (
UserRoleId int not null primary key,
. . .
NeedsEmployeeNumber boolean not null,
. . .
);
create table Users (
. . .
UserRoleId int,
NeedsEmployeeNumber boolean,
EmployeeNumber,
foreign key (UserRoleId, NeedsEmployeeNumber) references UserRoles(UserRoleId, NeedsEmployeeNumber),
check ((NeedsEmployeeNumber and EmployeeNumber is not null) or
(not NeedsEmployeeNumber and EmployeeNumber is null)
)
);
这应该可行,但这是一个笨拙的解决方案:
- 为员工添加角色时,需要在添加角色的同时添加标志。
- 如果更新角色以更改标志,则需要将其传播到现有记录——并且传播不能自动进行,因为您还可能需要设置
EmployeeNumber
.
澄清
此要求的表述留有解释空间:
其中 UserRole.role_name
包含员工角色名称。
我的解读:
UserRole
中的条目具有 role_name = 'employee'
。
您的命名约定是有问题(现已更新)。 User
是标准 SQL 和 Postgres 中的保留字。除非用双引号引起来,否则作为标识符是非法的——这是不明智的。用户合法名称,因此您不必双引号。
我在实施中使用了无故障标识符。
问题
FOREIGN KEY
和 CHECK
约束是经过验证的可靠工具,可确保关系完整性。触发器是强大、有用和通用的功能,但更复杂、更不严格,并且有更多设计错误和极端情况的空间。
你的情况很困难,因为 FK 约束起初似乎是不可能的:它需要一个 PRIMARY KEY
或 UNIQUE
约束来引用——两者都不允许 NULL 值。没有部分 FK 约束,由于默认 MATCH SIMPLE
,referencing 列中的 NULL 值是避免严格参照完整性的唯一方法FK 约束的行为。 Per documentation:
MATCH SIMPLE
allows any of the foreign key columns to be null; if any of them are null, the row is not required to have a match in the referenced table.
dba.SE 上的相关回答更多:
解决方法是引入一个布尔标志is_employee
来标记两边的员工,在users
中定义为NOT NULL
,但在[=中允许为NULL
26=]:
解决方案
这会完全执行您的要求,同时将噪音和开销保持在最低限度:
CREATE TABLE users (
users_id serial PRIMARY KEY
, employee_nr int
, is_employee bool NOT NULL DEFAULT false
, CONSTRAINT role_employee CHECK (employee_nr IS NOT NULL = is_employee)
, UNIQUE (is_employee, users_id) -- required for FK (otherwise redundant)
);
CREATE TABLE user_role (
user_role_id serial PRIMARY KEY
, users_id int NOT NULL REFERENCES users
, role_name text NOT NULL
, is_employee bool CHECK(is_employee)
, CONSTRAINT role_employee
CHECK (role_name <> 'employee' OR is_employee IS TRUE)
, CONSTRAINT role_employee_requires_employee_nr_fk
FOREIGN KEY (is_employee, users_id) REFERENCES users(is_employee, users_id)
);
就这些了。
这些 触发器 是可选的,但为了方便起见,建议自动设置添加的标签 is_employee
并且您不必执行任何操作 任何事情额外:
-- users
CREATE OR REPLACE FUNCTION trg_users_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = (NEW.employee_nr IS NOT NULL);
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF employee_nr ON users
FOR EACH ROW
EXECUTE PROCEDURE trg_users_insup_bef();
-- user_role
CREATE OR REPLACE FUNCTION trg_user_role_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = true;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF role_name ON user_role
FOR EACH ROW
WHEN (NEW.role_name = 'employee')
EXECUTE PROCEDURE trg_user_role_insup_bef();
再次强调,废话,优化,只在需要时调用。
SQL Fiddle Postgres 9.3 演示。应与 Postgres 9.1+ 一起使用。
要点
现在,如果我们要设置
user_role.role_name = 'employee'
,那么必须先有一个匹配的user.employee_nr
。您仍然可以将
employee_nr
添加到 任何 用户,并且您(然后)仍然可以使用is_employee
,不管实际的role_name
。如果需要,很容易禁止,但此实现不会引入比要求更多的限制。users.is_employee
只能是true
或false
并且被迫反映 [= 的存在30=] 由CHECK
约束。触发器自动使列保持同步。您可以允许false
额外用于其他目的,只需对设计进行少量更新。user_role.is_employee
的规则略有不同:如果role_name = 'employee'
则必须为真。由CHECK
约束强制执行并再次由触发器自动设置。但允许将role_name
更改为其他内容并仍然保留is_employee
。没有人说employee_nr
的用户 需要 在user_role
中有相应的条目,恰恰相反!同样,如果需要,可以轻松实施。如果有其他触发因素可能会干扰,请考虑:
How To Avoid Looping Trigger Calls In PostgreSQL 9.2.1
但我们不必担心可能会违反规则,因为上述触发器只是为了方便。规则本身是通过CHECK
和 FK 约束强制执行的,不允许有任何例外。旁白:出于某种原因,我将列
is_employee
放在约束UNIQUE (is_employee, users_id)
的第一位。users_id
已经在PK中覆盖了,所以这里可以排第二:
DB associative entities and indexing