Java 开发 2.0: 使用 Objectify-Appengine 进行 Twitter 挖掘,第 1 部分

对象域建模和非关系型数据存储的持久性

Objectify-Appengine 是一类新兴的工具,它通过在应用程序与 GAE 数据存储之间提供一个 Hibernate 式的映射层来扩展 NoSQL 的便利性。从这个月起开始使用 Objectify 的方便的、JPA 友好的(但不依赖)的 API。Andrew Glover 逐步介绍了如何将 Twitter retweets 映射到 Bigtable,以备在 Google App Engine 中部署它。

Andrew Glover, 作家和开发人员

Andrew GloverAndrew Glover 是具有行为驱动开发、持续集成和敏捷软件开发激情的开发人员、作家、演说家和企业家。他是 easyb 行为驱动开发(Behavior-Driven Development,BDD)框架的创建者和三本书的合著者:持续集成Groovy 在行动Java 测试模式。您可以通过他的博客与他保持一致并在 Twitter(http://twitter.com/aglover)上关注他。



2011 年 2 月 28 日

在过去几年里,NoSQL 数据存储已经引发了 Java™ 领域的创新狂潮,这对于 本系列 的读者来说已经不是什么秘密了。除了数据存储本身(比如 CouchDB、MongoDB 和 Bigtable)之外,我们已经开始看到扩展效用的工具。ORM 类的映射库通过解决 NoSQL 的一个致命挑战而走在前列,这个挑战就是:如何有效映射简单的 Java 对象(无模式数据存储的通用标准)并发挥这些对象的效用,很像 Hibernate 为关系型数据存储所做的那样。

关于本系列

自 Java 技术出现以来,Java 开发全貌就发生了根本变化。多亏成熟的开源框架和可靠的供出租部署基础结构,现在我们可以快速而廉价地组装、测试、运行和维护 Java 应用程序了。在这个 系列 中,Andrew Glover 探究使这个新 Java 开发范例成为可能的一系列技术和工具。

SimpleJPA 是一个这类的示例:它是一个持久性库,允许 JPA 注释对象几乎与 Amazon 的 SimpleDB 无缝集成。我在 前面的几个专栏 中介绍了 SimpleJPA,但是发现尽管它是基于 JPA 的,它不实现完整的 JPA 规范。这是因为 JPA 旨在对关系型数据库起作用,而 SimpleDB(及其小帮手 SimpleJPA)避免了这一点。其他项目甚至不会尝试模拟完整的 JPA 规范:它们仅从它那里借用想要的东西。这样的一个项目 — Objectify-Appengine — 是本月专栏的主题。

Objectify-Appengine:一个对象非关系型 映射库

Objectify-Appengine 或 Objectify 是一个 ORM 类的库,它简化 Bigtable 以及 GAE 中的数据持久性。作为一个映射层,Objectify 通过一个简洁的 API 将自身插入到 POJOs 与 Google 的重型设备之间。您可以使用一个熟悉的 JPA 注释子集(尽管 Objectify 不实现完整的规范)以及少量生命周期注释,来存留和检索 Java 对象形式的数据。从本质上讲,Objectify 是为 Google 的 Bigtable 明确设计的一个轻量级 Hibernate。

ORM 类?

Object-relational-mapping 是克服面向对象模型与关系型数据库之间的所谓的阻抗失配 的常见方式(参见 参考资料)。在非关系型环境下没有阻抗失配,因此 Objectify 不是一个真正的 ORM 库;它更像是一个 ONRM(对象非关系型映射)库。对于疲于记住缩略词的人来说,“ORM 类” 是一个方便的速记。

Objectify 与 Hibernate 的类似之处在于,它允许您针对 Bigtable 映射和利用 POJOs,您将这个看作是 GAE 中的一个抽象。除了 JPA 注释的子集之外,Objectify 运用其自己的注释,这体现了 GAE 数据存储的独特功能。Objectify 还允许关系,显示一个查询界面来支持 GAE 筛选和排序概念。

在下一节中,我们将开发一个示例应用程序,让您尝试将 Objectify 用于映射和数据持久性,使用 Google 的 Bigtable 来存储应用程序数据。在本文的第 2 部分中,我们将在一个 GAE web 应用程序中利用我们的数据。


重点,Bigtable

我要先绕过 “比赛和运动员” 领域,还要跳过违规停车罚单讨论。我们将挖掘 Twitter — 对于读过上个月的 MongoDB 简介 一文的读者来说,这是另一个熟悉的应用域。这次我们将不仅调查谁在 Twitter 上转发我们(我或你)的消息,而且要调查我们的置顶消息转发者中谁最具影响力

对于这一应用,我们需要创建两个域类:RetweetUserRetweet 对象显然代表来自 Twitter 帐户的一个转贴。User 对象代表我们正在挖掘其帐户信息的 Twitter 用户。(注意,这个 User 对象不同于 GAE User 对象)。每个 Retweet 有一个与 User 的关系。

关于 Bigtable

Bigtable 是一个以列为导向的 NoSQL 数据存储,可通过 Google App Engine 进行访问。不同于您在关系型数据库中找到的模式,Bigtable 基本上是一个大规模分布式持久性映射 — 允许对基础数据值的键和属性执行查询的一个映射。Bigtable 与 GAE 之间的关系如同 SimpleDB 与 Amazon Web Services 之间的关系。

Objectify 利用 Google 的低级 Entity API 直观地将域对象映射到 GAE 数据存储。我在先前的文章中介绍了 Entity API(参阅 参考资料),因此在这里不加獒述。最主要的,您需要知道,在 Entity API 域中名称变为 kind 类型 — 即 User 会逻辑地映射到一个 User kind— 这类似于关系术语中的一个表。(更进一步,可以将 kind 看作是包含键和值的一个映射。)那么域属性实质上就是关系术语中的列名,属性值就是列值。不同于 Amazon 的 SimpleDB,GAE 数据存储支持一组丰富的数据类型,包括 blobs(参见 参考资料)和各式各样的数字、日期和列表。


Objectify 中的类定义

User 对象将是非常基本的:仅仅是一个名称和与 Twitter 的 OAuth 实现相关的两个属性,我们将充分利用其直观的授权方法。一个 OAuth 范例中的用户不存储用户密码,而是存储令牌,该令牌表示用户有权代它们行事。OAuth 运作起来如同一张信用卡,只是将授权数据作为货币。不要为每个网站提供您的用户名和密码,而是提供站点权限来访问该信息。(OAuth 类似于 OpenID — 但两者不同;参见 参考资料 了解更多内容。)

清单 1. User 对象的开头
import javax.persistence.Id;

public class User {
 @Id
 private String name;	
 private String token;
 private String tokenSecret;

 public User() {
  super();	
 }

 public User(String name, String token, String tokenSecret) {
  super();
  this.name = name;
  this.token = token;
  this.tokenSecret = tokenSecret;	
 }

 public String getName() {
  return name;
 }

 //...
}

正如在 清单 1 中看到的,与 User 类相关的惟一特定于持久性的代码是 @Id 注释。 @Id 是标准 JDO,这可以从 import 中分辨出来。GAE 数据存储允许标识符或键为 Strings 或 Longs/longs。在 清单 1 中,我指定了 Twitter 帐户的名称为键。我还创建了一个接受所有三个属性的构造函数,这将便于创建新实例。注意,实际上我不必为这个对象定义 getters 和 setters 以便在 Objectify 中使用(不过如果我想通过编程方式访问或设置属性,我将会用到它们)。

当将 User 对象持久化到底层数据存储时,它将是一个 User kind。这个实体将有一个叫作 name 的键和另外两个属性:tokentokenSecret,这两个属性都是 Strings。很简单,对吧?

User 的权限

接下来,我要添加一点点的行为给我的 User 域类。我打算制造一个类名,让 User 对象根据名称查找其自己。

清单 2. 根据名称查找 Users
 //inside User.java... 
 private static Objectify getService() {
  return ObjectifyService.begin();
 }

 public static User findByName(String name){
  Objectify service = getService();
  return service.get(User.class, name);
 }

清单 2 中,新出现的 User 中正在发生一些事情。为了利用 Objectify,可以说我需要将它启动。因此获取 Objectify 的一个实例,它处理所有 CRUD 类的操作。您可以将 Objectify 类看作大致类似于 Hibernate 的 SessionFactory 类。

Objectify 有一个简单的 API。为根据键找到个别实体,您仅需调用 get 方法,该方法接受一个类类型和键。因此在 清单 2 中,我使用基础的 User 类和所需的名称发出一个 get 调用。还要注意,Objectify 的异常未经检查 — 这表示我无需担心捕捉到很多 Exception 类型。本质上,这并不是说不会出现异常;只是不必在编译时处理它们。例如,如果无法定位 User kind,get 方法会抛出一个 NotFoundException。(Objectify 还提供一个 find 方法,而该方法返回 null。)

接下来是实例行为:我希望我的 User 实例支持按影响力顺序列出所有转贴的能力,这意味着我需要添加另一个方法。但是首先我要建模我的 Retweet 对象。

有多少 Retweets?

Retweet,可想而知,表示一个 Twitter retweet。该对象将包含大量属性,包括与所属的 User 对象的关系。

我已经提到过,GAE 数据存储中的一个标识符或键必须是一个 StringLong/long。GAE 数据存储中的键也是惟一的,正如在传统的数据库中一样。这就解释了为什么 User 对象的键是 Twitter 帐户的名称,它本质上是惟一的。清单 3 中 Retweet 对象上的键将是 tweet id 和转发它的用户的结合。(Twitter 不允许将同样的文本消息发送两遍,因此这个键目前是可行的。)

清单 3. 定义 Retweet
import javax.persistence.Id;
import com.googlecode.objectify.Key;

public class Retweet {
 @Id
 private String id;
 private String userName;
 private Long tweetId;
 private Date date;
 private String tweet;
 private Long influence;
 private Key<User> owner;

 public Retweet() {
  super();
 }

 public Retweet(String userName, Long tweetId, Date date, String tweet,
   Long influence) {
  super();
  this.id = tweetId.toString() + userName;
  this.userName = userName;
  this.tweetId = tweetId;
  this.date = date;
  this.tweet = tweet;
  this.influence = influence;
 }

 public void setOwner(User owner) {
  this.owner = new Key<User>(User.class, owner.getName());
 }
 //...
}

注意 清单 3 中的键,id,它是一个 String;它结合了 tweetIduserName。在我解释关系之后,清单 3 中显示的 setOwner 方法会更有意义。


建模关系

这个应用中的 Retweets 和 Users 有一个关系;即每个 User 含有 Retweets 的一个逻辑集合,且每个 Retweet 包含一个与其 User直接 链接。回顾 清单 3,您可能会察觉到一些异常:一个 Retweet 对象拥有 User 类型的一个 Key 对象。

Objectify 对 Keys 的使用,而非对象引用,反映了 GAE 的非传统数据存储,尤其缺乏参照完整性。

两个对象之间的关系实际上只需要 Retweet 对象上的一个硬连接。这就解释了为什么 Retweet 的一个实例包含对 User 实例的一个直接 Key。因此,User 实例实际上不必在自己这边存留 RetweetKey— 一个 User 实例仅可以查询那些链接回自身的转贴。

不过,为了使对象之间的交互更直观,在清单 4 中我向 User 添加了一些接受 Retweet 的方法。这些方法加强了两个对象之间的关系:User 现在直接设置其对 Retweet 的所有权。

清单 4. 向一个 User 添加 Retweets
public void addRetweet(Retweet retweet){
 retweet.setOwner(this);
 Objectify service = getService();
 service.put(retweet);
}

public void addRetweets(List<Retweet> retweets){
 for(Retweet retweet: retweets){
  retweet.setOwner(this);
 }

 Objectify service = getService();
 service.put(retweets);
}

清单 4 中,我添加了两个新方法到 User 域对象。一个处理一批 Retweets,而另一个仅处理一个实例。您会注意到,对 service 的引用之前在 清单 2 中定义过,并且它的 put 方法被超载,以同时处理单个实例和 Lists。本例中的关系还由所属对象处理 —User 实例将自身添加到 Retweet。因此 Retweets 是被分别创建的,不过一旦将它们添加到 User 的一个实例,它们会正式连接。


Twitter 挖掘

我的下一步是在 User 对象上添加一个类似 finder 的方法。该方法将允许我按影响力顺序列出所有拥有的 Retweets — 即从最初拥有的帐户到已经转发过消息的帐户。我要从具有最多关注者的帐户一直追踪到拥有最少关注者的帐户。

清单 5. 按影响力列出 Retweets
public List<Retweet> listAllRetweetsByInfluence(){
 Objectify service = getService();
 return service.query(Retweet.class).filter("owner", this).order("-influence").list();
}

清单 5 中的代码位于 User 对象内。它返回按 influence 属性(是一个整数)排序的 Retweets 的 List。本例中的 “-” 表示我希望按降序排列 Retweets,从最高到最低。注意 Objectify 的查询代码:service 实例支持根据属性(在本例中是 owner)过滤,甚至对结果进行排序。还要注意,这里仍然对异常未加检查,这将使代码异常简洁。

查询多个属性

GAE 数据存储对发出的任何查询使用索引。这加快了读取速度,因为一个实体中的单个属性被自动编入索引。但是如果您要根据多个属性执行查询(正如我在 清单 5 中所做的,首先根据 owner 然后根据 influence 执行查询),您必须为 GAE 提供一个 datastore-index.xml 文件。这给 GAE 提供一个有关传入查询的预先警告。清单 6 是使得查询多个属性成为可能的自定义索引:

清单 6. 为 DAE 数据存储定义一个自定义索引
<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="true">
 <datastore-index kind="Retweet" ancestor="false">
  <property name="owner" direction="asc" />
  <property name="influence" direction="desc" />
 </datastore-index>
</datastore-indexes>

持久性

最后但并不是最重要的一点是,我需要添加一些功能来存留我的域对象。您可能会注意到,UserRetweet 对象之间的关系存在一个隐式工作流。也就是说,我需要创建一个 User 实例(并将其保存到 GAE 数据存储中),然后才能从逻辑上添加相关的 Retweets。

在清单 7 中,我在 User 对象上添加了一个 save 方法,但是在 Retweet 对象上不需要该方法。当我将 Retweets 添加到一个 User 实例时它们被自动保存 — 我通过 addRetweetaddRetweets 方法进行添加(注意 清单 4 中对 service.put 的调用)。

清单 7. 保存 Users
public void save(){
 Objectify service = getService();
 service.put(this);
}

看看这段代码有多么简洁?这就是 Objectify API 的有用之处。


注册域类

我接下来准备将我的 Twitter 挖掘应用组合起来,这需要对 Servlets API 进行一些连接工作。我将使用 servlets 来登录 Twitter、提取转贴数据,最后显示一个漂亮的报告。不过我打算暂时给您留有想象的余地,并专注于使用 Objectify 的最后一个要求:手动注册域类。

Objectify 不自动加载域类 — 这表示它不扫描实体的类路径。您必须预先告诉 Objectify 哪些类是特别的,以便您稍后可以通过 Objectify API 访问和使用它们。ObjectifyService 对象允许您注册域类,在尝试调用其 CRUD 类操作之前这当然是您需要做的。幸运的是,由于我在编写一个要部署到 GAE 上的简单 web 应用程序,我可以使用 Servlet API 来注册 ServletContextListener 实例中的两个类。

ServletContextListeners 有两个方法,一个在创建上下文时被调用,另一个在终止上下文时被调用。上下文会在您首次启动 web 应用程序时被创建,因此这将很好地工作。

清单 8. 注册域对象
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import com.googlecode.objectify.ObjectifyService;

public class ContextInitializer implements ServletContextListener {

 public void contextDestroyed(ServletContextEvent arg) {}

 public void contextInitialized(ServletContextEvent arg) {
  ObjectifyService.register(Retweet.class);
  ObjectifyService.register(User.class);
 }
}

清单 8 显示 ServletContextListener 的一个简单实现,其中我注册了我的两个 Objectify 域类 UserRetweet。依据 Servlet API,ServletContextListener 实例在一个 web.xml 文件中注册。当我的应用程序在 Google 的服务器上启动时,清单 8 中的代码会得到调用。使用我的域对象的所有未来 servlets 都将很好地运作,无需再用到 ado。


第 1 部分结束语

此时,我们已经编写了几个类、定义了它们的关系和 CRUD 类功能,这一切都是使用 Objectify-Appengine 完成的。在我们使用样例应用程序时您可能已经留意到有关 Objectify API 的一些事情 — 比如它消除了常规 Java 代码的冗长性。它还利用一些标准的 JPA 注释,因此为习惯于使用 Hibernate 等 JPA 增强框架的开发人员铺平了道路。总地来说,Objectify API 使得针对 GAE 的域建模更加容易、直观,从而提高了开发人员的开发效率。

在本文的第 2 部分,我们会将我们的域应用带到更高的级别,将其与 OAuth、Twitter API(通过 Twitter4J)以及 Ajax-plus-JSON 联系起来。鉴于我们是在 Google App Engine 上进行部署,这一切会有点复杂,对实现施加了一些限制。但是从好的方面讲,我们最终会得到一个真正可伸缩、基于云的 web 应用程序。我们会在下个月开始准备在 GAE 上部署样例应用程序时,进一步探究这些利弊权衡。

参考资料

学习

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=629673
ArticleTitle=Java 开发 2.0: 使用 Objectify-Appengine 进行 Twitter 挖掘,第 1 部分
publish-date=02282011