Пояснения
Формулирането на товаизискване оставя място за интерпретация:
където 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