在 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_numberNULL(这应该是你的非员工集)。

如果您需要更精确的解决方案,请提供有关您的 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 KEYCHECK 约束是经过验证的可靠工具,可确保关系完整性。触发器是强大、有用和通用的功能,但更复杂、更不严格,并且有更多设计错误和极端情况的空间。

你的情况很困难,因为 FK 约束起初似乎是不可能的:它需要一个 PRIMARY KEYUNIQUE 约束来引用——两者都不允许 NULL 值。没有部分 FK 约束,由于默认 MATCH SIMPLEreferencing 列中的 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 只能是 truefalse 并且被迫反映 [= 的存在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