Wyjaśnienia
Sformułowanie tego wymagania pozostawia miejsce na interpretację:
gdzie UserRole.role_name
zawiera nazwę roli pracownika.
Moja interpretacja:
z wpisem w UserRole
który ma role_name = 'employee'
.
Twoja konwencja nazewnictwa jest był problematyczny (aktualizacja teraz). User
jest słowem zastrzeżonym w standardowym SQL i Postgresie. Jako identyfikator jest niedozwolony, chyba że jest cytowany w podwójnym cudzysłowie - co byłoby nierozsądne. Oficjalne nazwy użytkowników, dzięki czemu nie trzeba wstawiać podwójnego cudzysłowu.
W mojej implementacji używam bezproblemowych identyfikatorów.
Problem
FOREIGN KEY
i CHECK
ograniczenia to sprawdzone, hermetyczne narzędzia do wymuszania integralności relacyjnej. Wyzwalacze to potężne, przydatne i wszechstronne funkcje, ale bardziej wyrafinowane, mniej rygorystyczne i z większą ilością miejsca na błędy projektowe i przypadki narożne.
Twoja sprawa jest trudna, ponieważ na początku ograniczenie FK wydaje się niemożliwe:wymaga PRIMARY KEY
lub UNIQUE
ograniczenie do odniesienia — żadne nie zezwala na wartości NULL. Nie ma częściowych ograniczeń FK, jedyną ucieczką od ścisłej spójności referencyjnej są wartości NULL w odwołaniach kolumny z powodu domyślnego MATCH SIMPLE
zachowanie ograniczeń FK. Zgodnie z dokumentacją:
MATCH SIMPLE
zezwala na wartość null dowolnej kolumny klucza obcego; jeśli którykolwiek z nich ma wartość null, wiersz nie musi mieć dopasowania w tabeli, do której się odwołuje.
Powiązana odpowiedź na dba.SE z więcej:
- Dwukolumnowe ograniczenie klucza obcego tylko wtedy, gdy trzecia kolumna NIE JEST NULL
Obejściem tego problemu jest wprowadzenie flagi logicznej is_employee
aby oznaczyć pracowników po obu stronach, zdefiniowano NOT NULL
w users
, ale może być NULL
w user_role
:
Rozwiązanie
To wymusza Twoje wymagania dokładnie , ograniczając do minimum hałas i koszty ogólne:
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)
);
To wszystko.
Te wyzwalacze są opcjonalne, ale zalecane dla wygody, aby ustawić dodane tagi is_employee
automatycznie i nie musisz nic robić dodatkowo:
-- 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();
Ponownie, bez sensu, zoptymalizowany i wywoływany tylko w razie potrzeby.
Skrzypce SQL demo dla Postgresa 9.3. Powinien działać z Postgresem 9.1+.
Główne punkty
-
Teraz, jeśli chcemy ustawić
user_role.role_name = 'employee'
, musi istnieć pasującyuser.employee_nr
pierwszy. -
Nadal możesz dodać
employee_nr
do dowolnego użytkownika i nadal możesz (wtedy) otagować każdąuser_role
zis_employee
, niezależnie od rzeczywistejrole_name
. Łatwe do odrzucenia, jeśli zajdzie taka potrzeba, ale ta implementacja nie wprowadza więcej ograniczeń niż jest to wymagane. -
users.is_employee
może być tylkotrue
lubfalse
i jest zmuszony do odzwierciedlenia istnieniaemployee_nr
przezCHECK
ograniczenie. Wyzwalacz automatycznie synchronizuje kolumnę. Możesz zezwolić nafalse
dodatkowo do innych celów z niewielkimi aktualizacjami projektu. -
Zasady dla
user_role.is_employee
są nieco inne:musi to być prawda, jeślirole_name = 'employee'
. Wymuszone przezCHECK
ograniczenie i ponownie ustawiane automatycznie przez wyzwalacz. Ale można zmienićrole_name
do czegoś innego i nadal zachowajis_employee
. Nikt nie powiedział, że użytkownik zemployee_nr
jest wymagane mieć odpowiedni wpis wuser_role
, tylko na odwrót! W razie potrzeby można je dodatkowo łatwo wyegzekwować. -
Jeśli istnieją inne wyzwalacze, które mogą przeszkadzać, rozważ to:
Jak uniknąć zapętlania wywołań wyzwalaczy w PostgreSQL 9.2.1
Ale nie musimy się martwić, że reguły mogą zostać naruszone, ponieważ powyższe wyzwalacze są tylko dla wygody. Zasady same w sobie są egzekwowane za pomocąCHECK
i ograniczenia FK, które nie dopuszczają wyjątków. -
Na bok:umieszczam kolumnę
is_employee
najpierw w ograniczeniuUNIQUE (is_employee, users_id)
z jakiegoś powodu .users_id
jest już omówiony w PK, więc może zająć drugie miejsce tutaj:
Jednostki asocjacyjne DB i indeksowanie