Skip to content

2. Le entità JPA

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

2.1.1. La tabella [personne]

Consideriamo un database con una singola tabella [personne] il cui scopo è quello di memorizzare alcune informazioni su individui:

 
ID
chiave primaria della tabella
VERSION
versione della riga nella tabella. Ogni volta che
la persona viene modificata, 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

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
  1. con le annotazioni Java a partire dalla versione 1.5 di JDK

In questo documento utilizzeremo quasi 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 al di fuori di 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 stabilire il collegamento tra un campo della classe e la colonna della tabella di cui il campo è il corrispondente. 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, la nostra persona 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 dal 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, accede alla pagina di 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 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 contiene 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 posizionate 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.

2.1.3. Il progetto Eclipse dei test

Condurremo i nostri primi esperimenti con l’entità [Personne] sopra citata. Li realizzeremo 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

Effettueremo diversi esperimenti:

  • generare lo schema di BD a partire da uno script Ant e dallo strumento Hibernate Tools
  • generare l’entità BD e inizializzarla con alcuni dati
  • utilizzare il BD ed eseguire le quattro operazioni di base sulla tabella [personne] (inserimento, aggiornamento, cancellazione, interrogazione)

Gli strumenti necessari sono i seguenti:

  • Eclipse e i relativi plugin descritti nel paragrafo 5.2.
  • il progetto [hibernate-personnes-entites] che si trova nella cartella <esempi>/hibernate/direct/persone-entità
  • i vari file SGBD descritti negli allegati (paragrafo 5 e seguenti).

Il progetto Eclipse è il seguente:

  • in [1]: la cartella del progetto Eclipse
  • in [2]: il progetto importato in Eclipse (File / Import)
  • in [3]: l’entità [Personne] oggetto dei test
  • in [4]: i programmi di test
  • in [5]: [persistence.xml] è il file di configurazione del livello JPA
  • in [6]: le librerie utilizzate. Sono state descritte al paragrafo 1.5.
  • in [8]: uno script Ant che verrà utilizzato per generare la tabella associata all’entità [Personne]
  • in [9]: i file [persistence.xml] per ciascuno dei SGBD utilizzati
  • in [10]: gli schemi del database generato per ciascuno dei SGBD utilizzati

Descriveremo questi elementi uno dopo l’altro.

2.1.4. L’entità [Personne] (2)

Apportiamo una leggera modifica alla descrizione fornita in precedenza dell’entità [Personne], oltre a un’ulteriore informazione:


package entites;

...

@SuppressWarnings({ "unused", "serial" })
@Entity
@Table(name="jpa01_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) {
....
    }

    // 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
...
}
  • riga 7: assegniamo il nome [jpa01_personne] alla tabella associata all'entità [Personne]. Nel documento verranno create diverse tabelle in uno schema denominato jpa. Al termine di questo tutorial, lo schema jpa conterrà numerose tabelle. Per facilitare l'orientamento del lettore, le tabelle correlate tra loro avranno lo stesso prefisso jpaxx_.
  • riga 45: un metodo [toString] per visualizzare un oggetto [Personne] sulla console.

2.1.5. Configurazione del livello di accesso ai dati

Nel progetto Eclipse sopra riportato, la configurazione del livello JPA è gestita dal file [META-INF/persistence.xml]:

All’esecuzione, il file [META-INF/persistence.xml] viene cercato nella cartella classpath dell’applicazione. Nel nostro progetto Eclipse, tutto il contenuto delle cartelle [/src] e [1] viene copiato nelle cartelle [/bin] e [2]. Quest'ultima fa parte del classpath del progetto. Per questo motivo, [META-INF/persistence.xml] verrà individuato quando verrà configurato il livello JPA.

Per impostazione predefinita, Eclipse non inserisce i codici sorgente nella cartella [/src] del progetto, ma direttamente nella cartella principale. Tutti i nostri progetti Eclipse saranno invece configurati in modo che i sorgenti si trovino in [/src] e le classi compilate in [/bin], come illustrato nel paragrafo 5.2.1.

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 su 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 su 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
  • Qui utilizziamo elementi spiegati negli allegati al paragrafo 5.5. Si invita il lettore a consultare questa sezione su MySQL5.
  • riga 22: Hibernate deve conoscere il SGBD con cui sta lavorando. Infatti, i SGBD hanno tutti estensioni proprietarie SQL, 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 di connessioni predefinito è 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 al 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...

I test hanno dimostrato che la fase di drop/create delle tabelle poteva fallire. Ciò si è verificato in particolare quando, per lo stesso test, si passava da un livello JPA/Hibernate a un livello JPA/Toplink o viceversa. Partendo dagli stessi oggetti @Entity, le due implementazioni non generano esattamente le stesse tabelle, generatori, sequenze, ecc. e talvolta è capitato che la fase di drop/create fallisse e che si fosse costretti a eliminare le tabelle manualmente. La sezione “Allegati”, paragrafo 5 e seguenti, descrive le applicazioni utilizzabili per eseguire questo lavoro manualmente. Si noti che l’implementazione JPA/Hibernate si è dimostrata la più efficiente in questa fase di creazione iniziale del contenuto del database: i crash sono stati rari.

Gli strumenti utilizzati dal livello JPA / Hibernate si trovano nella libreria [jpa-hibernate], presentata al paragrafo 1.5, pagina 8. I driver JDBC necessari per accedere ai SGBD si trovano nella libreria [jpa-divers]. Queste due librerie sono state inserite nel classpath del progetto qui esaminato. Di seguito ne riportiamo il contenuto:

2.1.6. Generazione del database con uno script Ant

Come abbiamo appena visto, Hibernate fornisce strumenti per generare il database immagine degli oggetti @Entity dell’applicazione. Hibernate può:

  • generare il file di testo contenente i comandi SQL per la creazione del database. In questo caso viene utilizzato esclusivamente il dialetto specificato in [persistence.xml].
  • creare le tabelle corrispondenti agli oggetti @Entity nel database di destinazione definito in [persistence.xml]. In questo caso viene utilizzato l’intero file [persistence.xml].

Presenteremo uno script Ant in grado di generare lo schema del database e le tabelle degli oggetti @Entity. Questo script non è mio: riprende uno script analogo presente in [ref1]. Ant (Another Neat Tool) è uno strumento per l'esecuzione in batch di attività Java. Gli script Ant non sono facili da comprendere per un principiante. Ne useremo solo uno, quello che commenteremo ora:

  • in [1]: la struttura delle directory degli esempi di questo tutorial.
  • in [2]: la cartella [personnes-entites] del progetto Eclipse attualmente in esame
  • in [3]: la cartella <lib> contenente le cinque librerie JAR definite nel paragrafo 1.5.
  • in [4]: l'archivio [hibernate-tools.jar] necessario per una delle attività dello script [ant-hibernate.xml] che andremo a studiare.
  • in [5]: il progetto Eclipse e lo script [ant-hibernate.xml]
  • in [6]: la cartella [src] del progetto

Lo script [ant-hibernate.xml] [5] utilizzerà gli archivi JAR presenti nella cartella <lib> [3], in particolare l’archivio [hibernate-tools.jar] [4] della cartella [lib/hibernate]. Abbiamo riprodotto la struttura delle cartelle affinché il lettore possa vedere che, per trovare la cartella [lib] partendo dalla cartella [personnes-entites] [2] dello script [ant-hibernate.xml], è necessario seguire il percorso: ../../../lib.

Esaminiamo lo script [ant-hibernate.xml]:


<project name="jpa-hibernate" default="compile" basedir=".">

    <!-- nome del progetto e versione -->
    <property name="proj.name" value="jpa-hibernate" />
    <property name="proj.shortname" value="jpa-hibernate" />
    <property name="version" value="1.0" />

    <!-- Proprietà globali -->
    <property name="src.java.dir" value="src" />
    <property name="lib.dir" value="../../../lib" />
    <property name="build.dir" value="bin" />

    <!-- il classpath del progetto -->
    <path id="project.classpath">
        <fileset dir="${lib.dir}">
            <include name="**/*.jar" />
        </fileset>
    </path>

    <!-- i file di configurazione che devono essere presenti nel classpath-->
    <patternset id="conf">
        <include name="**/*.xml" />
        <include name="**/*.properties" />
    </patternset>

    <!-- Pulizia del progetto -->
    <target name="clean" description="Nettoyer le projet">
        <delete dir="${build.dir}" />
        <mkdir dir="${build.dir}" />
    </target>

    <!-- Compilazione del progetto -->
<target name="compile" depends="clean">
        <javac srcdir="${src.java.dir}" destdir="${build.dir}" classpathref="project.classpath" />
    </target>

    <!-- Copia dei file di configurazione nel classpath -->
    <target name="copyconf">
        <mkdir dir="${build.dir}" />
        <copy todir="${build.dir}">
            <fileset dir="${src.java.dir}">
                <patternset refid="conf" />
            </fileset>
        </copy>
    </target>

    <!-- Strumenti Hibernate -->
    <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask" classpathref="project.classpath" />

    <!-- Generare il file DDL del database -->
    <target name="DDL" depends="compile, copyconf" description="Génération DDL base">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- Utilizzare META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- esportazione -->
            <hbm2ddl drop="true" create="true" export="false" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>

    <!-- Generare il database -->
    <target name="BD" depends="compile, copyconf" description="Génération BD">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- Utilizza META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- esportazione -->
            <hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>
</project>
  • riga 1: il progetto [ant] si chiama "jpa-hibernate". Comprende una serie di attività, una delle quali è l'attività predefinita: in questo caso l'attività denominata "compile". Viene richiamato uno script ant per eseguire un'attività T. Se questa non è specificata, viene eseguita l'attività predefinita. basedir="." indica che, per tutti i percorsi relativi presenti nello script, il punto di partenza è la cartella in cui si trova lo script ant, in questo caso la cartella <esempi>/hibernate/direct/persone-entità.
  • righe 3-11: definiscono le variabili dello script con il tag <property name="nomVariable" value="valeurVariable"/>. La variabile può quindi essere utilizzata nello script con la notazione ${nomVariable}. I nomi possono essere qualsiasi cosa. Soffermiamoci sulle variabili definite alle righe 9-11:
    • riga 9: definisce una variabile denominata "src.java.dir" (il nome è a scelta) che, nel prosieguo dello script, indicherà la cartella contenente i codici sorgente Java. Il suo valore è "src", un percorso relativo alla cartella indicata dall’attributo basedir (riga 1). Si tratta quindi del percorso "./src", dove . indica in questo caso la cartella <exemples>/hibernate/direct/personnes-entites. È proprio nella cartella <personnes-entites>/src che si trovano i codici sorgente Java (cfr. [6] sopra).
    • riga 10: definisce una variabile denominata "lib.dir" che, nel prosieguo dello script, indicherà la cartella contenente gli archivi jar necessari alle attività Java dello script. Il suo valore ../../../lib indica la cartella <exemples>/lib (cfr. [3] sopra).
    • riga 11: definisce una variabile denominata "build.dir" che, nel prosieguo dello script, indicherà la cartella in cui devono essere generati i file .class derivanti dalla compilazione dei sorgenti .java. Il suo valore "bin" indica la cartella <personnes-entites>/bin. Abbiamo già spiegato che nel progetto Eclipse esaminato, la cartella <bin> era quella in cui venivano generati i file .class. Ant procederà allo stesso modo.
    • righe 14-18: il tag <path> serve a definire gli elementi del classpath che dovranno essere utilizzati dai task ant. In questo caso, il percorso "project.classpath" (il nome è a scelta) raggruppa tutti gli archivi .jar presenti nella struttura della cartella <exemples>/lib.
    • righe 21-24: il tag <patternset> serve a indicare un insieme di file tramite modelli di nomi. In questo caso, il modello denominato patternset indica tutti i file con estensione .xml o .properties. Questo patternset servirà a indicare i file .xml e .properties della cartella <src> (persistence.xml, log4j.properties) (cfr. [6]), che sono file di configurazione dell’applicazione. Al momento dell’esecuzione di determinate attività, questi file devono essere copiati nella cartella <bin> affinché si trovino nel classpath del progetto. Si utilizzerà quindi il patternset conf per identificarli.
    • righe 27-30: il tag <target> indica un'attività dello script. È la prima che incontriamo. Tutto ciò che l'ha preceduta riguarda la configurazione dell'ambiente di esecuzione dello script ant. L'attività si chiama clean. Viene eseguita in due fasi: la cartella <bin> viene eliminata (riga 28) per poi essere ricreata (riga 29).
    • righe 33-35: l’attività «compile», che è l’attività predefinita dello script (riga 1). Essa dipende (attributo «depends») dall’attività «clean». Ciò significa che, prima di eseguire l’attività «compile», ant deve eseguire l’attività «clean», c.a.d, per ripulire la cartella <bin>. Lo scopo dell’attività «compile» in questo caso è quello di compilare i sorgenti Java presenti nella cartella <src>.
    • riga 34: chiamata al compilatore Java con tre parametri:
      • srcdir: la cartella contenente i sorgenti Java, in questo caso la cartella <src>
      • destdir: la cartella in cui devono essere salvati i file .class generati, in questo caso la cartella <bin>
      • classpathref: il classpath da utilizzare per la compilazione, in questo caso tutti gli archivi jar presenti nella struttura della cartella <lib>
  • (continua)
    • righe 38-45: il task copyconf, il cui scopo è copiare nella cartella <bin> tutti i file .xml e .properties presenti nella cartella <src>.
    • riga 48: definizione di un'attività tramite il tag <taskdef>. Tale attività è destinata a essere riutilizzata in altri punti dello script. Si tratta di una semplificazione nella scrittura del codice. Poiché l'attività viene utilizzata in vari punti dello script, la si definisce una sola volta con il tag <taskdef> e la si riutilizza poi tramite il suo nome, quando necessario.
      • L'attività si chiama hibernatetool (attributo name).
      • La sua classe è definita dall’attributo classname. In questo caso, la classe indicata si trova nell’archivio [hibernate-tools.jar] di cui abbiamo già parlato.
      • L’attributo classpathref indica a ant dove cercare la classe precedente
  • (continua)
    • le righe 51-60 riguardano l’attività che ci interessa in questa sede, ovvero la generazione dello schema del database di immagini degli oggetti @Entity del nostro progetto Eclipse.
      • riga 51: l’attività si chiama DDL (come Data Definition Language, il SQL associato alla creazione degli oggetti di un database). Dipende dalle attività compile e copyconf in quest’ordine. L’attività DDL provocherà quindi, in ordine, l’esecuzione delle attività clean, compile e copyconf. Quando l’attività DDL viene avviata, la cartella <bin> contiene i file .class corrispondenti ai sorgenti .java, in particolare gli oggetti @Entity, nonché il file [META-INF/persistence.xml] che configura il livello JPA / Hibernate.
      • righe 53-59: viene richiamata l'attività [hibernatetool] definita alla riga 48. Le vengono passati numerosi parametri, oltre a quelli già definiti alla riga 48:
      • riga 53: la cartella di output dei risultati generati dall’attività sarà la cartella corrente.
      • riga 54: la cartella <bin> sarà la cartella di output dell’attività classpath
      • riga 56: indica alla task [hibernatetool] come può conoscere il proprio ambiente di esecuzione: il tag <jpaconfiguration/> le indica che si trova in un ambiente JPA e che deve quindi utilizzare il file [META-INF/persistence.xml] che troverà qui nel suo classpath.
      • la riga 58 definisce le condizioni di generazione del database: drop=true indica che devono essere emessi comandi SQL drop table prima della creazione delle tabelle; create=true indica che deve essere creato il file di testo contenente i comandi SQL per la creazione del database; outputfilename indica il nome di questo file SQL - in questo caso schema.sql nella cartella <ddl> del progetto Eclipse, mentre export=false indica che i comandi SQL generati non devono essere eseguiti in una connessione al SGBD. Questo punto è importante: implica che, per eseguire l’attività, non è necessario avviare il SGBD di destinazione. delimiter imposta il carattere che separa due comandi SQL nello schema generato, mentre format=true richiede che venga applicata una formattazione di base al testo generato.
  • (continua)
    • le righe 63-72 definiscono l’attività denominata BD. È identica alla precedente attività DDL, tranne per il fatto che questa volta genera il database (export="true" alla riga 70). L'attività apre una connessione all'attività SGBD utilizzando le informazioni presenti in [persistence.xml], per eseguire lo schema SQL e generare il database. Per eseguire l'attività BD, è quindi necessario che sia stata avviata l'attività SGBD.

2.1.7. Esecuzione del task prima di DDL

Per eseguire lo script [ant-hibernate.xml], occorre innanzitutto effettuare alcune configurazioni all’interno di Eclipse.

  • in [1]: selezionare [External Tools]
  • in [2]: creare una nuova configurazione ant
  • in [3]: assegnare un nome alla configurazione ant
  • in [5]: selezionare lo script ant utilizzando il pulsante [4]
  • in [6]: applicare le modifiche
  • in [7]: è stata creata la configurazione ant DDL
  • in [8]: nella scheda JRE si definisce il JRE da utilizzare. Il campo [10] è normalmente precompilato con il JRE utilizzato da Eclipse. Di norma, quindi, non è necessario intervenire su questo pannello. Tuttavia, mi è capitato un caso in cui lo script ant non riusciva a trovare il compilatore <javac>. Quest’ultimo non si trova in un JRE (Java Runtime Environment), ma in un JDK (Java Development Kit). Lo strumento ant di Eclipse individua questo compilatore tramite la variabile d’ambiente JAVA_HOME (Start / Pannello di controllo / Prestazioni e manutenzione / Sistema / scheda Avanzate / pulsante Variabili d’ambiente) [A]. Se questa variabile non è stata definita, è possibile consentire a ant di individuare il compilatore <javac> inserendo in [10], non un JRE ma un JDK. Quest’ultimo è disponibile nella stessa cartella di JRE e [B]. Si utilizzerà il pulsante [9] per dichiarare il JDK tra i JRE disponibili e il [C], in modo da poterlo poi selezionare in [10].
  • in [12]: nella scheda [Targets], si seleziona l’attività DDL. In questo modo, la configurazione ant, che abbiamo denominato DDL [7], corrisponderà all’esecuzione dell’attività denominata DDL [12] che, come sappiamo, genera lo schema DDL del database delle immagini degli oggetti @Entity dell’applicazione.
  • in [13]: si convalida la configurazione
  • in [14]: la si esegue

Nella vista [console] si ottengono i log relativi all’esecuzione dell’attività ant DDL:


Buildfile: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\ant-hibernate.xml
clean:
   [delete] Deleting directory C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
    [mkdir] Created dir: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
compile:
    [javac] Compiling 3 source files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
copyconf:
     [copy] Copying 2 files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
DDL:
[hibernatetool] Executing Hibernate Tool with a JPA Configuration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] drop table if exists jpa01_personne;
[hibernatetool] create table jpa01_personne (
[hibernatetool] ID integer not null auto_increment,
[hibernatetool] VERSION integer not null,
[hibernatetool] NOM varchar(30) not null unique,
[hibernatetool] PRENOM varchar(30) not null,
[hibernatetool] DATENAISSANCE date not null,
[hibernatetool] MARIE bit not null,
[hibernatetool] NBENFANTS integer not null,
[hibernatetool] primary key (ID)
[hibernatetool] ) ENGINE=InnoDB;
BUILD SUCCESSFUL
Total time: 5 seconds
  • si ricorda che l’attività DDL ha come nome [hibernatetool] (riga 10) e che dipende dalle attività clean (riga 2), compile (riga 5) e copyconf (riga 7).
  • riga 10: l'attività [hibernatetool] utilizza il file [persistence.xml] di una configurazione JPA
  • riga 11: il processo [hbm2ddl] genererà lo schema DDL del database
  • righe 12-22: lo schema DDL del database

Ricordiamo che avevamo richiesto all’attività [hbm2ddl] di generare lo schema DDL in una posizione specifica:


<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
  • riga 74: lo schema deve essere generato nel file ddl/schema.sql. Verifichiamo:
  • in [1]: il file ddl/schema.sql è effettivamente presente (eseguire F5 per aggiornare l’albero)
  • in [2]: il suo contenuto. Si tratta dello schema di un database MySQL5. Il file di configurazione [persistence.xml] del livello JPA specificava infatti un SGBD MySQL5 (riga 8 qui sotto):


            <!-- connessione JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
...
            <!--  creazione automatica dello schema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialetto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  proprietà DataSource c3p0 -->
...

Esaminiamo il collegamento oggetto/relazionale che è stato realizzato qui, analizzando la configurazione dell’oggetto @Entity Personne e lo schema DDL generato:

Si notino alcuni punti:

  • A1-B1: il nome della tabella specificato in A1 corrisponde effettivamente a quello utilizzato in B1. Si noti che drop precede create in B1.
  • A2-B2: mostra la modalità di generazione della chiave primaria. La modalità AUTO specificata in A2 si è tradotta nell'attributo autoincrement proprio di MySQL5. La modalità di generazione della chiave primaria è solitamente specifica per SGBD.
  • A3-B3: mostra il bit del tipo SQL specifico per MySQL5 per rappresentare un tipo Java boolean.

Ripetiamo questo test con un altro SGBD:

  • la cartella [conf] [1] contiene i file [persistence.xml] per vari SGBD. Prendiamo ad esempio il file Oracle [2] e inseriamolo nella cartella [META-INF] [3] al posto di quello precedente. Il suo contenuto è il seguente:

<?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="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <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.OracleDialect" />
            <!--  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>

Si invita il lettore a consultare negli allegati la sezione dedicata a Oracle (paragrafo 5.7), in particolare per comprendere la configurazione di JDBC.

Solo la riga 25 è realmente importante in questo contesto: si indica a Hibernate che d'ora in poi il SGBD è un SGBD Oracle. L'esecuzione del task ant DDL produce il risultato [4] riportato sopra. Si noti che lo schema Oracle è diverso dallo schema MySQL5. Questo è uno dei punti di forza di JPA: lo sviluppatore non deve preoccuparsi di questi dettagli, il che aumenta notevolmente la portabilità dei suoi sviluppi.

2.1.8. Esecuzione del task ant BD

Si ricorderà forse che il task ant, denominato BD, svolge la stessa funzione del task ant DDL, ma genera anche il database. È quindi necessario che venga avviata l’attività SGBD. Consideriamo il caso di SGBD e MySQL5 e invitiamo il lettore a copiare il file [conf/mysql5/persistence.xml] nella cartella [src/META-INF]. Per verificare il funzionamento dell’attività, utilizzeremo il plugin SQL Explorer (cfr. paragrafo 5.2.6) per verificare lo stato del file jpa BD prima e dopo l’esecuzione delle attività ant e BD.

Innanzitutto, occorre creare una nuova configurazione ant per eseguire l'attività BD. Si invita il lettore a seguire la procedura illustrata per la configurazione ant DDL al paragrafo 2.1.7. La nuova configurazione ant si chiamerà BD:

  • in [1]: si duplica la configurazione precedente denominata DDL
  • in [2]: la nuova configurazione viene denominata BD. Essa esegue l’attività ant BD [3] che genera fisicamente il database.
  • Fatto ciò, avviare SGBD e MySQL5 (paragrafo 5.5).

Ora utilizziamo il plugin SQL Explorer per esplorare i database gestiti da SGBD. Il lettore deve prima familiarizzarsi con questo plugin, se necessario (cfr. paragrafo 5.2.6).

  • [1]: si apre la prospettiva SQL Explorer [Window / Open Perspective / Other]
  • [2]: se necessario, creare una connessione [mysql5-jpa] (cfr. paragrafo 5.5.5, pagina 252) e aprirla
  • [3]: si effettua l’autenticazione jpa / jpa
  • [4]: si è connessi a MySQL5.
  • in [5]: il jpa BD ha una sola tabella: [articles]
  • in [6]: si avvia l’esecuzione dell’attività ant BD. Poiché ci si trova nella prospettiva [SQL Explorer], non è visibile la vista [Console] che mostra i log dell'attività. È possibile visualizzare questa vista [Window / Show View / ...] oppure tornare alla prospettiva Java [Window / Open Perspective / ...].
  • In [7]: una volta completata l’attività Ant BD, tornare eventualmente alla prospettiva [SQL Explorer] e aggiornare la struttura ad albero del JPA BD.
  • in [8]: si vede la tabella [jpa01_personne] che è stata creata.

Si invita il lettore a ripetere questa generazione di BD con altri SGBD. La procedura da seguire è la seguente:

  • copiare il file [conf/<sgbd>/persistence.xml] nella cartella [src/META-INF], dove <sgbd> è il SGBD testato
  • avviare <sgbd> seguendo le istruzioni riportate negli allegati relativi a tale file
  • nella vista SQL Explorer, creare una connessione a <sgbd>. Anche questa operazione è spiegata negli allegati per ciascuno dei SGBD
  • ripetere i test precedenti

A questo punto, abbiamo acquisito alcune conoscenze:

  • comprendiamo meglio il concetto di ponte oggetto-relazionale. In questo caso è stato realizzato da Hibernate. In seguito utilizzeremo Toplink.
  • sappiamo che questo ponte oggetto-relazionale è configurato in due punti:
  • negli oggetti @Entity, dove si indicano i collegamenti tra i campi degli oggetti e le colonne delle tabelle di BD
  • in [META-INF/persistence.xml], dove si forniscono all’implementazione JPA le informazioni relative ai due elementi del ponte oggetto/relazionale: gli oggetti @Entity (oggetto) e il database (relazionale).
  • Abbiamo creato due attività Ant, denominate DDL e BD, che ci consentono di creare il database sulla base della configurazione precedente, prima ancora di scrivere qualsiasi codice Java.

Ora che il livello JPA della nostra applicazione è correttamente configurato, possiamo iniziare a esplorare API e JPA con il codice Java.

2.1.9. Il contesto di persistenza di un’applicazione

Chiariamo un po’ 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, tranne per il fatto che si interrogano oggetti anziché tabelle.
Query createNativeQuery(String queryText)
metodo analogo al precedente, con la differenza che queryText è,
un comando 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("jpa");
  • javax.persistence.Persistence è una classe statica che consente di ottenere una factory di oggetti EntityManager. Questa factory è collegata 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="jpa" transaction-type="RESOURCE_LOCAL">

Nell’esempio sopra riportato, l’unità di persistenza si chiama jpa. Ad essa è associata un’intera configurazione specifica, in particolare il SGBD con cui interagisce. L'istruzione [Persistence.createEntityManagerFactory("jpa")] 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 jpa. 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 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,
  2. al termine di una transazione sulla base
  3. a seguito di un'operazione flush o close sul contesto di persistenza

Nella modalità FlushModeType.COMMIT, il procedimento è 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:

Dovremo utilizzare i metodi da 1 a 4 sopra indicati:

  • 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 tipo «update» o «delete» 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.

2.1.10. Un primo client JPA

Torniamo a una prospettiva Java del progetto:

 

Ora conosciamo praticamente tutto di questo progetto, tranne il contenuto della cartella [src/tests] che stiamo esaminando in questo momento. La cartella contiene due programmi di test del livello JPA:

  • [InitDB.java] è un programma che inserisce alcune righe nella tabella [jpa01_personne] del database. Il suo codice ci fornirà i primi elementi del livello JPA.
  • [Main.java] è un programma che esegue le operazioni CRUD sulla tabella [jpa01_personne]. L’analisi del suo codice ci consentirà di affrontare i concetti fondamentali del contesto di persistenza e del ciclo di vita degli oggetti di tale contesto.

2.1.10.1. Il codice

Il codice del programma [InitDB.java] è il seguente:


package tests;

import java.text.ParseException;
import java.text.SimpleDateFormat;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import entites.Personne;

public class InitDB {
    // costanti
    private final static String TABLE_NAME = "jpa01_personne";

    public static void main(String[] args) throws ParseException {
        // Unità di persistenza
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        // recuperare un EntityManagerFactory dall'unità di persistenza
        EntityManager em = emf.createEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminare gli elementi dalla tabella delle persone
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // creare due persone
        Personne 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);
        // persistenza delle persone
        em.persist(p1);
        em.persist(p2);
        // visualizzazione delle persone
        System.out.println("[personnes]");
        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 EntityManager
        em.close();
        // fine EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé ...");
    }
}

Questo codice va letto alla luce di quanto spiegato nel paragrafo 2.1.9.

  • riga 19: viene richiesto un oggetto EntityManagerFactory emf per l’unità di persistenza jpa (definita in persistence.xml). Questa operazione viene normalmente eseguita una sola volta nel corso della vita di un’applicazione.
  • riga 21: viene richiesto un oggetto EntityManager em per gestire un contesto di persistenza.
  • riga 23: viene richiesto un oggetto Transaction per gestire una transazione. Si ricorda che le operazioni sul contesto di persistenza vengono eseguite all’interno di una transazione. Vedremo che ciò non è obbligatorio, ma che in tal caso si possono verificare dei problemi. Se l’applicazione viene eseguita in un contenitore EJB3, le operazioni sul contesto di persistenza vengono sempre eseguite all’interno di una transazione.
  • riga 24: la transazione ha inizio
  • riga 26: esegue un comando SQL DELETE sulla tabella "jpa01_personne" (nativeQuery). Si procede in questo modo per svuotare la tabella da ogni contenuto e poter così osservare meglio il risultato dell’esecuzione dell’applicazione [InitDB]
  • righe 28-29: vengono creati due oggetti Personne, denominati p1 e p2. Si tratta di oggetti normali che, per il momento, non hanno nulla a che vedere con il contesto di persistenza. Per quanto riguarda il contesto di persistenza, Hibernate definisce questi oggetti come transitori (transient), per distinguerli dagli oggetti persistenti (persistent) che sono gestiti dal contesto di persistenza. Parleremo piuttosto di oggetti non persistenti (espressione non francese) per indicare che non sono ancora gestiti dal contesto di persistenza e di oggetti persistenti per quelli che sono gestiti da quest’ultimo. Troveremo una terza categoria di oggetti, gli oggetti distaccati (detached), che sono oggetti precedentemente persistenti ma il cui contesto di persistenza è stato chiuso. Il client può detenere riferimenti a tali oggetti, il che spiega perché non vengono necessariamente distrutti alla chiusura del contesto di persistenza. Si dice quindi che si trovino in stato distaccato. L’operazione [EntityManager].merge consente di ricollegarli a un contesto di persistenza appena creato.
  • righe 31-32: le persone p1 e p2 vengono integrate nel contesto di persistenza tramite l’operazione [EntityManager].persist. Diventano quindi oggetti persistenti.
  • righe 35-37: viene eseguito un comando JPQL "select p from Personne p order by p.nom asc". Personne non è la tabella (che si chiama jpa01_personne), ma l’oggetto @Entity associato alla tabella. Qui si tratta di una query JPQL (Java Persistence Query Language) sul contesto di persistenza e non di un comando SQL sul database. Detto questo, a parte l’oggetto Personne che ha sostituito la tabella jpa01_personne, le sintassi sono identiche. Un ciclo for scorre l’elenco (di persone) risultante dalla query select per visualizzarne ogni elemento sulla console. In questo caso si vuole verificare che nella tabella siano effettivamente presenti gli elementi inseriti nel contesto di persistenza alle righe 31-32. In modo trasparente, avrà luogo una sincronizzazione del contesto di persistenza con il database. Infatti, verrà emessa una query select e abbiamo detto che questo era uno dei casi in cui veniva effettuata una sincronizzazione. È quindi in questo momento che, in background, JPA / Hibernate invierà i due comandi SQL e insert che inseriranno le due persone nella tabella jpa01_personne. L’operazione persist non lo aveva fatto. Questa operazione integra oggetti nel contesto di persistenza senza che ciò abbia conseguenze sul database. Le operazioni effettive avvengono durante le sincronizzazioni, in questo caso proprio prima dell’operazione select sul database.
  • riga 39: si conclude la transazione avviata alla riga 24. Avrà nuovamente luogo una sincronizzazione. Qui non accadrà nulla poiché il contesto di persistenza non è cambiato dall’ultima sincronizzazione.
  • riga 41: si chiude il contesto di persistenza.
  • riga 43: si chiude la fabbrica di EntityManager.

