Skip to content

4. JPA : in sintesi

Ci proponiamo di introdurre JPA (Java Persistence API) con alcuni esempi. JPA viene approfondito nel corso:

  • Persistenza Java 5 nella pratica: [http://tahe.developpez.com/java/jpa] - fornisce gli strumenti per costruire il livello di accesso ai dati con JPA

4.1. Il ruolo di JPA in un’architettura a livelli

Si invita il lettore a rileggere l’inizio di questo documento (paragrafo 2), che spiega il ruolo del livello JPA in un’architettura a livelli. Il livello JPA si inserisce nei livelli di accesso ai dati:

Il livello [DAO] interagisce con la specifica JPA. Indipendentemente dal prodotto che la implementa, l’interfaccia del livello JPA presentata al livello [DAO] rimane la stessa. Di seguito presentiamo alcuni esempi tratti da [ref1] che ci consentiranno di costruire il nostro livello JPA.

4.2. JPA - esempi

4.2.1. Esempio 1 - Rappresentazione a oggetti di una singola tabella

4.2.1.1. La tabella [personne]

Consideriamo un database con un’unica tabella [personne], il cui scopo è quello di memorizzare alcune informazioni relative a persone:

 
ID
chiave primaria della tabella
VERSION
versione della riga nella tabella. Ogni volta che i dati della persona vengono modificati, il suo numero di versione viene incrementato.
NOM
nome della persona
PRENOM
il suo nome
DATENAISSANCE
la sua data di nascita
MARIE
numero intero 0 (non sposato) o 1 (sposato)
NBENFANTS
numero di figli della persona

4.2.1.2. L'entità [Personne]

Ci troviamo nel seguente ambiente di esecuzione:

Il livello JPA [5] deve fungere da ponte tra il mondo relazionale del database [7] e il mondo degli oggetti [4] gestito dai programmi Java [3]. Questo collegamento si effettua tramite configurazione e vi sono due modi per farlo:

  1. utilizzando i file XML. Questo era praticamente l’unico modo per farlo fino all’avvento di JDK 1.5
  2. con le annotazioni Java a partire dalla versione 1.5 di JDK

In questo documento utilizzeremo esclusivamente il secondo metodo.

L’oggetto [Personne] che rappresenta la tabella [personne] presentata in precedenza potrebbe essere il seguente:


...

@SuppressWarnings("unused")
@Entity
@Table(name="Personne")
public class Personne implements Serializable{

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // costruttori
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
        setNom(nom);
        setPrenom(prenom);
        setDatenaissance(datenaissance);
        setMarie(marie);
        setNbenfants(nbenfants);
    }

    // toString
    public String toString() {
...
    }

    // getter e setter
...
}

La configurazione avviene tramite le annotazioni Java @Annotation. Le annotazioni Java vengono interpretate dal compilatore oppure da strumenti specializzati al momento dell’esecuzione. Ad eccezione dell’annotazione alla riga 3, destinata al compilatore, tutte le altre annotazioni sono qui destinate all’implementazione JPA utilizzata, Hibernate o Toplink. Verranno quindi interpretate al momento dell’esecuzione. In assenza di strumenti in grado di interpretarle, queste annotazioni vengono ignorate. Pertanto, la classe [Personne] sopra riportata potrebbe essere utilizzata in un contesto esterno a JPA.

Occorre distinguere due casi di utilizzo delle annotazioni JPA in una classe C associata a una tabella T:

  1. la tabella T esiste già: le annotazioni JPA devono quindi riprodurre ciò che già esiste (nome e definizione delle colonne, vincoli di integrità, chiavi esterne, chiavi primarie, ...)
  2. la tabella T non esiste e verrà creata in base alle annotazioni presenti nella classe C.

Il caso 2 è il più semplice da gestire. Con l’aiuto delle annotazioni JPA, indichiamo la struttura della tabella T che desideriamo. Il caso 1 è spesso più complesso. La tabella T potrebbe essere stata creata, molto tempo fa, al di fuori di qualsiasi contesto JPA. La sua struttura potrebbe quindi risultare poco adatta al ponte relazionale/oggetto di JPA. Per semplificare, consideriamo il caso 2, in cui la tabella T associata alla classe C verrà creata in base alle annotazioni JPA della classe C.

Commentiamo le annotazioni JPA della classe [Personne]:

  • riga 4: l’annotazione @Entity è la prima annotazione indispensabile. Va inserita prima della riga che dichiara la classe e indica che la classe in questione deve essere gestita dal livello di persistenza JPA. In assenza di questa annotazione, tutte le altre annotazioni JPA verrebbero ignorate.
  • riga 5: l'annotazione @Table indica la tabella del database di cui la classe è una rappresentazione. Il suo argomento principale è name, che indica il nome della tabella. In assenza di questo argomento, la tabella porterà il nome della classe, in questo caso [Personne]. Nel nostro esempio, l'annotazione @Table è quindi superflua.
  • riga 8: l'annotazione @Id serve a indicare il campo della classe che rappresenta la chiave primaria della tabella. Questa annotazione è obbligatoria. In questo caso indica che il campo id della riga 11 rappresenta la chiave primaria della tabella.
  • riga 9: l'annotazione @Column serve a collegare un campo della classe alla colonna della tabella di cui il campo è l'immagine. L'attributo name indica il nome della colonna nella tabella. In assenza di questo attributo, la colonna porta lo stesso nome del campo. Nel nostro esempio, l'argomento name non era quindi obbligatorio. L’argomento nullable=false indica che la colonna associata al campo non può assumere il valore NULL e che, pertanto, il campo deve necessariamente avere un valore.
  • riga 10: l’annotazione @GeneratedValue indica come viene generata la chiave primaria quando viene generata automaticamente dal SGBD. Questo sarà il caso in tutti i nostri esempi. Non è obbligatorio. Pertanto, il nostro soggetto potrebbe avere un numero di matricola che fungerebbe da chiave primaria e che non sarebbe generato da SGBD, ma fissato dall’applicazione. In questo caso, l’annotazione @GeneratedValue sarebbe assente. L’argomento strategy indica come viene generata la chiave primaria quando è generata da SGBD. I SGBD non utilizzano tutti la stessa tecnica di generazione dei valori della chiave primaria. Ad esempio:
Firebird
utilizza un generatore di valori chiamato prima di ogni inserimento
SQL server
il campo della chiave primaria è definito come di tipo Identity. Si ottiene un risultato simile al generatore di valori di Firebird, tranne per il fatto che il valore della chiave è noto solo dopo l’inserimento della riga.
Oracle
utilizza un oggetto denominato SEQUENCE che, anche in questo caso, svolge la funzione di generatore di valori

Il livello JPA deve generare comandi SQL diversi a seconda dei SGBD per creare il generatore di valori. Tramite la configurazione le viene indicato il tipo di SGBD che deve gestire. Di conseguenza, può determinare quale sia la strategia abituale di generazione dei valori della chiave primaria di quel SGBD. L'argomento strategy = GenerationType.AUTO indica al livello JPA che deve utilizzare questa strategia abituale. Questa tecnica ha funzionato in tutti gli esempi di questo documento per i sette SGBD utilizzati.

  • riga 14: l’annotazione @Version indica il campo utilizzato per gestire gli accessi concorrenti a una stessa riga della tabella.

Per comprendere questo problema di accesso concorrente a una stessa riga della tabella [personne], supponiamo che un’applicazione web consenta l’aggiornamento di una persona ed esaminiamo il seguente caso:

Al momento T1, un utente U1 accede alla modifica di una persona P. In quel momento, il numero di figli è 0. L’utente modifica tale numero portandolo a 1, ma prima che possa confermare la modifica, un altro utente, U2, avvia la modifica della stessa persona P. Poiché U1 non ha ancora confermato la propria modifica, U2 vede sullo schermo il numero di figli pari a 0. U2 scrive il nome della persona P in maiuscolo. Successivamente, U1 e U2 confermano le loro modifiche in quest’ordine. Sarà la modifica di U2 a prevalere: nel database, il nome verrà scritto in maiuscolo e il numero di figli rimarrà pari a zero, anche se U1 crede di averlo modificato in 1.

Il concetto di versione della persona ci aiuta a risolvere questo problema. Riprendiamo lo stesso caso d’uso:

Al momento T1, un utente U1 accede alla modifica di una persona P. In quel momento, il numero di figli è 0 e la versione è V1. L’utente porta il numero di figli a 1, ma prima che possa confermare la modifica, un altro utente U2 accede alla modifica della stessa persona P. Poiché U1 non ha ancora confermato la propria modifica, U2 vede il numero di figli pari a 0 e la versione V1. U2 scrive il nome della persona P in maiuscolo. Successivamente, U1 e U2 convalidano le proprie modifiche in questo ordine. Prima di convalidare una modifica, si verifica che chi modifica una persona P possieda la stessa versione della persona P attualmente registrata. Questo sarà il caso dell’utente U1. La sua modifica viene quindi accettata e si cambia la versione della persona modificata da V1 a V2 per segnalare che la persona ha subito una modifica. Al momento della convalida della modifica di U2, ci si accorgerà che U2 possiede una versione V1 della persona P, mentre attualmente la versione di quest’ultima è V2. A questo punto potremo comunicare all’utente U2 che qualcuno lo ha preceduto e che deve ripartire dalla nuova versione della persona P. Egli lo farà, recupererà una persona P della versione V2 che ora ha un figlio, scriverà il nome in maiuscolo e confermerà. La sua modifica sarà accettata se la persona P registrata ha ancora la versione V2. Alla fine, le modifiche apportate da U1 e U2 saranno prese in considerazione, mentre nel caso d’uso senza versione, una delle modifiche andava persa.

