编写POJO和持久化实体类
Hibernate与用POJO实现的领域模型合作得最好。Hibernate强加给领域模型实现的少数必备条件,对于POJO实现也是最佳实践,因此大部分POJO不用任何改变就可以与Hibernate兼容。Hibernate的必备条件几乎与EJB3.0实体类的相同,因此POJO实现可以轻松地用注解标识,并创造一个EJB3.0兼容的实体。POJO声明了定义行为的业务方法和表示状态的属性。有些属性表示与其他用户自定义的POJO的关联。
User类的POJO实现
public class User implements Serializable {
private static final long serialVersionUID = -7811634244303955773L;
private String username;
private Address address;
public User() {
super();
}
public User(String username, Address address) {
super();
this.username = username;
this.address = address;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
// 计算价格
public MonetaryAmount calcShippingCosts(Address fromLocation) {
return null;
}
}
Hibernate不要求持久化类实现Serializable(可序列化)。然而,当对象被存储在一个HttpSession中,或者用RMI按值传递时,就需要序列化。类可以是抽象的,必要时,可以扩展非持久化的类。
不同于JavaBeans规范,它不需要特定的构造函数,而Hibernate(和JPA)则要求每个持久化类都有个无参构造函数。Hibernate在这个构造函数上使用Java Reflection API调用持久化类来实例化对象。构造函数可以是非公共的,但必须至少是包可见(package-visible)的,如果运行时代理要用于性能优化的话。代理生成也要求这个类不作final声明(也没有final方法)。
JavaBean规范定义了命名这些方法的指导方针,允许普通的工具(如Hibernate)轻松地发现和操作属性值。获取方法的名称以get开头,接着是属性名称(首字母大写);设置方法的名称以set开关,并且类似地也跟着属性名称。用于Boolean属性的获取方法可以用Is而不是Get。
实现POJO关联
你用属性表达POJO类之间的关联,并用访问方法在运行时从一个对象到一个对象进行导航。
考虑由Category类定义的那些关联,如图3-3所示。
和所有的图一样,上图省略了与关联相关的属性(我们称之为parentCategory和childCategories),因为它们会把图弄乱。操作它们值的这些属性和方法也称作脚手架代码(scaffolding code)。
这是一个Category类一对多自关联的脚手架代码:
public class Category {
private String name;// 类型名称
private Category parentCategory;// 父类型
private Set<Category> childCategories = new HashSet<Category>();// 子类型
public Category() {
super();
}
public Category(String name, Category parentCategory,
Set<Category> childCategories) {
super();
this.name = name;
this.parentCategory = parentCategory;
this.childCategories = childCategories;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Category getParentCategory() {
return parentCategory;
}
public void setParentCategory(Category parentCategory) {
this.parentCategory = parentCategory;
}
public Set<Category> getChildCategories() {
return childCategories;
}
public void setChildCategories(Set<Category> childCategories) {
this.childCategories = childCategories;
}
}
管理两个Category实例之间的链接,比在数据库字段中设置外键值更难。依据我们的经验,开发人员经常不知道从一个包含双向引用的网络对象模型中所产生的这种复杂性。我们将一步一步地探讨这个问题。
把一个子Category添加给一个父Category的基本过程看起来像这样:
Category aParent = new Category();
Category aChild = new Category();
aChild.setParentCategory(aParent);
aParent.getChildCategories().add(aChild);
说明:Hibernate中的托管关系——Hibernate不管理持久化关联。如果你要操作一个关联,必须编写与没有Hibernate时要编写完全相同的代码。如果关联是双向的,则关系的两侧都必须考虑。编程模型(如EJB2.1实体bean)通过引入容器托管的关系扰乱了这个行为——如果一侧被应用程序修改,容器就会自动改变关系的另一侧。这就是为什么使用EJB2.1实体bean的代码不能在容器之外被重用的原因之一。EJB3.0关联是透明的,就像在Hibernate中一样。如果你不理解Hibernate中关联的这种行为,就问问你自己:“没有Hibernate时我会怎么做?”Hibernate不会改变一般的Java语义。
给这些操作进行分组的Category类添加一种方便的方法是个好主意,这样允许重用,帮助确保正确性,并且最终保证数据的完整性:
public void addChildCategory(Category childCategory) {
if (null == childCategory)
throw new IllegalArgumentException("Null child category");
if (null != childCategory.getParentCategory()) {
childCategory.getParentCategory().getChildCategories().remove(
childCategory);
childCategory.setParentCategory(this);
this.childCategories.add(childCategory);
}
}
addChildCategory()方法不仅在处理Category对象时减少了代码行,而且加强了关联的基数性(cardinality)。它避免了漏掉两个必需动作中的其中一个时产生的错误。如果可能的话,应该始终对关联提供这种操作的分组。
由于想要addChildCategory()成为子类中唯一外部可见的存储器方法(mutator method)(可能还要加上removeChildCateogry()方法),你可以让setChildCategories()方法为私有,或者删除它,。
在Category和Item类之间存在着一种不同的关系:一个双向的多对多关联,如图3-4所示.
对于多对多的关联,两侧都用集合值属性实现。添加访问Item关系的新属性和方法到Category类:
public class Category {
private String name;// 类型名称
private Category parentCategory;// 父类型
private Set<Category> childCategories = new HashSet<Category>();// 子类型
private Set<Item> items = new HashSet<Item>();// 商品列表
}
Item类的代码(多对多关联的另一侧)类似于Category类的代码。添加集合属性,标准的访问方法,以及简化关系管理的一种方法
public class Item implements Serializable {
private static final long serialVersionUID = 2473606759362725264L;
private String name;
private String description;
private BigDecimal initialPrice;
private BigDecimal reservePrice;
private Date startDate;
private Date endDate;
private ItemState state;
private Date approvalDatetime;
private Set<Category> categories = new HashSet<Category>();
public void addCategory(Category category) {
if (null == category)
throw new IllegalArgumentException("Null category");
category.getItems().add(this);
categories.add(category);
}
}
addCategory()方法类似于Category类的addChildCategory()便利方法。它被客户端用来操作Item和Category之间的链接。为了增加可读性,后面的代码样例中将不显示便利方法了,并假设你会根据自己的偏好来添加它们。
给关联处理使用便利方法并不是改善领域模型实现的唯一途径。也可以把逻辑添加到你的访问方法中。
把逻辑添加到访问方法
我们喜欢使用JavaBean风格的访问方法的原因之一在于它们提供封装:一个属性的内部隐藏实现可以不做任何改变地变换为公共接口。
例如,如果数据库把用户名称保存为单个NAME列,但是User类有firstname和lastname属性,你可以把下列持久化的name属性添加到这样类:
public class User implements Serializable {
private static final long serialVersionUID = -7811634244303955773L;
private String firstname;
private String lastname;
// ...
public String getName() {
return firstname + lastname;
}
public void setName(String name) {
// StringTokenizer是一个用来分隔String的应用类
StringTokenizer t = new StringTokenizer(name, " ");
this.firstname = t.nextToken();
this.lastname = t.nextToken();
}
// ...
}
稍后,你会明白Hibernate定制类型是处理多个这种情况的一种更好的方法。然而,有多种选择的话也自有好处。
访问方法也可以执行验证。例如,在下列例子中,setFirstName()方法验证大写的名称:
public class User implements Serializable {
private static final long serialVersionUID = -7811634244303955773L;
private String firstname;
// ...
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) throws InvalidNameException {
if (!StringUtil.isCapitalizedName(firstname))
throw new InvalidNameException(firstname);
this.firstname = firstname;
}
}
另一个要考虑的问题是脏检查。Hibernate自动侦测对象状态的改变,以便使更新过的状态与数据库同步。从获取方法返回一个不同的对象,通常比由Hibernate传递到设置方法的对象来得安全。Hibernate按值比较对象——不是按对象同一性——来确定一个属性的持久化状态是否需要被更新。例如,下列获取方法就不会导致不必要的SQL UPDATE(更新):
public String getFirstname() {
return new String(firstname);
}
还有个重要的例外:集合是按同一性比较的!对于一个被映射为持久化集合的属性,你应该从获取方法中返回与Hibernate传递到设置方法中完全相同的集合实例。如果没有,Hibernate将更新数据库,即使不需要更新,保存在内存中的状态每次也都会与数据库同步。
最后,必须知道访问方法中的异常如何处理,如果你在加载和存储实例时配置Hibernate来使用这些方法的话。如果抛出RuntimeException,当前的事务就被回滚,你要自己处理这个异常。如果抛出已检查应用异常,Hibernate就会把这个异常包在一个RuntimeException里面。
XML中的元数据
任何ORM解决方案都应该提供一个人类可读的、易于手工编辑的映射格式,而不只是一个GUI映射工具。目前最受欢迎的对象/关系元数据格式是XML。以XML编写或者包含XML的映射文档都是轻量级的、人类可读的、易于通过版本控制系统和文本编辑器操作的,并且可以在部署被定制。但是基于XML的元数据真的是最好的方法吗?
在我们看来,有三大原因:
1)基于元数据的解决方案经常被不恰当地应用。元数据重来就没有简单的Java代码那么灵活或者易于维护。
2)许多现有的元数据格式不是被设计成易读和易于用手工编辑的。
3)好的XML编辑器,特别是在IDE中,不像好的Java编码环境一样普遍。
在ORM中无法避免对元数据的需求。然而,Hibernate在设计时充分认识到典型的元数据问题。Hibernate的XML元数据格式非常易读,并定义了有用的默认值。如果没有属性值,就在被映射的类上用反射来确定默认值。Hibernate还包含提供文档化的完整DTD。最终,IDE对XML的支持近来已经得到了改善,现代的IDE提供动态的XML验证,甚至提供一项自动完成的特性。
Category类的XML映射文件:
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.jbit.entity">
<class name="Category" table="TBL_CATEGORY">
<id name="id" column="CATEGORY_ID">
<generator class="native" />
</id>
<property name="NAME" column="NAME" type="string" />
</class>
</hibernate-mapping>
在这个例子中有意省略了集合和关联映射。关联尤其是集合映射更加复杂,因此后续将再回到这个话题。