PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Vincoli di tabelle incrociate in PostgreSQL

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 un user.employee_nr corrispondente prima.

  • Puoi comunque aggiungere un employee_nr a qualsiasi utente e puoi (quindi) ancora taggare qualsiasi user_role con is_employee , indipendentemente dall'effettivo role_name . Facile da disabilitare se necessario, ma questa implementazione non introduce più restrizioni di quelle richieste.

  • users.is_employee può essere solo true o false ed è costretto a riflettere l'esistenza di un employee_nr dal CHECK vincolo. Il trigger mantiene la colonna sincronizzata automaticamente. Potresti consentire false inoltre per altri scopi con solo piccoli aggiornamenti al design.

  • Le regole per user_role.is_employee sono leggermente diversi:deve essere vero se role_name = 'employee' . Applicato da un CHECK vincolo e impostato di nuovo automaticamente dal trigger. Ma è consentito cambiare role_name a qualcos'altro e mantieni ancora is_employee . Nessuno ha detto un utente con un employee_nr è richiesto per avere una voce corrispondente in user_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 con CHECK e vincoli FK, che non consentono eccezioni.

  • A parte:metto la colonna is_employee prima nel vincolo UNIQUE (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