Il livello [DAO] dell’applicazione client può gestire autonomamente la versione della classe [Personne]. Ogni volta che verrà apportata una modifica a un oggetto P, la versione di tale oggetto verrà incrementata di 1 nella tabella. L’annotazione @Version consente di trasferire questa gestione al livello JPA. Il campo in questione non deve necessariamente chiamarsi version come nell’esempio. Può avere un nome qualsiasi.

I campi corrispondenti alle annotazioni @Id e @Version sono presenti per motivi di persistenza. Non sarebbero necessari se la classe [Personne] non dovesse essere persistita. Si vede quindi che un oggetto non ha la stessa rappresentazione a seconda che debba o meno essere persistito.

  • riga 17: ancora una volta l’annotazione @Column per fornire informazioni sulla colonna della tabella [personne] associata al campo nom della classe Personne. Qui si trovano due nuovi argomenti:
    • unique=true indica che il nome di una persona deve essere univoco. Ciò si tradurrà nel database con l’aggiunta di un vincolo di unicità sulla colonna NOM della tabella [personne].
    • length=30 imposta a 30 il numero di caratteri della colonna NOM. Ciò significa che il tipo di questa colonna sarà VARCHAR(30).
  • riga 24: l'annotazione @Temporal serve a indicare quale tipo SQL assegnare a una colonna/campo di tipo data/ora. Il tipo TemporalType.DATE indica una sola data senza ora associata. Gli altri tipi possibili sono TemporalType.TIME per codificare un'ora e TemporalType.TIMESTAMP per codificare una data con ora.

Commentiamo ora il resto del codice della classe [Personne]:

  • riga 6: la classe implementa l'interfaccia Serializable. La sérialisation di un oggetto consiste nel trasformarlo in una sequenza di bit. La désérialisation è l'operazione inversa. La serializzazione/deserializzazione viene utilizzata in particolare nelle applicazioni client/server in cui gli oggetti vengono scambiati tramite la rete. Le applicazioni client o server non sono a conoscenza di questa operazione, che viene eseguita in modo trasparente dai JVM. Affinché ciò sia possibile, è tuttavia necessario che le classi degli oggetti scambiati siano "contrassegnate" con la parola chiave Serializable.
  • riga 37: un costruttore della classe. Si noti che i campi id e version non fanno parte dei parametri. Infatti, questi due campi sono gestiti dal livello JPA e non dall’applicazione.
  • righe 51 e successive: i metodi get e set di ciascuno dei campi della classe. Si noti che le annotazioni JPA possono essere applicate ai metodi get dei campi anziché ai campi stessi. La posizione delle annotazioni indica la modalità che JPA deve utilizzare per accedere ai campi:
    • se le annotazioni sono applicate a livello di campo, JPA accederà direttamente ai campi per leggerli o scriverli
    • se le annotazioni sono applicate a livello di get, JPA accederà ai campi tramite i metodi get/set per leggerli o scriverli

È la posizione dell’annotazione @Id che determina la posizione delle annotazioni JPA in una classe. Se posizionata a livello di campo, indica un accesso diretto ai campi; se posizionata a livello di get, indica un accesso ai campi tramite i metodi get e set. Le altre annotazioni devono quindi essere posizionate allo stesso modo dell’annotazione @Id.

4.2.2. Configurazione del livello JPA

I test del livello JPA possono essere effettuati con la seguente architettura:

  • in [7]: il database che verrà generato a partire dalle annotazioni dell’entità [Personne] e dalle configurazioni aggiuntive definite in un file denominato [persistence.xml]
  • in [5, 6]: un livello JPA implementato da Hibernate
  • in [4]: l’entità [Personne]
  • in [3]: un programma di test in modalità console

La configurazione del livello JPA è gestita dal file [META-INF/persistence.xml]:

All'esecuzione, il file [META-INF/persistence.xml] viene cercato nel file Classpath dell'applicazione.

Esaminiamo la configurazione del livello JPA definita nel file [persistence.xml] del nostro progetto:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  provider -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- Classi persistenti -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- log SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- connessione JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  creazione automatica dello schema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialetto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  proprietà DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

Per comprendere questa configurazione, dobbiamo tornare sull’architettura di accesso ai dati della nostra applicazione:

  • il file [persistence.xml] configurerà i livelli [4, 5, 6]
  • [4]: implementazione Hibernate di JPA
  • [5]: Hibernate accede al database tramite un pool di connessioni. Un pool di connessioni è una riserva di connessioni aperte con SGBD. Un SGBD è utilizzato da più utenti, mentre per motivi di prestazioni non può superare un numero limite N di connessioni aperte contemporaneamente. Un codice ben scritto apre una connessione con il SGBD per un tempo minimo: invia i comandi SQL e chiude la connessione. Lo farà ripetutamente, ogni volta che avrà bisogno di lavorare con il database. Il costo dell’apertura e della chiusura di una connessione non è trascurabile ed è qui che entra in gioco il pool di connessioni. All’avvio dell’applicazione, il pool aprirà N1 connessioni con il SGBD. È a questo che l’applicazione richiederà una connessione aperta quando ne avrà bisogno. La connessione verrà restituita al pool non appena l’applicazione non ne avrà più bisogno, preferibilmente il più rapidamente possibile. La connessione non viene chiusa e rimane disponibile per l’utente successivo. Un pool di connessioni è quindi un sistema di condivisione delle connessioni aperte.
  • [6]: il driver JDBC utilizzato dal SGBD

