Vincoli di integrità e Trigger

13 / 100

In SQL è possibile specificare dei vincoli di integrità sui dati, sia quelli propri del modello relazionale che quelli che specificano le regole aziendali.

Vincoli di dominio. Essi vengono implicitamente specificati quando un attributo viene associato al corrispondente dominio: i valori dell’attributo devono appartenere al relativo dominio.

Obligatorietà degli attributi. In SQL, per default, un attributo (non facente parte della chiave primaria) è opzionale, cioè può assumere valori nulli. Per rendere obbligatorio un attributo, cioè per fare in modo che il suo valore sia sempre diverso da NULL, si usa il vincolo not null da usare come nel seguente esempio:

nome   varchar(20) not null 

Se il valore di un attributo obbligatorio non viene specificato in fase di inserimento di una riga, il DBMS genera un errore. Si noti che non è necessario specificare un valore, in fase di inserimento, per un attributo obbligatorio con valore di default diverso da NULL.

Chiave primaria. E’ possibile identificare una unica chiave primaria con il vincolo primary key da usare nel seguente modo:

create table dipendente
(
  cf               char(16) primary key, 
  nome             varchar(20), 
  cognome          varchar(20), 
) 

Se il vincolo coinvolge più attributi è possibile aggiungerlo dopo la definizione degli attributi come segue:

create table dipendente
(
  nome             varchar(20), 
  cognome          varchar(20), 
  dataDiNascita    date, 
  luogoDiNascita   varchar(20),
  primary key (nome, cognome) 
) 

Gli attributi della chiave primaria devono assumere valori univoci e devono essere obbligatori. Per loro non occorre specificare il vincolo di obligatorietà. Una violazione di questo vincolo genera un errore.

Chiavi candidate. E’ possibile specificare altre chiavi candidate con il vincolo unique. La specifica delle chiavi candidate avviene come per le chiavi primarie, ad esempio:

create table dipendente
(
  cf               char(16) primary key, 
  nome             varchar(20), 
  cognome          varchar(20),
  unique (nome, cognome) 
) 

Si noti che per la chiave primaria non occorre specificare il vincolo di univocità. I valori assunti da una chiave candidata devono essere univoci ma possono essere nulli. (a differenza di quanto accade per la chiave primaria). Una violazione di questo vincolo genera un errore.

Chiavi esterne. Il vincolo di chiave esterna (foreign key) coinvolge due tabelle (che possono essere due istanze della stessa tabella). La tabella che contiene la chiave esterna è detta secondaria (slave), mentre la tabella a cui fa riferimento la chiave esterna è detta principale (master). Gli attributi riferiti nella tabella principale devono formare una chiave candidata, di solito la chiave primaria. Il vincolo specifica che ogni valore non nullo della chiave esterna nella tabella secondaria deve corripondere ad un valore nella tabella principale. La definizione del vincolo di chiave esterna usa il costrutto foreign key come segue:

create table teatro
(
  nome             varchar(20) primary key, 
  indirizzo        varchar(40) not null,  
  email            varchar(20) unique
) 

create table biglietteria
(
  nome             varchar(20) primary key, 
  indirizzo        varchar(40) not null,
  teatro           varchar(20) foreign key references teatro(nome)
) 

Nell’esempio la tabella principale è teatro, la tabella secondaria è biglietteria e il vincolo lega l’attributo teatro di biglietteria all’attributo nome di teatro. Se il vincolo coinvolge più attributi è possibile specificarlo dopo la definizione degli attributi della tabella nel seguente modo:

foreign key(nomeDipendente, cognomeDipendente) 
references dipendente(nome, cognome)

A differenza degli altri vincoli di integrità discussi, una violazione di un vincolo di chiave esterna non genera necessariamente un errore. E’ infatti possibile stabilire diverse politiche per reagire ad una violazione. La violazione può avvenire in due modi:

  1. nella tabella secondaria, inserisco una nuova riga o modifico la chiave esterna. Si noti che la cancellazione di una riga dalla tabella secondaria non viola mai il vincolo di chiave esterna;
  2. nella tabella principale, cancello una riga o modifico la chiave riferita. Si noti che l’inserimento di una nuova riga dalla tabella principale non viola mai il vincolo di chiave esterna.

