PostgreSQL中的交叉表约束

时间:2015-04-16 11:36:53

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

使用PostgreSQL 9.2.4,我有一个表users,与表user_roles的关系为1:很多。 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必须具有值,但不能用于其他角色。

如何实现这一目标,最好不使用用户定义的函数或触发器?我发现(blog postSO Answer)SQL Server支持索引视图,这听起来像是为了我的目的。但是,我认为物化视图在我的情况下不起作用,因为它们不是动态更新的。

3 个答案:

答案 0 :(得分:3)

澄清

要求的表述为解释留下了空间:
其中UserRole.role_name包含员工角色名称。

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

您的命名惯例 存在问题(现在更新)。 User是标准SQL和Postgres中的保留字。除非双重引用,否则它作为标识符是非法的 - 这是不明智的。用户法律名称,因此您不必加倍引用。

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

问题

FOREIGN KEYCHECK约束是经过验证的,不透气的工具,可以强制执行关系完整性。触发器功能强大,实用且功能多样,但更复杂,不那么严格,并且设计错误和边角情况有更多空间。

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

  

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

关于dba.SE的相关答案:

解决方法是引入一个布尔标志is_employee来标记双方的员工,在NOT NULL中定义users,但在NULL中允许user_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,而不必另外任何 <:> < / p>

-- 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();

同样,没有废话,经过优化,只在需要时才被调用。

Postgres 9.3的

SQL Fiddle演示。应该使用Postgres 9.1 +。

重点

  • 现在,如果我们要设置user_role.role_name = 'employee',则必须首先匹配user.employee_nr

  • 您仍然可以向任何用户添加employee_nr,然后您可以(然后)使用user_role标记任何is_employee,无论如何实际的role_name。如果需要,很容易禁止,但此实现不会引入任何超出要求的限制。

  • users.is_employee 只能是truefalse,并且被强制反映employee_nr存在CHECK 1}}约束。触发器使列自动保持同步。您可以将false另外用于其他目的,只需对设计进行少量更新。

  • user_role.is_employee 的规则略有不同:如果role_name = 'employee',则必须为true。由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) 中的第一个。 PK中已涵盖users_id,因此它可以在这里取得第二名:
    DB associative entities and indexing

答案 1 :(得分:1)

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

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

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

答案 2 :(得分:0)

新答案:

这个(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和U serRole1.UserId,请将这些字段更改为实际值。

在查询的JOIN部分,左侧有一个OR,要求用户表中的员工编号不是NULL,而 UserRole1.role_name在列表中您必须向IN ()运营商提供

JOIN的正确部分恰恰相反,它要求User.employee_numberNULL(这应该是您的非员工集)。

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