Ora vediamo come il file [persistence.xml] configura i livelli [4, 5, 6] sopra indicati:

  • riga 2: il tag radice del file XML è <persistence>.
  • riga 3: <persistence-unit> serve a definire un’unità di persistenza. Possono esserci più unità di persistenza. Ciascuna di esse ha un nome (attributo name) e un tipo di transazione (attributo transaction-type). L’applicazione avrà accesso all’unità di persistenza tramite il suo nome, in questo caso jpa. Il tipo di transazione RESOURCE_LOCAL indica che l’applicazione gestisce autonomamente le transazioni con il SGBD. Questo sarà il caso in questione. Quando l’applicazione viene eseguita in un contenitore EJB3, può utilizzare il servizio di transazioni di quest’ultimo. In questo caso, si imposterà transaction-type=JTA (Java Transaction API). JTA è il valore predefinito quando l’attributo transaction-type è assente.
  • riga 5: il tag <provider> serve a definire una classe che implementa l'interfaccia [javax.persistence.spi.PersistenceProvider], interfaccia che consente all'applicazione di inizializzare il livello di persistenza. Poiché si utilizza un'implementazione JPA / Hibernate, la classe qui utilizzata è una classe di Hibernate.
  • riga 6: il tag <properties> introduce proprietà specifiche del particolare provider scelto. Pertanto, a seconda che si sia scelto Hibernate, Toplink, Kodo, ... si avranno proprietà diverse. Quelle che seguono sono specifiche di Hibernate.
  • riga 8: richiede a Hibernate di esplorare il classpath del progetto per individuare le classi con l’annotazione @Entity al fine di gestirle. Le classi @Entity possono anche essere dichiarate tramite i tag <class>nom_de_la_classe</class>, direttamente sotto il tag <persistence-unit>. È ciò che faremo con il provider JPA / Toplink.
  • Le righe 10-12, qui messe in commento, configurano i log della console di Hibernate:
    • riga 10: per decidere se visualizzare o meno i comandi SQL emessi da Hibernate sul SGBD. Ciò è molto utile durante la fase di apprendimento. A causa del ponte relazionale/oggetto, l’applicazione opera su oggetti persistenti ai quali applica operazioni di tipo [persist, merge, remove]. È molto interessante sapere quali sono i comandi SQL effettivamente emessi durante queste operazioni. Studiandoli, a poco a poco si arriva a intuire i comandi SQL che Hibernate genererà quando si esegue una determinata operazione sugli oggetti persistenti e il ponte relazionale/oggetto comincia a prendere forma nella mente.
    • riga 11: i comandi SQL visualizzati sulla console possono essere formattati in modo chiaro per facilitarne la lettura
    • riga 12: i comandi SQL visualizzati saranno inoltre commentati
  • le righe 15-19 definiscono il livello JDBC (livello [6] nell’architettura):
    • riga 15: la classe del driver JDBC del SGBD, in questo caso MySQL5
    • riga 16: l'URL del database utilizzato
    • righe 17, 18: l'utente della connessione e la sua password
  • riga 22: Hibernate deve conoscere il SGBD con cui sta interagendo. Infatti, i SGBD hanno tutti estensioni SQL proprietarie, un modo specifico per gestire la generazione automatica dei valori di una chiave primaria, ... il che fa sì che Hibernate debba conoscere il SGBD con cui sta lavorando per potergli inviare i comandi SQL che quest’ultimo comprenderà. [MySQL5InnoDBDialect] indica il SGBD MySQL5 con tabelle di tipo InnoDB che supportano le transazioni.
  • le righe 24-28 configurano il pool di connessioni c3p0 (livello [5] nell'architettura):
    • righe 24, 25: il numero minimo (predefinito 3) e massimo di connessioni (predefinito 15) nel pool. Il numero iniziale predefinito di connessioni è 3.
    • riga 26: tempo massimo di attesa, in millisecondi, per una richiesta di connessione da parte del client. Trascorso questo tempo, c3p0 restituirà un'eccezione.
    • riga 27: per accedere a BD, Hibernate utilizza comandi SQL preparati (PreparedStatement) che c3p0 può memorizzare nella cache. Ciò significa che se l’applicazione richiede una seconda volta un comando SQL preparato già presente nella cache, questo non dovrà essere preparato (la preparazione di un comando SQL ha un costo) e verrà utilizzato quello presente nella cache. Qui si indica il numero massimo di comandi SQL preparati che la cache può contenere, considerando tutte le connessioni (un comando SQL preparato appartiene a una connessione).
    • riga 28: frequenza di verifica, in millisecondi, della validità delle connessioni. Una connessione del pool può diventare non valida per vari motivi (il driver JDBC invalida la connessione perché è troppo lunga, il driver JDBC presenta dei "bug", ...).
  • riga 20: qui si richiede che, all’inizializzazione dell’unità di persistenza, venga generata la struttura del database degli oggetti @Entity. Hibernate dispone ora di tutti gli strumenti per emettere i comandi SQL di generazione delle tabelle del database:
    • la configurazione degli oggetti @Entity gli consente di individuare le tabelle da generare
    • le righe 15-18 e 24-28 gli consentono di stabilire una connessione con il SGBD
    • la riga 22 gli permette di sapere quale dialetto SQL utilizzare per generare le tabelle

Pertanto, il file [persistence.xml] qui utilizzato ricrea un nuovo database ad ogni nuova esecuzione dell’applicazione. Le tabelle vengono ricreate (create table) dopo essere state eliminate (drop table), qualora esistessero. Si noti che ovviamente questa operazione non deve essere eseguita su un database in produzione...

4.2.3. Esempio 2: relazione uno-a-molti

4.2.3.1. o dello schema del database

 
1
2

    alter table jpa06_article 
        drop 
        foreign key FKFFBDD9D8ECCE8750;

    drop table if exists jpa06_article;

    drop table if exists jpa06_categorie;

    create table jpa06_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa06_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa06_article 
        add index FKFFBDD9D8ECCE8750 (categorie_id), 
        add constraint FKFFBDD9D8ECCE8750 
        foreign key (categorie_id) 
references jpa06_categorie (id);
  • in [1], il database, e in [2], il suo DDL (MySQL5)

Un articolo A(id, versione, nome) appartiene esattamente a una categoria C(id, versione, nome). Una categoria C può contenere 0, 1 o più articoli. Si ha una relazione uno-a-molti (Categoria -> Articolo) e la relazione inversa molti-a-uno (Articolo -> Categoria). Questa relazione è rappresentata dalla chiave esterna che la tabella [article] possiede sulla tabella [categorie] (righe 24-28 della DDL).

4.2.3.2. Gli oggetti @Entity che rappresentano il database

Un articolo è rappresentato dalla seguente @Entity [Article]:


package entites;

...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {

    // campi
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relazione principale Articolo (molti) -> Categoria (uno)
    // implementata tramite una chiave esterna (categorie_id) in Articolo
    // Ogni Articolo ha necessariamente una Categoria (nullable=false)
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "categorie_id", nullable = false)
    private Categorie categorie;

    // costruttori
    public Article() {
    }

    // getter e setter
    ...
    // toString
    public String toString() {
        return String.format("Article[%d,%d,%s,%d]", id, version, nom, categorie.getId());
    }

}
  • righe 9-11: chiave primaria dell'@Entity
  • righe 13-15: il suo numero di versione
  • righe 17-18: nome dell'articolo
  • righe 20-25: relazione molti-a-uno che collega l'@Entity Article all'@Entity Categorie:
    • riga 23: l'annotazione ManyToOne. Il «Many» si riferisce all’@Entity Article in cui ci troviamo e il «One» all’@Entity Categorie (riga 25). Una categoria (One) può avere più articoli (Many).
    • riga 24: l'annotazione ManyToOne definisce la colonna chiave esterna nella tabella [article]. Si chiamerà (name) categorie_id e ogni riga dovrà avere un valore in questa colonna (nullable=false).
    • riga 25: la categoria a cui appartiene l’articolo. Quando un articolo verrà inserito nel contesto di persistenza, si richiede che la sua categoria non vi venga inserita immediatamente (fetch=FetchType.LAZY, riga 23). Non si sa se questa richiesta abbia senso. Vedremo.

Una categoria è rappresentata dalla seguente @Entity [Categorie]:


package entites;
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {

    // campi
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relazione inversa Categoria (uno) -> Articolo (molti) della relazione Articolo (molti) -> Categoria (uno)
    // inserimento a cascata da Categoria -> inserimento di Articoli
    // aggiornamento a cascata Categoria -> aggiornamento Articoli
    // cascata di eliminazione Categoria -> eliminazione Articoli
    @OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
    private Set<Article> articles = new HashSet<Article>();

    // costruttori
    public Categorie() {
    }

    // getter e setter
...
    // toString
    public String toString() {
        return String.format("Categorie[%d,%d,%s]", id, version, nom);
    }

    // associazione bidirezionale Categoria <--> Articolo
    public void addArticle(Article article) {
        // l'articolo viene aggiunto alla raccolta degli articoli della categoria
        articles.add(article);
        // l'articolo cambia categoria
        article.setCategorie(this);
    }
}
  • righe 8-11: la chiave primaria dell’@Entity
  • righe 12-14: la sua versione
  • righe 16-17: il nome della categoria
  • righe 19-24: l'insieme (set) degli articoli della categoria
    • riga 23: l'annotazione @OneToMany indica una relazione uno-a-molti. Il «One» indica l’@Entity [Categorie] in cui ci si trova, il «Many» il tipo [Article] della riga 24: una (One) categoria ha più (Many) articoli.
    • riga 23: l'annotazione è l'inverso (mappedBy) dell'annotazione ManyToOne applicata al campo categorie dell'@Entity Article: mappedBy=categoria. La relazione ManyToOne, definita sul campo categorie dell'@Entity Article, è la relazione principale. È indispensabile. Essa concretizza la relazione di chiave esterna che collega l'@Entity Article all'@Entity Categorie. La relazione OneToMany, definita sul campo articles dell'@Entity Categorie, è la relazione inversa. Non è indispensabile. Si tratta di una funzionalità che consente di recuperare gli articoli di una categoria. Senza questa funzionalità, tali articoli verrebbero recuperati tramite una query JPQL.
    • riga 23: cascadeType.ALL richiede che le operazioni (persist, merge, remove) eseguite su un'@Entity Categorie vengano propagate ai suoi articoli.
    • riga 24: gli articoli di una categoria saranno inseriti in un oggetto di tipo Set<Article>. Il tipo Set non accetta duplicati. Pertanto non è possibile inserire due volte lo stesso articolo nell’oggetto Set<Article>. Cosa si intende per «lo stesso articolo»? Per indicare che l’articolo a è lo stesso dell’articolo b, Java utilizza l’espressione a.equals(b). Nella classe Object, classe madre di tutte le classi, a.equals(b) è vero se a==b, c.a.d. se gli oggetti a e b occupano la stessa posizione in memoria. Si potrebbe voler dire che gli oggetti a e b sono gli stessi se hanno lo stesso nome. In questo caso, lo sviluppatore deve ridefinire due metodi nella classe [Article]:
  • equals: deve restituire vero se i due articoli hanno lo stesso nome
  • hashCode: deve restituire un valore intero identico per due oggetti [Article] che il metodo equals considera uguali. In questo caso, il valore sarà quindi costruito a partire dal nome dell'articolo. Il valore restituito da hashCode può essere un numero intero qualsiasi. Viene utilizzato in diversi contenitori di oggetti, in particolare nei dizionari (Hashtable).

La relazione OneToMany può utilizzare tipi diversi dal Set per memorizzare il Many, ad esempio oggetti List. Non tratteremo questi casi nel presente documento. Il lettore li troverà in [ref1].

  • riga 38: il metodo [addArticle] ci permette di aggiungere un articolo a una categoria. Il metodo provvede ad aggiornare entrambe le estremità della relazione OneToMany che collega [Categorie] a [Article].

