Afklaringer
Formuleringen af dette krav giver plads til fortolkning:
hvor UserRole.role_name
indeholder et medarbejderrollenavn.
Min fortolkning:
med en indtastning i UserRole
der har role_name = 'employee'
.
Din navngivningskonvention er var problematisk (opdateret nu). User
er et reserveret ord i standard SQL og Postgres. Det er ulovligt som identifikator, medmindre det er dobbelt-citeret - hvilket ville være urådigt. Bruger juridiske navne, så du ikke behøver at citere dobbelt.
Jeg bruger problemfrie identifikatorer i min implementering.
Problemet
FOREIGN KEY
og CHECK
constraint er de gennemprøvede, lufttætte værktøjer til at håndhæve relationel integritet. Udløsere er kraftfulde, nyttige og alsidige funktioner, men mere sofistikerede, mindre strenge og med mere plads til designfejl og hjørnekasser.
Din sag er vanskelig, fordi en FK-begrænsning umiddelbart virker umulig:den kræver en PRIMARY KEY
eller UNIQUE
begrænsning til reference - hverken tillader NULL-værdier. Der er ingen delvise FK-begrænsninger, den eneste undslippe fra streng referenceintegritet er NULL-værdier i henvisningen kolonner på grund af standarden MATCH SIMPLE
opførsel af FK-begrænsninger. Per dokumentation:
MATCH SIMPLE
tillader enhver af de fremmede nøglekolonner at være nul; hvis nogen af dem er nul, er rækken ikke forpligtet til at have en match i den refererede tabel.
Relateret svar på dba.SE med mere:
- Kun begrænsning af fremmednøgle med to kolonner, når tredje kolonne IKKE er NULL
Løsningen er at introducere et boolesk flag is_employee
at markere medarbejdere på begge sider, defineret NOT NULL
i users
, men tilladt at være NULL
i user_role
:
Løsning
Dette håndhæver dine krav præcis , mens støj og overhead holdes på et 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)
);
Det er alt.
Disse triggere er valgfrie, men anbefales for nemheds skyld for at indstille de tilføjede tags is_employee
automatisk, og du behøver ikke at gøre noget ekstra:
-- 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();
Igen, no-nonsense, optimeret og kun ringet op, når det er nødvendigt.
SQL Fiddle demo til Postgres 9.3. Bør fungere med Postgres 9.1+.
Vigtige punkter
-
Hvis vi nu vil indstille
user_role.role_name = 'employee'
, så skal der være en matchendeuser.employee_nr
først. -
Du kan stadig tilføje en
employee_nr
til enhver bruger, og du kan (så) stadig tagge enhveruser_role
medis_employee
, uanset det faktiskerole_name
. Let at afvise, hvis du har brug for det, men denne implementering indfører ikke flere begrænsninger end nødvendigt. -
users.is_employee
kan kun væretrue
ellerfalse
og er tvunget til at afspejle eksistensen af enemployee_nr
vedCHECK
begrænsning. Udløseren holder automatisk kolonnen synkroniseret. Du kan tilladefalse
desuden til andre formål med kun mindre opdateringer af designet. -
Reglerne for
user_role.is_employee
er lidt anderledes:det skal være sandt, hvisrole_name = 'employee'
. Håndhæves af enCHECK
begrænsning og indstilles automatisk af udløseren igen. Men det er tilladt at ændrerole_name
til noget andet og stadig beholdeis_employee
. Ingen sagde en bruger med enemployee_nr
er påkrævet at have en tilsvarende post iuser_role
, bare omvendt! Igen, let at håndhæve yderligere, hvis det er nødvendigt. -
Hvis der er andre triggere, der kan forstyrre, så overvej dette:
Sådan undgår du sløjfer af triggeropkald i PostgreSQL 9.2.1
Men vi behøver ikke bekymre os om, at reglerne kan blive overtrådt, fordi ovenstående triggere kun er for nemheds skyld. Reglerne i sig selv håndhæves medCHECK
og FK-begrænsninger, som ikke tillader undtagelser. -
Til side:Jeg lægger kolonnen
is_employee
først i begrænsningenUNIQUE (is_employee, users_id)
af en grund .users_id
er allerede dækket af PK, så den kan indtage andenpladsen her:
DB-associative entiteter og indeksering