As of Spring Roo 1.1.5, the Roo-generated Spring Web MVC application doesn't work on GAE. The reason for this is related to mismatch in the JSTL version used by GAE and by the Roo-generated Spring MVC application. Also, Roo-generated JPA entities support only unowned relationships (refer http://code.google.com/appengine/docs/java/datastore/jdo/relationships.html to learn about owned and unowned relationships). In this recipe, we'll look at a Spring 3.0 Web MVC application (which uses JSTL tags that work on GAE) consisting of FlightDescription
and Flight
JPA entities and demonstrates how to create a unidirectional owned one-to-many relationship between JPA entities. The FlightDescription
entity is on the one side of one-to-many relationship.
If you are using Spring Roo 1.2.x, then the Roo-generated Spring Web MVC application can be deployed successfully on GAE.
If you only want to run the Spring Web MVC application locally using App Engine SDK for Java, then you don't need to sign-up with Google App Engine and create an application identifier. If you want to deploy the application on GAE, then follow the steps described in the previous recipe and ensure that you modify the application identifier in WEB-INF/appengine-web.xml
file. It is recommended that you deploy the application on GAE servers, as we'll also discuss some of the features offered by Admin Console.
Extract the ch06-gae-spring-mvc.zip
file that accompanies this book to the C:
oo-cookbook
directory. Extracting the ZIP file will create a directory named ch06-gae-spring-mvc
, which contains the flightapp-gae-spring-mvc
Eclipse project. The flightapp-gae-spring-mvc
project makes use of App Engine SDK version 1.4.2 and Maven GAE Plugin 0.8.2.
To deploy a Spring Web MVC application on GAE follow the steps given here:
C:
oo-cookbookch06-gae-spring-mvc
directory. Execute mvn
gae:run
command to deploy the flightapp-gae-spring-mvc
project on App Engine development web server:C:
oo-cookbookch06-gae-spring-mvc> mvn gae:run
http://localhost:8080
. If you see the following page, then it means that your flightapp-gae-spring-mvc
project is successfully deployed:Flight
and FlightDescription
objects. Selecting the Manage Flight Descriptions hyperlink allows users to create and view FlightDescription
instances. Also, it allows users to child Flight
instances associated with a FlightDescription
instance. The Manage Flights option allows users to create and delete Flight
instances.FlightDescription
instances in the application, as shown here:FlightDescription
instances, the table shows No record found message.FlightDescription
instance, as shown here:FlightDescription
details as shown in the given screenshot and click the Create button. The newly created FlightDescription
instance is displayed in the list of FlightDescription
instances, as shown here:FlightDescription
record shows the FlightDescription
details along with the Flight
details, which are associated with the FlightDescription
instance. The Delete button deletes the FlightDescription
instance along with the associated Flight
instances.Flight
instances, as shown here:Flight
instances, as shown here:Flight
instance (with FLT-100
as the flight name), which is a child of FlightDescription
instance (origin: NYC, destination: DELHI, price: 1200). Similarly, create flights with names FLT-200
, FLT-300
, and FLT-400
. Now, if you go to the FlightDescription
listing page and click the View button, then you'll see the newly created Flight
instances as a child of the NYC-DELHI-1200
FlightDescription
instance, as shown here:Flight
instances that are associated with FlightDescription
instance.FlightDescription
instance by selecting the Delete button (as shown in the next screenshot) corresponding to the NYC-DELHI-1200
FlightDescription
instance, then it'll also delete associated Flight
instances.GAE datastore is a non-relational database in which relationships between entities are modeled as either owned or unowned. An owned relationship is based on the concept of parent-child relationship, where the child entity instances cannot exist without a parent. In an unowned relationship, entity instances can exist irrespective of their relationship with other entities.
An owned relationship can be best visualized as a tree structure in which the root of the tree is an entity instance, which is the ultimate parent for all the entities. The following figure shows an owned relationship example in which FlightDescription
is the root entity with Flight
entity instances as its child entities, and Booking
entity instances are the child of the Flight
entity.
In the given figure, FlightDescription
(NYC-DELHI-1200) is at the root of the tree. The Flight
instances (FLT-100, FLT-200, FLT-300, FLT-400) are children of the FlightDescription
(NYC-DELHI-1200) instance. The Booking
(ashish, greg) instances are children of the Flight
(FLT-400) instance. If such a parent-child relationship is defined in the entities stored in the App Engine datastore, then these entities are referred to as part of the same entity group. You can think of an entity group as a tree with a root entity at the top and this root entity doesn't have any parents. In this figure, FlightDescription
(NYC-DELHI-1200) represents the root entity. The other important point about owned relationships is that except the root entity, all other entities in the entity group cannot be created without a parent. For example, Flight
instance cannot exist without a FlightDescription
instance and Booking
instance cannot exist without a Flight
instance.
In GAE, creating an owned relationship puts a restriction on the type of primary key that you can define for the child entity. The reason for this is that the child entity instance needs to know its parent in the entity group apart from its own entity ID. The type of primary key of the child entity in an owned relationship is either com.google.appengine.api.datastore.Key
or encoded String
form of com.google.appengine.api.datastore.Key
. GAE provides a KeyFactory
class (discussed later in this recipe), which you can use to create a Key
instance or to convert a Key
instance value to String
(referred to as encoded form of the Key
) and vice versa.
In an unowned relationship, each entity instance exists independently of each other. For example, the relationship between Student
and Course
entities is an example of an unowned relationship. A Student
entity can exist without a Course
and a Course
entity can exist without a Student
.
In our Flight Booking application, the relationship between FlightDescription
and Flight
entities is an example of an owned relationship, where Flight
entity instances cannot exist without a FlightDescription
instance. Let's now see how an owned one-to-many relationship has been created between the FlightDescription
and Flight
entities in the Flight Booking application.
The following code listing shows some of the important methods and attributes of the FlightDescription
entity:
import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; @Entity public class FlightDescription { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic private Key flightDescriptionId; @Basic @NotNull private String origin; ... @OneToMany private List<Flight> flights; ... @Transactional public void persist() { if (this.entityManager == null) this.entityManager = entityManager(); this.entityManager.persist(this); } @Transactional public static FlightDescription findFlightDescription(String key) { return (FlightDescription)entityManager().createQuery("select o from FlightDescription o where o.flightDescriptionId = :id").setParameter("id", KeyFactory.stringToKey(key)).getSingleResult(); } ... public String getFlightDescriptionKeyAsString() { return KeyFactory.keyToString(flightDescriptionId); } ... }
The given code shows that FlightDescription
entity's primary key is of type com.google.appengine.api.datastore.Key
. The Key
not only holds the entity's primary key, but also holds information about the entity group to which the entity instance belongs. As the FlightDescription
represents a root entity (that is, without a parent), the Key
field will not contain entity group information. The @OneToMany
annotated flights
field defines an owned relationship between the FlightDescription
and Flight
entities.
If it was an unowned relationship the flights
field would have taken the following form:
private List<Key> flights;
Here, the flights
field refers to the primary keys of Flight
entity instances. Also, the field is not annotated with the @OneToMany
annotation.
The findFlightDescription
method of the FlightDescription
entity takes encoded String
value of Key
to find the matching FlightDescription
. The KeyFactory
class provides keyToString
and stringToKey
to convert Key
into its encoded String
form and vice versa. This is particularly useful when you are creating relationships between entities via the user interface of the web application. The query to fetch the FlightDescription
object makes use of the KeyFactory
class to obtain Key
from its String
representation and use it as part of the query, as shown here again:
entityManager().createQuery("select o from FlightDescription o where o.flightDescriptionId = :id").setParameter("id", KeyFactory.stringToKey(key)).getSingleResult();
The persist
method of FlightDescription
shows that you don't need to set the value of the primary key. It is set by the App Engine.
The getFlightDescriptionKeyAsString
method of the FlightDescription
entity makes use of KeyFactory
class to return the primary key of the entity as an encoded String
value.
Let's now look at how Flight
entity is modeled.
The following code listing shows noteworthy attributes and methods of the Flight
entity:
@Entity public class Flight { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic private Key flightId; @Basic @NotNull private String flightName; private transient String encodedFlightDescriptionId; @Transactional public void persist() { if (this.entityManager == null) this.entityManager = entityManager(); Key parentKey = KeyFactory.stringToKey( encodedFlightDescriptionId); Key flightKey = KeyFactory.createKey(parentKey, Flight.class.getSimpleName(), flightName); setFlightId(flightKey); this.entityManager.persist(this); } public String getEncodedFlightDescriptionId() { return encodedFlightDescriptionId; } public String getFlightKeyAsString() { return KeyFactory.keyToString(flightId); } }
In the given code, flightId
represents the primary key of the Flight
entity. The persist
method of the Flight
entity makes use of the createKey
method of the KeyFactory
class to create the primary key. The createKey(parent,
kind,
name)
method accepts primary key information of the parent entity (FlightDescription
is the parent in the case of Flight
entity), the kind
(which is similar to the concept of tables in relational databases), and the name
of the key that uniquely identifies the entity within the kind. In the case of the Flight
entity's primary key, the kind refers to the simple name of the entity class: Flight.class.getSimpleName()
and the name of the key is the flightName
property of the Flight
entity. So, if you re-create a Flight
entity with flightName
as FLT-100
, then it will overwrite the existing Flight
(FLT-100) entity instance. GAE datastore doesn't support a composite primary key, but you can still achieve it by concatenating multiple entity field values in creating the name of the key.
The encodedFlightDescriptionId
is the String
form of the FlightDescription
primary key who is the parent of Flight
entity instance. The value of encodedFlightDescriptionId
is set when the user selects the FlightDescription
from the user interface for creating the Flight instance, as shown here:
The <option>
element corresponding to the FlightDescription
(NYC-DELHI-1200) entity instance is rendered as shown here:
<option value="<string value of Key>">NYC-DELHI for only 1200 USD</option>
The value
attribute specifies the String
form of the FlightDescription
primary key (which is of type Key
). So, when the user clicks the Create button, the String
value of Key
is bound to the encodedFlightDescriptionId
field of the Flight
instance.
The flightapp-gae-spring-mvc
project contains the KeyEditor
property editor class that uses KeyFactory
to perform conversion from Key
to String
format and vice versa. The property editor has been used instead of a Spring ConversionService
implementation because the ConversionService
implementation doesn't currently work with the <options>
tag of the Spring form
tag library.
Let's now look at how we can manage persisted data in GAE datastore using Admin Console.
You can view, edit, and delete data persisted by your application in GAE datastore by using Admin Console. Go to http://appengine.google.com and sign in to view the applications that you have created, as shown here:
The given screenshot shows the two applications that I have created in GAE. The roo-cookbook-spring-mvc
application corresponds to the application that we saw in this recipe. Selecting the application will show the Dashboard for the application where you can view different statistics related to your application. We are only interested in viewing the data that we saved in the datastore, so select the Datastore Viewer option from the Dashboard. The Datastore Viewer shows the entities that you saved in the datastore, as shown in the following screenshot:
The given screenshot shows the data for the FlightDescription
kind that we created. You can use the Query tab to query the datastore using GQL. You can also delete or edit the entity instances. If you change kind to Flight
, then the Admin Console will show the Flight
entities that you have created, as shown here:
The given screenshot shows that the name set for the primary key of the Flight
instance is displayed under the ID/Name column.
If you are designing your entities for App Engine, it is important to note that within a transaction you can only operate on entities which belong to the same entity group. So, if you have Student
and Course
entities, which are in different entity groups, then you can't create or update them in a single transaction.
As of Spring Roo 1.1.3, you can only create unowned relationship between entities. For instance, if you execute the field
reference
or field
set
command to add a relationship field, then the *_Roo_JavaBean.aj
AspectJ ITD file corresponding to the JPA entity removes the @OneToMany
or @OneToOne
or @ManyToOne
or @ManyToMany
annotation from the corresponding field in the JPA entity Java source file, resulting in an unowned relationship. The following code fragment shows how the AspectJ ITD file removes the relationship annotation from Java source file:
privileged aspect FlightDescription_Roo_JavaBean {
declare @field: * FlightDescription.flights: -@OneToMany;
...
}
The given code shows that if the FlightDescription
and Flight
entities were created using Spring Roo, then the @OneToMany
annotation on the flights
(which refers to a collection of Flight
entities) field of FlightDescription
will be removed by the FlightDescription_Roo_JavaBean.aj
AspectJ ITD file. The minus sign (-)
indicates that the @OneToMany
annotation will be removed from the FlightDescription.java
file.
18.189.171.153