4.3. L'API del livello JPA

Spieghiamo l’ambiente di esecuzione di un client JPA:

Sappiamo che il livello JPA [2] crea un ponte oggetto [3] / relazionale [4]. Si definisce «contesto di persistenza» l’insieme degli oggetti gestiti dal livello JPA nell’ambito di questo ponte oggetto/relazionale. Per accedere ai dati del contesto di persistenza, un client JPA [1] deve passare attraverso il livello JPA [2]:

  1. può creare un oggetto e richiedere al livello JPA di renderlo persistente. L’oggetto diventa quindi parte del contesto di persistenza.
  2. può richiedere al livello [JPA] un riferimento a un oggetto persistente esistente.
  3. può modificare un oggetto persistente ottenuto dal livello JPA.
  4. può richiedere al livello JPA di eliminare un oggetto dal contesto di persistenza.

Il livello JPA presenta al client un'interfaccia denominata [EntityManager] che, come suggerisce il nome, consente di gestire gli oggetti @Entity del contesto di persistenza. Di seguito sono riportati i principali metodi di questa interfaccia:

void persist(Object entity)
inserisce entity nel contesto di persistenza
void remove(Object entity)
rimuove entity dal contesto di persistenza
<T> T merge(T entity)
unisce un oggetto entity del client non gestito dal contesto di persistenza
con l'oggetto entity del contesto di persistenza avente la stessa chiave primaria.
Il risultato restituito è l'oggetto entity del contesto di persistenza.
<T> T find(Class<T> entityClass,
Object primaryKey)
inserisce nel contesto di persistenza un oggetto ricercato nel database
tramite la sua chiave primaria. Il tipo T dell'oggetto consente al livello JPA
di sapere quale tabella interrogare. L'oggetto persistente così creato viene restituito al client.
Query createQuery(String queryText)
crea un oggetto Query a partire da una query JPQL (Java Persistence
Query Language). Una query JPQL è analoga a una query SQL se
se non fosse che interroga oggetti anziché tabelle.
Query createNativeQuery(String queryText)
metodo analogo al precedente, tranne per il fatto che queryText è un
ordine SQL e non JPQL.
Query createNamedQuery(String name)
Metodo identico a createQuery, tranne per il fatto che l'ordine JPQL queryText è stato
è stato esternalizzato in un file di configurazione e associato a un nome.
È proprio questo nome a costituire il parametro del metodo.

Un oggetto EntityManager ha un ciclo di vita che non è necessariamente quello dell’applicazione. Ha un inizio e una fine. Pertanto, un client JPA può lavorare in successione con diversi oggetti EntityManager. Il contesto di persistenza associato a un EntityManager ha lo stesso ciclo di vita di quest’ultimo. Sono indissociabili l’uno dall’altro. Quando un oggetto EntityManager viene chiuso, il suo contesto di persistenza viene, se necessario, sincronizzato con il database e poi cessa di esistere. È necessario creare un nuovo EntityManager per disporre nuovamente di un contesto di persistenza.

Il client JPA può creare un EntityManager e quindi un contesto di persistenza con la seguente istruzione:


EntityManagerFactory emf = Persistence.createEntityManagerFactory("nom d'une unité de persistance");
  • javax.persistence.Persistence è una classe statica che consente di ottenere una factory di oggetti EntityManager. Questa factory è associata a una specifica unità di persistenza. Ricordiamo che il file di configurazione [META-INF/persistence.xml] consente di definire le unità di persistenza e che queste hanno un nome:

    <persistence-unit name="elections-dao-jpa-mysql-01PU" transaction-type="RESOURCE_LOCAL">

Nell’esempio sopra riportato, l’unità di persistenza si chiama elections-dao-jpa-mysql-01PU. Ad essa è associata un’intera configurazione specifica, in particolare il SGBD con cui opera. L'istruzione [Persistence.createEntityManagerFactory("elections-dao-jpa-mysql-01PU")] crea una fabbrica di oggetti di tipo EntityManagerFactory in grado di fornire oggetti EntityManager destinati a gestire contesti di persistenza legati all'unità di persistenza denominata elections-dao-jpa-mysql-01PU. L’ottenimento di un oggetto EntityManager e quindi di un contesto di persistenza avviene a partire dall’oggetto EntityManagerFactory nel modo seguente:

        EntityManager em = emf.createEntityManager();

I seguenti metodi dell’interfaccia [EntityManager] consentono di gestire il ciclo di vita del contesto di persistenza:

void close()
il contesto di persistenza viene chiuso. Forza la sincronizzazione del contesto di persistenza con il database:
  • se un oggetto del contesto non è presente nel database, viene inserito tramite un'operazione SQL INSERT)
  • se un oggetto del contesto è presente nel database ed è stato modificato dopo essere stato letto, viene eseguita un'operazione (SQL UPDATE) per salvare la modifica
  • se un oggetto del contesto è stato contrassegnato come «eliminato» al termine di un'operazione remove su di esso, viene eseguita un'operazione SQL DELETE per eliminarlo dal database.
void clear()
il contesto di persistenza viene svuotato di tutti i suoi oggetti ma non chiuso.
void flush()
il contesto di persistenza viene sincronizzato con il database secondo le modalità descritte per close()

Il client JPA può forzare la sincronizzazione del contesto di persistenza con il database utilizzando il metodo [EntityManager].flush precedente. La sincronizzazione può essere esplicita o implicita. Nel primo caso, spetta al client eseguire le operazioni flush quando desidera effettuare le sincronizzazioni; in caso contrario, queste vengono eseguite in determinati momenti che specificheremo di seguito. La modalità di sincronizzazione è gestita dai seguenti metodi dell’interfaccia [EntityManager]:

void setFlushMode(FlushModeType
flushMode)
Esistono due possibili valori per flushmode:
FlushModeType.AUTO (impostazione predefinita): la sincronizzazione avviene prima di
ogni richiesta SELECT effettuata sul database.
FlushModeType.COMMIT: la sincronizzazione avviene solo alla
al termine delle transazioni sul database.
FlushModeType getFlushMode()
indica la modalità di sincronizzazione attuale

Riassumiamo. Nella modalità FlushModeType.AUTO, che è quella predefinita, il contesto di persistenza verrà sincronizzato con il database nei seguenti momenti:

  1. prima di ogni operazione SELECT sul database
  2. al termine di una transazione sul database
  3. a seguito di un'operazione flush o close sul contesto di persistenza

Nella modalità FlushModeType.COMMIT, il comportamento è identico, tranne per l’operazione 1 che non viene eseguita. La modalità normale di interazione con il livello JPA è una modalità transazionale. Il client esegue diverse operazioni sul contesto di persistenza, all’interno di una transazione. In questo caso, i momenti di sincronizzazione del contesto di persistenza con il database corrispondono ai casi 1 e 2 sopra indicati nella modalità AUTO, e al solo caso 2 nella modalità COMMIT.

Concludiamo con il codice API dell’interfaccia Query, che consente di inviare comandi JPQL al contesto di persistenza oppure comandi SQL direttamente al database per recuperare i dati. L’interfaccia Query è la seguente:

  • 1 - Il metodo getResultList esegue un SELECT che restituisce diversi oggetti. Questi saranno ottenuti in un oggetto List. Questo oggetto è un’interfaccia. Essa fornisce un oggetto Iterator che consente di scorrere gli elementi della lista L nella forma seguente:

        Iterator iterator = L.iterator();
        while (iterator.hasNext()) {
             // utilizzare l'oggetto iterator.next() che rappresenta l'elemento corrente dell'elenco
...
}

L'elenco L può essere utilizzato anche con un for:


        for (Object o : L) {
             // utilizzare l'oggetto o
}
  • 2 - Il metodo getSingleResult esegue un comando JPQL / SQL SELECT che restituisce un unico oggetto.
  • 3 - Il metodo executeUpdate esegue un comando SQL di aggiornamento o eliminazione e restituisce il numero di righe interessate dall'operazione.
  • 4 - Il metodo setParameter(String, Object) consente di assegnare un valore a un parametro denominato di un comando JPQL configurato
  • 5 - Il metodo setParameter(int, Object) non identifica il parametro tramite il suo nome, ma tramite la sua posizione nell’ordine JPQL.

4.4. Le query JPQL

JPQL (Java Persistence Query Language) è il linguaggio di query del livello JPA. Il linguaggio JPQL è affine al linguaggio SQL dei database. Mentre SQL opera con le tabelle, JPQL opera con gli oggetti immagine di tali tabelle. Esamineremo un esempio all’interno della seguente architettura:

Il database che chiameremo [dbrdvmedecins2] è un database MySQL5 con quattro tabelle:

  

Raccoglie informazioni che consentono di gestire gli appuntamenti di un gruppo di medici.

4.4.1. La tabella [MEDECINS]

Contiene informazioni sui medici.

  • ID: numero identificativo del medico - chiave primaria della tabella
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • NOM: il cognome del medico
  • PRENOM: il suo nome
  • TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)

4.4.2. La tabella [CLIENTS]

