Capitolo 1. Primi passi con Tomcat

1.1. Iniziare a lavorare con Hibernate

Questo corso introduttivo mostra come installare Hibernate 2.1 nel servlet container Apache Tomcat per realizzare una applicazione web. Hibernate funziona bene sia in ambienti gestiti dai principali server J2EE, sia in applicazioni java a sé stanti. Il sistema di gestione di basi di dati (DBMS) usato in questa introduzione è PostgreSQL 7.3, ma per farla funzionare su altri database è solo necessario modificare la configurazione del dialetto SQL che viene usato da Hibernate.

Prima di tutto, dobbiamo copiare tutte le librerie richieste nell'installazione di Tomcat. Usiamo un contesto web separato (webapps/quickstart) per questa introduzione, e quindi dobbiamo considerare sia il percorso di ricerca globale delle librerie (TOMCAT/common/lib) sia il classloader al livello del contesto in webapps/quickstart/WEB-INF/lib (per i file JAR) e webapps/quickstart/WEB-INF/classes. Ci riferiremo ai due livelli di classloader con i termini di "classpath globale" e "classpath di contesto".

Ora, copiate le librerie nei due classpath:

  1. Copiate il driver JDBC per il database nel classpath globale. Questo è richiesto per il software di gestione dei "lotti di connessioni" (connection pool) DBCP che è preinstallato con Tomcat. Hibernate usa le connessioni JDBC per eseguire l'SQL sul database, così gli si deve fornire connessioni JDBC provenienti da un pool o configurare Hibernate in modo tale da usare uno dei pool supportati direttamente (C3P0, Proxool). Per questo particolare tutoriale, copiate la libreria pg73jdbc3.jar (per ostgreSQL 7.3 e il JDK 1.4) nel percorso globale. Se voleste usare un database differente, copiate semplicemente il driver JDBC appropriato.

  2. Non copiate mai niente altro nel classpath globale di Tomcat, o avrete problemi con vari tool, compreso log4j, commons-logging e altri. Usate sempre il classpath di contesto per ogni applicazione web, cioè copiate le librerie in WEB-INF/lib e le vostre classi e file di configurazione/proprietà in WEB-INF/classes. Entrambe le cartelle sono situate per default nel classpath a livello di contesto.

  3. Hibernate è distribuito come una libreria JAR. Il file hibernate2.jar dovrebbe venire copiato nel classpath di contesto insieme con le altre classi dell'applicazione. Hibernate durante l'esecuzione richiede alcune librerie fornite da terze parti: queste sono fornite con la distribuzione di Hibernate nella cartella lib/; vedete Tabella 1.1, “ Librerie esterne richieste da Hibernate ”. Copiate ora le librerie richieste nel classpath di contesto.

Tabella 1.1.  Librerie esterne richieste da Hibernate

Libreria Descrizione
dom4j (obbligatoria) Hibernate usa dom4j per fare il parsing dei file di configurazione XML, così come i file di metadati (sempre in XML).
CGLIB (obbligatoria) Hibernate usa questa libreria di generazione del codice per potenziare le classi all'avvio (in combinazione con la "reflection" di Java)
Commons Collections, Commons Logging (obbligatorie) Hibernate usa varie librerie di utilità provenienti dal progetto Apache Jakarta Commons.
ODMG4 (obbligatoria) Hibernate fornisce un gestore di persistenza opzionale compatibile con la specifica ODMC. Se volete mappare delle collezioni, è obbligatorio anche se non intendete usare l'API ODMG. Non mapperemo collezioni, in questo articolo introduttivo, ma è comunque una buona idea copiare il JAR.
EHCache (obbligatoria) Hibernate può usare diversi fornitori di cache per la cache di secondo livello. EHCache è la cache predefinita, se non viene cambiata nella configurazione.
Log4j (opzionale) Hibernate usa l'API di Commons Logging, che a sua volta può usare Log4j come meccanismo sottostante di gestione delle tracce di esecuzione (logging). Se la libreria di Log4j è disponibile nella cartella di contesto, Commons Logging userà Log4j e il file di configurazione log4j.properties situato nel classpath di contesto. Un file di esempio per la configurazione di Log4j è fornito con la distribuzione di Hibernate. Quindi, copiate log4j,jar ed il file di configurazione (da src/) nel vostro classpath di contesto se volete vedere cosa succede dietro al sipario.
Richiesta o no? Date un'occhiata al file lib/README.txt nella distribuzione di Hibernate. È una lista aggiornata delle librerie di terze parti distribuite con Hibernate. Vi troverete elencate tutte le librerie opzionali ed obbligatorie.

