Пояснения
Формулирането на товаизискване оставя място за интерпретация:
където UserRole.role_name
съдържа име на ролята на служител.
Моята интерпретация:
със запис в UserRole
който има role_name = 'employee'
.
Вашата условност за именуване е беше проблематично (актуализирано сега). User
е запазена дума в стандартния SQL и Postgres. Незаконно е като идентификатор, освен ако не е в двойни кавички - което би било лошо. Юридически имена на потребители, така че да не се налага да цитирате двойни.
Използвам безпроблемни идентификатори в своята реализация.
Проблемът
FOREIGN KEY
и CHECK
ограниченията са доказаните, херметични инструменти за налагане на целостта на връзката. Тригерите са мощни, полезни и гъвкави функции, но по-сложни, по-малко строги и с повече място за грешки в дизайна и ъглови случаи.
Вашият случай е труден, защото FK ограничение изглежда невъзможно в началото:изисква PRIMARY KEY
или UNIQUE
ограничение за препратка - нито едно от двете не позволява NULL стойности. Няма частични FK ограничения, единственото избягване от строга референтна цялост са NULL стойности в препратката колони поради подразбиране MATCH SIMPLE
поведение на FK ограниченията. По документация:
MATCH SIMPLE
позволява всяка от колоните с външния ключ да бъде нула; ако някой от тях е нулев, не се изисква редът да има съвпадение в посочената таблица.
Свързан отговор на dba.SE с още:
- Ограничение за външен ключ от две колони само когато третата колона НЕ е NULL
Заобиколното решение е да се въведе булев флаг 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
автоматично и не е нужно да правите нищо допълнително:
-- 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_role
сis_employee
, независимо от действителнотоrole_name
. Лесно се забранява, ако е необходимо, но тази реализация не въвежда повече ограничения от необходимото. -
users.is_employee
може да бъде самоtrue
илиfalse
и е принуден да отразява съществуването наemployee_nr
чрезCHECK
ограничение. Тригерът поддържа колоната в синхрон автоматично. Можете да разрешитеfalse
освен това за други цели само с незначителни актуализации на дизайна. -
Правилата за
user_role.is_employee
са малко по-различни:трябва да е вярно, акоrole_name = 'employee'
. Наложено чрезCHECK
ограничение и се задава автоматично от спусъка отново. Но е позволено да се промениrole_name
към нещо друго и пак запазвайтеis_employee
. Никой не е казал потребител сemployee_nr
е задължителен да има съответен запис вuser_role
, точно обратното! Отново, лесно да се наложи допълнително, ако е необходимо. -
Ако има други тригери, които биха могли да попречат, помислете за това:
Как да избегнете повторни извиквания на тригери в PostgreSQL 9.2.1
Но не трябва да се притесняваме, че правилата може да бъдат нарушени, защото горните тригери са само за удобство. Правилата сами по себе си се прилагат сCHECK
и FK ограничения, които не позволяват изключения. -
Настрана:Сложих колоната
is_employee
първо в ограничениетоUNIQUE (is_employee, users_id)
по причина .users_id
вече е обхванат в PK, така че може да заеме второ място тук:
Асоциативни обекти и индексиране на DB