Il programmatore e le sue API

IX parte: Ancora sul design della persistenza con Hibernatedi

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:

  1. Utilizzare un'unica tabella per rappresentare tutte le classi della gerarchia
  2. Utilizzare una tabella per ognuna delle classi della gerarchia
  3. 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

 

 

 

Condividi

Pubblicato nel numero
134 novembre 2008
Alfredo Larotonda, laureato in Ingegneria Elettronica, lavora da diversi anni nel settore IT. Dal 1999 si occupa di Java ed in particolare dello sviluppo di applicazioni web J2EE. Dopo diverse esperienze di disegno e sviluppo di applicazioni web per il mercato finanziario e industriale, si occupa ora in particolare di…
Articoli nella stessa serie
Ti potrebbe interessare anche