Hibernate è un motore di persistenza utilizzato per lo più per realizzare il mapping di tabelle preesistenti in forma di oggetti. In questo articolo vedremo come realizzare la persistenza di un modello ad oggetti puro, completamente separato dal database e dal modello relazionale.
Introduzione
Oggigiorno è raro trovare un‘applicazione che non abbia bisogno di immagazzinare dati persistenti in qualche modo, su file nei casi più semplici fino ad arrivare ai più sofisticati Database Management Systems (DBMS). Quando si scrivono applicazioni con database ci si trova di fronte a due possibili scenari: uno in cui il database esiste già (legacy) e uno in cui va progettato da zero. Nel primo caso si può procedere con un reverse engineering per identificare un modello dei dati a partire dalle tabelle esistenti. Nel secondo caso l‘approccio tradizionale consiste nell‘identificare entità e relazioni coinvolte e creare le tabelle sul database. In entrambi i casi si parte dalla definizione dei dati e vi si costruisce sopra l‘applicazione. Questo approccio, indubbiamente immediato quando si usano strumenti di sviluppo RAD (Rapid Application Development), diviene immediatamente svantaggioso con metodologie di sviluppo più sofisticate, in quanto vincola le entità del modello dati (tipicamente ad oggetti con un linguaggio come Java) alla struttura relazionale tipica dei tradizionali database. I due modelli, quello relazionale e quello ad oggetti, sono fortemente diversi per cui è necessario escogitare una forma di “traduzione” da un modello all‘altro, detta Object/Relational Mapping (ORM). Questa traduzione è sempre necessaria ed introduce uno strato aggiuntivo di logica, con tutto ciò che ne consegue in termini di sviluppo, debugging, manutenzione. Inoltre l‘applicazione conserverà inevitabilmente la natura relazionale del modello di partenza, risultando strutturata prevalentemente su tale modello piuttosto che sul paradigma di sviluppo ad oggetti, con lo sviluppatore sempre costretto a fare i conti con una traduzione più o meno esplicita da un modello all‘altro.
Persistenza con Hibernate
Esistono diversi strumenti per agevolare lo sviluppo di applicazioni con dati persistenti, strumenti che variano da piattaforma a piattaforma. Uno dei più diffusi è il motore di persistenza Hibernate. Supponendo di lavorare con la metodologia appena descritta, avremo sostanzialmente tre fasi di progettazione: 1. definizione del modello dati relazionale, 2. definizione del modello ad oggetti e 3. definizione del mapping fra l‘uno e l‘altro. In questo scenario, una volta progettato il modello relazionale dei dati, Hibernate può prendersi carico di tradurlo e mapparlo in oggetti, rendendo trasparente il processo. Partendo da un database legacy Hibernate è in grado di mappare le tabelle in classi: ogni riga della tabella sarà un‘istanza della classe corrispondente. Lo sviluppatore non dovrà più occuparsi di interagire con il DBMS per mezzo di JDBC, ma userà la più familiare forma degli oggetti e le comuni strutture dati del linguaggio.
/* usando JDBC */ResultSet rs = stmt.executeQuery("SELECT * FROM TABELLA");while (rs.next()) {String s = rs.getString("CAMPO1");float n = rs.getFloat("CAMPO2");System.out.println(s + " " + n);}/* con Hibernate */List righe = session.createQuery("from Tabella").list();for (Iterator i = righe.iterator(); i.hasNext();) {Tabella riga = (Tabella) i.next();System.out.println(riga.getCampo1 + " " + riga.getCampo2);}
Ciò non deve però trarre in inganno. Nonostante l‘indubbio vantaggio di usare oggetti anzichà© statement SQL inviati tramite JDBC e l‘onere di gestire istanze di ResultSet, non ci troviamo di fronte ad un modello Object Oriented, bensì ad un modello relazionale in cui le tabelle appaiono in forma di oggetti: per lavorare con un modello realmente ad oggetti è sempre necessario introdurre un ulteriore strato di traduzione tra oggetti-tabella e oggetti-modello, con un costo sia in termini di tempo che in termini di codice da mantenere.
L‘approccio a Oggetti
Hibernate permette un approccio alla persistenza completamente diverso da quello tradizionale, un approccio totalmente orientato agli oggetti e trasparente allo sviluppatore. In un‘applicazione Java ben progettata troveremo tipicamente tre componenti distinte: un modello che rappresenta le entità manipolate dall‘applicazione; alcune viste del modello, che presentano il modello all‘utente in una forma a lui leggibile; un controllore, che si occupa di gestire l‘input dell‘utente, esaudirne le richieste (delegando al modello ove necessario), interagire con il database e delegare alle viste la presentazione dei risultati. Esaminiamo dunque il modello, la componente che ci interessa in questa sede. Il modello è un‘astrazione delle entità che la nostra applicazione deve gestire. Il paradigma ad oggetti permette una modellazione ricca e strutturata degli oggetti del mondo reale che dobbiamo rappresentare nell‘applicazione, fornendo costrutti come ereditarietà e associazioni, pattern, ecc. non direttamente rappresentabili con un modello relazionale. In questa ottica risulta quindi più naturale progettare un modello puramente ad oggetti, direttamente implementabile in Java, e a partire da esso costruire il database. Hibernate permette di sviluppare questo approccio in modo diretto ed elegante, consentendo di concentrare gli sforzi esclusivamente sulla modellazione ad oggetti e gestendo tutte le problematiche di persistenza autonomamente ed in maniera del tutto trasparente. In questo modo è possibile progettare il modello e considerare questi oggetti come “persistenti”, dimenticandosi completamente dell‘esistenza del database!
Modellazione e metainformazioni
Supponiamo di dover gestire una semplice anagrafica di clienti, che possono essere sia persone che aziende. Ogni soggetto avrà le sue informazioni anagrafiche e amministrative, oltre a vari indirizzi e numeri di telefono. Il modello potrebbe essere espresso come nel seguente diagramma:
Abbiamo il Soggetto, che può rappresentare appunto l‘astrazione del soggetto anagrafico ed incarnare il concetto di azienda; abbiamo poi una sua generalizzazione, Persona, che ne eredita attributi e comportamenti aggiungendone di nuovi; infine abbiamo la classe Indirizzo. Si possono notare alcune cose. Innanzitutto Soggetto possiede un attributo multivalore, numeroTelefono: un soggetto può avere da 0 a n numeri. Inoltre abbiamo modellato l‘associazione fra Soggetto ed Indirizzo come composizione, in quanto l‘indirizzo non può esistere se non associato ad un soggetto. Persona eredita da Soggetto ogni metodo e proprietà , comprese quindi la proprietà numeriTelefono e la composizione con Indirizzo. Il vantaggio immediato di un simile modello è che può essere implementato direttamente in Java, inserendo normalmente tutti i metodi e gli attributi necessari.
/* listato Java del modello */public class Soggetto { private Long id; private String partitaIva; private String cognomeRagioneSociale; private Set indirizzi; private Set numeriTelefono; /** Costruttore di default. */public Soggetto() {this(null);}/** Costruttore con id. */public Soggetto(final Long id) {this.id = id;indirizzi = new HashSet();numeriTelefono = new HashSet();}public Long getId() {return this.id;}public void setId(final Long id) {this.id = id;}public String getPartitaIva() {return this.partitaIva;}public void setPartitaIva(final String partitaIva) {this.partitaIva = partitaIva;}/** * Il cognome per una persona, la ragione sociale per un‘azienda, ecc. */public String getCognomeRagioneSociale() {return this.cognomeRagioneSociale;}public void setCognomeRagioneSociale(String name) {this.cognomeRagioneSociale = name;}public Set getIndirizzi() {return this.indirizzi;}public void setIndirizzi(final Set indirizzi) {this.indirizzi = indirizzi;}public Set getNumeriTelefono() {return this.numeriTelefono;}public void setNumeriTelefono(final Set numeri) {this.numeriTelefono = numeri;}public void addIndirizzo(final Indirizzo indirizzo) {indirizzi.add(indirizzo);}public void removeIndirizzo(final Indirizzo indirizzo) {indirizzi.remove(indirizzo);}public void addTelefono(final String telefono) {numeriTelefono.add(telefono);}public void removeTelefono(final String telefono) {numeriTelefono.remove(telefono);}}public class Persona extends Soggetto { private Date dataNascita; private String nomeProprio; private String genere;/** Costruttore di default. */public Persona() {super();}/** * La data di nascita. */public Date getDataNascita() {return this.dataNascita;}public void setDataNascita(final Date data) {this.dataNascita = data;}/** * Il nome proprio della persona. */public String getNomeProprio() {return this.nomeProprio;}public void setNomeProprio(final String nome) {this.nomeProprio = nome;}/** * Il genere, volgarmente detto sesso, che sarà "M" per il genere * maschile ed "F" per quello femminile. */public String getGenere() {return this.genere;}public void setGenere(final String genere) {this.genere = genere;}}public class Indirizzo { private String indirizzo; private String comune; private String localita; private String nome; private String provincia; private String cap;/** Costruttore di default. */public Indirizzo() {indirizzo = null;comune = null;localita = null;nome = null;provincia = null;cap = null;}/** * Restituisce l‘indirizzo, p.es. "via Roma, 23". */public String getIndirizzo() {return this.indirizzo;}
/** * Imposta l‘indirizzo, p.es. "via Roma, 23". */public void setIndirizzo(final String indirizzo) {this.indirizzo = indirizzo;}/** * Il comune. */public String getComune() {return this.comune;}public void setComune(final String comune) {this.comune = comune;}/** * La località . */public String getLocalita() {return this.localita;}public void setLocalita(final String localita) {this.localita = localita;}/** * Nome identificativo per l‘indirizzo, p.es "abitazione", "ufficio", ecc. */public String getNome() {return this.nome;}public void setNome(final String nome) {this.nome = nome;}/** * La provincia. */public String getProvincia() {return this.provincia;}public void setProvincia(final String provincia) {this.provincia = provincia;}/** * Il CAP (Codice Avviamento Postale). */public String getCap() {return this.cap;}public void setCap(final String cap) {this.cap = cap;}}
Abbiamo quindi tre classi, ognuna con i suoi attributi privati e i suoi metodi accessori. In particolare abbiamo aggiunto a Soggetto dei metodi per aggiungere ed eliminare facilmente indirizzi e numeri di telefono (add/removeIndirizzo(), ecc.). Sarebbe possibile aggiungere ulteriori metodi e controlli (che abbiamo omesso per brevità ), per esempio un metodo per calcolare l‘età di una persona in base alla data di nascita, o un controllo sulla correttezza del codice fiscale nel metodo setCodiceFiscale(). così com‘è abbiamo quindi un buon modello ad oggetti, funzionale alla nostra applicazione e facilmente gestibile dallo sviluppatore, ma non ancora persistente. Per renderlo tale dobbiamo fornire ad Hibernate le informazioni necessarie per poterlo gestire in modo appropriato. Per fare ciò abbiamo due strade: scrivere esplicitamente dei file XML contenenti le informazioni di mapping oppure fornire queste informazioni direttamente nei sorgenti del modello e lasciare che Hibernate generi questi file al posto nostro. Il secondo approccio è preferibile in quanto libera dall‘onere di scrivere esplicitamente file di configurazione aggiuntivi: avremo così un solo sorgente che implementa direttamente un modello persistente.
XDoclet è lo strumento che ci permette di inserire le informazioni di persistenza direttamente nei sorgenti. XDoclet mette a disposizione alcuni tag, interpretabili da Hibernate, da inserire nei commenti Javadoc (documentate i vostri sorgenti, vero?): questi tag, trovandosi nei commenti, vengono ignorati dal compilatore Java, ma sono visti e utilizzati profiquamente dagli stumenti di Hibernate. Questi tag permettono di definire quali classi e quali attributi dell‘oggetto devono essere immagazzinati in un database e come. I sorgenti così corredati sono quindi processati dagli strumenti di Hibernate, i quali si preoccupano di creare automaticamente lo schema del database e i file di mapping. Aggiungiamo ai nostri sorgenti i tag XDoclet.
/* sorgenti con xdoclet *//** * Soggetto. * * @hibernate.class */public class Soggetto { private Long id; private String partitaIva; private String cognomeRagioneSociale; private Set indirizzi; private Set numeriTelefono; /** Costruttore di default. */public Soggetto() {this(null);}/** Costruttore con id. */public Soggetto(final Long id) {this.id = id;indirizzi = new HashSet();numeriTelefono = new HashSet();}/** * @hibernate.id * generator-class="native" */public Long getId() {return this.id;}public void setId(final Long id) {this.id = id;}/** * @hibernate.property * length="16" * unique="true" */public String getPartitaIva() {return this.partitaIva;}public void setPartitaIva(final String partitaIva) {this.partitaIva = partitaIva;}/** * Il cognome per una persona, la ragione sociale per un‘azienda, ecc. * * @hibernate.property length="60" */public String getCognomeRagioneSociale() {return this.cognomeRagioneSociale;}public void setCognomeRagioneSociale(String name) {this.cognomeRagioneSociale = name;}/** * @hibernate.set * name="indirizzi" * @hibernate.collection-key * column="id" * @hibernate.collection-composite-element * class="articolo.Address" */public Set getIndirizzi() {return this.indirizzi;}public void setIndirizzi(final Set indirizzi) {this.indirizzi = indirizzi;}/** * @hibernate.set * table="numeri_telefono" * @hibernate.collection-key * column="id" * @hibernate.collection-element * column="telefono" * type="string" * not-null="true" */public Set getNumeriTelefono() {return this.numeriTelefono;}public void setNumeriTelefono(final Set numeri) {this.numeriTelefono = numeri;}public void addIndirizzo(final Indirizzo indirizzo) {indirizzi.add(indirizzo);}public void removeIndirizzo(final Indirizzo indirizzo) {indirizzi.remove(indirizzo);}public void addTelefono(final String telefono) {numeriTelefono.add(telefono);}public void removeTelefono(final String telefono) {numeriTelefono.remove(telefono);}}/** * Persona. * * @hibernate.joined-subclass * @hibernate.joined-subclass-key * column="id" */public class Persona extends Soggetto { private Date dataNascita; private String nomeProprio; private String genere;/** Costruttore di default. */public Persona() {super();}/** * La data di nascita. * * @hibernate.property */public Date getDataNascita() {return this.dataNascita;}public void setDataNascita(final Date data) {this.dataNascita = data;}/** * Il nome proprio della persona. * * @hibernate.property * length="30" */public String getNomeProprio() {return this.nomeProprio;}public void setNomeProprio(final String nome) {this.nomeProprio = nome;}/** * Il genere, volgarmente detto sesso, che sarà "M" per il genere maschile ed "F" per quello * femminile. * * @hibernate.property * length="1" */public String getGenere() {return this.genere;}public void setGenere(final String genere) {this.genere = genere;}}/** * Indirizzo. */public class Indirizzo { private String indirizzo; private String comune; private String localita; private String nome; private String provincia; private String cap;/** Costruttore di default. */public Indirizzo() {indirizzo = null;comune = null;localita = null;nome = null;provincia = null;cap = null;} /** * Restituisce l‘indirizzo, p.es. "via Roma, 23". * @return Una stringa contenente la via, la piazza, ecc. * * @hibernate.property * length="255" */public String getIndirizzo() {return this.indirizzo;}/** * Imposta l‘indirizzo, p.es. "via Roma, 23". * @param indirizzo Una stringa contenente la via, la piazza, ecc. */public void setIndirizzo(final String indirizzo) {this.indirizzo = indirizzo;}/** * Il comune. * * @hibernate.property * length="60" */public String getComune() {return this.comune;}public void setComune(final String comune) {this.comune = comune;}/** * La località . * * @hibernate.property * length="255" */public String getLocalita() {return this.localita;}public void setLocalita(final String localita) {this.localita = localita;}/** * Nome identificativo per l‘indirizzo, p.es "abitazione", "ufficio", ecc. * * @hibernate.property * length="30" * not-null="true" */public String getNome() {return this.nome;}public void setNome(final String nome) {this.nome = nome;}/** * La provincia. * * @hibernate.property * length="60" */public String getProvincia() {return this.provincia;}public void setProvincia(final String provincia) {this.provincia = provincia;}/** * Il CAP (Codice Avviamento Postale). * * @hibernate.property * length="5" */public String getCap() {return this.cap;}public void setCap(final String cap) {this.cap = cap;}}
Quello che abbiamo fatto è di dire ad Hibernate quali sono le entità e le proprietà che devono essere rese persistenti. Il tag @hibernate.class che abbiamo inserito nella Javadoc della classe Soggetto dice che vogliamo farne una classe persistente. Il tag @hibernate.id per getId() segnala che la proprietà rappresenta la chiave dell‘entità ed il parametro generator-class=”native” chiede di usare una chiave autogenerata dal DBMS. Per tutte le altre proprietà semplici non facciamo altro che dire con @hibernate.property che vogliamo una proprietà persistente, in alcuni casi indicando la lunghezza del campo nel database. Hibernate, salvo diversamente indicato, determina il tipo e crea un attributo sul database con lo stesso nome della proprietà . Le proprietà più interessanti sono quelle che riguardano indirizzi e numeri di telefono.
/** * @hibernate.set * name="indirizzi" * @hibernate.collection-key * column="id" * @hibernate.collection-composite-element * class="Indirizzo" */public Set getIndirizzi() {return this.indirizzi;}
Con @hibernate.set specifichiamo che la proprietà Indirizzi è appunto un insieme, sul quale definiamo la chiave id. Inoltre, con @hibernate.collection-composite-element, specifichiamo anche che la proprietà Indirizzi è una composizione di oggetti di classe Indirizzo. poiché nel nostro modello la classe Indirizzo non rappresenta un‘entità con esistenza propria bensì un attributo composto di Soggetto, omettiamo nella stringa Javadoc della classe Indirizzo il tag @hibernate.class, ma specifichiamo comunque le proprietà persistenti. Per i numeri di telefono abbiamo una specifica analoga, con la sola differenza che l‘attributo numeriTelefono è multivalore, ma semplice, per cui possiamo definire l‘elemento in loco con @hibernate.collection-element fornendo il nome della colonna ed il tipo. L‘ultima notazione d‘interesse è quella per la classe Persona.
/** * @hibernate.joined-subclass * @hibernate.joined-subclass-key * column="id" */public class Persona extends Soggetto {
poiché Persona estende Soggetto, ereditandone proprietà e comportamenti, vogliamo che per la persistenza avvenga una cosa analoga. Per questo motivo segnaliamo ad Hibernate che la tabella Persona venga messa in join con la tabella Soggetto, ottenendo così l‘effetto desiderato. Infatti nel database avremo una situazione di questo tipo
Soggetto(id, partitaIva, nome)Persona(id, dataNascita, codiceFiscale, cognome, sesso)
dove ogni istanza di Persona corrisponderà un‘istanza di Soggetto con uguale id. Il nostro lavoro per quanto riguarda lo sviluppo del modello e della sua persistenza finisce qui. Ci resta solo il compito di creare uno script di Ant che generi lo schema del database e i file di mapping per noi. Prima però vediamo come utilizzare il nostro modello persistente una volta messo in piedi.
Usare il modello
La prima cosa da fare è inizializzare il motore di Hibernate e aprire le strutture necessarie.
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();Session session = sessionFactory.openSession();Transaction tx = session.beginTransaction();
Aggiungiamo una nuova persona:
Persona p = new Persona();p.setNome("Rocco");p.setCognome("Smitelson");p.setCodiceFiscale("SMTRCC66L31H501P");p.setSesso("M");// data di nascitaDate data = null;try {data = new SimpleDateFormat("dd/MM/yyyy").parse("31/10/1966");} catch (ParseException e) {System.out.println("Non è stato possibile fare il parsing della data.");}p.setDataNascita(data);// recapiti telefonicip.addTelefono("06123456789");p.addTelefono("34700112233");// indirizzoIndirizzo indirizzo = new Indirizzo();indirizzo.setRiferimento("abitazione");indirizzo.setIndirizzo("via della Paura, 90");indirizzo.setCap("00100");indirizzo.setLocalita("Pomezia");indirizzo.setProvincia("RM");indirizzo.setComune("Roma");p.addIndirizzo(indirizzo);
Come si può vedere lavoriamo sul modello senza consapevolezza della presenza di un database, utilizzando in tutto e per tutto Java e le sue strutture. La presenza del meccanismo di persistenza si avverte solo quando si decide di salvare l‘oggetto appena creato:
session.save(p);tx.commit();
Tutto qui. Hibernate si occupa di tradurre il nostro modello ad oggetti in quello relazionale in modo del tutto trasparente.
Generazione con Ant
Come dicevamo, Hibernate ha bisogno di un file di configurazione che indichi i parametri di accesso al database e gli schemi di mapping da utilizzare.
com.mysql.jdbc.Driver jdbc:mysql://localhost/test utente password org.hibernate.dialect.MySQLDialect true
Abbiamo indicato come risorsa di mapping il file Soggetto.hbm.xml. Questo file ancora non esiste, ma verrà generato automaticamente dallo script che stiamo per scrivere. Il pacchetto XDoclet fornisce un task di Ant per la generazione dei file di mapping a partire dai sorgenti, opportunamente arricchiti come abbiamo visto in precedenza. Allo stesso modo la libreria di Hibernate contiene un task di Ant per generare lo schema del database a partire dai file di mapping. Quello che occorre quindi è uno script build.xml di Ant configurato con gli opportuni classpath che lanci i task sui nostri sorgenti.
E‘ sufficiente scrivere lo script di Ant una volta per tutte, modificando soltanto le locazioni dei sorgenti e le directory di output caso per caso. Nel nostro esempio abbiamo i sorgenti in src/, i file di configurazione (come hibernate.cfg.xml) in src/conf/ e lo script build.xml in setup/. Lanciando lo script troveremo nella directory src/conf/ il file di mapping (con lo stesso path della classe mappata) ed in setup/ lo script SQL per generare lo schema del database. Se abbiamo fornito dei parametri di accesso al DBMS con i privilegi di scrittura troveremo lo schema già presente nel database!
Conclusioni
Hibernate è un motore di persistenza alquanto potente, che può agire in modi diversi, a seconda del momento in cui interviene nello sviluppo. Se si progetta un modello dati completamente ad oggetti Hibernate rende il meccanismo di persistenza del tutto trasparente, permettendo allo sviluppatore di interagire con un modello realmente ad oggetti anzichà© con tabelle o oggetti-tabella, realizzando così un‘astrazione completa della base dati.
Riferimenti
Manuale Hibernate, paragrafo 6.4.1
http://www.hibernate.org/hib_docs/v3/reference/en/html/mapping.html#mapping-xdoclet
Progetto XDoclet
http://xdoclet.sourceforge.net