Ora configureremo il pooling delle connessioni e la condivisione sia in Tomcat sia in Hibernate. Questo significa che tomcat fornirà connessioni JDBC estratte da un pool (usando le funzionalità offerte dalla libreria DBCP inclusa), e Hibernate richiederà queste connessioni via JNDI. Tomcat collega il pool di connessioni al JNDI se aggiungiamo la dichiarazione della risorsa al file di configurazione principale di Tomcat, TOMCAT/conf/server.xml:

<Context path="/quickstart" docBase="quickstart">
    <Resource name="jdbc/quickstart" scope="Shareable" type="javax.sql.DataSource"/>
    <ResourceParams name="jdbc/quickstart">
        <parameter>
            <name>factory</name>
            <value>org.apache.commons.dbcp.BasicDataSourceFactory</value>
        </parameter>

        <!-- DBCP database connection settings -->
        <parameter>
            <name>url</name>
            <value>jdbc:postgresql://localhost/quickstart</value>
        </parameter>
        <parameter>
            <name>driverClassName</name><value>org.postgresql.Driver</value>
        </parameter>
        <parameter>
            <name>username</name>
            <value>quickstart</value>
        </parameter>
        <parameter>
            <name>password</name>
            <value>secret</value>
        </parameter>

        <!-- DBCP connection pooling options -->
        <parameter>
            <name>maxWait</name>
            <value>3000</value>
        </parameter>
        <parameter>
            <name>maxIdle</name>
            <value>100</value>
        </parameter>
        <parameter>
            <name>maxActive</name>
            <value>10</value>
        </parameter>
    </ResourceParams>
</Context>

Il contesto che configuriamo in questo esempio di chiamaquickstart, ed ha base nella cartella TOMCAT/webapp/quickstart. Per accedere dei servlet, bisogna chiamare il percorso http://localhost:8080/quickstart nel browser (naturalmente, aggiungendo il nome del servlet così come è mappato nel file web.xml). Potete anche procedere e creare un semplice servlet, che abbia un metodo process() vuoto

Tomcat usa il pool di connessioni DBCP con questa configurazione e fornisce Connessioni JDBC da un pool reperito tramite JNDI all'indirizzo java:comp/env/jdbc/quickstart. Se avete problemi a far funzionare il pool di connessioni, fate riferimento alla documentazione di Tomcat. Se ricevete messaggi di eccezione dal driver JDBC, provate prima ad impostare il pool senza Hibernate. Sul web è possibile trovare degli articoli introduttivi sia su Tomcat sia su JDBC.

Il prossimo passo è configurare Hibernate, usando le connessioni dal pool collegato al JNDI. Usiamo la configurazione XML di Hibernate. L'approccio più semplice, che usa file di proprietà, è equivalente in funzionalità ma non offre nessun vantaggio. Usiamo quindi la configurazione XML perché di solito è più conveniente: il file di configurazione XML si trova nel classpath di contesto (WEB-INF/classes), come hibernate.cfg.xml:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration
    PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
    "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">

<hibernate-configuration>

    <session-factory>

        <property name="connection.datasource">java:comp/env/jdbc/quickstart</property>
        <property name="show_sql">false</property>
        <property name="dialect">net.sf.hibernate.dialect.PostgreSQLDialect</property>

        <!-- Mapping files -->
        <mapping resource="Cat.hbm.xml"/>

    </session-factory>

