副标题#e#
摘要:
当工具耐久化到数据库中时,工具的标识符总时很难被得当的实现。尽量如此,问题其实完全是由存在着在生存之前不持有ID的工具的现象衍生而来的。我们可以通过从诸如Hibernate这样的工具—干系映像框架手中取走指派工具ID的职责来办理这个问题。相对的,一旦工具被实例化,它就应该被指派一个ID。这使工具标识符酿成简朴而不易堕落,也淘汰了规模模子中需要的代码量。
企业级java应用措施经常把数据在java工具和干系型数据库之间往返移动。从手动编写SQL代码到利用诸如hibernate这样的成熟的工具—干系映像(ORM)办理方案,有许多种要领可以实现这个进程。无论你回收什么样的技能,一旦你开始将java工具耐久化到数据库中,工具标识符都将成为一个巨大并且难以打点的课题。大概呈现的环境是:你实例化了两个差异的工具,而它们却代表了数据库中的同一行。为了办理这个问题,你大概采纳的法子是在你的耐久化工具中实现equals() 和hashCode()这两个要领,但是要得当的实现这两个要领比乍看之下要有能力一些。让问题更糟糕的是,那些传统的思路(包罗hibernate官方文档所倡导的那些)对付新的工程并不必然能提出最实用的办理方案。
工具标识在虚拟机(VM)中和在数据库中的差别是问题滋生的温床。在虚拟机中,你并不会获得工具的id,你只是简朴的持有工具的直接引用。而在幕后,虚拟机确实给每个工具指派了一个8字节巨细的id,这个id才是工具的真实引用。当你将工具耐久化到数据库中的时候,问题开始发生了。假定你建设了一个Person工具并将它存入数据库(我们可以叫它person1)。而你的其它某段代码从数据库中读取了这个Person工具的数据并将它实例化为另一个新的Person工具(我们可以叫它Person2)。此刻你的内存中有了两个映像到数据库中同一行的工具。一个工具引用只能指向它们俩的个中一个,但是我们需要一种要领来暗示这两个工具实际上暗示着同一个实体。这就是(在虚拟机中)引入工具标识符的原因。
在java语言中,工具标识符是由每个工具都持有的equals()要领(以及相关的hashCode()要领)来界说的。无论两个工具(引用)是否为同一个实例,equals()要领都应该可以或许鉴别出它们是否暗示同一个实体。hashCode()要领和equals()要领有关联是因为所有被判定等价(equal)的工具都应该返回沟通的哈希值(hashCode)。在缺省实现中,equals()要领仅仅较量工具的引用,一个工具和它自身是等价的,而和其它任何实例都不等价。对付耐久化工具来说,重写这两个要领,让代表着数据库中同一行的两个工具被判为等价是很重要的。而这对付java中的Collection数据布局(Set,Map和List)的正确事情更是尤为重要。
为了阐发实现equal()和hashCode()的差异途径,让我们一起思量一个筹备耐久化到数据库中的简朴工具Person。
public class Person {
private Long id;
private Integer version;
public Long getId() { return id; }
public void setId(Long id) {
this.id = id;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
// person-specific properties and behavior
}
在这个例子中,我们遵循了同时持有id字段和version字段的最佳实践。Id字段生存了在数据库中作为主键利用的值,而version字段则是一个从0开始增长的增量,跟着工具的每次更新而变革(它辅佐我们制止并发更新的问题)。为了看的更清楚,我们也一起看一下Hibernate把这个工具耐久化到数据库的映像文件。
<?xml version="1.0"?>
<hibernate-mapping package="my.package">
<class name="Person" table="PERSON">
<id name="id" column="ID" unsaved-value="null">
<generator class="sequence">
<param name="sequence">PERSON_SEQ</param>
</generator>
</id>
<version name="version" column="VERSION" />
<!-- Map Person-specific properties here. -->
</class>
</hibernate-mapping>
#p#副标题#e#
Hibernate映像文件指明白Person的id字段代表了数据库中的ID列(也就是说,它是PERSON表的主键)。包括在id标签中的unsaved-value="null"属性汇报Hibernate利用id字段来判定一个Person工具之前是否被生存过。ORM框架必需依靠这个来判定生存一个工具的时候应该利用SQL的INSERT字句照旧UPDATE字句。在这个例子中,Hibernate假定一个新工具的id字段一开始为null值,当它第一次被生存时才id才被赋予一个值。generator标签汇报Hibernate当工具第一次生存时,应该从那边得到指派的id。在这个例子中,Hibernate利用数据库序列作为发生独一id的来历。最后,version标签汇报Hibernate利用Person工具的version字段举办并发节制。Hibernate将会执行乐观锁方案(optimistic locking scheme),按照这个方案,Hibernate在生存工具之前会查抄比拟工具的version值和数据库中相应数据的version值。
#p#分页标题#e#
我们的Person工具还缺少的是equals()要领和hashCode()要领的实现。既然这是一个耐久化工具,我们并不想依赖于这两个要领的缺省实现,因为缺省实现并不能判别代表数据库中同一实体的差异实例。一种简朴而又显然的实现要领是操作id字段来举办equal()要领的较量以及生成hashCode()要领的功效。
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof Person))
return false;
Person other = (Person)o;
if (id == other.getId()) return true;
if (id == null) return false;
// equivalence by id
return id.equals(other.getId());
}
public int hashCode() {
if (id != null) {
return id.hashCode();
}
else {
return super.hashCode();
}
}
不走运的是,这个实现存在着问题。当我们首次建设Person工具的时候id的值是null,这意味着任何两个没有被生存的Person工具都将被认为是等价的。假如我们想建设一个Person工具并把它放到Set数据布局中,再建设了一个完全差异的Person工具也把它放到同一个Set内里,事实上第2个Person工具并不能被插手。这是因为Set会断定所有未经生存的工具都是沟通的。
你大概会试探着去实现一个只利用被配置过的id的equals()要领。究竟,假如两个工具都没有被生存过,我们可以假定它们是差异的工具。这是因为在它们被生存到数据库的时候,它们会被赋予差异的主键。
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof Person)) return false;
Person other = (Person)o;
// unsaved objects are never equal
if (id == null || other.getId() == null) return false;
return id.equals(other.getId());
}
这里有个埋没的问题。Java的Collection框架在它的生命周期中需要基于稳定字段的equals()和hashCode()要领。换句话来说,当一个工具处在Collection中的时候,你不行以改变equals()和hashCode()的返回值。举个例子,下面这段措施:
Person p = new Person();
Set set = new HashSet();
set.add(p);
System.out.println(set.contains(p));
p.setId(new Long(5));
System.out.println(set.contains(p));
打印功效: true false
对set.contains(p)的第2次挪用返回了false是因为Set再也找不到p了。用书面化的语言讲,Set丢失了这个工具!这是因为当工具在Set中时,我们改变了hashCode()的返回值。
当你想要建设一个将其它域工具生存在Set,Map或是List内里的域工具时,这是一个问题。为了办理这个问题,你必需为你的所有工具提供一种equals()和hashCode()的实现,这种实现可以或许担保在它们在工具生存前后正确事情而且当工具在内存中时(返回值)不会改变。Hibernate参考文档提供了以下的发起:
“不要利用数据库标识符来实现等价的判定,而应该利用贸易键值(business key),一种独一的,凡是不改变的属性的团结体。当一个buk不行序列化工具(transient object)被耐久化的时候,数据库标识符会产生改变。当一个不行序列化实例(经常和detached instances在一起)被包括在一个Set内里时,哈希值的改变会粉碎Set的从属干系。贸易键值的属性并不要求和数据库主键一样不变,你只要担保当工具在某个Set中时它们的不变性。
“我们推荐判定贸易键值的等价性来实现equals()和hashCode()两个要领。这意味着equals()要领只较量可以或许区分现实世界中的实例的贸易键值(某个候选码)的属性。“(Hibernate 参考文档 v. 3.1.1).
换句话说,equals()和hashCode()利用贸易键值举办处理惩罚,而工具利用Hibernate生成的键值作为id值。这要求对付每个工具有一个相关的不会改变的贸易键值。但是,并不是每个工具范例都有这样的一种键,这时候你大概会实验利用会改变但不时常改变的字段。这和贸易键值不必和数据库主键一样不变的思想相吻合。当工具在Collection中时候假如这种键不改变,那它们好像就“足够好”了。这是一种危险的主张,这意味着你的应用措施大概不会瓦解,可是前提是没有人在特定的环境下更新了特定的字段。所以,该当有一种更好的办理方案,而它确实也存在。 试图建设和维护在工具和数据库行两者间有着疏散的界说的标识符是今朝为止接头的所有问题的来源。假如我们统一所有标识符的形式,这些问题都将不复存在。也就时说,作为以数据库为中心和以工具为中心的标识符的替代品,我们应该建设一种通用的,特定于实体的ID来代表数据实体,这种ID应该在数据第一次输入的时候发生。无论一个独一数据实体是生存在数据库,是作为工具驻留在内存,还时存贮在其它名目标介质中,这个通用ID都应该可以识别它。通过利用数据实体第一次建设时指派的ID,我们可以安详的回到我们对equals()和hashCode()的原始界说。它们只是简朴地利用了这个id:
#p#分页标题#e#
public class Person {
// assign an id as soon as possible
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;
}
// Person-specific fields and behavior here
public 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字段是否为空来判定工具是否时新生成的。既然我们的工具id永远不为空,这个要领显然不再有效。为了办理这个问题,我们可以很容易的设置Hibernate,让它查抄version字段,而不是id字段是否为空。version字段是一个更为得当的用来判定你的工具是否被生存过的指示器。
下面是我们改造过的Person类的Hibernate映射文件。
<?xml version="1.0"?>
<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(),并且我们也不再需要思量对付一个类来说哪些字段的组合是独一且稳定的。我们只要简朴地担任这个抽象类。虽然,我们没须要强迫我们的域工具担任一个父类,所以我们界说了一个接口来担保设计的机动性。
#p#分页标题#e#
public interface PersistentObject {
public String getId();
public void setId(String id);
public Integer getVersion();
public void setVersion(Integer version);
}
public abstract class AbstractPersistentObject implements 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 false
if (id == null) return false;
// equivalence by id
return 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 Person extends AbstractPersistentObject { // Person-specific fields and behavior here}
从上一个例子开始Hibernate映像文件就不会再改变了。我们不想贫苦Hibernate去相识抽象父类,相对的,我们只要担保每个耐久化工具的映射文件包括一个id项(和一个被指派的生成器)和一个带有unsaved-value="null"属性的version标签。灵巧的读者大概已经留意到,每当一个耐久化工具被实例化的时候,它的id值获得了指派。这意味着当Hibernate在内存中建设一个已经生存过的工具时,固然这个工具是已经存在并从数据库中读取的,它也会获得一个新的id。这不会发生问题,因为Hibernate会接着挪用工具的setId()要领,用生存的真实id来替换新分派的id。剩下的id生成器并不是问题,因为实现它的算法是轻量级的(也就是说,它并不牵扯到数据库)。
到此刻为止一切都很好,可是我们漏掉了一个重要的细节:如何实现IdGenerator.createId().我们可觉得我们抱负中的键值生成器(key-generation)算法界说一些尺度。
。键值可以不牵扯到数据库而很轻量级的发生
。纵然超过差异的虚拟机和差异呆板,键值也要担保独一性。
。假如大概键值可以由其它措施,编程语言和数据库生成,至少要能和它们兼容。
我们需要的是通用独一标识符(UUID)。UUID是由尺度名目化的16个字节巨细的(128位)数字构成的。UUID的字符串版本是像这样的:
2cdb8cee-9134-453f-9d7a-14c0ae8184c6(各人应该可以留意到, Jmatrix今朝就是利用的UUID)
内里的字符是数字简朴的按字节的16进制暗示,横线把数字的差异部门支解开来。这种名目简朴并且易于处理惩罚,只是36个字符有点儿太长了。因为横线老是被安放在沟通的位置,所以可以把它们去掉而把字符的数目淘汰到32个。用一种更为简捷的暗示要领,你可以建设一个byte[16]的数组或是两个8字节巨细的长整型(long)来生存这些数字。假如你利用的是java1.5或更高版本,你可以直接利用UUID类,固然这不是它在内存中最简捷的名目。假如你要得到更多的信息,请参阅Wikipedia 的UUID条目 或 Java UUID参考文档。
对UUID的发生算法有多种实现。既然最终UUID是一种尺度名目,我们在IdGenerator类中回收哪一种实现都没有干系。既然无论回收什么算法每个id城市被担保独一,我们甚至可以在任何时候改变算法的实现或是殽杂匹配差异的实现。假如你利用的是java1.5或更高版本,最利便的实现是java.util.UUID类。
#p#分页标题#e#
public class IdGenerator {
public static String createId() {
UUID uuid = java.util.UUID.randomUUID();
return uuid.toString();
}
}
对不利用java1.5或更高版本的人来说,至少有两种扩展库实现了UUID而且和1.5之前的java版本兼容: Apache Commons ID project 和 Java UUID Generator(JUG) project.它们都在Apache的旗下。(在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字节。Id也可以直接利用位存储,这样将淘汰一半的占用空间,可是假如你直接查询数据库,id将变得难以领略。这些要领对你的工程是否可行取决于你的需求。 假如你的数据库不接管UUID作为主键,你可以思量利用数据库序列。但老是应该让新工具建设的时候被指派一个ID而不是让Hibernate打点你的ID。在这种环境下,建设新的域工具的贸易工具可以挪用一个利用data access object(DAO)从数据库序列中获取数据库id的处事。假如你利用一个长整型来暗示你的工具id,一个单独的数据库序列(以及处事要领)对你的域工具来说已经足够了。
小结
当工具耐久化到数据库中时,工具的标识符总时很难被得当的实现。尽量如此,问题其实完全是由存在着在生存之前不持有ID的工具的现象衍生而来的。我们可以通过从诸如Hibernate这样的工具—干系映像框架手中取走指派工具ID的职责来办理这个问题。相对的,一旦工具被实例化,它就应该被指派一个ID。这使工具标识符酿成简朴而不易堕落,也淘汰了规模模子中需要的代码量。