I clienti dei diversi medici sono registrati nella tabella [CLIENTS]:

  • ID: numero identificativo del cliente - chiave primaria della tabella
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • NOM: il nome del cliente
  • PRENOM: il suo nome
  • TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)

4.4.3. La tabella [CRENEAUX]

Elenca le fasce orarie in cui sono possibili i RV:

  • ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • ID_MEDECIN: numero identificativo del medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna MEDECINS (ID).
  • HDEBUT: ora di inizio della fascia oraria
  • MDEBUT: minuti di inizio della fascia oraria
  • HFIN: ora di fine della fascia oraria
  • MFIN: minuti di fine della fascia oraria

La seconda riga della tabella [CRENEAUX] (cfr. [1] sopra) indica, ad esempio, che la fascia n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (dott.ssa Marie PELISSIER).

4.4.4. La tabella [RV]

Elenca i RV assegnati a ciascun medico:

  • ID: numero che identifica in modo univoco il RV – chiave primaria
  • JOUR: giorno del RV
  • ID_CRENEAU: fascia oraria del RV – chiave esterna sul campo [ID] della tabella [CRENEAUX] – determina sia la fascia oraria che il medico interessato.
  • ID_CLIENT: numero del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]

Questa tabella presenta un e vincolo di unicità sui valori delle colonne collegate (JOUR, ID_CRENEAU):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

Se una riga della tabella [RV] presenta il valore (JOUR1, ID_CRENEAU1) per le colonne (JOUR, ID_CRENEAU), tale valore non può comparire in nessun altro punto. Altrimenti, ciò significherebbe che due RV sono stati registrati contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database avvia un SQLException quando si verifica questo caso.

La riga di id pari a 3 (cfr. [1] sopra) indica che un RV è stato prenotato per la fascia oraria n. 20 e il cliente n. 4 il 23/08/2006. La tabella [CRENEAUX] ci indica che la fascia n. 20 corrisponde alla fascia oraria 16:20 - 16:40 e appartiene al medico n. 1 (la sig.ra Marie PELISSIER). La tabella [CLIENTS] ci indica che il cliente n. 4 è la signorina Brigitte BISTROU.

4.4.5. Generazione del database

Per creare le tabelle e compilarle è possibile utilizzare lo script [dbrdvmedecins2.sql]. Con [WampServer], si può procedere come segue:

  • in [1], cliccare sull'icona di [WampServer] e selezionare l'opzione [PhpMyAdmin] [2],
  • in [3], nella finestra che si è aperta, selezionare il link [Bases de données],
  • in [2], si crea un database a cui è stato assegnato il nome [4] e la codifica [5],
  • in [7], il database è stato creato. Si fa clic sul relativo link,
  • in [8], si importa un file SQL,
  • che si seleziona nel file system tramite il pulsante [9],
  • in [11], si seleziona lo script SQL e in [12] lo si esegue,
  • in [13], le quattro tabelle del database sono state create. Si segue uno dei link,
  • in [14], il contenuto della tabella.

In seguito, non torneremo più su questo database. Il lettore è tuttavia invitato a seguirne l’evoluzione nel corso dei programmi, soprattutto quando qualcosa non funziona.

4.4.6. Il livello [JPA]

Torniamo all’architettura dell’esempio:

Ora costruiamo il progetto Maven del livello [JPA].

4.4.7. Il progetto NetBeans

È il seguente:

  • in [1], si crea un progetto Maven di tipo [Java Application] [2],
  • in [3], si assegna un nome al progetto,
  • in [4], il progetto generato.

4.4.8. Generazione del livello [JPA]

Torniamo all’architettura che dobbiamo costruire:

Con NetBeans è possibile generare automaticamente il livello [JPA]. È interessante conoscere questi metodi di generazione automatica poiché il codice generato fornisce preziose indicazioni su come scrivere le entità JPA.

4.4.9. Creazione di una connessione NetBeans al database

  • avviare il SGBD MySQL 5 affinché il BD sia disponibile,
  • creare una connessione NetBeans al database [dbrdvmedecins2],
  • nella scheda [Services] [1], nel ramo [Databases] [2], selezionare il driver JDBC MySQL [3],
  • quindi selezionare l'opzione [4] "Connect Using" che consente di creare una connessione con un database MySQL,
  • in [5], inserire le informazioni richieste. In [6], il nome del database, in [7] l'utente del database e la sua password,
  • in [8] è possibile verificare le informazioni fornite,
  • in [9], il messaggio che ci si aspetta quando i dati sono corretti,
  • in [10], la connessione è stata stabilita. Qui si vedono le quattro tabelle del database a cui ci si è collegati.

4.4.10. Creazione di un'unità di persistenza

Torniamo all’architettura in fase di realizzazione:

Stiamo realizzando il livello [JPA]. La sua configurazione avviene in un file [persistence.xml] in cui si definiscono le unità di persistenza. Ciascuna di esse richiede le seguenti informazioni:

  • le caratteristiche JDBC di accesso al database (URL, utente, password),
  • le classi che fungeranno da immagini delle tabelle del database,
  • l'implementazione JPA utilizzata. Infatti, JPA è una specifica implementata da diversi prodotti. In questo caso, utilizzeremo Hibernate.

NetBeans può generare questo file di persistenza tramite un wizard.

  • fare clic con il tasto destro del mouse sul progetto e scegliere di creare un'unità di persistenza [1],
  • in [2], creare un'unità di persistenza,
  • in [3], assegnare un nome all'unità di persistenza che si sta creando,
  • in [4], scegliere l’implementazione JPA Hibernate (JPA 2.0),
  • in [5], indicare che le tabelle di BD sono già state create e che quindi non le si creerà. Si conferma la procedura guidata,
  • in [6], il nuovo progetto,
  • in [7], il file [persistence.xml] è stato generato nella cartella [META-INF],
  • in [8], sono state aggiunte nuove dipendenze al progetto Maven.

Il file [META-INF/persistence.xml] generato è il seguente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
    </properties>
  </persistence-unit>
</persistence>

Riporta le informazioni fornite nella procedura guidata:

  • riga 3: il nome dell'unità di persistenza,
  • riga 3: il tipo di transazioni con il database. In questo caso, RESOURCE_LOCAL indica che l’applicazione gestirà autonomamente le proprie transazioni,
  • righe 6-9: le proprietà JDBC della fonte dati.

Nella scheda [Design] è possibile avere una visione d’insieme del file [persistence.xml]:

Per ottenere i log di Hibernate, completiamo il file [persistence.xml] nel modo seguente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>      
    </properties>
  </persistence-unit>
</persistence>
  • riga 11: si richiede di visualizzare gli ordini SQL emessi da Hibernate,
  • riga 12: questa proprietà consente di visualizzarli in formato formattato.

Al progetto sono state aggiunte delle dipendenze. Il file [pom.xml] è il seguente:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-rdvmedecins-jpql-hibernate</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-rdvmedecins-jpql-hibernate</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.1.2</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.logging</groupId>
      <artifactId>jboss-logging</artifactId>
      <version>3.1.0.GA</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.spec.javax.transaction</groupId>
      <artifactId>jboss-transaction-api_1.1_spec</artifactId>
      <version>1.0.0.Final</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>4.1.2</version>
    </dependency>
    <dependency>
      <groupId>antlr</groupId>
      <artifactId>antlr</artifactId>
      <version>2.7.7</version>
    </dependency>
    <dependency>
      <groupId>dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>1.6.1</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate.javax.persistence</groupId>
      <artifactId>hibernate-jpa-2.0-api</artifactId>
      <version>1.0.1.Final</version>
    </dependency>
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.15.0-GA</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate.common</groupId>
      <artifactId>hibernate-commons-annotations</artifactId>
      <version>4.0.1.Final</version>
    </dependency>
  </dependencies>
</project>

Le dipendenze aggiunte riguardano tutte Hibernate ORM. Aggiungeremo la dipendenza del driver JDBC da MySQL:


    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>        

4.4.11. Generazione delle entità JPA

Le entità JPA possono essere generate tramite una procedura guidata di NetBeans:

  • in [1], si creano entità JPA da un database,
  • in [2], si seleziona la connessione [dbrdvmedecins2] creata in precedenza,
  • in [3], si selezionano tutte le tabelle del database associato,
  • in [4], si assegna un nome alle classi Java associate alle quattro tabelle,
  • nonché un nome di pacchetto [5],
  • in [6], JPA raggruppa le righe delle tabelle di BD in collezioni. Scegliamo la lista come collezione,
  • in [7], le classi Java create dall'assistente.

4.4.12. Le entità JPA generate

L’entità [Medecin] è l’immagine della tabella [medecins]. La classe Java è piena di annotazioni che rendono il codice poco leggibile a prima vista. Se si mantiene solo ciò che è essenziale per comprendere il ruolo dell’entità, si ottiene il codice seguente:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
  
@Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
  
  @Column(name = "TITRE")
  private String titre;

  @Column(name = "NOM")
  private String nom;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "PRENOM")
  private String prenom;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
  private List<Creneau> creneauList;