</hibernate-configuration>

Disattiviamo il tracciamento dei comandi SQL e diciamo ad Hibernate quale dialetto SQL deve venire usato, e dove prendere le connessioni JDBC (dichiarando l'indirizzo del datasource che dà accesso al pool collegato in Tomcat). Il dialetto è un'impostazione necessaria, poiché i database differiscono nella loro interpretazione dello "standard" SQL. Hibernate gestisce le differenze e viene fornito con dialetti per tutti i principali database commerciali e open source.

La SessionFactory è il concetto che in Hibernate corrisponde ad un contenitore di dati (datastore) univoco. Database multipli possono essere usati creando file di configurazione XML multipli e creando più oggettiConfiguration e SessionFactory nella vostra applicazione.

L'ultimo elemento del file hibernate.cfg.xml dichiara Cat.hbm.xml come nome di un file di mappaggio XML di Hibernate per la classe persistente Cat. Questo file contiene i metadati per il mappaggio delle classi POJO (acronimo che sta per "plain old java object", ovvero più o meno "oggetto java puro e semplice", contrapposto ad oggetti che implementano interfacce particolari come gli Enterprise Javabeans) verso una tabella di database (o verso tabelle multiple). Torneremo presto su questo file, prima però scriviamo la classe POJO e poi dichiariamo i suoi metadati di mappaggio.

1.2. La prima classe persistente

Hibernate funziona meglio con il modello degli oggetti POJO per le classi persistenti. Un POJO è più o meno come un JavaBean, con proprietà accessibili tramite metodi "getter" e "setter" (rispettivametne per recuperare e impostare le proprietà), mascherando la rappresentazione interna tramite l'interfaccia pubblicamente visibile:

package net.sf.hibernate.examples.quickstart;

public class Cat {

    private String id;
    private String name;
    private char sex;
    private float weight;

    public Cat() {
    }

    public String getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }

    public float getWeight() {
        return weight;
    }

    public void setWeight(float weight) {
        this.weight = weight;
    }

}

Hibernate non è limitato nel suo uso dei tipi di proprietà Java, tutti i tipi ed i tipi primitivi del JDK (comeString, char e Date) possono essere mappati, comprese le classi dal framework delle collezioni. Potete mapparle come valori, collezioni di valori o associazioni verso altre entità. La proprietà id è una proprietà speciale che rappresenta l'identificatore principale nel database per quella classe (chiave primaria), ed è fortemente raccomandato per entità come Cat. Hibernate può anche usare identificatori gestiti solo internamente, ma perderemmoe una parte della flessibilità nella nostra architettura applicativa.

Per le classi persistenti non è richiesta l'implementazione di alcuna interfaccia particolare, né dobbiamo ereditare da una speciale classe persistente radice. Hibernate non usa neppure alcun tipo di computazione in fase di "build" (costruzione del software), come manipolazione del codice binario (byte-code): si appoggia esclusivamente su reflection Java e su potenziamento in fase di esecuzione delle classi (tramite CGLIB). Così, possiamo mappare le classi POJO sul database senza alcuna dipendenza da Hibernate.

1.3. Mappare il gatto

Il file di mappaggio Cat.hbm.xml contiene i metadati richiesti per il mappaggio oggetto-relazione. I metadati includono la dichiarazione delle classi persistenti e il mappaggio delle proprietà sulle tabelle del database (tramite le colonne e le relazioni con altre entità gestite da chiavi esterne)

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

<hibernate-mapping>

    <class name="net.sf.hibernate.examples.quickstart.Cat" table="CAT">

        <!-- A 32 hex character is our surrogate key. It's automatically
            generated by Hibernate with the UUID pattern. -->
        <id name="id" type="string" unsaved-value="null" >
            <column name="CAT_ID" sql-type="char(32)" not-null="true"/>
            <generator class="uuid.hex"/>
        </id>

        <!-- A cat has to have a name, but it shouldn' be too long. -->
        <property name="name">
            <column name="NAME" length="16" not-null="true"/>
        </property>

        <property name="sex"/>

        <property name="weight"/>

    </class>

