CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 148 Building Indexes At runtime, if App Engine executes a query with no corresponding index, it will fail miserably. By default, App Engine builds a number of simple indexes for you. For more complex indexes, you will have to build them manually in the index configuration file, as shown in Listing 7-2. Listing 7-2. Sample datastore-index.xml file <?xml version="1.0" encoding="utf-8"?> <datastore-indexes xmlns="http://appengine.google.com/ns/datastore-indexes/1.0" autoGenerate="true"> <datastore-index kind="Contact" ancestor="false"> <property name="countryName" direction="asc" /> </datastore-index> </datastore-indexes> Indexes are built automatically by App Engine for queries that contain: • Single property inequality filters • Only one property sort order (ascending or descending) and no filters • Inequality or range filters on keys and equality filters on properties • Only ancestor and equality filters You must specify in the index configuration file any queries containing: • Multiple sort orders • Inequality and ancestor filters • A sort order on multiple keys in descending order • One or more inequality filters on a property and one or more equality filters over the properties Creating Indexes In Development Mode During development, App Engines tries to create your indexes for you in the configuration file. If the development web server encounters a query that does not have a corresponding index, it will try to create an index for you automatically. If your unit tests call every possible query for your application, the generated configuration file will contain a complete set of all indexes. This is where confusion creeps into the process. If CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 149 you think your tests call all possible queries but your application still fails at runtime, you’ll have to edit the datastore-index.xml file and add these indexes manually. Using Transactions At a high level, the App Engine datastore supports transactions like most relational databases. A transaction consists of one or more database operations that either succeed or fail in entirety. If a transaction succeeds, then all operations are committed to the datastore. However, if one of the operations fails, then all operations are rolled back to their original state. An example method using transactions is shown in Listing 7-3. Listing 7-3. Sample transaction import javax.jdo.Transaction; public void createContact(Contact contact, String accountId) { PersistenceManager pm = PMF.get().getPersistenceManager(); Transaction tx = pm.currentTransaction(); try { // start the transaction tx.begin(); // persist the contact pm.makePersistent(contact); // fetch the parent account Account account = pm.getObjectById(Account.class, accountId); account.incrementContacts(1); pm.makePersistent(account); // commit if no errors tx.commit(); } finally { // roll back the transactions in case of an error if (tx.isActive()) { tx.rollback(); } } } CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 150 All entities in the datastore belong to an entity group. Entities in the same group are stored in the same part of Google’s distributed network. Better distribution across database nodes improves performance when creating and updating data. When creating a new entity, you can assign an existing entity as its parent so that the new entity becomes part of that entity group. If you do not specify a parent for an entity, it is considered a root entity. The datastore places restrictions on what operations can be performed inside a single transaction: • Your application can perform a query inside a transaction but only if the query includes an ancestor filter to retrieve all descendants of the specific entity. • A transaction must operate only on entities in the same entity group. • If your transaction fails, your application must try again programmatically. JDO will not attempt to retry the transaction automatically, like most systems with optimistic concurrency. • A transaction can only update an entity once. Finishing Up Your Application Now that you have a good understanding of the App Engine datastore and how to use JDO to interact with it, you can finish up the application. You’ll need to tie various parts of your application into the datastore using GWT RPC to create a fully functioning application, following these steps: • Populate your Projects picklist with values. • Populate your Milestones picklist with values based on the selected project. • Implement your Save handler to persist your timecard entries to the datastore. • Display the current user’s timecard entries from the datastore. Making Remote Procedure Calls with GWT RPC Similar to your authentication service, your data service will use GTW RPC to communicate with your server (see Figure 7-1). You’ll create a server-side service that CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 151 is invoked by your client to fetch and save timecard entries and related project information. You will need to implement the following components to round out your application: 1. A server-side service containing the methods that your client will invoke 2. The client-side code that will invoke the service 3. A serializable POJO containing your actual timecard data that is passed between your server and client Figure 7-1. Your GWT RPC components model TimeEntryData POJO Your client and server will need a POJO to pass data back and forth. The POJO in Listing 7-4 will be a single timecard entry that will be persisted to the datastore. When using GWT RPC, the class, parameters, and return types must be serializable so that the object can be moved from layer to layer. CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 152 Listing 7-4. The TimeEntryData POJO package com.appirio.timeentry.client; import java.io.Serializable; import java.util.Date; public class TimeEntryData implements Serializable { private String project; private String milestone; private Boolean billable; private Date date; private double hours; public String getProject() { return project; } public void setProject(String project) { this.project = project; } public String getMilestone() { return milestone; } public void setMilestone(String milestone) { this.milestone = milestone; } public Boolean getBillable() { return billable; } public void setBillable(Boolean billable) { this.billable = billable; } public Date getDate() { return date; } CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 153 public void setDate(Date date) { this.date = date; } public double getHours() { return hours; } public void setHours(double hours) { this.hours = hours; } } ■ Note GWT serialization is a little different from the Java Serializable interface. Check out the GWT Developer’s Guide for details on the differences and reasons behind them. TimeEntryEntity JDO Class Your TimeEntryData POJO is transferred across the wire to your server and is deserialized automatically. For flexibility, you’re going to create the JDO class in Listing 7-5 for persisting your instances to the datastore. Listing 7-5. The code for your JDO class, TimeEntryEntity.java package com.appirio.timeentry.server; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; import java.util.Date; @PersistenceCapable(identityType = IdentityType.APPLICATION) public class TimeEntryEntity { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 154 private Long id; @Persistent private String email; @Persistent private String project; @Persistent private String milestone; @Persistent private Boolean billable; @Persistent private Date date; @Persistent private double hours; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getProject() { return project; } public void setProject(String project) { this.project = project; } public String getMilestone() { return milestone; } CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 155 public void setMilestone(String milestone) { this.milestone = milestone; } public Boolean getBillable() { return billable; } public void setBillable(Boolean billable) { this.billable = billable; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } public double getHours() { return hours; } public void setHours(double hours) { this.hours = hours; } } NotLoggedIn Exception When interacting with the datastore, your service needs to ensure that the user is logged in to the application with her Google account. If the user has not logged in or her session has expired, you need to handle this by throwing the NotLoggedInException shown in Listing 7-6. Listing 7-6. The code for the NotLoggedInException package com.appirio.timeentry.client; import java.io.Serializable; CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 156 public class NotLoggedInException extends Exception implements Serializable { public NotLoggedInException() { super(); } public NotLoggedInException(String message) { super(message); } } Creating Your Data Service In order to create your data service for your server, you need to define both a service interface and the actual service. For your service interface you need to define the interface extending the GWT RemoteService interface,GWT RemoteService interface, as shown in Listing 7-7. Your service will consist of the following methods that will be called from your client: 1. getProjects: Returns an Array of Strings for the Project picklist 2. getMilestones: Accepts a project name and returns an Array of Strings for the Milestones picklist 3. addEntries: Accepts a Vector of TimeEntryData objects and returns a String with the results of the datastore commit 4. getEntries: Returns a Vector of TimeEntryData objects containing the current timecard entries for the current user Listing 7-7. Your data service extending the GWT RemoteService package com.appirio.timeentry.client; import java.util.Vector; import com.google.gwt.user.client.rpc.RemoteService; import com.google.gwt.user.client.rpc.RemoteServiceRelativePath; import com.appirio.timeentry.client.TimeEntryData; @RemoteServiceRelativePath("data") public interface DataService extends RemoteService { CHAPTER 7 ■ USING THE APP ENGINE DATASTORE 157 String[] getProjects(); String[] getMilestones(String project); String addEntries(Vector<TimeEntryData> entries) throws NotLoggedInException; Vector<TimeEntryData> getEntries() throws NotLoggedInException; } ■ Note Notice the @RemoteServiceRelativePath annotation. You’ll define this path in the deployment descriptor based on the relative path of the base URL. The guts of your service reside in the DataServiceImpl class shown in Listing 7-8. The methods defined in your interface are implemented in addition to a number of helper methods. This class extends GWT RemoteServiceServlet and does the heavy lifting of serializing responses and deserializing requests for you. Since the servlet runs as Java bytecode instead of JavaScript on the client, you are not hamstrung by the functionality of the browser. Listing 7-8. The entire listing for DataServiceImpl.java package com.appirio.timeentry.server; import java.util.List; import java.util.Vector; import java.util.logging.Logger; import java.util.logging.Level; import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; import javax.jdo.JDOHelper; import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; import com.appirio.timeentry.client.NotLoggedInException; import com.appirio.timeentry.client.DataService; import com.appirio.timeentry.client.TimeEntryData; import com.google.gwt.user.server.rpc.RemoteServiceServlet; . import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; import javax.jdo.JDOHelper; import com .google. appengine.api.users.User; import com .google. appengine.api.users.UserService;. javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; import java. util.Date;. com .google. appengine.api.users.UserService; import com .google. appengine.api.users.UserServiceFactory; import com.appirio.timeentry.client.NotLoggedInException; import com.appirio.timeentry.client.DataService; import com.appirio.timeentry.client.TimeEntryData;