Persistenz mit JPA und Google App Engine
JPA (Java Persistence API) ist der aktuelle Java-Standard für Persistenz und kann neben JDO (Java Data Objects) zum Zugriff auf den Google App Engine Datastore genutzt werden. Die JPA Implementierung basiert dabei auf der DataNucleus Access Platform. Da der Datastore allerdings keine klassische relationale Datenbank ist, gibt es leider auch einige nicht unterstützte Featurs von JPA, die bei der Entwicklung berücksichtigt werden müssen.
Ausgangspunkt für das folgende Tutorial ist das NetBeans Projekt aus dem JSF 2.0 mit NetBeans 6.8 und Google App Engine Tutorial. Das Tutorial ist dabei wieder bewusst detailliert gehalten, um neben der Implementierung für die Google App Engine auch möglichst viele nützliche Features und Wizards von NetBeans vorzustellen.
1. Erstellen der Persistence Unit
Durch einen Rechtsklick auf das Projekt und “New > Other…” und dann “Persistence > Persistence Unit” wird diese für das Projekt erstellt. Leider erlaubt es der NetBeans Wizard nicht, eine Persistence Unit ohne Database Connection zu erstellen. Daher in der Dropdown-Liste für Database Connection einfach eine bestehende Connection (z.B. “sample”) auswählen und “Finish” klicken.

Trotz dieser “falschen” Angabe ist die durch das Google App Engine Plugin erstellte Persistence Unit (fast) korrekt. Nur die Zeile “<exclude-unlisted-classes>false</exclude-unlisted-classes>” hat bei mir im weiteren Verlauf folgende Exception verursacht und sollte daher entfernt werden.
java.lang.IllegalArgumentException: Type ("de.alteskind.appengine.jpa.Message") is not that of an entity but needs to be for this operation
Caused by: org.datanucleus.exceptions.NoPersistenceInformationException: The class "de.alteskind.appengine.jpa.Message" is required to be persistable yet no Meta-Data/Annotations can be found for this class. Please check that the Meta-Data/annotations is defined in a valid file location.
Die korrekte Persistence Unit ist in folgendem Listing aufgeführt:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
<persistence-unit name="GoogleAppEngine_JSF20PU"
transaction-type="RESOURCE_LOCAL">
<provider>
org.datanucleus.store.appengine.jpa.
DatastorePersistenceProvider
</provider>
<properties>
<property name="datanucleus.ConnectionURL"
value="appengine"/>
<property name="datanucleus.NontransactionalRead"
value="true"/>
<property name="datanucleus.NontransactionalWrite"
value="true"/>
</properties>
</persistence-unit>
</persistence>
2. Implementieren der Entity Klasse
In unserem Beispiel erstellen wir durch einen Rechtsklick auf das Projekt und “New > Entity Class…” eine einfache Entität mit dem Namen “Message”. Als Package kann z.B. “de.alteskind.appengine.jpa” angegeben werden. Zu dem bereits bestehenden Feld “id” werden nun noch die Felder “message” (String), “ip” (String) und “dateTime” (java.util.Date) hingzugefügt. Die Getter und Setter können durch einen rechten Mausklick in den Editor und dann “Insert Code… > Getter and Setter…” generiert werden.
NetBeans zeigt nun im Editor für das Feld dateTime einen Fehler (“A temporal attribute must be marked with the @Temporal attribute”) an. Durch einen Klick auf den Fehler kann mit “Create @Temporal annotation” eine passende Annotation erstellt werden. Da in unserem Fall ein Timestamp gesetzt werden soll, könnte als TemporalType auch direkt “TIMESTAMP” gewählt werden. Leider wird das durch die Google App Engine Implementierung von DataNucleus Access Platform nicht unterstützt, so dass der Timestamp später im Code manuell gesetzt werden muss.
Für die Implementierung der Entity Klassen sind wie bereits beschrieben außerdem einige Beschränkungen der Google App Engine JPA Implementierung zu beachten. So wird z.B. in der @GeneratedValue Annotation nur die Strategie “GenerationType.IDENTITY” unterstützt, so dass auch diese Annotation entsprechend angepasst werden muss.
package de.alteskind.appengine.jpa;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
public class Message implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String message;
private String ip;
@Temporal(TemporalType.DATE)
private Date dateTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Date getDateTime() {
return dateTime;
}
public void setDateTime(Date dateTime) {
this.dateTime = dateTime;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof Message)) {
return false;
}
Message other = (Message) object;
if ((this.id == null && other.id != null) ||
(this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "de.alteskind.appengine.jpa.Message"
+ "[id=" + id + "]";
}
}
3. Implementieren der JPA Controller und Entity Manager Factory Klassen
Nachdem die Entity erfolgreich erstellt wurde, kann nun über einen Rechtsklick auf das Projekt und “New > Other…” und dann “Persistence > JPA Controller Classes from Entity Classes” automatisch eine JPA Controller (=DAO) Klasse für die Entität erstellt werden. Als Package sollte das Package der Message Entität ausgewählt werden.