</hibernate-mapping>

Ogni classe persistente dovrebbe avere un attributo di identificazione (in realtà, solo classi che rappresentano entità, e non gli oggetti dipendenti, che sono mappati come componenti di un'entità). Questa proprità viene usata per distinguere oggetti persistenti: due gatti sono uguali se catA.getId().equals(catB.getId()) è vera: questo concetto è chiamata identità del database. Hibernate è fornito con vari generatori di identificatori per scenari differenti (compresi generatori nativi per "sequence" del database, tabelle hi/lo sul database, e identificatori assegnati dall'applicazione. Usiamo il generator UUID generator (raccomandato solo per il testing, poiché chiavi surrogate intere generate dal database dovrebbero venire preferite) e specifichiamo anche la colonna CAT_ID della tabella CAT per il valore dell'identificatore generato da Hibernate (come chiave primaria della tabella).

Tutte le altre proprietà di Cat sono mappate sulla stessa tabella. Nel caseo della proprietà name, l'abbiamo mappata con una colonna del database dichiarata esplicitamente. Questo è particolarmente utile quando si voglia che lo schema del database venga generato automaticamente (sotto forma di istruzioni SQL DDL) a partire dai file di mappaggio tramite lo strumento di Hibernate SchemaExport. Tutte le altre proprietà vengono mappate usando le impostazioni predefinite di Hibernate, che è ciò di cui si ha bisogno la maggior parte delle volte. La tabella CAT nel database appare così:

 Column |         Type          | Modifiers
--------+-----------------------+-----------
 cat_id | character(32)         | not null
 name   | character varying(16) | not null
 sex    | character(1)          |
 weight | real                  |
Indexes: cat_pkey primary key btree (cat_id)

Dovreste ora creare manualmente la tabella nel database, e più tardi leggere Capitolo 15, Guida degli strumenti se volete automatizzare questo passo con lo strumento SchemaExport. Questo strumento può generare un DDL SQL completo, comprendente definizioni delle tabelle, vincoli sui tipi delle colonne, vincoli di unicità e indici.

1.4. Giochiamo con i gatti

Ora siamo pronti per lanciare la Session Hibernate. È l'interfaccia di gestione della persistenza, la usiamo per memorizzare e recuperare istanze della classe Cat sul e dal database. Prima però dobbiamo recuperare una istanza di Session (che è l'unità di lavoro di Hibernate) dalla SessionFactory:

SessionFactory sessionFactory =
            new Configuration().configure().buildSessionFactory();

La SessionFactory è responsabile per un singolo database, e può usare un file di configurazione solo (hibernate.cfg.xml). Potete impostare altre proprietà (e anche cambiare i metadati di mappaggio) accedendo all'oggetto Configuration prima di costruire la SessionFactory (è immutabile). Ma dove creiamo la SessionFactory e come vi accediamo nella nostra applicazione?

Una SessionFactory viene solitamente costruita una volta sola, ad esempio all'avvio con un servlet impostato con il parametro load-on-startup. Questo significa anche che non dovreste tenerlo in una variabile di itanza nei vostri sevlet, ma in qualche altra posizione. Abbiamo bisogno di qualche tipo di Singleton, in modo tale da poter accedere facilmente alla SessionFactory. L'approccio che viene mostrato nel seguito mostra entrambi i problemi: configurazione e accesso facile ad una SessionFactory.

Implementiamo una classe di utilità HibernateUtil:

import net.sf.hibernate.*;
import net.sf.hibernate.cfg.*;

public class HibernateUtil {

    private static final SessionFactory sessionFactory;

    static {
        try {
            // Create the SessionFactory
            sessionFactory = new Configuration().configure().buildSessionFactory();
        } catch (HibernateException ex) {
            throw new RuntimeException("Configuration problem: " + ex.getMessage(), ex);
        }
    }

