在介绍 SimpleDB 文章的 第一部分 中,我向您介绍了如何使用 Amazon 本身的 API 进行一个 CRUD 网络的赛跑应用的建模。除了对大多数 Java 开发人员而言,Amazon 只使用字符串来描述数据类型的方法的明显独特性之外,您可能发现自己对于 Amazon API 还有一些疑虑。毕竟,现在使用关系数据库的 API 已经非常标准且成熟了 — 而且更重要的是,他们已经很熟悉这些技术了。
除此之外,现在有许多关系框架实现了 Java Persistence API。因此为各种 RDBMS 进行各种类型的 Java 应用进行域对象建模都是非常容易和常见的。当您已经掌握了一种方法之后,很自然您会对于学习新的域对象建模方法会有一些抵触 — 而好消息是使用 SimpleDB 时,您不需要学习新东西。
在 SimpleDB 文章的第 2 部分中,我将向您介绍如何重构第 1 部分的赛跑应用,使之符合 JPA 规范。然后我们将把应用移植到 SimpleJPA,并且探讨一些能够使这个创新的开放源码平台经过调整而支持 NoSQL 域建模和基于云的存储的方法,这一样很简单。
现在有许许多多的 Java 开发人员都使用 Hibernate(和 Spring)实现数据持久化。除了是最先成功的开放源码项目,Hibernate 也彻底改变了 ORM 领域。在出现 Hibernate 之前,Java 开发人员必须处理复杂的 EJB 实体 Bean;而在这之前,我们只能自己实现 ORM 或者使用来自诸如 IBM® 等供应商的产品。Hibernate 去掉了 EJB 的所有复杂性和开销,转而使用我们现在许多人都使用的基于 POJO 的建模平台。
Java Persistence API (JPA) 是由于 Hibernate 创新地使用 POJO 进行数据建模方法的流行而出现的。现在,EJB 3.0 实现了 JPA,Google App Engine
也一样实现了 JPA。甚至如果您使用 Hibernate EntityManager,那么 Hibernate 本身也是一个 JPA 实现,
既然 Java 开发人员已经越来越熟悉使用 POJO 对以数据为中心的应用进行建模,那么可以说,SimpleDB 这样一个数据存储应该能够给我们提供一个类似的选项。毕竟,它与数据库有些相似,不是吗?
要使用 SimpleJPA,我们需要修改一下我们的 Racer 和 Runner
对象,使它们符合 JPA 规范。幸好,JPA 基本要素是很简单的:给平常的 POJO 加上注释,而 EntityManager
实现会负责完成其他处理 — 不需要 XML。
JPA 所使用的两个主要的注释是 @Entity 和 @Id,这两个注释分别将一个 POJO
指定为持久化类,同时确定它的标识键。为了将我们的赛跑应用转换为 JPA,我们也将使用另外两个管理关系的注释:@OneToMany 和
@ManyToOne。
在本文的第 1 部分中,我已经向您介绍了如何持久化选手和比赛对象了。然而,我没有使用对象来表示这些实体 — 我只是使用了 Amazon 的原始 API 来存储这两个对象的属性。如果我希望对一个比赛和比赛选手的关系进行建模,那么我可以编写如清单 1 所示的代码:
清单 1. 一个简单的 Race 对象
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
//setters and getters left out...
}
|
在 清单 1 中,我给 Race 对象设置了 4 个属性,最后一个是一个选手
Collection。接下来,我可以创建一个简单的 Runner 对象(如清单 2
所示),它包含每位选手的姓名(现在我将尽量保持简单),与他/她所参加的 Race 实例相关的 SSN。
清单 2. 与 Race 相关的一个简单的 Runner
public class Runner {
private String name;
private String ssn;
private Race race;
//setters and getters left out...
}
|
您可以从 清单 1 和 2 看到,我在选手和比赛之间逻辑上建立了一个多对一的关系。在实际情况中,可能多对多关系更准确些(选手一般会参加多个比赛),但是这里这样做是为了简单起见。另外,现在我也忽略构造函数、setter 和 getter。我将在后面向您介绍。
要使这两个对象能够使用 SimpleJPA 并不是很难。首先,我需要通过为每个对象添加 @Entity
注释来将它们变成可持久化的。我也需要在 Race 对象中使用 @OneToMany,在
Runner 对象中使用 @ManyToOne 来正确定义它们的关系。
@Entity 注释是在类上标注的,而关系注释是在 getter 函数上标注的。这些注释见清单 3 和 4:
清单 3. JPA 注释的 Race
@Entity
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
|
在 清单 3 中,我使用 @OneToMany 注释来标注 getRunners
函数。我也在实例 Runner 的 race 属性上定义了一个关系。
在清单 4 中,我用类似的方法注释了 Runner 对象的 getRace 函数。
清单 4. JPA 注释的 Runner
@Entity
public class Runner {
private String name;
private String ssn;
private Race race;
@ManyToOne
public Race getRace() {
return race;
}
//other setters and getters left out...
}
|
大多数数据存储(关系型或非关系型)都需要某种描述数据唯一性的方法。所以如果我将这两个对象存储到数据存储中,我至少需要给它们添加
ID。在清单 5 中,我添加一个类型为 BigInteger 的 id 属性到
Race 域对象。在 Runner 对象中我会使用相同的做法。
清单 5. 给 Race 添加一个 ID
@Entity
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
private BigInteger id;
@Id
public BigInteger getId() {
return id;
}
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
|
清单 5 中的 @Id 注释并没有关于 ID
是如何管理的信息。这样程序就会假设由我手动管理这个 ID,而不是使用 EntityManager 来管理。
到现在为止,我还没有进行任何 SimpleDB 的配置。Race 和
Runner 对象都只是使用 JPA 注释进行标注,从而可以存储在任何由 JPA 实现所支持的数据存储中。可选的存储方式包括
Oracle、DB2、MySQL 和(您可能已经猜到的)SimpleDB。
SimpleJPA 是 Amazon 的 SimpleDB 的开源实现。虽然它并不支持完整的 JPA 规范(例如,您不能联合 JPA 查询),但是它支持大量很有用的一部分 JPA 规范。
使用 SimpleJPA 的一个很大的优点是它能够无缝地处理我在 本文的第 1 部分 中所讨论的按字母的问题。SimpleJPA 会对依赖于数字类型的对象进行字符串转换和后续的填充(如果需要)。在大多数情况中,这意味着您不需要修改您的域模型来使用
String 类型。(其中只有一个例外情况,我将在后面进行讨论。)
因为 SimpleJPA 是一个 JPA 实现,您可以很容易在其中使用符合 JPA 的域对象。SimpleJPA 只要求您使用
String ID,这意味着您的 id 属性必须是
java.lang.String。为了简化,SimpleJPA 提供了基本的类
IdedTimestampedBase,它负责管理域对象的 ID 属性,以及日期属性
created 和 updated。(在底层, SimpleDB 会生成一个唯一的
Id。)
为了使 Race 和 Runner 类兼容 SimpleJPA,我可以扩展 SimpleJPA
便利基础类,或者将每一个类的 id 属性从 BigInteger 修改为
String。我选择了第一种方法,如清单 6 所示:
清单 6. 修改 Race 类为使用 SimpleJPA 的 IdedTimestampedBase 基础类
@Entity
public class Race extends IdedTimestampedBase{
private String name;
private String location;
private double distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
|
虽然我不会向您显示 Runner 中相同的代码,但是您可以随时查看这些代码:扩展
IdedTimestampedBase,并删除 Runner
的 id 属性。
修改 Race 和 Runner 的 ID 是使赛跑应用符合 SimpleJPA
规范的第一步。接下来,我需要将基本数据类型(如,double、int 和
float)转换为诸如 Integer 和 BigDecimal
的对象。
我将从修改 Race 的 distance 属性开始。我发现(在当前版本的 SimpleJPA 中)使用
BigDecimal 比 Double 更可靠,所以我将 Race 的
distance 修改为 BigDecimal,如清单 7 所示:
清单 7. 将 distance 修改为 BigDecimal
@Entity
public class Race extends IdedTimestampedBase{
private String name;
private String location;
private BigDecimal distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
|
现在 Runner 和 Race 都已经可以通过 SimpleJPA 实现进行持久化了。
使用 SimpleJPA 来处理您的域对象在 SimpleDB 中的存储与使用 JPA 实现进行普通的关系数据库存储差别不大。即使您从未使用过 JPA
开发应用,那么对于您来说它也不会有太大的困难。唯一的新东西是要配置 SimpleJPA 的
EntityManagerFactoryImpl,这要求使用您的 Amazon Web Services 证书和您的 SimpleDB
域的前缀名。(另一个方法是在编译路径上增加一个包含您的证书的属性文件。)
在创建一个 SimpleJPA EntityManagerFactoryImpl 实例时使用您指定的前缀名,这样产生的
SimpleDB 域会由您的前缀,加一根横杠,再加域对象名称组成。所以,如果我指定 “b50” 为前缀,而我在 SimpleDB 中创建一个
Race,那么这个域将会是 “b50-Race”。
一旦您创建一个 SimpleDB EntityManagerFactoryImpl 实例,其他方面就由这个接口完成了。您需要使用一个
EntityManager 实例,这个实例是从 EntityManagerFactoryImpl 获取的,如清单 8 所示:
清单 8. 获得一个 EntityManager
Map<String,String> props = new HashMap<String,String>();
props.put("accessKey","...");
props.put("secretKey","..");
EntityManagerFactoryImpl factory =
new EntityManagerFactoryImpl("b50", props);
EntityManager em = factory.createEntityManager();
|
一旦您拥有了一个 EntityManager 对象,您就可根据需要处理域对象了。例如,我可以像下面一样创建一个
Race 实例:
清单 9. 创建一个 Race
Race race = new Race();
race.setName("Charlottesville Marathon");
race.setLocation("Charlottesville, VA");
race.setDistance(new BigDecimal(26.2));
em.persist(race);
|
在 清单 9 中,SimpleJPA 处理了所有 HTTP 请求来在云中创建
Race。使用 SimpleJPA 意味着我也能够使用一个 JPA 查询来查询比赛,如清单 10
所示。(记住您不能够联合这些查询,但是我仍然可以使用数字进行搜索。)
清单 10. 根据实例查找一个比赛
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race race : races){
System.out.println(race);
}
|
例如,SimpleJPA 内部的数字到字符串转换的方法是非常有用的,如果您在 SimpleJPA 中启用查询输出,那么您可以看到有哪些查询发送到
SimpleDB。所提交的查询如清单 11 所示。注意 distance 是如何编码的。
清单 11. SimpleJPA 很好地处理数字!
amazonQuery: Domain=b50-Race, query=select * from `b50-Race` where `distance` = '0922337203685477583419999999999999928946' |
自动填充和编码使开发更加简单,您不觉得吗?
即使 SimpleDB 不允许查询中进行域联合,您也仍然可以在域中使用关联项。正如我在
第 1 部分
中介绍的,您在一个对象中存储另一个相关对象的键,然后在您需要时查询这个对象。这也正是 SimpleJPA
所做的。例如,之前我向您介绍了如何使用 JPA 注释将 Runner 链接到一个
Race。因此,我可以创建一个 Runner 实例,将现有的
race 添加到这个实例,然后再存储 Runner 实例,如清单 12 所示:
清单 12. 使用 SimpleJPA 处理关系
Runner runner = new Runner();
runner.setName("Mark Smith");
runner.setSsn("555-55-5555");
runner.setRace(race);
race.addRunner(runner);
em.persist(runner);
em.persist(race); //update the race now that it has a runner
|
从 清单 12 我们也可以看到,我需要更新 Race 实例,这样我所添加的
Runner 才会被存储起来(同时,我给 Race 函数添加了一个
addRunner 函数,它只是直接将一个 Runner 添加到
Runner 内部的 Collection。)
再一次,如果我通过它的实例搜索一个比赛,我也可以得到它的一组选手,如清单 13 所示:
清单 13. 更多有趣的关系操作!
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race races : race){
System.out.println(race);
List<Runner> runners = race.getRunners();
for(Runner rnr : runners){
System.out.println(rnr);
}
}
|
使用 EntityManager 实例使我能够通过 remove 函数删除一些实体,如清单 14 所示:
清单 14. 删除一个类的实例
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race races : race){
em.remove(race);
}
|
当我在 清单 14 中删除了一个 Race 实例,所关联的
Runners 并没有删除。(当然,我可以使用 JPA 的 EntityListeners
注释来处理这个问题,这意味着我可以监控删除事件,在事件发生时删除 Runner 实例。)
这篇 SimpleDB 的快速教程向您介绍了如何使用 Amazon Web Services API 和 SimpleJPA 来处理非关系数据存储的对象。SimpleJPA 实现了一部分的 Java Persistence API 来简化 SimpleDB 的对象持久化。您已经看到,使用 SimpleJPA 的其中一个好处是它能够自动地将基本数据类型转换为 SimpleDB 能识别的字符串对象。SimpleJPA 也能够自动地处理 SimpleDB 中的非联合规则,从而简化它的关系建模。SimpleJPA 扩展的监听接口也使它能够实现逻辑数据统一规则,这是您在关系数据库所希望使用的。
SimpleJPA 的关键是它能够帮助您快速简单且廉价地实现重要的可扩展性。通过 SimpleJPA,您可以在非关系的、基于云的存储环境中利用您在多年的工作中所接触到的诸如 Hibernate 等框架的知识。
学习
-
Java 开发 2.0
:这个 developerWorks 系列文章阐述了 Java 开发领域重新定义的技术和工具,包括
Gaelyk(2009 年 12
月),Google App Engine(2009 年 8
月)和 CouchDB(2009 年 12 月)。
-
“
NoSQL Patterns”(Ricky Ho,Pragmatic Programming Techniques,2009 年 12 月):NoSQL 数据库的概述和清单,以及 NoSQL
数据存储的通用架构的更深入介绍。
-
“用 Amazon Web Services 进行云计算,第 5 部分:用 SimpleDB 在云中处理数据集”(Prabhakar
Chaganti,developerWorks,2009 年 2 月):了解基本的 Amazon SimpleDB 概念,以及一个能与 SDB 交互的开源 Python 库 boto
所实现的一些功能。
-
“用 Amazon Web Services 进行云计算,第 1 部分:简介”(Prabhakar
Chaganti,IBM developerWorks,2008 年 7 月):介绍这些服务是如何实现一种有说服力的设计和创建可扩展且可靠的应用的方法。
-
“Google App Engine for Java:第 3 部分:持久性和关系”(Richard
Hightower,developerWorks,2009 年 8 月):Rick Hightower 阐述了 Google App Engine 目前基于 Java
的持久化框架的一些缺点,并提出了一些解决方法。
-
浏览
技术书店,了解关于这些相关方面的书籍。
-
developerWorks Java 技术专区:查看大量关于 Java 编程的方方面面的文章。
获得产品和技术
-
SimpleJPA:Amazon SimpleDB 的一个 Java Persistence API (JPA)
实现。换句话说,是用于操作 Amazon 的云数据库的一个对象关系映射(ORM)框架。
讨论
-
加入
My developerWorks 中文社区。与其他 developerWorks 用户交流,同时浏览由开发人员驱动的博客、论坛、群组和 Wiki。

Andrew Glover 是一名开发人员、作家、演讲家和企业家,他对行为驱动开发、持续集成和敏捷软件开发有巨大的热情。可以通过他的 博客 关注他。