Förtydliganden
Formuleringen av detta krav lämnar utrymme för tolkning:
där UserRole.role_name
innehåller ett anställds rollnamn.
Min tolkning:
med en post i UserRole
som har role_name = 'employee'
.
Din namnkonvention är var problematisk (uppdaterad nu). User
är ett reserverat ord i standard SQL och Postgres. Det är olagligt som identifierare såvida det inte är dubbelt citerat - vilket skulle vara felaktigt. Användarrättsliga namn så att du inte behöver dubbelcitera.
Jag använder problemfria identifierare i min implementering.
Problemet
FOREIGN KEY
och CHECK
begränsning är de beprövade, lufttäta verktygen för att upprätthålla relationsintegritet. Utlösare är kraftfulla, användbara och mångsidiga funktioner men mer sofistikerade, mindre strikta och med mer utrymme för designfel och hörnfall.
Ditt fall är svårt eftersom en FK-begränsning verkar omöjlig till en början:den kräver en PRIMARY KEY
eller UNIQUE
begränsning till referens - varken tillåter NULL-värden. Det finns inga partiella FK-begränsningar, den enda flykten från strikt referensintegritet är NULL-värden i referensen kolumner på grund av standardvärdet MATCH SIMPLE
FK-begränsningarnas beteende. Per dokumentation:
MATCH SIMPLE
tillåter vilken som helst av kolumnerna för främmande nyckel att vara null; om någon av dem är null krävs inte att raden matchar den refererade tabellen.
Relaterat svar på dba.SE med mer:
- Tvåkolumns främmande nyckelbegränsning endast när den tredje kolumnen INTE är NULL
Lösningen är att introducera en boolesk flagga is_employee
för att markera anställda på båda sidor, definierad NOT NULL
i users
, men får vara NULL
i user_role
:
Lösning
Detta upprätthåller dina krav exakt , samtidigt som buller och overhead hålls till ett 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 är allt.
Dessa utlösare är valfria men rekommenderas för enkelhets skull för att ställa in de tillagda taggarna is_employee
automatiskt och du behöver inte göra något extra:
-- 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();
Återigen, no-nonsense, optimerad och bara anropad när det behövs.
SQL Fiddle demo för Postgres 9.3. Bör fungera med Postgres 9.1+.
Huvudpunkter
-
Om vi nu vill ställa in
user_role.role_name = 'employee'
, då måste det finnas en matchandeuser.employee_nr
först. -
Du kan fortfarande lägga till en
employee_nr
till alla användare, och du kan (då) fortfarande tagga vilkenuser_role
som helst medis_employee
, oavsett det faktiskarole_name
. Lätt att tillåta om du behöver, men den här implementeringen introducerar inte fler begränsningar än vad som krävs. -
users.is_employee
kan bara varatrue
ellerfalse
och tvingas återspegla existensen av enemployee_nr
medCHECK
begränsning. Utlösaren håller kolumnen automatiskt synkroniserad. Du kan tillåtafalse
dessutom för andra ändamål med endast mindre uppdateringar av designen. -
Reglerna för
user_role.is_employee
är något annorlunda:det måste vara sant omrole_name = 'employee'
. Tillämpas av enCHECK
begränsning och ställs in automatiskt av utlösaren igen. Men det är tillåtet att ändrarole_name
till något annat och fortfarande behållais_employee
. Ingen sa en användare med enemployee_nr
är obligatoriskt att ha en motsvarande post iuser_role
, precis tvärtom! Återigen, lätt att genomdriva om det behövs. -
Om det finns andra utlösare som kan störa, överväg detta:
Hur man undviker att utlösa utlösare i PostgreSQL 9.2.1
Men vi behöver inte oroa oss för att reglerna kan överträdas eftersom ovanstående utlösare bara är för bekvämlighets skull. Reglerna i sig tillämpas medCHECK
och FK-begränsningar, som inte tillåter några undantag. -
Åt sidan:Jag lägger kolumnen
is_employee
först i begränsningenUNIQUE (is_employee, users_id)
av en anledning .users_id
är redan täckt i PK, så det kan ta andra plats här:
DB-associativa enheter och indexering