Diese Klasse ist allerdings nicht direkt für den Einsatz in der Google App Engine geeignet, da sie folgende Exception verursacht:
java.lang.IllegalStateException: Application code attempted to create a EntityManagerFactory named GoogleAppEngine_JSF20PU, but one with this name already exists! Instances of EntityManagerFactory are extremely slow to create and it is usually not necessary to create one with a given name more than once. Instead, create a singleton and share it throughout your code. If you really do need to create a duplicate EntityManagerFactory (such as for a unittest suite), set the appengine.orm.disable.duplicate.emf.exception system property to avoid this error.
Um diesen Fehler zu vermeiden, sollte – wie auch in der offiziellen Google App Engine Dokumentation beschrieben – eine Entity Manager Factory als Singelton implementiert werden.
package de.alteskind.appengine.jpa.utils;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public final class EMF {
private static final EntityManagerFactory emfInstance =
Persistence.
createEntityManagerFactory("GoogleAppEngine_JSF20PU");
private EMF() {
}
public static EntityManagerFactory get() {
return emfInstance;
}
}
Nun kann die durch NetBeans generierte JPA Controller Klasse geändert werden, so dass die Entity Manager Factory verwendet wird. Es muss außerdem die SELECT Abfrage geändert werden, da die Google App Engine Datanucleus Implementierung den Operator “object” nicht unterstützt und die folgende Exception verursacht.
org.datanucleus.store.appengine.query.DatastoreQuery$
UnsupportedDatastoreOperatorException: Problem with query <SELECT object(o) FROM Message as o>: App Engine datastore does not support operator object.
Es tritt außerdem eine Exception auf, wenn versucht wird über das ResultSet des SELECT Abfrage zu iterieren (z.B. bei der Anzeige der Tabelle mit allen Messages) obwohl der EntityManager bereits geschlossen ist.
org.datanucleus.exceptions.NucleusUserException: Object Manager has been closed
Diese Verhalten ist allerdings nicht JPA konform und es existiert dazu auch bereits ein Bugreport. Um das Problem zu umgehen kann z.B. ein neues Object mit dem ResultSet erstellt werden. Außerdem liefert die Query in der “getMessageCount” Methode ein Integer zurück, daher muss der Cast von Long auf Integer geändert werden.
Die korrekte und unter der Google App Engine funktionierende JPA Controller Klasse ist in folgendem Listing aufgeführt.
package de.alteskind.appengine.jpa;
import de.alteskind.appengine.jpa.exceptions.
NonexistentEntityException;
import de.alteskind.appengine.jpa.utils.EMF;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Query;
import javax.persistence.EntityNotFoundException;
public class MessageJpaController {
private EntityManagerFactory emf = null;
public MessageJpaController() {
emf = EMF.get();
}
public EntityManager getEntityManager() {
return emf.createEntityManager();
}
public void create(Message message) {
EntityManager em = null;
try {
em = getEntityManager();
em.getTransaction().begin();
em.persist(message);
em.getTransaction().commit();
} finally {
if (em != null) {
em.close();
}
}
}
public void edit(Message message)
throws NonexistentEntityException, Exception {
EntityManager em = null;
try {
em = getEntityManager();
em.getTransaction().begin();
message = em.merge(message);
em.getTransaction().commit();
} catch (Exception ex) {
String msg = ex.getLocalizedMessage();
if (msg == null || msg.length() == 0) {
Long id = message.getId();
if (findMessage(id) == null) {
throw new NonexistentEntityException("The message "
+ "with id " + id + " no longer exists.");
}
}
throw ex;
} finally {
if (em != null) {
em.close();
}
}
}
public void destroy(Long id)
throws NonexistentEntityException {
EntityManager em = null;
try {
em = getEntityManager();
em.getTransaction().begin();
Message message;
try {
message = em.getReference(Message.class, id);
message.getId();
} catch (EntityNotFoundException enfe) {
throw new NonexistentEntityException("The message "
+" with id " + id + " no longer exists.", enfe);
}
em.remove(message);
em.getTransaction().commit();
} finally {
if (em != null) {
em.close();
}
}
}
public List
return findMessageEntities(true, -1, -1);
}
public List
int firstResult) {
return findMessageEntities(false, maxResults,
firstResult);
}
private List
int maxResults, int firstResult) {
EntityManager em = getEntityManager();
try {
Query q = em.createQuery("select o from Message as o");
if (!all) {
q.setMaxResults(maxResults);
q.setFirstResult(firstResult);
}
return new ArrayList
} finally {
em.close();
}
}
public Message findMessage(Long id) {
EntityManager em = getEntityManager();
try {
return em.find(Message.class, id);
} finally {
em.close();
}
}
public int getMessageCount() {
EntityManager em = getEntityManager();
try {
Query q = em.createQuery("select count(o) from Message "
+ "as o");
return ((Integer) q.getSingleResult()).intValue();
} finally {
em.close();
}
}
}
4. Implementieren der JSF 2.0 Managed Bean
Nun kann die JSF 2.0 ManagedBean implementiert werden. Dazu wird die aus dem ersten Teil des Tutorial erstellte Klasse “HelloWorld” gelöscht und stattdessen eine Klasse “MessageView” erstellt (siehe auch Teil 1 des Tutorials). Interessant sind dabei insbesondere die Methoden “postMessage”, “deleteMessage”, “getNumberOfMessages” und “getMessages”, welche Methoden der JPA Controller Klasse aufrufen.
package de.alteskind.appengine.jsf;
import de.alteskind.appengine.jpa.Message;
import de.alteskind.appengine.jpa.MessageJpaController;
import java.util.Date;
import java.util.List;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.component.html.HtmlDataTable;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;
@ManagedBean(name = "MessageView")
@RequestScoped
public class MessageView {
private String helloWorld;
private HtmlDataTable dataTable;
private Message message;
private List
private MessageJpaController messageJpaController;
public MessageView() {
this.helloWorld = "Hello World from JSF 2.0/Facelets "
+ "and JPA on Google AppEngine!";
this.message = new Message();
this.messageJpaController = new MessageJpaController();
}
public void postMessage() {
// Get HttpServletRequest object
FacesContext facesContext =
FacesContext.getCurrentInstance();
ExternalContext externalContext =
facesContext.getExternalContext();
HttpServletRequest request =
(HttpServletRequest) externalContext.getRequest();
// Set remote host as IP
message.setIp(request.getRemoteHost());
// Set timestamp
message.setDateTime(new Date());
// Create message
messageJpaController.create(message);
}
public void deleteMessage()
throws NonexistentEntityException {
// Get message object
Message message = (Message) dataTable.getRowData();
// Destroy message
messageJpaController.destroy(message.getId());
}
public int getNumberOfMessages() {
return messageJpaController.getMessageCount();
}
public String getHelloWorld() {
return helloWorld;
}
public void setDataTable(HtmlDataTable dataTable) {
this.dataTable = dataTable;
}
public HtmlDataTable getDataTable() {
return dataTable;
}
public Message getMessage() {
return message;
}
public void setMessage(Message message) {
this.message = message;
}
public List
return messageJpaController.findMessageEntities();
}
}
5. Erstellen der JSF/Facelets Seite
Als letzter Schritt wird nun die JSF/Facelets Seite index.html angepasst, so dass der Body folgenden Code enthält:
<h:form>
<h:outputText value="#{MessageView.helloWorld}" />
<br /><br />
<h:outputLabel value="Message:"/>
<h:inputText value="#{MessageView.message.message}"/>
<h:commandButton action="#{MessageView.postMessage}"
value="Post Message"/>
<br /><br />
<h:dataTable value="#{MessageView.messages}"
binding="#{MessageView.dataTable}"
rendered="#{MessageView.numberOfMessages > 0}"
var="message" border="1">
<h:column>
<f:facet name="header">
<h:outputText value="ID" />
</f:facet>
<h:outputText value="#{message.id}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Message" />
</f:facet>
<h:outputText value="#{message.message}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="IP" />
</f:facet>
<h:outputText value="#{message.ip}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Timestamp" />
</f:facet>
<h:outputText value="#{message.dateTime}">
<f:convertDateTime pattern="dd.MM.yyyy HH:mm" />
</h:outputText>
</h:column>
<h:column>
<h:commandButton action="#{MessageView.deleteMessage}"
value="Delete"/>
</h:column>
</h:dataTable>
</h:form>