// costruttori
....

   // getter e setter
....

  @Override
  public int hashCode() {
  ...
  }

  @Override
  public boolean equals(Object object) {
  ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • alla riga 4, l’annotazione @Entity rende la classe [Medecin] un’entità JPA, c.a.d. una classe collegata a una tabella di BD tramite API e JPA,
  • riga 5, il nome della tabella BD associata all’entità JPA. Ogni campo della tabella corrisponde a un campo nella classe Java,
  • riga 6, la classe implementa l’interfaccia Serializable. Ciò è necessario nelle applicazioni client/server, dove le entità vengono serializzate tra il client e il server.
  • righe 10-11: il campo id della classe [Medecin] corrisponde al campo [ID] (riga 10) della tabella [medecins],
  • righe 13-14: il campo titolo della classe [Medecin] corrisponde al campo [TITRE] (riga 13) della tabella [medecins],
  • righe 16-17: il campo «nome» della classe [Medecin] corrisponde al campo [NOM] (riga 16) della tabella [medecins],
  • righe 19-20: il campo "versione" della classe [Medecin] corrisponde al campo [VERSION] (riga 19) della tabella [medecins]. In questo caso, l’assistente non riconosce che la colonna è in realtà una colonna di versione che deve essere incrementata ad ogni modifica della riga a cui appartiene. Per assegnarle questo ruolo, è necessario aggiungere l’annotazione @Version. Lo faremo in un passaggio successivo,
  • righe 22-23: il campo prenom della classe [Medecin] corrisponde al campo [PRENOM] della tabella [medecins],
  • righe 10-11: il campo id corrisponde alla chiave primaria [ID] della tabella. Le annotazioni delle righe 8-9 precisano questo punto,
  • riga 8: l'annotazione @Id indica che il campo annotato è associato alla chiave primaria della tabella,
  • riga 9: il livello [JPA] genererà la chiave primaria delle righe che inserirà nella tabella [Medecins]. Esistono diverse strategie possibili. In questo caso, la strategia GenerationType.IDENTITY indica che il livello JPA utilizzerà la modalità auto_increment della tabella MySQL,
  • righe 25-26: la tabella [creneaux] ha una chiave esterna sulla tabella [medecins]. Una fascia oraria appartiene a un medico. Viceversa, a un medico sono associate diverse fasce orarie. Si ha quindi una relazione uno (medico) a molti (slot), una relazione qualificata dall’annotazione @OneToMany da JPA (riga 25). Il campo della riga 26 conterrà tutti gli slot del medico. Ciò avviene senza alcuna programmazione. Per comprendere appieno la riga 25, è necessario presentare la classe [Creneau].

Essa è la seguente:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "MDEBUT")
  private int mdebut;

  @Column(name = "HFIN")
  private int hfin;

  @Column(name = "HDEBUT")
  private int hdebut;

  @Column(name = "MFIN")
  private int mfin;

  @Column(name = "VERSION")
  private int version;

  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin idMedecin;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
  private List<Rv> rvList;

// costruttori
...
// getter e setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}

Commentiamo solo le nuove annotazioni:

  • abbiamo detto che la tabella [creneaux] aveva una chiave esterna verso la tabella [medecins]: una fascia oraria è associata a un medico. Più fasce orarie possono essere associate allo stesso medico. Esiste una relazione dalla tabella [creneaux] alla tabella [medecins] che è definita come "molti (slot)" a "uno (medico)". È l’annotazione @ManyToOne della riga 32 che serve a definire la chiave esterna,
  • la riga 31 con l'annotazione @JoinColumn specifica la relazione di chiave esterna: la colonna [ID_MEDECIN] della tabella [creneaux] è una chiave esterna sulla colonna [ID] della tabella [medecins],
  • riga 33: un riferimento al medico titolare della fascia oraria. Anche in questo caso lo si ottiene senza programmazione.

Il collegamento tramite chiave esterna tra l’entità [Creneau] e l’entità [Medecin] è quindi concretizzato da due annotazioni:

  • nell’entità [Creneau]:

@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
private Medecin idMedecin;
  • nell’entità [Medecin]:

@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;

Le due annotazioni riflettono la stessa relazione: quella della chiave esterna della tabella [creneaux] verso la tabella [medecins]. Si dice che siano inverse l'una rispetto all'altra. Solo la relazione @ManyToOne è indispensabile. Essa definisce in modo inequivocabile la relazione di chiave esterna. La relazione @OneToMany è facoltativa. Se presente, si limita a fare riferimento alla relazione @ManyToOne a cui è associata. Questo è il significato dell’attributo mappedBy della riga 1 dell’entità [Medecin]. Il valore di questo attributo è il nome del campo dell’entità [Creneau] che presenta l’annotazione @ManyToOne, la quale specifica la chiave esterna. Sempre nella stessa riga 1 dell’entità [Medecin], l’attributo cascade=CascadeType.ALL definisce il comportamento dell’entità [Medecin] rispetto all’entità [Creneau]:

  • se si inserisce una nuova entità [Medecin] nel database, allora devono essere inserite anche le entità [Creneau] del campo della riga 2,
  • se si modifica un'entità [Medecin] nel database, allora devono essere modificate anche le entità [Creneau] del campo della riga 2,
  • se si elimina un'entità [Medecin] dal database, allora devono essere eliminate anche le entità [Creneau] del campo della riga 2.

Forniamo il codice delle altre due entità senza commenti particolari, poiché non introducono nuove notazioni.

L'entità [Client]


package rdvmedecins.jpa;

...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "TITRE")
  private String titre;

  @Column(name = "NOM")
  private String nom;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "PRENOM")
  private String prenom;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
  private List<Rv> rvList;

// costruttori
...
// getter e setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • le righe 24-25 riflettono la relazione di chiave esterna tra la tabella [rv] e la tabella [clients].

L'entità [Rv]:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;

  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau idCreneau;

  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client idClient;

   // costruttori
...

   // getter e setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • la riga 13 descrive il campo «giorno» di tipo Java Date. Si indica che nella tabella [rv], la colonna [JOUR] (riga 12) è di tipo data (senza ora),
  • righe 16-18: definiscono la relazione di chiave esterna che la tabella [rv] ha con la tabella [creneaux],
  • righe 20-22: definiscono la relazione di chiave esterna che la tabella [rv] ha con la tabella [clients].

La generazione automatica delle entità JPA ci permette di ottenere una base di lavoro. A volte è sufficiente, altre volte no. È il caso in questione:

  • è necessario aggiungere l’annotazione @Version ai vari campi versione delle entità,
  • è necessario scrivere metodi toString più espliciti di quelli generati,
  • le entità [Medecin] e [Client] sono analoghe. Le faremo derivare da una classe [Personne],
  • elimineremo le relazioni inverse @OneToMany delle relazioni @ManyToOne. Non sono indispensabili e comportano complicazioni di programmazione,
  • elimineremo la validazione @NotNull sulle chiavi primarie. Quando si salva un'entità JPA insieme a MySQL, l'entità iniziale ha una chiave primaria null. È solo dopo la persistenza nel database che la chiave primaria dell’elemento persistito assume un valore.

Con queste specifiche, le diverse classi diventano le seguenti:

La classe Persona viene utilizzata per rappresentare medici e clienti:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@MappedSuperclass
public class Personne implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Basic(optional = false)
  @Column(name = "TITRE")
  private String titre;

  @Basic(optional = false)
  @Column(name = "NOM")
  private String nom;

  @Basic(optional = false)
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @Basic(optional = false)
  @Column(name = "PRENOM")
  private String prenom;
// costruttori
...

// getter e setter
  ...

  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
  }
  
}
  • riga 6: si noti che la classe [Personne] non è essa stessa un’entità (@Entity). Sarà la classe padre delle entità. L’annotazione @MappedSuperClass indica questa situazione.

L’entità [Client] incapsula le righe della tabella [clients]. Deriva dalla precedente classe [Personne]:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
  private static final long serialVersionUID = 1L;

// costruttori
...

  @Override
  public int hashCode() {
...
  }

  @Override
  public boolean equals(Object object) {
  ...
  }

  @Override
  public String toString() {
    return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}
  • riga 6: la classe [Client] è un'entità JPA,
  • riga 7: è associata alla tabella [clients],
  • riga 8: deriva dalla classe [Personne].

L'entità [Medecin], che incapsula le righe della tabella [medecins], segue lo stesso modello:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
  private static final long serialVersionUID = 1L;

  // costruttori
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
  }
  
}

L'entità [Creneau] incapsula le righe della tabella [creneaux]:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;

@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @Column(name = "MDEBUT")
  private int mdebut;
  
  @Basic(optional = false)
  @Column(name = "HFIN")
  private int hfin;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "HDEBUT")
  private int hdebut;
  
  @Basic(optional = false)
  @Column(name = "MFIN")
  private int mfin;
  
  @Basic(optional = false)
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Medecin medecin;

  // costruttori
  ...

  // getter e setter
  ...
 
  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    // TODO: Avviso - questo metodo non funzionerà nel caso in cui i campi id non siano impostati
    ...
  }

  @Override
  public String toString() {
    return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
  }
}
  • le righe 40-42 modellano la relazione «molti a uno» esistente tra la tabella [creneaux] e la tabella [medecins] del database: un medico ha più fasce orarie, una fascia oraria appartiene a un solo medico.