Nel primo caso viene sempre generato un errore. Nel secondo posso stabilire le delle politiche di reazione. Per le operazione di modifica vi sono le seguenti politiche:

  • cascade: il nuovo valore dell’attributo della tabella principale viene riportato su tutte le corrispondenti righe della tabella secondaria;
  • set null: alla chiave esterna della tabella secondaria viene assegnato il valore nullo al posto del valore modificato nella tabella principale;
  • set default: alla chiave esterna della tabella secondaria viene assegnato il corrispondente valore di default al posto del valore modificato nella tabella principale;
  • no action: nessuna azione viene intrapresa e viene generato un errore.

Per le operazione di cancellazione posso reagire come segue:

  • cascade: le corrispondenti righe della tabella secondaria vengono cancellate;
  • set null: alla chiave esterna della tabella secondaria viene assegnato il valore nullo al posto del valore cancellato nella tabella principale;
  • set default: alla chiave esterna della tabella secondaria viene assegnato il corrispondente valore di default al posto del valore cancellato nella tabella principale;
  • no action: nessuna azione viene intrapresa e viene generato un errore.

La sintassi per specificare queste politiche usa i costrutti on update e on delete come segue:

create table biglietteria
(
  nome             varchar(20) primary key, 
  indirizzo        varchar(40) not null,
  teatro           varchar(20) foreign key references teatro(nome)
                               on update cascade
                               on delete set null
) 

Come regola generale, per le modifiche si usa la politica on update cascade. Per le cancellazioni si usa la politica on delete cascade per chiavi esterne di tabelle che corrispondono a relazioni concettuali (ad esempio la tabella lavoro nel nostro caso di studio) oppure ad entità deboli (ad esempio la tabella prenotazione) e la politica on delete set null negli altri casi. La ragione è che nel primo caso vi è un forte collegamento tra la tabella master e la tabella slave e dunque una cancellazione nella tabella master dovrebbe provocare corrispondenti cancellazioni nella tabella slave.

Regole aziendali. Ulteriori vincoli, detti vincoli di integrità generici in quanto non legati al modello relazionale, sono quelli imposti dalle regole aziendali. Tali vincoli possono essere rappresentati in SQL in più modi: mediante il costrutto check nella definizione di una tabella, mediante le asserzioni, oppure attraverso l’uso di regole attive (trigger).

E’ bene chiarire in anticipo che i vincoli di integrità generici rappresentano un argomento contrastato. A differenza dei vincoli relazionali, gli strumenti per specificare vincoli generici non sono stabilmente inseriti nello standard SQL (ad esempio i trigger sono stati aggiunti sono nell’ultima versione di SQL dopo essere stati disponibili per molto tempo nei DBMS). Di conseguenza, mentre i vincoli tipici del modello relazionale sono supportati efficientemente da tutti i DBMS relazionali, gli strumenti per specificare vincoli generici variano notevolmente tra i vari DBMS disponibili e non sempre garantiscono l’efficienza del sistema risulante. E’ quindi fortemente consigliato accertarsi di quali siano e di come funzionino gli strumenti per vincoli generici supportati dal DBMS prescelto.

Il costrutto check permette di specificare, mediante una condizione come quella che può apparire nella clausola where di una interrogazione SQL, vincoli generici a livello di di tabella o, mediante le asserzioni, a livello di schema di base di dati.

Si noti che un uso indiscriminato di questi vincoli appesantisce il carico del sistema in quanto, solitamente, i DBMS garantiscono una implementazione efficiente solo per i vincoli propri del modello relazionale.

Vediamo alcuni esempi. Supponiamo di avere le tabelle dipendente, teatro e lavoro. La tabella lavoro associa i dipendenti ai relativi teatri. Un teatro può avere più dipendenti e un dipendente può lavorare per più teatri. Supponiamo di voler esprimere un vincolo massimo di partecipazione per un dipendente rispetto alla relazione lavoro: un dipendente non può lavorare per più di due teatri. Questo vincolo può essere specificato sulla tabella lavoro nel seguente modo:

