The App Engine Java SDK includes implementations of two data access interface standards: the Java Persistence API (JPA) and Java Data Objects (JDO). These interfaces provide two essential features.
For one, these interfaces define a mechanism for describing the structure of data objects in terms of Java classes. You can use them to define and enforce consistent data schemas on top of App Engine’s schemaless datastore, and take advantage of type safety in the Java language. These interfaces serve as a data modeling layer.
Because each interface is a standard supported by other popular data storage solutions, using a standard interface makes it easier to port an application to and from these other solutions. Different databases have varying degrees of support for these standards. Since the standards were developed with SQL-based relational databases in mind, the App Engine datastore can only be said to support a portion of the standard, and it is often easier to port away from App Engine than to it. But this alone adds value, as you can reserve the right to move your app to your company’s own servers at any time. These interfaces are a portability layer.
The App Engine SDK uses an open source product called DataNucleus Access Platform as the basis for its implementations of JDO and JPA. Access Platform uses an adapter layer that translates both standards to an underlying implementation. The App Engine SDK includes an Access Platform adapter based on its low-level datastore API.
The JDO and JPA standards are similar, and share similar roots. The concepts that apply to the App Engine datastore have similar interfaces in both standards but with different terminology and minor behavioral differences. Which one you choose may depend on how familiar you are with it, or how well it is implemented for your most likely porting target, if you have one in mind.
In this chapter, we look at how to use JPA with App Engine. If you’d prefer to use JDO, check out the official documentation for App Engine, which includes a JDO tutorial.
A quick note on terminology: JPA refers to data objects as “entities.” This similarity to datastore entities is convenient in some ways, and not so convenient in others. For this chapter, we’ll refer to JPA entities as “data objects” (or just “objects”) to avoid confusion with datastore entities.
To use JPA, you must perform a few steps to set up the library.
JPA needs a configuration file that specifies that you want to use the App Engine implementation of the interface. This file is named persistence.xml, and should appear in your WAR’s WEB-INF/classes/META-INF/ directory. If you’re using Eclipse, you can create this file in the src/META-INF/ directory, and Eclipse will copy it to the final location automatically. It should look like this:
<?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.api.jpa.PersistenceProviderImpl </provider> <properties> <property name="datanucleus.NontransactionalRead" value="true"/> <property name="datanucleus.NontransactionalWrite" value="true"/> <property name="datanucleus.ConnectionURL" value="appengine"/> </properties> </persistence-unit> </persistence>
This configuration tells Access Platform to use the "appengine"
adapter. It also says to allow
reads and writes outside of transactions (NontransactionalRead
and NontransactionalWrite
are
true
), which fits the semantics of the datastore that we
described earlier. We named this configuration set
"transactions-optional"
to match.
Your application uses an EntityManager
object to
perform a set of datastore operations. The application creates an
EntityManager
, using an EntityManagerFactory
.
The factory loads the configuration file and uses it for subsequent
datastore interactions. You get an instance of the factory by calling a
static method and passing it the name of the configuration set
("transactions-optional"
):
import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; // ... EntityManagerFactory emfInstance = Persistence.createEntityManagerFactory("transactions-optional");
The createEntityManagerFactory()
static method performs
a nontrivial amount of work. You can think of the factory as a connection
pool, and each EntityManager
as an individual connection.
Since you only need one factory for the entire existence of the
application, a best practice is to call the method only once, store the
factory in a static member, and reuse it for multiple web requests.
package myapp; // where "myapp" is your app's package 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; } }
Access Platform hooks up the persistence plumbing to your JPA data classes in a post-compilation process that it calls “enhancement.” If you are using Eclipse with the Google Plugin, the plug-in performs this step automatically. If you are not using Eclipse, you must add the enhancement step to your build process. See the official documentation for information about performing this build step with Apache Ant.
In JPA, you define data classes as plain old Java objects (POJOs). You use annotations to tell JPA which classes to persist to the datastore, and how to store its members. Defining your data exclusively in terms of the Java classes your application uses makes it easy to manipulate your persistent data. It also makes it easy to test your application, since you can create mock data objects directly from the classes.
JPA also lets you use an XML file instead of annotations to describe how to persist data classes. We’ll only cover the annotation style here, but if you are familiar with the XML file mechanism, you can use it with Access Platform.
Here’s a simple example of a data class:
import java.util.Date; import javax.persistence.Entity; import javax.persistence.Id; @Entity(name = "Book") public class Book { @Id private String isbn; private String title; private String author; private int copyrightYear; private Date authorBirthdate; // ... constructors, accessors ... }
JPA knows instances of the Book
class can be made
persistent (saved to the datastore) because of the @Entity
annotation. This annotation takes a name
argument that
specifies the name to be used in JPA queries for objects of this class.
The name
must be unique across all data classes in the
application.
By default, the name of the datastore kind is derived from the name
of the class. Specifically, this is the simple name of the class, without
the package path (everything after the last .
, e.g.,
"Book"
). If you have two data classes with the same simple
name in different packages, you can specify an alternate kind name by
using the @Table
annotation. (JPA was designed with tabular
databases in mind, but the concept is equivalent.)
import javax.persistence.Entity; import javax.persistence.Table; @Entity(name = "Book") @Table(name = "BookItem") public class Book { // ... }
The Book
class has five members. Four of these members
are stored as properties on the datastore entity: title
,
author
, copyrightYear
, and
authorBirthdate
. The fifth member, isbn
, represents the key name for the entity.
JPA knows this because the member has the @Id
annotation, and
because the type of the member is String
.
Every data class needs a member that represents the object’s primary
key, annotated with @Id
. If the type of this member is String
and it has no other annotations, then
the key has no ancestors, and the value of the member is the string key
name. The application must set this field before saving the object for the
first time.
To tell JPA to let the datastore assign a unique numeric ID instead
of using an app-provided key name string, you declare the member with a
type of Long
and give it the annotation
@GeneratedValue(strategy = GenerationType.IDENTITY)
, like
so:
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity(name = "Book") public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // ... }
The member is set with the system-assigned ID when the object is saved to the datastore for the first time.
These simple key member types are sufficient for entities without
ancestors. Together with the entity kind ("Book"
), the member
represents the complete key of a root entity. If an instance of the class
may represent an entity with ancestors, the key member must be able to
represent the full key path. There are two ways to do this.
One way is to declare the type of the key member to be the
com.google.appengine.api.datastore.Key
class:
import javax.persistence.Entity; import javax.persistence.Id; import com.google.appengine.api.datastore.Key; @Entity(name = "Book") public class Book { @Id private Key id; // ... }
You can use this key member type to create a complete Key
with a string name. You can also use
system-assigned numeric IDs with ancestors by using the @GeneratedValue
annotation, then assigning a
Key
value with neither the name nor the ID set.
If you’d prefer not to create a dependency on an App Engine–specific
class, there is another way to implement a key with ancestors. Simply
declare the ID field’s type as String
and use a DataNucleus JPA extension that
encodes the complete key as a string value, like so:
import javax.persistence.Entity; import javax.persistence.Id; import org.datanucleus.api.jpa.annotations.Extension; import com.google.appengine.api.datastore.Key; @Entity(name = "Book") public class Book { @Id @Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true") private String id; // ... }
You can convert between a Key
and a string-encoded key
using the KeyFactory
class’s keyToString()
and
stringToKey()
methods. (Note that the Key
class’s toString()
method returns something else.)
You can use a Key
ID field or a string-encoded ID field
in combination with the @GeneratedValue
annotation
to produce keys with ancestors and system-assigned numeric IDs.
The fields of the object become the properties of the
corresponding entity. The name of a field is used as the name of the
property. The @Id
field is not stored as a property value,
only as the key.
JPA and App Engine support many types of fields. Any of the types mentioned in Table 5-1 can be used as a field type. A field can contain a serializable object, stored as a single property. A field can also be a collection of one of the core datastore types or a serializable class, to be stored as a multivalued property. Additionally, App Engine supports JPA embedded data objects and relationships between entities using fields.
In some cases, JPA must be told which fields to save to the
datastore. For the Java standard types (such as Long
or String
or
Date
), JPA assumes that fields of those types should be
saved. For other types, especially the datastore-specific classes such as
datastore.ShortBlob
, you must tell JPA
to save the field by giving it the @Basic
annotation. If you have a field that
should not be saved to the datastore, give it the @Transient
annotation:
import java.util.List; import javax.persistence.Basic; import javax.persistence.Id; import javax.persistence.Transient; import com.google.appengine.api.datastore.ShortBlob; @Entity(name = "Book") public class Book { // ... private String title; // saved @Basic // saved private ShortBlob coverIcon; @Basic // saved private List<String> tags; @Transient // not saved private int debugAccessCount; }
As with the low-level API, some types are widened before being
stored. int
and Integer
are converted to
Long
, and float
and Float
become Double
. With the JPA
interface, these values are converted back to the declared field types
when loaded into an object.
A Serializable
class can be used as a field type,
using the @Lob
annotation. These values are stored in
serialized form as datastore.Blob
values. As such, these
values are not indexed, and cannot be used in queries.
Collection types are stored as multivalued properties in iteration order. When loaded into the data class, multivalued properties are converted back into the specified collection type.
By default, the name of a field is used as the name of the
corresponding property. You can override this by using the
@Column
annotation:
import javax.persistence.Column; import javax.persistence.Entity; @Entity(name = "Book") public class Book { // ... @Column(name = "long_description") private String longDescription; }
You can declare that the datastore property of a field should not be
mentioned in indexes—the property of each entity should be created as a
nonindexed property—using an @Extension
annotation:
import org.datanucleus.api.jpa.annotations.Extension; @Entity(name = "Book") public class Book { // ... @Extension(vendorName = "datanucleus", key = "gae.unindexed", value = "true") private String firstSentence; }
App Engine supports JPA embedded classes by storing the
fields of the embedded class as properties on the same datastore entity as
the fields of the primary class. You must declare the class to embed using
the @Embeddable
annotation:
import javax.persistence.Embeddable; @Embeddable public class Publisher { private String name; private String address; private String city; private String stateOrProvince; private String postalCode; // ... }
To embed the class, simply use it as a field type:
import javax.persistence.Entity; import Publisher; @Entity(name = "Book") public class Book { // ... private Publisher publisher; }
Because fields of embedded classes are stored as separate
properties, they are queryable just like other properties. You can refer
to an embedded field in a property with the name of the outer field with a
dot-notation, such as publisher.name
. The actual property
name is just the name of the inner field, and you can change this if
needed, using an @Column
annotation.
To start a session with the datastore, you use the
EntityManagerFactory
to create an EntityManager
.
You must create a new EntityManager
for each request handler,
and close it when you’re done:
import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import myapp.EMF; // where "myapp" is your app's package // ... EntityManagerFactory emf = EMF.get(); EntityManager em = null; try { em = emf.createEntityManager(); // ... do datastore stuff ... } finally { if (em != null) em.close(); }
To create a new data object, you construct the data class and then
call the EntityManager
’s persist()
method with
the object:
import myapp.Book; // our data class // ... EntityManager em = null; try { em = emf.createEntityManager(); Book book = new Book(); book.setTitle("The Grapes of Wrath"); // ... em.persist(book); } finally { if (em != null) em.close(); }
If you create an object with a complete key, and an entity with that
key already exists in the datastore, saving the new object will overwrite
the old one. In App Engine’s implementation, JPA’s merge()
method is equivalent to persist()
in this way. (Other implementations
may do something different in this case.)
To fetch an entity with a known key, you use the find()
method. This method takes the class of the object in which to load the
entity, and the key of the object. The key can be any appropriate type: a
string key name, a numeric ID, a datastore.Key
object, or a
string-encoded complete key. The method returns an object of the given
class, or null
if no object with that key is found:
Book book = em.find(Book.class, "9780596156732"); if (book == null) { // not found }
The ability of find()
to accept all four key types is
nonstandard. To make your code more portable, only call
find()
, using the type of key you used in the data
class.
When you create or fetch an entity (or get an entity back from a
query), the data object becomes “attached” to (or managed by) the entity
manager. If you make changes to an attached object and do not save them by
calling the persist()
method explicitly, the object is saved
automatically when you close the entity manager. As we’ll see in the next
section, if you need the entity to be updated in a transaction, you pass
the updated object to the persist()
method at the moment it
needs to be saved.
To delete an entity, you call the remove()
method. This
method takes the data object as its sole argument. The object still exists
in memory after it is removed from the datastore:
em.remove(book);
The remove()
method requires a loaded data object.
There is no way to delete an entity with this method without fetching its
object first. (You can delete entities without fetching them by using a
JPQL delete query. See the section Queries and JPQL.)
The API for performing transactions in JPA is similar to the
low-level datastore API. You call a method on the entity manager to create
a Transaction
object, then call methods on the object to
begin and commit or roll back the transaction:
import javax.persistence.EntityTransaction; // ... EntityTransaction txn = em.getTransaction(); txn.begin(); try { Book book = em.find(Book.class, "9780596156732"); BookReview bookReview = new BookReview(); bookReview.setRating(5); book.getBookReviews().add(bookReview); // Persist all updates and commit. txn.commit(); } finally { if (txn.isActive()) { txn.rollback(); } }
The JPA transaction interface was designed for databases that support global transactions, so it knows nothing of App Engine’s local transactions and entity groups. It’s up to the application to know which operations are appropriate to perform in a single transaction. You can manage entity groups and ancestors by using App Engine’s extensions to JPA.
One way to set up a data class that can represent entities with
parents is to use either a datastore.Key
or a string-encoded
key for the @Id
field. When you create a new object, you can
construct the complete key, including ancestors, and assign it to this
field.
Alternatively, you can establish a second field to contain the
parent key as either a Key
or string-encoded key, using an
extension:
import javax.persistence.Basic; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import org.datanucleus.api.jpa.annotations.Extension; @Entity public class BookReview { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true") private String keyString; @Basic @Extension(vendorName = "datanucleus", key = "gae.parent-pk", value = "true") private String bookKeyString; }
The parent key field makes it easier to port your application to another database at a later time. It declares a slot in the data class for the ancestor relationship that is separate from the entity’s key name.
The parent key field is required if you want to perform queries with ancestor filters. As we’ll see in the next section, JPA queries must refer to fields on the data class.
The App Engine implementation of JPA includes features for managing entity groups automatically using object relationships. We’ll discuss relationships later in this chapter.
JPA includes a SQL-like query language called JPQL. JPQL provides access to the underlying database’s query functionality at the level of abstraction of JPA data objects. You form queries for data objects in terms of the data classes, and get objects as results.
To perform a query, you call the entity manager’s
createQuery()
method with the text of the JPQL query. This
returns a Query
object. To get the results, you call getResultList()
on the Query
object:
import java.util.List; import javax.persistence.Query; // ... Query query = em.createQuery("SELECT b FROM Book b"); @SuppressWarnings("unchecked") List<Book> results = (List<Book>) query.getResultList();
In this example, the cast to List<Book>
generates
a compiler warning, so we suppress this warning by using an
@SuppressWarnings
annotation.
JPA knows which class to use for each result from the
@Entity(name = "...")
annotation on the class. You can also
use the full package path of the class in the query.
You can use parameters in your JPQL query, and replace the
parameters with values by calling setParameter()
:
Query query = em.createQuery( "SELECT b FROM Book b WHERE copyrightYear >= :earliestYear"); query.setParameter("earliestYear", 1923);
getResultList()
returns a special App Engine–specific
implementation of List
that knows how to fetch results in
batches. If you iterate over the entire list, the List
implementation may make multiple calls to the datastore to fetch
results.
If you are only expecting one result, you can call
getSingleResult()
instead. This gets the first result if any,
or null
if there are no results:
Book book = (Book) query.getSingleResult();
You can fetch a range of results by setting an offset and a maximum
number of results, using the setFirstResult()
and
setMaxResults()
methods before calling getResultList()
:
// Get results 5-15. query.setFirstResult(4); query.setMaxResults(10); @SuppressWarnings("unchecked") List<Book> results = (List<Book>) query.getResultList();
The syntax of JPQL is straightforward, and similar to SQL or the Python API’s GQL. JPQL keywords can be all uppercase or all lowercase, and are shown as uppercase here, as is tradition. Class and field names are case-sensitive. The query begins by identifying the simple name of the class of objects to query, corresponding to the kind of the entities:
SELECT b FROM Book b
This query returns all Book
data objects, where
Book
is the value of the name
argument to the @Entity
annotation on the data class (which happens to also be named
Book
). The class name is followed by an identifier
(b
); stating that identifier after the word
SELECT
tells JPA to return objects of that class as
results.
To perform a keys-only query, give the name of the key field instead
of the class identifier. The methods that return results return values of
the type used for the @Id
field in the data class:
Query query = em.createQuery("SELECT isbn FROM Book"); @SuppressWarnings("unchecked") List<String> results = (List<String>) query.getResultList();
The App Engine implementation of JPQL supports queries for specific
fields, although perhaps not in the way you’d expect. For a query for
specific fields, the datastore returns the complete data for each entity
to the application, and the interface implementation selects the requested
fields and assembles the results. This is only true if one of the
requested fields is a datastore property, and is not true if the only
field is a key field (@Id
).
If the query is for one field, each result is a value of the type of
that field. If the query is for multiple fields, each result is an
Object[]
whose elements are the field values in the order
specified in the query:
Query query = em.createQuery("SELECT isbn, title, author FROM Book"); // Fetch complete Book objects, then // produce result objects from 3 fields // of each result @SuppressWarnings("unchecked") List<Object[]> results = (List<Object[]>) query.getResultList(); for (Object[] result : results) { String isbn = (String) result[0]; String title = (String) result[1]; String author = (String) result[2]; // ... }
You specify filters on fields by using a WHERE
clause
and one or more conditions:
SELECT b FROM Book b WHERE author = "John Steinbeck" AND copyrightYear >= 1940
To filter on the entity key, refer to the field that represents the
key in the data class (the @Id
field):
SELECT b FROM Book b WHERE author = "John Steinbeck" AND isbn > :firstKeyToFetch
You can perform an ancestor filter by establishing a parent key field (as we did in the previous section) and referring to that field in the query:
SELECT br FROM BookReview br WHERE bookKey = :pk
As with find()
, you can use any of the four key types
with parameterized queries, but the most portable way is to use the type
used in the class.
You specify sort orders by using an ORDER BY
clause.
Multiple sort orders are comma-delimited. Each sort order can have a
direction of ASC
(the default) or DESC
.
SELECT b FROM Book b ORDER BY rating DESC title
The App Engine implementation of JPQL includes a couple of additional tricks that the datastore can support natively. One such trick is the string prefix trick:
SELECT b FROM Book b WHERE title LIKE 'The Grape%'
The implementation translates this to WHERE title >= 'The
Grape'
, which does the same thing: it returns all books with a
title that begins with the string The Grape
, including
"The Grape"
, "The Grapefruit"
, and "The
Grapes of Wrath"
.
This trick only supports a single wildcard at the end of a string. It does not support a wildcard at the beginning of the string.
Another trick App Engine’s JPQL implementation knows how to do is to translate queries on key fields into batch gets. For example:
SELECT b FROM Book b WHERE isbn IN (:i1, :i2, :i3)
This becomes a batch get of three keys, and does not perform a query at all.
In addition to these SELECT
queries, App Engine’s JPA
implementation supports deleting entities that meet criteria with JPQL. A
delete query can include filters on keys and properties to specify the
entities to delete:
DELETE FROM Book b WHERE isbn >= "TEST_000" AND isbn <= "TEST_999"
To execute a DELETE
query, call the Query
object’s executeUpdate()
method. This method returns no
results.
As with other mechanisms for modifying data, if you perform a delete query outside of a transaction, it is possible for a delete of one entity to fail while the others succeed. If you perform it inside a transaction, it’ll be all or nothing, but all entities must be in the same entity group, and the delete query must use an ancestor filter.
The JPA specification supports many features of queries that are common to SQL databases, but are not supported natively in the App Engine datastore. With a SQL database, using one of these features calls the database directly, with all the performance implications (good and bad) of the datastore’s implementation.
When an app uses a feature of JPQL that the underlying database does not support, DataNucleus Access Platform tries to make up the difference using its own in-memory query evaluator. It attempts to load all the information it needs to perform the query into memory, execute the nonnative operations itself, then return the result.
Because such features are potential scalability hazards—an
AVG()
query would require fetching every entity of the
kind, for example—the App Engine implementation disables the Access
Platform in-memory query evaluator.
Most useful data models involve relationships between classes of data objects. Players are members of guilds, book reviews are about books, messages are posted to message boards, customers place orders, and orders have multiple line items. For logical reasons or architectural reasons, two concepts may be modeled as separate but related classes. Those relationships are as much a part of the data model as the data fields of the objects.
In the App Engine datastore (and most databases), one easy way to
model a relationship between two objects is to store the entity key of one
object as a property of the other, and (if needed) vice versa. The
datastore supports Key
values as a native property value
type, and also provides a way to encode key values as strings. You don’t
need any help from JPA to model relationships this way.
But relationships are so important to data modeling that JPA has a
family of features to support them. In JPA, you can define owned
relationships in the data model that enforce constraints by
managing changes. With owned relationships, you can say that a book has
zero or more book reviews, and JPA ensures that you can’t have a book
review without a book. If you delete a Book
object, JPA knows to also delete all its
BookReview
objects. In the Java code, the
relationship is represented by a field whose type is of the related data
class, ensuring that only the appropriate classes are used on either side
of the relationship.
The App Engine implementation of JPA supports one-to-one and one-to-many relationships. It does not yet support JPA’s notion of many-to-many relationships.
An unowned relationship is a
relationship without these constraints. App Engine supports unowned
relationships through the storing of literal key values, but does not yet
support them through JPA. You can use multivalued properties of
Key
values to model unowned one-to-one, one-to-many, and
many-to-many relationships.
To completely support the semantics of JPA owned relationships, App Engine stores objects with owned relationships in the same entity group. It’s easy to see why this has to be the case. If one object is deleted within a transaction, the relationship says the related object must also be deleted. But to do that in the same transaction requires that both objects be in the same entity group. If one object is deleted outside of a transaction, then the other object must be deleted in a separate operation, and if one delete or the other fails, an object remains that doesn’t meet the relationship requirement.
While the use of entity groups may sound constraining, it’s also a powerful feature. You can use JPA owned relationships to perform transactions on related entities, and the JPA implementation will manage entity groups for you automatically.
You specify an owned one-to-one relationship by creating a field
whose type is of the related class, and giving the field an
@OneToOne
annotation. For example, you could associate each
book with a cover image, like so:
import javax.persistence.Entity; import javax.persistence.OneToOne; import bookstore.BookCoverImage; @Entity(name = "Book") public class Book { // ... @OneToOne(cascade=CascadeType.ALL) private BookCoverImage bookCoverImage; }
This annotation declares a one-to-one relationship between the
Book
and BookCoverImage
classes.
In every relationship, one class “owns” the relationship. The owner
of a relationship is responsible for propagating changes to related
objects. In this example, the Book
class is the “owner” of
the relationship.
The cascade=CascadeType.ALL
argument annotation says
that all kinds of changes should propagate to related objects (including
PERSIST
, REFRESH
, REMOVE
, and
MERGE
). For example:
// EntityManager em; // ... Book book = new Book(); book.setBookCoverImage(new BookCoverImage()); book.setTitle("The Grapes of Wrath"); book.bookCoverImage.setType("image/jpg"); EntityTransaction txn = em.getTransaction(); txn.begin(); try { em.persist(book); txn.commit(); } finally { if (txn.isActive()) { txn.rollback(); } } em.close();
This code creates a Book
and a related
BookCoverImage
. When it makes the Book
persistent, the BookCoverImage
is made persistent
automatically (the PERSIST
action cascades). Similarly, if we
were to delete the Book
, the BookCoverImage
would also be deleted (the DELETE
action cascades). Cascading
actions follow all ownership paths from “owner” to “owned,” and do the
right thing if the objects they find have changed since they were loaded
from the datastore.
You can have JPA populate a field on the “owned” class that points back to the owner automatically, like so:
import javax.persistence.Entity; import javax.persistence.OneToOne; import bookstore.Book; @Entity(name = "BookCoverImage") public class BookCoverImage { // ... @OneToOne(mappedBy="bookCoverImage") private Book book; }
The mappedBy
argument tells JPA that the
book
field refers to the Book
object that is
related to this object. This is managed from the “owner” side of the
relationship: when the BookCoverImage
is assigned to the
Book
’s field, JPA knows that the back-reference refers to the
Book
object.
To specify a one-to-many relationship, you use a field type
that is a List
or Set
of the related class,
and use the @OneToMany
annotation on the “one” class, with
a mappedBy
argument that refers to the property on the
entities of the “many” class:
import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.OneToMany; import bookstore.BookReview; @Entity(name = "Book") public class Book { // ... @OneToMany(cascade=CascadeType.ALL, mappedBy="book") private List<BookReview> bookReviews = null; }
To create a back-reference from the “many” class to the “one” class,
you use a @ManyToOne
annotation, with no
arguments:
// BookReview.java @Entity(name = "BookReview") public class BookReview { // ... @ManyToOne() private Book book; } // Book.java @Entity(name = "Book") public class Book { // ... @OneToMany(cascade=CascadeType.ALL, mappedBy="book") private List<BookReview> bookReviews; }
In a one-to-many relationship, the “one” is always the owner
class, and the “many” is the owned class. In a one-to-one relationship,
JPA knows which is the “owned” class by the absence of a back-reference
field, or a back-reference field mentioned by a mappedBy
annotation argument: the side with the
mappedBy
is the owned class.
When you fetch a data object that has a relationship field,
JPA does not fetch the related objects right away. Instead, it waits until
you access the field to fetch the object (or objects). This is called
“lazy” fetching, and it saves your app from unnecessary datastore
operations. The App Engine implementation of JPA only supports lazy
fetching (FetchType.LAZY
), and does not yet support its
opposite, “eager” fetching (FetchType.EAGER
). Note that you
must access related objects prior to closing the EntityManager
, so they are fetched into
memory:
// Fetch a Book, but not its BookCoverImage. Book book = em.find(Book.class, "9780596156732"); // ... // The BookCoverImage is fetched when it is first accessed. resp.setContentType(book.bookCoverImage.type);
In the datastore, the relationship is represented using ancestors. The owner object is the parent, and all owned objects are children. When you access the relationship field on the owner object, the interface uses an ancestor query to get the owned objects. When you access a back-reference to the owner from the owned object, the interface parses the owned object’s key to get the parent.
Related objects are created in the same entity group, so they can
all be updated within the same transaction if necessary. The owner’s
entity is created first (if necessary) and becomes the parent of the owned
objects’ entities. If you declare a back-reference by using
mappedBy
, no property is stored on the owned object’s entity.
Instead, when the field is dereferenced, the implementation uses the owned
object’s key path to determine the owner’s key and fetches it.
The App Engine implementation does not support many-to-one relationships where the “many” is the owner. That is, it does not support a one-to-many relationship where actions cascade from the many to the one.
Creating new relationships between existing data classes can be tricky, because the entity group requirements must be met in the migrated data. Adding a relationship to the owner class is like adding a field: the entities that represent instances of the owner class must be updated to have appropriate key properties. The “owned” side is trickier: since an owned object’s entity must have the owner as its parent, if the owned object already exists in the datastore, it must be deleted and re-created with the new parent. You can’t change an entity’s parent after the entity has been created.
This use of datastore ancestors means you cannot reassign an owned object to another owner after it has been saved to the datastore. This also means that one object cannot be on the “owned” side of more than one relationship, since the entity can have only one parent.
Relationships and cascading actions imply that an operation on a
data object can translate to multiple datastore operations on multiple
entities, all in the same entity group. If you want these operations to
occur in a single transaction, you must perform the initial operation
(such as em.merge(...)
) within an explicit
transaction.
If you perform a cascading action outside of an explicit transaction, each of the datastore operations performed by JPA occurs in a separate operation. Some of these operations may fail while others succeed. As such, it’s a best practice to perform all JPA updates within explicit transactions, so there is no confusion as to what succeeded and what failed.
You can perform queries on relationship fields in JPQL by using key values. For a query on the owner class, the query is a simple key property query:
SELECT FROM Book b WHERE bookCoverImage = :bci AND publishDate > :pdate
For a query on an owned class, a query on the back-reference field becomes an ancestor filter:
SELECT FROM BookCoverImage bci WHERE book = :b
You cannot refer to properties of the related entity in the query filter. App Engine does not support join queries.
The JDO and JPA interfaces have many useful features that work with the App Engine implementation. One excellent source of documentation on these interfaces is the DataNucleus Access Platform website:
http://www.datanucleus.org/products/accessplatform/ |
The App Engine implementation is an open source project hosted on Google Code. The source includes many unit tests that exercise and demonstrate many features of JDO and JPA. You can browse the source code at the project page:
http://code.google.com/p/datanucleus-appengine/ |
3.129.23.181