将数据存储在可扩展的网络应用程序中可能会非常复杂。用户可在指定时间与任意数量的网络服务器进行交互,并且该用户的下一个请求可能会转到与处理上一个请求不同的网络服务器上。所有网络服务器都需要与同样分布在多个机器(可能位于世界各地的不同位置)上的数据进行交互。
有了 Google App Engine,您就不必再为上述任何问题担心了。App Engine 的基础架构通过一个简单的 API 就能处理好数据的所有分布、复制和负载平衡,同时您还可获得一个功能强大的查询引擎和事务。
App Engine 数据存储区是由具有以下两种 API 的 App Engine 所提供的若干服务之一:标准 API 和低级 API。通过使用标准 API,您可以更轻松地将应用程序移植到其他托管环境和其他数据库技术(如果您需要这么做)。标准 API 可将应用程序与 App Engine 服务“分离”。App Engine 服务还提供了低级 API,可以直接显示服务功能。您可以使用低级 API 来实现新的适配器接口,或者就直接在应用程序中使用 API。
App Engine 对数据存储区的以下两种不同 API 标准都提供了支持:Java 数据对象 (JDO) 和 Java 持久性 API (JPA)。这些接口是由 DataNucleus 访问平台(若干 Java 持久性标准的开源实现)提供的,具有用于 App Engine 数据存储区的适配器。
对于留言簿,我们将使用 JDO 接口来检索和发布由用户留下的消息。
访问平台需要配置文件来指示其使用 App Engine 数据存储区作为 JDO 实现的后端。在最终 WAR 中,该文件的名称为 jdoconfig.xml,且位于 war/WEB-INF/classes/META-INF/ 目录中。
如果您使用的是 Eclipse,则该文件已创建为 src/META-INF/jdoconfig.xml。当您构建项目时,该文件自动复制到 war/WEB-INF/classes/META-INF/ 中。
如果您没有使用 Eclipse,可直接创建 war/WEB-INF/classes/META-INF/ 目录,或者在构建过程中创建该目录,然后从其他位置复制配置文件。使用 Apache Ant 中所述的 Ant 构建脚本从 src/META-INF/ 复制该文件。
jdoconfig.xml 文件应具有以下内容:
<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">
<persistence-manager-factory name="transactions-optional">
<property name="javax.jdo.PersistenceManagerFactoryClass"
value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
<property name="javax.jdo.option.ConnectionURL" value="appengine"/>
<property name="javax.jdo.option.NontransactionalRead" value="true"/>
<property name="javax.jdo.option.NontransactionalWrite" value="true"/>
<property name="javax.jdo.option.RetainValues" value="true"/>
<property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
</persistence-manager-factory>
</jdoconfig>
当您创建 JDO 类时,使用 Java 批注来说明应如何将实例存储在数据存储区中,以及应如何在从数据存储区检索实例时重新创建实例。访问平台使用后编译处理步骤将数据类连接到实现,DataNucleus 称之为使类“增强”。
如果您使用的是 Eclipse 和 Google 插件,则该插件自动将 JDO 类增强步骤作为构建过程的一部分来执行。
如果您使用的是使用 Apache Ant 中所述的 Ant 构建脚本,则该构建脚本将包括必要的增强步骤。
有关 JDO 类增强的详细信息,请参阅使用 JDO。
JDO 允许您将 Java 对象(有时称为简单传统 Java 对象或 POJO)存储在任何带有 JDO 兼容适配器的数据存储区中,如 DataNucleus 访问平台。App Engine SDK 包括了用于 App Engine 数据存储区的访问平台插件。这意味着您可以存储 App Engine 数据存储区中所定义的类的实例,并使用 JDO API 将这些实例作为对象来检索。可告知 JDO 如何使用 Java 批注来存储和重构类的实例。
我们将创建 Greeting 类来表示发布到留言簿的各条消息。
在 guestbook 包中新建一个名为 Greeting 的类。(非 Eclipse 用户,在 src/guestbook/ 目录中创建 Greeting.java 文件。)为源文件提供以下内容:
package guestbook;
import java.util.Date;
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 com.google.appengine.api.users.User;
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Greeting {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;
@Persistent
private User author;
@Persistent
private String content;
@Persistent
private Date date;
public Greeting(User author, String content, Date date) {
this.author = author;
this.content = content;
this.date = date;
}
public Long getId() {
return id;
}
public User getAuthor() {
return author;
}
public String getContent() {
return content;
}
public Date getDate() {
return date;
}
public void setAuthor(User author) {
this.author = author;
}
public void setContent(String content) {
this.content = content;
}
public void setDate(Date date) {
this.date = date;
}
}
这个简单的类定义了问候语的 3 个属性:author、content 和 date。用 @Persistent 对这三个私有字段进行批注,指示 DataNucleus 将这些字段作为对象属性存储在 App Engine 数据存储区中。
该类定义了属性的取值函数和设值函数。这些函数仅供应用程序使用,JDO 并不需要。
该类还定义了名为 id 的字段,这是一个同时以 @Persistent 和 @PrimaryKey 进行批注的 Long。App Engine 数据存储区具有实体键的概念,并且可在对象上以多种不同方式表示键。在这种情况下,键字段为长整型,且在保存对象时被自动设置为唯一的数字 ID。
有关 JDO 批注的详细信息,请参阅定义数据类。
每个使用数据存储区的请求都新建一个 PersistenceManager 类的实例。使用 PersistenceManagerFactory 类的实例完成此操作。
PersistenceManagerFactory 实例的初始化需要时间。好在您对于应用程序只需一个实例,而该实例可存储在将由多个请求和多个类使用的静态变量中。简单的做法是为静态实例创建一个单独的包装器类。
在 guestbook 包中创建新建一个名为 PMF 的类(src/guestbook/ 目录中名为 PMF.java 的文件),然后为其提供以下内容:
package guestbook;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;
public final class PMF {
private static final PersistenceManagerFactory pmfInstance =
JDOHelper.getPersistenceManagerFactory("transactions-optional");
private PMF() {}
public static PersistenceManagerFactory get() {
return pmfInstance;
}
}
在 DataNucleus 和 Greeting 类就绪的情况下,表单处理逻辑现在可以在数据存储区中存储新的问候语。
按照指示将 src/guestbook/SignGuestbookServlet.java 编辑如下:
package guestbook;
import java.io.IOException;
import java.util.Date;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager;
import javax.servlet.http.*;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import guestbook.Greeting;
import guestbook.PMF;
public class SignGuestbookServlet extends HttpServlet {
private static final Logger log = Logger.getLogger(SignGuestbookServlet.class.getName());
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
String content = req.getParameter("content");
Date date = new Date();
Greeting greeting = new Greeting(user, content, date);
PersistenceManager pm = PMF.get().getPersistenceManager();
try {
pm.makePersistent(greeting);
} finally {
pm.close();
}
resp.sendRedirect("/intl/zh-CN/guestbook.jsp");
}
}
该代码通过调用构造函数新建一个 Greeting 实例。为了将该实例保存到数据存储区,其使用 PersistenceManagerFactory 创建 PersistenceManager,然后将该实例传递到 PersistenceManager 的 makePersistent() 方法。批注和字节码增强由此获取实例。makePersistent() 返回后,新对象将存储在数据存储区中。
JDO 标准定义了一种用于查询持久性对象的机制,称为 JDOQL。您可以使用 JDOQL 在 App Engine 数据存储区中对实体执行查询,并将结果作为 JDO 增强的对象进行检索。
在该示例中,为了简单起见,我们将查询代码直接写入 guestbook.jsp。对于较大的应用程序,可能要将查询逻辑委托给另一个类。
编辑 war/guestbook.jsp 并添加所指示的行,如下所示:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="javax.jdo.PersistenceManager" %>
<%@ page import="com.google.appengine.api.users.User" %>
<%@ page import="com.google.appengine.api.users.UserService" %>
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
<%@ page import="guestbook.Greeting" %>
<%@ page import="guestbook.PMF" %>
<html>
<body>
<%
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
if (user != null) {
%>
<p>Hello, <%= user.getNickname() %>! (You can
<a href="<%= userService.createLogoutURL(request.getRequestURI()) %>">sign out</a>.)</p>
<%
} else {
%>
<p>Hello!
<a href="<%= userService.createLoginURL(request.getRequestURI()) %>">Sign in</a>
to include your name with greetings you post.</p>
<%
}
%>
<%
PersistenceManager pm = PMF.get().getPersistenceManager();
String query = "select from " + Greeting.class.getName();
List<Greeting> greetings = (List<Greeting>) pm.newQuery(query).execute();
if (greetings.isEmpty()) {
%>
<p>The guestbook has no messages.</p>
<%
} else {
for (Greeting g : greetings) {
if (g.getAuthor() == null) {
%>
<p>An anonymous person wrote:</p>
<%
} else {
%>
<p><b><%= g.getAuthor().getNickname() %></b> wrote:</p>
<%
}
%>
<blockquote><%= g.getContent() %></blockquote>
<%
}
}
pm.close();
%>
<form action="/sign" method="post">
<div><textarea name="content" rows="3" cols="60"></textarea></div>
<div><input type="submit" value="Post Greeting" /></div>
</form>
</body>
</html>
要准备查询,请调用 PersistenceManager 实例的 newQuery() 方法,其中查询的文本以字符串表示。该方法返回一个查询对象。该查询对象的 execute() 方法执行查询,然后返回相应类型结果对象的 List<>。查询字符串必须包括要查询类的完整名称,其中包含包名称。
重构项目并重新启动服务器。访问 http://localhost:8080/。输入问候语,然后提交。问候语将出现在表单上方。输入另一条问候语,然后提交。两条问候语都会显示。尝试使用链接来退出和登录,并尝试在登录和未登录的情况下提交消息。
提示:在实际应用程序中,在显示用户提交的内容(如本应用程序中的问候语)转意 HTML 字符不失为一种很好的做法。JavaServer Pages 标准标记库 (JSTL) 包括了进行此操作的例程。App Engine 包括 JSTL(以及其他 JSP 相关的运行时 JAR),因此您无需将其加入应用程序。在标记库 http://java.sun.com/jsp/jstl/functions 中查找 escapeXml 函数。有关详细信息,请参阅 Sun 的 J2EE 1.4 辅导手册。
我们的留言簿当前显示了曾发布到系统的所有消息,并且以它们的创建顺序来显示。当留言簿有很多消息时,仅显示最近的消息并在顶部显示最新消息会很实用。通过调整数据存储区查询可实现此操作。
使用 JDOQL(类似于 SQL 的查询语言,用于检索数据对象)通过 JDO 接口执行查询。在我们的 JSP 页面中,下一行定义了 JDOQL 查询字符串:
String query = "select from " + Greeting.class.getName();
换句话说,JDOQL 查询字符串如下所示:
select from guestbook.Greeting
此查询对数据存储区询问迄今所保存的 Greeting 类的每个实例。
查询可通过对相关对象加入对象属性必须满足的条件来过滤结果。例如,要检索 author 属性为 alfred@example.com 的 Greeting,您应使用以下查询:
select from guestbook.Greeting where author == 'alfred@example.com'
查询可指定结果根据属性值返回的顺序。要按照所有 Greeting 对象发布顺序的相反顺序(从最新到最早)来检索这些对象,您应使用以下查询:
select from guestbook.Greeting order by date desc
查询可将返回的结果限制为某一结果范围。如果只要检索最新的 5 个问候语,您应同时使用 order by 和 range,如下所示:
select from guestbook.Greeting order by date desc range 0,5
针对留言簿应用程序执行此操作。在 guestbook.jsp 中,将 query 定义替换为如下内容:
String query = "select from " + Greeting.class.getName() + " order by date desc range 0,5";
将问候语发布到留言簿,直到留言簿中的问候语超过 5 个。只显示按反向时间顺序排列的 5 个最新问候语。
有关查询和 JDOQL 的详细信息,请参阅查询和索引。
每个网络应用程序都可通过模板或一些其他机制,返回从应用程序代码动态生成的 HTML。大部分网络应用程序还需要提供静态内容,例如图像、CSS 样式表或 JavaScript 文件。为了提高效率,App Engine 对静态文件的处理方式与对应用程序源和数据文件的处理方式不同。为该应用程序创建静态文件形式的 CSS 样式表。
继续转至使用静态文件。