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.
The formulation of this requirement leaves room for interpretation:
"where user_roles.role_name
contains an employee role name."
My interpretation:
"with an entry in user_roles
that has role_name = 'employee'
."
FOREIGN KEY
and CHECK
constraints are proven, air-tight tools to enforce relational integrity. Triggers are powerful, versatile features but more sophisticated, less strict, and with more room for design errors and corner cases.
A FK constraint seems impossible for your case 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 referencing columns - with default MATCH SIMPLE
behavior of FK constraints. The manual:
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.
See:
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_roles
:
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_roles (
user_roles_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.
The following 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
LANGUAGE plpgsql AS
$func$
BEGIN
NEW.is_employee := (NEW.employee_nr IS NOT NULL);
RETURN NEW;
END
$func$;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF employee_nr ON users
FOR EACH ROW
EXECUTE FUNCTION trg_users_insup_bef();
-- user_roles
CREATE OR REPLACE FUNCTION trg_user_roles_insup_bef()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
NEW.is_employee = true;
RETURN NEW;
END
$func$;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF role_name ON user_roles
FOR EACH ROW
WHEN (NEW.role_name = 'employee')
EXECUTE FUNCTION trg_user_roles_insup_bef();
No-nonsense, optimized and only called when needed.
(Before Postgres 11, use the (misleading) key word PROCEDURE
. instead,)
fiddle
Old sqlfiddle. (Works with ancient Postgres 9.1, too.)
Now, if we want to set user_roles.role_name = 'employee'
, then there has to be a matching user.employee_nr
first.
You can still add an employee_nr
to any user, and you can (then) still tag any user_roles
with is_employee
, regardless of the actual role_name
. Easy to disallow if you need to, but this implementation does not introduce any more restrictions than required.
users.is_employee
can only be true
or false
and is forced to reflect the existence of an employee_nr
by the CHECK
constraint. The trigger keeps the column in sync automatically. You could allow false
additionally for other purposes with only minor updates to the design.
The rules for user_roles.is_employee
are slightly different: it must be true
if role_name = 'employee'
. Enforced by a CHECK
constraint and set automatically by the trigger again. But it's allowed to change role_name
to something else and still keep is_employee
. Nobody said a user with an employee_nr
is required to have an according entry in user_roles
, just the other way round! Again, easy to enforce additionally if needed.
If there are other triggers that could interfere, consider:
But we need not worry that rules might be violated by the triggers. The requested rules are enforced with CHECK
and FK constraints, which allow no exceptions.
Aside: I put the column is_employee
first in the constraint UNIQUE (is_employee, users_id)
for a reason. users_id
is already covered in the PK, so it can take second place here: