Xây dựng chỉ số Khi chạy, nếu App Engine thực hiện một truy vấn không có chỉ số tương ứng, nó sẽ thất bại thảm hại. Theo mặc định, App Engine xây dựng một số chỉ số đơn giản cho bạn. Đối với các chỉ số phức tạp hơn, bạn sẽ phải xây dựng chúng bằng tay trong file cấu hình chỉ số, như trong ví dụ 7-2.
CHAPTER ■ USING THE APP ENGINE DATASTORE 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 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 148 CHAPTER ■ USING THE APP ENGINE DATASTORE 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(); } } } 149 CHAPTER ■ USING THE APP ENGINE DATASTORE 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 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 150 CHAPTER ■ USING THE APP ENGINE DATASTORE 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: A server-side service containing the methods that your client will invoke The client-side code that will invoke the service 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 151 CHAPTER ■ USING THE APP ENGINE DATASTORE 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 private private private private String project; String milestone; Boolean billable; Date date; 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; } 152 CHAPTER ■ USING THE APP ENGINE DATASTORE 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 import import import import import javax.jdo.annotations.IdGeneratorStrategy; javax.jdo.annotations.IdentityType; javax.jdo.annotations.PersistenceCapable; javax.jdo.annotations.Persistent; javax.jdo.annotations.PrimaryKey; java.util.Date; @PersistenceCapable(identityType = IdentityType.APPLICATION) public class TimeEntryEntity { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) 153 CHAPTER ■ USING THE APP ENGINE DATASTORE 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; } 154 CHAPTER ■ USING THE APP ENGINE DATASTORE 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; 155 CHAPTER ■ USING THE APP ENGINE DATASTORE 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: getProjects: Returns an Array of Strings for the Project picklist getMilestones: Accepts a project name and returns an Array of Strings for the Milestones picklist addEntries: Accepts a Vector of TimeEntryData objects and returns a String with the results of the datastore commit 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 { 156 CHAPTER ■ USING THE APP ENGINE DATASTORE String[] getProjects(); String[] getMilestones(String project); String addEntries(Vector entries) throws NotLoggedInException; Vector 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 import import import java.util.List; java.util.Vector; java.util.logging.Logger; java.util.logging.Level; import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; import javax.jdo.JDOHelper; import import import import com.google.appengine.api.users.User; com.google.appengine.api.users.UserService; com.google.appengine.api.users.UserServiceFactory; 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; 157 ... javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; import javax.jdo.JDOHelper; import import import import com .google. appengine.api.users.User; com .google. appengine.api.users.UserService; com .google. appengine.api.users.UserServiceFactory;... DataServiceImpl .java package com.appirio.timeentry.server; import import import import java. util.List; java. util.Vector; java. util.logging.Logger; java. util.logging.Level; import javax.jdo.PersistenceManager;... 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 private private