2.1.10.2. L' 'esecuzione del codice

  • avviare SGBD MySQL5
  • inserire conf/mysql5/persistence.xml in META-INF/persistence.xml se necessario
  • eseguire l'applicazione [InitDB]

Si ottengono i seguenti risultati:

  • in [1]: l'output della console nella prospettiva Java. Si ottiene il risultato previsto.
  • in [2]: si verifica il contenuto della tabella [jpa01_personne] con la prospettiva SQL Explorer, come spiegato nel paragrafo 2.1.8. Si possono notare due punti:
    • la chiave primaria ID è stata generata automaticamente
    • lo stesso vale per il numero di versione. Si nota che la prima versione ha il numero 0..

Ecco i primi elementi della struttura JPA. Siamo riusciti a inserire dei dati in una tabella. Partiremo da queste basi per scrivere il secondo test, ma prima parliamo dei log.

2.1.11. Implementazione dei log di Hibernate

È possibile conoscere i comandi SQL emessi sul database dal livello JPA / Hibernate. È interessante conoscerli per verificare se il livello JPA sia altrettanto efficiente di uno sviluppatore che avesse scritto personalmente i comandi SQL.

Con JPA / Hibernate, i log SQL possono essere controllati nel file [persistence.xml]:


            <!-- 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" />

  • righe 4-6: i log SQL non erano ancora attivati. Ora li attiviamo rimuovendo il tag di commento dalle righe 3 e 7.

Si riesegue l’applicazione [InitDB]. I messaggi visualizzati in console diventano quindi i seguenti:

Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
[personnes]
Hibernate: 
    select
        personne0_.ID as ID0_,
        personne0_.VERSION as VERSION0_,
        personne0_.NOM as NOM0_,
        personne0_.PRENOM as PRENOM0_,
        personne0_.DATENAISSANCE as DATENAIS5_0_,
        personne0_.MARIE as MARIE0_,
        personne0_.NBENFANTS as NBENFANTS0_ 
    from
        jpa01_personne personne0_ 
    order by
        personne0_.NOM asc
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
terminé ...
  • righe 2-4: l'ordine SQL delete derivante dall'istruzione:

        // eliminare gli elementi dalla tabella delle persone
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
  • righe 5-18: gli ordini SQL insert derivanti dalle istruzioni:

        // persistenza delle persone
        em.persist(p1);
        em.persist(p2);
  • righe 21-32: il comando SQL select derivante dall'istruzione:

        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) 

Se si effettuano visualizzazioni intermedie sulla console, si noterà che la scrittura dei log SQL relativi a un'istruzione I del codice Java avviene nel momento in cui l'istruzione I viene eseguita. Ciò non significa che il comando SQL visualizzato venga eseguito sul database in quel momento. Viene infatti memorizzato nella cache per essere eseguito alla successiva sincronizzazione del contesto di persistenza con il database.

È possibile ottenere altri log tramite il file [src/log4j.properties]:

  • in [1], il file [log4j.properties] viene elaborato dall’archivio [log4j-1.2.13.jar] [2] dello strumento denominato LOG4j (Logs for Java) disponibile all'URL [http://logging.apache.org/log4j/docs/index.html]. Inserito nella cartella [src] del progetto Eclipse, sappiamo che [log4j.properties] verrà copiato automaticamente nella cartella [bin] del progetto [3]. Una volta fatto ciò, il file si trova ora nella cartella classpath del progetto ed è da lì che l’archivio [2] lo preleverà.

Il file [log4j.properties] ci permette di monitorare alcuni log di Hibernate. Nelle esecuzioni precedenti il suo contenuto era il seguente:


# Invia i messaggi di log 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

# Opzioni di registrazione di root
log4j.rootLogger=ERROR, stdout

# Opzioni di registrazione in modalità ibernazione (INFO mostra solo i messaggi di avvio)
#log4j.logger.org.hibernate=INFO

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

Non mi soffermerò molto su questa configurazione, non avendo mai avuto il tempo di informarmi seriamente su LOG4j.

  • Le righe da 1 a 8 sono presenti in tutti i file log4j.properties che ho avuto modo di incontrare
  • le righe 10-14 sono presenti nei file log4j.properties degli esempi di Hibernate.
  • Riga 11: controlla i log generali di Hibernate. Poiché la riga è commentata, questi log sono qui disabilitati. Sono disponibili diversi livelli di log: INFO (informazioni generali sulle attività di Hibernate), WARN (Hibernate segnala un possibile problema), DEBUG (log dettagliati). Il livello INFO è il meno dettagliato, mentre la modalità DEBUG è la più dettagliata. Attivando la riga 11 è possibile sapere cosa sta facendo Hibernate, in particolare all’avvio dell’applicazione. Spesso è utile.
  • La riga 12, se attiva, permette di conoscere gli argomenti effettivamente utilizzati durante l’esecuzione delle query SQL configurate.

Iniziamo rimuovendo il commento dalla riga 14


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

e rieseguiamo [InitDB]. I nuovi log generati da questa modifica sono i seguenti (vista parziale):

Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 1
07:20:03,843 DEBUG StringType:80 - binding 'Durant' to parameter: 2
07:20:03,843 DEBUG StringType:80 - binding 'Sylvie' to parameter: 3
07:20:03,843 DEBUG DateType:80 - binding '05 juillet 2001' to parameter: 4
07:20:03,843 DEBUG BooleanType:80 - binding 'false' to parameter: 5
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 6
  • le righe 8-10 sono nuovi log generati dall’attivazione della riga 14 di [log4j.properties]. Indicano i 5 valori assegnati ai parametri formali ? della query parametrizzata delle righe 2-7. Si nota quindi che la colonna VERSION riceverà il valore 0 (riga 8).

Ora attiviamo la riga 11 di [log4j.properties]:

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

e rieseguiamo [InitDB]:

07:50:23,937  INFO Version:15 - Hibernate EntityManager 3.2.0.CR3
07:50:23,968  INFO Version:15 - Hibernate Annotations 3.2.0.CR3
07:50:23,984  INFO Environment:500 - Hibernate 3.2.0.cr5
07:50:23,984  INFO Environment:533 - hibernate.properties not found
07:50:23,984  INFO Environment:667 - Bytecode provider name : cglib
07:50:24,000  INFO Environment:584 - using JDK 1.4 java.sql.Timestamp handling
07:50:24,375  INFO AnnotationBinder:387 - Binding entity from annotated class: entites.Personne
07:50:24,421  INFO EntityBinder:340 - Bind entity entites.Personne on table jpa01_personne
07:50:24,609  INFO C3P0ConnectionProvider:50 - C3P0 using driver: com.mysql.jdbc.Driver at URL: jdbc:mysql://localhost:3306/jpa
07:50:24,609  INFO C3P0ConnectionProvider:51 - Connection properties: {user=jpa, password=****, autocommit=true, release_mode=auto}
07:50:24,609  INFO C3P0ConnectionProvider:54 - autocommit mode: true
07:50:25,296  INFO SettingsFactory:81 - RDBMS: MySQL, version: 5.0.37-community-nt
07:50:25,296  INFO SettingsFactory:82 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
07:50:25,312  INFO Dialect:141 - Using dialect: org.hibernate.dialect.MySQL5InnoDBDialect
07:50:25,312  INFO TransactionFactoryFactory:34 - Transaction strategy: org.hibernate.transaction.JDBCTransactionFactory
07:50:25,312  INFO TransactionManagerLookupFactory:33 - No TransactionManagerLookup configured (in JTA environment, use of read-write or transactional second-level cache is not recommended)
07:50:25,328  INFO SettingsFactory:134 - Automatic flush during beforeCompletion(): disabled
07:50:25,328  INFO SettingsFactory:138 - Automatic session close at end of transaction: disabled
07:50:25,328  INFO SettingsFactory:145 - JDBC batch size: 15
07:50:25,328  INFO SettingsFactory:148 - JDBC batch updates for versioned data: disabled
07:50:25,328  INFO SettingsFactory:153 - Scrollable result sets: enabled
07:50:25,328  INFO SettingsFactory:161 - JDBC3 getGeneratedKeys(): enabled
07:50:25,328  INFO SettingsFactory:169 - Connection release mode: auto
07:50:25,328  INFO SettingsFactory:193 - Maximum outer join fetch depth: 2
07:50:25,328  INFO SettingsFactory:196 - Default batch fetch size: 1
07:50:25,328  INFO SettingsFactory:200 - Generate SQL with comments: disabled
07:50:25,328  INFO SettingsFactory:204 - Order SQL updates by primary key: disabled
07:50:25,328  INFO SettingsFactory:369 - Query translator: org.hibernate.hql.ast.ASTQueryTranslatorFactory
07:50:25,328  INFO ASTQueryTranslatorFactory:24 - Using ASTQueryTranslatorFactory
07:50:25,328  INFO SettingsFactory:212 - Query language substitutions: {}
07:50:25,328  INFO SettingsFactory:217 - JPA-QL strict compliance: enabled
07:50:25,328  INFO SettingsFactory:222 - Second-level cache: enabled
07:50:25,328  INFO SettingsFactory:226 - Query cache: disabled
07:50:25,328  INFO SettingsFactory:356 - Cache provider: org.hibernate.cache.NoCacheProvider
07:50:25,328  INFO SettingsFactory:241 - Optimize cache for minimal puts: disabled
07:50:25,328  INFO SettingsFactory:250 - Structured second-level cache entries: disabled
07:50:25,343  INFO SettingsFactory:270 - Echoing all SQL to stdout
07:50:25,343  INFO SettingsFactory:277 - Statistics: disabled
07:50:25,343  INFO SettingsFactory:281 - Deleted entity synthetic identifier rollback: disabled
07:50:25,343  INFO SettingsFactory:296 - Default entity-mode: pojo
07:50:25,468  INFO SessionFactoryImpl:161 - building session factory
07:50:25,750  INFO SessionFactoryObjectFactory:82 - Not binding factory to JNDI, no JNDI name configured
07:50:25,765  INFO SchemaExport:154 - Running hbm2ddl schema export
07:50:25,765  INFO SchemaExport:179 - exporting generated schema to database
07:50:25,968  INFO SchemaExport:196 - schema export complete
Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    ... 

La lettura di questi log fornisce molte informazioni interessanti:

  • riga 7: Hibernate indica il nome di una classe @Entity che ha trovato
  • riga 8: indica che la classe [Personne] verrà associata alla tabella [jpa01_personne]
  • riga 9: indica il pool di connessioni C3P0 che verrà utilizzato, il nome del driver JDBC, l'URL del database da gestire
  • riga 10: fornisce ulteriori caratteristiche del collegamento JDBC: proprietario, tipo di commit, ...
  • riga 14: il dialetto utilizzato per comunicare con SGBD
  • riga 15: il tipo di transazione utilizzato. JDBCTransactionFactory indica che l'applicazione gestisce autonomamente le proprie transazioni. Non viene eseguita in un contenitore EJB3 che fornirebbe un proprio servizio di transazioni.
  • Le righe successive si riferiscono a opzioni di configurazione di Hibernate che non abbiamo incontrato. Il lettore interessato è invitato a consultare la documentazione di Hibernate.
  • riga 37: i comandi SQL verranno visualizzati sulla console. Ciò è stato richiesto in [persistence.xml]:

            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="use_sql_comments" value="true" />
  • righe 43-45: lo schema del database viene esportato nei file SGBD e c.a.d. Il database viene svuotato e poi ricreato. Questo meccanismo deriva dalla configurazione effettuata in [persistence.xml] (riga 4 qui sotto):

            ...
            <property name="hibernate.connection.password" value="jpa" />
            <!-- Creazione automatica dello schema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialetto -->
            ...

Quando un’applicazione “si blocca” con un’eccezione Hibernate che non si riesce a comprendere, si inizierà attivando i log di Hibernate in modalità DEBUG in [log4j.properties] per fare chiarezza:


# Opzione logger principale
log4j.rootLogger=ERROR, stdout

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

Nel prosieguo di questo documento, i log sono disattivati per impostazione predefinita al fine di ottenere una visualizzazione della console più leggibile.

2.1.12. Scoprire il linguaggio JPQL / HQL con la console Hibernate

Nota: questa sezione richiede il plugin Hibernate Tools (paragrafo 5.2.5).

Nel codice dell’applicazione [InitDB] abbiamo utilizzato una query JPQL. JPQL (Java Persistence Query Language) è un linguaggio per l’interrogazione del contesto di persistenza. La query riscontrata era la seguente:

select p from Personne p order by p.nom asc

Essa selezionava tutti gli elementi della tabella associata all’@Entity [Personne] e li restituiva in ordine crescente in base al nome. Nella query sopra riportata, p.nom è il campo «nome» di un’istanza p della classe [Personne]. Una query JPQL opera quindi sugli oggetti @Entity del contesto di persistenza e non direttamente sulle tabelle del database. Il livello JPA tradurrà a sua volta questa query JPQL in una query SQL adeguata al SGBD con cui opera. Pertanto, nel caso di un’implementazione JPA / Hibernate collegata a un SGBD MySQL5, la query precedente JPQL viene tradotta nella seguente query SQL:

select
  personne0_.ID as ID0_,
  personne0_.VERSION as VERSION0_,
  personne0_.NOM as NOM0_,
  personne0_.PRENOM as PRENOM0_,
  personne0_.DATENAISSANCE as DATENAIS5_0_,
  personne0_.MARIE as MARIE0_,
  personne0_.NBENFANTS as NBENFANTS0_ 
 from
  jpa01_personne personne0_ 
 order by
  personne0_.NOM asc

Il livello JPA ha utilizzato la configurazione dell'oggetto @Entity [Personne] per generare l'ordine corretto SQL. In questo caso è stato implementato il ponte oggetto/relazionale.

Il plugin [Hibernate Tools] (paragrafo 5.2.5) offre uno strumento denominato “Console Hibernate” che consente

  • di emettere comandi JPQL o del superset HQL (Hibernate Query Language) sul contesto di persistenza
  • di ottenerne i risultati
  • di conoscere l’equivalente SQL che è stato eseguito sul database

La console di Hibernate è uno strumento di grande valore per imparare il linguaggio JPQL e familiarizzare con il bridge JPQL / SQL. È noto che JPA si è fortemente ispirato a strumenti ORM come Hibernate o Toplink. JPQL è molto simile al linguaggio HQL di Hibernate, ma non ne riprende tutte le funzionalità. Nella console di Hibernate è possibile impartire comandi HQL che verranno eseguiti normalmente nella console, ma che non fanno parte del linguaggio JPQL e che quindi non potrebbero essere utilizzati in un client JPA. Quando ciò accadrà, lo segnaleremo.

Creiamo una console Hibernate per il nostro attuale progetto Eclipse:

  • [1]: passiamo a una prospettiva [Hibernate Console] (Window / Open Perspective / Other)
  • [2]: creiamo una nuova configurazione nella finestra [Hibernate Configuration]
  • utilizzando il pulsante [4], selezioniamo il progetto Java per il quale viene creata la configurazione Hibernate. Il suo nome viene visualizzato in [3].
  • In [5], assegniamo il nome desiderato a questa configurazione. In questo caso, abbiamo utilizzato [3].
  • In [6], indichiamo che utilizziamo una configurazione JPA affinché lo strumento sappia che deve utilizzare il file [META-INF/persistence.xml]
  • in [7]: indichiamo che in questo file [META-INF/persistence.xml] occorre utilizzare l’unità di persistenza denominata jpa.
  • Nel file [8] si convalida la configurazione.

Successivamente, è necessario avviare il file SGBD. In questo caso, si tratta del file MySQL5.

  • in [1]: la configurazione creata presenta una struttura ad albero a tre rami
  • in [2]: il ramo [Configuration] elenca gli oggetti che la console ha utilizzato per configurarsi: in questo caso l’@Entity Personne.
  • in [3]: la Session Factory è un concetto di Hibernate simile a EntityManager di JPA. Realizza il ponte oggetto/relazionale grazie agli oggetti del ramo [Configuration]. In [3] vengono presentati gli oggetti del contesto di persistenza, in questo caso nuovamente l’@Entity Personne.
  • In [4]: il database a cui si accede tramite la configurazione presente in [persistence.xml]. Qui si trova la tabella [jpa01_personne].
  • in [1], si crea un editor HQL
  • nell’editor HQL,
    • in [2], si sceglie la configurazione di Hibernate da utilizzare se ce ne sono diverse
    • in [3], si digita il comando JPQL che si desidera eseguire
    • in [4], lo si esegue
  • in [5], si ottengono i risultati della query nella finestra [Hibernate Query Result]. Qui si possono incontrare due difficoltà:
    • non si ottiene nulla (nessuna riga). La console Hibernate ha utilizzato il contenuto di [persistence.xml] per creare una connessione con SGBD. Tuttavia, questa configurazione presenta una proprietà che indica di svuotare il database:

            <property name="hibernate.hbm2ddl.auto" value="create" />

È quindi necessario rieseguire l’applicazione [InitDB] prima di ripetere il comando JPQL sopra indicato.

  • (continua)
    • non è presente la finestra [Hibernate Query Result]. La si richiede tramite [Window / Show View / ...]

La finestra [Hibernate Dynamic SQL preview] ([1] qui sotto) consente di visualizzare la richiesta SQL che verrà eseguita per attuare il comando JPQL che si sta scrivendo. Non appena la sintassi del comando JPQL risulta corretta, in questa finestra compare il comando corrispondente SQL:

  • in [2], si cancella il comando precedente HQL
  • in [3], se ne esegue uno nuovo
  • in [4], il risultato
  • in [5], il comando SQL che è stato eseguito sulla base

L'editor HQL offre un aiuto alla scrittura dei comandi HQL:

  • in [1]: una volta che l’editor riconosce che p è un oggetto Personne, può suggerirci i campi di p durante la digitazione.
  • in [2]: un comando HQL errato. È necessario scrivere where p.marie=true.
  • in [3]: l’errore viene segnalato nella finestra [SQL Preview]

Invitiamo il lettore a eseguire altri comandi HQL / JPQL sulla base.

2.1.13. Un secondo client JPA

Torniamo a una prospettiva Java del progetto:

 
  • [InitDB.java] è un programma che inseriva alcune righe nella tabella [jpa01_personne] del database. L’analisi del suo codice ci ha permesso di acquisire i primi elementi di API e JPA.
  • [Main.java] è un programma che esegue le operazioni CRUD sulla tabella [jpa01_personne]. L’analisi del suo codice ci consentirà di approfondire i concetti fondamentali relativi al contesto di persistenza e al ciclo di vita degli oggetti in tale contesto.

2.1.13.1. La struttura del codice

[Main.java] eseguirà una serie di test, ciascuno dei quali mira a illustrare un aspetto specifico di JPA:

 

Il metodo [main]

  • richiama in successione i metodi da test1 a test11. Presenteremo separatamente il codice di ciascuno di questi metodi.
  • Utilizza inoltre metodi di utilità privati: clean, dump, log, getEntityManager, getNewEntityManager.

Presentiamo il metodo main e i cosiddetti metodi di utilità:


package tests;

...
import entites.Personne;

@SuppressWarnings("unchecked")
public class Main {

    //  Costanti
    private final static String TABLE_NAME = "jpa01_personne";

    // Contesto di persistenza
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
    private static EntityManager em = null;

    // Oggetti condivisi
    private static Personne p1, p2, newp1;

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

        // dump tabella
        dump();

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

...
        // test11
        log("test11");test11();

        // fine contesto di persistenza
        if (em.isOpen())
            em.close();

        // chiusura di EntityManagerFactory
        emf.close();
    }

    // recupera l'EntityManager corrente
    private static EntityManager getEntityManager() {
        if (em == null || !em.isOpen()) {
            em = emf.createEntityManager();
        }
        return em;
    }

    // recuperare un nuovo EntityManager
    private static EntityManager getNewEntityManager() {
        if (em != null && em.isOpen()) {
            em.close();
        }
        em = emf.createEntityManager();
        return em;
    }

    // visualizza il contenuto della tabella
    private static void dump() {
        // contesto di persistenza corrente
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // visualizza persone
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fine transazione
        tx.commit();
    }

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

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

    // creazione di oggetti
    public static void test1() throws ParseException {
...
    }

    // modifica di un oggetto del contesto
    public static void test2() {
...
    }

    // richiesta di oggetti
    public static void test3() {
...
    }

    // eliminazione di un oggetto appartenente al contesto di persistenza
    public static void test4() {
....
    }

    // scollegare, ricollegare e modificare
    public static void test5() {
...
    }

    // eliminare un oggetto che non appartiene al contesto di persistenza
    public static void test6() {
...
    }

    // modificare un oggetto che non appartiene al contesto di persistenza
    public static void test7() {
...
    }

    // riassociare un oggetto al contesto di persistenza
    public static void test8() {
...
    }

    // una query SELECT provoca una sincronizzazione
    // del database con il contesto di persistenza
    public static void test9() {
....
    }

    // controllo di versione (blocco ottimistico)
    public static void test10() {
...
    }

    // rollback di una transazione
    public static void test11() throws ParseException {
...
    }

}
  • riga 13: l’oggetto EntityManagerFactory emf costruito a partire dall’unità di persistenza jpa definita in [persistence.xml]. Ci consentirà di creare, nel corso dell’applicazione, vari contesti di persistenza.
  • riga 14: un contesto di persistenza EntityManager em ancora non inizializzato
  • riga 17: tre oggetti [Personne] condivisi dai test
  • riga 21: la tabella jpa01_personne viene svuotata e poi visualizzata alla riga 24 per assicurarsi di partire da una tabella vuota.
  • righe 27-31: sequenza di test
  • righe 34-35: chiusura del contesto di persistenza em, se era aperto.
  • riga 38: chiusura dell’oggetto EntityManagerFactory emf.
  • righe 42-47: il metodo [getEntityManager] rende l'oggetto EntityManager (ovvero il contesto di persistenza) corrente o ne crea uno nuovo se non esiste (righe 43-44).
  • righe 50-56: il metodo [getNewEntityManager] crea un nuovo contesto di persistenza. Se ne esisteva uno in precedenza, viene chiuso (righe 51-52)
  • righe 59-72: il metodo [dump] visualizza il contenuto della tabella [jpa01_personne]. Questo codice è già stato incontrato in [InitDB].
  • righe 75-85: il metodo [clean] svuota la tabella [jpa01_personne]. Questo codice è già stato incontrato in [InitDB].
  • righe 88-90: il metodo [log] visualizza sulla console il messaggio che gli viene passato come parametro, in modo che venga notato.

Ora possiamo passare all'analisi dei test.

2.1.13.2. Test 1

Il codice di test1 è il seguente:


// creazione di oggetti
    public static void test1() throws ParseException {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // creazione di persone
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistenza delle persone
        em.persist(p1);
        em.persist(p2);
        // fine transazione
        tx.commit();
        // visualizzazione della tabella
        dump();

}

Questo codice è già stato incontrato in [InitDB]: crea due persone e le inserisce nel contesto di persistenza.

  • riga 4: viene richiesto il contesto di persistenza corrente
  • righe 6-7: si creano i due soggetti
  • righe 9-15: i due soggetti vengono inseriti nel contesto di persistenza all’interno di una transazione.
  • riga 15: a seguito del commit della transazione, avviene la sincronizzazione del contesto di persistenza con il database. Le due persone verranno aggiunte alla tabella [jpa01_personne].
  • riga 17: si visualizza la tabella

L'output della console di questo primo test è il seguente:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]

2.1.13.3. Test 2

Il codice di test2 è il seguente:


// modifica di un oggetto del contesto
    public static void test2() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si incrementa il numero di figli di p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // si modifica il suo stato civile
        p1.setMarie(false);
        // l'oggetto p1 viene salvato automaticamente (dirty checking)
        // alla successiva sincronizzazione (commit o select)
        // fine transazione
        tx.commit();
        // viene visualizzata la nuova tabella
        dump();
    }
  • Il test 2 ha lo scopo di modificare un oggetto del contesto di persistenza e quindi visualizzare il contenuto della tabella per verificare se la modifica è stata effettuata
  • riga 4: si recupera il contesto di persistenza corrente
  • righe 6-7: le operazioni verranno eseguite all'interno di una transazione
  • righe 9, 11: si modificano il numero di figli della persona p1 e il suo stato civile
  • riga 15: fine della transazione, quindi sincronizzazione del contesto di persistenza con il database
  • riga 17: visualizzazione della tabella

L'output della console del test 2 è il seguente:

1
2
3
4
5
6
7
8
main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
  • riga 4: la persona p1 prima della modifica
  • riga 8: la persona p1 dopo la modifica. Si noti che il suo numero di versione è passato a 1. Questo viene incrementato di 1 ad ogni aggiornamento della riga.

2.1.13.4. Test 3

Il codice del test 3 è il seguente:


    // richiedere oggetti
    public static void test3() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si richiede la persona p1
        Personne p1b = em.find(Personne.class, p1.getId());
        // poiché p1 è già nel contesto di persistenza, non è stato effettuato alcun accesso al database
        // p1b e p1 sono gli stessi riferimenti
        System.out.format("p1==p1b ? %s%n", p1 == p1b);
        // richiedere un oggetto che non esiste restituisce un puntatore nullo
        Personne px = em.find(Personne.class, -4);
        System.out.format("px==null ? %s%n", px == null);
        // fine transazione
        tx.commit();
}
  • Il test 3 riguarda il metodo [EntityManager.find], che consente di recuperare un oggetto dal database per inserirlo nel contesto di persistenza. D'ora in poi non spiegheremo più la transazione che avviene in tutti i test, tranne quando viene utilizzata in modo insolito.
  • riga 9: si richiede al contesto di persistenza la persona che ha la stessa chiave primaria della persona p1. Ci sono due casi:
    • p1 si trova già nel contesto di persistenza. È proprio questo il caso. Pertanto non viene effettuato alcun accesso al database. Il metodo find si limita a restituire un riferimento all'oggetto persistito.
    • p1 non si trova nel contesto di persistenza. In questo caso viene effettuato un accesso al database tramite la chiave primaria fornita. La riga recuperata viene inserita nel contesto di persistenza e find restituisce il riferimento a questo nuovo oggetto persistito.
  • riga 12: si verifica che find abbia restituito il riferimento all’oggetto p1 già presente nel contesto
  • riga 14: viene richiesto un oggetto che non esiste né nel contesto di persistenza né nel database. Il metodo find restituisce quindi il puntatore null. Questo punto viene verificato alla riga 15.

L'output della console del test 3 è il seguente:

1
2
3
main : ----------- test3
p1==p1b ? true
px==null ? true

2.1.13.5. Test 4

Il codice del test 4 è il seguente:


    // eliminare un oggetto appartenente al contesto di persistenza
    public static void test4() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si elimina l'oggetto persistito p2
        em.remove(p2);
        // fine transazione
        tx.commit();
        // viene visualizzata la nuova tabella
        dump();
}
  • Il test 4 si concentra sul metodo [EntityManager.remove], che consente di rimuovere un elemento dal contesto di persistenza e quindi dal database.
  • riga 9: la persona p2 viene rimossa dal contesto di persistenza
  • riga 11: sincronizzazione del contesto con il database
  • riga 13: visualizzazione della tabella. Normalmente, la persona p2 non dovrebbe più essere presente.

L'output della console del test 4 è il seguente:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test3
p1==p1b ? true
px==null ? true
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
  • riga 3: la persona p2 in test1
  • righe 12-14: non esiste più al termine di test4.

2.1.13.6. Test 5

Il codice del test 5 è il seguente:


// scollegare, ricollegare e modificare
    public static void test5() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // p1 scollegato
        Personne oldp1=p1;
        // p1 viene ricollegato al nuovo contesto
        p1 = em.find(Personne.class, p1.getId());
        // verifica
        System.out.format("p1==oldp1 ? %s%n", p1 == oldp1);        
        // fine transazione
        tx.commit();
        // si incrementa il numero di figli di p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // viene visualizzata la nuova tabella
        dump();
    }
  • il test 5 si concentra sul ciclo di vita degli oggetti persistenti attraverso diversi contesti di persistenza successivi. Finora, avevamo sempre utilizzato lo stesso contesto di persistenza nei vari test.
  • riga 4: viene richiesto un nuovo contesto di persistenza. Il metodo [getNewEntityManager] chiude quello precedente e ne apre uno nuovo. Di conseguenza, gli oggetti p1 e p2 detenuti dall’applicazione non si trovano più in uno stato persistente. Appartenevano a un contesto che è stato chiuso. Si dice che si trovino in uno stato distaccato. Non appartengono al nuovo contesto di persistenza.
  • righe 6-7: inizio della transazione. In questo caso, verrà utilizzata in modo insolito.
  • riga 9: si annota l’indirizzo dell’oggetto p1, ora distaccato.
  • riga 11: si richiede al contesto di persistenza la persona p1 (utilizzando la chiave primaria di p1). Poiché il contesto è nuovo, la persona p1 non vi è presente. Si effettuerà quindi un accesso al database. L’oggetto recuperato verrà inserito nel nuovo contesto.
  • riga 13: si verifica che l’oggetto persistente p1 del contesto sia diverso dall’oggetto oldp1, che era il precedente oggetto p1 distaccato.
  • riga 15: la transazione è terminata
  • riga 17: si modifica, al di fuori della transazione, il nuovo oggetto persistito p1. Cosa succede in questo caso? Vogliamo scoprirlo.
  • riga 19: si richiede la visualizzazione della tabella. Si ricorda che, a causa del select emesso dal metodo dump, viene eseguita automaticamente una sincronizzazione del contesto di persistenza con il database.

L'output della console del test 5 è il seguente:

1
2
3
4
5
6
7
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • riga 5: il metodo find ha effettivamente effettuato un accesso al database, altrimenti i due puntatori sarebbero uguali
  • righe 7 e 3: il numero di figli di p1 è effettivamente aumentato di 1. La modifica, effettuata al di fuori della transazione, è stata quindi presa in considerazione. Ciò dipende infatti dal SGBD utilizzato. In un SGBD, un comando SQL viene sempre eseguito all’interno di una transazione. Se il client JPA non avvia autonomamente una transazione esplicita, il SGBD avvierà una transazione implicita. Esistono due casi comuni:
    • 1 - ogni singolo ordine SQL è oggetto di una transazione, aperta prima dell’ordine e chiusa dopo. Si dice che si è in modalità autocommit. Tutto avviene quindi come se il client JPA effettuasse transazioni per ogni singolo ordine SQL.
    • 2 - L’ordine SGBD non è in modalità autocommit e avvia una transazione implicita al primo ordine SQL, che il cliente JPA emette al di fuori di una transazione, lasciando che sia il cliente stesso a chiuderla. Tutti gli ordini SQL emessi dal client JPA fanno quindi parte della transazione implicita. Questa può terminare in seguito a diversi eventi: il client chiude la connessione, avvia una nuova transazione, ...