    public static final ThreadLocal session = new ThreadLocal();

    public static Session currentSession() throws HibernateException {
        Session s = (Session) session.get();
        // Open a new Session, if this Thread has none yet
        if (s == null) {
            s = sessionFactory.openSession();
            session.set(s);
        }
        return s;
    }

    public static void closeSession() throws HibernateException {
        Session s = (Session) session.get();
        session.set(null);
        if (s != null)
            s.close();
    }
}

Questa classe non si occupa solo della SessionFactory con il suo attributo statico, ma ha anche un ThreadLocal per mantenere la Session per il thread che è in esecuzione. Assicuratevi di capire il concetto di variabile thread-local in java prima di provare ad usare questa classe.

Una SessionFactory è "threadsafe", ovvero vari thread possono accedervi concorrentemente e richiedere oggetti Session. Una Session è un oggetto non-threadsafe, che rappresenta una singola unità di lavoro con il database. Le Session vengono aperte da una SessionFactory e vengono chiuse quando tutto il lavoro è completato.

Session session = HibernateUtil.currentSession();

Transaction tx= session.beginTransaction();

Cat princess = new Cat();
princess.setName("Princess");
princess.setSex('F');
princess.setWeight(7.4f);

session.save(princess);
tx.commit();

HibernateUtil.closeSession();

In una Session, ogni operazione sul database avviene in una transazione che isola le operazioni (anche quelle di sola lettura). Usiamo l'API Transaction di Hibernate per astrarre dalla strategia transazionale sottostante (nel nostro caso, transazioni JDBC). Questo consente di mettere in esecuzione il nostro codice con transazioni gestite dal contenitore (usando JTA) senza alcun cambiamento. Notate che l'esempio sopra non gestisce alcuna eccezione.

Notate anche che potete chiamare HibernateUtil.currentSession(); tutte le volte che volete, e otterrete sempre la Session corrente per il thread. Dovete assicurarvi che la Session venga chiusa dopo che l'unità di lavoro si completi, o nel servlet o in un servlet filter prima che la risposta HTTP venga inviata. L'effetto collaterale piacevole dell'ultimo approccio è un facile accesso ad un meccanismo di inizializzazione "lazy" (pigro, ovvero che carica i dati solo quando servono): la Session è ancora aperta quando la pagina viene generata, così Hibernate può caricare oggetti non inizializzati quando navigate nel grafo.

Hibernate ha vari metodi che possono essere usati per recuperare oggetti dal database. Il più flessibile è usare il linguaggio di query di HIbernate(HQL), che è facile da imparare ed una estensinoe potente ed orientata agli oggetti dell'SQL:

Transaction tx = session.beginTransaction();

Query query = session.createQuery("select c from Cat as c where c.sex = :sex");
query.setCharacter("sex", 'F');
for (Iterator it = query.iterate(); it.hasNext();) {
    Cat cat = (Cat) it.next();
    out.println("Female Cat: " + cat.getName() );
}

tx.commit();

Hibernate offre anche un'API ad oggetti di query per criteri che può essere usata per formulare query "type-safe" (ovvero il cui tipo viene verificato in fase di compilazione). Hibernate usa naturalmente oggetti PreparedStatement e binding di parametri per tutte le comunicazioni SQL con il database. Potete anche usare le funzionalità di interrogazione diretta via SQL di Hibernate, o ricevere una connessione JDBC da una Session.

1.5. Infine

Abbiamo solo sfiorato la superficie di Hibernate in questo breve articolo. Notate che non includiamo alcun codice specifico per i servlet nei nostri esempi: dovete creare un serlvet voi stessi ed inserire il codice di Hibernate come preferite.

Ricordate che Hibernate, come strato di accesso ai dati, è strettamente integrato nella vostra applicazione. Solitamente, tutti gli altri strati dipendono dal meccanismo di persistenza: siate certi di comprendere le implicazioni di questo design.