Wie die große Anzahl der hier aufgeführten Exceptions zeigt, gilt es bei der Verwendung von JPA in der Google App Engine einiges zu beachten. Ich hoffe mein Blogeintrag hilft diese Probleme zu umgehen. ;-)
Analog zu Teil 1 des Tutorials kann das komplette NetBeans Projekt inkl. WAR-Archiv hier heruntergeladen sowie unter http://2.latest.altes-kind.appspot.com/ direkt aufgerufen werden.
#1 Markus - http://javathreads.de
13. Januar 2010 | 20:32 UhrMit einer einzigen Entity Bean hat das bei mir auch noch alles wunderbar funktioniert. Kniffliger wurde es bei einer 1:n Beziehung mit JPA. Hast du das auch mal ausprobiert?
#2 Matthias - http://altes-kind.de
13. Januar 2010 | 20:43 UhrHallo Markus :-),
nee… ich muss zugeben, dass ich mich bisher bzgl. Google App Engine auf das “billige” Beispiel beschränkt habe. Aber du hast Recht, ich lese in der App Engine Mailing List auch laufend “Need Help with One to Many relationships in Datastore…” Posts.
Ich werde mir das aber demnächst auch mal genauer anschauen. Vielleicht schreibe ich ja dann nochmal nen Blogeintrag. ;-)