Java Persistence API (JPA) è un'interfaccia standard per accedere ai database in Java, che fornisce una mappatura automatica tra le classi Java e le tabelle del database. È disponibile un plug-in open source per l'utilizzo di JPA con Datastore e questa pagina fornisce informazioni su come iniziare a utilizzarlo.
Avviso: riteniamo che la maggior parte degli sviluppatori avrà un'esperienza migliore utilizzando l'API Datastore di basso livello o una delle API open source sviluppate appositamente per Datastore, come Objectify. JPA è stato progettato per l'utilizzo con i database relazionali tradizionali e, pertanto, non ha modo di rappresentare esplicitamente alcuni aspetti di Datastore che lo rendono diverso dai database relazionali, come i gruppi di entità e le query sugli antenati. Ciò può portare a problemi sottili che sono difficili da comprendere e risolvere.
La versione 1.x del plug-in è inclusa nell'SDK Java di App Engine, che implementa la versione 1.0 di JPA. L'implementazione si basa sulla versione 1.1 della piattaforma DataNucleus Access.
Nota:le istruzioni riportate in questa pagina si applicano alla versione 1 di JPA, che utilizza la versione 1.x del plug-in DataNucleus per App Engine. È disponibile anche la versione 2.x del plug-in DataNucleus, che consente di utilizzare JPA 2.0. Il plug-in 2.x fornisce una serie di nuove API e funzionalità. Tuttavia, l'upgrade non è completamente compatibile con le versioni precedenti 1.x. Se ricostruisci un'applicazione utilizzando JPA 2.0, devi aggiornare e riesaminare il codice. Per ulteriori informazioni sulla nuova versione, consulta Utilizzo di JPA 2.0 con App Engine.
Configurazione di JPA
Per utilizzare JPA per accedere al datastore, un'app App Engine ha bisogno di quanto segue:
- I file JAR JPA e del datastore devono trovarsi nella directory
war/WEB-INF/lib/
dell'app. - Un file di configurazione denominato
persistence.xml
deve trovarsi nella directorywar/WEB-INF/classes/META-INF/
dell'app, con una configurazione che indichi a JPA di utilizzare il datastore di App Engine. - Il processo di compilazione del progetto deve eseguire un passaggio di "miglioramento " post-compilazione sulle classi di dati compilate per associarle all'implementazione JPA.
Copia dei file JAR
I file JAR JPA e Datastore sono inclusi nell'SDK Java di App Engine. Puoi trovarli nella directory appengine-java-sdk/lib/user/orm/
.
Copia i file JAR nella directory war/WEB-INF/lib/
della tua applicazione.
Assicurati che appengine-api.jar
sia presente anche nella directory war/WEB-INF/lib/
. Potresti averlo già copiato al momento della creazione del progetto. Il plug-in DataNucleus di App Engine utilizza questo file JAR per accedere al datastore.
Creazione del file persistence.xml
L'interfaccia JPA richiede un file di configurazione denominato
persistence.xml
nella directory
war/WEB-INF/classes/META-INF/
dell'applicazione. Puoi creare questo file direttamente in questa posizione o chiedere alla processo di compilazione di copiarlo da una directory di origine.
Crea il file con i seguenti contenuti:
<?xml version="1.0" encoding="UTF-8" ?> <persistence 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_1_0.xsd" version="1.0"> <persistence-unit name="transactions-optional"> <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider> <properties> <property name="datanucleus.NontransactionalRead" value="true"/> <property name="datanucleus.NontransactionalWrite" value="true"/> <property name="datanucleus.ConnectionURL" value="appengine"/> </properties> </persistence-unit> </persistence>
Norme di lettura del datastore e scadenza chiamata
Come descritto nella pagina Query sul datastore, puoi impostare il criterio per la lettura (elevata coerenza o coerenza eventuale) e la scadenza della chiamata al datastore per un EntityManagerFactory
nel file persistence.xml
.
Queste impostazioni vanno nell'elemento <persistence-unit>
. Tutte le chiamate effettuate con una determinata istanza EntityManager
utilizzano la configurazione selezionata al momento della creazione del gestore da parte di EntityManagerFactory
. Puoi anche eseguire l'override di queste opzioni per un singolo Query
(descritto di seguito).
Per impostare il criterio per la lettura, includi una proprietà denominata
datanucleus.appengine.datastoreReadConsistency
. I valori possibili sono EVENTUAL
(per le letture con coerenza finale) e STRONG
(per le letture con elevata coerenza). Se non specificato, il valore predefinito è STRONG
.
<property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
Puoi impostare scadenze di chiamata del datastore separate per le letture e per le scritture. Per le letture, utilizza la proprietà standard JPA
javax.persistence.query.timeout
. Per le scritture, utilizza
datanucleus.datastoreWriteTimeout
. Il valore è un periodo di tempo in millisecondi.
<property name="javax.persistence.query.timeout" value="5000" /> <property name="datanucleus.datastoreWriteTimeout" value="10000" />
Se vuoi utilizzare le transazioni tra gruppi (XG), aggiungi la seguente proprietà:
<property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />
Puoi avere più elementi <persistence-unit>
nello stesso file persistence.xml
, utilizzando attributi name
diversi, per utilizzare istanze EntityManager
con configurazioni diverse nella stessa app. Ad esempio, il seguente file persistence.xml
stabilisce due insiemi di configurazione, uno denominato "transactions-optional"
e l'altro "eventual-reads-short-deadlines"
:
<?xml version="1.0" encoding="UTF-8" ?> <persistence 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_1_0.xsd" version="1.0"> <persistence-unit name="transactions-optional"> <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider> <properties> <property name="datanucleus.NontransactionalRead" value="true"/> <property name="datanucleus.NontransactionalWrite" value="true"/> <property name="datanucleus.ConnectionURL" value="appengine"/> </properties> </persistence-unit> <persistence-unit name="eventual-reads-short-deadlines"> <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider> <properties> <property name="datanucleus.NontransactionalRead" value="true"/> <property name="datanucleus.NontransactionalWrite" value="true"/> <property name="datanucleus.ConnectionURL" value="appengine"/> <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" /> <property name="javax.persistence.query.timeout" value="5000" /> <property name="datanucleus.datastoreWriteTimeout" value="10000" /> </properties> </persistence-unit> </persistence>
Consulta Ottenere un'istanza EntityManager di seguito per informazioni su come creare un EntityManager
con un set di configurazione denominato.
Puoi ignorare il criterio per la lettura e la scadenza della chiamata per un singolo oggettoQuery
. Per sostituire il criterio per la lettura per un Query
,
chiamate il relativo metodo setHint()
come segue:
Query q = em.createQuery("select from " + Book.class.getName()); q.setHint("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");
Come sopra, i valori possibili sono "EVENTUAL"
e
"STRONG"
.
Per ignorare il timeout di lettura, chiama setHint()
come segue:
q.setHint("javax.persistence.query.timeout", 3000);
Non è possibile ignorare la configurazione di queste opzioni quando recupero le entità per chiave.
Miglioramento delle classi di dati
L'implementazione di JPA di DataNucleus utilizza un passaggio di "miglioramento" post-compilazione nel processo di compilazione per associare le classi di dati all'implementazione di JPA.
Puoi eseguire il passaggio di miglioramento sulle classi compilate dalla riga di comando con il seguente comando:
java -cp classpath org.datanucleus.enhancer.DataNucleusEnhancer class-files
Il classpath deve contenere i file JAR
datanucleus-core-*.jar
, datanucleus-jpa-*
,
datanucleus-enhancer-*.jar
, asm-*.jar
e
geronimo-jpa-*.jar
(dove *
è il numero di versione appropriato di ogni JAR) dalla directory appengine-java-sdk/lib/tools/
, nonché tutti i tuoi classi di dati.
Per ulteriori informazioni sull'ottimizzatore del bytecode di DataNucleus, consulta la documentazione di DataNucleus.
Ottenere un'istanza di EntityManager
Un'app interagisce con JPA utilizzando un'istanza della EntityManager
class. Puoi ottenere questa istanza creando un'istanza e chiamando un metodo su un'istanza della classe EntityManagerFactory
. La factory utilizza la configurazione JPA (identificata dal nome "transactions-optional"
) per creare istanze EntityManager
.
Poiché l'inizializzazione di un'istanza EntityManagerFactory
richiede tempo, è consigliabile riutilizzare una singola istanza il più possibile. Un modo semplice per farlo è creare una classe wrapper singleton con un'istanza statica, come segue:
EMF.java
import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; public final class EMF { private static final EntityManagerFactory emfInstance = Persistence.createEntityManagerFactory("transactions-optional"); private EMF() {} public static EntityManagerFactory get() { return emfInstance; } }
Suggerimento:"transactions-optional"
fa riferimento al nome della configurazione impostata nel file persistence.xml
. Se la tua app utilizza più set di configurazione, dovrai estendere questo codice per chiamare Persistence.createEntityManagerFactory()
come preferisci. Il codice deve memorizzare nella cache un'istanza singleton di ogni EntityManagerFactory
.
L'app utilizza l'istanza di fabbrica per creare un'istanza EntityManager
per ogni richiesta che accede all'archivio dati.
import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import EMF; // ... EntityManager em = EMF.get().createEntityManager();
Utilizzi EntityManager
per archiviare, aggiornare ed eliminare gli oggetti
dati ed eseguire query sul datastore.
Al termine dell'utilizzo dell'istanza EntityManager
, devi chiamarne il metodo close()
. È un errore utilizzare l'istanza EntityManager
dopo aver chiamato il relativo metodo close()
.
try { // ... do stuff with em ... } finally { em.close(); }
Annotazioni di classi e campi
Ogni oggetto salvato da JPA diventa un'entità nel datastore di App Engine. Il tipo di entità è dedotto dal nome semplice della classe (senza il nome del pacchetto). Ogni campo permanente della classe rappresenta una proprietà dell'entità, con il nome della proprietà uguale al nome del campo (con la maiuscola conservata).
Per dichiarare una classe Java come archiviabile e recuperabile dal datastore con JPA, assegna alla classe un'annotazione @Entity
. Ad
esempio:
import javax.persistence.Entity; @Entity public class Employee { // ... }
I campi della classe di dati da archiviare nel datastore devono essere di un tipo permanente per impostazione predefinita o dichiarati esplicitamente come permanenti.
Puoi trovare un grafico che descrive il comportamento di persistenza predefinito di JPA sul
sito web di DataNucleus. Per dichiarare esplicitamente un campo come persistente, gli assegni un'annotazione @Basic
:
import java.util.Date; import javax.persistence.Enumerated; import com.google.appengine.api.datastore.ShortBlob; // ... @Basic private ShortBlob data;
Il tipo di un campo può essere uno dei seguenti:
- uno dei tipi principali supportati dal datastore
- Una raccolta (ad esempio un
java.util.List<...>
) di valori di un tipo di datastore di base - un'istanza o una raccolta di istanze di una classe
@Entity
- una classe incorporata, memorizzata come proprietà nell'entità
Una classe di dati deve avere un costruttore predefinito pubblico o protetto e un campo dedicato allo stoccaggio della chiave primaria dell'entità del datastore corrispondente. Puoi scegliere tra quattro diversi tipi di campi chiave, ognuno con un tipo di valore e annotazioni diversi. Per ulteriori informazioni, consulta Creazione di dati: chiavi. Il campo chiave più semplice è un valore intero lungo
che viene compilato automaticamente da JPA con un valore univoco in tutte le altre
istanze della classe quando l'oggetto viene salvato nel datastore per la primeira volta. Le chiavi di interi lunghi utilizzano un'annotazione @Id
e un'annotazione @GeneratedValue(strategy = GenerationType.IDENTITY)
:
import com.google.appengine.api.datastore.Key; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; // ... @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Key key;
Ecco un esempio di classe di dati:
import com.google.appengine.api.datastore.Key; import java.util.Date; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Key key; private String firstName; private String lastName; private Date hireDate; // Accessors for the fields. JPA doesn't use these, but your application does. public Key getKey() { return key; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Date getHireDate() { return hireDate; } public void setHireDate(Date hireDate) { this.hireDate = hireDate; } }
Ereditarietà
JPA supporta la creazione di classi di dati che utilizzano l'ereditarietà. Prima di parlare di come funziona l'eredità JPA su App Engine, ti consigliamo di leggere la documentazione di DataNucleus su questo argomento e di tornare qui. Completato? OK. L'eredità JPA su App Engine funziona come descritto nella documentazione di DataNucleus con alcune limitazioni aggiuntive. Discuteremo di queste limitazioni e poi forniremo alcuni esempi concreti.
La strategia di ereditarietà "JOINED" consente di suddividere i dati di un singolo oggetto di dati in più "tabelle", ma poiché il datastore App Engine non supporta le unioni, l'utilizzo di un oggetto di dati con questa strategia di ereditarietà richiede una chiamata di procedura remota per ogni livello di ereditarietà. Questo è potenzialmente molto inefficiente, pertanto la strategia di ereditarietà "JOINED" non è supportata nelle classi di dati.
In secondo luogo, la strategia di ereditarietà "SINGLE_TABLE" consente di archiviare i dati per un oggetto dati in un'unica "tabella" associata alla classe persistente alla radice della gerarchia di ereditarietà. Sebbene non siano presenti inefficienze intrinseche in questa strategia, al momento non è supportata. Potremmo rivedere questo aspetto nelle release future.
Ecco le buone notizie: le strategie "TABLE_PER_CLASS" e "MAPPED_SUPERCLASS" funzionano come descritto nella documentazione di DataNucleus. Vediamo un puro esempio:
Worker.java
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.MappedSuperclass; @Entity @MappedSuperclass public abstract class Worker { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Key key; private String department; }
Employee.java
// ... imports ... @Entity public class Employee extends Worker { private int salary; }
Intern.java
import java.util.Date; // ... imports ... @Entity public class Intern extends Worker { private Date internshipEndDate; }
In questo esempio abbiamo aggiunto un'annotazione @MappedSuperclass
alla dichiarazione della classe Worker
. Questo indica a JPA di archiviare tutti
i campi permanenti di Worker
nelle entità del datastore delle sue
sottoclassi. L'entità del datastore creata come risultato della chiamata
persist()
con un'istanza Employee
avrà due proprietà denominate "department" e "salary". L'entità del datastore creata come risultato della chiamata a persist()
con un'istanza Intern
avrà due proprietà denominate "department" e "internshipEndDate". Nel datastore non ci saranno entità di tipo "Lavoratore".
Ora rendiamo le cose un po' più interessanti. Supponiamo che, oltre a avere Employee
e Intern
, vogliamo anche una specializzazione di Employee
che descriva i dipendenti che hanno lasciato l'azienda:
FormerEmployee.java
import java.util.Date; import javax.persistence.Inheritance; import javax.persistence.InheritanceType; // ... imports ... @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public class FormerEmployee extends Employee { private Date lastDay; }
In questo esempio abbiamo aggiunto un'annotazione @Inheritance
alla dichiarazione della classe FormerEmployee
con l'attributo strategy
impostato su InheritanceType.TABLE_PER_CLASS
. Questo indica a JPA di memorizzare tutti i campi permanenti di FormerEmployee
e delle sue superclassi nelle entità del datastore corrispondenti alle istanze di FormerEmployee
. L'entità del datastore creata come risultato della chiamata
persist()
con un'istanza FormerEmployee
avrà
tre proprietà denominate "department", "salary" e "lastDay". Non esisterà mai un'entità di tipo "Dipendente" che corrisponda a un FormerEmployee
, ma se chiami persist()
con un oggetto il cui tipo di runtime è Employee
, creerai un'entità di tipo "Dipendente".
La combinazione di relazioni con l'eredità funziona a condizione che i tipi dichiarati dei campi delle relazioni corrispondano ai tipi di runtime degli oggetti che assegni a questi campi. Per scoprire di più, consulta la sezione relativa alle relazioni polimorfe. Questa sezione contiene esempi di JDO, ma i concetti e le limitazioni sono gli stessi per JPA.
Funzionalità non supportate di JPA 1.0
Le seguenti funzionalità dell'interfaccia JPA non sono supportate dall'implementazione di App Engine:
- Relazioni many-to-many di proprietà e relazioni non di proprietà. Puoi implementare relazioni non di proprietà utilizzando valori chiave espliciti, anche se il controllo del tipo non è applicato nell'API.
- Query "join". Non puoi utilizzare un campo di un'entità secondaria in un filtro quando esegui una query sul tipo principale. Tieni presente che puoi testare il campo della relazione del parente direttamente in una query utilizzando una chiave.
- Query di aggregazione (group by, having, sum, avg, max, min)
- Query polimorfiche. Non puoi eseguire una query su una classe per ottenere le istanze di una sottoclasse. Ogni classe è rappresentata da un tipo di entità distinto nel datastore.