create table teatro
(
  nome             varchar(20) primary key, 
  indirizzo        varchar(40) not null
) 

create table dipendente
(
  cf                     char(16) primary key, 
  nome                   varchar(20) not null, 
  cognome                varchar(20) not null,
  dataDiNascita          date,
  luogoDiNascita         varchar(30),
  capo                   char(16),
  foreign key capo references dipendente(cf)  
)

create table lavoro
(
  teatro              varchar(20), 
  dipendente          char(16),
  primary key(teatro, dipendente),
  foreign key teatro references teatro(nome),
  foreign key dipendente references dipendente(cf),
  check(2 >= (select count(*)
              from lavoro L
              where dipendente = L.dipendente))
) 

Il vincolo afferma che per ogni dipendente non ci possono essere più di due righe nella tabella lavoro, quindi più di due teatri per cui il dipendente lavoro. Nella query di definizione del vincolo si può usare il nome degli attributi sui quali si sta definenendo il vincolo (dipendente in questo caso).

Lo stesso vincolo può essere espresso mediante una asserzione. Solitamente viene scritta una interrogazione SQL che seleziona le righe della base di dati che violano il vincolo. La condizione dell’asserzione viene fomata mettendo tale interrogazione come argomento del predicano not exists. Dunque il vincolo di integrità specificato dall’asserzione è verificato che il risultato della interrogazione è vuoto, cioè se non esistono righe che violano il vincolo. Vediamo un esempio:

create assertion limitaImpieghi check (not exists(
  
  select dipendente
  from lavoro 
  group by dipendente
  having count(*) > 2

))

Supponiamo ora di voler affermare il seguente vincolo minimo di partecipazione per un dipendente rispetto alla relazione lavoro: ogni dipendente deve essere assunto presso almeno un teatro. Possiamo imporre questo vincolo sull’attributo cf della tabella dipendente:

create table dipendente
(
  cf                     char(16) primary key, 
  nome                   varchar(20) not null, 
  cognome                varchar(20) not null,
  dataDiNascita          date,
  luogoDiNascita         varchar(30),
  capo                   varchar(20),
  foreign key capo references dipendente(cf),
  check (cf in (select dipendente from lavoro))  
)

Lo stesso vincolo può essere espresso mediante una asserzione come segue:

create assertion disoccupato check (not exists (

  select cf
  from dipendente
  where cf not in (select dipendente from lavoro)

))

Vediamo un vincolo che coinvolge più attributi della stessa tabella. Ad esempio, supponiamo di voler affermare che i dipendenti nati a Milano devono essere nati prima del 1970. Possiamo riscrivere la definizione della tabella dipendente come segue:

create table dipendente
(
  cf                     char(16) primary key, 
  nome                   varchar(20) not null, 
  cognome                varchar(20) not null,
  dataDiNascita          date,
  luogoDiNascita         varchar(30),
  capo                   varchar(20),
  foreign key capo references dipendente(cf),
  check (luogoDiNascita <> "Milano" or 
         dataDiNascita < '1970-01-01')        
)

Vediamo un esempio di vincolo che coinvolge più tabelle. Supponiamo di voler specificare il seguente vincolo minimo di partecipazione: un teatro deve avere almeno 5 dipendenti. Possiamo scrivere la seguente asserzione:

create assertion vincoloDipendentiTeatro check (

  not exists (select nome
              from teatro
              where nome not in (select teatro from lavoro)) 
  
  and                           
  
  not exists (select count(*)
              from lavoro
              group by teatro
              having count(*) < 5)

)

Il vincolo asserisce che non esistono teatri privi di dipendenti e, tra quelli che hanno almeno un dipendente, non esistono teatri con meno di 5 dipendenti. Dunque tutti i teatri hanno almeno 5 dipendenti.

Inoltre, vediamo un vincolo sulla cardinalità di una tabella. La seguente asserzione afferma che ci devono essere almeno 3 teatri nella rete:

create assertion vincoloTeatriRete check (
  
  3 <= (select count(*) from teatro)

)

