Java开发2.0: 使用Amazon SimpleDB实现云存储,第2部分:使用SimpleJPA实现简单对象持久化
使用诸如 Grails 的关系框架对几乎所有类型的应用进行域对象建模是很容易的,但是使用 SimpleDB 又怎么样呢?在 Andrew Glover 的介绍 SimpleDB 的系列文章的第 2 部分,他向您介绍了如何使用 SimpleJPA,而非 Amazon SDK,在 SimpleDB 的云存储中实现对象持久化。除了使您能够使用简单 Java™ 对象进行域建模(通过 JPA)之外,SimpleJPA 还能够自动地将基本数据类型转换成兼容 Amazon 的字符串。您确实找不到比这更简单的云存储方法了。
在介绍 SimpleDB 文章的 第一部分 中,我向您介绍了如何使用 Amazon 本身的 API 进行一个 CRUD 网络的赛跑应用的建模。除了对大多数 Java 开发人员而言,Amazon 只使用字符串来描述数据类型的方法的明显独特性之外,您可能发现自己对于 Amazon API 还有一些疑虑。毕竟,现在使用关系数据库的 API 已经非常标准且成熟了 — 而且更重要的是,他们已经很熟悉这些技术了。
除此之外,现在有许多关系框架实现了 Java Persistence API。因此为各种 RDBMS 进行各种类型的 Java 应用进行域对象建模都是非常容易和常见的。当您已经掌握了一种方法之后,很自然您会对于学习新的域对象建模方法会有一些抵触 — 而好消息是使用 SimpleDB 时,您不需要学习新东西。
在 SimpleDB 文章的第 2 部分中,我将向您介绍如何重构第 1 部分的赛跑应用,使之符合 JPA 规范。然后我们将把应用移植到 SimpleJPA,并且探讨一些能够使这个创新的开放源码平台经过调整而支持 NoSQL 域建模和基于云的存储的方法,这一样很简单。
为什么使用 SimpleDB?
Amazon 的 SimpleDB 是一个简单且极具可扩展性和可靠性的基于云的数据存储方法。由于它本质上是非关系/NoSQL,SimpleDB 既灵活又快速。作为 Amazon Web Service 家族的一部分,SimpleDB 使用 HTTP 作为底层通信机制,所以它能够支持多种语言,包括 Java 语言、Ruby、C# 和 Perl。SimpleDB 价格也很便宜:根据 SimpleDB 的授权方式,您只需要为您使用的资源支付费用,这跟根据预计使用和空间预先购买授权的传统方法很不一样。作为新兴的 NoSQL,或非关系数据存储的一部分,SimpleDB 是与 Google 的 Bigtable 或 CouchDB 相对应的,它们在 这些系列文章中 有相应的介绍。
Hibernate 和 JPA:背景概况
现在有许许多多的 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。我将在后面向您介绍。
JPA 中的注释
要使这两个对象能够使用 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 来管理。
进入 SimpleJPA
到现在为止,我还没有进行任何 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。)
将应用移植到 SimpleJPA
为了使 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
使用 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'
自动填充和编码使开发更加简单,您不觉得吗?
SimpleJPA 中的关系
即使 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 等框架的知识。