【问题标题】:Cross table constraints in PostgreSQLPostgreSQL 中的跨表约束
【发布时间】:2015-06-22 18:57:21
【问题描述】:

使用 PostgreSQL 9.2.4,我有一个表 users 与表 user_roles 的关系为 1:many。 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)

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

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

【问题讨论】:

  • 您能提供更多关于这些列的详细信息吗?也许您的问题还有其他解决方案。
  • @Gordon 当然,我更新了问题以稍微澄清用例。
  • 如果您提供表定义(psql 中的\d tbl)以及像始终一样提供您的 Postgres 版本,将会有所帮助。
  • @ErwinBrandstetter 我用请求的信息更新了这个问题以供后代使用。我还更新了表名以避免分散注意力的命名问题,即使这可能会让你的答案看起来很奇怪。我非常感谢您如何不断指出这些问题。 (虽然在我的实际数据库中名称很好,但在提取此问题的相关部分时我犯了一个错误。)

标签: postgresql database-design triggers constraints referential-integrity


【解决方案1】:

说明

要求的表述留有解释空间:
UserRole.role_name 包含员工角色名称。

我的解释:
UserRole 中的条目具有 role_name = 'employee'

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

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

问题

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

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

MATCH SIMPLE 允许任何外键列为空;如果有的话 其中为空,该行不需要在引用的表中匹配。

关于 dba.SE 的相关答案还有更多:

解决方法是引入布尔标志is_employee 来标记两边的员工,在users 中定义NOT NULL,但在user_role 中允许为NULL

解决方案

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

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 标记任何user_role,而不管实际的role_name。如果需要,很容易禁止,但此实现不会引入任何超出要求的限制。

  • users.is_employee 只能是truefalse,并通过CHECK 约束强制反映employee_nr 的存在。触发器自动使列保持同步。您可以另外允许 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

【讨论】:

  • 。 .我将“强制执行某些角色的约束”解释为比仅称为“员工”的单个角色更广泛。我的解释是,有一个标志表明需要员工编号,而不是预先知道的特定角色。
  • @GordonLinoff:可能是这样。无论哪种方式,您提到的标志都将与我的设计中的 user_role.is_employee 相同,这将使其成为此解决方案的更简单子集。
  • 关于“某些角色”的解释,Gordon Linoff 是正确的。正如所指出的,这实际上使解决方案更简单。
  • @ErwinBrandstetter 这解决了我的问题。谢谢!我花了一点时间阅读以了解 MATCH SIMPLE 的行为。这让我想到我可以通过设置user_role.is_employee = false 来指定“无法使用”的角色。如果我从user_role.is_employee 中删除CHECK(is_employee),哪个有效。你为什么把那张支票放在首位?
  • @ErwinBrandstetter 。 . .像往常一样,你给出了一个非常彻底和解释清楚的答案。
【解决方案2】:

首先,您可以使用触发器解决此问题。

但是,我认为你可以使用约束来解决这个问题,只是有点奇怪:

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

【讨论】:

  • 我认为在 check 和 fk 约束下这是朝着正确的方向发展,但并不是一路走来。一个干净的解决方案是可能的。
【解决方案3】:

新答案:

这(SQL Sub queries in check constraint)似乎回答了您的问题,并且该语言仍在 9.4 文档中(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 在用户表中,并且UserRole1.role_name 在您必须提供的列表中IN () 运算符。

JOIN 的右边部分是相反的,它要求User.employee_numberNULL(这应该是你的非员工集)。

如果您需要更精确的解决方案,请提供有关您的表结构以及必须为员工选择哪些角色的更多详细信息。

【讨论】:

  • 对不起,如果我的问题不够清楚,但我正在寻找 约束,而不是查询。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多