Le regole attive (trigger) permettono di gestire i vincoli di integrità. La differenza rispetto agli strumenti fin ora introdotti per specificare vincoli di integrità (relazionali o generici) è la seguente: un trigger specifica una azione da intraprendere qualora in vincolo non sia soddisfatto, solitamente una azione riparatrice della integrità violata.

Un trigger segue il paradigma evento-condizione-azione: se un certo evento si verifica, la relativa condizione viene controllata e, se soddisfatta, l’azione viene intrapresa. Un evento è solitamento un aggiornamento della base di dati (insert, update, delete). Una condizione è un predicato espresso in SQL. Una azione è una interrogazione SQL (solitamente di aggiornamento della base di dati) oppure una eccezione che annulla gli effetti dell’operazione che ha attivato il trigger riportando la base di dati allo stato precedente a tale operazione (rollback). Il trigger può essere attivato prima o dopo l’evento.

Si noti che ci possono essere più trigger associati ad un evento. L’ordine di esecuzione dei trigger in tal caso è gestito dal sistema e generalmente tiene conto dell’ordine di creazione dei trigger. Un trigger che come azione aggiorna lo stato della base di dati può a sua volta innescare altri trigger, che a loro volta possono attivare altri trigger, con la possibilità di avere reazioni a catena infinite. Inoltre, l’azione di un trigger può violare vincoli di integrità. La violazione di un vincolo di integrità di chiave esterna può causare, come conseguenza delle politiche di gestione di tali vincoli, ulteriori modifiche alla base di dati che al loro volta possono scatenare altri trigger, oppure violare altri vincoli di integrità. Si badi bene che la violazione di un vincolo di integrità non gestito, a qualsiasi livello della catena di attivazione, produce un annullamento degli effetti di tutte le operazioni innescate dalla primitiva madre che ha generato la catena di trigger, compresi gli effetti della primitiva madre stessa. I trigger sono dunque strumenti semplici da scrivere in modo indipendente ma difficili da gestire in modo integrato.

Supponiamo di voler specificare un trigger per il vincolo che afferma che lo stipendio di un dipendente non può essere incrementato più del 20%:

create trigger LimitaIncrementoStipendio
after update of stipendio on dipendente
for each row
when (New.stipendio > Old.Stipendio * 1.2)
update dipendente
set New.stipendio = Old.Stipendio * 1.2
where cf = New.cf

Il trigger LimitaIncrementoStipendio viene attivato dall’evento modifica (update) dello stipendio di un dipendende. Per ogni riga modificata, se il nuovo stipendio è stato incrementato più del 20% rispetto al vecchio (condizione when), allora lo stipendio viene incrementato del massimo possibile senza violare il vincolo di integrità. Si noti che è possibile usare le variabili di tupla New e Old per riferirsi, rispettivamente, alla tupla dopo e prima la modifica. Per gli eventi di inserimento, solo New è accessibile, per gli eventi di cancellazione, solo Old è accessibile.

Vediamo un altro esempio. Vogliamo modellare la regola che dice che una prenotazione per uno spettacolo può essere effettuata solo se vi sono ancora posti a disposizione in sala. Usiamo i seguenti quattro trigger:

create trigger disponibilità-1
after insert on messaInScena
for each row
update messaInScena
set postiDisponibili = (select capienza
                        from spazio
                        where nome = New.spazio) 
where (data = New.data and 
       ora = New.ora 
       spazio = New.spazio)

create trigger disponibilità-2
after insert on prenotazione
for each row
update messaInScena
set postiDisponibili = postiDisponibili - 1
where (data = New.dataSpettacolo and 
       ora = New.oraSpettacolo 
       spazio = New.spazioSpettacolo)
  

create trigger disponibilità-3
after delete on prenotazione
for each row
update messaInScena
set postiDisponibili = postiDisponibili + 1
where (data = Old.dataSpettacolo and 
       ora = Old.oraSpettacolo 
       spazio = Old.spazioSpettacolo)

create trigger disponibilità-4
before insert on prenotazione
for each row
when (0 = (select postiDisponibili
          from messaInScena
          where (data = New.dataSpettacolo and 
                 ora = New.oraSpettacolo 
                 spazio = New.spazioSpettacolo)))
