当前位置: 首页 > 图文教程 > Java技术 > 数据库持久层 > 别让Hibernate偷走了您的身份(二)

数据库持久层
数据库持久层:Hibernate中cascade和inverse的用法
数据库持久层:hibernate的配置信息
数据库持久层:谈hibernate的持续性
数据库持久层:小议Hibernate查询及数据检索
数据库持久层:浅谈hibernate中的Session
数据库持久层:小编浅谈hibernate中抓取策略
数据库持久层:iBATIS框架中Serializable可读写缓存及存储类型
数据库持久层:ibatis高级特性
数据库持久层:为持久层编写单元测试
数据库持久层:小编浅谈ibatis基础语义
数据库持久层:Hibernate中与关联有关的问题
数据库持久层:浅谈ORM(对象关系映射)
总结JDBC连接SQLServer的错误Error establishing socket
数据库持久层:iBATIS的OR 映射
数据库持久层:DAO和ODBC的相似之处
数据库持久层:小编浅谈DAO的特色
数据库持久层:小编浅谈XDoclet 与Hibernate 映射
数据库属性hibernate.dialect的设置

数据库持久层 中的 别让Hibernate偷走了您的身份(二)


出处:互联网   整理: 软晨网(RuanChen.com)   发布: 2009-08-14   浏览: 71 ::
收藏到网摘: n/a

不要让Hibernate管理您的id.
  试图创建和维护对象及数据库行的各自身份定义是目前为止所有讨论问题的根源。如果我们统一所有身份形式,这些问题都将不复存在。也就是说,作为以数据库为中心和以对象为中心的ID的替代品,我们应该创建一种通用的、特定于实体的ID来代表数据实体,这种ID应该在数据第一次输入的时候创建。无论这个唯一数据实体是保存在数据库中,是作为对象驻留在内存中,还是存储在其他格式的介质中,这个通用ID都应该可以识别它。通过使用数据实体第一次创建时指派的实体ID,我们可以安全地回到equals()和hashCode()的原始定义,它们只需使用这个id:

public class Person {// assign an id as soon as possibleprivate String id = IdGenerator.createId();private Integer version;public String getId() {return id;}public void setId(String id) {this.id = id;}public Integer getVersion() {return version;}public void setVersion(Integer version) {this.version = version;}// Person-specific fields and behavior herepublic boolean equals(Object o) {if (this == o) return true;if (o == null || !(o instanceof Person))return false;Person other = (Person)o;if (id == null) return false;return id.equals(other.getId());}public int hashCode() {if (id != null) {return id.hashCode();} else {return super.hashCode();}}}


  这个例子使用对象id作为equals()方法判断相等的标准,以及hashCode()返回哈希码的来源。这就简单了许多。但是,要让它正常工作,我们需要两样东西。首先,我们需要保证每个对象在被保存之前都有一个id值。在这个例子里,当id变量被声明的时候,它就被指派了一个值。其次,我们需要一种判断这个对象是新生成的还是之前保存过的的手段。在我们最早的例子中,Hibernate通过检查id字段是否为null来判断对象是否为新的。既然对象id永不为null,很显然这种方法不再有效。通过配置 Hibernate,让它检查version字段,而不是id字段是否为null,我们可以很容易地解决这个问题。version字段是一个更恰当的用来判断对象是否被保存过的指示符。

  下面是我们改进过的Person类的Hibernate映射文件。

<?xml version="1.0"?><!DOCTYPE hibernate-mapping SYSTEM"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"><hibernate-mapping package="my.package"><class name="Person" table="PERSON"><id name="id" column="ID"><generator class="assigned" /></id><version name="version" column="VERSION"unsaved-value="null" /><!-- Map Person-specific properties here. --></class></hibernate-mapping>


  注意,id下面的generator标签包含了属性 class="assigned".这个属性告诉Hibernate我们不是从数据库指派id值,而是在代码中指派id值。Hibernate会简单地认为即使是新的未保存的对象也有id值。我们也给version标签新增了一个属性:unsaved-value="null".这个属性告诉 Hibernate应该把version值而不是id值为null作为对象是新创建而成的指示器。我们也可以简单地告诉Hibernate把负值作为对象未保存的指示符,如果您喜欢把version字段的类型设置为int而不是Integer,这将是很有用的。

  我们已经从转移到纯对象id中获取了不少好处。我们对equals()和 hashCode()方法的实现更加简单而且更易阅读。这些方法再也不易出错而且无论在保存对象之前还是之后,它们都能与Collection一起正常工作。Hibernate也变得更快一些,这是因为在保存新的对象之前它再也不需要从数据库读取一个序列值。此外,新定义的equals()和 hashCode()对于所有包含id对象的对象来说是通用的。这意味着我们可以把这些方法移至一个抽象父类。我们不再需要为每个域对象重新实现 equals()和hashCode(),而且我们也不再需要考虑对于每个类来说哪些字段组合是唯一且不变的。我们只要简单地扩展这个抽象父类。当然,我们没必要强迫域对象从父类中扩展出来,所以我们定义了一个接口来保证设计的灵活性。

public interface PersistentObject {public String getId();public void setId(String id);public Integer getVersion();public void setVersion(Integer version);}public abstract class AbstractPersistentObjectimplements PersistentObject {private String id = IdGenerator.createId();private Integer version;public String getId() {return id;}public void setId(String id) {this.id = id;}public Integer getVersion() {return version;}public void setVersion(Integer version) {this.version = version;}public boolean equals(Object o) {if (this == o) return true;if (o == null ||!(o instanceof PersistentObject)) {return false;}PersistentObject other= (PersistentObject)o;// if the id is missing, return falseif (id == null) return false;// equivalence by idreturn id.equals(other.getId());}public int hashCode() {if (id != null) {return id.hashCode();} else {return super.hashCode();}}public String toString() {return this.getClass().getName()+ "[id=" + id + "]";}}


