4. JPA : in sintesi
Ci proponiamo di introdurre JPA (Java Persistence API) con alcuni esempi. JPA viene approfondito nel corso:
- Persistenza Java 5 nella pratica: [http://tahe.developpez.com/java/jpa] - fornisce gli strumenti per costruire il livello di accesso ai dati con JPA
4.1. Il ruolo di JPA in un’architettura a livelli
Si invita il lettore a rileggere l’inizio di questo documento (paragrafo 2), che spiega il ruolo del livello JPA in un’architettura a livelli. Il livello JPA si inserisce nei livelli di accesso ai dati:
![]() |
Il livello [DAO] interagisce con la specifica JPA. Indipendentemente dal prodotto che la implementa, l’interfaccia del livello JPA presentata al livello [DAO] rimane la stessa. Di seguito presentiamo alcuni esempi tratti da [ref1] che ci consentiranno di costruire il nostro livello JPA.
4.2. JPA - esempi
4.2.1. Esempio 1 - Rappresentazione a oggetti di una singola tabella
4.2.1.1. La tabella [personne]
Consideriamo un database con un’unica tabella [personne], il cui scopo è quello di memorizzare alcune informazioni relative a persone:
![]() |
chiave primaria della tabella | |
versione della riga nella tabella. Ogni volta che i dati della persona vengono modificati, il suo numero di versione viene incrementato. | |
nome della persona | |
il suo nome | |
la sua data di nascita | |
numero intero 0 (non sposato) o 1 (sposato) | |
numero di figli della persona |
4.2.1.2. L'entità [Personne]
Ci troviamo nel seguente ambiente di esecuzione:
![]() |
Il livello JPA [5] deve fungere da ponte tra il mondo relazionale del database [7] e il mondo degli oggetti [4] gestito dai programmi Java [3]. Questo collegamento si effettua tramite configurazione e vi sono due modi per farlo:
- utilizzando i file XML. Questo era praticamente l’unico modo per farlo fino all’avvento di JDK 1.5
- con le annotazioni Java a partire dalla versione 1.5 di JDK
In questo documento utilizzeremo esclusivamente il secondo metodo.
L’oggetto [Personne] che rappresenta la tabella [personne] presentata in precedenza potrebbe essere il seguente:
...
@SuppressWarnings("unused")
@Entity
@Table(name="Personne")
public class Personne implements Serializable{
@Id
@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name = "VERSION", nullable = false)
@Version
private int version;
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
@Column(name = "PRENOM", length = 30, nullable = false)
private String prenom;
@Column(name = "DATENAISSANCE", nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(name = "MARIE", nullable = false)
private boolean marie;
@Column(name = "NBENFANTS", nullable = false)
private int nbenfants;
// costruttori
public Personne() {
}
public Personne(String nom, String prenom, Date datenaissance, boolean marie,
int nbenfants) {
setNom(nom);
setPrenom(prenom);
setDatenaissance(datenaissance);
setMarie(marie);
setNbenfants(nbenfants);
}
// toString
public String toString() {
...
}
// getter e setter
...
}
La configurazione avviene tramite le annotazioni Java @Annotation. Le annotazioni Java vengono interpretate dal compilatore oppure da strumenti specializzati al momento dell’esecuzione. Ad eccezione dell’annotazione alla riga 3, destinata al compilatore, tutte le altre annotazioni sono qui destinate all’implementazione JPA utilizzata, Hibernate o Toplink. Verranno quindi interpretate al momento dell’esecuzione. In assenza di strumenti in grado di interpretarle, queste annotazioni vengono ignorate. Pertanto, la classe [Personne] sopra riportata potrebbe essere utilizzata in un contesto esterno a JPA.
Occorre distinguere due casi di utilizzo delle annotazioni JPA in una classe C associata a una tabella T:
- 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, ...)
- la tabella T non esiste e verrà creata in base alle annotazioni presenti nella classe C.
Il caso 2 è il più semplice da gestire. Con l’aiuto delle annotazioni JPA, indichiamo la struttura della tabella T che desideriamo. Il caso 1 è spesso più complesso. La tabella T potrebbe essere stata creata, molto tempo fa, al di fuori di qualsiasi contesto JPA. La sua struttura potrebbe quindi risultare poco adatta al ponte relazionale/oggetto di JPA. Per semplificare, consideriamo il caso 2, in cui la tabella T associata alla classe C verrà creata in base alle annotazioni JPA della classe C.
Commentiamo le annotazioni JPA della classe [Personne]:
- riga 4: l’annotazione @Entity è la prima annotazione indispensabile. Va inserita prima della riga che dichiara la classe e indica che la classe in questione deve essere gestita dal livello di persistenza JPA. In assenza di questa annotazione, tutte le altre annotazioni JPA verrebbero ignorate.
- riga 5: l'annotazione @Table indica la tabella del database di cui la classe è una rappresentazione. Il suo argomento principale è name, che indica il nome della tabella. In assenza di questo argomento, la tabella porterà il nome della classe, in questo caso [Personne]. Nel nostro esempio, l'annotazione @Table è quindi superflua.
- riga 8: l'annotazione @Id serve a indicare il campo della classe che rappresenta la chiave primaria della tabella. Questa annotazione è obbligatoria. In questo caso indica che il campo id della riga 11 rappresenta la chiave primaria della tabella.
- riga 9: l'annotazione @Column serve a collegare un campo della classe alla colonna della tabella di cui il campo è l'immagine. L'attributo name indica il nome della colonna nella tabella. In assenza di questo attributo, la colonna porta lo stesso nome del campo. Nel nostro esempio, l'argomento name non era quindi obbligatorio. L’argomento nullable=false indica che la colonna associata al campo non può assumere il valore NULL e che, pertanto, il campo deve necessariamente avere un valore.
- riga 10: l’annotazione @GeneratedValue indica come viene generata la chiave primaria quando viene generata automaticamente dal SGBD. Questo sarà il caso in tutti i nostri esempi. Non è obbligatorio. Pertanto, il nostro soggetto potrebbe avere un numero di matricola che fungerebbe da chiave primaria e che non sarebbe generato da SGBD, ma fissato dall’applicazione. In questo caso, l’annotazione @GeneratedValue sarebbe assente. L’argomento strategy indica come viene generata la chiave primaria quando è generata da SGBD. I SGBD non utilizzano tutti la stessa tecnica di generazione dei valori della chiave primaria. Ad esempio:
utilizza un generatore di valori chiamato prima di ogni inserimento | |
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. | |
utilizza un oggetto denominato SEQUENCE che, anche in questo caso, svolge la funzione di generatore di valori |
Il livello JPA deve generare comandi SQL diversi a seconda dei SGBD per creare il generatore di valori. Tramite la configurazione le viene indicato il tipo di SGBD che deve gestire. Di conseguenza, può determinare quale sia la strategia abituale di generazione dei valori della chiave primaria di quel SGBD. L'argomento strategy = GenerationType.AUTO indica al livello JPA che deve utilizzare questa strategia abituale. Questa tecnica ha funzionato in tutti gli esempi di questo documento per i sette SGBD utilizzati.
- riga 14: l’annotazione @Version indica il campo utilizzato per gestire gli accessi concorrenti a una stessa riga della tabella.
Per comprendere questo problema di accesso concorrente a una stessa riga della tabella [personne], supponiamo che un’applicazione web consenta l’aggiornamento di una persona ed esaminiamo il seguente caso:
Al momento T1, un utente U1 accede alla modifica di una persona P. In quel momento, il numero di figli è 0. L’utente modifica tale numero portandolo a 1, ma prima che possa confermare la modifica, un altro utente, U2, avvia la modifica della stessa persona P. Poiché U1 non ha ancora confermato la propria modifica, U2 vede sullo schermo il numero di figli pari a 0. U2 scrive il nome della persona P in maiuscolo. Successivamente, U1 e U2 confermano le loro modifiche in quest’ordine. Sarà la modifica di U2 a prevalere: nel database, il nome verrà scritto in maiuscolo e il numero di figli rimarrà pari a zero, anche se U1 crede di averlo modificato in 1.
Il concetto di versione della persona ci aiuta a risolvere questo problema. Riprendiamo lo stesso caso d’uso:
Al momento T1, un utente U1 accede alla modifica di una persona P. In quel momento, il numero di figli è 0 e la versione è V1. L’utente porta il numero di figli a 1, ma prima che possa confermare la modifica, un altro utente U2 accede alla modifica della stessa persona P. Poiché U1 non ha ancora confermato la propria modifica, U2 vede il numero di figli pari a 0 e la versione V1. U2 scrive il nome della persona P in maiuscolo. Successivamente, U1 e U2 convalidano le proprie modifiche in questo ordine. Prima di convalidare una modifica, si verifica che chi modifica una persona P possieda la stessa versione della persona P attualmente registrata. Questo sarà il caso dell’utente U1. La sua modifica viene quindi accettata e si cambia la versione della persona modificata da V1 a V2 per segnalare che la persona ha subito una modifica. Al momento della convalida della modifica di U2, ci si accorgerà che U2 possiede una versione V1 della persona P, mentre attualmente la versione di quest’ultima è V2. A questo punto potremo comunicare all’utente U2 che qualcuno lo ha preceduto e che deve ripartire dalla nuova versione della persona P. Egli lo farà, recupererà una persona P della versione V2 che ora ha un figlio, scriverà il nome in maiuscolo e confermerà. La sua modifica sarà accettata se la persona P registrata ha ancora la versione V2. Alla fine, le modifiche apportate da U1 e U2 saranno prese in considerazione, mentre nel caso d’uso senza versione, una delle modifiche andava persa.
Il livello [DAO] dell’applicazione client può gestire autonomamente la versione della classe [Personne]. Ogni volta che verrà apportata una modifica a un oggetto P, la versione di tale oggetto verrà incrementata di 1 nella tabella. L’annotazione @Version consente di trasferire questa gestione al livello JPA. Il campo in questione non deve necessariamente chiamarsi version come nell’esempio. Può avere un nome qualsiasi.
I campi corrispondenti alle annotazioni @Id e @Version sono presenti per motivi di persistenza. Non sarebbero necessari se la classe [Personne] non dovesse essere persistita. Si vede quindi che un oggetto non ha la stessa rappresentazione a seconda che debba o meno essere persistito.
- riga 17: ancora una volta l’annotazione @Column per fornire informazioni sulla colonna della tabella [personne] associata al campo nom della classe Personne. Qui si trovano due nuovi argomenti:
- unique=true indica che il nome di una persona deve essere univoco. Ciò si tradurrà nel database con l’aggiunta di un vincolo di unicità sulla colonna NOM della tabella [personne].
- length=30 imposta a 30 il numero di caratteri della colonna NOM. Ciò significa che il tipo di questa colonna sarà VARCHAR(30).
- riga 24: l'annotazione @Temporal serve a indicare quale tipo SQL assegnare a una colonna/campo di tipo data/ora. Il tipo TemporalType.DATE indica una sola data senza ora associata. Gli altri tipi possibili sono TemporalType.TIME per codificare un'ora e TemporalType.TIMESTAMP per codificare una data con ora.
Commentiamo ora il resto del codice della classe [Personne]:
- riga 6: la classe implementa l'interfaccia Serializable. La sérialisation di un oggetto consiste nel trasformarlo in una sequenza di bit. La désérialisation è l'operazione inversa. La serializzazione/deserializzazione viene utilizzata in particolare nelle applicazioni client/server in cui gli oggetti vengono scambiati tramite la rete. Le applicazioni client o server non sono a conoscenza di questa operazione, che viene eseguita in modo trasparente dai JVM. Affinché ciò sia possibile, è tuttavia necessario che le classi degli oggetti scambiati siano "contrassegnate" con la parola chiave Serializable.
- riga 37: un costruttore della classe. Si noti che i campi id e version non fanno parte dei parametri. Infatti, questi due campi sono gestiti dal livello JPA e non dall’applicazione.
- righe 51 e successive: i metodi get e set di ciascuno dei campi della classe. Si noti che le annotazioni JPA possono essere applicate ai metodi get dei campi anziché ai campi stessi. La posizione delle annotazioni indica la modalità che JPA deve utilizzare per accedere ai campi:
- se le annotazioni sono applicate a livello di campo, JPA accederà direttamente ai campi per leggerli o scriverli
- se le annotazioni sono applicate a livello di get, JPA accederà ai campi tramite i metodi get/set per leggerli o scriverli
È la posizione dell’annotazione @Id che determina la posizione delle annotazioni JPA in una classe. Se posizionata a livello di campo, indica un accesso diretto ai campi; se posizionata a livello di get, indica un accesso ai campi tramite i metodi get e set. Le altre annotazioni devono quindi essere posizionate allo stesso modo dell’annotazione @Id.
4.2.2. Configurazione del livello JPA
I test del livello JPA possono essere effettuati con la seguente architettura:
![]() |
- in [7]: il database che verrà generato a partire dalle annotazioni dell’entità [Personne] e dalle configurazioni aggiuntive definite in un file denominato [persistence.xml]
- in [5, 6]: un livello JPA implementato da Hibernate
- in [4]: l’entità [Personne]
- in [3]: un programma di test in modalità console
La configurazione del livello JPA è gestita dal file [META-INF/persistence.xml]:
![]() |
All'esecuzione, il file [META-INF/persistence.xml] viene cercato nel file Classpath dell'applicazione.
Esaminiamo la configurazione del livello JPA definita nel file [persistence.xml] del nostro progetto:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- provider -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<!-- Classi persistenti -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- log SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- connessione JDBC -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
<property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
<!-- creazione automatica dello schema -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- Dialetto -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<!-- proprietà DataSource c3p0 -->
<property name="hibernate.c3p0.min_size" value="5" />
<property name="hibernate.c3p0.max_size" value="20" />
<property name="hibernate.c3p0.timeout" value="300" />
<property name="hibernate.c3p0.max_statements" value="50" />
<property name="hibernate.c3p0.idle_test_period" value="3000" />
</properties>
</persistence-unit>
</persistence>
Per comprendere questa configurazione, dobbiamo tornare sull’architettura di accesso ai dati della nostra applicazione:
![]() |
- il file [persistence.xml] configurerà i livelli [4, 5, 6]
- [4]: implementazione Hibernate di JPA
- [5]: Hibernate accede al database tramite un pool di connessioni. Un pool di connessioni è una riserva di connessioni aperte con SGBD. Un SGBD è utilizzato da più utenti, mentre per motivi di prestazioni non può superare un numero limite N di connessioni aperte contemporaneamente. Un codice ben scritto apre una connessione con il SGBD per un tempo minimo: invia i comandi SQL e chiude la connessione. Lo farà ripetutamente, ogni volta che avrà bisogno di lavorare con il database. Il costo dell’apertura e della chiusura di una connessione non è trascurabile ed è qui che entra in gioco il pool di connessioni. All’avvio dell’applicazione, il pool aprirà N1 connessioni con il SGBD. È a questo che l’applicazione richiederà una connessione aperta quando ne avrà bisogno. La connessione verrà restituita al pool non appena l’applicazione non ne avrà più bisogno, preferibilmente il più rapidamente possibile. La connessione non viene chiusa e rimane disponibile per l’utente successivo. Un pool di connessioni è quindi un sistema di condivisione delle connessioni aperte.
- [6]: il driver JDBC utilizzato dal SGBD
Ora vediamo come il file [persistence.xml] configura i livelli [4, 5, 6] sopra indicati:
- riga 2: il tag radice del file XML è <persistence>.
- riga 3: <persistence-unit> serve a definire un’unità di persistenza. Possono esserci più unità di persistenza. Ciascuna di esse ha un nome (attributo name) e un tipo di transazione (attributo transaction-type). L’applicazione avrà accesso all’unità di persistenza tramite il suo nome, in questo caso jpa. Il tipo di transazione RESOURCE_LOCAL indica che l’applicazione gestisce autonomamente le transazioni con il SGBD. Questo sarà il caso in questione. Quando l’applicazione viene eseguita in un contenitore EJB3, può utilizzare il servizio di transazioni di quest’ultimo. In questo caso, si imposterà transaction-type=JTA (Java Transaction API). JTA è il valore predefinito quando l’attributo transaction-type è assente.
- riga 5: il tag <provider> serve a definire una classe che implementa l'interfaccia [javax.persistence.spi.PersistenceProvider], interfaccia che consente all'applicazione di inizializzare il livello di persistenza. Poiché si utilizza un'implementazione JPA / Hibernate, la classe qui utilizzata è una classe di Hibernate.
- riga 6: il tag <properties> introduce proprietà specifiche del particolare provider scelto. Pertanto, a seconda che si sia scelto Hibernate, Toplink, Kodo, ... si avranno proprietà diverse. Quelle che seguono sono specifiche di Hibernate.
- riga 8: richiede a Hibernate di esplorare il classpath del progetto per individuare le classi con l’annotazione @Entity al fine di gestirle. Le classi @Entity possono anche essere dichiarate tramite i tag <class>nom_de_la_classe</class>, direttamente sotto il tag <persistence-unit>. È ciò che faremo con il provider JPA / Toplink.
- Le righe 10-12, qui messe in commento, configurano i log della console di Hibernate:
- riga 10: per decidere se visualizzare o meno i comandi SQL emessi da Hibernate sul SGBD. Ciò è molto utile durante la fase di apprendimento. A causa del ponte relazionale/oggetto, l’applicazione opera su oggetti persistenti ai quali applica operazioni di tipo [persist, merge, remove]. È molto interessante sapere quali sono i comandi SQL effettivamente emessi durante queste operazioni. Studiandoli, a poco a poco si arriva a intuire i comandi SQL che Hibernate genererà quando si esegue una determinata operazione sugli oggetti persistenti e il ponte relazionale/oggetto comincia a prendere forma nella mente.
- riga 11: i comandi SQL visualizzati sulla console possono essere formattati in modo chiaro per facilitarne la lettura
- riga 12: i comandi SQL visualizzati saranno inoltre commentati
- le righe 15-19 definiscono il livello JDBC (livello [6] nell’architettura):
- riga 15: la classe del driver JDBC del SGBD, in questo caso MySQL5
- riga 16: l'URL del database utilizzato
- righe 17, 18: l'utente della connessione e la sua password
- riga 22: Hibernate deve conoscere il SGBD con cui sta interagendo. Infatti, i SGBD hanno tutti estensioni SQL proprietarie, un modo specifico per gestire la generazione automatica dei valori di una chiave primaria, ... il che fa sì che Hibernate debba conoscere il SGBD con cui sta lavorando per potergli inviare i comandi SQL che quest’ultimo comprenderà. [MySQL5InnoDBDialect] indica il SGBD MySQL5 con tabelle di tipo InnoDB che supportano le transazioni.
- le righe 24-28 configurano il pool di connessioni c3p0 (livello [5] nell'architettura):
- righe 24, 25: il numero minimo (predefinito 3) e massimo di connessioni (predefinito 15) nel pool. Il numero iniziale predefinito di connessioni è 3.
- riga 26: tempo massimo di attesa, in millisecondi, per una richiesta di connessione da parte del client. Trascorso questo tempo, c3p0 restituirà un'eccezione.
- riga 27: per accedere a BD, Hibernate utilizza comandi SQL preparati (PreparedStatement) che c3p0 può memorizzare nella cache. Ciò significa che se l’applicazione richiede una seconda volta un comando SQL preparato già presente nella cache, questo non dovrà essere preparato (la preparazione di un comando SQL ha un costo) e verrà utilizzato quello presente nella cache. Qui si indica il numero massimo di comandi SQL preparati che la cache può contenere, considerando tutte le connessioni (un comando SQL preparato appartiene a una connessione).
- riga 28: frequenza di verifica, in millisecondi, della validità delle connessioni. Una connessione del pool può diventare non valida per vari motivi (il driver JDBC invalida la connessione perché è troppo lunga, il driver JDBC presenta dei "bug", ...).
- riga 20: qui si richiede che, all’inizializzazione dell’unità di persistenza, venga generata la struttura del database degli oggetti @Entity. Hibernate dispone ora di tutti gli strumenti per emettere i comandi SQL di generazione delle tabelle del database:
- la configurazione degli oggetti @Entity gli consente di individuare le tabelle da generare
- le righe 15-18 e 24-28 gli consentono di stabilire una connessione con il SGBD
- la riga 22 gli permette di sapere quale dialetto SQL utilizzare per generare le tabelle
Pertanto, il file [persistence.xml] qui utilizzato ricrea un nuovo database ad ogni nuova esecuzione dell’applicazione. Le tabelle vengono ricreate (create table) dopo essere state eliminate (drop table), qualora esistessero. Si noti che ovviamente questa operazione non deve essere eseguita su un database in produzione...
4.2.3. Esempio 2: relazione uno-a-molti
4.2.3.1. o dello schema del database
1 ![]() | 2 |
- in [1], il database, e in [2], il suo DDL (MySQL5)
Un articolo A(id, versione, nome) appartiene esattamente a una categoria C(id, versione, nome). Una categoria C può contenere 0, 1 o più articoli. Si ha una relazione uno-a-molti (Categoria -> Articolo) e la relazione inversa molti-a-uno (Articolo -> Categoria). Questa relazione è rappresentata dalla chiave esterna che la tabella [article] possiede sulla tabella [categorie] (righe 24-28 della DDL).
4.2.3.2. Gli oggetti @Entity che rappresentano il database
Un articolo è rappresentato dalla seguente @Entity [Article]:
package entites;
...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {
// campi
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@SuppressWarnings("unused")
@Version
private int version;
@Column(length = 30)
private String nom;
// relazione principale Articolo (molti) -> Categoria (uno)
// implementata tramite una chiave esterna (categorie_id) in Articolo
// Ogni Articolo ha necessariamente una Categoria (nullable=false)
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "categorie_id", nullable = false)
private Categorie categorie;
// costruttori
public Article() {
}
// getter e setter
...
// toString
public String toString() {
return String.format("Article[%d,%d,%s,%d]", id, version, nom, categorie.getId());
}
}
- righe 9-11: chiave primaria dell'@Entity
- righe 13-15: il suo numero di versione
- righe 17-18: nome dell'articolo
- righe 20-25: relazione molti-a-uno che collega l'@Entity Article all'@Entity Categorie:
- riga 23: l'annotazione ManyToOne. Il «Many» si riferisce all’@Entity Article in cui ci troviamo e il «One» all’@Entity Categorie (riga 25). Una categoria (One) può avere più articoli (Many).
- riga 24: l'annotazione ManyToOne definisce la colonna chiave esterna nella tabella [article]. Si chiamerà (name) categorie_id e ogni riga dovrà avere un valore in questa colonna (nullable=false).
- riga 25: la categoria a cui appartiene l’articolo. Quando un articolo verrà inserito nel contesto di persistenza, si richiede che la sua categoria non vi venga inserita immediatamente (fetch=FetchType.LAZY, riga 23). Non si sa se questa richiesta abbia senso. Vedremo.
Una categoria è rappresentata dalla seguente @Entity [Categorie]:
package entites;
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {
// campi
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@SuppressWarnings("unused")
@Version
private int version;
@Column(length = 30)
private String nom;
// relazione inversa Categoria (uno) -> Articolo (molti) della relazione Articolo (molti) -> Categoria (uno)
// inserimento a cascata da Categoria -> inserimento di Articoli
// aggiornamento a cascata Categoria -> aggiornamento Articoli
// cascata di eliminazione Categoria -> eliminazione Articoli
@OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
private Set<Article> articles = new HashSet<Article>();
// costruttori
public Categorie() {
}
// getter e setter
...
// toString
public String toString() {
return String.format("Categorie[%d,%d,%s]", id, version, nom);
}
// associazione bidirezionale Categoria <--> Articolo
public void addArticle(Article article) {
// l'articolo viene aggiunto alla raccolta degli articoli della categoria
articles.add(article);
// l'articolo cambia categoria
article.setCategorie(this);
}
}
- righe 8-11: la chiave primaria dell’@Entity
- righe 12-14: la sua versione
- righe 16-17: il nome della categoria
- righe 19-24: l'insieme (set) degli articoli della categoria
- riga 23: l'annotazione @OneToMany indica una relazione uno-a-molti. Il «One» indica l’@Entity [Categorie] in cui ci si trova, il «Many» il tipo [Article] della riga 24: una (One) categoria ha più (Many) articoli.
- riga 23: l'annotazione è l'inverso (mappedBy) dell'annotazione ManyToOne applicata al campo categorie dell'@Entity Article: mappedBy=categoria. La relazione ManyToOne, definita sul campo categorie dell'@Entity Article, è la relazione principale. È indispensabile. Essa concretizza la relazione di chiave esterna che collega l'@Entity Article all'@Entity Categorie. La relazione OneToMany, definita sul campo articles dell'@Entity Categorie, è la relazione inversa. Non è indispensabile. Si tratta di una funzionalità che consente di recuperare gli articoli di una categoria. Senza questa funzionalità, tali articoli verrebbero recuperati tramite una query JPQL.
- riga 23: cascadeType.ALL richiede che le operazioni (persist, merge, remove) eseguite su un'@Entity Categorie vengano propagate ai suoi articoli.
- riga 24: gli articoli di una categoria saranno inseriti in un oggetto di tipo Set<Article>. Il tipo Set non accetta duplicati. Pertanto non è possibile inserire due volte lo stesso articolo nell’oggetto Set<Article>. Cosa si intende per «lo stesso articolo»? Per indicare che l’articolo a è lo stesso dell’articolo b, Java utilizza l’espressione a.equals(b). Nella classe Object, classe madre di tutte le classi, a.equals(b) è vero se a==b, c.a.d. se gli oggetti a e b occupano la stessa posizione in memoria. Si potrebbe voler dire che gli oggetti a e b sono gli stessi se hanno lo stesso nome. In questo caso, lo sviluppatore deve ridefinire due metodi nella classe [Article]:
- equals: deve restituire vero se i due articoli hanno lo stesso nome
- hashCode: deve restituire un valore intero identico per due oggetti [Article] che il metodo equals considera uguali. In questo caso, il valore sarà quindi costruito a partire dal nome dell'articolo. Il valore restituito da hashCode può essere un numero intero qualsiasi. Viene utilizzato in diversi contenitori di oggetti, in particolare nei dizionari (Hashtable).
La relazione OneToMany può utilizzare tipi diversi dal Set per memorizzare il Many, ad esempio oggetti List. Non tratteremo questi casi nel presente documento. Il lettore li troverà in [ref1].
- riga 38: il metodo [addArticle] ci permette di aggiungere un articolo a una categoria. Il metodo provvede ad aggiornare entrambe le estremità della relazione OneToMany che collega [Categorie] a [Article].
4.3. L'API del livello JPA
Spieghiamo l’ambiente di esecuzione di un client JPA:
![]() |
Sappiamo che il livello JPA [2] crea un ponte oggetto [3] / relazionale [4]. Si definisce «contesto di persistenza» l’insieme degli oggetti gestiti dal livello JPA nell’ambito di questo ponte oggetto/relazionale. Per accedere ai dati del contesto di persistenza, un client JPA [1] deve passare attraverso il livello JPA [2]:
- può creare un oggetto e richiedere al livello JPA di renderlo persistente. L’oggetto diventa quindi parte del contesto di persistenza.
- può richiedere al livello [JPA] un riferimento a un oggetto persistente esistente.
- può modificare un oggetto persistente ottenuto dal livello JPA.
- 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:
inserisce entity nel contesto di persistenza | |
rimuove entity dal contesto di persistenza | |
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. | |
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. | |
crea un oggetto Query a partire da una query JPQL (Java Persistence Query Language). Una query JPQL è analoga a una query SQL se se non fosse che interroga oggetti anziché tabelle. | |
metodo analogo al precedente, tranne per il fatto che queryText è un ordine SQL e non JPQL. | |
Metodo identico a createQuery, tranne per il fatto che l'ordine JPQL queryText è stato è stato esternalizzato in un file di configurazione e associato a un nome. È proprio questo nome a costituire il parametro del metodo. |
Un oggetto EntityManager ha un ciclo di vita che non è necessariamente quello dell’applicazione. Ha un inizio e una fine. Pertanto, un client JPA può lavorare in successione con diversi oggetti EntityManager. Il contesto di persistenza associato a un EntityManager ha lo stesso ciclo di vita di quest’ultimo. Sono indissociabili l’uno dall’altro. Quando un oggetto EntityManager viene chiuso, il suo contesto di persistenza viene, se necessario, sincronizzato con il database e poi cessa di esistere. È necessario creare un nuovo EntityManager per disporre nuovamente di un contesto di persistenza.
Il client JPA può creare un EntityManager e quindi un contesto di persistenza con la seguente istruzione:
EntityManagerFactory emf = Persistence.createEntityManagerFactory("nom d'une unité de persistance");
- javax.persistence.Persistence è una classe statica che consente di ottenere una factory di oggetti EntityManager. Questa factory è associata a una specifica unità di persistenza. Ricordiamo che il file di configurazione [META-INF/persistence.xml] consente di definire le unità di persistenza e che queste hanno un nome:
<persistence-unit name="elections-dao-jpa-mysql-01PU" transaction-type="RESOURCE_LOCAL">
Nell’esempio sopra riportato, l’unità di persistenza si chiama elections-dao-jpa-mysql-01PU. Ad essa è associata un’intera configurazione specifica, in particolare il SGBD con cui opera. L'istruzione [Persistence.createEntityManagerFactory("elections-dao-jpa-mysql-01PU")] crea una fabbrica di oggetti di tipo EntityManagerFactory in grado di fornire oggetti EntityManager destinati a gestire contesti di persistenza legati all'unità di persistenza denominata elections-dao-jpa-mysql-01PU. L’ottenimento di un oggetto EntityManager e quindi di un contesto di persistenza avviene a partire dall’oggetto EntityManagerFactory nel modo seguente:
I seguenti metodi dell’interfaccia [EntityManager] consentono di gestire il ciclo di vita del contesto di persistenza:
il contesto di persistenza viene chiuso. Forza la sincronizzazione del contesto di persistenza con il database:
| |
il contesto di persistenza viene svuotato di tutti i suoi oggetti ma non chiuso. | |
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]:
Esistono due possibili valori per flushmode: FlushModeType.AUTO (impostazione predefinita): la sincronizzazione avviene prima di ogni richiesta SELECT effettuata sul database. FlushModeType.COMMIT: la sincronizzazione avviene solo alla al termine delle transazioni sul database. | |
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:
- prima di ogni operazione SELECT sul database
- al termine di una transazione sul database
- a seguito di un'operazione flush o close sul contesto di persistenza
Nella modalità FlushModeType.COMMIT, il comportamento è identico, tranne per l’operazione 1 che non viene eseguita. La modalità normale di interazione con il livello JPA è una modalità transazionale. Il client esegue diverse operazioni sul contesto di persistenza, all’interno di una transazione. In questo caso, i momenti di sincronizzazione del contesto di persistenza con il database corrispondono ai casi 1 e 2 sopra indicati nella modalità AUTO, e al solo caso 2 nella modalità COMMIT.
Concludiamo con il codice API dell’interfaccia Query, che consente di inviare comandi JPQL al contesto di persistenza oppure comandi SQL direttamente al database per recuperare i dati. L’interfaccia Query è la seguente:
![]() |
- 1 - Il metodo getResultList esegue un SELECT che restituisce diversi oggetti. Questi saranno ottenuti in un oggetto List. Questo oggetto è un’interfaccia. Essa fornisce un oggetto Iterator che consente di scorrere gli elementi della lista L nella forma seguente:
Iterator iterator = L.iterator();
while (iterator.hasNext()) {
// utilizzare l'oggetto iterator.next() che rappresenta l'elemento corrente dell'elenco
...
}
L'elenco L può essere utilizzato anche con un for:
for (Object o : L) {
// utilizzare l'oggetto o
}
- 2 - Il metodo getSingleResult esegue un comando JPQL / SQL SELECT che restituisce un unico oggetto.
- 3 - Il metodo executeUpdate esegue un comando SQL di aggiornamento o eliminazione e restituisce il numero di righe interessate dall'operazione.
- 4 - Il metodo setParameter(String, Object) consente di assegnare un valore a un parametro denominato di un comando JPQL configurato
- 5 - Il metodo setParameter(int, Object) non identifica il parametro tramite il suo nome, ma tramite la sua posizione nell’ordine JPQL.
4.4. Le query JPQL
JPQL (Java Persistence Query Language) è il linguaggio di query del livello JPA. Il linguaggio JPQL è affine al linguaggio SQL dei database. Mentre SQL opera con le tabelle, JPQL opera con gli oggetti immagine di tali tabelle. Esamineremo un esempio all’interno della seguente architettura:
![]() |
Il database che chiameremo [dbrdvmedecins2] è un database MySQL5 con quattro tabelle:
![]() |
Raccoglie informazioni che consentono di gestire gli appuntamenti di un gruppo di medici.
4.4.1. La tabella [MEDECINS]
Contiene informazioni sui medici.
![]() | ![]() |
- ID: numero identificativo del medico - chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- NOM: il cognome del medico
- PRENOM: il suo nome
- TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)
4.4.2. La tabella [CLIENTS]
I clienti dei diversi medici sono registrati nella tabella [CLIENTS]:
![]() | ![]() |
- ID: numero identificativo del cliente - chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- NOM: il nome del cliente
- PRENOM: il suo nome
- TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)
4.4.3. La tabella [CRENEAUX]
Elenca le fasce orarie in cui sono possibili i RV:
![]() |
![]() |
- ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- ID_MEDECIN: numero identificativo del medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna MEDECINS (ID).
- HDEBUT: ora di inizio della fascia oraria
- MDEBUT: minuti di inizio della fascia oraria
- HFIN: ora di fine della fascia oraria
- MFIN: minuti di fine della fascia oraria
La seconda riga della tabella [CRENEAUX] (cfr. [1] sopra) indica, ad esempio, che la fascia n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (dott.ssa Marie PELISSIER).
4.4.4. La tabella [RV]
Elenca i RV assegnati a ciascun medico:
![]() |
- ID: numero che identifica in modo univoco il RV – chiave primaria
- JOUR: giorno del RV
- ID_CRENEAU: fascia oraria del RV – chiave esterna sul campo [ID] della tabella [CRENEAUX] – determina sia la fascia oraria che il medico interessato.
- ID_CLIENT: numero del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]
Questa tabella presenta un e vincolo di unicità sui valori delle colonne collegate (JOUR, ID_CRENEAU):
Se una riga della tabella [RV] presenta il valore (JOUR1, ID_CRENEAU1) per le colonne (JOUR, ID_CRENEAU), tale valore non può comparire in nessun altro punto. Altrimenti, ciò significherebbe che due RV sono stati registrati contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database avvia un SQLException quando si verifica questo caso.
La riga di id pari a 3 (cfr. [1] sopra) indica che un RV è stato prenotato per la fascia oraria n. 20 e il cliente n. 4 il 23/08/2006. La tabella [CRENEAUX] ci indica che la fascia n. 20 corrisponde alla fascia oraria 16:20 - 16:40 e appartiene al medico n. 1 (la sig.ra Marie PELISSIER). La tabella [CLIENTS] ci indica che il cliente n. 4 è la signorina Brigitte BISTROU.
4.4.5. Generazione del database
Per creare le tabelle e compilarle è possibile utilizzare lo script [dbrdvmedecins2.sql]. Con [WampServer], si può procedere come segue:
![]() |
- in [1], cliccare sull'icona di [WampServer] e selezionare l'opzione [PhpMyAdmin] [2],
- in [3], nella finestra che si è aperta, selezionare il link [Bases de données],
![]() |
- in [2], si crea un database a cui è stato assegnato il nome [4] e la codifica [5],
- in [7], il database è stato creato. Si fa clic sul relativo link,
![]() |
- in [8], si importa un file SQL,
- che si seleziona nel file system tramite il pulsante [9],
![]() |
- in [11], si seleziona lo script SQL e in [12] lo si esegue,
- in [13], le quattro tabelle del database sono state create. Si segue uno dei link,
![]() |
- in [14], il contenuto della tabella.
In seguito, non torneremo più su questo database. Il lettore è tuttavia invitato a seguirne l’evoluzione nel corso dei programmi, soprattutto quando qualcosa non funziona.
4.4.6. Il livello [JPA]
Torniamo all’architettura dell’esempio:
![]() |
Ora costruiamo il progetto Maven del livello [JPA].
4.4.7. Il progetto NetBeans
È il seguente:
![]() |
- in [1], si crea un progetto Maven di tipo [Java Application] [2],
- in [3], si assegna un nome al progetto,
![]() |
- in [4], il progetto generato.
4.4.8. Generazione del livello [JPA]
Torniamo all’architettura che dobbiamo costruire:
![]() |
Con NetBeans è possibile generare automaticamente il livello [JPA]. È interessante conoscere questi metodi di generazione automatica poiché il codice generato fornisce preziose indicazioni su come scrivere le entità JPA.
4.4.9. Creazione di una connessione NetBeans al database
- avviare il SGBD MySQL 5 affinché il BD sia disponibile,
- creare una connessione NetBeans al database [dbrdvmedecins2],
![]() |
- nella scheda [Services] [1], nel ramo [Databases] [2], selezionare il driver JDBC MySQL [3],
- quindi selezionare l'opzione [4] "Connect Using" che consente di creare una connessione con un database MySQL,
- in [5], inserire le informazioni richieste. In [6], il nome del database, in [7] l'utente del database e la sua password,
- in [8] è possibile verificare le informazioni fornite,
- in [9], il messaggio che ci si aspetta quando i dati sono corretti,
![]() |
- in [10], la connessione è stata stabilita. Qui si vedono le quattro tabelle del database a cui ci si è collegati.
4.4.10. Creazione di un'unità di persistenza
Torniamo all’architettura in fase di realizzazione:
![]() |
Stiamo realizzando il livello [JPA]. La sua configurazione avviene in un file [persistence.xml] in cui si definiscono le unità di persistenza. Ciascuna di esse richiede le seguenti informazioni:
- le caratteristiche JDBC di accesso al database (URL, utente, password),
- le classi che fungeranno da immagini delle tabelle del database,
- l'implementazione JPA utilizzata. Infatti, JPA è una specifica implementata da diversi prodotti. In questo caso, utilizzeremo Hibernate.
NetBeans può generare questo file di persistenza tramite un wizard.
![]() |
- fare clic con il tasto destro del mouse sul progetto e scegliere di creare un'unità di persistenza [1],
- in [2], creare un'unità di persistenza,
![]() |
- in [3], assegnare un nome all'unità di persistenza che si sta creando,
- in [4], scegliere l’implementazione JPA Hibernate (JPA 2.0),
- in [5], indicare che le tabelle di BD sono già state create e che quindi non le si creerà. Si conferma la procedura guidata,
- in [6], il nuovo progetto,
- in [7], il file [persistence.xml] è stato generato nella cartella [META-INF],
- in [8], sono state aggiunte nuove dipendenze al progetto Maven.
Il file [META-INF/persistence.xml] generato è il seguente:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
</properties>
</persistence-unit>
</persistence>
Riporta le informazioni fornite nella procedura guidata:
- riga 3: il nome dell'unità di persistenza,
- riga 3: il tipo di transazioni con il database. In questo caso, RESOURCE_LOCAL indica che l’applicazione gestirà autonomamente le proprie transazioni,
- righe 6-9: le proprietà JDBC della fonte dati.
Nella scheda [Design] è possibile avere una visione d’insieme del file [persistence.xml]:
![]() |
Per ottenere i log di Hibernate, completiamo il file [persistence.xml] nel modo seguente:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
- riga 11: si richiede di visualizzare gli ordini SQL emessi da Hibernate,
- riga 12: questa proprietà consente di visualizzarli in formato formattato.
Al progetto sono state aggiunte delle dipendenze. Il file [pom.xml] è il seguente:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-jpql-hibernate</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-rdvmedecins-jpql-hibernate</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.1.0.GA</version>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.1_spec</artifactId>
<version>1.0.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>antlr</groupId>
<artifactId>antlr</artifactId>
<version>2.7.7</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
<version>1.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.15.0-GA</version>
</dependency>
<dependency>
<groupId>org.hibernate.common</groupId>
<artifactId>hibernate-commons-annotations</artifactId>
<version>4.0.1.Final</version>
</dependency>
</dependencies>
</project>
Le dipendenze aggiunte riguardano tutte Hibernate ORM. Aggiungeremo la dipendenza del driver JDBC da MySQL:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
4.4.11. Generazione delle entità JPA
Le entità JPA possono essere generate tramite una procedura guidata di NetBeans:
![]() |
- in [1], si creano entità JPA da un database,
![]() |
- in [2], si seleziona la connessione [dbrdvmedecins2] creata in precedenza,
- in [3], si selezionano tutte le tabelle del database associato,
![]() |
- in [4], si assegna un nome alle classi Java associate alle quattro tabelle,
- nonché un nome di pacchetto [5],
- in [6], JPA raggruppa le righe delle tabelle di BD in collezioni. Scegliamo la lista come collezione,
![]() |
- in [7], le classi Java create dall'assistente.
4.4.12. Le entità JPA generate
L’entità [Medecin] è l’immagine della tabella [medecins]. La classe Java è piena di annotazioni che rendono il codice poco leggibile a prima vista. Se si mantiene solo ciò che è essenziale per comprendere il ruolo dell’entità, si ottiene il codice seguente:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
// costruttori
....
// getter e setter
....
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- alla riga 4, l’annotazione @Entity rende la classe [Medecin] un’entità JPA, c.a.d. una classe collegata a una tabella di BD tramite API e JPA,
- riga 5, il nome della tabella BD associata all’entità JPA. Ogni campo della tabella corrisponde a un campo nella classe Java,
- riga 6, la classe implementa l’interfaccia Serializable. Ciò è necessario nelle applicazioni client/server, dove le entità vengono serializzate tra il client e il server.
- righe 10-11: il campo id della classe [Medecin] corrisponde al campo [ID] (riga 10) della tabella [medecins],
- righe 13-14: il campo titolo della classe [Medecin] corrisponde al campo [TITRE] (riga 13) della tabella [medecins],
- righe 16-17: il campo «nome» della classe [Medecin] corrisponde al campo [NOM] (riga 16) della tabella [medecins],
- righe 19-20: il campo "versione" della classe [Medecin] corrisponde al campo [VERSION] (riga 19) della tabella [medecins]. In questo caso, l’assistente non riconosce che la colonna è in realtà una colonna di versione che deve essere incrementata ad ogni modifica della riga a cui appartiene. Per assegnarle questo ruolo, è necessario aggiungere l’annotazione @Version. Lo faremo in un passaggio successivo,
- righe 22-23: il campo prenom della classe [Medecin] corrisponde al campo [PRENOM] della tabella [medecins],
- righe 10-11: il campo id corrisponde alla chiave primaria [ID] della tabella. Le annotazioni delle righe 8-9 precisano questo punto,
- riga 8: l'annotazione @Id indica che il campo annotato è associato alla chiave primaria della tabella,
- riga 9: il livello [JPA] genererà la chiave primaria delle righe che inserirà nella tabella [Medecins]. Esistono diverse strategie possibili. In questo caso, la strategia GenerationType.IDENTITY indica che il livello JPA utilizzerà la modalità auto_increment della tabella MySQL,
- righe 25-26: la tabella [creneaux] ha una chiave esterna sulla tabella [medecins]. Una fascia oraria appartiene a un medico. Viceversa, a un medico sono associate diverse fasce orarie. Si ha quindi una relazione uno (medico) a molti (slot), una relazione qualificata dall’annotazione @OneToMany da JPA (riga 25). Il campo della riga 26 conterrà tutti gli slot del medico. Ciò avviene senza alcuna programmazione. Per comprendere appieno la riga 25, è necessario presentare la classe [Creneau].
Essa è la seguente:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "MDEBUT")
private int mdebut;
@Column(name = "HFIN")
private int hfin;
@Column(name = "HDEBUT")
private int hdebut;
@Column(name = "MFIN")
private int mfin;
@Column(name = "VERSION")
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
private List<Rv> rvList;
// costruttori
...
// getter e setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
Commentiamo solo le nuove annotazioni:
- abbiamo detto che la tabella [creneaux] aveva una chiave esterna verso la tabella [medecins]: una fascia oraria è associata a un medico. Più fasce orarie possono essere associate allo stesso medico. Esiste una relazione dalla tabella [creneaux] alla tabella [medecins] che è definita come "molti (slot)" a "uno (medico)". È l’annotazione @ManyToOne della riga 32 che serve a definire la chiave esterna,
- la riga 31 con l'annotazione @JoinColumn specifica la relazione di chiave esterna: la colonna [ID_MEDECIN] della tabella [creneaux] è una chiave esterna sulla colonna [ID] della tabella [medecins],
- riga 33: un riferimento al medico titolare della fascia oraria. Anche in questo caso lo si ottiene senza programmazione.
Il collegamento tramite chiave esterna tra l’entità [Creneau] e l’entità [Medecin] è quindi concretizzato da due annotazioni:
- nell’entità [Creneau]:
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
- nell’entità [Medecin]:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
Le due annotazioni riflettono la stessa relazione: quella della chiave esterna della tabella [creneaux] verso la tabella [medecins]. Si dice che siano inverse l'una rispetto all'altra. Solo la relazione @ManyToOne è indispensabile. Essa definisce in modo inequivocabile la relazione di chiave esterna. La relazione @OneToMany è facoltativa. Se presente, si limita a fare riferimento alla relazione @ManyToOne a cui è associata. Questo è il significato dell’attributo mappedBy della riga 1 dell’entità [Medecin]. Il valore di questo attributo è il nome del campo dell’entità [Creneau] che presenta l’annotazione @ManyToOne, la quale specifica la chiave esterna. Sempre nella stessa riga 1 dell’entità [Medecin], l’attributo cascade=CascadeType.ALL definisce il comportamento dell’entità [Medecin] rispetto all’entità [Creneau]:
- se si inserisce una nuova entità [Medecin] nel database, allora devono essere inserite anche le entità [Creneau] del campo della riga 2,
- se si modifica un'entità [Medecin] nel database, allora devono essere modificate anche le entità [Creneau] del campo della riga 2,
- se si elimina un'entità [Medecin] dal database, allora devono essere eliminate anche le entità [Creneau] del campo della riga 2.
Forniamo il codice delle altre due entità senza commenti particolari, poiché non introducono nuove notazioni.
L'entità [Client]
package rdvmedecins.jpa;
...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
private List<Rv> rvList;
// costruttori
...
// getter e setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- le righe 24-25 riflettono la relazione di chiave esterna tra la tabella [rv] e la tabella [clients].
L'entità [Rv]:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau idCreneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client idClient;
// costruttori
...
// getter e setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- la riga 13 descrive il campo «giorno» di tipo Java Date. Si indica che nella tabella [rv], la colonna [JOUR] (riga 12) è di tipo data (senza ora),
- righe 16-18: definiscono la relazione di chiave esterna che la tabella [rv] ha con la tabella [creneaux],
- righe 20-22: definiscono la relazione di chiave esterna che la tabella [rv] ha con la tabella [clients].
La generazione automatica delle entità JPA ci permette di ottenere una base di lavoro. A volte è sufficiente, altre volte no. È il caso in questione:
- è necessario aggiungere l’annotazione @Version ai vari campi versione delle entità,
- è necessario scrivere metodi toString più espliciti di quelli generati,
- le entità [Medecin] e [Client] sono analoghe. Le faremo derivare da una classe [Personne],
- elimineremo le relazioni inverse @OneToMany delle relazioni @ManyToOne. Non sono indispensabili e comportano complicazioni di programmazione,
- elimineremo la validazione @NotNull sulle chiavi primarie. Quando si salva un'entità JPA insieme a MySQL, l'entità iniziale ha una chiave primaria null. È solo dopo la persistenza nel database che la chiave primaria dell’elemento persistito assume un valore.
Con queste specifiche, le diverse classi diventano le seguenti:
La classe Persona viene utilizzata per rappresentare medici e clienti:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@MappedSuperclass
public class Personne implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Column(name = "TITRE")
private String titre;
@Basic(optional = false)
@Column(name = "NOM")
private String nom;
@Basic(optional = false)
@Column(name = "VERSION")
@Version
private int version;
@Basic(optional = false)
@Column(name = "PRENOM")
private String prenom;
// costruttori
...
// getter e setter
...
@Override
public String toString() {
return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
}
}
- riga 6: si noti che la classe [Personne] non è essa stessa un’entità (@Entity). Sarà la classe padre delle entità. L’annotazione @MappedSuperClass indica questa situazione.
L’entità [Client] incapsula le righe della tabella [clients]. Deriva dalla precedente classe [Personne]:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// costruttori
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
- riga 6: la classe [Client] è un'entità JPA,
- riga 7: è associata alla tabella [clients],
- riga 8: deriva dalla classe [Personne].
L'entità [Medecin], che incapsula le righe della tabella [medecins], segue lo stesso modello:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// costruttori
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
L'entità [Creneau] incapsula le righe della tabella [creneaux]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Column(name = "MDEBUT")
private int mdebut;
@Basic(optional = false)
@Column(name = "HFIN")
private int hfin;
@Basic(optional = false)
@NotNull
@Column(name = "HDEBUT")
private int hdebut;
@Basic(optional = false)
@Column(name = "MFIN")
private int mfin;
@Basic(optional = false)
@Column(name = "VERSION")
@Version
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin medecin;
// costruttori
...
// getter e setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
// TODO: Avviso - questo metodo non funzionerà nel caso in cui i campi id non siano impostati
...
}
@Override
public String toString() {
return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
}
}
- le righe 40-42 modellano la relazione «molti a uno» esistente tra la tabella [creneaux] e la tabella [medecins] del database: un medico ha più fasce orarie, una fascia oraria appartiene a un solo medico.
L’entità [Rv] incapsula le righe della tabella [rv]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau creneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client client;
// costruttori
...
// getter e setter
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Rv[%s, %s, %s]", id, creneau, client);
}
}
- le righe 27-29 modellano la relazione «molti a uno» esistente tra la tabella [rv] e la tabella [clients] (un cliente può comparire in più Rv) del database, mentre le righe 23-25 rappresentano la relazione «molti a uno» esistente tra la tabella [rv] e la tabella [creneaux] (una fascia oraria può comparire in più Rv).
4.4.13. Il codice di accesso ai dati
Aggiungeremo ora al progetto il codice di accesso ai dati tramite il livello JPA:
![]() |
![]() |
La classe [MainJpql] è la seguente:
package rdvmedecins.console;
import java.util.Scanner;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class MainJpql {
public static void main(String[] args) {
// EntityManagerFactory
EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-rdvmedecins-jpql-hibernatePU");
// entityManager
EntityManager em = emf.createEntityManager();
// scanner da tastiera
Scanner clavier = new Scanner(System.in);
// ciclo di immissione delle richieste JPQL
System.out.println("Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :");
String requete = clavier.nextLine();
while (!requete.trim().equals("*")) {
try {
// visualizzazione del risultato della richiesta
for (Object o : em.createQuery(requete).getResultList()) {
System.out.println(o);
}
} catch (Exception e) {
System.out.println("L'exception suivante s'est produite : " + e);
}
// si svuota il contesto di persistenza
em.clear();
// nuova richiesta
System.out.println("---------------------------------------------");
System.out.println("Requete JPQL sur la base dbrdvmedecins2 (* pour arrêter) :");
requete = clavier.nextLine();
}
// chiusura delle risorse
em.close();
emf.close();
}
}
- riga 12: creazione di EntityManagerFactory associato all'unità di persistenza che abbiamo creato in precedenza. Il parametro del metodo createEntityManagerFactory è il nome di questa unità di persistenza:
<persistence-unit name="mv-rdvmedecins-jpql-hibernatePU" transaction-type="RESOURCE_LOCAL">
...
</persistence-unit>
- riga 14: creazione di EntityManager che gestisce il livello di persistenza,
- riga 19: immissione di una query JPQL select,
- righe 23-28: visualizzazione del risultato della query,
- riga 20: l'immissione si interrompe quando l'utente digita *.
Domanda: indicare le query JPQL che consentono di ottenere le seguenti informazioni:
- elenco dei medici in ordine decrescente in base ai loro nomi
- elenco dei medici il cui titolo è 'Mr'
- elenco delle fasce orarie della signora Pelissier
- elenco degli appuntamenti fissati in ordine crescente di giorno
- elenco dei clienti (nome) che hanno effettuato un appuntamento con la sig.ra PELISSIER il 24/08/2006
- numero di clienti della sig.ra PELISSIER il 24/08/2006
- i clienti che non hanno fissato un appuntamento
- i medici che non hanno appuntamenti
Ci si ispirerà all'esempio del paragrafo 2.7 di [ref1]. Ecco un esempio di esecuzione:
- riga 2: la query JPQL,
- righe 3-11: la query corrispondente SQL,
- righe 12-15: il risultato della richiesta JPQL.
4.5. Collegamenti tra il contesto di persistenza e SGBD
4.5.1. La classe Persona
4.5.2. Il programma di test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
4.5.3. La configurazione di Hibernate
4.5.4. La configurazione di log4j.properties
4.5.5. I risultati
Domanda: individuate la correlazione tra il codice Java e i risultati visualizzati.





