L’entità [Rv] incapsula le righe della tabella [rv]:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;

@Entity
@Table(name = "rv")
public class Rv implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @Column(name = "JOUR")
  @Temporal(TemporalType.DATE)
  private Date jour;
  
  @JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Creneau creneau;
  
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client client;

   // costruttori
...

   // getter e setter
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    return String.format("Rv[%s, %s, %s]", id, creneau, client);
  }
}
  • le righe 27-29 modellano la relazione «molti a uno» esistente tra la tabella [rv] e la tabella [clients] (un cliente può comparire in più Rv) del database, mentre le righe 23-25 rappresentano la relazione «molti a uno» esistente tra la tabella [rv] e la tabella [creneaux] (una fascia oraria può comparire in più Rv).

4.4.13. Il codice di accesso ai dati

Aggiungeremo ora al progetto il codice di accesso ai dati tramite il livello JPA:

La classe [MainJpql] è la seguente:


package rdvmedecins.console;

import java.util.Scanner;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class MainJpql {

  public static void main(String[] args) {
    // EntityManagerFactory
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-rdvmedecins-jpql-hibernatePU");
    // entityManager
    EntityManager em = emf.createEntityManager();
    // scanner da tastiera
    Scanner clavier = new Scanner(System.in);
    // ciclo di immissione delle richieste JPQL
    System.out.println("Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :");
    String requete = clavier.nextLine();
    while (!requete.trim().equals("*")) {
      try {
        // visualizzazione del risultato della richiesta
        for (Object o : em.createQuery(requete).getResultList()) {
          System.out.println(o);
        }
      } catch (Exception e) {
        System.out.println("L'exception suivante s'est produite : " + e);
      }
      // si svuota il contesto di persistenza
      em.clear();
      // nuova richiesta
      System.out.println("---------------------------------------------");
      System.out.println("Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :");
      requete = clavier.nextLine();
    }
    // chiusura delle risorse
    em.close();
    emf.close();
  }
}
  • riga 12: creazione di EntityManagerFactory associato all'unità di persistenza che abbiamo creato in precedenza. Il parametro del metodo createEntityManagerFactory è il nome di questa unità di persistenza:

  <persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
    ...
</persistence-unit>
  • riga 14: creazione di EntityManager che gestisce il livello di persistenza,
  • riga 19: immissione di una query JPQL select,
  • righe 23-28: visualizzazione del risultato della query,
  • riga 20: l'immissione si interrompe quando l'utente digita *.

Domanda: indicare le query JPQL che consentono di ottenere le seguenti informazioni:


  • elenco dei medici in ordine decrescente in base ai loro nomi
  • elenco dei medici il cui titolo è 'Mr'
  • elenco delle fasce orarie della signora Pelissier
  • elenco degli appuntamenti fissati in ordine crescente di giorno
  • elenco dei clienti (nome) che hanno effettuato un appuntamento con la sig.ra PELISSIER il 24/08/2006
  • numero di clienti della sig.ra PELISSIER il 24/08/2006
  • i clienti che non hanno fissato un appuntamento
  • i medici che non hanno appuntamenti

Ci si ispirerà all'esempio del paragrafo 2.7 di [ref1]. Ecco un esempio di esecuzione:

Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :
select c from Client c
Hibernate: 
    select
        client0_.ID as ID2_,
        client0_.NOM as NOM2_,
        client0_.PRENOM as PRENOM2_,
        client0_.TITRE as TITRE2_,
        client0_.version as version2_ 
    from
        clients client0_
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
  • riga 2: la query JPQL,
  • righe 3-11: la query corrispondente SQL,
  • righe 12-15: il risultato della richiesta JPQL.

4.5. Collegamenti tra il contesto di persistenza e SGBD

4.5.1. La classe Persona

package entites;

...

@Entity
@Table(name = "jpa01_personne")
public class Personne {

  @Id
  @Column(name = "ID", nullable = false)
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Integer id;

  @Column(name = "VERSION", nullable = false)
  @Version
  private int version;

  @Column(name = "NOM", length = 30, nullable = false, unique = true)
  private String nom;

  @Column(name = "PRENOM", length = 30, nullable = false)
  private String prenom;

  @Column(name = "DATENAISSANCE", nullable = false)
  @Temporal(TemporalType.DATE)
  private Date datenaissance;

  @Column(name = "MARIE", nullable = false)
  private boolean marie;

  @Column(name = "NBENFANTS", nullable = false)
  private int nbenfants;

   // costruttori

  public Personne() {
  }

  public Personne(String nom, String prenom, Date datenaissance, boolean marie, int nbenfants) {
    setNom(nom);
    setPrenom(prenom);
    setDatenaissance(datenaissance);
    setMarie(marie);
    setNbenfants(nbenfants);
  }

   // toString

  public String toString() {
    return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(), getNom(), getPrenom(), 
                         new SimpleDateFormat("dd/MM/yyyy").format(getDatenaissance()), isMarie(), getNbenfants());
  }

   // getters e setters
...
}

4.5.2. Il programma di test

package tests;

....
import entites.Personne;

@SuppressWarnings("unchecked")
public class Test1 {

   // costanti
  private final static String TABLE_NAME = "jpa01_personne";  // Contesto di persistenza
  private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
  private static Personne p1;

  public static void main(String[] args) throws Exception {
     // pulizia del database
    log("clean");
    clean();

     // dump
    log("dump");
    dump();

     // test1
    log("test1");
    test1();

     // test2
    log("test2");
    test2();

     // chiusura EntityManagerFactory
    emf.close();
  }

   // visualizzazione contenuto tabella
  private static void dump() {
     // contesto di persistenza
    EntityManager em = emf.createEntityManager();
     // inizio transazione
    EntityTransaction tx = em.getTransaction();
    tx.begin();
     // visualizzazione persone
    for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
      System.out.println(p);
    }
     // fine transazione
    tx.commit();
     // fine contesto
    em.close();
  }

   // azzeramento BD
  private static void clean() {
     // contesto di persistenza
    EntityManager em = emf.createEntityManager();
     // inizio transazione
    EntityTransaction tx = em.getTransaction();
    tx.begin();
     // eliminare gli elementi della tabella PERSONNES
    em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
     // fine transazione
    tx.commit();
     // fine contesto
    em.close();
  }

   // log
  private static void log(String message) {
    System.out.println("main : ----------- " + message);
  }

   // gestione degli oggetti persistenti
  public static void test1() throws ParseException {
     // contesto di persistenza
    EntityManager em = emf.createEntityManager();
     // creazione persone
    p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
    Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
     // inizio transazione
    EntityTransaction tx = em.getTransaction();
    System.out.println("début transaction");
    tx.begin();
     // persistenza delle persone
     // i log mostrano che l'operazione SQL INSERT viene generata immediatamente dopo l'operazione di persistenza
     // probabilmente per ottenere la chiave primaria
    System.out.println(String.format("Personne p1 %s non persistée", p1));
    System.out.println("em.persist(p1)");
    em.persist(p1);
    System.out.println(String.format("Personne p1 %s persistée", p1));
     // persona p2
     // INSERT viene generato non appena viene eseguita l'operazione persist
    System.out.println(String.format("Personne p2 %s non persistée", p2));
    System.out.println("em.persist(p2)");
    em.persist(p2);
    System.out.println(String.format("Personne p2 %s persistée", p2));
    p2.setMarie(true);
    System.out.println(String.format("Personne p2 %s modifiée", p2));
     // l'operazione DELETE, collegata all'operazione di rimozione, viene eseguita solo al termine della transazione
    System.out.println("em.remove(p2)");
    em.remove(p2);
    System.out.println(String.format("Personne p2 %s supprimée", p2));
     // modifica p1
    p1.setNom("P1");
     // fine transazione
    System.out.println("fin transaction");
    tx.commit();
     // fine contesto
    em.close();
     // viene visualizzata la tabella
    dump();
  }

   // gestione degli oggetti persistenti
  public static void test2() throws ParseException {
     // contesto di persistenza
    EntityManager em = emf.createEntityManager();
     // inizio transazione
    EntityTransaction tx = em.getTransaction();
    System.out.println("début transaction");
    tx.begin();
     // si modifica la persona p1 attualmente distaccata
    System.out.println(String.format("Personne p1 %s actuelle non persistée", p1));
    p1.setMarie(false);
    System.out.println(String.format("Personne p1 %s nouvelle non persistée", p1));
     // si ricollega la persona P1
    System.out.println("em.merge(p1)");
    Personne p1b = em.merge(p1);
    System.out.println(String.format("Personne p1b %s attachée", p1b));
     // fine transazione
    System.out.println("fin transaction");
    tx.commit();
       // fine contesto
    em.close();
   // viene visualizzata la tabella
    dump();
  }
}

