Partendo dal modello di dominio, continuiamo l‘analisi delle varie tipologie di relazione tra le classi e vediamo come sia possibile mappare la struttura ad oggetti su quella relazionale con l‘ausilio di Hibernate.
Introduzione
Nel presente articolo proseguiamo lo studio del modello di dominio del nostro sistema e di come sia possibile mappare le classi che lo costituiscono sulla struttura relazionale con l’ausilio di Hibernate.
Affronteremo in questo numero un caso già esaminato nella trattazione fatta per l’implementazione dello strato di persistenza con la tecnologia EJB 3.0 richiamando e approfondendo alcuni concetti e mostrando, in un ideale parallelo, come il medesimo obiettivo possa essere raggiunto tramite una diversa tecnologia di implementazione che nel nostro caso è Hibernate.
In Figura 1 riportiamo per comodità del lettore il diagramma UML del Domain Model del nostro sistema.
Figura 1 – Il modello delle entità.
Tratteremo quindi un caso di studio molto interessante in ottica generale. Cercheremo infatti di capire come sia possibile trasportare nel modello relazionale una caratteristica quale l’ereditarietà, tipica del modello ad oggetti; vedremo le modalità possibili per colmare il paradigm mismatch in questo particolare caso e utilizzeremo Hibernate come strumento di implementazione.
Implementazione dell’ereditarietà nel modello relazionale
Il concetto di ereditarietà è ben noto a qualsiasi programmatore, analista o architetto object-oriented essendo non solo una delle caratteristiche fondamentali del modello a oggetti ma forse quella più immediata e intuitiva anche per chi approccia da neofita il mondo della programmazione a oggetti, tant’è che spesso è abusata e utilizzata anche quando ciò non è conveniente.
Nel nostro modello di dominio abbiamo un esempio di classi che fanno parte di una gerarchia ovvero classi che tra di loro sono in una relazione di ereditarietà. È l’esempio della classe Scheda e delle sue classi derivate SchedaTrattamenti e SchedaSalute.
Allo scopo di descrivere come tradurre nel modello relazionale un simile legame, non ci interessa tanto dettagliare ogni singolo attributo di queste classi quanto capire come i dati delle entità rappresentate da queste possano essere resi persistenti in un database. A tale scopo faremo riferimento ad attributi generici per ciascuna di queste classi.
Esistono diversi approcci per modellare l’ereditarietà in una struttura relazionale, già in parte descritti negli articoli precedenti.
Possiamo dire che gli approcci sono essenzialmente riconducibili a tre casistiche principali fondamentali:
- Utilizzare un’unica tabella per rappresentare tutte le classi della gerarchia
- Utilizzare una tabella per ognuna delle classi della gerarchia
- Utilizzare una tabella per ciascuna classe concreta della gerarchia
Caso 1
Il primo caso è probabilmente quello che conduce alla soluzione più semplice dal punto di vista del database. In pratica si definisce un’unica tabella che rappresenta tutte le classi della gerarchia nella quale si inseriranno tutti i possibili attributi, quindi quelli presenti nella classe base e in tutte le classi derivate. Ciò comporta la necessità di inserire una colonna “tipo” che consenta di discriminare a quale entità del modello ad oggetti fa riferimento la singola riga della tabella.
Questo modello, molto semplice da implementare, nel nostro caso condurrebbe a una tabella simile alla seguente:
Figura 2 – Implementazione dell’ereditarietà. Caso 1.
In questo caso una riga della tabella, identificata dalla colonna id_scheda che è la chiave primaria, rappresenta i dati di una singola entità della gerarchia. La colonna tipo_scheda consente di individuare se si tratta di una Scheda Trattamenti, di una Scheda Salute o di un altro tipo di scheda che in futuro possa dover essere messa nel conto.
In un simile modello l’aggiunta di un attributo ad una qualsiasi delle classi della gerarchia si traduce nell’aggiunta di una nuova colonna alla tabella. Il modello risulta essere piuttosto semplice ma comporta una notevole ridondanza di dati nel database in quanto ciascuna riga presumibilmente conterrà colonne prive di significato per la tipologia corrispondente.
Caso 2
Il secondo modello prevede una corrispondenza uno-a-uno tra le classi del modello e le tabelle del database e quindi si colloca all’estremo opposto della soluzione precedente. In questo caso si trasporta così com’è nel database il modello ad oggetti mettendo in relazione le tabelle mediante opportune foreign-key. Nel nostro caso si giunge allo schema seguente.
Figura 3 – Implementazione dell’ereditarietà. Caso 2.
In base a questa soluzione, l’aggiunta di una nuova classe nella gerarchia comporterebbe la definizione di una nuova tabella nel database, mentre nel caso precedente avrebbe comportato l’inserimento di una nuova riga di tipo appropriato nell’unica tabella presente. Il modello in questione porta ad una struttura di database più complessa e quindi a query di estrazione dati più complicate ma elimina la ridondanza dei dati ed è più coerente con il modello ad oggetti.
Caso 3
Il terzo modello prevede una tabella per ogni classe derivata della gerarchia. In questo caso le tabelle associate alle classi derivate conterranno tutti gli attributi propri ma anche quelli della classe base. Questa soluzione è una sorta di ibrido tra le due precedenti e si pone quindi a metà tra i due estremi. Nel nostro caso lo schema sarebbe quello raffigurato nella Figura 4.
Figura 4 – Implementazione dell’ereditarietà. Caso 3.
In questo caso, se si aggiungesse un attributo alla classe base, si dovrebbero di conseguenza modificare tutte le tabelle delle classi derivate. Aggiungere una nuova classe alla gerarchia comporterebbe l’aggiunta di una nuova tabella. La ridondanza dei dati nel database è minore rispetto al primo caso ma non è eliminata a fronte di una struttura di database più complessa.
Una volta esaminate le possibilità va fatta una scelta sulla soluzione da adottare tenendo sempre presente che qualsiasi scelta comporta inevitabilmente un trade-off tra costi e benefici. Per differenziare la trattazione rispetto a quanto visto nel caso degli EJB 3.0, scegliamo la soluzione 2, ovvero quella che prevede una tabella per ciascuna classe della gerarchia, e implementiamola con Hibernate.
Le classi della gerarchia le supponiamo fatte come segue.
Scheda
public class Scheda { public Scheda() {} private int id_scheda; private String attr_scheda; //accessors ... }
SchedaSalute
public class SchedaSalute extends Scheda { public SchedaSalute() {} private String attr_scheda_salute; //accessors ... }
SchedaTrattamenti
public class SchedaTrattamenti extends Scheda { public SchedaTrattamenti() {} private String attr_scheda_trattamenti; //accessors ... }
Dobbiamo realizzare con Hibernate un mapping tra le classi in questione e le tre tabelle corrispondenti. A questo scopo si può usare l’elemento nel file di mapping delle classi che sarà fatto come segue:
"-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> table="SCHEDA_TRATTAMENTI"> column="ATTR_SCHEDA_TRATTAMENTI"/> table="SCHEDA_SALUTE"> column="ATTR_SCHEDA_SALUTE"/>
Ad ogni sottoclasse è associata la corrispondente tabella mediante l’elemento nel quale vanno specificati la colonna sulla quale esiste il constraint di foreign-key con la tabella della superclasse e il mapping di tutti gli attributi della sottoclasse stessa.
Se si volessero acquisire ad esempio tutte le istanze della entità SchedaTrattamenti si utilizzerebbe il seguente codice
... ..... //aperture sessione Hibernate Session session = factory.openSession(); session.beginTransaction(); //esecuzione query Query q = session.createQuery(" from SchedaTrattamenti "); List res = q.list(); //chiusura sessione Hibernate session.getTransaction().commit(); //display risultati Iterator i = res.iterator(); while(i.hasNext()) { SchedaTrattamenti scheda = i2.next(); System.out.println(scheda.getAttr_scheda()); System.out.println(scheda.getAttr_scheda_trattamenti()); }
in corrispondenza del quale Hibernate eseguirà la seguente query
select schedatrat0_.ID_SCHEDA as ID1_0_, schedatrat0_1_.ATTR_SCHEDA as ATTR2_0_, schedatrat0_.ATTR_SCHEDA_TRATTAMENTI as ATTR2_1_ from SCHEDA_TRATTAMENTI schedatrat0_ inner join SCHEDA schedatrat0_1_ on schedatrat0_.ID_SCHEDA=schedatrat0_1_.ID_SCHEDA
per acquisire anche gli attributi corrispondenti alla tabella della classe base.
Viceversa se si facesse la stessa cosa però sulla entità Scheda, Hibernate eseguirebbe la seguente query:
select scheda0_.ID_SCHEDA as ID1_0_, scheda0_.ATTR_SCHEDA as ATTR2_0_, scheda0_1_.ATTR_SCHEDA_TRATTAMENTI as ATTR2_1_, scheda0_2_.ATTR_SCHEDA_SALUTE as ATTR2_2_, case when scheda0_1_.ID_SCHEDA is not null then 1 when scheda0_2_.ID_SCHEDA is not null then 2 when scheda0_.ID_SCHEDA is not null then 0 end as clazz_ from SCHEDA scheda0_ left outer join SCHEDA_TRATTAMENTI scheda0_1_ on scheda0_.ID_SCHEDA=scheda0_1_.ID_SCHEDA left outer join SCHEDA_SALUTE scheda0_2_ on scheda0_.ID_SCHEDA=scheda0_2_.ID_SCHEDA
con il CASE per discriminare l’esistenza di righe nelle tabelle associate alle sottoclassi.
Qualora invece si volesse rendere persistente una istanza di SchedaTrattamenti utilizzando il codice seguente
... Session session = factory.openSession(); session.beginTransaction(); SchedaTrattamenti scheda = new SchedaTrattamenti(); scheda.setAttr_scheda("Scheda3"); scheda.setAttr_scheda_trattamenti("SchedaTrattamenti3"); session.save(scheda); session.getTransaction().commit();
ci penserebbe Hibernate a inserire nella tabella associata alla sottoclasse gli attributi di questa e la corrispondente riga nella tabella associata alla superclasse con gli attributi di quest’ultima.
Le istruzioni SQL eseguite in questo caso sarebbero le seguenti:
select hibernate_sequence.nextval from dual insert into SCHEDA (ATTR_SCHEDA, ID_SCHEDA) values (?, ?) insert into SCHEDA_TRATTAMENTI (ATTR_SCHEDA_TRATTAMENTI, ID_SCHEDA) values (?, ?)
Questa è solo una delle molteplici strategie di implementazione dell’ereditarietà con Hibernate. Una trattazione completa richiederebbe ben altro spazio e pertanto si rimanda il lettore ai riferimenti bibliografici per un approfondimento della problematica.
Conclusioni
In questo articolo abbiamo affrontato un argomento molto importante concettualmente, l’implementazione dell’ereditarietà in una struttura relazionale. Vi sono molte strategie per ottenere questo scopo; per descriverle tutte sarebbe necessaria una serie interamente dedicata allo scopo. L’obiettivo che si è invece cercato di raggiungere è fornire alcuni elementi di base utili ad affrontare il problema e un esempio di implementazione che possa fungere da guida nelle realizzazioni concrete. Nel prossimo articolo concluderemo l’esplorazione del nostro modello di dominio esaminando le altre relazioni fino ad ora non affrontate.
Riferimenti
[1] Hibernate Reference Documentation 3.3.1, Copyright © 2004 Red Hat Middleware, LLC
[2] Christian Bauer – Gavin King, “Java Persistence with Hibernate”, 2006