Chiarimenti
La formulazione di questo requisito lascia spazio all'interpretazione:
dove UserRole.role_name contiene un nome di ruolo dipendente.
La mia interpretazione:
con una voce in UserRole che ha role_name = 'employee' .
La tua convenzione di denominazione è era problematico (aggiornato ora). User è una parola riservata in SQL standard e Postgres. È illegale come identificatore a meno che non sia doppiato, il che sarebbe sconsiderato. Nomi legali dell'utente in modo da non dover virgolette.
Sto utilizzando identificatori senza problemi nella mia implementazione.
Il problema
FOREIGN KEY e CHECK vincolo sono gli strumenti collaudati e ermetici per rafforzare l'integrità relazionale. I trigger sono funzionalità potenti, utili e versatili ma più sofisticate, meno rigide e con più spazio per errori di progettazione e casi angolari.
Il tuo caso è difficile perché un vincolo FK sembra impossibile all'inizio:richiede una PRIMARY KEY o UNIQUE vincolo di riferimento - nessuno dei due consente valori NULL. Non ci sono vincoli FK parziali, l'unica via di fuga dalla rigorosa integrità referenziale sono i valori NULL nel riferimento colonne a causa del valore predefinito MATCH SIMPLE comportamento dei vincoli FK. Per documentazione:
MATCH SIMPLE consente a qualsiasi colonna di chiave esterna di essere nulla; se qualcuno di essi è nullo, non è necessario che la riga abbia una corrispondenza nella tabella di riferimento.
Risposta correlata su dba.SE con altro:
- vincolo di chiave esterna a due colonne solo quando la terza colonna NON è NULL
La soluzione alternativa è introdurre un flag booleano is_employee per contrassegnare i dipendenti su entrambi i lati, definito NOT NULL in users , ma può essere NULL in user_role :
Soluzione
Questo fa rispettare i tuoi requisiti esattamente , riducendo al minimo rumore e spese generali:
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)
);
Questo è tutto.
Questi trigger sono facoltativi ma consigliati per comodità di impostare i tag aggiunti is_employee automaticamente e non devi fare nulla 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();
Ancora una volta, senza fronzoli, ottimizzato e chiamato solo quando necessario.
SQL Fiddle demo per Postgres 9.3. Dovrebbe funzionare con Postgres 9.1+.
Punti principali
-
Ora, se vogliamo impostare
user_role.role_name = 'employee', quindi deve esserci unuser.employee_nrcorrispondente prima. -
Puoi comunque aggiungere un
employee_nra qualsiasi utente e puoi (quindi) ancora taggare qualsiasiuser_roleconis_employee, indipendentemente dall'effettivorole_name. Facile da disabilitare se necessario, ma questa implementazione non introduce più restrizioni di quelle richieste. -
users.is_employeepuò essere solotrueofalseed è costretto a riflettere l'esistenza di unemployee_nrdalCHECKvincolo. Il trigger mantiene la colonna sincronizzata automaticamente. Potresti consentirefalseinoltre per altri scopi con solo piccoli aggiornamenti al design. -
Le regole per
user_role.is_employeesono leggermente diversi:deve essere vero serole_name = 'employee'. Applicato da unCHECKvincolo e impostato di nuovo automaticamente dal trigger. Ma è consentito cambiarerole_namea qualcos'altro e mantieni ancorais_employee. Nessuno ha detto un utente con unemployee_nrè richiesto per avere una voce corrispondente inuser_role, proprio il contrario! Ancora una volta, facile da applicare ulteriormente se necessario. -
Se ci sono altri trigger che potrebbero interferire, considera questo:
Come evitare il looping delle chiamate trigger in PostgreSQL 9.2.1
Ma non dobbiamo preoccuparci che le regole possano essere violate perché i trigger di cui sopra sono solo per comodità. Le regole di per sé vengono applicate conCHECKe vincoli FK, che non consentono eccezioni. -
A parte:metto la colonna
is_employeeprima nel vincoloUNIQUE (is_employee, users_id)per un motivo .users_idè già trattato nel PK, quindi può occupare il secondo posto qui:
Entità associative DB e indicizzazione