问题
Using PostgreSQL 9.2.4, I have a table users
with a 1:many relation to the table user_roles
. The users
table stores both employees and other kinds of 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)
I want to ensure that the column users.employee_number
cannot be NULL
if there is a related row where user_roles.role_name
contains an employee role name. That is, I want the database to enforce the constraint that for some roles, users.employee_number
must have a value, but not for others.
How can I accomplish this, preferably without user-defined functions or triggers? I found (blog post, SO Answer) that SQL Server supports indexed views, which sounds like it would serve my purpose. However, I assume that materialized views will not work in my case, since they are not dynamically updated.
回答1:
Clarifications
The formulation of this requirement leaves room for interpretation:
where UserRole.role_name
contains an employee role name.
My interpretation:
with an entry in UserRole
that has role_name = 'employee'
.
Your naming convention is was problematic (updated now). User
is a reserved word in standard SQL and Postgres. It's illegal as identifier unless double-quoted - which would be ill-advised. User legal names so you don't have to double-quote.
I am using trouble-free identifiers in my implementation.
The problem
FOREIGN KEY
and CHECK
constraint are the proven, air-tight tools to enforce relational integrity. Triggers are powerful, useful and versatile features but more sophisticated, less strict and with more room for design errors and corner cases.
Your case is difficult because a FK constraint seems impossible at first: it requires a PRIMARY KEY
or UNIQUE
constraint to reference - neither allows NULL values. There are no partial FK constraints, the only escape from strict referential integrity are NULL values in the referencing columns due to the default MATCH SIMPLE
behavior of FK constraints. 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.
Related answer on dba.SE with more:
- Two-column foreign key constraint only when third column is NOT NULL
The workaround is to introduce a boolean flag is_employee
to mark employees on both sides, defined NOT NULL
in users
, but allowed to be NULL
in user_role
:
Solution
This enforces your requirements exactly, while keeping noise and overhead to a minimum:
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)
);
That's all.
These triggers are optional but recommended for convenience to set the added tags is_employee
automatically and you don't have to do anything extra:
-- 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();
Again, no-nonsense, optimized and only called when needed.
SQL Fiddle demo for Postgres 9.3. Should work with Postgres 9.1+.
Major points
Now, if we want to set
user_role.role_name = 'employee'
, then there has to be a matchinguser.employee_nr
first.You can still add an
employee_nr
to any user, and you can (then) still tag anyuser_role
withis_employee
, irregardless of the actualrole_name
. Easy to disallow if you need to, but this implementation does not introduce any more restrictions than required.users.is_employee
can only betrue
orfalse
and is forced to reflect the existence of anemployee_nr
by theCHECK
constraint. The trigger keeps the column in sync automatically. You could allowfalse
additionally for other purposes with only minor updates to the design.The rules for
user_role.is_employee
are slightly different: it must be true ifrole_name = 'employee'
. Enforced by aCHECK
constraint and set automatically by the trigger again. But it's allowed to changerole_name
to something else and still keepis_employee
. Nobody said a user with anemployee_nr
is required to have an according entry inuser_role
, just the other way round! Again, easy to enforce additionally if needed.If there are other triggers that could interfere, consider this:
How To Avoid Looping Trigger Calls In PostgreSQL 9.2.1
But we need not worry that rules might be violated because the above triggers are only for convenience. The rules per se are enforce withCHECK
and FK constraints, which allow no exceptions.Aside: I put the column
is_employee
first in the constraintUNIQUE (is_employee, users_id)
for a reason.users_id
is already covered in the PK, so it can take second place here:
DB associative entities and indexing
回答2:
First, you can solve this using a trigger.
But, I think you can solve this using constraints, with just a little weirdness:
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)
)
);
This should work, but it is an awkward solution:
- When you add a role to an employee, you need to add the flag along with the role.
- If a role is updated to change the flag, then this needs to be propagated to existing records -- and the propagation cannot be automatic because you also need to potentially set
EmployeeNumber
.
回答3:
New Answer:
This( 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 ).
Old Answer:
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)
)
The above query selects all fields from User
and all fields from UserRole
(aliased as UserRole1). I assumed that the key field between the two fields is known as User.id and UserRole1.UserId
, please change these to whatever the real values are.
In the JOIN
part of the query there is an OR that on the left side requires an employee number not be NULL
in the user table and that UserRole1.role_name be in a list that you must supply to the IN ()
operator.
The right part of the JOIN
is the opposite, it requires that User.employee_number
be NULL
(this should be your non-employee set).
If you require a more exact solution then please provide more details on your table structures and what roles must be selected for employees.
来源:https://stackoverflow.com/questions/29673536/cross-table-constraints-in-postgresql