postgresql - PostgreSQL 中的跨表约束

标签 postgresql database-design triggers constraints referential-integrity

使用 PostgreSQL 9.2.4,我有一个表 users与表的 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)

我要确保列users.employee_number不能是 NULL如果有相关行,其中 user_roles.role_name包含员工角色名称。也就是说,我希望数据库强制执行某些角色的约束,users.employee_number必须有值(value),但不是为别人。

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

最佳答案

澄清

这个的配方要求 留下解释的空间:
哪里UserRole.role_name包含员工角色名称。

我的解读:
UserRole 中有一个条目有 role_name = 'employee' .

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

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

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

你的情况很困难,因为 FK 约束一开始似乎是不可能的:它需要一个 PRIMARY KEYUNIQUE引用约束 - 都不允许 NULL 值。没有部分 FK 约束,由于默认 ,唯一可以逃避严格参照完整性的是引用列中的 NULL 值。 MATCH SIMPLE 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 上的相关答案更多:
  • Two-column foreign key constraint only when third column is NOT NULL

  • 解决方法是引入一个 bool 标志 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)
    );
    

    就这样。

    这些 触发器 是可选的,但为了方便设置添加的标签,建议使用 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给任何用户,您(然后)仍然可以标记任何 user_roleis_employee , 不管实际 role_name .如果需要,很容易禁止,但此实现不会引入比要求更多的限制。
  • users.is_employee 只能是 truefalse并被迫反射(reflect) employee_nr 的存在由 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 约束,不允许有任何异常(exception)。
  • 旁白:我把专栏is_employee约束中的第一个 UNIQUE (is_employee, users_id)因为某种原因。 users_id已经包含在 PK 中,因此它可以在此处排在第二位:
    DB associative entities and indexing
  • 关于postgresql - PostgreSQL 中的跨表约束,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29673536/

    相关文章:

    mysql - MSQ 无法从它选择的同一表格中删除 - 需要帮助解决我的删除触发器中的问题

    ruby-on-rails - 在 Rails 应用程序中更改 PostgreSQL 表会禁用触发器

    MySQL 5.0 无法删除和创建触发器

    ruby - 使用 ActiveRecord 时 createdb 未被识别为命令

    database - 在数据库列中存储分隔列表真的那么糟糕吗?

    database - 从 Oracle 连接转换为 Postgres 连接

    c# - 提高在 EF 中为一对多关系插入数据的性能

    php - 获取sql格式的服务器时间戳

    Python + PostgreSQL + 奇怪的ascii = UTF8编码错误

    node.js - Express Sequelize 和 Passport.js 身份验证策略不起作用