Ci troviamo in una situazione che dipende dalla configurazione di SGBD. Abbiamo quindi un codice non portabile. Mostreremo più avanti un codice senza transazioni e vedremo che non tutti i SGBD hanno lo stesso comportamento rispetto a tale codice. Considereremo quindi che lavorare al di fuori delle transazioni sia un errore di programmazione.

  • riga 7: si noti che il numero di versione è passato a 2.

2.1.13.7. Test 6

Il codice del test 6 è il seguente:


// eliminare un oggetto che non appartiene al contesto di persistenza
    public static void test6() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si elimina p1 che non appartiene al nuovo contesto
        try {
            em.remove(p1);
            // fine transazione
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur à la suppression de p1 : [%s,%s]%n", e1.getClass().getName(), e1.getMessage());
            // si esegue un rollback della transazione
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // visualizzazione della nuova tabella
        dump();
    }
  • Il test 6 cerca di eliminare un oggetto che non appartiene al contesto di persistenza.
  • riga 4: viene richiesto un nuovo contesto di persistenza. Quello precedente viene quindi chiuso e gli oggetti in esso contenuti diventano distaccati. È il caso dell'oggetto p1 del precedente test 5.
  • righe 6-7: inizio della transazione.
  • riga 10: si elimina l'oggetto distaccato p1. Poiché si sa che ciò provocherà un'eccezione, l'operazione è stata racchiusa in un try/catch.
  • riga 12: il commit non verrà eseguito.
  • righe 16-21: una transazione deve terminare con un commit (tutte le operazioni della transazione sono confermate) o con un rollback (tutte le operazioni della transazione sono annullate). Si è verificata un'eccezione, quindi si esegue un rollback sulla transazione. Non c'è nulla da annullare poiché l'unica operazione della transazione è fallita, ma il rollback pone fine alla transazione. È la prima volta che utilizziamo l’operazione [EntityTransaction].rollback. Avremmo dovuto farlo fin dai primi esempi. Non l’abbiamo fatto per mantenere il codice semplice. Il lettore deve tuttavia tenere presente che il caso del rollback della transazione deve sempre essere previsto nel codice.
  • riga 24: viene visualizzata la tabella. Normalmente, non dovrebbe essere cambiata.

L'output della console del test 6 è il seguente:

1
2
3
4
5
6
7
8
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • riga 6: l’eliminazione di p1 non è andata a buon fine. Il messaggio di eccezione spiega che si è tentato di eliminare un oggetto distaccato, quindi non facente parte del contesto. Ciò non è possibile.
  • riga 8: la persona p1 è ancora presente.

2.1.13.8. Test 7

Il codice del test 7 è il seguente:


// modifica di un oggetto che non appartiene al contesto di persistenza
    public static void test7() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si incrementa il numero di figli di p1 che non appartiene al nuovo contesto
        p1.setNbenfants(p1.getNbenfants() + 1);
        // fine transazione
        tx.commit();
        // viene visualizzata la nuova tabella - non dovrebbe essere cambiata
        dump();
    }
  • Il test 7 cerca di modificare un oggetto che non appartiene al contesto di persistenza per verificare l’impatto che ciò ha sul database. È lecito supporre che non ce ne sia alcuno. È quanto dimostrano i risultati del test.
  • riga 4: viene richiesto un nuovo contesto di persistenza. Si ha quindi un contesto nuovo senza oggetti persistenti al suo interno.
  • righe 6-7: inizio della transazione.
  • riga 9: si modifica l’oggetto distaccato p1. Si tratta di un’operazione che non coinvolge il contesto di persistenza em. Non ci si deve quindi aspettare un’eccezione o qualcosa di simile. È un’operazione di base su un POJO.
  • riga 11: il commit provoca la sincronizzazione del contesto con il database. Questo contesto è vuoto. Il database non viene quindi modificato.
  • riga 24: si visualizza la tabella. Normalmente, non dovrebbe essere cambiata.

L'output della console del test 7 è il seguente:

1
2
3
4
5
6
7
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • riga 7: la persona p1 non è cambiata nel database. Per il test successivo, terremo comunque presente che in memoria il numero dei suoi figli è ora pari a 5.

2.1.13.9. Test 8

Il codice del test 8 è il seguente:


    // ricollegare un oggetto al contesto di persistenza
    public static void test8() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si ricollega l'oggetto scollegato p1 al nuovo contesto
        newp1 = em.merge(p1);
        // ora è newp1 a far parte del contesto, non p1
        // fine transazione
        tx.commit();
        // viene visualizzata la nuova tabella: il numero di figli di p1 deve essere cambiato
        dump();
}
  • il test 8 ricollega al contesto di persistenza un oggetto scollegato.
  • riga 4: viene richiesto un nuovo contesto di persistenza. Si ha quindi un contesto nuovo senza oggetti persistenti al suo interno.
  • righe 6-7: inizio della transazione.
  • riga 9: si ricollega al contesto di persistenza l’oggetto distaccato p1. L’operazione merge può comportare diverse operazioni:
    • caso 1: nel contesto di persistenza esiste un oggetto persistente ps1 con la stessa chiave primaria dell’oggetto distaccato p1. Il contenuto di p1 viene copiato in ps1, mentre merge diventa il riferimento di ps1.
    • Caso 2: nel contesto di persistenza non esiste un oggetto persistente ps1 con la stessa chiave primaria dell’oggetto distaccato p1. Si interroga quindi il database per verificare se l’oggetto ricercato esiste nel database. In caso affermativo, tale oggetto viene trasferito nel contesto di persistenza, diventa l’oggetto persistente ps1 e si ricade nel caso 1 precedente.
    • Caso 3: non esiste, né nel contesto di persistenza né nel database, un oggetto con la stessa chiave primaria dell’oggetto distaccato p1. Viene quindi creato un nuovo oggetto [Personne] (new), che viene poi inserito nel contesto di persistenza. Si torna quindi al caso 1.
    • In definitiva: l’oggetto distaccato p1 rimane distaccato. L’operazione merge restituisce un riferimento (in questo caso newp1) all’oggetto persistente ps1 derivato da merge. L'applicazione client deve ora operare con l'oggetto persistente ps1 e non con l'oggetto distaccato p1.
    • Si noti una differenza tra i casi 1 e 3 per quanto riguarda l’ordine SQL programmato per il merge: nei casi 1 e 2 si tratta dell’ordine UPDATE, mentre nel caso 3 si tratta di un ordine INSERT.
  • riga 12: il commit provoca la sincronizzazione del contesto con il database. Questo contesto non è più vuoto. Contiene l'oggetto newp1. Quest'ultimo verrà salvato nel database.
  • riga 24: si visualizza la tabella per verificarla.

L'output della console del test 8 è il seguente:

main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test8
[personnes]
[1,3,Martin,Paul,31/01/2000,false,5]
  • il numero di figli di p1 era pari a 4 nel test 6 (riga 4), poi era passato a 5 nel test 7 ma non era stato salvato nel database (riga 7). Dopo il merge, il newp1 è stato salvato nel database: alla riga 10 si contano effettivamente 5 figli.
  • riga 10: il numero di versione di newp1 è passato a 3.

2.1.13.10. Test 9

Il codice del test 9 è il seguente:


// una query SELECT provoca una sincronizzazione
    // del database con il contesto di persistenza
    public static void test9() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si incrementa il numero di figli di newp1
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // visualizzazione persone - il numero di figli di newp1 deve essere cambiato
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fine transazione
        tx.commit();
    }
  • Il test 9 intende illustrare il meccanismo di sincronizzazione del contesto che avviene automaticamente prima di un select.
  • riga 5: il contesto di persistenza non viene modificato. newp1 è quindi al suo interno.
  • righe 7-8: inizio della transazione.
  • riga 10: il numero di elementi figli dell’oggetto persistente newp1 viene incrementato di 1 (da 5 a 6).
  • righe 12-15: si visualizza la tabella tramite un SELECT. Il contesto verrà sincronizzato con il database prima dell’esecuzione di select.
  • riga 17: fine della transazione

Per visualizzare la sincronizzazione, si attiva la visualizzazione dei log di Hibernate in modalità DEBUG (log4j.properties):


# Opzione Root Logger
log4j.rootLogger=ERROR, stdout

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

La visualizzazione della console del test 9 è la seguente:

main : ----------- test9
14:27:27,250 DEBUG JDBCTransaction:54 - begin
14:27:27,250 DEBUG ConnectionManager:415 - opening JDBC connection
14:27:27,250 DEBUG JDBCTransaction:59 - current autocommit status: true
14:27:27,250 DEBUG JDBCTransaction:62 - disabling autocommit
14:27:27,250 DEBUG JDBCContext:210 - after transaction begin
[personnes]
14:27:27,250 DEBUG QueryPlanCache:76 - located HQL query plan in cache (select p from Personne p order by p.nom asc)
14:27:27,250 DEBUG AbstractFlushingEventListener:58 - flushing session
...
14:27:27,250 DEBUG AbstractEntityPersister:3116 - entites.Personne.nbenfants is dirty
14:27:27,250 DEBUG DefaultFlushEntityEventListener:229 - Updating entity: [entites.Personne#1]
14:27:27,250 DEBUG Versioning:27 - Incrementing: 3 to 4
...
14:27:27,250 DEBUG AbstractFlushingEventListener:85 - Flushed: 0 insertions, 1 updates, 0 deletions to 1 objects
...
14:27:27,250 DEBUG ConnectionManager:463 - registering flush begin
14:27:27,250 DEBUG AbstractEntityPersister:2274 - Updating entity: [entites.Personne#1]
14:27:27,265 DEBUG AbstractEntityPersister:2276 - Existing version: 3 -> New version: 4
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - update jpa01_personne set VERSION=?, NOM=?, PRENOM=?, DATENAISSANCE=?, MARIE=?, NBENFANTS=? where ID=? and VERSION=?
14:27:27,265 DEBUG AbstractBatcher:476 - preparing statement
14:27:27,265 DEBUG AbstractEntityPersister:1927 - Dehydrating entity: [entites.Personne#1]
14:27:27,265 DEBUG IntegerType:80 - binding '4' to parameter: 1
14:27:27,265 DEBUG StringType:80 - binding 'Martin' to parameter: 2
14:27:27,265 DEBUG StringType:80 - binding 'Paul' to parameter: 3
14:27:27,265 DEBUG DateType:80 - binding '31 janvier 2000' to parameter: 4
14:27:27,265 DEBUG BooleanType:80 - binding 'false' to parameter: 5
14:27:27,265 DEBUG IntegerType:80 - binding '6' to parameter: 6
14:27:27,265 DEBUG IntegerType:80 - binding '1' to parameter: 7
14:27:27,265 DEBUG IntegerType:80 - binding '3' to parameter: 8
14:27:27,265 DEBUG AbstractBatcher:366 - about to close PreparedStatement (open PreparedStatements: 1, globally: 1)
14:27:27,265 DEBUG AbstractBatcher:525 - closing statement
14:27:27,265 DEBUG ConnectionManager:472 - registering flush end
14:27:27,265 DEBUG HQLQueryPlan:150 - find: select p from Personne p order by p.nom asc
14:27:27,265 DEBUG QueryParameters:277 - named parameters: {}
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - select personne0_.ID as ID0_, personne0_.VERSION as VERSION0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.DATENAISSANCE as DATENAIS5_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_ from jpa01_personne personne0_ order by personne0_.NOM asc
...
14:27:27,265 DEBUG Loader:1164 - result row: EntityKey[entites.Personne#1]
...
14:27:27,265 DEBUG Loader:839 - total objects hydrated: 0
14:27:27,265 DEBUG StatefulPersistenceContext:748 - initializing non-lazy collections
[1,4,Martin,Paul,31/01/2000,false,6]
14:27:27,265 DEBUG JDBCTransaction:103 - commit
14:27:27,265 DEBUG SessionImpl:337 - automatically flushing session
...
14:27:27,265 DEBUG AbstractFlushingEventListener:91 - Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
...
14:27:27,296 DEBUG JDBCTransaction:116 - committed JDBC Connection
...
  • riga 1: il test 9 ha inizio
  • righe 2-6: la transazione JDBC ha inizio. La modalità autocommit di SGBD è disattivata (riga 5)
  • riga 7: output generato dalla riga 12 del codice Java. Le righe successive del codice Java genereranno un select e quindi una sincronizzazione del contesto di persistenza con il database.
  • riga 8: il comando JPQL che si desidera emettere è già stato emesso. Hibernate lo trova nella sua cache delle "query preparate".
  • riga 9: Hibernate annuncia che procederà a un flush del contesto di persistenza
  • righe 11-12: Hibernate (Hb) rileva che l’entità Personne#1 (con chiave primaria 1) è stata modificata (dirty).
  • righe 12-13: Hb annuncia che aggiorna questo elemento e ne porta il numero di versione da 3 a 4.
  • riga 15: la sincronizzazione del contesto provocherà 0 inserimenti, 1 aggiornamento (update), 0 eliminazioni (delete)
  • righe 17-34: sincronizzazione del contesto (flush). Da notare: l’incremento della versione (riga 19), l’ordine SQL update preparato (riga 21), i valori dei parametri dell’ordine update (righe 24-31).
  • riga 35: inizia il comando select
  • riga 38: l'ordine SQL che verrà eseguito
  • riga 40: il select restituisce solo una riga
  • riga 42: Hb rileva di avere già nel proprio contesto di persistenza l’entità Personne#1 che il select ha recuperato dal database. Non copia quindi la riga ottenuta dal database nel contesto, operazione che definisce “idratazione”.
  • riga 43: verifica se gli oggetti restituiti da select presentano dipendenze (in genere chiavi esterne) che dovrebbero essere caricate a loro volta (collezioni non lazy). In questo caso non ce ne sono.
  • riga 44: visualizzazione innescata dal codice Java
  • riga 45: fine della transazione JDBC richiesta dal codice Java
  • riga 46: ha inizio la sincronizzazione automatica del contesto che avviene durante l’esecuzione di commit.
  • riga 48: Hb rileva che il contesto non è cambiato dalla sincronizzazione precedente.
  • riga 50: fine di commit.

Ancora una volta, i log di Hibernate in modalità DEBUG si rivelano molto utili per capire esattamente cosa sta facendo Hibernate.

2.1.13.11. Test 10

Il codice di test10 è il seguente:


// controllo di versione (blocco ottimistico)
    public static void test10() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // incrementare la versione di newp1 direttamente nel database (query nativa)
        em.createNativeQuery(String.format("update %s set VERSION=VERSION+1 WHERE ID=%d", TABLE_NAME, newp1.getId())).executeUpdate();
        // fine transazione
        tx.commit();
        // inizio nuova transazione
        tx = em.getTransaction();
        tx.begin();
        // si incrementa il numero di figli di newp1
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // fine transazione - deve fallire perché newp1 non ha più la versione corretta
        try {
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur lors de la mise à jour de newp1 [%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause().getClass().getName(), e1.getCause().getMessage());
            // si esegue un rollback della transazione
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // si chiude il contesto che non è più aggiornato
        em.close();
        // dump della tabella - la versione di p1 deve essere cambiata
        dump();
    }
  • Il test 10 intende illustrare il meccanismo introdotto dal campo version dell’@Entity Personne, che è dotato dell’attributo JPA @Version. Abbiamo spiegato che questa annotazione fa sì che, nel database, il valore della colonna associata all’annotazione @Version venga incrementato ad ogni operazione update effettuata sulla riga a cui appartiene. Questo meccanismo, chiamato anche blocco ottimistico (optimistic locking), impone che il client che desidera modificare un oggetto O nel database disponga dell’ultima versione dello stesso. Se non ne dispone, significa che l’oggetto è stato modificato da quando lo ha ottenuto e occorre avvisarlo.
  • riga 4: non si modifica il contesto di persistenza. newp1 si trova quindi al suo interno.
  • righe 6-7: inizio di una transazione.
  • riga 9: la versione dell'oggetto newp1 viene incrementata di 1 (da 4 a 5) direttamente nel database. Le richieste di tipo nativeQuery aggirano il contesto di persistenza e accedono direttamente al database. Di conseguenza, l'oggetto persistente newp1 e la sua corrispondenza nel database non hanno più la stessa versione.
  • riga 10: fine della prima transazione
  • righe 13-14: inizio di una seconda transazione
  • riga 16: il numero di figli dell’oggetto persistente newp1 viene aumentato di 1 (da 6 a 7).
  • riga 19: fine della transazione. Ha quindi luogo una sincronizzazione. Questa provocherà l’aggiornamento del numero di figli di newp1 nel database. L'aggiornamento fallirà poiché l'oggetto persistente newp1 ha la versione 4, mentre nel database l'oggetto da aggiornare ha la versione 5. Verrà generata un'eccezione, il che giustifica l'uso del blocco try/catch nel codice.
  • riga 21: si visualizzano l’eccezione e la sua causa.
  • riga 25: rollback della transazione
  • riga 33: visualizzazione della tabella: si dovrebbe vedere che la versione di newp1 nel database è 5.

L'output della console del test 10 è il seguente:

1
2
3
4
5
6
7
main : ----------- test9
[personnes]
[1,4,Martin,Paul,31/01/2000,false,6]
main : ----------- test10
Erreur lors de la mise à jour de newp1 [javax.persistence.RollbackException,Error while commiting the transaction,org.hibernate.StaleObjectStateException,Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [entites.Personne#1]]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • riga 5: il commit genera effettivamente un'eccezione. È di tipo [javax.persistence.RollbackException]. Il messaggio associato è vago. Se si esamina la causa di questa eccezione (Exception.getCause), si nota che si tratta di un'eccezione Hibernate dovuta al fatto che si sta tentando di modificare una riga del database senza disporre della versione corretta.
  • riga 7: si nota che la versione di newp1 nel database è stata effettivamente aggiornata a 5 da nativeQuery.

2.1.13.12. Test 11

Il codice di test11 è il seguente:


// rollback di una transazione
    public static void test11() throws ParseException {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = null;
        try {
            tx = em.getTransaction();
            tx.begin();
            // si ricollega p1 al contesto recuperandolo dal database
            p1 = em.find(Personne.class, p1.getId());
            // si incrementa il numero di figli di p1
            p1.setNbenfants(p1.getNbenfants() + 1);
            // visualizzazione delle persone - il numero di figli di p1 deve essere cambiato
            System.out.println("[personnes]");
            for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
                System.out.println(p);
            }
            // creazione di 2 persone con lo stesso nome, cosa vietata dalla DDL
            Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            // persistenza delle persone
            em.persist(p3);
            em.persist(p4);
            // fine transazione
            tx.commit();
        } catch (RuntimeException e1) {
            // si è verificato un problema
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // si abbandona il contesto corrente
            em.clear();
        }
        // dump - la tabella non dovrebbe essere cambiata a causa del rollback
        dump();
    }
  • Il test 11 si concentra sul meccanismo del rollback di una transazione. Una transazione funziona secondo il principio «tutto o niente»: le operazioni SQL in essa contenute vengono o tutte eseguite con successo (commit), oppure tutte annullate in caso di fallimento di una di esse (rollback).
  • riga 4: si prosegue con lo stesso contesto di persistenza. Il lettore ricorderà forse che il contesto era stato chiuso in seguito al crash del test precedente. In questo caso, [getEntityManager] fornisce un contesto completamente nuovo, quindi vuoto.
  • righe 7-27: un unico try/catch per gestire gli eventuali problemi che si presenteranno
  • righe 8-9: inizio di una transazione che conterrà diverse operazioni SQL
  • riga 11: p1 viene cercato nel database e inserito nel contesto
  • riga 13: si aumenta il numero di figli di p1 (da 6 a 7)
  • righe 15-18: si visualizza il contenuto del database, il che forzerà una sincronizzazione del contesto. Nel database, il numero di figli di p1 passerà a 7, cosa che dovrebbe essere confermata dalla visualizzazione in console.
  • righe 20-21: creazione di 2 persone p3 e p4 con lo stesso nome. Tuttavia, il campo «nome» dell’@Entity «Persona» ha l’attributo unique=true, il che ha comportato l’applicazione di un vincolo di unicità sulla colonna NOM della tabella [jpa01_personne].
  • righe 23-24: le persone p3 e p4 vengono inserite nel contesto di persistenza.
  • riga 26: la transazione viene confermata. Segue una seconda sincronizzazione del contesto, la prima delle quali era avvenuta in occasione di select. JPA emetterà due ordini SQL e insert per le persone p3 e p4. p3 verrà inserito. Per p4, SGBD genererà un'eccezione, poiché p4 ha lo stesso nome di p3. p4 non viene quindi inserito e il driver JDBC segnala un'eccezione al client.
  • riga 27: si gestisce l’eccezione
  • righe 29-31: si visualizza l’eccezione e le due cause precedenti nella catena di eccezioni che ci hanno portato a questo punto.
  • riga 34: si esegue un rollback della transazione attualmente attiva. Questa è iniziata alla riga 9 del codice Java. Successivamente è stata eseguita un’operazione update per modificare il numero di figli di p1, seguita da un’operazione insert relativa alla persona p3. Tutto ciò verrà annullato dal rollback.
  • riga 39: il contesto di persistenza viene svuotato
  • riga 42: viene visualizzata la tabella [jpa01_personne]. È necessario verificare che p1 abbia ancora 6 figli e che né p3 p4 siano presenti nella tabella.

L'output della console del test 11 è il seguente:


main : ----------- test11
[personnes]
[1,6,Martin,Paul,31/01/2000,false,7]
14:50:30,312 ERROR JDBCExceptionReporter:72 - Duplicate entry 'X' for key 2
Erreur dans transaction [javax.persistence.EntityExistsException,org.hibernate.exception.ConstraintViolationException: could not insert: [entites.Personne],org.hibernate.exception.ConstraintViolationException,could not insert: [entites.Personne],java.sql.SQLException,Duplicate entry 'X' for key 2]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • riga 3: il numero di figli di p1 è passato da 6 a 7 nel database, la versione di p1 è passata a 6.
  • riga 4: l'eccezione rilevata durante il commit della transazione. Se si legge attentamente, si nota che la causa è una chiave duplicata X (il nome). È l'inserimento di p4 a provocare questo errore, poiché anche p3, già inserito, ha lo stesso nome X.
  • riga 7: la tabella dopo il rollback. p1 è tornato alla versione 5 e al numero di figli pari a 6, mentre p3 e p4 non sono stati inseriti.

2.1.13.13. Test 12

Il codice del test 12 è il seguente:


    // si ripete la stessa operazione ma senza le transazioni
    // si ottiene lo stesso risultato di prima con i SGBD: FIREBIRD, ORACLE XE, POSTGRES, MYSQL5
    // con SQLSERVER si ottiene una tabella vuota. La connessione viene lasciata in uno stato che impedisce la riesecuzione
    // del programma. È quindi necessario riavviare il server.
    // lo stesso vale per il Derby SGBD
    // HSQL inserisce la prima persona - non c'è rollback

    public static void test12() throws ParseException {
        // si ricollega p1
        p1 = em.find(Personne.class, p1.getId());
        // si incrementa il numero di figli di p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // visualizzazione delle persone - il numero di figli di p1 deve essere cambiato
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // creazione di 2 persone con lo stesso nome, cosa vietata dalla DDL
        Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        // persistenza delle persone
        em.persist(p3);
        em.persist(p4);
        // dump che provocherà la sincronizzazione del contesto em con BD
        try {
            dump();
        } catch (RuntimeException e3) {
            System.out.format("Erreur dans dump [%s,%s,%s,%s]%n", e3.getClass().getName(), e3.getMessage(), e3.getCause().getClass().getName(), e3
                    .getCause().getMessage());
        }
        // si chiude il contesto corrente
        em.close();
        // dump
        dump();
}
  • Il test 12 ripete la stessa operazione del test 11, ma al di fuori della transazione. Vogliamo vedere cosa succede in questo caso.
  • righe 1-6: riportano i risultati dei test con vari SGBD:
  • con un certo numero di SGBD (Firebird, Oracle, MySQL5, Postgres) si ottiene lo stesso risultato del test 11. Ciò fa supporre che questi SGBD abbiano avviato autonomamente una transazione che copre tutti gli ordini SQL ricevuti fino a quello che ha causato l’errore e che abbiano avviato essi stessi un rollback.
  • In presenza di altri SGBD (SQL Server, Apache Derby) si verifica un arresto anomalo dell’applicazione e/o del SGBD.
  • con il SGBD e il HSQLDB, sembra che la transazione aperta dal SGBD sia in modalità autocommit: la modifica del numero di figli di p1 e l’inserimento di p3 vengono resi permanenti. Solo l’inserimento di p4 fallisce.

Si ottiene quindi un risultato dipendente da SGBD, il che rende l’applicazione non portabile. È importante ricordare che le operazioni sul contesto di persistenza devono sempre essere eseguite all’interno di una transazione.

2.1.14. Modifica di SGBD

Torniamo all’architettura di test del nostro progetto attuale:

L’applicazione client [3] vede solo l’interfaccia JPA [5]. Non vede né la sua effettiva implementazione, né il SGBD di destinazione. Dovrebbe quindi essere possibile modificare questi due elementi della catena senza apportare modifiche al client [3]. È ciò che stiamo cercando di verificare ora, iniziando con la modifica di SGBD. Finora avevamo utilizzato MySQL5. Ne presentiamo altri sei descritti negli allegati (paragrafo 5), sperando che tra questi ci sia il SGBD preferito dal lettore.

In ogni caso, la modifica da apportare nel progetto Eclipse è semplice (vedi sotto): sostituire il file di configurazione del livello JPA con uno di quelli presenti nella cartella conf [2] del progetto. I driver JDBC e SGBD sono già presenti nella libreria [jpa-divers], [3] e [4].

2.1.14.1. Oracle 10g Express

Oracle 10g Express è presentato negli Allegati al paragrafo 5.7. Il file persistence.xml di Oracle è il seguente:


<?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="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <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.OracleDialect" />
            <!--  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>

Questa configurazione è identica a quella effettuata per i file SGBD e MySQL5, con le seguenti piccole differenze:

  • righe 15-18 che configurano il collegamento JDBC con il database
  • riga 22: che specifica il dialetto SQL da utilizzare

Per gli esempi che seguiranno, specificheremo solo le righe che cambiano. Per una spiegazione della configurazione, si consulti l’appendice dedicata al SGBD utilizzato. In essa viene fornito ogni volta un esempio di utilizzo del collegamento JDBC, nel contesto del plugin [SQL Explorer]. Grazie alle informazioni contenute nell’appendice, il lettore potrà ripetere l’operazione di verifica del risultato dell’applicazione [InitDB] effettuata al paragrafo 2.1.10.2.

Procediamo come indicato nel paragrafo sopra citato:

  • avviare il plugin Oracle SGBD
  • inserire conf/oracle/persistence.xml in META-INF/persistence.xml
  • eseguire l'applicazione [InitDB]

Sulla console si ottengono i seguenti risultati:

D'ora in poi non presenteremo più questa schermata, che è sempre la stessa. Più interessante è la prospettiva di SQL Explorer sul collegamento tra JDBC e SGBD. Seguiremo la procedura spiegata al paragrafo 2.1.8.

  • in [1]: la connessione con Oracle
  • in [2]: l'albero della connessione dopo l'esecuzione di [InitDB]
  • in [3]: la struttura della tabella [jpa01_personne]
  • in [4]: il suo contenuto.

Fatto ciò, il lettore è invitato a eseguire l'applicazione [Main] e quindi a terminare SGBD.

2.1.14.2. PostgreSQL 8.2

PostgreSQL 8.2 è riportato negli Allegati al paragrafo 5.6. Il relativo file persistence.xml è il seguente:


<?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">
...
            <!-- accesso JDBC -->
            <property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
            <property name="hibernate.connection.url" value="jdbc:postgresql:jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- Dialetto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
...
    </persistence-unit>
</persistence>

Per eseguire [InitDB]:

  • avviare SGBD PostgreSQL
  • inserire conf/postgres/persistence.xml in META-INF/persistence.xml
  • eseguire l'applicazione [InitDB]

La vista SQL Explorer del collegamento tra JDBC e SGBD è la seguente:

  • in [1]: il collegamento con PostgreSQL
  • in [2]: l'albero della connessione dopo l'esecuzione di [InitDB]
  • in [3]: la struttura della tabella [jpa01_personne]
  • in [4]: il suo contenuto.

Fatto ciò, il lettore è invitato a eseguire l'applicazione [Main] e quindi a terminare SGBD

2.1.14.3. SQL Server Express 2005

SQL Server Express 2005 è presentato negli Allegati al paragrafo 5.8, pagina 270. Il suo file persistence.xml è il seguente:


<?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">
...
            <!-- connessione JDBC -->
            <property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
            <property name="hibernate.connection.url" value="jdbc:sqlserver://localhost\\SQLEXPRESS:1433;databaseName=jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- Dialetto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
...
    </persistence-unit>
</persistence>

Per eseguire [InitDB]:

  • avviare SGBD SQL Server
  • inserire conf/sqlserver/persistence.xml in META-INF/persistence.xml
  • eseguire l'applicazione [InitDB]

La vista SQL Explorer del collegamento tra JDBC e SGBD è la seguente:

  • in [1]: la connessione con il server SQL
  • in [2]: l'albero della connessione dopo l'esecuzione di [InitDB]
  • in [3]: la struttura della tabella [jpa01_personne]
  • in [4]: il suo contenuto.

Fatto ciò, il lettore è invitato a eseguire l'applicazione [Main] e quindi a terminare SGBD

2.1.14.4. Firebird 2.0

Firebird 2.0 è presentato negli Allegati al paragrafo 5.4. Il suo file persistence.xml è il seguente:


<?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">
...
            <!-- connessione JDBC -->
            <property name="hibernate.connection.driver_class" value="org.firebirdsql.jdbc.FBDriver" />
            <property name="hibernate.connection.url" value="jdbc:firebirdsql:localhost/3050:C:\data\2006-2007\eclipse\dvp-jpa\annexes\firebird\jpa.fdb" />
            <property name="hibernate.connection.username" value="sysdba" />
            <property name="hibernate.connection.password" value="masterkey" />
...
            <!-- Dialetto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.FirebirdDialect" />
...
    </persistence-unit>
</persistence>

Per eseguire [InitDB]:

  • avviare il file SGBD di Firebird
  • inserire conf/firebird/persistence.xml in META-INF/persistence.xml
  • eseguire l'applicazione [InitDB]

La vista SQL Explorer del collegamento tra JDBC e SGBD è la seguente:

  • in [1]: la connessione con Firebird
  • in [2]: l'albero della connessione dopo l'esecuzione di [InitDB]
  • in [3]: la struttura della tabella [jpa01_personne]
  • in [4]: il suo contenuto.

Fatto ciò, il lettore è invitato a eseguire l'applicazione [Main] e quindi a terminare SGBD.

2.1.14.5. Apache Derby

Apache Derby è descritto negli Allegati al paragrafo 5.10. Il suo file persistence.xml è il seguente:


<?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">
...
            <!-- connessione JDBC -->
            <property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.ClientDriver" />
            <property name="hibernate.connection.url" value="jdbc:derby://localhost:1527//data/2006-2007/eclipse/dvp-jpa/annexes/derby/jpa;create=true" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- Dialetto -->
...
    </persistence-unit>
</persistence>

Per eseguire [InitDB]:

  • avviare il file SGBD di Apache Derby
  • inserire conf/derby/persistence.xml in META-INF/persistence.xml
  • eseguire l'applicazione [InitDB]

La vista SQL Explorer del collegamento tra JDBC e SGBD è la seguente:

  • in [1]: la connessione con Apache Derby
  • in [2]: l'albero della connessione dopo l'esecuzione di [InitDB]. Si noti la tabella [HIBERNATE_UNIQUE_KEY] creata da JPA / Hibernate per generare automaticamente i valori successivi della chiave primaria ID. Abbiamo già indicato che questo meccanismo è spesso proprietario. Lo si vede chiaramente qui. Grazie a JPA, lo sviluppatore non deve occuparsi di questi dettagli di SGBD.
  • in [3]: la struttura della tabella [jpa01_personne]
  • in [4]: il suo contenuto.

Fatto ciò, il lettore è invitato a eseguire l’applicazione [Main] e quindi a terminare SGBD.

2.1.14.6. HSQLDB

HSQLDB è riportato negli Allegati al paragrafo 5.9. Il relativo file persistence.xml è il seguente:


<?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">
...
            <!-- connessione JDBC -->
            <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
            <property name="hibernate.connection.url" value="jdbc:hsqldb:hsql://localhost" />
            <property name="hibernate.connection.username" value="sa" />
            <!-- 
                <property name="hibernate.connection.password" value="" />
            -->
...
            <!-- Dialetto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
...
        </properties>
    </persistence-unit>
</persistence>

Per eseguire [InitDB]:

  • avviare il file SGBD HSQL
  • inserire conf/hsql/persistence.xml in META-INF/persistence.xml
  • eseguire l'applicazione [InitDB]

La vista SQL Explorer del collegamento tra JDBC e SGBD è la seguente:

  • in [1]: il collegamento con HSQL
  • in [2]: l'albero della connessione dopo l'esecuzione di [InitDB].
  • in [3]: la struttura della tabella [jpa01_personne]
  • in [4]: il suo contenuto.

Fatto ciò, il lettore è invitato a eseguire l'applicazione [Main] e quindi a terminare SGBD.

2.1.15. Cambiare l'implementazione JPA

Torniamo all’architettura di test del nostro progetto attuale:

Lo studio precedente ha dimostrato che siamo riusciti a sostituire SGBD con [7] senza apportare alcuna modifica al codice client [3]. Ora modifichiamo l’implementazione JPA [6] e dimostriamo ancora una volta che ciò avviene in modo trasparente per il codice client [3]. Prendiamo un’implementazione TopLink e [http://www.oracle.com/technology/products/ias/toplink/jpa/index.html]:

2.1.15.1. Il progetto Eclipse

In occasione del cambio di implementazione JPA, creiamo un nuovo progetto Eclipse per non interferire con il progetto esistente. Infatti, il nuovo progetto utilizza librerie di persistenza che potrebbero entrare in conflitto con quelle di Hibernate:

  • in [1]: la cartella [<exemples>/toplink/direct/personnes-entites] contiene il progetto Eclipse. Importarlo.
  • in [2]: il progetto [toplink-personnes-entites] importato. È identico (è stato ottenuto tramite copia) al progetto [hibernate-personne-entites], tranne che per due dettagli:
    • il file [META-INF/persistence.xml] [3] ora configura un livello JPA / Toplink
    • la libreria [jpa-hibernate] è stata sostituita dalla libreria [jpa-toplink], [4] e [5] (cfr. paragrafo 1.5).
  • in [6]: la cartella [conf] contiene una versione del file [persistence.xml] per ogni SGBD.
  • in [7]: la cartella [ddl] che conterrà gli script SQL per la generazione dello schema del database.

Sappiamo che il livello JPA è configurato dal file [META-INF/persistence.xml]. Quest’ultimo configura ora un’implementazione JPA / Toplink. Il suo contenuto per un livello JPA interfacciato con SGBD e MySQL5 è il seguente:


<?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>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- classi persistenti -->
        <class>entites.Personne</class>
        <!-- proprietà dell'unità di persistenza -->
        <properties>
            <!-- connessione JDBC -->
            <property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="toplink.jdbc.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="MySQL4" />
            <!--  server dell'applicazione -->
            <property name="toplink.target-server" value="None" />
            <!--  generazione dello schema -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- log -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>
  • riga 3: invariata
  • riga 5: il provider è ora Toplink. La classe qui indicata si trova nella libreria [jpa-toplink] ([1] di seguito):
  • riga 7: il tag <class> serve a specificare tutte le classi @Entity del progetto, in questo caso solo la classe Personne. Hibernate disponeva di un'opzione di configurazione che ci evitava di specificare tali classi. Esplorava il file classpath del progetto per individuare le classi @Entity.
  • riga 9: il tag <properties> che introduce le proprietà specifiche dell’implementazione JPA utilizzata, in questo caso Toplink.
  • righe 11-14: configurazione del collegamento JDBC con il SGBD MySQL5
  • righe 15-18: configurazione del pool di connessioni JDBC gestito in modo nativo da Toplink:
  • righe 15, 16: numero massimo e minimo di connessioni nel pool di connessioni in lettura. Valore predefinito (2,2)
  • righe 17, 18: numero massimo e minimo di connessioni nel pool di connessioni in scrittura. Valore predefinito (10,2)
  • riga 20: il SGBD di destinazione. L'elenco dei SGBD utilizzabili è disponibile nel pacchetto [oracle.toplink.essentials.platform.database] (cfr. [2] sopra). Il SGBD MySQL5 non è presente nell'elenco [2], pertanto è stato scelto MySQL4. Toplink supporta un numero leggermente inferiore di SGBD rispetto a Hibernate. Pertanto, dei sette SGBD utilizzati nei nostri esempi, Firebird non è supportato. Nell’elenco non compare nemmeno Oracle. In realtà si trova in un altro pacchetto ([3] sopra indicato). Se in questi due pacchetti il SGBD di destinazione è indicato dalla classe <Sgbd>Platform.class, il tag sarà scritto come segue:

            <property name="toplink.target-database" value="<Sgbd>" />
  • riga 22: imposta il server dell’applicazione se l’applicazione viene eseguita su un server di questo tipo. Valori attualmente possibili (None, OC4J_10_1_3, SunAS9). Impostazione predefinita (None).
  • righe 24-28: all’inizializzazione del livello JPA, gli viene richiesto di eseguire una pulizia del database definito dal collegamento JDBC delle righe 11-14. Si partirà così da un database vuoto.
    • riga 24: si richiede a Toplink di eseguire un drop seguito da un create sulle tabelle dello schema del database
    • riga 25: si richiede a Toplink di generare gli script SQL relativi alle operazioni drop e create. application-location specifica la cartella in cui verranno generati tali script. Impostazione predefinita: (cartella corrente).
    • riga 26: nome dello script SQL delle operazioni create.. Impostazione predefinita: createDDL.jdbc.
    • riga 27: nome dello script SQL delle operazioni drop.. Impostazione predefinita: dropDDL.jdbc.
    • riga 28: modalità di generazione dello schema (Predefinito: both):
      • both: script e database
      • database: solo database
      • sql-script: solo script
  • riga 30: si disabilitano (OFF) i log di Toplink. I diversi livelli di accesso disponibili sono i seguenti: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST. Impostazione predefinita: INFO.

Per una definizione esaustiva dei tag <property> utilizzabili con Toplink, consultare l'URL [http://www.oracle.com/technology/products/ias/toplink/JPA/essentials/toplink-jpa-extensions.html].

2.1.15.3. Test [InitDB]

Non c’è altro da fare. Siamo pronti per eseguire il primo test [InitDB]:

  • avviare il SGBD, in questo caso MySQL5
  • eseguire [InitDB]
  • in [1]: l’output della console. Ritroviamo i risultati già ottenuti con JPA / Hibernate.
  • in [3]: si apre la prospettiva [SQL Explorer], quindi si apre la connessione [mysql5-jpa]
  • in [4]: la struttura ad albero del database jpa. Si nota che l'esecuzione di [InitDB] ha creato due tabelle: [jpa01_personne], che era prevista, e la tabella [sequence], che invece non era prevista.
  • in [5]: la struttura della tabella [jpa01_personne] e in [6] il suo contenuto
  • in [7]: la struttura della tabella [sequence] e in [8] il suo contenuto.

Il file di configurazione [persistence.xml] richiedeva la generazione degli script di DDL:


            <!--  generazione dello schema -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />

Vediamo cosa è stato generato nella cartella [ddl/mysql5]:

 

create.sql


CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • riga 1: il record DDL della tabella [jpa01_personne]. Si nota che Toplink non ha utilizzato l'attributo autoincrement per la chiave primaria ID. Di conseguenza, non si ha un incremento automatico della stessa durante l'inserimento delle righe.
  • riga 2: la tabella DDL della tabella [sequence]. Il suo nome sembra indicare che Toplink utilizzi questa tabella per generare i valori della chiave primaria ID.
  • riga 3: inserimento di una singola riga nella tabella [SEQUENCE]

drop.sql


DROP TABLE jpa01_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
  • riga 1: eliminazione della tabella [jpa01_personne]
  • riga 2: eliminazione di una riga specifica dalla tabella [SEQUENCE]. La tabella stessa non viene eliminata, né le altre eventuali righe che potrebbe contenere.

Per saperne di più sul ruolo della tabella [SEQUENCE], in [persistence.xml] si attivano i log di Toplink a livello FINE, un livello che traccia i comandi SQL emessi da Toplink:


            <!-- log -->
<property name="toplink.logging.level" value="FINE" />

Si riesegue InitDB. Di seguito è riportata solo una parte della visualizzazione della console:


...
[TopLink Config]: 2007.05.28 12:07:52.796--ServerSession(12910198)--Connessione(30708295)--Thread(Thread[main,5,main])--Connected: jdbc:mysql://localhost:3306/jpa
    User: jpa@localhost
    Database: MySQL  Version: 5.0.37-community-nt
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
...
[TopLink Fine]: 2007.05.28 12:07:53.093--ServerSession(12910198)--Connessione(19255406)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.265--ServerSession(12910198)--Connessione(30708295)--Thread(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Connessione(19255406)--Thread(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[TopLink Warning]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Thread(Thread[main,5,main])--Eccezione [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (30/03/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery()
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Connessione(30708295)--Thread(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Connessione(19255406)--Thread(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Connessione(30708295)--Thread(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) valori ('SEQ_GEN', 1)
[TopLink Fine]: 2007.05.28 12:07:53.734--ClientSession(15308417)--Connessione(14069849)--Thread(Thread[main,5,main])--elimina da jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Connessione(14069849)--Thread(Thread[main,5,main])--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ? WHERE SEQ_NAME = ?
    bind => [50, SEQ_GEN]
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Connessione(14069849)--Thread(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
    bind => [SEQ_GEN]
[personnes]
[TopLink Fine]: 2007.05.28 12:07:53.906--ClientSession(15308417)--Connessione(14069849)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Fine]: 2007.05.28 12:07:53.921--ClientSession(15308417)--Connessione(14069849)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
[TopLink Fine]: 2007.05.28 12:07:53.937--ClientSession(15308417)--Connessione(14069849)--Thread(Thread[main,5,main])--SELECT ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS FROM jpa01_personne ORDER BY NOM ASC
[3,1,Durant,Sylvie,05/07/2001,false,0]
[2,1,Martin,Paul,31/01/2000,true,2]
[TopLink Config]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Connessione(30708295)--Thread(Thread[main,5,main])--disconnessione
[TopLink Info]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Thread(Thread[main,5,main])--file:/C:/data/2006-2007/eclipse/dvp-jpa/toplink/direct/personnes-entites/bin/-jpa logout riuscito
...
terminé ...
  • righe 2-5: una connessione a SGBD con i relativi parametri. In realtà, i log mostrano che Toplink crea 3 connessioni con SGBD. Occorrerebbe verificare se questo numero sia correlato a uno dei valori di configurazione utilizzati per il pool di connessioni JDBC:

            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
  • riga 7: eliminazione della tabella [jpa01_personne]. Normale, poiché il file [persistence.xml] richiede la pulizia del database JPA.
  • riga 8: creazione della tabella [jpa01_personne]. Si nota che la chiave primaria ID non presenta l’attributo autoincrement.
  • riga 9: creazione della tabella [SEQUENCE], che esiste già, essendo stata creata durante la precedente esecuzione.
  • righe 10-13: Toplink segnala l'errore di creazione della tabella [SEQUENCE].
  • righe 15-18: Toplink pulisce la tabella [SEQUENCE]. Al termine di questa operazione di pulizia, la tabella [SEQUENCE] contiene una riga (SEQ_NAME, SEQ_COUNT) con i valori ('SEQ_GEN', 1).
  • riga 18: la tabella [jpa01_personne] viene svuotata.
  • righe 19-20: Toplink trasferisce l'unica riga in cui SEQ_NAME='SEQ_GEN' dalla tabella [SEQUENCE], dal valore ('SEQ_GEN', 1) al valore ('SEQ_GEN', 51)
  • riga 21: Toplink recupera il valore 51 dalla riga ('SEQ_GEN', 51) della tabella [SEQUENCE].
  • righe 24-27: Toplink inserisce nella tabella [jpa01_personne] le due persone 'Martin' e 'Durant'. Qui c'è un mistero: le chiavi primarie di queste due righe assumono i valori 2 e 3 senza che si sappia come siano stati ottenuti tali valori. Non si sa se il valore SEQ_COUNT (51) ottenuto alla riga 21 sia servito a qualcosa. Si noti che il valore della versione delle righe è 1, mentre Hibernate partiva da 0.
  • riga 28: Toplink genera il valore SELECT per ottenere tutte le righe della tabella [jpa01_personne]
  • righe 29-30: righe visualizzate dal client Java
  • righe 31-32: Toplink chiude una connessione. Ripeterà l’operazione per ciascuna delle connessioni inizialmente aperte.

In definitiva, non si conosce esattamente la funzione della tabella [SEQUENCE], ma sembra comunque che essa svolga un ruolo nella generazione dei valori della chiave primaria ID. Impostando il livello di log più dettagliato, FINEST, si ottengono ulteriori informazioni sul ruolo della tabella [SEQUENCE].


            <!-- log -->
            <property name="toplink.logging.level" value="FINEST" />

Di seguito abbiamo riportato solo i log relativi all’inserimento delle due persone nella tabella. È qui che si vede il meccanismo di generazione dei valori della chiave primaria:

[TopLink Finest]: 2007.05.28 03:05:04.046--ClientSession(30617157)--Thread(Thread[main,5,main])--Esegui query ValueReadQuery()
[TopLink Fine]: 2007.05.28 03:05:04.046--ClientSession(30617157)--Connessione(13301441)--Thread(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
    bind => [SEQ_GEN]
[TopLink Finest]: 2007.05.28 03:05:04.062--ClientSession(30617157)--Connessione(13301441)--Thread(Thread[main,5,main])--preallocazione sequenziale locale per SEQ_GEN: oggetti: 50, primo: 2, ultimo: 51
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--assegnazione della sequenza all'oggetto (2 -> [null,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Esegui query DoesExistQuery()
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--PERSIST operazione richiamata su: [null,0,Durant,Sylvie,05/07/2001,false,0].
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--assegnare una sequenza all'oggetto (3 -> [null,0,Durant,Sylvie,05/07/2001,false,0])
[personnes]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Esegui query InsertObjectQuery([3,0,Durant,Sylvie,05/07/2001,false,0])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Assegna riga di ritorno DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Connessione(13301441)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Esegui query InsertObjectQuery([2,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--Assegnazione riga di ritorno DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Connessione(13301441)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
  • riga 4: si vede che il numero 51 recuperato dalla tabella [SEQUENCE] alla riga 2 serve a delimitare un intervallo di valori per la chiave primaria: [2,51]
  • riga 5: alla prima persona viene assegnato il valore 2 come chiave primaria
  • riga 8: alla seconda persona viene assegnato il valore 3 come chiave primaria
  • riga 12: mostra la gestione delle versioni della prima persona
  • riga 17: lo stesso vale per la seconda persona

Il livello di log [FINEST] mostra anche i limiti delle transazioni emesse da Toplink. L’analisi di questi log illustra il funzionamento di Toplink e rappresenta un ottimo strumento per comprendere il ponte tra il modello a oggetti e quello relazionale.

Da quanto sopra si ricava quanto segue:

  • che implementazioni diverse di JPA genereranno schemi di database diversi. In questo esempio, Hibernate e Toplink non hanno generato gli stessi schemi.
  • che i livelli di log FINE, FINER, FINEST di Toplink andranno utilizzati ogni volta che si desideri chiarire cosa stia facendo esattamente Toplink.

2.1.15.4. Test [Main]

Ora eseguiamo il test [Main]:

  • in [1]: tutti i test vengono superati tranne il test 11 [2]
  • in [3]: riga 376, la riga di codice in cui si è verificata l'eccezione

Il codice che genera l’eccezione è il seguente:


} catch (RuntimeException e1) {
            // si è verificato un problema
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
            ...
  • riga [3]: la riga dell’eccezione. Abbiamo un NullPointerException, il che suggerisce che uno dei metodi getCause delle righe 4 e 5 abbia restituito un puntatore null. Un'espressione come [e1.getCause().getCause()] presuppone che la catena di eccezioni contenga 3 elementi [e1.getCause().getCause(), e1.getCause(), e1]. Se ne contiene solo due, la prima espressione causerà un'eccezione.

Modifichiamo il codice precedente in modo che visualizzi solo le ultime due eccezioni della catena di eccezioni:


        } catch (RuntimeException e1) {
            // si è verificato un problema
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage());
            try {
...

All'esecuzione, si ottiene quindi il seguente risultato:


...
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
main : ----------- test11
[personnes]
Erreur dans transaction [javax.persistence.OptimisticLockException,Exception [TOPLINK-5006] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.OptimisticLockException
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],oracle.toplink.essentials.exceptions.OptimisticLockException,
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],]
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]

Questa volta, il test 11 viene superato. Le visualizzazioni relative all’eccezione (righe 6-10) sono state richieste dal codice Java (riga 3 del codice sopra riportato). Ricordiamo che il test 11 concatenava, in un’unica transazione, diverse operazioni SQL, una delle quali falliva e doveva comportare un rollback della transazione. Gli stati della tabella [jpa01_personne] prima (riga 3) e dopo il test (riga 12) sono identici, a dimostrazione del fatto che il rollback ha avuto luogo.

Va sottolineato un punto importante: le implementazioni JPA / Hibernate e JPA / Toplink non sono intercambiabili al 100%. In questo esempio, dobbiamo modificare il codice del client JPA per evitare un NullPointerException. Ritroveremo questo problema in seguito e nuovamente nel contesto di un'eccezione.

Torniamo all’architettura di test del nostro progetto attuale:

In precedenza, il SGBD utilizzato in [7] era MySQL5. Mostriamo con Oracle come passare da SGBD. In ogni caso, la modifica da apportare nel progetto Eclipse è semplice (vedi sotto): sostituire il file di configurazione del livello JPA, persistence.xml [1], con uno di quelli presenti nella cartella conf ([2] e [3]) del progetto.

2.1.16.1. Oracle 10g Express

Oracle 10g Express è presentato negli Allegati al paragrafo 5.7. Il file persistence.xml di Oracle per Toplink è il seguente:


<?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>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- classi persistenti -->
        <class>entites.Personne</class>
        <!-- proprietà dell'unità di persistenza -->
        <properties>
            <!-- connessione JDBC -->
            <property name="toplink.jdbc.driver" value="oracle.jdbc.OracleDriver" />
            <property name="toplink.jdbc.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="Oracle" />
            <!--  server dell'applicazione -->
            <property name="toplink.target-server" value="None" />
            <!--  generazione dello schema -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/oracle" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- log -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>

Questa configurazione è identica a quella effettuata per i file SGBD e MySQL5, con le seguenti differenze:

  • righe 11-14 che configurano il collegamento JDBC con il database
  • riga 20: che specifica il SGBD di destinazione
  • riga 25: che imposta la cartella di generazione degli script SQL del DDL

Per eseguire il test [InitDB]:

  • avviare il processo Oracle SGBD
  • inserire conf/oracle/persistence.xml in META-INF/persistence.xml
  • eseguire l'applicazione [InitDB]

Si ottengono i seguenti risultati sulla console e nella prospettiva [SQL Explorer]:

  • [1]: visualizzazione della console
  • [2]: la connessione [oracle-jpa] in SQL Explorer
  • [3]: il database jpa
  • [4]: InitDB ha creato due tabelle: JPA01_PERSONNE e SEQUENCE, come nel caso di MySQL5. A volte, in [4], compaiono tabelle [BIN*]. Queste corrispondono a tabelle eliminate. Per osservare il fenomeno, è sufficiente rieseguire [InitDB]. La fase di inizializzazione del livello JPA prevede una pulizia del database jpa durante la quale la tabella [JPA01_PERSONNE] viene eliminata:

In [A] compare una tabella [BIN]. Oracle non elimina definitivamente una tabella che ha subito un'operazione drop, ma la sposta nel cestino [Recycle Bin]. Questo cestino è visibile ([B]) tramite lo strumento SQL Developer descritto al paragrafo 5.7.4. In [B] è possibile svuotare la tabella [JPA01_PERSONNE] che si trova nel cestino. Ciò svuota il cestino [C]. Se in SQL Explorer si aggiornano (clic destro / Refresh) le tabelle, si nota che la tabella BIN non è più presente [D].

  • [5, 6]: struttura e contenuto della tabella [JPA01_PERSONNE]
  • [7, 8]: struttura e contenuto della tabella [SEQUENCE]

Ecco fatto! Il lettore è ora invitato a eseguire l’applicazione [Main] su Oracle.

2.1.16.2. Gli altri SGBD

Non ci soffermeremo molto sugli altri SGBD. È sufficiente replicare la procedura seguita per Oracle. Si notino i seguenti punti:

  • indipendentemente dal SGBD, Toplink utilizza sempre la stessa tecnica per la generazione dei valori della chiave primaria ID della tabella [JPA01_PERSONNE]: utilizza la tabella [SEQUENCE] descritta in precedenza.
  • Toplink non riconosce il SGBD di Firebird. Esiste un database generico per questi casi:
                <property name="toplink.target-database" value="Auto" />

Con questa base generica denominata [Auto], i test con Firebird falliscono a causa di errori di sintassi SQL. Toplink utilizza per la chiave primaria ID un tipo SQL Number(10) che Firebird non riconosce. È quindi necessario scegliere un SGBD con gli stessi tipi SQL di Firebird (in questo esempio). È il caso di Apache Derby:


            <!-- connessione JDBC -->
            <property name="toplink.jdbc.driver" value="org.firebirdsql.jdbc.FBDriver" />
...
            <!-- SGBD -->
            <!-- 
            TopLink ne reconnaît pas Firebird pour l'instant (05/07). Derby convient pour remplacer.
            -->
            <property name="toplink.target-database" value="Derby" />
...
  • Toplink non è in grado di generare lo schema originale del database per il SGBD HSQLDB. Ciò significa che la direttiva:

            <!--  generazione schema -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />

non funziona per HSQLDB. La causa è un errore di sintassi durante la creazione della tabella [jpa01_personne]:


[TopLink Fine]: 2007.05.29 09:44:18.515--ServerSession(12910198)--Connessione(29775659)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Connessione(29775659)--Thread(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Warning]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Thread(Thread[main,5,main])--Eccezione [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (30/03/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Unexpected token: UNIQUE in statement [CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE]

Riga 4, la sintassi NOM VARCHAR(30) UNIQUE NOT NULL non è accettata da HSQL. Hibernate aveva utilizzato la sintassi: NOM VARCHAR(30) NOT NULL, UNIQUE(NOM).

In generale, Hibernate si è dimostrato più efficace di Toplink nel riconoscere i SGBD utilizzati per i test descritti in questo documento.

2.1.17. Conclusione

Lo studio dell’@Entity [Personne] si conclude qui. Da un punto di vista concettuale, è stato fatto ben poco: abbiamo studiato il ponte oggetto/relazionale nel caso più semplice: un oggetto @Entity <--> una tabella. Il suo studio ci ha tuttavia permesso di presentare gli strumenti che utilizzeremo in tutto il documento. Ciò ci consentirà d’ora in poi di procedere un po’ più rapidamente nello studio degli altri casi del ponte oggetto/relazionale che esamineremo:

  • all’@Entity [Personne] precedente, aggiungeremo un campo adresse modellato da una classe [Adresse]. Dal punto di vista del database, vedremo due possibili implementazioni. Gli oggetti [Personne] e [Adresse] danno origine a
  • un'unica tabella [personne] contenente l'indirizzo
  • due tabelle [personne] e [adresse] collegate da una relazione di chiave esterna di tipo uno-a-uno.
  • un esempio di relazione uno-a-molti in cui una tabella [article] è collegata a una tabella [categorie] tramite una chiave esterna
  • un esempio di relazione molti-a-molti in cui due tabelle, [personne] e [activite], sono collegate tramite una tabella di join [personne_activite].

2.2. Esempio 2: relazione uno-a-uno tramite inclusione

2.2.1. Lo schema del database

 
1
2

    drop table if exists jpa02_personne;

    create table jpa02_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
) ENGINE=InnoDB;

  • in [1]: il database (plugin Azurri Clay)
  • in [2]: la tabella DDL generata da Hibernate per MySQL5

La tabella [jpa02_personne] è la tabella [jpa01_personne] esaminata in precedenza, alla quale è stato aggiunto un indirizzo (righe 12-18 della DDL).

2.2.2. Gli oggetti @Entity che rappresentano il database

L'indirizzo di una persona sarà rappresentato dalla seguente classe [Adresse]:


package entites;

...
@SuppressWarnings("serial")
@Embeddable
public class Adresse implements Serializable {

    // campi
    @Column(length = 30, nullable = false)
    private String adr1;

    @Column(length = 30)
    private String adr2;

    @Column(length = 30)
    private String adr3;

    @Column(length = 5, nullable = false)
    private String codePostal;

    @Column(length = 20, nullable = false)
    private String ville;

    @Column(length = 3)
    private String cedex;

    @Column(length = 20, nullable = false)
    private String pays;

    // costruttori
    public Adresse() {

    }

    public Adresse(String adr1, String adr2, String adr3, String codePostal, String ville, String cedex, String pays) {
...
    }

    // getter e setter
...

    // toString
    public String toString() {
        return String.format("A[%s,%s,%s,%s,%s,%s,%s]", getAdr1(), getAdr2(), getAdr3(), getCodePostal(), getVille(), getCedex(), getPays());
    }
}
  • la principale novità risiede nell'annotazione @Embeddable alla riga 5. La classe [Adresse] non è destinata a generare una tabella, pertanto non presenta l'annotazione @Entity. L'annotazione @Embeddable indica che la classe è destinata a essere integrata in un oggetto @Entity e quindi nella tabella ad esso associata. Ecco perché, nello schema del database, la classe [Adresse] non compare come una tabella a sé stante, ma come parte della tabella associata all’@Entity [Personne].

L’@Entity [Personne] presenta poche modifiche rispetto alla versione precedente: viene semplicemente aggiunto un campo adresse:


package entites;

...
@Entity
@Table(name = "jpa02_hb_personne")
public class Personne implements Serializable{

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

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

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

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    @Embedded
    private Adresse adresse;

    // costruttori
    public Personne() {
    }
...
}
  • la modifica avviene alle righe 33-34. L’oggetto [Personne] presenta ora un campo adresse di tipo Adresse. Questo vale per il POJO. L'annotazione @Embedded è destinata al ponte oggetto/relazionale. Indica che il campo [Adresse adresse] dovrà essere incapsulato nella stessa tabella dell'oggetto [Personne].

2.2.3. L'ambiente di test

Eseguiremo dei test molto simili a quelli esaminati in precedenza. Verranno effettuati nel seguente contesto:

L’implementazione utilizzata è JPA / Hibernate [6]. Il progetto Eclipse dei test è il seguente:

Il progetto Eclipse [1] differisce dal precedente solo per i codici Java [2]. L'ambiente (librerie – persistence.xml – SGBD – cartelle conf, DDL – script Ant) è quello già esaminato in precedenza, in particolare al paragrafo 2.1.5. Sarà sempre così per i futuri progetti Hibernate e, salvo eccezioni, non torneremo più su questo ambiente. In particolare, i file persistence.xml che configurano il livello JPA/Hibernate per diversi SGBD sono quelli già esaminati e che si trovano nella cartella <conf>.

In caso di dubbi sulle procedure da seguire, il lettore è invitato a consultare quelle illustrate nello studio precedente.

Il progetto Eclipse è presente come [3] nella cartella degli esempi [4]. Lo importeremo.

2.2.4. Generazione del file DDL dal database

Seguendo le istruzioni del paragrafo 2.1.7, il file DDL ottenuto per il SGBD MySQL5 è il seguente:


    drop table if exists jpa02_hb_personne;

    create table jpa02_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
) ENGINE=InnoDB;

Hibernate ha correttamente riconosciuto che l'indirizzo della persona doveva essere inserito nella tabella associata all'@Entity Personne (righe 11-17).

2.2.5. InitDB

Il codice di [InitDB] è il seguente:


package tests;
...

public class InitDB {

    // costanti
    private final static String TABLE_NAME = "jpa02_hb_personne";

    public static void main(String[] args) throws ParseException {

        // Contesto di persistenza
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // si recupera un EntityManager  dal precedente EntityManagerFactory
        em = emf.createEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // richiesta
        Query sql1;
        // eliminazione degli elementi dalla tabella PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_NAME);
        sql1.executeUpdate();
        // creazione persone
        Personne 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);
        // creazione indirizzi
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // associazioni persona <--> indirizzo
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // persistenza delle persone
        em.persist(p1);
        em.persist(p2);
        // visualizzazione persone
        System.out.println("[personnes]");
        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 EntityManager
        em.close();
        // fine EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}

Non c’è nulla di nuovo in questo codice. È già stato visto tutto. L’esecuzione di [InitDB] insieme a MySQL5 fornisce i seguenti risultati:

  • [1]: l'output della console
  • [2]: la tabella [jpa02_hb_personne] nella prospettiva SQL Explorer
  • [3] e [4]: struttura e contenuto.

2.2.6. Home

La classe [Main] è la seguente:


package tests;

...
import entites.Adresse;
import entites.Personne;

@SuppressWarnings( { "unused", "unchecked" })
public class Main {

    // costanti
    private final static String TABLE_NAME = "jpa02_hb_personne";

    // Contesto di persistenza
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = null;

    // oggetti condivisi
    private static Personne p1, p2, newp1;

    private static Adresse a1, a2, a3, a4, newa1, newa4;

    public static void main(String[] args) throws Exception {
        // si recupera un EntityManager  da EntityManagerFactory
        em = emf.createEntityManager();

        // pulizia del database
        log("clean");clean();

        // dump della tabella
        dumpPersonne();

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

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

        // test3
        log("test3"); test3();

        // test4
        log("test4"); test4();

        // test5
        log("test5");test5();

        // fine contesto di persistenza
        if (em != null && em.isOpen())
            em.close();

        // chiusura EntityManagerFactory
        emf.close();
    }

    // recuperare l'EntityManager corrente
    private static EntityManager getEntityManager() {
...
    }

    // recuperare un nuovo EntityManager
    private static EntityManager getNewEntityManager() {
...
    }

    // visualizza il contenuto della tabella «Persona»
    private static void dumpPersonne() {
...
    }

    // azzerare BD
    private static void clean() {
    ...
    }

    // log
    private static void log(String message) {
...
    }

    // creazione di oggetti
    public static void test1() throws ParseException {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // creazione persone
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // creazione indirizzi
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // associazioni persona <--> indirizzo
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistenza delle persone
        em.persist(p1);
        em.persist(p2);
        // fine transazione
        tx.commit();
        // dump
        dumpPersonne();
    }

    // modifica di un oggetto del contesto
    public static void test2() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si incrementa il numero di figli di p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // si modifica il suo stato civile
        p1.setMarie(false);
        // l'oggetto p1 viene salvato automaticamente (dirty checking)
        // alla successiva sincronizzazione (commit o select)
        // fine transazione
        tx.commit();
        // viene visualizzata la nuova tabella
        dumpPersonne();
    }

    // eliminare un oggetto appartenente al contesto di persistenza
    public static void test4() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si elimina l'oggetto allegato p2
        em.remove(p2);
        // fine transazione
        tx.commit();
        // visualizzazione della nuova tabella
        dumpPersonne();
    }

    // scollegare, ricollegare e modificare
    public static void test5() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si ricollega p1 al nuovo contesto
        p1 = em.find(Personne.class, p1.getId());
        // fine transazione
        tx.commit();
        // si modifica l'indirizzo di p1
        p1.getAdresse().setVille("Paris");
        // viene visualizzata la nuova tabella
        dumpPersonne();
    }

}

Ancora una volta, nulla di già visto. L'output della console è il seguente:

main : ----------- clean
[personnes]
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,0,Martin,Paul,31/01/2000,true,2,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test5
[personnes]
P[1,2,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Paris,null,France]]

Il lettore è invitato a collegare i risultati al codice.

Ora utilizziamo un'implementazione JPA / Toplink:

Il nuovo progetto Eclipse dei test è il seguente:

I codici Java sono identici a quelli del precedente progetto Hibernate. L'ambiente (librerie – persistence.xml – SGBD – cartelle conf, ddl – script ant) è quello già esaminato nel paragrafo 2.1.15.2. Sarà sempre così per i futuri progetti Toplink e, salvo eccezioni, non torneremo più su questo ambiente. In particolare, i file persistence.xml che configurano il livello JPA/Toplink per diversi SGBD sono quelli già esaminati e che si trovano nella cartella <conf>.

In caso di dubbi sulle procedure da seguire, il lettore è invitato a consultare quelle illustrate nello studio precedente.

Il progetto Eclipse è presente come [3] nella cartella degli esempi [4]. Lo importeremo.

L’esecuzione di [InitDB] con SGBD e MySQL5 fornisce i seguenti risultati:

  • [1]: l'output della console
  • [2]: le tabelle [jpa02_tl_personne] e [SEQENCE] nella prospettiva SQL Explorer
  • [3] e [4]: struttura e contenuto di [jpa02_tl_personne].

Gli script SQL generati in ddl/mysql5 [5] sono i seguenti:

create.sql


CREATE TABLE jpa02_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR3 VARCHAR(30), CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


DROP TABLE jpa02_tl_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.3. Esempio 3: relazione uno-a-uno tramite chiave esterna

2.3.1. o dello schema del database

1
2

    alter table jpa03_hb_personne 
        drop 
        foreign key FKFBBBFDD05FE379D0;

    drop table if exists jpa03_hb_adresse;

    drop table if exists jpa03_hb_personne;

    create table jpa03_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa03_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa03_hb_personne 
        add index FKFBBBFDD05FE379D0 (adresse_id), 
        add constraint FKFBBBFDD05FE379D0 
        foreign key (adresse_id) 
references jpa03_hb_adresse (id);
  • in [1]: il database. In questo caso, l'indirizzo della persona è inserito in una tabella a sé stante, denominata [adresse]. La tabella [personne] è collegata a questa tabella tramite una chiave esterna.
  • in [2]: la tabella DDL generata da Hibernate per MySQL5:
    • righe 9-20: la tabella [adresse] che verrà collegata alla classe [Adresse], diventata un oggetto @Entity.
    • riga 10: la chiave primaria della tabella [adresse]
    • riga 30: al posto di un indirizzo completo, nella tabella [personne] si trova ora l’identificativo [adresse_id] di tale indirizzo.
    • righe 34-38: persona (adresse_id) è una chiave esterna su indirizzo (id).

2.3.2. Gli oggetti @Entity che rappresentano il database

Una persona con indirizzo è ora rappresentata dalla seguente classe [Personne]:


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

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

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

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

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;
...
}
  • righe 32-34: l'indirizzo della persona
    • riga 32: l'annotazione @OneToOne indica una relazione uno-a-uno: una persona ha almeno e al massimo un indirizzo. L'attributo cascade = CascadeType.ALL significa che qualsiasi operazione (persist, merge, remove) sull'@Entity [Personne] deve essere propagata all'@Entity [Adresse]. Dal punto di vista del contesto di persistenza em, ciò significa quanto segue. Se p è una persona e ha il suo indirizzo:
      • un'operazione esplicita em.persist(p) comporterà un'operazione implicita em.persist(a)
      • un'operazione esplicita em.merge(p) comporterà un'operazione implicita em.merge(a)
      • un'operazione esplicita em.remove(p) comporterà un'operazione implicita em.remove(a)

L'esperienza dimostra che queste cascate implicite non sono la panacea. Lo sviluppatore finisce per dimenticare cosa fanno. Si potrebbero preferire operazioni esplicite nel codice. Esistono diversi tipi di cascata. L'annotazione @OneToOne avrebbe potuto essere scritta come segue:


//@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.REMOVE}, fetch=FetchType.LAZY)

L'attributo cascade accetta qui come valore un array di costanti che specificano i tipi di cascata desiderati.

L'attributo fetch=FetchType.LAZY richiede a Hibernate di caricare la dipendenza all'ultimo momento. Quando si inserisce un elenco di persone nel contesto di persistenza, non si desidera necessariamente includervi i loro indirizzi. Ad esempio, si potrebbe voler ottenere tale indirizzo solo per una persona specifica selezionata da un utente tramite un’interfaccia web. L’attributo fetch=FetchType.EAGER, invece, richiede il caricamento immediato delle dipendenze.

  • (continua)
    • riga 33: l’annotazione @JoinColumn definisce la chiave esterna che la tabella dell’@Entity [Personne] possiede sulla tabella dell’@Entity [Adresse]. L'attributo name definisce il nome della colonna che funge da chiave esterna. L'attributo unique=true impone una relazione uno-a-uno: non è possibile avere due volte lo stesso valore nella colonna [adresse_id]. L'attributo nullable=false impone che una persona abbia un indirizzo.

L'indirizzo di una persona è ora rappresentato dalla seguente @Entity [Adresse]:


package entites;

...
@Entity
@Table(name = "jpa03_hb_adresse")
public class Adresse implements Serializable {

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

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;

    @Column(length = 30)
    private String adr2;

    @Column(length = 30)
    private String adr3;

    @Column(length = 5, nullable = false)
    private String codePostal;

    @Column(length = 20, nullable = false)
    private String ville;

    @Column(length = 3)
    private String cedex;

    @Column(length = 20, nullable = false)
    private String pays;

    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
    private Personne personne;

    // costruttori
    public Adresse() {

    }
...
}
  • riga 4: la classe [Adresse] diventa un oggetto @Entity. Sarà quindi oggetto di una tabella nel database.
  • righe 9-12: come ogni oggetto @Entity, [Adresse] ha una chiave primaria. È stata denominata Id e presenta le stesse annotazioni (standard) della chiave primaria Id dell’@Entity [Personne].
  • righe 39-40: la relazione uno-a-uno con l’@Entity [Personne]. Ci sono diverse sottigliezze qui:
    • innanzitutto, il campo personne non è obbligatorio. Ci permette, partendo da un indirizzo, di risalire all’unica persona che possiede quell’indirizzo. Se non avessimo voluto questa comodità, il campo personne non esisterebbe e tutto funzionerebbe comunque.
    • La relazione uno-a-uno che collega le due entità [Personne] e [Adresse] è già stata configurata nell’@Entity [Personne]:

    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;

Affinché le due configurazioni uno-a-uno non entrino in conflitto tra loro, una viene considerata come principale e l’altra come inverse. È la relazione denominata principale che viene gestita dal ponte oggetto/relazionale. L'altra relazione, denominata inverse, non viene gestita direttamente, ma indirettamente tramite la relazione principale. In @Entity [Adresse]:


@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

è l’attributo mappedBy a definire la relazione uno-a-uno sopra indicata, la relazione inverse della relazione principale uno-a-uno definita dal campo adresse dell’entità @Entity [Personne].

2.3.3. Il progetto Eclipse / Hibernate 1

L’implementazione JPA qui utilizzata è quella di Hibernate. Il progetto Eclipse dei test è il seguente:

Il progetto si trova in [3] nella cartella degli esempi [4]. Lo importeremo.

2.3.4. Generazione del file DDL dal database

Seguendo le istruzioni del paragrafo 2.1.7, il file DDL ottenuto per il SGBD MySQL5 è quello mostrato all’inizio di questo paragrafo.

2.3.5. InitDB

Il codice di [InitDB] è il seguente:


package tests;
...
import entites.Adresse;
import entites.Personne;

public class InitDB {

    // costanti
    private final static String TABLE_PERSONNE = "jpa03_hb_personne";

    private final static String TABLE_ADRESSE = "jpa03_hb_adresse";

    public static void main(String[] args) throws ParseException {
        // Contesto di persistenza
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // si recupera un EntityManager  dal precedente EntityManagerFactory
        em = emf.createEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // richiesta
        Query sql1;
        // eliminare gli elementi dalla tabella PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // eliminare gli elementi dalla tabella ADRESSE
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // creazione persone
        Personne 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);
        // creazione indirizzi
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        Adresse a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // associazioni persona <--> indirizzo
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // persistenza delle persone e, di conseguenza, dei loro indirizzi
        em.persist(p1);
        em.persist(p2);
        // e degli indirizzi a3 e a4 non collegati a persone
        em.persist(a3);
        em.persist(a4);
        // visualizzazione delle persone
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // visualizzazione indirizzi
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }

        // fine transazione
        tx.commit();
        // fine EntityManager
        em.close();
        // fine EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}

Commentiamo solo ciò che presenta un interesse nuovo rispetto a quanto già studiato:

  • righe 31-32: si creano due persone
  • righe 34-37: si creano quattro indirizzi
  • righe 39-42: si associano le persone (p1, p2) agli indirizzi (a1, a2). Gli indirizzi (a3, a4) sono orfani. Nessuna persona li fa riferimento. Il codice DDL lo consente. Se una persona ha necessariamente un indirizzo, il contrario non è vero.
  • righe 44-45: si conservano le persone (p1, p2). Poiché abbiamo assegnato un attributo cascade = CascadeType.ALL alla relazione uno-a-uno che collega una persona al suo indirizzo, anche gli indirizzi (a1, a2) di queste due persone dovrebbero essere sottoposti a un persist. È proprio questo che vogliamo verificare. Per gli indirizzi orfani (a3, a4), siamo costretti a procedere in modo esplicito (righe 47-48).
  • righe 51-53: visualizzazione della tabella delle persone
  • righe 56-57: visualizzazione della tabella degli indirizzi

L’esecuzione di [InitDB] insieme a MySQL5 fornisce i seguenti risultati:

  • [1]: visualizzazione della console
  • [2]: le tabelle [jpa03_hb_*] nella prospettiva SQL Explorer
  • [3]: la tabella delle persone
  • [4]: la tabella degli indirizzi. Sono tutte presenti. Si noti inoltre il collegamento tra la colonna [adresse_id] in [3] e la colonna [id] in [4] (chiave esterna).

2.3.6. Home

La classe [Main] concatena sei test che esamineremo ora.

2.3.6.1. Test1

Questo test è il seguente:


// creazione di oggetti
    public static void test1() throws ParseException {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // creazione di persone
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // creazione indirizzi
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // associazioni persona <--> indirizzo
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistenza delle persone
        em.persist(p1);
        em.persist(p2);
        // e degli indirizzi a3 e a4 non collegati a persone
        em.persist(a3);
        em.persist(a4);
        // fine transazione
        tx.commit();
        // vengono visualizzate le tabelle
        dumpPersonne();
        dumpAdresse();
    }

Questo codice è tratto da [InitDB]. Il risultato è il seguente:

1
2
3
4
5
6
7
8
9
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]

Entrambe le tabelle sono state compilate.

2.3.6.2. Test2

Questo test è il seguente:


    // modifica di un oggetto del contesto
    public static void test2() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si incrementa il numero di figli di p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // si modifica il suo stato civile
        p1.setMarie(false);
        // l'oggetto p1 viene salvato automaticamente (dirty checking)
        // durante la successiva sincronizzazione (commit o select)
        // fine transazione
        tx.commit();
        // viene visualizzata la nuova tabella
        dumpPersonne();
}

Il risultato è il seguente:

1
2
3
4
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
  • riga 4: la persona p1 ha visto aumentare di 1 il numero dei propri figli e la propria versione passare da 0 a 1

2.3.6.3. Test4

Questo test è il seguente:


    // eliminare un oggetto appartenente al contesto di persistenza
    public static void test4() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si elimina l'oggetto associato p2
        em.remove(p2);
        // fine transazione
        tx.commit();
        // vengono visualizzate le nuove tabelle
        dumpPersonne();
        dumpAdresse();
}
  • riga 9: si elimina la persona p2. Quest'ultima ha una relazione a cascata con l'indirizzo a2. Pertanto, anche l'indirizzo a2 dovrebbe essere eliminato.

Il risultato del test 4 è il seguente:

main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • la persona p2 presente alla riga 3 del test 1 non è più presente nel test 4
  • lo stesso vale per il suo indirizzo a2, presente alla riga 7 del test 1 e assente dal test 4.

2.3.6.4. Test 5

Questo test è il seguente:


// scollegare, ricollegare e modificare
    public static void test5() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si ricollega p1 al nuovo contesto
        p1 = em.find(Personne.class, p1.getId());
        // si modifica l'indirizzo di p1
        p1.getAdresse().setVille("Paris");
        // fine transazione
        tx.commit();
        // vengono visualizzate le nuove tabelle
        dumpPersonne();
        dumpAdresse();
    }
  • riga 4: si ha un contesto di persistenza nuovo, quindi vuoto.
  • riga 9: si inserisce la persona p1 al suo interno. p1 viene cercato nel database perché non è presente nel contesto. Gli elementi dipendenti da p1 (il suo indirizzo), invece, non vengono recuperati dal database perché è stato scritto:

    @OneToOne(..., fetch=FetchType.LAZY)

È il concetto di “lazy loading” o “caricamento just-in-time”: le dipendenze di un oggetto persistente vengono caricate in memoria solo quando sono necessarie.

  • riga 11: si modifica il campo «città» dell’indirizzo di p1. A causa di getAdresse e se l’indirizzo di p1 non fosse già nel contesto di persistenza, verrà caricato tramite una lettura dal database.
  • riga 13: si conferma la transazione, il che comporterà la sincronizzazione del contesto di persistenza con il database. Quest’ultimo rileverà che l’indirizzo della persona p1 è stato modificato e lo salverà.

L'esecuzione di test5 fornisce i seguenti risultati:

main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • la città della persona p1 (riga 3 test4, riga 10 test5) è effettivamente passata da Angers (riga 5 test4) a Parigi (riga 12 test5).

2.3.6.5. Test6

Questo test è il seguente:


// eliminare un oggetto Indirizzo
    public static void test6() {
        EntityTransaction tx = null;
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        tx = em.getTransaction();
        tx.begin();
        // si ricollega l'indirizzo a3 al nuovo contesto
        a3 = em.find(Adresse.class, a3.getId());
        System.out.println(a3);
        // lo si elimina
        em.remove(a3);
        // fine transazione
        tx.commit();
        // dump della tabella Indirizzi
        dumpAdresse();
    }
  • riga 5: ci si trova in un nuovo contesto di persistenza, quindi vuoto.
  • riga 10: si inserisce l'indirizzo a3 nel contesto di persistenza
  • riga 13: lo si elimina. Si trattava di un indirizzo orfano (non collegato a una persona). L'eliminazione è quindi possibile.

Il risultato dell'esecuzione è il seguente:

main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • l’indirizzo a3 del test 5 (riga 6) è scomparso dagli indirizzi del test 6 (righe 11-12)

2.3.6.6. Test 7

Questo test è il seguente:


// rollback
    public static void test7() {
        EntityTransaction tx = null;
        try {
            // nuovo contesto di persistenza
            EntityManager em = getNewEntityManager();
            // inizio transazione
            tx = em.getTransaction();
            tx.begin();
            // si ricollega l'indirizzo a1 al nuovo contesto
            newa1 = em.find(Adresse.class, a1.getId());
            // si ricollega l'indirizzo a4 al nuovo contesto
            newa4 = em.find(Adresse.class, a4.getId());
            // si tenta di eliminarli - dovrebbe generare un'eccezione poiché non è possibile eliminare un indirizzo associato a una persona, come nel caso di newa1
            em.remove(newa4);
            em.remove(newa1);
            // fine transazione
            tx.commit();
        } catch (RuntimeException e1) {
            // si è verificato un problema
            System.out.format("Erreur dans transaction [%s%n%s%n%s%n%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause(), e1.getCause()
                    .getCause());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // si abbandona il contesto corrente
            em.clear();
        }
        // dump - la tabella Indirizzo non dovrebbe essere cambiata a causa del rollback
        dumpAdresse();
    }
  • test7: si verifica il rollback di una transazione
    • riga 6: ci si trova in un nuovo contesto di persistenza, quindi vuoto.
    • riga 11: si inserisce l'indirizzo a1 nel contesto di persistenza, sotto il riferimento newa1
    • riga 13: si inserisce l'indirizzo a4 nel contesto di persistenza, sotto il riferimento newa4
    • righe 15-16: si eliminano i due indirizzi newa1 e newa4. newa1 è l'indirizzo della persona p1 e quindi, nel database, p1 fa riferimento a newa1 tramite una chiave esterna. L'eliminazione di newa1 fallirà quindi e genererà un'eccezione durante la sincronizzazione del contesto di persistenza al momento del commit della transazione (riga 18). Quest'ultima subirà un rollback (riga 25) e quindi entrambe le operazioni della transazione verranno annullate. Si dovrebbe quindi constatare che l'indirizzo newa4, che avrebbe potuto essere legalmente eliminato, non lo è stato.

L'esecuzione fornisce il seguente risultato:


main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.ObjectDeletedException: deleted entity passed to persist: [entites.Adresse#<null>]
null]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • la tabella degli indirizzi del test 7 (righe 12-13) è identica a quella del test 6 (righe 4-5). Il rollback sembra essere avvenuto. Detto questo, il messaggio di errore della riga 9 è un enigma e merita di essere approfondito. Sembrerebbe che l’eccezione verificatasi non sia quella prevista. È necessario convertire i log di Hibernate in log4j.properties in modalità DEBUG per fare maggiore chiarezza:

# Opzione Root logger
log4j.rootLogger=ERROR, stdout

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

Si nota quindi che, quando l’indirizzo a1 è stato inserito nel contesto di persistenza, Hibernate vi ha inserito anche la persona p1, probabilmente a causa della relazione uno-a-uno dell’@Entity [Adresse]:


    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

Sebbene qui sia stato richiesto "LazyLoading", la dipendenza [Personne] viene comunque caricata immediatamente. Ciò significa probabilmente che l'attributo fetch=FetchType.LAZY non ha senso in questo contesto. Si nota poi che, al momento del commit della transazione, Hibernate ha preparato l’eliminazione degli indirizzi a1 e a4, ma anche il salvataggio della persona p1. Ed è qui che si verifica l’eccezione: poiché la persona p1 ha una cascata sul proprio indirizzo, Hibernate vuole salvare anche l’indirizzo a1, nonostante sia stato appena eliminato. È Hibernate a generare l’eccezione e non il driver JDBC. Da qui il messaggio della riga 9 sopra riportata. Inoltre, si può notare che il comando rollback della riga 25 non viene mai eseguito poiché la transazione è diventata inattiva. Il test della riga 24 impedisce quindi l’esecuzione di rollback.

Non è stato quindi raggiunto l’obiettivo desiderato: mostrare un rollback. Nessun comando SQL è stato infatti emesso sul database. Si ricorderanno alcuni punti:

  • l’importanza di attivare i log dettagliati per comprendere il funzionamento di ORM
  • se un ORM può semplificare la vita dello sviluppatore, può anche complicargliela nascondendo comportamenti che lo sviluppatore avrebbe bisogno di conoscere. In questo caso, la modalità di caricamento delle dipendenze di un @Entity.

2.3.7. Progetto Eclipse / Hibernate 2

Copiamo e incolliamo il progetto Eclipse / Hibernate per modificare leggermente la configurazione degli oggetti @Entity:

Il progetto si trova in [3] nella cartella degli esempi [4]. Lo importeremo.

Modifichiamo solo l’@Entity [Adresse] in modo che non abbia più una relazione inversa uno-a-uno con l’@Entity [Personne]:


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

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

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;

    ...

    @Column(length = 20, nullable = false)
    private String pays;

//     @OneToOne(mappedBy = "indirizzo", fetch=FetchType.LAZY)
//     private Persona persona;

    // costruttori
    public Adresse() {

    }
  • righe 25-26: la relazione inversa @OneToOne viene rimossa. È importante comprendere che una relazione inversa non è mai indispensabile. Solo la relazione principale lo è. La relazione inversa può essere utilizzata per comodità. In questo caso, consentiva di ottenere in modo semplice il proprietario di un indirizzo. Una relazione inversa può sempre essere sostituita da una query JPQL. È ciò che mostreremo nell’esempio seguente.

I programmi di test sono riportati identici. Quello che ci interessa è esclusivamente il test 7, quello in cui abbiamo visto la relazione inversa uno-a-uno in azione. Aggiungiamo inoltre un test 8 per mostrare come, senza la relazione inversa Indirizzo -> Persona, sia comunque possibile recuperare la persona con un determinato indirizzo.

Il test 7 non cambia. La sua esecuzione fornisce ora i seguenti risultati (log disattivati):


main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.exception.ConstraintViolationException: could not delete: [entites.Adresse#1]
java.sql.SQLException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa04_hb_personne`, CONSTRAINT `FKEA3F04515FE379D0` FOREIGN KEY (`adresse_id`) REFERENCES `jpa04_hb_adresse` (`id`))]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • questa volta si verifica effettivamente l’eccezione prevista: quella generata dal driver JDBC perché si è tentato di eliminare dalla tabella [adresse] una riga referenziata da una chiave esterna di una riga della tabella [personne]. La riga [10] indica chiaramente la causa dell’errore.
  • Il rollback è stato effettivamente eseguito: al termine del test 7, la tabella [adresse] (righe 12-13) è quella che avevamo al termine del test 6 (righe 4-5).

Qual è la differenza rispetto al test 7 del precedente progetto Eclipse? Perché in questo caso si verifica un'eccezione JDBC che non si era verificata nel test precedente? Poiché l'@Entity [Adresse] non ha più una relazione inversa uno-a-uno con l'@Entity [Personne], viene gestita in modo isolato da Hibernate. Quando l’indirizzo newa1 è stato inserito nel contesto di persistenza, Hibernate non ha inserito in tale contesto anche la persona p1 a cui appartiene quell’indirizzo. L'eliminazione degli indirizzi newa1 e newa4 è quindi avvenuta senza che l'entità Personne fosse presente nel contesto.

Ora, come si può ricavare, a partire dall'indirizzo newa1, la persona p1 a cui corrisponde tale indirizzo? È una domanda legittima. Il test 8 che segue fornisce la risposta:


// relazione inversa uno-a-uno
    // realizzata tramite una query JPQL
    public static void test8() {
        EntityTransaction tx = null;
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        tx = em.getTransaction();
        tx.begin();
        // si ricollega l'indirizzo a1 al nuovo contesto
        newa1 = em.find(Adresse.class, a1.getId());
        // si recupera il proprietario di questo indirizzo
        Personne p1 = (Personne) em.createQuery("select p from Personne p join p.adresse a where a.id=:adresseId").setParameter("adresseId", newa1.getId())
                .getSingleResult();
        // li si visualizza
        System.out.println("adresse=" + newa1);
        System.out.println("personne=" + p1);
        // fine transazione
        tx.commit();
    }
  • riga 6: nuovo contesto di persistenza vuoto
  • righe 8-9: inizio transazione
  • riga 11: l'indirizzo a1 viene inserito nel contesto di persistenza e referenziato da newa1.
  • riga 13: si recupera la persona p1 con l’indirizzo newa1 tramite una query JPQL. Si sa che [Personne] e [Adresse] sono collegati da una relazione di chiave esterna. Nella classe [Personne], è il campo [adresse], che presenta l’annotazione @OneToOne, a concretizzare tale relazione. La sintassi JPQL "select p from Personne p join p.adresse a" esegue un join tra le tabelle [personne] e [adresse]. L'equivalente SQL generato in una console Hibernate (cfr. esempi del paragrafo 2.1.12) è il seguente:
SQL #0 tipi: entites.Personne
-----------------
select
  personne0_.id as id1_,
  personne0_.version as version1_,
  personne0_.nom as nom1_,
  personne0_.prenom as prenom1_,
  personne0_.datenaissance as datenais5_1_,
  personne0_.marie as marie1_,
  personne0_.nbenfants as nbenfants1_,
  personne0_.adresse_id as adresse8_1_ 
 from
  jpa04_hb_personne personne0_ 
 inner join
  jpa04_hb_adresse adresse1_ 
on personne0_.adresse_id=adresse1_.id

Si nota chiaramente il join tra le due tabelle. Ogni persona è ora collegata al proprio indirizzo. Resta da precisare che ci interessa solo l’indirizzo newa1. La query diventa "select p from Personne p join p.adresse a where a.id=:adresseId". Si noti l’uso degli alias p e a. Le query JPQL fanno un uso intensivo degli alias. Pertanto, l'espressione "from Personne p join p.adresse a" fa sì che una persona sia rappresentata dall'alias p e il suo indirizzo (p.adresse) dall'alias a. L'operazione di restrizione «where a.id=:adresseId» limita le righe richieste alle sole persone p che hanno il valore:adresseId come identificativo del loro indirizzo a. :adresseId è denominato parametro, mentre l'ordine JPQL è un ordine JPQL parametrizzato. Al momento dell'esecuzione, a questo parametro deve essere assegnato un valore. Questo è il metodo

Query setParameter(String nomParamètre, Object valeurParamètre)

che consente di assegnare un valore a un parametro identificato dal suo nome. Si noti che setParameter restituisce un oggetto Query, così come il metodo createQuery. Pertanto è possibile concatenare le chiamate ai metodi [em.createQuery(...).setParameter(...).getSingleResult(...)], poiché i metodi [setParameter, getSingleResult] sono metodi dell’interfaccia Query. Il metodo [getSingleResult] viene utilizzato per le query Select che restituiscono un solo risultato. È il caso in questione.

  • righe 16-17: vengono visualizzati l'indirizzo newa1 e la persona p1 a cui corrisponde tale indirizzo, a scopo di verifica.

Il risultato ottenuto è il seguente:

1
2
3
main : ----------- test8
adresse=A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
personne=P[1,1,Martin,Paul,31/01/2000,false,3,1]

È corretto. Da questo esempio si evince che la relazione inversa uno-a-uno dall’@entity [Adresse] verso l’@entity [Personne] non era indispensabile. L’esperimento ha dimostrato in questo caso che la sua rimozione ha portato a un comportamento più prevedibile del codice. Questo accade spesso.

2.3.8. Console Hibernate

Il test 8 precedente ha utilizzato un comando JPQL per eseguire un join tra le entità Personne e Adresse. Sebbene analoghi al linguaggio SQL, i linguaggi JPQL, JPA o HQL di Hibernate richiedono un periodo di apprendimento e la console di Hibernate è eccellente a questo scopo. L’abbiamo già utilizzata nel paragrafo 2.1.12 per interrogare una singola tabella. Riproviamo ora a utilizzarla per interrogare due tabelle collegate da una relazione di chiave esterna.

Creiamo una console Hibernate per il nostro attuale progetto Eclipse:

  • [1]: passiamo alla prospettiva [Hibernate Console] (Window / Open Perspective / Other)
  • [2]: creiamo una nuova configurazione
  • utilizzando il pulsante [4], selezioniamo il progetto Java per il quale viene creata la configurazione Hibernate. Il suo nome viene visualizzato in [3].
  • In [5], assegniamo il nome desiderato a questa configurazione. In questo caso, abbiamo ripreso il nome del progetto Java.
  • nel file [6], specifichiamo che utilizziamo una configurazione JPA affinché lo strumento sappia che deve elaborare il file [META-INF/persistence.xml]
  • in [7]: in questo file [META-INF/persistence.xml] specifichiamo che occorre utilizzare l'unità di persistenza denominata jpa.
  • Nel file [8] si convalida la configurazione.

Successivamente, è necessario avviare il file SGBD. In questo caso, si tratta del file MySQL5.

  • in [1]: la configurazione creata presenta una struttura ad albero a tre rami
  • in [2]: il ramo [Configuration] elenca gli oggetti che la console ha utilizzato per configurarsi: in questo caso le @Entity Personne e Adresse.
  • in [3]: la Session Factory è un concetto di Hibernate simile a EntityManager di JPA. Realizza il ponte oggetto/relazionale grazie agli oggetti del ramo [Configuration]. In [3] sono presentati gli oggetti del contesto di persistenza, in questo caso nuovamente le @Entity Personne e Adresse.
  • In [4]: il database a cui si accede tramite la configurazione presente in [persistence.xml]. Qui si trovano le tabelle [jpa04_hb_*] generate dal nostro attuale progetto Eclipse.
  • in [1], si crea un editor HQL
  • nell’editor HQL,
    • in [2], si sceglie la configurazione Hibernate da utilizzare se ce ne sono diverse (come in questo caso)
    • in [3], si digita il comando JPQL che si desidera eseguire, in questo caso il comando JPQL del test 8
    • in [4], lo si esegue
    • in [5], si ottengono i risultati della query nella finestra [Hibernate Query Result].
    • in [6], la finestra [Hibernate Dynamic SQL preview] consente di visualizzare la query SQL che è stata eseguita.

Un altro modo per ottenere lo stesso risultato:

  • in [1]: il comando JPQL esegue il join delle entità Personne e Adresse. [ref1] definisce questa forma «giunzione theta».
  • in [2]: l'equivalente SQL
  • in [3]: il risultato

Una terza forma accettata solo da Hibernate (HQL):

  • in [1]: il comando HQL. JPQL non accetta la notazione p.adresse.id. Accetta solo un livello di indirezione.
  • in [2]: l'equivalente SQL. Si nota che evita il join tra le tabelle.
  • in [3]: il risultato

Ecco altri esempi:

  • in [1]: l'elenco delle persone con i rispettivi indirizzi
  • in [2]: l'equivalente SQL.
  • in [3]: il risultato
  • in [1]: l'elenco degli indirizzi con il relativo proprietario, se presente, oppure nessuno in caso contrario (giunzione esterna destra: l’entità Adresse, che fornirà le righe non correlate a Personne, si trova a destra della parola chiave join).
  • in [2]: l'equivalente di SQL.
  • in [3]: il risultato

Si noti che solo l’entità Personne ha una relazione con l’entità Adresse. Il contrario non è più vero da quando è stata eliminata la relazione inversa uno-a-uno denominata personne nell’entità Adresse. Se questa relazione inversa fosse esistita, si sarebbe potuto scrivere:

  • in [1]: l'elenco degli indirizzi con il relativo proprietario, se presente, oppure nessuno in caso contrario (join esterno sinistro: l’entità Adresse, che fornirà le righe senza relazione con Personne, si trova a sinistra della parola chiave join).
  • in [2]: l'equivalente di SQL.
  • in [3]: il risultato

Invitiamo vivamente il lettore a esercitarsi con il linguaggio JPQL utilizzando la console Hibernate.

Ora utilizziamo un'implementazione JPA / Toplink:

Il nuovo progetto Eclipse per i test è il seguente:

I codici Java sono identici a quelli del precedente progetto Hibernate. L'ambiente (librerie – persistence.xml – SGBD – cartelle conf, ddl – script ant) è quello descritto nel paragrafo 2.1.15.2. Il progetto Eclipse è presente come [3] nella cartella degli esempi [4]. Lo importeremo.

Il file <persistence.xml> viene modificato in un punto, ovvero nelle entità dichiarate:


    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  provider -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- classi persistenti -->
        <class>entites.Personne</class>
        <class>entites.Adresse</class>
        <!-- proprietà dell'unità di persistenza -->
...
  • righe 5 e 6: le due entità gestite

L'esecuzione di [InitDB] insieme a SGBD e MySQL5 fornisce i seguenti risultati:

In [1], l'output della console; in [2], le due tabelle [jpa04_tl] generate; in [3], gli script SQL generati. Il loro contenuto è il seguente:

create.sql


CREATE TABLE jpa04_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa04_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa04_tl_personne ADD CONSTRAINT FK_jpa04_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa04_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa04_tl_personne DROP FOREIGN KEY FK_jpa04_tl_personne_adresse_id
DROP TABLE jpa04_tl_personne
DROP TABLE jpa04_tl_adresse
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.4. Esempio 4: relazione uno-a-molti

2.4.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).

2.4.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
    // 1 Articolo ha necessariamente 1 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 si trova 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 associata al 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 applicate a cascata 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 articoli a e b sono gli stessi se hanno lo stesso nome. In questo caso, lo sviluppatore deve ridefinire due metodi nella classe [Article]:
      • equals: che 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].

2.4.3. Il progetto Eclipse / Hibernate 1

L’implementazione JPA qui utilizzata è quella di Hibernate. Il progetto Eclipse dei test è il seguente:

Il progetto si trova in [3] nella cartella degli esempi [4]. Lo importeremo.

2.4.4. Generazione del file DDL dal database

Seguendo le istruzioni del paragrafo 2.1.7, il file DDL ottenuto per il SGBD MySQL5 è quello mostrato all’inizio di questo esempio, al paragrafo 2.4.1.

2.4.5. InitDB

Il codice di [InitDB] è il seguente:


package tests;

...
public class InitDB {

    // costanti
    private final static String TABLE_ARTICLE = "jpa05_hb_article";

    private final static String TABLE_CATEGORIE = "jpa05_hb_categorie";

    public static void main(String[] args) {
        // Contesto di persistenza
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // si recupera un EntityManager  dal precedente EntityManagerFactory
        em = emf.createEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // richiesta
        Query sql1;
        // eliminare gli elementi dalla tabella ARTICLE
        sql1 = em.createNativeQuery("delete from " + TABLE_ARTICLE);
        sql1.executeUpdate();
        // eliminare gli elementi dalla tabella CATEGORIE
        sql1 = em.createNativeQuery("delete from " + TABLE_CATEGORIE);
        sql1.executeUpdate();
        // crea tre categorie
        Categorie categorieA = new Categorie();
        categorieA.setNom("A");
        Categorie categorieB = new Categorie();
        categorieB.setNom("B");
        Categorie categorieC = new Categorie();
        categorieC.setNom("C");
        // creare 3 articoli
        Article articleA1 = new Article();
        articleA1.setNom("A1");
        Article articleA2 = new Article();
        articleA2.setNom("A2");
        Article articleB1 = new Article();
        articleB1.setNom("B1");
        // collegarli alla rispettiva categoria
        categorieA.addArticle(articleA1);
        categorieA.addArticle(articleA2);
        categorieB.addArticle(articleB1);
        // salvare le categorie e, di conseguenza (inserimento), gli articoli
        em.persist(categorieA);
        em.persist(categorieB);
        em.persist(categorieC);
        // visualizzazione delle categorie
        System.out.println("[categories]");
        for (Object p : em.createQuery("select c from Categorie c order by c.nom asc").getResultList()) {
            System.out.println(p);
        }
        // visualizzazione articoli
        System.out.println("[articles]");
        for (Object p : em.createQuery("select a from Article a order by a.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fine transazione
        tx.commit();
        // fine EntityManager
        em.close();
        // fine EntityMangerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}
  • righe 22-27: le tabelle [article] e [categorie] vengono svuotate. Si noti che è necessario iniziare da quella che contiene la chiave esterna. Se si iniziasse dalla tabella [categorie], verrebbero eliminate le categorie a cui fanno riferimento le righe della tabella [article] e ciò verrebbe rifiutato dalla tabella SGBD.
  • righe 29-34: si creano tre categorie A, B, C
  • righe 36-41: si creano tre articoli A1, A2, B1 (la lettera indica la categoria)
  • righe 43-45: i 3 articoli vengono inseriti nelle rispettive categorie
  • righe 47-49: le 3 categorie vengono inserite nel contesto di persistenza. A causa della cascata Categoria -> Articolo, anche i relativi articoli verranno inseriti in tale contesto. Pertanto, tutti gli oggetti creati si trovano ora nel contesto di persistenza.
  • righe 50-59: viene richiesto il contesto di persistenza per ottenere l'elenco delle categorie e degli articoli. È noto che ciò provocherà una sincronizzazione del contesto con il database. È in questo momento che le categorie e gli articoli verranno salvati nelle rispettive tabelle.

L'esecuzione di [InitDB] insieme a MySQL5 fornisce i seguenti risultati:

  • [1]: l’output della console
  • [2]: le tabelle [jpa05_hb_*] nella prospettiva SQL Explorer
  • [3]: la tabella delle categorie
  • [4]: la tabella degli articoli. Da notare il collegamento tra [categorie_id] in [4] e [id] in [3] (chiave esterna).

2.4.6. Pagina principale

La classe [Main] concatena una serie di test che esamineremo, ad eccezione dei test 1 e 2 che riprendono il codice di [InitDB] per inizializzare il database.

2.4.6.1. Test3

Questo test è il seguente:


    // cerca un elemento specifico
    public static void test3() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // caricamento categoria
        Categorie categorie = em.find(Categorie.class, categorieA.getId());
        // visualizzazione della categoria e dei relativi articoli
        System.out.format("Articles de la catégorie %s :%n", categorie);
        for (Article a : categorie.getArticles()) {
            System.out.println(a);
        }
        // fine transazione
        tx.commit();
}
  • riga 4: si ha un contesto di persistenza nuovo, quindi vuoto
  • righe 6-7: inizio transazione
  • riga 9: la categoria A viene recuperata dal database nel contesto di persistenza
  • riga 11: si visualizza la categoria A
  • righe 12-14: si visualizzano gli articoli della categoria A. Qui si evidenzia l’utilità della relazione inversa OneToMany articoli dell’@Entity Categorie. La sua presenza ci evita di eseguire una query JPQL per richiedere gli articoli della categoria A. Per ottenerli, si utilizza il metodo get del campo articles.

I risultati sono i seguenti:

main : ----------- test1
[categories]
Categorie[1,0,A]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[1,0,A1,1]
Article[2,0,A2,1]
Article[3,0,B1,2]
main : ----------- test2
3 categorie(s) trouvée(s) :
A
B
C
3 article(s) trouvé(s) :
A1
A2
B1
main : ----------- test3
Articles de la catégorie Categorie[1,0,A] :
Article[2,0,A2,1]
Article[1,0,A1,1]
  • riga 20: la categoria A
  • righe 21-22: i due articoli della categoria A

2.4.6.2. Test4

Questo test è il seguente:


    // eliminazione di un articolo
    @SuppressWarnings("unchecked")
    public static void test4() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // caricamento articolo A1
        Article newarticle1 = em.find(Article.class, articleA1.getId());
        // eliminazione articolo A1 (attualmente non è caricata alcuna categoria)
        em.remove(newarticle1);
        // toplink: l'articolo deve essere rimosso dalla sua categoria, altrimenti il test6 va in crash
        // hibernate: non è necessario
        newarticle1.getCategorie().getArticles().remove(newarticle1);
        // fine transazione
        tx.commit();
        // dump degli articoli
        dumpArticles();
}
  • il test 4 elimina l'articolo A1
  • riga 5: si parte da un contesto nuovo e vuoto
  • riga 10: l'articolo A1 viene inserito nel contesto di persistenza. Qui sarà referenziato come newarticle1.
  • riga 12: viene eliminato dal contesto
  • riga 15: le categorie A, B e C e gli articoli A1, A2 e B1, sebbene non siano più persistenti, sono comunque ancora in memoria. Sono semplicemente scollegati dal contesto di persistenza. L'articolo A1, che fa parte degli articoli della categoria A, viene rimosso da quest'ultima. Ciò consentirà in seguito di ricollegare la categoria A al contesto di persistenza. Se non lo si fa, la categoria A verrà ricollegata a un insieme di articoli di cui uno è stato rimosso. Ciò non sembra creare problemi a Hibernate, ma causa un crash di Toplink.
  • Riga 19: si visualizzano tutti gli articoli per verificare che A1 sia scomparso.

I risultati sono i seguenti:

1
2
3
4
main : ----------- test4
[articles]
Article[2,0,A2,1]
Article[3,0,B1,2]

L’articolo A1 è effettivamente scomparso.

2.4.6.3. Test5

Questo test è il seguente:


// modifica di 1 articolo
    public static void test5() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // modifica di articleA2
        articleA2.setNom(articleA2.getNom() + "-");
        // articleA2 viene reinserito nel contesto di persistenza
        em.merge(articleA2);
        // fine transazione
        tx.commit();
        // dump degli articoli
        dumpArticles();
    }
  • il test 5 modifica il nome dell'articolo A2
  • riga 4: si parte da un contesto nuovo e vuoto
  • riga 9: si modifica il nome dell'articolo distaccato A2, che diventerà "A2-".
  • riga 11: l’articolo distaccato A2 viene ricollegato al contesto di persistenza. Si noti che A2 rimane sempre un oggetto distaccato. È l’oggetto em.merge (articleA2) che ora fa parte del contesto di persistenza. Questo oggetto non è stato memorizzato in una variabile, come di consueto. È quindi inaccessibile.
  • riga 13: sincronizzazione del contesto di persistenza con il database. L’articolo A2 verrà modificato nel database e il suo numero di versione passerà da N a N+1. La versione in memoria distaccata articleA2 non è più valida. Lo stesso vale per l’oggetto distaccato che rappresenta la categoria A, poiché questo contiene articleA2 tra i propri articoli.
  • riga 15: vengono visualizzati tutti gli articoli per verificare la modifica del nome dell’articolo A2

I risultati sono i seguenti:

1
2
3
4
main : ----------- test5
[articles]
Article[2,1,A2-,1]
Article[3,0,B1,2]

L’articolo A2 ha effettivamente cambiato nome.

2.4.6.4. Test6

Questo test è il seguente:


// modifica di 1 categoria e dei relativi articoli
    public static void test6() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // caricamento categoria
        categorieA = em.find(Categorie.class, categorieA.getId());
        // elenco degli articoli della categoria A
        for (Article a : categorieA.getArticles()) {
            a.setNom(a.getNom() + "-");
        }
        // modifica del nome della categoria
        categorieA.setNom(categorieA.getNom() + "-");
        // fine transazione
        tx.commit();
        // dump delle categorie e degli articoli
        dumpCategories();
        dumpArticles();
}
  • il test 6 modifica il nome della categoria A e di tutti i suoi articoli
  • riga 4: si parte da un contesto nuovo e vuoto
  • riga 9: si recupera la categoria A dal database. Non si esegue un merge sull'oggetto distaccato categorieA poiché si sa che esso contiene un riferimento all'articolo A2, ormai obsoleto. Si ricomincia quindi da zero.
  • righe 11-12: si modifica il nome di tutti gli articoli della categoria A. Ancora una volta si utilizza la relazione inversa OneToMany tramite il metodo getArticles.
  • riga 15: viene modificato anche il nome della categoria
  • riga 17: fine della transazione. Viene eseguita una sincronizzazione del contesto con il database. Tutti gli oggetti del contesto che sono stati modificati verranno aggiornati nel database.
  • righe 21-22: vengono visualizzati gli articoli e le categorie per la verifica

I risultati sono i seguenti:

1
2
3
4
5
6
7
8
main : ----------- test6
[categories]
Categorie[1,2,A-]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[2,2,A2--,1]
Article[3,0,B1,2]

L'articolo A2 ha cambiato nuovamente nome, così come la categoria A.

2.4.6.5. Test7

Questo test è il seguente:


// eliminazione di una categoria
    public static void test7() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistenza catégorieB e eliminazione a cascata (merge) degli articoli associati
        Categorie mergedcategorieB = em.merge(categorieB);
        // eliminazione della categoria e, a cascata (delete), degli articoli associati
        em.remove(mergedcategorieB);
        // fine transazione
        tx.commit();
        // esportazione delle categorie e degli articoli
        dumpCategories();
        dumpArticles();
    }
  • il test 7 elimina la categoria B e, di conseguenza, i relativi articoli
  • riga 4: si parte da un contesto nuovo e vuoto
  • riga 9: la categoria B esiste in memoria come oggetto separato dal contesto di persistenza. La si reintegra (merge) nel contesto di persistenza. Di conseguenza, i suoi articoli (l’articolo B1) subiranno un’operazione merge e quindi verranno reintegrati nel contesto di persistenza.
  • riga 11: ora che la categoria B si trova nel contesto, è possibile eliminarla (remove). Per effetto a cascata, anche i suoi articoli subiranno un'operazione remove. Questa operazione è possibile proprio perché l'operazione merge della riga 9 li ha reintegrati nel contesto di persistenza.
  • riga 13: fine della transazione. Il contesto verrà sincronizzato. Gli oggetti del contesto che hanno subito un’operazione remove verranno eliminati dal database.
  • righe 15-16: vengono visualizzati gli articoli e le categorie per la verifica

I risultati sono i seguenti:

1
2
3
4
5
6
main : ----------- test7
[categories]
Categorie[1,2,A-]
Categorie[3,0,C]
[articles]
Article[1,2,A2--,1]

La categoria B e l’articolo B1 sono stati effettivamente rimossi.

2.4.6.6. Test8

Questo test è il seguente:


// richieste
    @SuppressWarnings("unchecked")
    public static void test8() {
        // nuovo contesto di persistenza
        EntityManager em = getNewEntityManager();
        // transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // elenco degli articoli della categoria A
        List articles = em
                .createQuery(
                        "select a from Categorie c join c.articles a where c.nom like 'A%' order by a.nom asc")
                .getResultList();
        // visualizzazioni articoli
        System.out.println("Articles de la catégorie A");
        for (Object a : articles) {
            System.out.println(a);
        }
        // fine transazione
        tx.commit();
    }
  • il test 7 mostra come recuperare gli articoli di una categoria senza passare attraverso la relazione inversa. Ciò dimostra che quest'ultima non è quindi indispensabile.
  • riga 4: si parte da un contesto nuovo e vuoto
  • riga 10: una query JPQL che richiede tutti gli articoli di una categoria il cui nome inizi con A
  • righe 15-17: visualizzazione del risultato della query.

I risultati sono i seguenti:

1
2
3
main : ----------- test8
Articles de la catégorie A
Article[2,2,A2--,1]

2.4.7. Progetto Eclipse / Hibernate 2

Copiamo e incolliamo il progetto Eclipse / Hibernate per chiarire un punto relativo al concetto di relazione principale / relazione inversa che abbiamo creato attorno all'annotazione @ManyToOne (principale) dell'@Entity [Article] e la relazione inversa @OneToMany (inversa) dell'@Entity [Categorie]. Vogliamo dimostrare che, se quest’ultima relazione non viene dichiarata inversa rispetto all’altra, lo schema generato per il database sarà completamente diverso da quello generato in precedenza.

In [1] il nuovo progetto Eclipse. In [2] il codice Java, in [3] lo script ant che genererà lo schema SQL del database. Il progetto si trova in [4] nella cartella degli esempi [5]. Lo importeremo.

Modifichiamo solo l’@Entity [Categorie] in modo che la sua relazione @OneToMany con l’@Entity [Article] non sia più dichiarata inversa rispetto alla relazione @ManyToOne che l'@Entity [Article] ha con l'@Entity [Categorie]:


...
@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 OneToMany non inversa (assenza di mappedby) Categoria (uno) -> Articolo (molti)
    // implementata tramite una tabella di join Categorie_Article affinché, partendo da una categoria
    // si possano raggiungere gli articoli di quella categoria
    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

    // produttori
...
  • righe 18-22: si vuole comunque mantenere la possibilità di trovare gli articoli di una determinata categoria grazie alla relazione @OneToMany della riga 21. Ma vogliamo verificare l’influenza dell’attributo mappedBy, che rende una relazione l’inverso di una relazione principale definita altrove, in un’altra @Entity. In questo caso, il mappedBy è stato rimosso.

Eseguiamo il task ant-DLL (cfr. paragrafo 2.1.7) con SGBD e MySQL5. Lo schema ottenuto è il seguente:

Si notino i seguenti punti:

  • è stata creata una nuova tabella [categorie_article] [1]. In precedenza non esisteva.
  • Si tratta di una tabella di join tra le tabelle [categorie] [2] e [article] [3]. Se gli oggetti Articolo a1 e a2 appartengono alla categoria c1, nella tabella di join si troveranno le righe:
[c1,a1]
[c1,a2]

dove c1, a1, a2 sono le chiavi primarie degli oggetti corrispondenti.

  • La tabella di join [categorie_article] [1] è stata creata da Hibernate affinché, a partire da un oggetto Categorie c, sia possibile recuperare gli oggetti Articolo a appartenenti a c. È stata la relazione @OneToMany a determinare la creazione di questa tabella. Poiché non è stata dichiarata come inversa della relazione principale @ManyToOne dell’@Entity Article, Hibernate non sapeva di poter utilizzare questa relazione principale per recuperare gli articoli di una categoria c. Ha quindi trovato un’altra soluzione.
  • Con questo esempio si comprendono meglio i concetti di relazioni principale e inverse. Una (quella inversa) utilizza le proprietà dell’altra (quella principale).

Lo schema SQL di questo database per MySQL5 è il seguente:


    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D26D17756;

    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D424C61C9;

    alter table jpa06_hb_article 
        drop 
        foreign key FK4547168FECCE8750;

    drop table if exists jpa05_hb_categorie;

    drop table if exists jpa05_hb_categorie_jpa06_hb_article;

    drop table if exists jpa06_hb_article;

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

    create table jpa05_hb_categorie_jpa06_hb_article (
        jpa05_hb_categorie_id bigint not null,
        articles_id bigint not null,
        primary key (jpa05_hb_categorie_id, articles_id),
        unique (articles_id)
    ) ENGINE=InnoDB;

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

    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D26D17756 (jpa05_hb_categorie_id), 
        add constraint FK79D4BA1D26D17756 
        foreign key (jpa05_hb_categorie_id) 
        references jpa05_hb_categorie (id);

    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D424C61C9 (articles_id), 
        add constraint FK79D4BA1D424C61C9 
        foreign key (articles_id) 
        references jpa06_hb_article (id);

    alter table jpa06_hb_article 
        add index FK4547168FECCE8750 (categorie_id), 
        add constraint FK4547168FECCE8750 
        foreign key (categorie_id) 
references jpa05_hb_categorie (id);
  • righe 19-24: creazione della tabella [categorie] e righe 33-39: creazione della tabella [article]. Si noti che sono identiche a quelle dell’esempio precedente.
  • righe 26-31: creazione della tabella di join [categorie_article] dovuta alla presenza della relazione non inversa @OneToMany dell’@Entity Categorie. Le righe di questa tabella sono di tipo [c,a], dove c è la chiave primaria di una categoria c e a è la chiave primaria diun articolo a appartenente alla categoria c. La chiave primaria di questa tabella di join è costituita dalle due chiavi primarie [c,a] concatenate (riga 29).
  • righe 41-45: il vincolo di chiave esterna dalla tabella [categorie_article] alla tabella [categorie]
  • righe 47-51: il vincolo di chiave esterna dalla tabella [categorie_article] alla tabella [article]
  • righe 53-57: il vincolo di chiave esterna dalla tabella [article] alla tabella [categorie]

Si invita il lettore a eseguire i test [InitDB] e [Main]. Essi forniscono gli stessi risultati di prima. Lo schema del database è tuttavia ridondante e le prestazioni risulteranno inferiori rispetto alla versione precedente. Sarebbe probabilmente opportuno approfondire la questione delle relazioni inverse/principali per verificare se la nuova configurazione non comporti ulteriori conflitti dovuti al fatto che si hanno due relazioni indipendenti per rappresentare la stessa cosa: la relazione molti-a-uno che la tabella [article] ha con la tabella [categorie].

Ora utilizziamo un'implementazione JPA / Toplink:

Il progetto Eclipse con Toplink è una copia del progetto Eclipse con Hibernate, versione 1:

I codici Java sono identici a quelli del precedente progetto Hibernate - versione 1. L'ambiente (librerie – persistence.xml – SGBD - cartelle conf, ddl – script ant) è quello descritto nel paragrafo 2.1.15.2. Il progetto Eclipse è presente come [3] nella cartella degli esempi [4]. Lo importeremo.

Il file <persistence.xml> [2] viene modificato in un punto, ovvero nelle entità dichiarate:


        ...
        <!-- classi persistenti -->
        <class>entites.Categorie</class>
        <class>entites.Article</class>
...
  • righe 3 e 4: le due entità gestite

L'esecuzione di [InitDB] con SGBD e MySQL5 fornisce i seguenti risultati:

In [1], l'output della console; in [2], le due tabelle [jpa05_tl] generate; in [3], gli script SQL generati. Il loro contenuto è il seguente:

create.sql


CREATE TABLE jpa05_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa05_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
ALTER TABLE jpa05_tl_article ADD CONSTRAINT FK_jpa05_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa05_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa05_tl_article DROP FOREIGN KEY FK_jpa05_tl_article_categorie_id
DROP TABLE jpa05_tl_article
DROP TABLE jpa05_tl_categorie
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

L'esecuzione di [Main] avviene senza errori.

Questo progetto Eclipse è stato creato dal precedente tramite copia. Poiché è stato realizzato con Hibernate, si rimuove l'attributo mappedBy dalla relazione @OneToMany dell'@Entity Categorie.


@Entity
@Table(name = "jpa06_tl_categorie")
public class Categorie implements Serializable {

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

    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relazione OneToMany non inversa (assenza di mappedby) Categoria (one) ->
    // Articolo (many)
    // implementata tramite una tabella di join Categorie_Article affinché, a partire
    // di una categoria
    // si possano raggiungere più articoli
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

Lo schema SQL generato per MySQL5 è quindi il seguente:

create.sql


CREATE TABLE jpa06_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
CREATE TABLE jpa06_tl_categorie_jpa06_tl_article (Categorie_ID BIGINT NOT NULL, articles_ID BIGINT NOT NULL, PRIMARY KEY (Categorie_ID, articles_ID))
CREATE TABLE jpa06_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID FOREIGN KEY (articles_ID) REFERENCES jpa06_tl_article (ID)
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT jpa06_tl_categorie_jpa06_tl_article_Categorie_ID FOREIGN KEY (Categorie_ID) REFERENCES jpa06_tl_categorie (ID)
ALTER TABLE jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa06_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • riga 2: la tabella di join che concretizza la precedente relazione @OneToMany non inversa.

L'esecuzione di [InitDB] avviene senza errori, mentre quella di [Main] si interrompe al test 7 con i seguenti log (FINEST):

main : ----------- test7
[TopLink Finer]: 2007.06.01 01:41:48.734--ServerSession(15290002)--Thread(Thread[main,5,main])--client acquisito
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Unisci clone con riferimenti Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Registra l'oggetto esistente Article[6,1,B1]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Registra l'oggetto esistente Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--L'operazione di rimozione è stata eseguita su: Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--L'operazione di rimozione è stata eseguita su: Article[6,1,B1]
[TopLink Finer]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--inizio commit dell'unità di lavoro
[TopLink Finer]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Connessione(6330655)--Thread(Thread[main,5,main])--inizio transazione
[TopLink Finest]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Esegui query DeleteObjectQuery(Articolo[6,1,B1])
[TopLink Fine]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Connessione(6330655)--Thread(Thread[main,5,main])--DELETE FROM jpa06_tl_article WHERE ((ID = ?) AND (VERSION = ?))
    bind => [6, 1]
[TopLink Warning]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Stack delle eccezioni locali: 
Exception [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa06_tl_categorie_jpa06_tl_article`, CONSTRAINT `FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID` FOREIGN KEY (`articles_ID`) REFERENCES `jpa06_tl_article` (`ID`))
Error Code: 1451
Call: DELETE FROM jpa06_tl_article WHERE ((ID = ?) AND (VERSION = ?))
    bind => [6, 1]
  • riga 3: il merge nella categoria B
  • riga 4: l'articolo dipendente B1 viene inserito nel contesto
  • riga 5: lo stesso vale per la categoria B stessa
  • riga 6: il codice remove nella categoria B
  • riga 7: il codice remove sull'articolo B1 (per cascata)
  • riga 8: il codice Java richiede il commit della transazione
  • riga 9: viene avviata una transazione; a quanto pare, quindi, non era ancora iniziata.
  • riga 10: l’articolo B1 verrà eliminato da un’operazione DELETE sulla tabella [article]. È qui che si trova il problema. La tabella di join [categorie_article] ha un riferimento alla riga B1 della tabella [article]. L'eliminazione di B1 in [article] violerà un vincolo di chiave esterna.
  • righe 13 e successive: si verifica l'eccezione

Cosa si può concludere?

  • Ancora una volta, si presenta un problema di portabilità tra Hibernate e Toplink: Hibernate aveva superato questo test
  • Toplink non gestisce correttamente il caso in cui due relazioni siano di fatto inverse l’una rispetto all’altra, senza che una sia dichiarata principale e l’altra inversa. Si può accettare questa situazione poiché si tratta in realtà di un errore di configurazione. Nel nostro esempio, la tabella [article] non ha alcuna relazione con la tabella di join [categorie_article]. Sembra quindi naturale che, durante un’operazione sulla tabella [article], Toplink non cerchi di lavorare con la tabella [categorie_article].

2.5. Esempio 5: relazione molti-a-molti con una tabella di join esplicita

2.5.1. Lo schema del database

  • in [1], il database MySQL5

Conosciamo già le tabelle [personne], [2] e [adresse], [3]. Sono state esaminate nel paragrafo 2.3.1. Prendiamo in esame la versione in cui l’indirizzo della persona è oggetto di una tabella a sé stante: [adresse] e [3]. Nella tabella [personne], la relazione che collega una persona al proprio indirizzo è rappresentata da un vincolo di chiave esterna.

Una persona svolge delle attività. Queste sono presenti nelle tabelle [activite] e [4]. Una persona può svolgere più attività e un’attività può essere svolta da più persone. Una relazione molti-a-molti collega quindi le tabelle [personne] e [activite]. Questa relazione è rappresentata dalla tabella di join [personne_activite] [5].

2.5.2. Gli oggetti @Entity che rappresentano il database

Le tabelle precedenti saranno rappresentate dalle seguenti @Entity:

  • l'@Entity Personne rappresenterà la tabella [personne]
  • l’@Entity Adresse rappresenterà la tabella [adresse]
  • l'@Entity Activite rappresenterà la tabella [activite]
  • l'@Entity PersonneActivite rappresenterà la tabella [personne_activite]

Le relazioni tra queste entità sono le seguenti:

  • una relazione uno-a-uno collega l’entità Personne all’entità Adresse: una persona p ha un indirizzo a. L'entità Personne, che detiene la chiave esterna, avrà la relazione principale, mentre l'entità Adresse avrà la relazione inversa.
  • Una relazione molti-a-molti collega le entità Personne e Activite: una persona svolge diverse attività e un’attività è svolta da più persone. Questa relazione potrebbe essere realizzata direttamente tramite un’annotazione @ManyToMany in ciascuna delle due entità, dichiarando l’una come inversa dell’altra. Questa soluzione verrà approfondita in seguito. In questa sede, realizziamo la relazione molti-a-molti tramite due relazioni uno-a-molti:
    • una relazione uno-a-molti che collega l’entità Personne all’entità PersonneActivite: una riga (One) della tabella [personne] è referenziata da più (Many) righe della tabella [personne_activite]. La tabella [personne_activite], che contiene la chiave esterna, avrà la relazione principale @ManyToOne, mentre l’entità Personne avrà la relazione inversa @OneToMany.
    • una relazione uno-a-molti che collega l’entità Activite all’entità PersonneActivite: una riga (One) della tabella [activite] è referenziata da più (Many) righe della tabella [personne_activite]. La tabella [personne_activite], che contiene la chiave esterna, avrà la relazione principale @ManyToOne, mentre l'entità Activite avrà la relazione inversa @OneToMany.

L'@Entity Personne è la seguente:


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

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

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

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

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    // relazione principale Persona (una) -> Indirizzo (uno)
    // implementata dalla chiave esterna Persona(adresse_id) -> Indirizzo
    // inserimento a cascata Persona -> inserimento Indirizzo
    // aggiornamento a cascata Persona -> aggiornamento Indirizzo
    // cascata eliminazione Persona -> eliminazione Indirizzo
    // una Persona deve avere 1 Indirizzo (nullable=false)
    // 1 Indirizzo appartiene a una sola persona (unico=vero)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // relazione Persona (uno) -> PersonneActivite (molti)
    // inverso della relazione esistente PersonneActivite (many) -> Persona (one)
    // cascata di eliminazione Persona -> eliminazione PersonneActivite
    @OneToMany(mappedBy = "personne", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> activites = new HashSet<PersonneActivite>();

    // costruttori

Questa @Entity è nota. Commentiamo solo le relazioni che essa ha con le altre entità:

  • righe 30-39: una relazione uno-a-uno @OneToOne con l'@Entity Adresse, concretizzata da una chiave esterna [adresse_id] (riga 38) che la tabella [personne] avrà sulla tabella [adresse].
  • righe 41-45: una relazione uno-a-molti @OneToMany con l'@Entity PersonneActivite. Una persona (One) è referenziata da più (Many) righe della tabella di join [personne_activite] rappresentata dall'@Entity PersonneActivite. Questi oggetti PersonneActivite saranno inseriti in un tipo Set<PersonneActivite>, dove PersonneActivite è un tipo che definiremo tra poco.
  • riga 44: la relazione uno-a-molti qui definita è la relazione inversa di una relazione principale definita sul campo personne dell’@Entity PersonneActivite (parola chiave mappedBy). È presente una cascata Persona -> Attività per le eliminazioni: l’eliminazione di una persona p comporterà l’eliminazione degli elementi persistenti di tipo PersonneActivite presenti nell’insieme p.activites.

L'@Entity Adresse è la seguente:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {

    // campi
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;

  • righe 28-29: la relazione @OneToOne, inversa della relazione @OneToOne, rimanda all’@Entity Personne (righe 37-38 di Personne).

L'@Entity Activite è la seguente


@Entity
@Table(name = "jpa07_hb_activite")
public class Activite implements Serializable {

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

    @Column(nullable = false)
    @Version
    private int version;

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

    // relazione Attività (uno) -> PersonneActivite (molti)
    // inverso della relazione esistente PersonneActivite (molti) -> Attività (uno)
    // cascata di eliminazione Attività -> eliminazione PersonneActivite
    @OneToMany(mappedBy = "activite", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> personnes = new HashSet<PersonneActivite>();

  • righe 6-9: la chiave primaria dell’attività
  • righe 11-13: il numero di versione dell’attività
  • righe 15-16: il nome dell’attività
  • righe 18-22: la relazione uno-a-molti che collega l'@Entity Activite all'@Entity PersonneActivite: un'attività (One) è referenziata da più (Many) righe della tabella di join [personne_activite] rappresentata dall'@Entity PersonneActivite. Questi oggetti PersonneActivite saranno inseriti in un tipo Set<PersonneActivite>.
  • riga 22: la relazione uno-a-molti qui definita è la relazione inversa di una relazione principale definita sul campo activite nell’@Entity PersonneActivite (parola chiave mappedBy). È presente una cascata Attività -> PersonneActivite per le eliminazioni: l’eliminazione della tabella [activite] daun'attività a comporterà l'eliminazione della tabella di join [personne_activite] degli elementi persistenti di tipo PersonneActivite presenti nell'insieme a.personnes.

L'@Entity PersonneActivite è la seguente:


@Entity
// tabella di join
@Table(name = "jpa07_hb_personne_activite")
public class PersonneActivite {

    @Embeddable
    public static class Id implements Serializable {
        // componenti della chiave composita
        // punta a una Persona
        @Column(name = "PERSONNE_ID")
        private Long personneId;

        // punta a un'attività
        @Column(name = "ACTIVITE_ID")
        private Long activiteId;

        // costruttori
...

        // getter e setter
...
        // toString
        public String toString() {
            return String.format("[%d,%d]", getPersonneId(), getActiviteId());
        }
    }

    // campi della classe Personne_Activite
    // chiave composita
    @EmbeddedId
    private Id id = new Id();

    // relazione principale PersonneActivite (many) -> Persona (one)
    // implementata dalla chiave esterna: personneId (PersonneActivite (molti) -> Persona (uno)
    // personneId è allo stesso tempo un elemento della chiave primaria composita
    // JPA non deve gestire questa chiave esterna (insertable = false, updatable = false) poiché ciò viene effettuato dall'applicazione stessa nel proprio costruttore
    @ManyToOne
    @JoinColumn(name = "PERSONNE_ID", insertable = false, updatable = false)
    private Personne personne;

    // relazione principale PersonneActivite -> Attività
    // implementata dalla chiave esterna: activiteId (PersonneActivite (molte) -> Attività (una)
    // activiteId è allo stesso tempo un elemento della chiave primaria composita
    // JPA non deve gestire questa chiave esterna (insertable = false, updatable = false) poiché ciò viene effettuato dall'applicazione stessa nel proprio costruttore
    @ManyToOne()
    @JoinColumn(name = "ACTIVITE_ID", insertable = false, updatable = false)
    private Activite activite;

    // costruttori
    public PersonneActivite() {

    }

    public PersonneActivite(Personne p, Activite a) {
        // le chiavi esterne sono definite dall’applicazione
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // associazioni bidirezionali
        this.setPersonne(p);
        this.setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
    }

    // getter e setter
...
    // toString
    public String toString() {
        return String.format("[%s,%s,%s]", getId(), getPersonne().getNom(), getActivite().getNom());
    }
}

Questa classe è più complessa rispetto alle precedenti.

  • La tabella [personne_activite] contiene righe della forma [p,a], dove p è la chiave primaria di una persona e a è la chiave primaria di un'attività. Ogni tabella deve avere una chiave primaria e [personne_activite] non fa eccezione alla regola. Finora avevamo definito chiavi primarie generate dinamicamente da SGBD. Potremmo farlo anche in questo caso. Utilizzeremo invece un’altra tecnica, quella in cui è l’applicazione stessa a definire i valori della chiave primaria di una tabella. In questo caso, una riga [p1,a1] indica che una persona p1 pratica l’attività a1. Non è possibile trovare una seconda volta questa stessa riga nella tabella. Pertanto, la coppia (p, a) è un buon candidato per fungere da chiave primaria. Si parla in questo caso di chiave primaria composita.
  • righe 30-31: la chiave primaria composita. L’annotazione @EmbeddedId (di solito era @Id) è analoga alla notazione @Embedded applicata al campo Adresse di una persona. In quest’ultimo caso, ciò significava che il campo Adresse era oggetto di una classe esterna ma doveva essere inserito nella stessa tabella della persona. Qui il significato è lo stesso, ma per indicare che si tratta della chiave primaria, la notazione diventa @EmbeddedId.
  • riga 31: un oggetto vuoto che rappresenta la chiave primaria id viene creato non appena viene creato l’oggetto [PersonneActivite]. La classe che rappresenta la chiave primaria è definita alle righe 7-26 come una classe pubblica, statica e interna alla classe [PersonneActivite]. Il fatto che sia pubblica e statica è imposto da Hibernate. Se si sostituisce «public static» con «private,», si verifica un'eccezione e nel messaggio di errore associato si vede che Hibernate ha tentato di eseguire l'istruzione «new PersonneActivite$Id». È quindi necessario che la classe Id sia sia statica che pubblica.
  • riga 6: la classe Id della chiave primaria è dichiarata @Embeddable. Ricordiamo che la chiave primaria id della riga 31 è stata dichiarata @EmbeddedId. La classe corrispondente deve quindi avere l’annotazione @Embeddable.
  • Abbiamo detto che la chiave primaria della tabella [personne_activite] era composta dalla coppia (p,a), dove p è la chiave primaria di una persona e a la chiave primaria di un’attività. I due elementi (p,a) della chiave composita si trovano alla riga 11 (personneId) e alla riga 15 (activiteId). Le colonne associate a questi due campi sono denominate: PERSONNE_ID per la persona, ACTIVITE_ID per l'attività.
  • riga 31: la chiave primaria è stata definita con le sue due colonne (PERSONNE_ID, ACTIVITE_ID). Non ci sono altre colonne nella tabella [personne_activite]. Non resta che definire le relazioni esistenti tra l’@Entity PersonneActivite che stiamo descrivendo e le altre @Entity dello schema relazionale. Queste relazioni riflettono i vincoli di chiave esterna che la tabella [personne_activite] ha con le altre tabelle.
  • righe 33-39: definiscono la chiave esterna che la tabella [personne_activite] ha sulla tabella [personne]
  • riga 37: la relazione è di tipo @ManyToOne: una riga (One) della tabella [personne] è referenziata da più (Many) righe della tabella [personne_activite].
  • riga 38: si assegna un nome alla colonna della chiave esterna. Si riprende lo stesso nome assegnato alla componente «persona» della chiave esterna (riga 10). Gli attributi insertable=false, updatable=false servono a impedire a Hibernate di gestire la chiave esterna. Quest’ultima è infatti la componente di una chiave primaria calcolata dall’applicazione e Hibernate non deve intervenire.
  • righe 41-47: definiscono la chiave esterna che la tabella [personne_activite] possiede sulla tabella [activite]. Le spiegazioni sono le stesse fornite in precedenza.
  • righe 54-63: costruttore di un oggetto PersonneActivite a partire da una persona p e da un’attività a. Ricordiamo che durante la creazione di un oggetto PersonneActivite, la chiave primaria id della riga 31 puntava a un oggetto Id vuoto. Le righe 56-57 assegnano un valore a ciascuno dei campi (personneId, activiteId) dell’oggetto Id. Questi valori sono rispettivamente le chiavi primarie della persona p e dell’attività a passate come parametri al costruttore. La chiave primaria id (riga 31) ha quindi ora un valore.
  • riga 59: il campo personne della riga 39 riceve il valore p
  • riga 60: il campo activite della riga 47 riceve il valore a
  • È stato creato e inizializzato un oggetto [PersonneActivite]. Si aggiornano le relazioni inverse tra le @Entity Personne (riga 61) e Activite (riga 62) e la @Entity PersonneActivite appena creata.

Abbiamo completato la descrizione delle entità del database. Ci troviamo in una situazione complessa ma purtroppo frequente. Vedremo che esiste un’altra configurazione possibile del livello JPA che nasconde parte di questa complessità: la tabella di join diventa implicita, costruita e gestita dal livello JPA. Abbiamo scelto qui la soluzione più complessa, ma che consente allo schema relazionale di evolversi. Consente quindi di aggiungere colonne alla tabella di join, cosa che non è possibile nella configurazione in cui la tabella di join non è un’@Entity esplicita. [ref1] consiglia la soluzione che stiamo esaminando. È in [ref1] che sono state trovate le informazioni che hanno permesso l’elaborazione di questa soluzione.

2.5.3. Il progetto Eclipse / Hibernate

L’implementazione JPA qui utilizzata è quella di Hibernate. Il progetto Eclipse dei test è il seguente:

 

Image

In [1] si trova il progetto Eclipse, mentre in [2] i codici Java. Il progetto è presente in [3] nella cartella degli esempi [4]. Lo importeremo.

2.5.4. Generazione del file DDL dal database

Seguendo le istruzioni del paragrafo 2.1.7, il DDL ottenuto per il SGBD MySQL5 è il seguente:


alter table jpa07_hb_personne 
        drop 
        foreign key FKB5C817D45FE379D0;

    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B06CD852024;

    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B0668C7A284;

    drop table if exists jpa07_hb_activite;

    drop table if exists jpa07_hb_adresse;

    drop table if exists jpa07_hb_personne;

    drop table if exists jpa07_hb_personne_activite;

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

    create table jpa07_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;

    alter table jpa07_hb_personne 
        add index FKB5C817D45FE379D0 (adresse_id), 
        add constraint FKB5C817D45FE379D0 
        foreign key (adresse_id) 
        references jpa07_hb_adresse (id);

    alter table jpa07_hb_personne_activite 
        add index FKD3E49B06CD852024 (ACTIVITE_ID), 
        add constraint FKD3E49B06CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa07_hb_activite (id);

    alter table jpa07_hb_personne_activite 
        add index FKD3E49B0668C7A284 (PERSONNE_ID), 
        add constraint FKD3E49B0668C7A284 
        foreign key (PERSONNE_ID) 
        references jpa07_hb_personne (id);
  • righe 21-26: la tabella [activite]
  • righe 28-39: la tabella [adresse]
  • righe 41-51: la tabella [personne]
  • righe 53-57: la tabella di join [personne_activite]. Si noti la chiave composita (riga 56)
  • righe 59-63: la chiave esterna dalla tabella [personne] alla tabella [adresse]
  • righe 65-69: la chiave esterna dalla tabella [personne_activite] alla tabella [activite]
  • righe 71-75: la chiave esterna dalla tabella [personne_activite] alla tabella [personne]

2.5.5. InitDB

Il codice di [InitDB] è il seguente:


package tests;

...
public class InitDB {

    // costanti
    private final static String TABLE_PERSONNE_ACTIVITE = "jpa07_hb_personne_activite";

    private final static String TABLE_PERSONNE = "jpa07_hb_personne";

    private final static String TABLE_ACTIVITE = "jpa07_hb_activite";

    private final static String TABLE_ADRESSE = "jpa07_hb_adresse";

    public static void main(String[] args) throws ParseException {
        // Contesto di persistenza
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // si recupera un EntityManager  dal EntityManagerFactory
        // precedente
        em = emf.createEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // richiesta
        Query sql1;
        // eliminare gli elementi dalla tabella PERSONNE_ACTIVITE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE_ACTIVITE);
        sql1.executeUpdate();
        // eliminare gli elementi della tabella PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // eliminare gli elementi della tabella ACTIVITE
        sql1 = em.createNativeQuery("delete from " + TABLE_ACTIVITE);
        sql1.executeUpdate();
        // eliminare gli elementi della tabella ADRESSE
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // creazione attività
        Activite act1 = new Activite();
        act1.setNom("act1");
        Activite act2 = new Activite();
        act2.setNom("act2");
        Activite act3 = new Activite();
        act3.setNom("act3");
        // persistenza attività
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // creazione persone
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        Personne p3 = new Personne("p3", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // creazione indirizzi
        Adresse adr1 = new Adresse("adr1", null, null, "49000", "Angers", null, "France");
        Adresse adr2 = new Adresse("adr2", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse adr3 = new Adresse("adr3", "x", "x", "x", "x", "x", "x");
        Adresse adr4 = new Adresse("adr4", "y", "y", "y", "y", "y", "y");
        // associazioni persona <--> indirizzo
        p1.setAdresse(adr1);
        adr1.setPersonne(p1);
        p2.setAdresse(adr2);
        adr2.setPersonne(p2);
        p3.setAdresse(adr3);
        adr3.setPersonne(p3);
        // persistenza delle persone e quindi degli indirizzi associati
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // persistenza dell'indirizzo a4 non collegato a una persona
        em.persist(adr4);
        // visualizzazione delle persone
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // visualizzazione indirizzi
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        // associazioni persona <--> attività
        PersonneActivite p1act1 = new PersonneActivite(p1, act1);
        PersonneActivite p1act2 = new PersonneActivite(p1, act2);
        PersonneActivite p2act1 = new PersonneActivite(p2, act1);
        PersonneActivite p2act3 = new PersonneActivite(p2, act3);
        // persistenza delle associazioni persona <--> attività
        em.persist(p1act1);
        em.persist(p1act2);
        em.persist(p2act1);
        em.persist(p2act3);
        // visualizzazione persone
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // visualizzazione indirizzi
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[personnes/activites]");
        for (Object pa : em.createQuery("select pa from PersonneActivite pa").getResultList()) {
            System.out.println(pa);
        }
        // fine transazione
        tx.commit();
        // fine EntityManager
        em.close();
        // fine EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}
  • righe 27-38: le tabelle [personne_activite], [personne], [adresse] e [activite] vengono svuotate. Si noti che è necessario iniziare dalle tabelle che contengono le chiavi esterne.
  • righe 40-45: si creano tre attività act1, act2 e act3
  • righe 47-49: vengono inserite nel contesto di persistenza.
  • righe 51-53: si creano tre persone p1, p2 e p3.
  • righe 55-58: si creano quattro indirizzi da adr1 a adr4.
  • righe 60-65: gli indirizzi adri vengono associati alle persone pi. In ogni caso sono necessarie due operazioni, poiché la relazione Persona <-> Indirizzo è bidirezionale.
  • righe 67-69: le persone da p1 a p3 vengono inserite nel contesto di persistenza. A causa della cascata Persona -> Indirizzo, ciò vale anche per gli indirizzi da adr1 a adr3.
  • riga 71: il quarto indirizzo adr4, non associato a una persona, viene inserito esplicitamente nel contesto di persistenza.
  • righe 73-85: viene interrogato il contesto di persistenza per ottenere l’elenco delle entità di tipo [Personne], [Adresse] e [Activite]. È noto che queste richieste provocheranno la sincronizzazione del contesto con il database: le entità create verranno inserite nel database e otterranno la loro chiave primaria. È importante comprenderlo per il prosieguo.
  • righe 87-90: si creano 4 associazioni Persona <-> Attività. Il loro nome indica quale persona è collegata a quale attività. Si ricorderà forse che la chiave primaria di un’entità PersonneActivite è una chiave composita formata dalla chiave primaria di una persona e da quella di un’attività. È quindi proprio perché le entità Personne e Activite hanno ottenuto le loro chiavi primarie durante una sincronizzazione precedente che questa operazione è possibile.
  • righe 92-95: queste 4 associazioni vengono inserite nel contesto di persistenza.
  • righe 87-86: viene interrogato il contesto di persistenza per ottenere l’elenco delle entità di tipo [Personne], [Adresse], [Activite] e [PersonneActivite]. È noto che queste richieste provocheranno la sincronizzazione del contesto con il database: le entità PersonneActivite create verranno inserite nel database.

L'esecuzione di [InitDB] insieme a MySQL5 produce il seguente output in console:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
terminé...

Potrebbe sorprendere notare che alle righe 15-16 le persone p1 e p2 hanno il numero di versione pari a 1 e che lo stesso vale, alle righe 24-26, per le tre attività. Cerchiamo di capire.

Nelle righe 2-4, i numeri di versione delle persone sono pari a 0 e nelle righe 11-13, i numeri di versione delle attività sono pari a 0. Le visualizzazioni precedenti avvengono prima della creazione delle relazioni Persona <-> Attività. Nelle righe 87-90 del codice Java vengono create relazioni tra le persone p1 e p2 e le attività act1, act2, act3. Esse vengono realizzate tramite il costruttore dell’@Entity PersonneActivite (cfr. paragrafo 2.5.2). L’analisi del codice di questo costruttore mostra che quando una persona p è collegata a un’attività a:

  • l’attività a viene aggiunta all’insieme p.activites
  • la persona p viene aggiunta all’insieme a.personnes

Pertanto, quando si scrive new PersonneActivite(p,a), la persona p e l’attività a subiscono una modifica in memoria. Quando si eseguono le righe 97-113 di [InitDB], il contesto di persistenza viene sincronizzato con il database, JPA / Hibernate rileva che gli elementi persistenti p1, p2, act1, act2 e act3 sono stati modificati. Queste modifiche devono essere apportate nel database. Esse sono infatti registrate nella tabella di join [personne_activite], ma JPA / Hibernate incrementa comunque il numero di versione di ciascuno degli elementi persistenti modificati.

Nella vista SQL Explorer, i risultati sono i seguenti:

  • [2]: le tabelle [jpa07_hb_*]
  • [3]: la tabella delle persone
  • [4]: la tabella degli indirizzi.
  • [5]: la tabella delle attività
  • [6]: la tabella di join persona <-> attività

2.5.6. Home

La classe [Main] concatena una serie di test che esamineremo, ad eccezione del test 1, che riprende il codice di [InitDB] per inizializzare il database.

2.5.6.1. Test 2

Questo test è il seguente:


// eliminazione Persona p1
    public static void test2() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminazione delle dipendenze su p1: non necessario per Hibernate ma
        // indispensabile per TopLink
        act1.getPersonnes().remove(p1act1);
        act2.getPersonnes().remove(p1act2);
        // eliminazione persona p1
        em.remove(p1);
        // fine transazione
        tx.commit();
        // vengono visualizzate le nuove tabelle
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • riga 4: si utilizza il contesto di persistenza di test1, in cui la persona p1 è un oggetto del contesto.
  • riga 13: eliminazione della persona p1. A causa dell'attributo:
    • cascadeType.ALL su Adresse, l'indirizzo della persona p1 verrà eliminato
    • cascadeType.REMOVE su PersonneActivite, le attività della persona p1 verranno eliminate.
  • righe 10-11: si eliminano le dipendenze che le altre entità hanno nei confronti della persona p1, che verrà eliminata alla riga 13. Le attività act1 e act2 sono svolte dalla persona p1. I collegamenti sono stati creati dal costruttore dell’entità PersonneActivite, il cui codice è il seguente:

    public PersonneActivite(Personne p, Activite a) {
        // le chiavi esterne sono definite dall'applicazione
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // associazioni bidirezionali
        setPersonne(p);
        setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
}

alla riga 9, l’attività a riceve un elemento aggiuntivo di tipo PersonneActivite all’interno del proprio insieme personnes. Tale elemento è di tipo (p,a) per indicare che la persona p svolge l'attività a. In test1 di [Main] sono stati quindi creati due collegamenti: (p1,act1) e (p1,act2). Le righe 10 e 11 di test2 eliminano queste dipendenze. Va notato che Hibernate funziona senza l’eliminazione di queste dipendenze per la persona p1, ma non Toplink.

  • righe 17-20: vengono visualizzate tutte le tabelle

I risultati sono i seguenti:

main : ----------- test1
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
  • la persona p1 presente in test1 (riga 3) non è più presente al termine di test2 (righe 22-23)
  • l'indirizzo adr1 della persona p1 presente in test1 (riga 11) non lo è più al termine di test2 (righe 29-31)
  • le attività (p1,act1) (riga 16) e (p1,act2) (riga 18) della persona p1, presenti in test1, non lo sono più al termine di test2 (righe 33-34)

2.5.6.2. Test3

Questo test è il seguente:


// eliminazione attività act1
    public static void test3() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminazione delle dipendenze su act1: non necessario per Hibernate ma
        // indispensabile per TopLink
        p2.getActivites().remove(p2act1);
        // eliminazione dell'attività act1
        em.remove(act1);
        // fine transazione
        tx.commit();
        // vengono visualizzate le nuove tabelle
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • riga 4: si utilizza il contesto di persistenza di test2
  • riga 12: eliminazione dell'attività act1. A causa dell'attributo:
    • cascadeType.REMOVE su PersonneActivite, le righe (p, act1) della tabella [personne_activite] verranno eliminate.
  • riga 10: prima di rimuovere act1 dal contesto di persistenza, vengono eliminate le dipendenze che altre entità potrebbero avere su questo oggetto persistente. Dopo l'eliminazione della persona p1 nel test precedente, solo la persona p2 svolge l'attività act1.
  • righe 13-16: vengono visualizzate tutte le tabelle

I risultati sono i seguenti:

main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,3],p2,act3]
  • in test2, l'attività act1 esiste (riga 6). In test3, non esiste più (righe 21-22)
  • in test2, il collegamento (p2,act1)) esiste (riga 14). In test3, non esiste più (riga 28)

2.5.6.3. Test4

Questo test è il seguente:


// recupero delle attività di una persona
    public static void test4() {
        // contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si recupera la persona p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("1 - Activités de la personne p2 (JPQL) :%n");
        // si analizzano le sue attività
        for (Object pa : em.createQuery("select a.nom from Activite a join a.personnes pa where pa.personne.nom='p2'").getResultList()) {
            System.out.println(pa);
        }
        // si passa attraverso la relazione inversa di p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("2 - Activités de la personne p2 (relation inverse) :%n");
        // si analizzano le sue attività
        for (PersonneActivite pa : p2.getActivites()) {
            System.out.println(pa.getActivite().getNom());
        }
        // fine transazione
        tx.commit();
    }
  • il test 4 mostra le attività della persona p2.
  • riga 4: si parte da un contesto nuovo e vuoto
  • righe 12-14: vengono visualizzati i nomi delle attività svolte dalla persona p2 tramite una query JPQL.
    • viene eseguito un join Activite (a) / PersonneActivite (pa) (join a.personnes)
    • nelle righe di questo join (a,pa), viene visualizzato il nome dell'attività (a.nom) per la persona p2 (pa.personne.nom='p2').
  • righe 16-21: si esegue la stessa operazione di prima, ma utilizzando la relazione OneToMany p2.activites della persona p2. La richiesta JPQL verrà generata da JPA. Qui si vede l’utilità della relazione inversa OneToMany: evita una richiesta JPQL.

I risultati sono i seguenti:

1
2
3
4
5
main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation inverse) :
act3

2.5.6.4. Test5

Questo test è il seguente:


// recupero delle persone che svolgono una determinata attività
    public static void test5() {
        // contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // si richiedono le attività di p2
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites pa where pa.activite.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        // si passa attraverso la relazione inversa di act3
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (PersonneActivite pa : act3.getPersonnes()) {
            System.out.println(pa.getPersonne().getNom());
        }
        // fine transazione
        tx.commit();
    }
  • il test 6 mostra le persone che svolgono l'attività act3. La procedura è analoga a quella del test 6. Lasciamo al lettore il compito di stabilire il collegamento tra i due codici.

I risultati sono i seguenti:

1
2
3
4
5
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2

I test 4 e 5 avevano lo scopo di dimostrare ancora una volta che una relazione inversa non è mai indispensabile e può sempre essere sostituita da una query JPQL.

Ora utilizziamo un'implementazione JPA / Toplink:

Il progetto Eclipse con Toplink è una copia del progetto Eclipse con Hibernate:

I codici Java sono identici a quelli del precedente progetto Hibernate, salvo alcuni dettagli che tratteremo in seguito. L'ambiente (librerie – persistence.xml – DBMS – cartelle conf, ddl – script ant) è quello descritto nel paragrafo 2.1.15.2. Il progetto Eclipse è presente ([3]) nella cartella degli esempi ([4]). Lo importeremo.

Il file <persistence.xml> [2] viene modificato in un punto, ovvero nelle entità dichiarate:


        <!-- classi persistenti -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
<class>entites.PersonneActivite</class>
  • righe 2-5: le quattro entità gestite

L'esecuzione di [InitDB] con SGBD e MySQL5 fornisce i seguenti risultati:

In [1], l'output della console; in [2], le tabelle generate [jpa07_tl]; in [3], gli script generati SQL. Il loro contenuto è il seguente:

create.sql


CREATE TABLE jpa07_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa07_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa07_tl_activite (ID)
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa07_tl_personne (ID)
ALTER TABLE jpa07_tl_personne ADD CONSTRAINT FK_jpa07_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa07_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

L'esecuzione di [InitDB] e di [Main] avviene senza errori.

2.6. Esempio 6: relazione molti-a-molti con una tabella di join implicita

Riprendiamo l’esempio 4, ma ora lo trattiamo con una tabella di join implicita generata dallo stesso livello JPA.

2.6.1. Lo schema del database

  • in [1], il database MySQL5 - in [2]: la tabella [personne] – in [3]: la tabella associata [adresse] – in [4]: la tabella [activite] delle attività – in [5]: la tabella di join [personne_activite] che collega le persone alle attività.

2.6.2. Gli oggetti @Entity che rappresentano il database

Le tabelle precedenti saranno rappresentate dai seguenti @Entity:

  • l'@Entity Personne rappresenterà la tabella [personne]
  • l'@Entity Adresse rappresenterà la tabella [adresse]
  • l'@Entity Activite rappresenterà la tabella [activite]
  • la tabella [personne_activite] non è più rappresentata da un @Entity

Le relazioni tra queste entità sono le seguenti:

  • una relazione uno-a-uno collega l’entità Personne all’entità Adresse: una persona p ha un indirizzo a. L'entità Personne, che detiene la chiave esterna, avrà la relazione principale, mentre l'entità Adresse avrà la relazione inversa.
  • Una relazione molti-a-molti collega le entità Personne e Activite: una persona svolge diverse attività e un'attività è svolta da più persone. Questa relazione sarà concretizzata da un'annotazione @ManyToMany in ciascuna delle due entità, una delle quali sarà dichiarata inversa rispetto all'altra.

L'@Entity Personne è la seguente:


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

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // toplink sqlserver :@GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

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

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

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    // relazione principale Persona (one) -> Indirizzo (one)
    // implementata tramite la chiave esterna Persona (adresse_id) -> Indirizzo
    // cascata inserimento Persona -> inserimento Indirizzo
    // aggiornamento a cascata Persona -> aggiornamento Indirizzo
    // cascata eliminazione Persona -> eliminazione Indirizzo
    // una Persona deve avere 1 Indirizzo (nullable=false)
    // 1 Indirizzo appartiene a una sola persona (unico=vero)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // relazione Persona (molte) -> Attività (molte) tramite una tabella di join personne_activite
    // personne_activite(PERSONNE_ID) è una chiave esterna su Persona(id)
    // personne_activite(ACTIVITE_ID) è una chiave estranea su Attività(id)
    // cascade=CascadeType.PERSIST: la persistenza di una persona comporta quella delle sue attività
    @ManyToMany(cascade={CascadeType.PERSIST})
    @JoinTable(name="jpa08_hb_personne_activite",joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
    private Set<Activite> activites = new HashSet<Activite>();

    // costruttori
    public Personne() {
    }

Commentiamo solo la relazione @ManyToMany delle righe 46-48 che collega l’@Entity Personne all’@Entity Activite:

  • riga 48: una persona ha delle attività. Il campo «activites» le rappresenterà. Nella versione precedente, il tipo degli elementi dell’insieme activites era PersonneActivite. Qui è Activite. Si accede quindi direttamente alle attività di una persona, mentre nella versione precedente era necessario passare attraverso l’entità intermedia PersonneActivite.
  • riga 46: la relazione che collega l'@Entity Personne che stiamo esaminando all'@Entity Activite dell'insieme activites della riga 48 è di tipo molti-a-molti (ManyToMany):
    • una persona (One) svolge diverse attività (Many)
    • un’attività (One) è svolta da più persone (Many)
    • In definitiva, le @Entity Personne e Activite sono collegate da una relazione ManyToMany. Come nella relazione OneToOne, anche in questa relazione si riscontra una simmetria tra le entità. È possibile scegliere liberamente quale @Entity avrà la relazione principale e quale quella inversa. In questo caso, decidiamo che l'@Entity Personne avrà la relazione principale.
    • Come abbiamo visto nell’esempio precedente, la relazione @ManyToMany richiede una tabella di join. Mentre in precedenza l’avevamo definita utilizzando un’@Entity, in questo caso la tabella di join è definita tramite l’annotazione @JoinTable alla riga 47.
      • L'attributo name assegna un nome alla tabella.
      • La tabella di join è costituita dalle chiavi esterne presenti nelle tabelle che essa unisce. In questo caso, sono presenti due chiavi esterne: una sulla tabella [personne], l’altra sulla tabella [activite]. Queste colonne di chiave esterna sono definite dagli attributi joinColumns e inverseJoinColumns.
      • L'annotazione @JoinColumn dell'attributo joinColumns definisce la chiave esterna sulla tabella dell'@Entity che detiene la relazione principale @ManyToMany, in questo caso la tabella [personne]. Questa colonna chiave esterna si chiamerà PERSONNE_ID.
      • L'annotazione @JoinColumn dell'attributo inverseJoinColumns definisce la chiave esterna nella tabella dell'@Entity che contiene la relazione inversa @ManyToMany, in questo caso la tabella [activite]. Questa colonna chiave esterna si chiamerà ACTIVITE_ID.

L'@Entity Adresse è la seguente:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {

    // campi
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;

  • righe 28-29: la relazione @OneToOne è l'inversa della relazione @OneToOne e rimanda all'@Entity Personne (righe 37-38 di Personne).

L'@Entity Activite è la seguente


@Entity
@Table(name = "jpa08_hb_activite")
public class Activite implements Serializable {

    // campi
    @Id()
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // toplink sqlserver: @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

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

    // relazione inversa Attività -> Persona
    @ManyToMany(mappedBy = "activites")
    private Set<Personne> personnes = new HashSet<Personne>();
...
  • righe 20-21: la relazione molti-a-molti che collega l'@Entity Activite all'@Entity Personne. Questa relazione è già stata definita nell'@Entity Personne. Ci si limita quindi qui a specificare che la relazione è l'inversa (mappedBy) della relazione @ManyToMany esistente sul campo «activites» (mappedBy = «activites») dell'@Entità Personne.
  • Ricordiamo che una relazione inversa è sempre facoltativa. In questo caso, la utilizziamo per ottenere le persone che praticano l’attività corrente. È l’insieme Set<Persona> persone che consentirà di ottenerle. La modalità di caricamento delle dipendenze Personne dell’@Entity Activite non è specificata. Non l’avevamo specificata nemmeno nell’esempio precedente. Per impostazione predefinita, questa modalità è fetch=FetchType.LAZY.

Abbiamo completato la descrizione delle entità del database. È stata più semplice rispetto al caso in cui la tabella di join [personne_activite] fosse una tabella esplicita. Questa soluzione più semplice può presentare degli svantaggi nel corso del tempo: non consente di aggiungere colonne alla tabella di join. Ciò potrebbe tuttavia rivelarsi necessario per soddisfare nuove esigenze, ad esempio aggiungere alla tabella [personne_activite] una colonna che indichi la data di iscrizione della persona all’attività.

2.6.3. Il progetto Eclipse / Hibernate

L’implementazione JPA qui utilizzata è quella di Hibernate. Il progetto Eclipse dei test è il seguente:

In [1] si trova il progetto Eclipse, mentre in [2] il codice Java. Il progetto è presente in [3] nella cartella degli esempi [4]. Lo importeremo.

2.6.4. Generazione del file DDL dal database

Seguendo le istruzioni del paragrafo 2.1.7, il DDL ottenuto per il SGBD MySQL5 è il seguente:


alter table jpa08_hb_personne 
        drop 
        foreign key FKA44B1E555FE379D0;

    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A5CD852024;

    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A568C7A284;

    drop table if exists jpa08_hb_activite;

    drop table if exists jpa08_hb_adresse;

    drop table if exists jpa08_hb_personne;

    drop table if exists jpa08_hb_personne_activite;

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

    create table jpa08_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;

    alter table jpa08_hb_personne 
        add index FKA44B1E555FE379D0 (adresse_id), 
        add constraint FKA44B1E555FE379D0 
        foreign key (adresse_id) 
        references jpa08_hb_adresse (id);

    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A5CD852024 (ACTIVITE_ID), 
        add constraint FK5A6A55A5CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa08_hb_activite (id);

    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A568C7A284 (PERSONNE_ID), 
        add constraint FK5A6A55A568C7A284 
        foreign key (PERSONNE_ID) 
        references jpa08_hb_personne (id);

Questo codice DDL è analogo a quello ottenuto con la tabella di join esplicita e corrisponde allo schema già presentato:

2.6.5. InitDB

Non ci soffermeremo molto sulla classe [InitDB], identica alla versione precedente e che fornisce gli stessi risultati. Ci limiteremo a soffermarci sul codice seguente, che mostra la giunzione tra Personne e Activite:


        // visualizzazione persone/attività
        System.out.println("[personnes/activites]");
        Iterator iterator = em.createQuery("select p.id,a.id from Personne p join p.activites a").getResultList().iterator();
        while (iterator.hasNext()) {
            Object[] row = (Object[]) iterator.next();
            System.out.format("[%d,%d]%n", (Long) row[0], (Long) row[1]);
}
  • riga 3: il comando JPQL che esegue la join. Il risultato di select restituisce gli identificatori delle entità Personne e Activite collegate tra loro dalla tabella di join. L'elenco restituito da select è costituito da righe contenenti due oggetti di tipo Long. Per scorrere questo elenco, la riga 3 richiede un oggetto Iterator presente nell'elenco.
  • righe 4-7: utilizzando l’oggetto di tipo Iterator precedente, si scorre l’elenco.
    • riga 5: ogni elemento dell'elenco è un array contenente una riga risultante dal select
    • riga 6: si recuperano gli elementi della riga corrente risultante da select apportando le modifiche di tipo adeguate.

Il risultato di [InitDB] è il seguente:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
terminé...

2.6.6. Main

La classe [Main] esegue una serie di test, alcuni dei quali esamineremo.

2.6.6.1. Test3

Questo test è il seguente:


// eliminazione attività act1
    public static void test3() {
        // contesto di persistenza
        EntityManager em = getEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminazione dell'attività act1 da p2
        p2.getActivites().remove(act1);
        // si rimuove act1 dal contesto di persistenza
        em.remove(act1);
        // fine transazioni
        tx.commit();
        // vengono visualizzate le nuove tabelle
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • riga 11: l'attività act1 viene rimossa dal contesto di persistenza
  • riga 9: l'attività act1 fa parte delle attività dell'unica persona rimasta nel contesto, la persona p2. La riga 9 rimuove l'attività act1 dalle attività della persona p2. Lo facciamo per mantenere coerente il contesto di persistenza, poiché lo conserviamo per il seguito.

I risultati sono i seguenti:

main : ----------- test1
[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
main : ----------- test2
[personnes]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,1]
[2,3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,3]
  • l’attività act1, presente alla riga 26 in test2, è scomparsa dalle attività di test3 (righe 40-41)
  • la persona p2 aveva in test2 l’attività act1 (riga 33). Al termine di test3, non la possiede più (riga 47)

2.6.6.2. Test6

Questo test è il seguente:


// modifica delle attività di una persona
    public static void test6() {
        // contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // si recupera la persona p2
        p2 = em.find(Personne.class, p2.getId());
        // si recupera l'attività act2
        act2 = em.find(Activite.class, act2.getId());
        // p2 svolge ormai solo l'attività act2
        p2.getActivites().clear();
        p2.getActivites().add(act2);
        // fine transazione
        tx.commit();
        // vengono visualizzate le nuove tabelle
        dumpPersonne();
        dumpActivite();
        dumpPersonne_Activite();
    }
  • riga 4: si utilizza un contesto di persistenza nuovo e vuoto
  • riga 9: l’attività p2 viene trasferita dal database nel contesto di persistenza
  • riga 11: l'attività act2 viene trasferita dal database nel contesto di persistenza
  • riga 13: le attività della persona p2 (act3) vengono trasferite dal database nel contesto (fetchType.LAZY). È la chiamata [getActivites] a provocare questo caricamento. Si eliminano le attività di p2. Non si tratta di un’effettiva eliminazione delle attività (remove), ma di una modifica dello stato della persona p2. Non svolge più alcuna attività.
  • riga 14: si aggiunge alla persona p2 l’attività act2. Alla fine, l’insieme delle nuove attività della persona p2 è l’insieme {act2}.
  • riga 16: fine della transazione. La sincronizzazione esaminerà gli oggetti del contesto (p2, act2, act3) e rileverà che lo stato di p2 è cambiato. Verranno eseguiti gli ordini SQL che riflettono questa modifica nel database.
  • righe 18-20: vengono visualizzate tutte le tabelle

I risultati sono i seguenti:

main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation principale) :
act3
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2
main : ----------- test6
[personnes]
P[2,2,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes/activites]
[2,2]
  • Al termine del test 4, la persona p2 svolgeva l’attività act3 (riga 3).
  • Al termine del test 6 (riga 19), la persona p2 non svolge più l'attività act3 (riga 3) e svolge invece l'attività act2.

Ora utilizziamo un'implementazione JPA / Toplink:

Il progetto Eclipse con Toplink è una copia del progetto Eclipse con Hibernate:

Il file <persistence.xml> [2] è stato modificato in un punto, quello relativo alle entità dichiarate:


        <!--  provider -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- classi persistenti -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
...
  • righe 4-6: le entità gestite

L'esecuzione di [InitDB] con SGBD e MySQL5 fornisce i seguenti risultati:

In [1], l'output della console; in [2], le tabelle generate [jpa07_tl]; in [3], gli script generati SQL. Il loro contenuto è il seguente:

create.sql


CREATE TABLE jpa08_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa08_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa08_tl_activite (ID)
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa08_tl_personne (ID)
ALTER TABLE jpa08_tl_personne ADD CONSTRAINT FK_jpa08_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa08_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

L'esecuzione di [InitDB] e quella di [Main] avvengono senza errori.

2.6.8. Il progetto Eclipse / Hibernate 2

Creiamo un progetto Eclipse derivato dal precedente tramite copia:

In [1] si trova il progetto Eclipse, mentre in [2] i codici Java. Il progetto è presente in [3] nella cartella degli esempi [4]. Lo importeremo.

Modifichiamo la relazione che collega Personne a Activité nel modo seguente:

Persona


    // relazione Persona (molte) -> Attività (molte) tramite una tabella di join personne_activite
    // personne_activite(PERSONNE_ID) è una chiave estranea su Persona(id)
    // personne_activite(ACTIVITE_ID) è una chiave estranea su Attività(id)
    // nessuna cascata sulle attività
    // @ManyToMany(cascata={CascadeType.PERSIST})
    @ManyToMany()
    @JoinTable(name = "jpa09_hb_personne_activite", joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
  • riga 6: la relazione principale @ManyToMany non presenta più la cascata di persistenza Persona -> Attività (cfr. versione precedente, riga 5)

Attività


    // nessuna relazione inversa con Persona
    // @ManyToMany(mappedBy = "attività")
// private Set<Persona> persone = new HashSet<Persona>();
  • righe 2-3: la relazione inversa @ManyToMany Attività -> Persona è stata eliminata

Intendiamo dimostrare che gli attributi rimossi (cascata e relazione inversa) non sono indispensabili. La prima modifica introdotta da questa nuova configurazione si trova in [InitDB]:


        // associazioni persone <--> attività
        p1.getActivites().add(act1);
        p1.getActivites().add(act2);
        p2.getActivites().add(act1);
        p2.getActivites().add(act3);
        // persistenza delle attività
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // persistenza delle persone
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // e dell'indirizzo a4 non associato a una persona
em.persist(adr4);
  • righe 7-9: siamo obbligati a inserire esplicitamente le attività da act1 a act3 nel contesto di persistenza. Quando esisteva la cascata di persistenza Persona -> Attività, le righe 11-13 conservavano sia le persone da p1 a p3 sia le attività di queste persone da act1 a act3.

Una seconda modifica è visibile in [Main]:


    // recupero delle persone che svolgono una determinata attività
    public static void test5() {
        // contesto di persistenza
        EntityManager em = getNewEntityManager();
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // si richiedono le attività di p2
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites a where a.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        // fine transazione
        tx.commit();
}
  • righe 9-12: la query JPQL che recupera le persone che svolgono l’attività act3
  • nella versione precedente, lo stesso risultato era stato ottenuto anche tramite la relazione inversa Attività -> Persona, ora eliminata:

        // si passa attraverso la relazione inversa di act3
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (Personne p : act3.getPersonnes()) {
            System.out.println(p.getNom());
}

Stiamo realizzando un progetto Eclipse derivato dal precedente progetto Eclipse / Toplink tramite copia:

In [1] si trova il progetto Eclipse, mentre in [2] i codici Java. Il progetto è presente in [3] nella cartella degli esempi [4]. Lo importeremo.

I codici Java sono identici a quelli della versione Hibernate.

2.7. Esempio 7: utilizzo delle query denominate

Concludiamo questa lunga presentazione delle entità JPA, iniziata al paragrafo 2, con un ultimo esempio che illustra l’utilizzo di query JPQL esternalizzate in un file di configurazione. Questo esempio trae origine dalla seguente fonte:

[ref2]: "Getting started With JPA in Spring 2.0" di Mark Fisher all'URL

[http://blog.springframework.com/markf/archives/2006/05/30/getting-started-with-jpa-in-spring-20/].

2.7.1. Il database di esempio

Il database è il seguente:

  • in [1]: un elenco di ristoranti con nome e indirizzo
  • in [2]: la tabella degli indirizzi dei ristoranti, limitata al numero civico e al nome della via. Esiste una relazione uno-a-uno tra le tabelle restaurant e adresse: un ristorante ha un solo indirizzo.
  • in [3]: una tabella dei piatti con il loro nome e un indicatore vero/falso che specifica se il piatto è vegetariano o meno
  • in [4]: la tabella di join ristoranti/piatti: un ristorante serve più piatti e lo stesso piatto può essere servito da più ristoranti. Esiste una relazione molti-a-molti tra le tabelle restaurant e plat.

2.7.2. Gli oggetti @Entity che rappresentano il database

Le tabelle precedenti saranno rappresentate dalle seguenti @Entity:

  • l'@Entity Restaurant rappresenterà la tabella [restaurant]
  • l’@Entity Adresse rappresenterà la tabella [adresse]
  • l'@Entity Plat rappresenterà la tabella [plat]

Le relazioni tra queste entità sono le seguenti:

  • una relazione uno-a-uno collega l’entità Restaurant all’entità Adresse: un ristorante r ha un indirizzo a. L'entità Restaurant, che detiene la chiave esterna, avrà la relazione principale. L'entità Adresse non avrà una relazione inversa.
  • Una relazione molti-a-molti collega le entità Restaurant e Plat: un ristorante serve più piatti e lo stesso piatto può essere servito da più ristoranti. Questa relazione sarà rappresentata da un'annotazione @ManyToMany nell'entità Restaurant. L'entità Plat non avrà una relazione inversa.

L'@Entity Restaurant è la seguente:


package entites;

...
@Entity
@Table(name = "jpa10_hb_restaurant")
public class Restaurant implements java.io.Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

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

    @OneToOne(cascade = CascadeType.ALL)
    private Adresse adresse;

    @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
    @JoinTable(name = "jpa10_hb_restaurant_plat", inverseJoinColumns = @JoinColumn(name = "plat_id"))
    private Set<Plat> plats = new HashSet<Plat>();

    // costruttori
    public Restaurant() {

    }

    public Restaurant(String name, Adresse address, Set<Plat> entrees) {
...
    }

    // getter e setter
...

    // toString
    public String toString() {
        String signature = "R[" + getNom() + "," + getAdresse();
        for (Plat e : getPlats()) {
            signature += "," + e;
        }
        return signature + "]";
    }
}
  • riga 17: la relazione uno-a-uno che l’entità Restaurant ha con l’entità Adresse. Tutte le operazioni di persistenza su un ristorante vengono propagate al suo indirizzo.
  • riga 20: la relazione che collega l'@Entity Restaurant all'@Entity Plat dell'insieme plats della riga 22 è di tipo molti-a-molti (ManyToMany):
    • un ristorante (One) ha più piatti (Many)
    • un piatto (One) può essere servito da più ristoranti (Many)
    • in definitiva, le @Entity Restaurant e Plat sono collegate da una relazione ManyToMany. Decidiamo che l'@Entity Restaurant avrà la relazione principale e che l'@Entity Plat non avrà una relazione inversa.
    • La relazione @ManyToMany richiede una tabella di join. Questa viene definita tramite l'annotazione @JoinTable alla riga 47.
      • L'attributo name assegna un nome alla tabella.
      • La tabella di join è costituita dalle chiavi esterne delle tabelle che unisce. In questo caso, sono presenti due chiavi esterne: una sulla tabella [restaurant], l’altra sulla tabella [plat]. Queste colonne di chiave esterna sono definite dagli attributi joinColumns e inverseJoinColumns.
      • L'attributo joinColumns definisce la chiave esterna nella tabella dell'@Entity che detiene la relazione principale @ManyToMany, in questo caso la tabella [restaurant]. L'attributo joinColumns in questo caso è assente. JPA ha un valore predefinito in questo caso: [table]_[clé_primaire_de_table], in questo caso [jpa10_hb_restaurant_id].
      • L'annotazione @JoinColumn dell'attributo inverseJoinColumns definisce la chiave esterna sulla tabella dell'@Entity che contiene la relazione inversa @ManyToMany, in questo caso la tabella [plat]. Questa colonna chiave esterna si chiamerà plat_id.

L'@Entity Adresse è la seguente:


package entites;

...
@Entity
@Table(name="jpa10_hb_adresse")
public class Adresse implements java.io.Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;
  
  @Column(name = "NUMERO_RUE")
  private int numeroRue;
  
  @Column(name = "NOM_RUE", length=30, nullable=false)
  private String nomRue;
  
  // getter e setter
 ...
 
  // costruttori
  public Adresse(int streetNumber, String streetName){
...
  }
  
  public Adresse(){
    
  }
  
  // toString
  public String toString(){
    return "A["+getNumeroRue()+","+getNomRue()+"]";
  }
}
  • L'@Entity Adresse è un'entità senza relazioni dirette con le altre entità. Può essere salvata solo tramite un'entità Restaurant.
  • Un indirizzo è definito da un nome di via (riga 16) e da un numero civico (riga 13).

L'@Entity Plat è la seguente


package entites;
...
@Entity
@Table(name="jpa10_hb_plat")
public class Plat implements java.io.Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(unique=true, length=50, nullable=false)
    private String nom;

    private boolean vegetarien;

    // costruttori
    public Plat() {

    }

    public Plat(String name, boolean vegetarian) {
...
    }

    // getter e setter
...

    // toString
    public String toString() {
        return "E[" + getNom() + "," + isVegetarien() + "]";
    }

}
  • L'@Entity Plat è un'entità senza relazione diretta con le altre entità. Può essere salvata solo tramite l'entità Restaurant.
  • Un piatto è definito da un nome (riga 12) e da un tipo (vegetariano o meno) (riga 14).

2.7.3. Il progetto Eclipse / Hibernate

L'implementazione JPA qui utilizzata è quella di Hibernate. Il progetto Eclipse dei test è il seguente:

In [1] si trova il progetto Eclipse, in [2] i codici Java e in JPA la configurazione del livello. Si noti la presenza di un file [orm.xml] che non è mai stato incontrato prima. Il progetto è presente in [3] nella cartella degli esempi [4]. Lo importeremo.

2.7.4. Generazione del file DDL dal database

Seguendo le istruzioni del paragrafo 2.1.7, il file DDL ottenuto per i file SGBD e MySQL5 è il seguente:


alter table jpa10_hb_restaurant 
        drop 
        foreign key FK3E8E4F5D5FE379D0;

    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D11F0F78A4;

    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D1AFAC3E44;

    drop table if exists jpa10_hb_adresse;

    drop table if exists jpa10_hb_plat;

    drop table if exists jpa10_hb_restaurant;

    drop table if exists jpa10_hb_restaurant_plat;

    create table jpa10_hb_adresse (
        id bigint not null auto_increment,
        NUMERO_RUE integer,
        NOM_RUE varchar(30) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_plat (
        id bigint not null auto_increment,
        nom varchar(50) not null unique,
        vegetarien bit not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_restaurant (
        id bigint not null auto_increment,
        nom varchar(30) not null unique,
        adresse_id bigint,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_restaurant_plat (
        jpa10_hb_restaurant_id bigint not null,
        plat_id bigint not null,
        primary key (jpa10_hb_restaurant_id, plat_id)
    ) ENGINE=InnoDB;

    alter table jpa10_hb_restaurant 
        add index FK3E8E4F5D5FE379D0 (adresse_id), 
        add constraint FK3E8E4F5D5FE379D0 
        foreign key (adresse_id) 
        references jpa10_hb_adresse (id);

    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D11F0F78A4 (plat_id), 
        add constraint FK1D2D06D11F0F78A4 
        foreign key (plat_id) 
        references jpa10_hb_plat (id);

    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D1AFAC3E44 (jpa10_hb_restaurant_id), 
        add constraint FK1D2D06D1AFAC3E44 
        foreign key (jpa10_hb_restaurant_id) 
        references jpa10_hb_restaurant (id);
  • righe 21-26: la tabella [adresse]
  • righe 28-33: la tabella [plat]
  • righe 35-40: la tabella [restaurant]
  • righe 42-46: la tabella di join [restaurant_plat]. Si noti la chiave composita (riga 45)
  • righe 48-52: la chiave esterna dalla tabella [restaurant] alla tabella [adresse]
  • righe 54-58: la chiave esterna dalla tabella [restaurant_plat] alla tabella [plat]
  • righe 60-64: la chiave esterna dalla tabella [restaurant_plat] alla tabella [restaurant]

Questo DDL corrisponde allo schema già presentato:

Nella vista SQL Explorer, il database si presenta come segue:

  • in [1]: le 4 tabelle del database
  • in [2]: gli indirizzi
  • in [3]: i piatti
  • in [4]: i ristoranti. [adresse_id] fa riferimento agli indirizzi di [2].
  • in [5]: la tabella di join [restaurant,plat]. [jpa10_hb_restaurant_id] fa riferimento ai ristoranti di [4] e [plat_id] ai piatti di [3]. Pertanto, [1,1] significa che il ristorante "Burger Barn" serve il piatto "CheeseBurger".

Per ottenere i dati sopra riportati, è stato eseguito il programma [QueryDB] del progetto Eclipse.

2.7.5. Richieste JPQL con una console Hibernate

Creiamo una console Hibernate collegata al precedente progetto Eclipse. Seguiremo la procedura già illustrata due volte, in particolare al paragrafo 2.1.12.

  • in [1] e [2]: la configurazione della console Hibernate
  • in [3]: una query JPQL e in [4] il risultato.
  • in [5]: l’ordine equivalente SQL

Presentiamo ora una serie di query JPQL. Il lettore è invitato a provarle e a scoprire l'ordine SQL generato da Hibernate per eseguirle.

Ottenere tutti i ristoranti con i relativi piatti:

Ottenere i ristoranti che servono almeno un piatto vegetariano:

Ottenere i nomi dei ristoranti che servono solo piatti vegetariani:

Ottenere i ristoranti che servono hamburger:

2.7.6. QueryDB

Passiamo ora al programma [QueryDB] del progetto Eclipse che:

  • popola il database
  • e invia ad essa una serie di richieste JPQL. Queste vengono registrate nel file [META-INF/orm.xml] del progetto Eclipse:

Il file [orm.xml] può essere utilizzato per configurare il livello JPA al posto delle annotazioni Java. Ciò garantisce una maggiore flessibilità nella configurazione del livello JPA. È possibile modificarla senza ricompilare il codice Java. È possibile utilizzare entrambi i metodi contemporaneamente: annotazioni Java e file [orm.xml]. La configurazione JPA viene inizialmente effettuata con le annotazioni Java e successivamente con il file [orm.xml]. Se quindi si desidera modificare una configurazione creata tramite un'annotazione Java senza ricompilare, è sufficiente inserire tale configurazione nel file [orm.xml]. Sarà quest'ultimo ad avere la precedenza.

Nel nostro esempio, il file [orm.xml] viene utilizzato per salvare i testi delle query di JPQL. Il suo contenuto è il seguente:


<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0">
    <description>Restaurants</description>
    <named-query name="supprimer le contenu de la table restaurant">
        <query>delete from Restaurant</query>
    </named-query>
    <named-query name="supprimer le contenu de la table plat">
        <query>delete from Plat</query>
    </named-query>
    <named-query name="obtenir tous les restaurants">
        <query>select r from Restaurant r order by r.nom asc</query>
    </named-query>
    <named-query name="obtenir toutes les adresses">
        <query>select a from Adresse a order by a.nomRue asc</query>
    </named-query>
    <named-query name="obtenir tous les plats">
        <query>select p from Plat p order by p.nom asc</query>
    </named-query>
    <named-query name="obtenir tous les restaurants avec leurs plats">
        <query>select r.nom,p.nom from Restaurant r join r.plats p</query>
    </named-query>
    <named-query name="obtenir les restaurants ayant au moins un plat vegetarien">
        <query>select distinct r from Restaurant r join r.plats p where p.vegetarien=true</query>
    </named-query>
    <named-query name="obtenir les restaurants avec uniquement des plats vegetariens">
        <query>
            select distinct r1.nom from Restaurant r1 where not exists (select p1 from Restaurant r2 join r2.plats p1 where r2.id=r1.id and
            p1.vegetarien=false)
        </query>
    </named-query>
    <named-query name="obtenir les restaurants d'une certaine rue">
        <query>select r from Restaurant r where r.adresse.nomRue=:nomRue</query>
    </named-query>
    <named-query name="obtenir les restaurants qui servent des burgers">
        <query>select r.nom,r.adresse.numeroRue, r.adresse.nomRue, p.nom from Restaurant r join r.plats p where p.nom like '%burger'</query>
    </named-query>
    <named-query name="obtenir les plats du restaurant untel">
        <query>select p.nom from Restaurant r join r.plats p where r.nom=:nomRestaurant</query>
    </named-query>
</entity-mappings>
  • la radice del file [orm.xml] è <entity-mappings> (riga 2).
  • righe 5-7: le query JPQL specificate sono racchiuse nei tag <named-query name= "... ">testo</namedquery>.
    • L'attributo name del tag è il nome della query.
    • Il contenuto texte del tag è il testo della query.

QueryDB eseguirà le query precedenti. Il suo codice è il seguente:


package tests;

...
public class QueryDB {

    // Contesto di persistenza
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = emf.createEntityManager();

    public static void main(String[] args) {
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminare gli elementi dalla tabella [restaurant]
        em.createNamedQuery("supprimer le contenu de la table restaurant").executeUpdate();
        // eliminare gli elementi dalla tabella [plat]
        em.createNamedQuery("supprimer le contenu de la table plat").executeUpdate();
        // creazione di oggetti Address
        Adresse adr1 = new Adresse(10, "Main Street");
        Adresse adr2 = new Adresse(20, "Main Street");
        Adresse adr3 = new Adresse(123, "Dover Street");
        // Creazione di oggetti «Entree»
        Plat ent1 = new Plat("Hamburger", false);
        Plat ent2 = new Plat("Cheeseburger", false);
        Plat ent3 = new Plat("Tofu Stir Fry", true);
        Plat ent4 = new Plat("Vegetable Soup", true);
        // creazione di oggetti Ristorante
        Restaurant restaurant1 = new Restaurant();
        restaurant1.setNom("Burger Barn");
        restaurant1.setAdresse(adr1);
        restaurant1.getPlats().add(ent1);
        restaurant1.getPlats().add(ent2);
        Restaurant restaurant2 = new Restaurant();
        restaurant2.setNom("Veggie Village");
        restaurant2.setAdresse(adr2);
        restaurant2.getPlats().add(ent3);
        restaurant2.getPlats().add(ent4);
        Restaurant restaurant3 = new Restaurant();
        restaurant3.setNom("Dover Diner");
        restaurant3.setAdresse(adr3);
        restaurant3.getPlats().add(ent1);
        restaurant3.getPlats().add(ent2);
        restaurant3.getPlats().add(ent4);
        // persistenza degli oggetti Ristorante (e degli altri oggetti a cascata)
        em.persist(restaurant1);
        em.persist(restaurant2);
        em.persist(restaurant3);
        // fine transazione
        tx.commit();
        // dump del database
        dumpDataBase();
        // fine EntityManager
        em.close();
        // fine EntityManagerFactory
        emf.close();
    }

    // visualizzazione del contenuto del database
    @SuppressWarnings("unchecked")
    private static void dumpDataBase() {
        // test2
        log("données de la base");
        // inizio transazione
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // visualizzazione ristoranti
        log("[restaurants]");
        for (Object restaurant : em.createNamedQuery("obtenir tous les restaurants").getResultList()) {
            System.out.println(restaurant);
        }
        // visualizzazioni indirizzi
        log("[adresses]");
        for (Object adresse : em.createNamedQuery("obtenir toutes les adresses").getResultList()) {
            System.out.println(adresse);
        }
        // visualizzazioni dei piatti
        log("[plats]");
        for (Object plat : em.createNamedQuery("obtenir tous les plats").getResultList()) {
            System.out.println(plat);
        }
        // visualizzazioni collegamenti ristoranti <--> piatti
        log("[restaurants/plats]");
        Iterator record = em.createNamedQuery("obtenir tous les restaurants avec leurs plats").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%s]%n", currentRecord[0], currentRecord[1]);
        }
        log("[Liste des restaurants avec au moins un plat végétarien]");
        for (Object r : em.createNamedQuery("obtenir les restaurants ayant au moins un plat vegetarien").getResultList()) {
            System.out.println(r);
        }
        // query
        log("[Liste des restaurants avec seulement des plats végétariens]");
        for (Object r : em.createNamedQuery("obtenir les restaurants avec uniquement des plats vegetariens").getResultList()) {
            System.out.println(r);
        }
        // query
        log("[Liste des restaurants dans Dover Street]");
        for (Object r : em.createNamedQuery("obtenir les restaurants d'une certaine rue").setParameter("nomRue", "Dover Street").getResultList()) {
            System.out.println(r);
        }
        // query
        log("[Liste des restaurants ayant un plat de type burger]");
        record = em.createNamedQuery("obtenir les restaurants qui servent des burgers").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%d,%s,%s]%n", currentRecord[0], currentRecord[1], currentRecord[2], currentRecord[3]);
        }
        // query
        log("[Plats de Veggie Village]");
        for (Object r : em.createNamedQuery("obtenir les plats du restaurant untel").setParameter("nomRestaurant", "Veggie Village").getResultList()) {
            System.out.println(r);
        }
        // fine transazione
        tx.commit();
    }

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

}

Il risultato dell'esecuzione di [QueryDB] è il seguente:

-----------données de la base
 -----------[restaurants]
R[Burger Barn,A[10,Main Street],E[Cheeseburger,false],E[Hamburger,false]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
 -----------[adresses]
A[123,Dover Street]
A[10,Main Street]
A[20,Main Street]
 -----------[plats]
E[Cheeseburger,false]
E[Hamburger,false]
E[Tofu Stir Fry,true]
E[Vegetable Soup,true]
 -----------[restaurants/plats]
[Burger Barn,Cheeseburger]
[Burger Barn,Hamburger]
[Dover Diner,Cheeseburger]
[Dover Diner,Hamburger]
[Dover Diner,Vegetable Soup]
[Veggie Village,Tofu Stir Fry]
[Veggie Village,Vegetable Soup]
 -----------[Liste des restaurants avec au moins un plat végétarien]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants avec seulement des plats végétariens]
Veggie Village
 -----------[Liste des restaurants dans Dover Street]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants ayant un plat de type burger]
[Burger Barn,10,Main Street,Cheeseburger]
[Burger Barn,10,Main Street,Hamburger]
[Dover Diner,123,Dover Street,Cheeseburger]
[Dover Diner,123,Dover Street,Hamburger]
 -----------[Plats de Veggie Village]
Tofu Stir Fry
Vegetable Soup

Lasciamo al lettore il compito di collegare il codice ai risultati. A tal fine, consigliamo di eseguire le query JPQL nella console Hibernate e di esaminare il codice SQL corrispondente.

Il lettore interessato troverà negli esempi scaricabili insieme a questo tutorial il progetto precedente implementato con Toplink:

Il progetto Eclipse con Toplink è una copia del progetto Eclipse con Hibernate:

Il file <persistence.xml> [2] dichiara le entità gestite:


        <!--  provider -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
            <!-- classi persistenti -->
        <class>entites.Restaurant</class>
        <class>entites.Adresse</class>
        <class>entites.Plat</class>

...
  • righe 4-6: le entità gestite

Le query JPQL salvate in [orm.xml] vengono eseguite correttamente da Toplink. A tal fine, nel progetto precedente ci si era assicurati di non utilizzare le query HQL (Hibernate Query Language), che costituiscono di fatto un superset di JPQL e la cui sintassi, in alcuni casi, non è accettata da JPQL.

2.8. Conclusion

Concludiamo qui il nostro studio sulle entità JPA. È stato un percorso lungo e tuttavia alcuni aspetti importanti (per lo sviluppatore esperto) non sono stati trattati. Ancora una volta, si consiglia di leggere un libro di riferimento come quello utilizzato per questo tutorial:

[ref1]: Java Persistence with Hibernate, di Christian Bauer e Gavin King, edito da Manning.