  现在我们有了一个简单而高效的方法来创建域对象。它们扩展了 AbstractPersistentObject,该父类能在它们第一次创建时自动赋予一个id,并且恰当地实现了equals()和 hashCode()。域对象也得到了一个toString()方法的合理默认实现,这个方法可以有选择地被重写。如果这是一个查询例子的测试对象或者示例对象,id可以被修改或者被设为null.否则它是不应当被改变的。如果因为某些原因我们需要创建一个扩展其他类的域对象,这个对象就应当实现 PersistentObject接口而不是扩展抽象类。

  Person类现在就简单多了:

public class Personextends AbstractPersistentObject {// Person-specific fields and behavior here}


  从上一个例子开始Hibernate映射文件就不会再改变了。我们不想麻烦Hibernate去了解抽象父类,我们只要保证每个PersistentObject映射文件包含一个id项(和一个“被指派的”生成器)和一个带有 unsaved-value="null"属性的version标签。机敏的读者可能已经注意到,每当一个持久性对象被实例化的时候,它的id得到了指派。这意味着当Hibernate在内存中创建一个已保存对象的实例时,虽然这个对象是已经存在并从数据库中读取的,它也会得到一个新的id.这说好了。然后Hibernate会接着调用对象的setId()方法,用保存的id来替换新分配的id.额外的id生成并不是什么问题,因为id生成算法是廉价的(也就是说,它并不牵扯到数据库)。

  到现在为止一切都很好,但是我们遗漏了一个重要的细节:如何实现IdGenerator.createId()。我们可以为理想中的键生成(key-generation)算法定义一些标准:

  键可以不牵扯到数据库而很廉价地生成。

  即使跨越不同的虚拟机和不同机器,键也要保证唯一性。

  如果可能,键可以由其他程序、编程语言和数据库生成,但是至少要能与它们兼容。

  我们所需的是通用唯一标识符(universally unique identifier,UUID)。UUID由16个字节(128位)的数字组成,遵守标准格式。UUID的String版本看起来类似如下:

  2cdb8cee-9134-453f-9d7a-14c0ae8184c6

  里面的字符是简单的字节16进制表示,横线把数字的不同部分分隔开来。这种格式简单而且易于处理,只是36个字符有点长了。因为横线总是被安置在相同的位置,所以可以把它们去掉,从而把字符的数目减少到32个。为了更为简洁地表示,可以创建一个byte[16]的数组或是两个8字节大小的long来保存这些数字。如果您使用的是Java 1.5或更高版本,可以直接使用UUID类,虽然这不是它在内存中最简洁的格式。有关更多信息,请参阅Wikipedia UUID条目和JavaDoc UUID类条目。

  UUID生成算法有多种实现。既然最终UUID是一种标准格式,我们在 IdGenerator类中采用哪一种实现都没有关系。既然无论采用什么算法每个id都会被保证唯一,我们甚至可以在任何时候改变算法的实现或是混合匹配不同的实现。如果您使用的是Java 1.5或更高版本,最方便的实现是java.util.UUID类:

public class IdGenerator {public static String createId() {UUID uuid = java.util.UUID.randomUUID();return uuid.toString();}}


  对不使用Java 1.5或更高版本的人来说,至少有两种扩展库实现了UUID并且与1.5之前的Java版本兼容:Apache Commons ID项目和Java UUID Generator (JUG)项目。它们在Apache License之下都是可用的(在LGPL之下JUG也是可用的)。

  这是使用JUG库实现IdGenerator的例子:

import org.safehaus.uuid.UUIDGenerator;public class IdGenerator {public static final UUIDGenerator uuidGen= UUIDGenerator.getInstance();public static String createId() {UUID uuid= uuidGen.generateRandomBasedUUID();return uuid.toString();}}


  Hibernate中内置的UUID生成器算法又如何呢?这是获得对象身份的UUID的适当途径吗?如果您想让对象身份独立于对象持久性,这就不是一个好方法。虽然Hibernate确实提供了生成UUID的选项,但这样的话我们又回到了最早的那个问题上:对象ID的获得并不在它们被创建的时候,而是在它们被保存的时候。

  使用UUID作为数据库主键的最大障碍是它们在数据库中(而不是在内存中)的大小,在数据库中索引和外键的复合会促使主键大小的增加。您必须在不同情况下使用不同的表示方法。使用String表示,数据库的主键大小将会是32或36 字节。数字也可以直接以字节存储,这样大小就减少一半,但是如果直接查询数据库,标识符将变得难以理解。这些方法对您的项目是否可行取决于您的需求。

  如果数据库不接受UUID作为主键,您可以考虑使用数据库序列。但总是应该在新对象创建的时候被指派一个ID而不是让Hibernate管理ID.在这种情况下,创建新域对象的业务对象可以调用一个使用数据访问对象(DAO)从数据库序列中检索id的服务。如果使用一个Long数据类型来表示对象id,一个单独的数据库序列(以及服务方法)对您的域对象来说就已经足够了。

  结束语

  当对象持久存储到数据库中时,对象身份总是很难被恰当地实现。尽管如此,问题其实完全在于,对象在保存之前允许对象没有id就存在。我们可以通过从诸如Hibernate这样的对象关系映射框架中获得指派对象ID的职责来解决这个问题。一旦对象被实例化,它就应该被指派一个ID.这使对象身份变得简单而不易出错,也减少了域模型中需要的代码量。