MokaByte 83 - Marzo 2004 
Motori di persistenza in Java
I parte: Hibernate
di
Massimiliano Bigatti
I motori di persistenza, come Hibernate, non sono altro che framework che, in modo meno invasivo possibile, consentono il salvataggio ed il recupero dello stato di un oggetto sulla base dati. Una evoluzione di Memento [gof] o Snapshot [grand98]? No: la persitenza qui non è ottenuta attraverso l'implementazione di pattern, ma principalmente tramite la riflessione, che consente nella piattaforma Java di ispezionare il contenuto degli oggetti per conoscerne i metodi e gli attributi, e quindi per accedere agli uno o agli altri.

EJB o POJO?
Questo tipo di persistenza trasparente somiglia da vicino a quella promessa dai bean di entità CMP, dove è il component container ad svolgere una funzione che corrisponde alla descrizione data sopra. La tecnologia EJB, però, richiede dei descrittori più complessi, ed impone l'utilizzo di interfacce e superclassi presenti nel framework EJB, come javax.ejb.EntityBean e javax.ejb.SessionBean.
I motori di persistenza come Hibernate lavorano invece con semplici POJO, acronimo che sta per Plain Old Java Object, termine coniato da Martin Fowler (http://www.martinfowler.com, di refactoriana memoria [fowler]) - e da altri. Un POJO indica un semplice oggetto Java, privo degli orpelli che solitamente vengono imposti dai framework per questa o quella funzione, come le sopracitate interfacce EJB. Un esempio di POJO è:

package com.mokabyte.samples.Prodotto;

public class Prodotto {
  String id;
  String descrizione;

  public String getId() {
    return id;
  }

  public void setId( String id ) {
    this.id = id;
  }

  public String getDescrizione() {
    return descrizione;
  }

  public void setDescrizione( String descrizione ) {
    this.descrizione = descrizione;
  }
}

Quante volte avete implementato una classe come questa, aderendo alle convenzioni sui nomi della specifica Javabean? Questo stile di programmazione è ormai divenuto uno standard, ed è sufficiente alla persistenza con Hibernate, se accompagnato da un file XML che definisce alcune informazioni basilari, come il nome della tabella su cui andare a leggere e scrivere ed il tipo dei dati coinvolti:

<hibernate-mapping>

<class name="com.mokabyte.samples.Prodotto" table="T_PRODOTTI">
<property name="id" type="string"/>
<property name="descrizione" type="string"/>
</class>

</hibernate-mapping>

 

Arriva MokaTrack
Per esemplificare un motore di persistenza è necessario fare riferimento ad un modello di entità non troppo banale, che consenta di affrontare i diversi problemi che si riscontrano in un modello applicativo reale. Per questo scopo utilizziamo il modello di un ipotetico software di usse tracking, MokaTrack, il cui modello (per la verità abbastanza basilare) è osservabile in figura 1.


Figura 1
- Le entità coinvolte nel modello di MokaTrack

 

Elenco dei progetti
Mokatrack raccoglie le segnalazioni sulla base del singolo progetto; l'elenco dei progetti disponibili è presentato dalla pagina index.jsp, che utilizza direttamente l'oggetto ProjectList per ottenerne l'elenco (ovviamente un approccio migliore sarebbe stato quello di utilizzare un oggetto helper). Questa pagina JSP 2.0 utilizza JSTL per produrre la tabella dei progetti e gli URL:

<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<html>
<body>
<h1>Elenco dei progetti</h1>

<jsp:useBean id="projectList" class="com.mokabyte.tracking.model.ProjectList"/>

<table cellspacing="10">
<tr>
<td>Chiave</td>
<td>Descrizione</td>
<td>Sito</td>
<td>Team leader</td>
</tr>
<c:forEach var="project" items="${projectList.projects}">
<c:url var="projectLink" value="project.jsp">
<c:param name="projectId" value="${project.id}" />
<c:param name="projectKey" value="${project.key}" />
</c:url>
<c:url var="extLink" value="${project.site}"/>

<tr>
<td>
<a href="${projectLink}">${project.key}</a>
</td>
<td>${project.description}</td>
<td>
<a href="${extLink}">${project.site}</a>
</td>
<td>${project.lead.name}</td>
</tr>
</c:forEach>
</table>

</body>
</html>

Come si osserva dal listato, l'elenco dei progetti avviene invocando il metodo getProjects(). Si noti che ProjectList non è un oggetto persistito con Hibernate, ma che le informazioni necessarie vengono ottenute con le opportune chiamate alle API dello stesso. In particolare, l'elenco dei progetti avviene eseguendo una find() con una espressione molto simile ad SQL:

public List getProjects() throws HibernateException, SQLException {
  createSession();
  return session.find("from projects in class com.mokabyte.tracking.model.Project");
}

Il linguaggio HSQL (Hibernate SQL) implementa molte delle funzionalità presenti in SQL, mantenendo però l'approccio ad oggetti.
L'oggetto session è una sessione Hibernate, ottenuta come segue:

Datastore ds = Hibernate.createDatastore()
.storeClass( com.mokabyte.tracking.model.Project.class )
.storeClass( com.mokabyte.tracking.model.Issue.class )
.storeClass( com.mokabyte.tracking.model.User.class );

SessionFactory sessions = ds.buildSessionFactory();
return sessions.openSession();

L'oggetto DataStore viene configurato con le classi che saranno persistite, attraverso il metodo storeClass(). Per ciascuna di queste classi sarà necessario un descrittore XML, oppure un solo descrittore cumulativo. Questi file XML vengono ricercati come risorse e quindi dovrebbero essere presenti nel CLASSPATH nello stesso package delle classi che definiscono.

 

Persistenza della classe Project
Il file XML che definisce la persistenza dell'oggetto Project contiene tre elementi principali: le proprietà dell'oggetto, il legame alla tabella delle segnalazioni e la relazionalità con la tabella degli utenti; un attributo di Project è infatti leader, che è di tipo User ed identifica il team leader del progetto. Nel dettaglio, troviamo:

  • class name. Nome completo di package della classe da persistere;
  • table. Nome della tabella SQL su cui scrivere le informazioni;
  • id. Chiave univoca della tabella;
  • property. Proprietà dell'oggetto, che viene mappata su una colonna della tabella;
  • many-to-one. Relazionalità con l'oggetto User
  • list. Mappatura dell'oggetto List presente in Project e che mantiene l'elenco delle segnalazioni.


<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-1.1.dtd">

<hibernate-mapping>

<class name="com.mokabyte.tracking.model.Project" table="projects">
<id name="id" column="id" type="long">
<generator class="native"/>
</id>
<property name="key" column="ukey" type="string" not-null="true"/>
<property name="description" type="string"/>
<property name="site" type="string"/>

<many-to-one
name="lead"
column="id_lead"
class="com.mokabyte.tracking.model.User"
/>

<list role="issues" table="issues" cascade="all">
<key column="id_project"/>
<index column="id"/>
<one-to-many class="com.mokabyte.tracking.model.Issue"/>
</list>

</class>
</hibernate-mapping>

La gestione dell'identificativo univoco è particolarmente interessante, perchè Hibernate dispone di una serie di meccanismi che permettono una ampia varietà di algoritmi predefiniti, quando nella tecnologia EJB non esiste un supporto similare. In particolare si trovano:

  • uuid.hex algoritmo UUID a 128 bit che risulta in una stringa di cifre esadecimali lunga 32. Utilizza l'IP della macchina per rendere la chiave univoc a livello di network.
  • uuid.string lo stesso di uuid.hex ma utilizza tutti i caratteri ASCII (non supportato su PostgreSQL)
  • vm.long long univoco all'interno della stessa VM. Non funziona in cluster
  • vm.hex come il precedente ma codifica in esadecimale
  • assigned fornito dall'applicazione
  • native consente di supportare le tipologie native presenti in MySQL, DB2, Sybase, Hypersonic SQL e MS SQL Server.
  • sequence consente di utilizzare i sequence su database quali DB2, PostgreSQL, Oracle, SAP DB, McKoi o un generatore su Interbase.
  • hilo.long utilizza l'algoritmo hi/lo per generare una chiave univoca di tipo long
  • hilo.hex come il precedente ma ritorna una stringa lunga 16 con cifre esadecimali
  • seqhilo.long utilizza l'algoritmo hi/lo a fronte di un sequence


Nel caso di MokaTrack sono stati utilizzati per tutte le tabelle identificativi ottenuti in modo nativo, visto che il database utilizzato è stato MySQL.

 

Configurazione del database
Per indicare ad Hibernate quale database utilizzare è necessario posizionare nelle classi di sistema un file di proprietà chiamato hibernate.properties, che dovrà contenere le informazioni di configurazione specifiche del database scelto. Ad esempio:

hibernate.dialect cirrus.hibernate.sql.MySQLDialect
hibernate.connection.driver_class com.mysql.jdbc.Driver
hibernate.connection.url jdbc:mysql://127.0.0.1/mokatrack
hibernate.connection.username root
hibernate.connection.password ***

E' particolarmente importante la proprietà hibernate.dialect, perchè identifica la classe che eseguirà la mappatura tra il dialetto SQL generico utilizzato da Hibernate con quello reale dello specifico database utilizzato. E' noto infatti che, se da una parte JDBC è una API standard per l'accesso a database di tipo diverso, è anche vero che il dialetto SQL utilizzato nei comandi dell'applicazione deve essere diverso in funzione del database. Questo strato di indirezione presente in Hibernate consente al motore di persistenza principale di utilizzare un dialetto unico che poi viene tradotto in quello specifico impiegato dal database utilizzato.

 

Aggiunta di una segnalazione
All'interno di MokaTrack è la pagina addIssue.jsp ad occuparsi di raccogliere le informazioni per la memorizzazione nel database di una nuova segnalazione:

<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<html>
<body>
<h2>Aggiunta di una segnalazione ${param.projectKey}</h2>

<jsp:useBean id="projectList" class="com.mokabyte.tracking.model.ProjectList"/>
<jsp:setProperty name="projectList" property="projectId" value="${param.projectId}"/>
<c:set var="issueList" value="${projectList.project}"/>

<form action="Project.jsp" method="post">

<!-- Campi nascosti necessari alla pagina destinataria -->
<input type="hidden" name="projectId" value="${param.projectId}"/>
<input type="hidden" name="projectKey" value="${param.projectKey}"/>

<table cellspacing="10">
<tr>
<td>Sommario</td>
<td><textarea name="summary" rows="10" cols="80"></textarea></td>
</tr>
<tr>
<td>Priorit&agrave;</td>
<td>
<select name="priority">
<option value="1">Triviale</option>
<option value="2">Minore</option>
<option value="3">Maggiore</option>
<option value="4">Critico</option>
<option value="5">Bloccante</option>
</select>
</td>
</tr>
<tr>
<td>Descrizione</td>
<td><textarea name="description" rows="25" cols="80"></textarea></td>
</tr>
</table>

<input name="submit" type="submit" value="addIssue"/>
</form>

</body>
</html>

Le informazioni raccolte vengono, in modo poco elegante, invate alla pagina Project.jsp per la memorizzazione, che avviene attraverso l'helper IssueAdder.java. La pagina Project.jsp contiene infatti le seguenti azioni che individuano l'eventuale arrivo dalla pagina addIssue.jsp; in tal caso viene creato un oggetto IssueAdder ed impostate le proprietà:

<c:if test="${!empty param.submit}">
<jsp:useBean id="issueAdder" class="com.mokabyte.tracking.process.IssueAdder"/>
<jsp:setProperty name="issueAdder" property="*"/>
<jsp:setProperty name="issueAdder" property="add" value="${param.projectId}"/>
</c:if>

La classe IssueAdder si limita a memorizzare le informazioni passate:

package com.mokabyte.tracking.process;

import java.sql.SQLException;
import cirrus.hibernate.*;

import com.mokabyte.tracking.model.*;
import com.mokabyte.tracking.util.*;

public class IssueAdder {

String summary;
int priority;
String description;


//Getter e setter
public void setSummary(String summary) {
this.summary = summary;
}

public void setPriority(int priority) {
this.priority = priority;
}

public void setDescription(String description) {
this.description = description;
}

public String getSummary() {
return summary;
}

public int getPriority() {
return priority;
}

public String getDescription() {
return description;
}

}

L'interazione con Hibernate avviene alla chiamata di setAdd(), a cui viene passato l'id del progetto a cui aggiungere la segnalazione. In una applicazione reale, avremmo già avuto l'oggetto Project corretto in sessione, ma in questo caso abbiamo solo l'ID, quindi è necessario rivolgersi alla classe ProjectList per ottenere l'oggetto Project corretto. Il codice carica anche un oggetto User per essere passato come utente che ha inviato la segnalazione; anche in questo caso l'utente sarebbe dovuto essere in sessione:

public void setAdd( String projectId ) throws HibernateException, SQLException {
Session session = HibernateUtils.getSession();

//L'utente andrebbe preso dalla sessione
User currentUser = (User)session.load( User.class, new Long( 1 ) );

ProjectList list = new ProjectList();
Project currentProject = list.getProject( projectId );
currentProject.addIssue( currentUser, summary, description, priority );
}

Come si nota osservando il listato, il caricamento di una specifica istanza di oggetto avviene tramite il metodo load() esposto dall'oggetto Session, che si aspetta come primo parametro la classe dell'oggetto da caricare e come secondo parametro la chiave relativa all'istanza desiderata.
A sua volta, il metodo Project.addIssue() aggiunge la segnalazione all'elenco aggiungendo semplicemente un nuovo oggetto Issue alla List.

public void addIssue( User user, String summary, String description,
int priority) throws HibernateException, SQLException {

Issue issue = new Issue( this, user, summary, description, priority );
issues.add( issue );
System.out.println("------------------------");
session.saveOrUpdate(this);
session.flush();
}

Per persistere questa modifica dell'oggetto è necessario chiamare il metodo saveOrUpdate(): questo verifica se l'oggetto è già presente in tabella e se lo è ne aggiorna gli eventuali dati cambiati. Si noti che Hibernate utilizza la chiave della tabella per come identità di un oggetto e con questo dato riesce a capire se una istanza è già presente sul database o meno. In questo caso la scrittura delle informazioni percorre tutta la gerarchia degli oggetti contenuti (quindi anche delle Collection come issues) fino ad arrivare agli oggetti finali (le entità). Comunque, la scrittura effettiva avviene solo all'esecuzione del metodo flush().

 

Conclusioni
Che dire di una possibile convergenza tra EJB CMP e framework di persistenza? Pendiamo dalle labbra di SUN che, con in mano anche JDO (Java Data Objects, una API standard per la persistenza di POJO) ha ampia scelta di movimento.
JBoss Group, dalla sua, ha già assunto il responsabile del progetto Hibernate, con il preciso scopo di integrare il suo prodotto nel noto application server open source e costruire appunto la persistenza CMP di JBoss con Hibernate. JBoss Group rassicura: il famoso motore di persistenza vivrà anche come entità a se stante, e non saremo obbligati ad utilizzare JBoss per poter sfruttare le potenzialità di Hibernate. Sarà vero? Noi speriamo di si, come speriamo che la logica "Embrace & Extend" rimanga una pratica lontana dal mondo open source. Anzi, che scompaia proprio, anche da Redmond.

 

Bibliografia
[gof] Gamma, altri -"Design Patterns", Addison-Wesley 1995
[grand98] Grand - "Java Design Patterns vol 1", Wiley 1998
[fowler] Martin Fowler - "Refactoring", Addison-Wesley 1999

 

Esempi
Scarica gli esempi allegati all'articolo

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it