PostgreSQL 中的跨表约束

Dag*_*ahl 3 postgresql triggers database-design referential-integrity constraints

使用 PostgreSQL 9.2.4,我有一个users与table有 1:many 关系的表user_roles。该users表存储员工和其他类型的用户。

                                       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)
Run Code Online (Sandbox Code Playgroud)

如果有包含员工角色名称的相关行,我想确保该列users.employee_number不能NULL存在user_roles.role_name。也就是说,我希望数据库强制执行约束,即对于某些角色,users.employee_number必须具有值,但对于其他角色则没有。

我怎样才能做到这一点,最好没有用户定义的函数或触发器?我发现(博客文章SO Answer)SQL Server 支持索引视图,这听起来很适合我的目的。但是,我认为物化视图在我的情况下不起作用,因为它们不是动态更新的。

Erw*_*ter 7

澄清

要求的制定留下了解释空间:
whereUserRole.role_name包含员工角色名称。

我的解释:
有一个条目UserRolerole_name = 'employee'.

您的命名约定 问题(现已更新)。User是标准 SQL 和 Postgres 中的保留字。除非双引号,否则作为标识符是非法的——这是不明智的。用户合法名称,因此您不必双引号。

我在我的实现中使用了无故障标识符。

问题

FOREIGN KEYCHECK约束是经过验证的、密封的工具,用于强制执行关系完整性。触发器是强大、有用和通用的功能,但更复杂、更不严格,并且有更多的设计错误和极端情况的空间。

您的情况很困难,因为 FK 约束一开始似乎是不可能的:它需要一个PRIMARY KEYorUNIQUE约束来引用 - 两者都不允许 NULL 值。没有部分 FK 约束,由于FK 约束的默认行为,严格参照完整性的唯一逃避是引用列中的 NULL 值MATCH SIMPLE根据文档:

MATCH SIMPLE允许任何外键列为空;如果它们中的任何一个为空,则该行不需要在引用的表中具有匹配项。

dba.SE 上的相关答案更多:

解决办法是引入一个布尔标志is_employee来标记员工两侧,定义NOT NULLusers,但允许为NULLuser_role

解决方案

完全符合您的要求,同时将噪音和开销降至最低:

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)
);
Run Code Online (Sandbox Code Playgroud)

就这样。

这些触发器是可选的,但为了方便起见,建议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();
Run Code Online (Sandbox Code Playgroud)

再次,废话,优化,仅在需要时调用。

Postgres 9.3 的SQL Fiddle演示。应该与 Postgres 9.1+ 一起使用。

要点

  • 现在,如果我们想设置user_role.role_name = 'employee',那么user.employee_nr首先必须有一个匹配。

  • 您还可以添加employee_nr任何用户,并且你可以(当时)还是任何标记user_roleis_employee实际的,irregardless role_name。如果需要,很容易禁止,但此实现不会引入比要求更多的限制。

  • users.is_employee只能是trueorfalse并且被强制反映一个employee_nrCHECK约束的存在。触发器自动保持列同步。您可以允许false其他目的,只需对设计进行少量更新。

  • 的规则user_role.is_employee略有不同:如果 必须为真role_name = 'employee'。由CHECK约束强制执行并再次由触发器自动设置。但是允许更改role_name为其他内容并仍然保留is_employee. 没有人说有一个用户employee_nr要求有在根据条目user_role,只是另一种方式圆!同样,如果需要,易于另外执行。

  • 如果还有其他触发器可能会干扰,请考虑:
    如何避免在 PostgreSQL 9.2.1 中循环触发器调用
    但是我们不必担心可能会违反规则,因为上述触发器只是为了方便起见。规则本身是由CHECKFK 和 FK 约束强制执行的,不允许有任何例外。

  • 旁白:出于某种原因,我将列is_employee放在约束UNIQUE (is_employee, users_id) 中的首位。users_idPK 中已经涵盖,因此它可以在此处排在第二位:
    DB 关联实体和索引