B Sperimentazioni con le basi di dati Illustreremo adesso con esempi i principi di utilizzazione di un DBMS. Come abbiamo già visto, questi sistemi vengono incontro al problema di gestire in modo efficace ed efficiente grandi quantità di dati strutturati residenti nella memoria di massa. Affinché ciò avvenga, occorre però che il progettista esamini con attenzione il problema a lui sottoposto e lo esprima successivamente in modo corretto nel DBMS a disposizione. La prima fase prende il nome di “analisi dei requisiti”; in essa viene prevista anche la lista delle possibili domande da porre al sistema, come abbiamo mostrato nella metafora di mago Merlino. Non vogliamo soffermarci sulla prima fase, e supponiamo che il programmatore di basi di dati conosca completamente i requisiti del suo problema. Ci concentriamo piuttosto su come questo possa essere trasferito su un sistema di gestione relazionale e più precisamente sul programma Microsoft Access, di cui adotteremo anche la terminologia della versione italiana. Alla fine del capitolo potremo provare a costruirci una piccola base di dati per la nostra videoteca, o per i nostri file MP3 che abbiamo scaricato dalla Rete, o anche per l’armadietto dei nostri medicinali. E saremo in grado di capire che eventuali errori in documenti che ci riguardano non si sono verificati per “colpa del computer” e spesso neanche per colpa di chi lo utilizza: possono derivare invece da una programmazione superficiale di strumenti sofisticati sì, ma, proprio per questo, da usare con attenzione. 2 B.1 Capitolo B DDL in Access Un sistema relazionale come Access fornisce una notazione per definire i dati (DDL), assegnare i valori, interrogare le tabelle (DML) e stabilire dei privilegi d’uso (DCL). Alla sua apertura propone all’utente la scelta del database su cui agire. Se questo non esiste, deve essere definito e memorizzato prima che si possa usarlo. Supponiamo allora di assegnare il nome “Ferrovia” a un nuovo database. Il sistema ci consente, con la sua schermata iniziale (Figura B.1), di generare tabelle, query, maschere, report, pagine, ecc. Di tutte queste parti, quelle veramente legate al modello relazionale sono le tabelle e le query. B.1.1 Definizione delle tabelle Scegliamo l’area delle tabelle e attiviamo il pulsante “Crea una tabella in visualizzazione Struttura”, evidenziato in Figura B.1. Si presenta davanti a noi un’interfaccia a due pannelli: in quello superiore è possibile assegnare il nome e il tipo agli attributi, mentre in quello inferiore ci sono gli strumenti per scegliere le loro proprietà. Riportiamo in Figura B.2 la struttura che abbiamo dato alla tabella personale. I nomi degli attributi sono autoesplicativi; con RESPONSABILE abbiamo indicaRelazioni Figura B.1 Sperimentazioni con le basi di dati Chiave primaria Indici 3 Proprietà Figura B.2 Interfaccia per la definizione delle tabelle. to la matricola del responsabile immediato della persona presa in considerazione. Il triangolino nero, che nella figura in discussione è posto sulla seconda riga, evidenzia che le “Proprietà campo” della parte inferiore riguardano esattamente quell’attributo (COGNOME). A fianco del campo MATRICOLA notiamo il simbolo di chiave che è stato immesso selezionando il campo e successivamente cliccando sull’icona Chiave primaria (evidenziata nella Figura B.2 con un indicatore). Non abbiamo definito l’attributo MATRICOLA di tipo numerico, in quanto si tratta solo di un codice su cui non avrà senso fare operazioni algebriche. Poiché abbiamo dichiarato tale campo “chiave primaria” il sistema ci impedirà di ripetere un valore già immesso. Inoltre, come tutti gli attributi che fanno parte di una chiave, MATRICOLA non può assumere il valore NULL. Si tratta di un segnale di valore mancante (non conosciuto) o inesistente (senza significato per la particolare ennupla). In concreto, un campo ha valore NULL se non abbiamo immesso alcun valore. Anche se a livello di visualizzazione i risultati sono identici, per il sistema esiste invece una radicale differenza se abbiamo immesso degli spazi bianchi con la barra spaziatrice. Il valore NULL ha un’interessante conseguenza sulla logica che, invece di essere booleana, come siamo abituati 4 Capitolo B solitamente, è a tre valori: VERO , FALSO , SCONOSCIUTO (risultato di un confronto tra un NULL ed un altro qualsiasi valore). Nella definizione per il campo COGNOME abbiamo immesso il valore “Sì” sulla proprietà Richiesto. Questa scelta impedisce per l’attributo il valore NULL, che potrebbe derivare da un errore (il campo non viene riempito) durante l’immissione dei dati. Abbiamo scelto il valore “No” per la proprietà Consenti lunghezza zero, che altrimenti permetterebbe l’inserimento di “” (o di una sequenza di spazi), visualizzato dal sistema come nel caso del valore NULL. Degli attributi restanti mettiamo in evidenza il campo RESPONSABILE, in quanto dobbiamo permettere il valore NULL . Occorre infatti prevedere che la persona di cui stiamo immettendo i dati possa non avere un superiore. Non è questo l’unico modo per creare le tabelle. Ne esiste un altro ad interfaccia testuale, molto più vicino allo standard SQL. Nella schermata iniziale di Figura B.1, invece di scegliere l’area delle tabelle si sceglie l’area delle query e successivamente il pulsante Crea una query in visualizzazione Struttura. Tra le finestre generate adesso dal sistema occorre chiudere quella dal nome Mostra tabella per poi scegliere Visualizzazione SQL dal menu Visualizza come illustrato nella Figura B.3. Possiamo digitare la seguente istruzione: CREATE TABLE nometabella ( MATRICOLA CHAR (50), COGNOME CHAR (50) NOT NULL, NOME CHAR (50) NOT NULL, DATA_NASCITA DATE NOT NULL, MANSIONE CHAR (50) NOT NULL, DATA_ASSUNZIONE DATE NOT NULL, STIPENDIO INTEGER NOT NULL, PREMIO_PRODUZIONE INTEGER NOT NULL, STAZIONE_APP CHAR (50) NOT NULL, RESPONSABILE CHAR (50), PRIMARY KEY (MATRICOLA)); SQL Figura B.3 Menu per la creazione delle query. Esegui Sperimentazioni con le basi di dati 5 La sua esecuzione, ottenuta premendo il pulsante Esegui evidenziato in Figura B.3 e rappresentato dal punto esclamativo, genera una tabella simile alla precedente. Ritornando nell’ambiente Tabelle, ne vediamo infatti una dal nome “nometabella”. B.1.2 Definizione degli indici Per gestire la chiave primaria, il sistema crea automaticamente un indice. Si tratta, in generale, di una struttura che rallenta le fasi di inserimento, ma rende più rapida la consultazione della base di dati. Il DDL consente la definizione di ulteriori indici su singoli attributi e su insiemi di essi. Saranno quelli su cui pensiamo che saranno effettuate le ricerche. Definiamo per la nostra tabella l’indice “Cognome_Nome” costituito dai due campi NOME e COGNOME. A tale scopo si usa il pulsante Indici evidenziato in Figura B.2, col quale si apre la finestra di creazione. Qui possiamo inserire il nome dell’indice e i campi di cui esso è composto. La nostra realizzazione è mostrata nella Figura B.4, dove abbiamo impostato a “No” la proprietà Univoco dell’indice per permettere l’inserimento di persone che hanno cognomi e nomi uguali. Per avere un’idea funzionale di un indice, paragoniamolo a quello analitico che conclude spesso un libro cartaceo (anche il presente). La sua struttura è ordinata su parole prestabilite con l’indicazione precisa delle pagine del testo che le contengono, in modo che il lettore possa trovarle immediatamente. Il sistema di basi di dati organizza e usa quelli che dichiariamo indici, proprio come noi facciamo con l’indice analitico: costruisce l’elenco dei valori assunti dall’attributo indicizzato e lo completa con i puntatori opportuni. Si tratta solo di un’analogia a grandi linee. Figura B.4 Interfaccia per la definizione degli indici. 6 Capitolo B B.1.3 Definizione dei vincoli Limitiamoci in questo paragrafo a una sola tabella e vediamo come si definiscono in Access i vincoli: ■ di chiave; ■ sui singoli attributi; ■ su più attributi della stessa tabella. Vincoli di chiave La loro definizione consiste nel dichiarare la chiave primaria e le chiavi candidate per evitare duplicazioni. Per la chiave primaria si indicano i campi con la tecnica usuale di Windows e si sceglie il pulsante con il simbolo di chiave, come visto nell’esempio precedente con il campo MATRICOLA. Per una chiave candidata il procedimento è più complesso, non essendo previsto un meccanismo immediato. Si procede innanzitutto settando le prorpietà dei suoi attributi in modo da impedire che assumano il valore NULL . Poi si crea un indice, come mostrato precedentemente, ma impostando a “Sí” la proprietà Univoco dell’indice. Nel nostro esempio di Figura B.4 definiamo chiave candidata l’insieme degli attributi COGNOME, NOME e DATA_NASCITA e le assegnano il nome “Chiave_candidata”. Se la chiave primaria è costituita da molti attributi è spesso conveniente dichiararla candidata e sostituirla con una chiave ad attributo singolo sfruttando eventualmente il contatore offerto dal sistema. Vincoli sui singoli attributi Osserviamo che è necessario stabilire il tipo di valore previsto per un campo che viene predefinito testuale dal sistema. Possiamo essere più restrittivi usando il campo Valido se nel pannello inferiore della Figura B.2. Esempio B.1 Uno stipendio deve essere compreso tra 900,01 Euro e 17999,99 Euro. Tale vincolo si esprime nella struttura della tabella indicando sull’attributo STIPENDIO la condizione: Sperimentazioni con le basi di dati 7 Vincoli su più attributi della stessa tabella Si possono esplicitare altri tipi di legami tra più attributi di una stessa tabella, oltre a quelli (univocità, richiesto) esprimibili tramite gli indici. In Access il vincolo si esprime attivando l’icona Proprietà riferita alla tabella evidenziata in Figura B.2. Sulla finestra attivata (vedi Figura B.5) ci posizioniamo sulla proprietà Valido se e scriviamo la condizione opportuna. Figura B.6 Interfaccia per la definizione delle proprietà della tabella. Esempio B.2 La ditta assume solo persone di età superiore ai 18 anni e inferiore ai 35. In questo caso, nella riga Valido se scriviamo: (DateDiff("aaaa";DATA_DI_NASCITA;DATA_ASSUNZIONE)>18) and (DateDiff("aaaa";DATA_DI_NASCITA; DATA_ASSUNZIONE)<35) Qui si usa una funzione offerta dall’ambiente, che permette di effettuare una differenza tra le date in anni, espressa mediante il parametro “aaaa”, e quelle in giorni e in settimane, espressa con altri parametri. Questi vincoli vengono controllati dal sistema solo in caso di immissione dati. Ma ci sono casi alquanto particolari. Ipotizziamo di aggiungere un campo ANZIANITA per esprimere l’età lavorativa. Dobbiamo allora imporre il vincolo che il suo valore sia uguale alla differenza tra la data attuale (del sistema) e la data di assunzione. Dovremmo scrivere, come mostrato in Figura B.6, nel campo Valido se: [ANZIANITA]=DateDiff(“aaaa”;[DATA_ASSUNZIONE];Date()) Il sistema controlla che gli inserimenti siano corretti, ma cosa accadrà l’anno seguente? La colonna ANZIANITA conterrà valori errati. Questi 8 Capitolo B Figura B.7 Un uso scorretto di vincoli. vincoli su valori dinamici devono essere trattati diversamente: considereremo in un momento successivo gli attributi calcolati. B.1.4 Osservazioni sui vincoli Fermiamoci un momento a riflettere sulla differenza tra l’aspetto teorico e quello pratico nella definizione dei vincoli. I vincoli che abbiamo espresso sui domini degli attributi possono essere teoricamente sufficienti, ma in concreto non lo sono in quanto non difendono la base di dati da banali refusi umani nell’immissione dati o da modi diversi di scrivere una stessa informazione. Inoltre, possiamo attribuire una stessa matricola a una persona diversa o addirittura immetterne una non esistente. Possiamo scrivere nel campo MANSIONE “Capo Stazione” oppure “Capostazione” che per il sistema sono dati diversi. Dobbiamo allora cercare di esprimere vincoli che permettano al sistema di controllare il più possibile la situazione. Le dipendenze tra gli attributi possono esserci d’aiuto per garantire la correttezza delle immissioni. Nello schema COD_FISC COGNOME NOME SESSO DATA_NASCITA COMUNE_NASCITA è perfetto dichiarare COD_FISC chiave primaria. La chiave, in questo caso, oltre ad essere univoca, può essere dedotta e dunque, il sistema può controllare che il valore calcolato sia uguale a quello immesso prima di Sperimentazioni con le basi di dati 9 accettare che l’ennupla entri a far parte della tabella. Questo è un caso particolarmente favorevole: è compito del progettista cercare soluzioni ottimali. B.2 Le interrogazioni Torniamo alla struttura della tabella personale, rappresentata in Figura B.2, per introdurre il linguaggio di interrogazione tramite il quale possiamo estrarre l’informazione che riteniamo utile. Ipotizziamo di usare la base di dati Ferrovie (presente sul sito). Per effettuare un’interrogazione, dal menu principale attiviamo l’icona “Query”. Il sistema presenta una serie di scelte la più generale delle quali è “Crea una query in visualizzazione Struttura”. L’utilizzo di questa funzionalità ci mostra una finestra con l’elenco delle nostre tabelle. Scegliamo solo la tabella personale e operiamo in modo da ottenere quanto rappresentato in Figura B.7. Possiamo memorizzare la query dandole un nome e eseguirla in una fase successiva oppure possiamo eseguirla immediatamente tramite l’icona Esegui, rappresentata (si veda la Figura B.3) da un punto esclamativo. Si ottengono tutte le ennuple della tabella. Dal menu Visualizza della stessa figura scegliamo di vedere l’interrogazione espressa in SQL: troveremo Campo Figura B.8 Semplice interrogazione. 10 Capitolo B SELECT personale.* FROM personale; Di solito l’utente è portato ad usare l’interfaccia grafica, data la sua immediatezza. Noi, invece, preferiamo presentare le interrogazioni SQL, per dare al lettore uno strumento di più ampio raggio, e non limitato ad un solo tipo di interfaccia grafica; il nostro obiettivo è mirato anche ad ottenere una scrittura compatta. Il lettore è invitato a sperimentare sull’interfaccia grafica di Access le interrogazioni presentate. B.2.1 Le interrogazioni su una tabella con la clausola WHERE Cominciamo ad illustrare il linguaggio di interrogazione partendo dalla struttura semplificata SELECT lista attributi FROM tabella WHERE condizione La condizione della clausola WHERE può usare gli operatori logici AND di relazione =, <, >, <= ecc, l’operatore LIKE con i caratteri wildcard per combinare i paragoni e altri operatori. OR NOT , Esempio B.3 Vogliamo visualizzare il cognome degli impiegati che lavorano nella stazione di Pisa. SELECT personale.COGNOME FROM personale WHERE personale.STAZIONE_APP = "Pisa C.le" La sintassi sopra riportata è quella che Access genera automaticamente in seguito alle nostre azioni sull’interfaccia grafica. Come si vede, il sistema usa la notazione puntata nometabella.nomecampo per individuare univocamente l’attributo. Lo standard SQL accetta anche la notazione senza il punto. Nel presente paragrafo useremo quest’ultima perchè i nostri esempi si svolgono su una sola tabella. La filosofia del sistema Access assimila le interrogazioni alle viste: quindi consente di cambiare i risultati delle interrogazioni e, di conseguenza, aggiorna le tabelle primarie da cui essi provengono. Se cambiamo la stazione di appartenenza, nel risultato della query precedente troviamo la modifica nella tabella personale! Sperimentazioni con le basi di dati 11 Esempio B.4 Vogliamo visualizzare gli impiegati che lavorano nelle stazioni di Pisa e di Firenze. SELECT COGNOME, STAZIONE_APP FROM personale WHERE (STAZIONE_APP = "Pisa C.le") OR (STAZIONE_ APP = "Firenze"); La condizione WHERE si può anche esprimere con notazione insiemistica WHERE STAZIONE_APP IN (“Pisa C.le” ,”Firenze”) Esempio B.5 Vogliamo visualizzare il personale ordinato secondo il “peso” della busta paga. Usiamo la clausola ORDER BY e l’opzione DESC (l’opzione ASC darebbe l’ordinamento opposto): SELECT * FROM personale ORDER BY STIPENDIO + PREMIO_PRODUZIONE DESC Esempio B.6 Vogliamo visualizzare le qualifiche del personale in servizio presso la stazione di Pisa. SELECT MANSIONE FROM personale WHERE STAZIONE_APP="Pisa C.le"; Questa interrogazione produce duplicati che vengono soppressi grazie alla parola chiave DISTINCT che compare nell’interrogazione seguente. SELECT DISTINCT MANSIONE FROM personale WHERE STAZIONE_APP="Pisa C.le"; Evidenziamo che negli esempi seguenti useremo le funzioni di aggregazione COUNT e SUM . Esistono altre funzioni di aggregazione: MIN , MAX, AVG . Per il loro uso nella interfaccia grafica occorre attivare il pulsante Totali evidenziato nella Figura B.3. 12 Capitolo B Esempio B.7 Si chiede il costo di tutto il personale: SELECT SUM(STIPENDIO) AS TOTALE_COSTI FROM personale; In questo esempio, tramite la funzione SUM abbiamo ottenuto la somma totale sulla colonna STIPENDIO. Poiché il risultato avrebbe avuto un’intestazione poco significativa, abbiamo usato la parola chiave AS. In questo modo abbiamo dichiarato un nuovo identificatore TOTALE_COSTI, di cui il sistema si servirà nel presentare la risposta. Esempio B.8 Si chiede il numero totale dei dipendenti. SELECT COUNT(*) AS TOTALE_DIPENDENTI FROM personale; B.2.2 Le interrogazioni su una tabella con la clausola GROUP BY Presentiamo ora il tipo d’interrogazione: SELECT attributo, aggregazione(attributo) FROM tabella GROUP BY attributo HAVING condizione Per ottenere il risultato si procede raggruppando le ennuple con lo stesso valore dell’attributo indicato nella clausola GROUP BY . La clausola HAVING qualifica gli attributi che devono appartenere al risultato. Esempio B.9 Si chiedono le mansioni e il numero dei dipendenti che le svolgono quando questi siano più di due. SELECT MANSIONE, COUNT(MANSIONE) AS QUANTITA FROM personale GROUP BY MANSIONE HAVING COUNT(MANSIONE)>2; Sperimentazioni con le basi di dati 13 B.2.3 Le interrogazioni annidate Presentiamo adesso un’interrogazione in cui la clausola WHERE contiene un’altra interrogazione che ha una sola ennupla in risposta. Esempio B.10 Chiediamo quali sono gli impiegati che hanno le stesse mansioni di Luca (matricola M34587). SELECT NOME, MANSIONE FROM personale WHERE MANSIONE=( SELECT MANSIONE FROM personale WHERE MATRICOLA=M34587) Se la sotto-interrogazione contiene più valori occorre indicare se il confronto attuale risulta vero con tutti ( ALL ) o con almeno uno ( ANY ) di essi. Vedremo più avanti come evitare l’annidamento delle interrogazioni che, nonostante l’approccio intuitivo, ha tuttavia un’influenza negativa sulla performance del sistema. B.2.4 Attributi calcolati Mostriamo adesso come le interrogazioni ci vengono incontro nel risolvere il problema degli attributi calcolati, esposto alla fine del Paragrafo B.1.3. Abbiamo già notato che, se si vuole aggiungere nella tabella personale un nuovo campo che rappresenti l’anzianità nell’azienda dei suoi impiegati, occorre calcolarne il valore in funzione della data attuale. Poiché nella SELECT possiamo aggiungere nuovi nomi di campi creiamo l’interrogazione seguente SELECT *, DateDiff(“aaaa”;DATA_ASSUNZIONE; Date()) AS ANZIANITA FROM personale; Nell’interfaccia grafica (vedi Figura B.7) la condizione espressa con [ANZIANITA]=DateDiff(“aaaa”;[DATA_ASSUNZIONE];Date()) si scrive in corrispondenza dell’etichetta Campo. 14 Capitolo B Figura B.10 Una istanza della tabella modelli_e_tratte. B.2.5 Un’interrogazione su due tabelle Il nostro lavoro si è svolto fino a questo momento con una sola tabella. Consideriamo adesso le due tabelle rappresentate in Figura B.8 e Figura B.9 La tabella macchinisti_e_modelli di Figura B.8 indica le matricole dei macchinisti della compagnia ferroviaria con l’abilitazione che hanno conseguito, ovvero con l’elenco delle locomotive che possono guidare. La tabella modelli_e_tratte di Figura B.9 indica quali tipi di locomotive possono essere impiegati su una tratta. Supponiamo adesso che la compagnia voglia conoscere le matricole dei macchinisti cui può affidare la responsabilità di guida nelle varie tratte. Per fare ciò serve un’operazione che si chiama natural join (o giunzione naturale), che ora passiamo a descrivere. Figura B.9 Una istanza della tabella macchinisti_e_modelli. Sperimentazioni con le basi di dati 15 Si inizia costruendo un nuovo schema con tutti gli attributi di entrambi gli schemi delle due tabelle in join. Nel nostro caso, poiché gli attributi dei due schemi sono (MATRICOLA TIPO) e (TIPO NUM_TRATTA), la loro unione è data da (MATRICOLA TIPO NUM_TRATTA). Le colonne che governano il join sono le colonne con lo stesso nome nelle due tabelle; nel nostro esempio si tratta dell’attributo TIPO. Si costruiscono poi i valori della nuova tabella “prolungando” ogni riga della prima tabella con quelle righe della seconda per cui i valori sugli attributi comuni coincidono. Qualsiasi riga contenente sulle colonne di join valori non presenti nell’altra tabella, resta esclusa dal join. Si noti che, come per tutte le operazioni, la tabella generata dal join non viene memorizzata in nessuna memoria permanente del computer e quindi deve essere calcolata ogni qual volta la si vuole visualizzare. Vediamo come si esprime in SQL il join naturale. Innanzitutto questo è possibile perché la clausola FROM permette l’uso di più tabelle, i cui nomi sono separati dalla virgola. La notazione puntata è adesso essenziale per consentirci di distinguere gli attributi delle due tabelle. La condizione che si esprime nella clausola WHERE contiene la condizione di join. Nel nostro esempio scriviamo: SELECT mm.*, mlt.NUM_TRATTA FROM modelli_e_tratte AS mlt, macchinisti_e_modelli AS mm WHERE mm.TIPO=mlt.TIPO; In questa interrogazione stiamo usando i sinonimi AS per le tabelle. Ciò permette di definire nuovi nomi per adattarli alle proprie esigenze anche senza creare duplicati delle tabelle. Il risultato della giunzione, è rappresentato nella Figura B.10: Figura B.11 Join. 16 Capitolo B A parte l’ovvia considerazione che non è bene avere locomotive che non possono essere guidate dai macchinisti della propria compagnia (in questo caso la ETR470), si osservi come questa operazione permette di navigare tra le tabelle per arrivare a rispondere alle interrogazioni che ci interessano. È da notare che se la tabella modelli_e_tratte si presentasse così: l’operazione di join restituirebbe una tabella vuota, ovvero senza righe, che si presenta all’utente così: Nel qual caso sarebbe legittimo nutrire molti dubbi sulla siffatta compagnia... Per definizione, nel caso estremo in cui le tabelle non hanno attributi in comune, il natural join risulta costituito da tutte le possibili combinazioni delle righe delle tabelle. Si può notare come la struttura della SELECT consente di definire la giunzione su attributi con nomi diversi, realizzando un’operazione più ampia del natural join che abbiamo prima definito. L’operazione di join è il punto di forza del modello relazionale. Essa dà all’utente la possibilità di navigare a suo piacimento nella base, di dati e non solo lungo strade già previste in fase di definizione, come accade in altri modelli. Ci siamo limitati a trattare in dettaglio il natural join, ma esistono altri tipi di join, sui quali è stata prodotta una vasta letteratura. Nelle query costruite con l’interfaccia grafica di default Access preferisce, come vedremo più avanti, generare INNER JOIN. Da un punto di vista formale, questo differisce dal natural join, in quanto non esprime la condizione nella clausola WHERE, bensì nella clausola FROM . B.3 Normalizzazione degli schemi Prendono il nome di forme normali alcune regole di costruzione delle tabelle relazionali che garantiscono certe coerenze sui dati. Infatti una tabella, per sua natura, può essere composta da attributi scoordinati tra loro o viceversa troppo dipendenti tra loro. Le forme normali cercano di guidare il progettista nel superamento di realizzazioni scorrette. Sperimentazioni con le basi di dati 17 B.3.1 Prima forma normale Nella definizione degli schemi occorre anche avere ben chiaro quali sono le informazioni che dobbiamo trarre dalla base di dati. Prendiamo infatti in esame la tabella abilitazione di Figura B.11, dove sono riportati i piloti e modelli di locomotive che possono guidare. Figura B.12 Supponiamo di volere l’elenco di tutti i cognomi dei piloti. A causa dello schema assegnato, non possiamo formulare una tale interrogazione al sistema. Si esprime questa situazione dicendo che, nel modello relazionale, i valori che gli attributi assumono sono atomici, non decomponibili. In altre parole, i valori non sono composti da insiemi di valori. Certo, un valore che è atomico in un’applicazione può non esserlo in un’altra. La regola generale è che un valore è non atomico se l’applicazione dovrà trattare con una parte semanticamente significativa di esso. Nel nostro caso specifico, se pensiamo che potremmo aver necessità di conoscere i cognomi, dovremo pensare allo schema abilitazione (COGNOME NOME TIPO ) e su questo potremo effettuare l’interrogazione desiderata. Uno schema che rispetta la regola di atomicità si dice in prima forma normale. B.3.2 Altre forme normali Proviamo a chiederci per quale motivo la compagnia decida di mantenere le due tabelle macchinisti_e_modelli e modelli_e_tratte viste nel Paragrafo B.2.5, e non la sola assegnabilità (MATRICOLA TIPO NUM_TRATTA ) ottenuta effettuando il join tra di esse. Sembra infatti che assegnabilità sintetizzi in modo più immediato il legame tra i macchinisti e le tratte ma 18 Capitolo B facciamo le seguenti osservazioni. La chiave primaria deve essere costituita da tutti gli attributi, per permettere ad un macchinista di avere più abilitazioni, ad un TIPO di locomotiva di essere guidato da più macchinisti e ad ogni macchinista, abilitato alla guida di un tipo di locomotiva, di percorrere più tratte. Con questo vincolo è impossibile inserire in una tale tabella l'elettrotreno ETR470 di proprietà della compagnia poichè, nella riga che la riguarderebbe, il valore dell’attributo MATRICOLA (dell’inesistente macchinista abilitato a guidarla) sarebbe NULL . Analogamente, sarebbe impossibile inserire ulteriori abilitazioni dei macchinisti per locomotive non possedute attualmente dalla compagnia, in quanto TIPO e NUM_TRATTA avrebbero valore NULL. Il cambiamento di un tipo di locomotiva per una tratta implicherebbe quindi un complesso aggiornamento della tabella stessa, perchè riguarderebbe tutti i macchinisti che erano assegnati a quella tratta sulla precedente locomotiva. Lo spreco di memoria che si vede in questo limitato esempio (vedi Figura B.12) diventerebbe massiccio in una situazione reale, con centinaia di macchinisti. Figura B.13 Questi problemi sono stati ampiamente trattati dagli studiosi di teoria relazionale ed hanno portato all’identificazione delle forme normali. Non è il caso di esporle tutte in questo contesto. Piuttosto, torniamo con altre parole ad osservare che, per loro natura, le tabelle non rappresentano solo concettualizzazioni. Con conseguenze di solito disastrose sulla base di dati. Si raccomanda allora di farsi guidare dal modello concettuale e dalle regole di traduzione tra i modelli, anche se queste ultime non sono da intendersi in senso totalmente algoritmico. L’operazione di join consente, come possiamo intuire, di memorizzare tabelle più piccole, più regolari da cui ottenere, al momento opportuno, la tabella con tutti gli attributi che ci occorrono. Il frammentare (ovvero so- Sperimentazioni con le basi di dati 19 stituire una tabella con altre più piccole) non è però una panacea e diventa eccessivo quando nessun join permette di ricostruire l’informazione nella sua interezza. Comunque, come accade spesso in informatica, una buona soluzione è un compromesso tra il tempo di calcolo e l’occupazione di memoria. Così, se l’interesse è maggiormente rivolto al primo, può essere forse più conveniente, nonostante i problemi segnalati, mantenere tabelle più grandi. B.4 Le relazioni tra due tabelle Nel Capitolo 6 abbiamo parlato della definizione dello schema logico riferendoci ad una sola classe del modello concettuale. Ma le classi di un progetto sono solitamente molto più numerose e legate da associazioni semantiche. Vediamo adesso la loro classificazione e successivamente la loro rappresentazione nel modello logico. B.4.1 Modello concettuale Relazione 1 a n Per dare l’esempio di una tale relazione, supponiamo che la compagnia memorizzi alcune caratteristiche delle stazioni: numero di binari, di biglietterie, indicazioni di presenza officina e di servizio auto al seguito. In questo caso, le stazioni devono essere una classe, e il semplice attributo “stazione di appartenenza” della classe personale si trasforma in un’associazione tra classi, che nel diagramma di Chen si rappresenta come in Figura B.13. La freccia a punta unica indica che ogni persona fa capo ad una sola stazione, quella a doppia punta indica invece che un’arbitraria stazione può avere più dipendenti. 20 Capitolo B personale stazioni Figura B.14 Esempio di relazione 1 a n. Relazione n a m Supponiamo che l’azienda mantenga sia l’anagrafe dei macchinisti sia le caratteristische delle locomotive che possiede. È importante che associ ad ogni macchinista l’elenco delle locomotive che può guidare. Si tratta di una relazione n a m che si indica come nella Figura B.14. Le doppie frecce indicano che ogni macchinista può guidare più modelli di locomotive e che un modello di locomotiva può essere guidato da più macchinisti. macchinisti modelli locomotive Figura B.15 Sperimentazioni con le basi di dati 21 Sottoclasse Questo caso può essere esemplificato ritenendo che per la classe personale occorra, quando ci si riferisce ai macchinisti, tenere in considerazione il numero di ore di esperienza e i tipi di abilitazioni di guida possedute (informazioni totalmente inutili per l’altra parte del personale). La situazione è rappresentata nel diagramma di Chen in Figura B.15, e si dice che macchinisti è una sottoclasse di personale. Questa sottoclasse è caratterizzata da un attributo in più ( ESPERIENZA ) e da un’associazione in più: i modelli di locomotiva che ciascuna entità macchinista è abilitata a guidare. Si dice che l’associazione tra modelli locomotive e macchinisti è di tipo n a m per indicare che ogni macchinista può guidare più modelli e ogni modello può essere guidato da più macchinisti. personale ESPERIENZA macchinisti Figura B.16 Esempio di sottoclasse Relazione riflessiva Analizziamo ora un caso particolare, quello in cui si considera il responsabile come elemento della classe personale. Questo, nel diagramma di Chen si esprime creando un arco come in Figura B.16. personale Figura B.17 Esempio di associazione riflessiva. 22 Capitolo B B.4.2 Modello logico Chiediamoci adesso come possiamo rappresentare tutte queste situazioni nel modello relazionale. Qui ci viene incontro l’operazione di join che abbiamo già presentato. Relazione 1 a n Cominciamo con il primo caso. Alla tabella personale aggiungiamo un nuovo attributo che è la chiave della tabella stazioni. Poiché ogni dipendente ha una sola sede, sarà ben definito il valore corretto da assegnare a quel campo, valore che nella tabella stazioni fornisce il punto d’attracco dell’operazione di join. Il linguaggio grafico di Access, mostrato in Figura B.17, è molto espressivo. Il segmento che lega fra loro gli attributi collegati porta il segno 1 ad un estremo, il segno × all’altro, indicando che l’associazione è uno a molti. In generale, per rappresentare un’associazione 1 a n (×) si procede analogamente a quanto abbiamo appena fatto. Si opera in modo che la tabella in associazione univoca contenga tutti gli attributi che fanno da chiave primaria per l’altra. Tale insieme di attributi è denominato chiave esterna (in inglese foreign key) e viene indicata la tabella di riferimento: o graficamente, come in Figura B.17, o in SQL, con la parola chiave REFERENCES. La dichiarazione di una chiave esterna consente al sistema di controllare che ogni inserimento nella tabella personale di un valore per il campo STAZIONE_APP sia già presente nel campo NOME della tabella stazioni. Per modellare macchinisti come sottoclasse di personale possiamo pensare ad una tabella macchinisti costituita dal campo ESPERIENZA e dal campo MATRICOLA, i cui valori sono quelli del personale assunto con MAN- Figura B.18 Sperimentazioni con le basi di dati 23 SIONE macchinista. In questo modo MATRICOLA è contemporaneamente chiave primaria e chiave esterna in riferimento alla tabella personale. CREATE TABLE macchinisti ( MATRICOLA CHAR (15), ESPERIENZA DECIMAL(8,2), PRIMARY KEY(MATRICOLA) FOREIGN KEY (MATRICOLA) REFERENCES personale) Le modalità di inserimento dati nelle sottoclassi devono essere opportunatamente controllate: è necessario che nella tabella sottoclasse dell’esempio sia possibile inserire solo macchinisti. Questa condizione la esprimeremo tramite i vincoli di integrità referenziale. Relazione n a m Modelliamo adesso l’associazione tra la classe macchinisti e la classe modelli. A tale scopo definiamo una nuova tabella, per la quale scegliamo il nome macchinisti_e_modelli. Le chiavi primarie delle due tabelle macchinisti e modelli sono gli unici suoi attributi e fanno da chiave esterna rispettivamente per la prima e per la seconda tabella. Inoltre, esse ne sono la chiave primaria. CREATE TABLE macchinisti_e_modelli ( MATRICOLA CHAR (15), ID_LOCOMOTIVA CHAR (15), PRIMARY KEY(MATRICOLA, ID_LOCOMOTIVA) FOREIGN KEY (MATRICOLA) REFERENCES macchinisti, FOREIGN KEY (ID_LOCOMOTIVA) REFERENCES modelli) Relazione riflessiva Per modellare l’associazione riflessiva “uno a molti” tra personale e personale che riferisce al dirigente immediato, usiamo l’attributo RESPONSABILE come chiave esterna per la stessa tabella. Per creare graficamente la chiave esterna, come si vede in Figura B.18, dobbiamo usare anche l’alias della tabella in questione. 24 Capitolo B Figura B.19 Associazione riflessiva in Access. B.4.3 La chiave esterna in Access Per definire i legami tra tabelle, si usa il pulsante Relazioni, evidenziato in Figura B.1. Il sistema ci presenta l’elenco delle tabelle tra le quali possiamo scegliere quelle che ci interessano. Poi chiusa la finestra Visualizza tabelle, effettuiamo la dichiarazione di chiave esterna graficamente, “trascinando gli attributi” di collegamento delle due tabelle e scegliendo nella tabella di dialogo visualizzata dal sistema Applica integrità referenziale. L’elenco di scelte che il sistema successivamente permette serve a stabilire i vincoli di cui parleremo nel paragrafo seguente. B.5 Vincoli esprimibili su più tabelle Distinguiamo i vincoli in: vincoli di integrità referenziale, di aggiornamento e di cancellazione. B.5.1 Vincolo di integrità referenziale Quando definiamo la chiave esterna, il sistema controlla errori di immissione verificando che l’inserimento effettuato sia coerente con la tabella di riferimento. I controlli possono essere ancora più stringenti, permettendo esclusivamente una scelta da un elenco di dati predefiniti. In pratica, facciamo riferimento al pannello inferiore della Figura B.2. Scegliamo l’etichetta Ricerca ed effettuiamo le seguenti scelte: Sperimentazioni con le basi di dati Visualizza controllo Tipo origine riga Solo in elenco 25 Casella combinata Tabella/query Sì Nel campo Origine riga occorre inserire la query opportuna. Come risultato, il sistema in fase di immissione dati permette solo di prelevarli da un menu a tendina. Esempio B.11 Nella tabella macchinisti_e_modelli, sia i nomi delle locomotive da assegnare ad un pilota sia le matricole dei piloti devono essere presi da liste predefinite. La prima è una tabella con i tipi delle locomotive di cui la società ferroviaria può disporre, la seconda si ottiene dalla tabella personale. La query per estrarre gli identificativi dei macchinisti è la seguente: SELECT personale.MATRICOLA FROM personale WHERE personale.MANSIONE="macchinista"; È questa l’interrogazione che scriviamo perché il sistema sia in grado di prelevare direttamente i dati da una tabella che può cambiare nel tempo. Il risultato è visibile in Figura B.19. Si nota che il dato da inserire è prelevabile da un menu a tendina. Figura B.20 B.5.2 Vincoli di aggiornamento e vincoli di cancellazione Quando si definisce un vincolo di integrità referenziale, si possono definire anche le azioni che il sistema deve compiere in caso di aggiornamento e di cancellazione su ennuple correlate. La parola CASCADE serve a segnalare la propagazione della modifica. 26 Capitolo B Ad esempio, una piena espressione del vincolo per cui tutti i macchinisti devono far parte del personale, impone che la cancellazione di un macchinista dalla tabella personale abbia come conseguenza la cancellazione di tutte le sue mansioni di pilota. In concreto, dovranno essere cancellate, nella tabella macchinisti_e_modelli, tutte le ennuple che contengono i suoi identificativi. Attenzione però ad un superficiale uso di CASCADE: nella nostra base di dati potremmo trovare cancellazioni indesiderate su tabelle distanti da quella su cui abbiamo cominciato. La specifica RESTRICT segnala la restrizione delle modifiche: non possiamo eliminare una ennupla se essa è riferita da un’altra tabella. Ad esempio, possiamo imporre che sulla tabella mansioni non sia possibile eliminare una mansione se quest’ultima è associata a qualche dipendente. Ci sono altre specifiche interessanti come SET NULL, SET DEFAULT e NO ACTION , di cui possiamo già intuire il significato, ma rimandiamo ai manuali specializzati. B.5.3 Il trigger Possiamo notare che i vincoli referenziali eseguono un’azione ( RESTRICT, CASCADE ecc.) quando si verifica un evento di aggiornamento o cancellazione. I trigger sono una estensione di questo paradigma in cui, al verificarsi dell’evento di base, l’azione successiva del sistema non è indifferenziata, ma invece regolata da qualche condizione. Sono infatti desiderabili funzioni che ci permettano di controllare situazioni senza scrivere complessi blocchi di codice. Ad esempio, si potrebbe definire un trigger per avvisare l’azienda che troppi macchinisti stanno andando in pensione. Comunque, non esistono in questo momento standard condivisi di definizione e di ordine nell’esecuzione delle regole dei trigger. B.6 Interrogazioni su una base di dati Prendiamo in esame lo schema visualizzato in Figura B.20 ed effettuiamo le seguenti interrogazioni. Esempio B.12 Chiediamo le ore di esperienza di tutti i macchinisti. A questa interrogazione possiamo rispondere usando il join naturale che esprimiamo nel modo seguente: SELECT personale.COGNOME,macchinisti.ORE_DI_ESPERIENZA FROM personale, macchinisti WHERE personale.MATRICOLA=macchinisti.MATRICOLA; Sperimentazioni con le basi di dati 27 Figura B.21 Lo schema su cui effettuiamo le interrogazioni. In alternativa, possiamo usare l’interfaccia grafica. Dalla finestra Scegli tabelle scegliamo le tabelle macchinisti e personale. Il sistema segnala automaticamente il legame di join. Poi operiamo nel pannello inferiore le seguenti scelte: Campo COGNOME ORE_DI_ESPERIENZA MATRICOLA Tabella personale macchinisti personale Visualizza ✓ ✓ Condizione =macchinisti.MATRICOLA Se visualizziamo la soluzione SQL offerta da Access, troviamo: SELECT personale.COGNOME,macchinisti.ORE_DI_ESPERIENZA FROM personale INNER JOIN macchinisti ON personale.MATRICOLA = macchinisti.MATRICOLA; 28 Capitolo B Come si vede, Access ha usato automaticamente l’operatore INNER e ci ha inoltre esonerato dallo scrivere il noioso elenco degli attributi coinvolti. Quest’ultima è la caratteristica più interessante dell’interfaccia grafica. Gli utenti sono fortemente invitati a usarla ed eventualmente a completare e/o modificare l’interrogazione automatica usando lo standard SQL quando lo ritengono opportuno. JOIN Esempio B.13 Cerchiamo i cognomi dei macchinisti che possono guidare su locomotive elettriche. L’interrogazione coinvolge tutte le tabelle illustrate nella Figura B.20 tranne personale_1, ma non è complessa da rappresentare perché, al solito, il sistema ci viene incontro scrivendo la lunga condizione di join. Si scelgono tutte le tabelle e si effettuano le seguenti scelte: Campo COGNOME ELETTRICA Tabella personale modelli Visualizza ✓ ✓ Condizione TRUE Esempio B.14 Cerchiamo il cognome dell'immediato responsabile di Giuseppe Rossi. Operiamo come nei casi precedenti, usando due volte la tabella personale. Il sistema crea l’alias personale_1, ma ignora il legame precedentemente costruito tra la chiave RESPONSABILE della tabella personale_1 e l’attributo MATRICOLA della tabella personale. Dobbiamo dunque ricrearlo e poi effettuare le seguenti scelte: Campo COGNOME NOME COGNOME Tabella personale personale personale_1 ✓ Visualizza Condizione "Rossi" "Giuseppe" Esempio B.15 Cerchiamo i nomi dei responsabili dei macchinisti che possono guidare l’ETR460. Sperimentazioni con le basi di dati 29 Per rispondere a questa interrogazione, occorre scegliere le tabelle, macchinisti, macchinisti_modelli e due volte la tabella personale (come nel caso precedente). Effettuiamo le seguenti scelte: Campo COGNOME COGNOME TIPO Tabella personale personale_1 macchinisti_e_modelli Visualizza ✓ ✓ Condizione “ETR460” Esempio B.16 Cerchiamo il cognome del personale che ha il responsabile non appartenente alla propria stazione. L’interrogazione in SQL risulta essere: Campo STAZIONE_APP COGNOME [CAPO]:COGNOME Tabella personale personale personale_1 Visualizza ✓ ✓ Condizione <>personale_1.STAZIONE_APP “ETR460”