rollback("Posti esauriti")

Per specificare la regola aziendale sui posti disponibili abbiamo usato i seguenti trigger:

  1. disponibilità-1, che imposta il numero di posti disponibili alla capienza dello spazio teatrale quando uno spettacolo viene inserito;
  2. disponibilità-2, che decrementa di uno i posti disponibili quando una prenotazione viene inserita;
  3. disponibilità-3, che incrementa di uno i posti disponibili quando una prenotazione viene cancellata;
  4. disponibilità-4, che controlla, prima dell’inserimento della prenotazione nella base di dati, se esistono posti disponibili. Se non ne esistono, esso annulla l’operazione di inserimento e avvisa l’utente che i posti sono esauriti.

Si noti che la soluzione funziona assumendo che disponibilità-4 venga eseguito prima di disponibilità-2 (di solito è così in quanto disponibilità-4 è di tipo before e disponibilità-2 è di tipo after).

I trigger sono anche utili per specificare le regole di calcolo degli attributi calcolati. Supponiamo che il prezzo ridotto di uno spettacolo debba essere scontato del 20% rispetto a quello intero. Dunque l’attributo prezzo ridotto è calcolato rispetto al prezzo intero. I trigger per gestire questo vincolo seguono:

create trigger CalcolaPrezzoRidottoInsert
after insert on messaInScena
for each row
update messaInScena
set New.prezzoRidotto = New.prezzoIntero * 0.8
where codice = New.codice

create trigger CalcolaPrezzoRidottoUpdate
after update of prezzoIntero on messaInScena
for each row
update messaInScena
set New.prezzoRidotto = New.prezzoIntero * 0.8
where codice = New.codice

Non tutti i vincoli di integrità possono essere descritti a livello di schema in SQL. Solitamente, quando un vincolo non è descrivibile in SQL, esso viene catturato a livello di applicazione implementandolo in qualche linguaggio di programmazione. E’ bene che tutti i vincoli esprimibili in SQL vengano definiti a livello di schema in modo da renderli condivisi da tutte le applicazioni invece che replicare il vincolo per ogni applicazione. In tal modo le modifiche di un vincolo sono gestite a livello di schema senza modificare le applicazioni. Si parla in tal caso di indipendenza dalla conoscenza, dove per conoscenza si intende l’insieme delle regole codificate nei vincoli che regolano l’integrità della base.

E’ possibile aggiungere e rimuovere vincoli di integrità definiti su una tabella mediante il comando alter. Per rimuovere un vincolo occorre averlo definito per nome mediante il costrutto constraint. Ad esempio:

create table dipendente
(
  cf                     char(16) primary key, 
  nome                   varchar(20) not null, 
  cognome                varchar(20) not null,
  indirizzo              varchar(30),
  constraint chiaveCandidata unique(nome, cognome)  
)

alter table dipendente drop constraint chiaveCandidata
alter table dipendente add constraint chiaveCandidata unique(indirizzo)

Per rimuovere una asserzione o un trigger occorre usare il comando drop seguito dal nome del costrutto.

Concludiamo la parte sulla definizione dei dati in SQL parlando brevemente del catalogo dei dati. Il catalogo dei dati è una base relazionale per archiviare lo schema fisico di una base di dati; tale base contiene una descrizione dei dati e non i dati veri e propri. Ad esempio, il catalogo dei dati contiene una tabella per gli attributi delle tabelle di uno schema fisico. Ogni riga della tabella specifica, tra l’altro, il nome dell’attributo, la tabella di appartenenza, il suo valore di default e l’obligatorietà.

Il catalogo dei dati viene solitamente mantenuto dal DBMS e non deve essere creato o modificato dall’utente. Il catalogo dei dati può però essere interrogato dall’utente. Questo offre la possibilità interessante di costruire interrogazioni che accedano sia ai dati che ai metadati. E’ bene che i dati e i metadati vengano organizzati nel medesimo modello dei dati (relazionale, ad oggetti, XML). In questo modo è possibile archiviare dati e metadati con le stesse strutture e interrogarli con lo stesso linguaggio. Questa caratteristica prende il nome di riflessività.

Translate »