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 SIMPLEtillå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_nrförst. -
Du kan fortfarande lägga till en
employee_nrtill alla användare, och du kan (då) fortfarande tagga vilkenuser_rolesom 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_employeekan bara varatrueellerfalseoch tvingas återspegla existensen av enemployee_nrmedCHECKbegränsning. Utlösaren håller kolumnen automatiskt synkroniserad. Du kan tillåtafalsedessutom 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 enCHECKbegränsning och ställs in automatiskt av utlösaren igen. Men det är tillåtet att ändrarole_nametill 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 medCHECKoch FK-begränsningar, som inte tillåter några undantag. -
Åt sidan:Jag lägger kolumnen
is_employeefö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