4.5.3. La configurazione di Hibernate

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
  <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
         <!- - provider -->
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
             <!-- Classi persistenti -->
      <property name="hibernate.archive.autodetection" value="class, hbm" />
      <property name="hibernate.show_sql" value="true"/>
....
             <! -- creazione automatica dello schema -->
      <property name="hibernate.hbm2ddl.auto" value="create" />
....
    </properties>
  </persistence-unit>
</persistence>

4.5.4. La configurazione di log4j.properties

# Invia i messaggi di log direttamente a stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

# Opzione logger radice
log4j.rootLogger=ERROR, stdout

# Opzioni di registrazione di Hibernate (INFO mostra solo i messaggi di avvio)
#log4j.logger.org.hibernate=DEBUG

# Registra gli argomenti di runtime dei parametri di binding di JDBC
log4j.logger.org.hibernate.type=DEBUG

4.5.5. I risultati

init:
deps-jar:
Compiling 1 source file to C:\data\travail\2008-2009\netbeans\jpa\hibernate-personnes-entites\build\classes
compile-single:
run-single:
main : ----------- pulizia
Hibernate: delete from jpa01_personne
main : ----------- dump
Hibernate: select personne0_.ID as ID0_, personne0_.DATENAISSANCE as DATENAIS2_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.VERSION as VERSION0_ from jpa01_personne personne0_ order by personne0_.NOM asc
main : ----------- test1
début transaction
Personne p1 [null,0,Martin,Paul,31/01/2000,true,2] non persistée
em.persist(p1)
Hibernate: insert into jpa01_personne (DATENAISSANCE, MARIE, NBENFANTS, NOM, PRENOM, VERSION) values (?, ?, ?, ?, ?, ?)
17:57:26,312 DEBUG DateType:133 - binding '31 janvier 2000' to parameter: 1
17:57:26,312 DEBUG BooleanType:133 - binding 'true' to parameter: 2
17:57:26,312 DEBUG IntegerType:133 - binding '2' to parameter: 3
17:57:26,312 DEBUG StringType:133 - binding 'Martin' to parameter: 4
17:57:26,312 DEBUG StringType:133 - binding 'Paul' to parameter: 5
17:57:26,312 DEBUG IntegerType:133 - binding '0' to parameter: 6
Personne p1 [1,0,Martin,Paul,31/01/2000,true,2] persistée
Personne p2 [null,0,Durant,Sylvie,05/07/2001,false,0] non persistée
em.persist(p2)
Hibernate: insert into jpa01_personne (DATENAISSANCE, MARIE, NBENFANTS, NOM, PRENOM, VERSION) values (?, ?, ?, ?, ?, ?)
17:57:26,328 DEBUG DateType:133 - binding '05 juillet 2001' to parameter: 1
17:57:26,328 DEBUG BooleanType:133 - binding 'false' to parameter: 2
17:57:26,328 DEBUG IntegerType:133 - binding '0' to parameter: 3
17:57:26,328 DEBUG StringType:133 - binding 'Durant' to parameter: 4
17:57:26,328 DEBUG StringType:133 - binding 'Sylvie' to parameter: 5
17:57:26,328 DEBUG IntegerType:133 - binding '0' to parameter: 6
Personne p2 [2,0,Durant,Sylvie,05/07/2001,false,0] persistée
Personne p2 [2,0,Durant,Sylvie,05/07/2001,true,0] modifiée
em.remove(p2)
Personne p2 [2,0,Durant,Sylvie,05/07/2001,true,0] supprimée
fin transaction
Hibernate: update jpa01_personne set DATENAISSANCE=?, MARIE=?, NBENFANTS=?, NOM=?, PRENOM=?, VERSION=? where ID=? and VERSION=?
17:57:26,343 DEBUG DateType:133 - binding '31 janvier 2000' to parameter: 1
17:57:26,343 DEBUG BooleanType:133 - binding 'true' to parameter: 2
17:57:26,343 DEBUG IntegerType:133 - binding '2' to parameter: 3
17:57:26,343 DEBUG StringType:133 - binding 'P1' to parameter: 4
17:57:26,359 DEBUG StringType:133 - binding 'Paul' to parameter: 5
17:57:26,359 DEBUG IntegerType:133 - binding '1' to parameter: 6
17:57:26,359 DEBUG IntegerType:133 - binding '1' to parameter: 7
17:57:26,359 DEBUG IntegerType:133 - binding '0' to parameter: 8
Hibernate: delete from jpa01_personne where ID=? and VERSION=?
17:57:26,359 DEBUG IntegerType:133 - binding '2' to parameter: 1
17:57:26,359 DEBUG IntegerType:133 - binding '0' to parameter: 2
Hibernate: select personne0_.ID as ID0_, personne0_.DATENAISSANCE as DATENAIS2_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.VERSION as VERSION0_ from jpa01_personne personne0_ order by personne0_.NOM asc
17:57:26,375 DEBUG IntegerType:172 - returning '1' as column: ID0_
17:57:26,390 DEBUG DateType:172 - returning '31 janvier 2000' as column: DATENAIS2_0_
17:57:26,390 DEBUG BooleanType:172 - returning 'true' as column: MARIE0_
17:57:26,390 DEBUG IntegerType:172 - returning '2' as column: NBENFANTS0_
17:57:26,390 DEBUG StringType:172 - returning 'P1' as column: NOM0_
17:57:26,390 DEBUG StringType:172 - returning 'Paul' as column: PRENOM0_
17:57:26,390 DEBUG IntegerType:172 - returning '1' as column: VERSION0_
[1,1,P1,Paul,31/01/2000,true,2]
main : ----------- test2
début transaction
Personne p1 [1,1,P1,Paul,31/01/2000,true,2] actuelle non persistée
Personne p1 [1,1,P1,Paul,31/01/2000,false,2] nouvelle non persistée
em.merge(p1)
Hibernate: select personne0_.ID as ID0_0_, personne0_.DATENAISSANCE as DATENAIS2_0_0_, personne0_.MARIE as MARIE0_0_, personne0_.NBENFANTS as NBENFANTS0_0_, personne0_.NOM as NOM0_0_, personne0_.PRENOM as PRENOM0_0_, personne0_.VERSION as VERSION0_0_ from jpa01_personne personne0_ where personne0_.ID=?
17:57:26,406 DEBUG IntegerType:133 - binding '1' to parameter: 1
17:57:26,406 DEBUG DateType:172 - returning '31 janvier 2000' as column: DATENAIS2_0_0_
17:57:26,406 DEBUG BooleanType:172 - returning 'true' as column: MARIE0_0_
17:57:26,406 DEBUG IntegerType:172 - returning '2' as column: NBENFANTS0_0_
17:57:26,406 DEBUG StringType:172 - returning 'P1' as column: NOM0_0_
17:57:26,406 DEBUG StringType:172 - returning 'Paul' as column: PRENOM0_0_
17:57:26,406 DEBUG IntegerType:172 - returning '1' as column: VERSION0_0_
Personne p1b [1,1,P1,Paul,31/01/2000,false,2] attachée
fin transaction
Hibernate: update jpa01_personne set DATENAISSANCE=?, MARIE=?, NBENFANTS=?, NOM=?, PRENOM=?, VERSION=? where ID=? and VERSION=?
17:57:26,406 DEBUG DateType:133 - binding '31 janvier 2000' to parameter: 1
17:57:26,406 DEBUG BooleanType:133 - binding 'false' to parameter: 2
17:57:26,406 DEBUG IntegerType:133 - binding '2' to parameter: 3
17:57:26,421 DEBUG StringType:133 - binding 'P1' to parameter: 4
17:57:26,421 DEBUG StringType:133 - binding 'Paul' to parameter: 5
17:57:26,421 DEBUG IntegerType:133 - binding '2' to parameter: 6
17:57:26,421 DEBUG IntegerType:133 - binding '1' to parameter: 7
17:57:26,421 DEBUG IntegerType:133 - binding '1' to parameter: 8
Hibernate: select personne0_.ID as ID0_, personne0_.DATENAISSANCE as DATENAIS2_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.VERSION as VERSION0_ from jpa01_personne personne0_ order by personne0_.NOM asc
17:57:26,453 DEBUG IntegerType:172 - returning '1' as column: ID0_
17:57:26,453 DEBUG DateType:172 - returning '31 janvier 2000' as column: DATENAIS2_0_
17:57:26,453 DEBUG BooleanType:172 - returning 'false' as column: MARIE0_
17:57:26,453 DEBUG IntegerType:172 - returning '2' as column: NBENFANTS0_
17:57:26,453 DEBUG StringType:172 - returning 'P1' as column: NOM0_
17:57:26,453 DEBUG StringType:172 - returning 'Paul' as column: PRENOM0_
17:57:26,453 DEBUG IntegerType:172 - returning '2' as column: VERSION0_
[1,2,P1,Paul,31/01/2000,false,2]
BUILD SUCCESSFUL (total time: 3 seconds)

Domanda: individuate la correlazione tra il codice Java e i risultati visualizzati.