知识大全 别让Hibernate偷走了您的身份

Posted

篇首语:读书不要贪多,而是要多加思索,这样的读书使我获益不少。本文由小常识网(cha138.com)小编为大家整理,主要介绍了知识大全 别让Hibernate偷走了您的身份相关的知识,希望对你有一定的参考价值。

别让Hibernate偷走了您的身份  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!

  企业级Java应用程序常常把数据在Java对象和相关数据库之间来回移动 从手工编写SQL代码到诸如Hibernate这样成熟的对象关系映射(ORM)解决方案 有很多种方法可以实现这个过程 无论采用什么样的技术 一旦开始将Java对象持久存储到数据库中 身份将成为一个复杂且难以管理的课题 可能出现的情况是 您实例化了两个不同的对象 而它们却代表数据库中的同一行 为了解决这个问题 您可能采取的措施是在持久性对象中实现equals()和hashCode() 可是要恰当地实现这两个方法比乍看之下要有技巧一些 让问题更糟糕的是 那些传统的思路(包括Hibernate官方文档所提倡的)对于新的项目并不一定能提出最实用的解决方案

  对象身份在虚拟机(VM)中和在数据库中的差异是问题滋生的温床 在虚拟机中 您并不会得到对象的ID 您只是简单地持有对象的直接引用 而在幕后 虚拟机确实给每个对象指派了一个 字节大小的ID 这个ID才是对象的真实引用 当您将对象持久存储到数据库中的时候 问题开始产生了 假定您创建了一个Person对象并将它存入数据库(我们可以叫它person ) 而您的其他某段代码从数据库中读取了这个Person对象的数据 并将它实例化为另一个新的Person对象(我们可以叫它Person ) 现在您的内存中有了两个映射到数据库中同一行的对象 一个对象引用只能指向它们的其中一个 可是我们需要一种方法来表示这两个对象实际上表示著同一个实体 这就是(在虚拟机中)引入对象身份的原因

  在Java语言中 对象身份是由每个对象都持有的equals()方法(以及相关的hashCode()方法)来定义的 无论两个对象是否为同一个实例 equals()方法都应该能够判别出它们是否表示同一个实体 hashCode()方法和equals()方法有关联是因为所有相等的对象都应该返回相同的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字段则是一个从 开始增长的增量 随着对象的每次更新而变化(这帮助我们避免并发更新的问题) 为了更清楚一些 让我们看看允许Hibernate把这个对象持久存储到数据库的Hibernate映射文件

  

  

  <?xml version= ?><!DOCTYPE hibernate mapping SYSTEM hibernate mapping dtd >

  <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>

  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将会执行乐观锁定方案 根据这个方案 Hibernate在保存对象之前会根据数据库版本号检查对象的版本号

  我们的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 idreturn 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里面 事实上第二个Person对象并不能被加入 这是因为Set会断定所有未保存的对象都是相同的

  您可能会试图去实现一个使用id(只在已设置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 equalif (id == null || other getId() == null)return false;

  return id equals(other getId());

  这里有个隐含的问题 Java Collection框架在Collection的生命周期中需要基于不变字段的equals()和hashCode()方法 换句话来说 当一个对象处在Collection中的时候 不可以改变equals()和hashCode()的值 举个例子 下面这段程序

  

  Person p = new Person();Set set = new HashSet();set add(p);System out println(ntains(p));p setId(new Long( ));System out println(ntains(p));输出结果 true false

  对ntains(p)的第 次调用返回false 这是因为Set再也找不到p了 用专业术语来讲 就是Set丢失了这个对象!这是因为当对象在集合中时 我们改变了hashCode()的值

  当您想要创建一个将其他域对象保存在Set Map或是List中的域对象时 这是一个问题 为了解决这个问题 您必须为所有对象提供一种equals()和hashCode()的实现 这种实现能够保证在它们在对象保存前后正确工作并且当对象在内存中时(返回值)不可变 Hibernate Reference Documentation (v )提供了以下的建议

   不要使用数据库标识符来实现相等性判断 而应该使用业务键(business key) 这是一个唯一的 通常不改变的属性的组合体 当一个瞬态对象(transient object)被持久化的时候 数据库标识符会发生改变 当一个瞬态实例(常常与detached实例一起使用)保存在一个Set中时 哈希码的改变会破坏Set的约定 业务键的属性并不要求和数据库主键一样稳定 只要保证当对象在同一个Set中时它们的稳定性 (Hibernate Reference Documentation v )

   我们推荐通过判断业务键相等性来实现equals()和hashCode() 业务键相等性意味着equals()方法只比较能够区分现实世界中实例的业务键(普通候选键)的属性 (Hibernate Reference Documentation v )

  换句话说 普通键用于equals()和hashCode() 而Hibernate生成的代理项键用于对象的id 这要求对于每个对象有一个相关的不可变的业务键 可是 并不是每个对象类型都有这样的一种键 这时候您可能会尝试使用会改变但不经常改变的字段 这和业务键不必与数据库主键一样稳定的思想相吻合 如果这种键在对象所在集合的生存期中不改变 那这就 足够好 了 这是一种危险的观点 因为这意味着您的应用程序可能不会崩溃 但是前提是没有人在特定的情况下更新了特定的字段 所以 应当有一种更好的解决方案 这种解决方案确实也存在

  不要让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 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字段是否为null来判断对象是否为新的 既然对象id永不为null 很显然这种方法不再有效 通过配置Hibernate 让它检查version字段 而不是id字段是否为null 我们可以很容易地解决这个问题 version字段是一个更恰当的用来判断对象是否被保存过的指示符

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

  

  

  <?xml version= ?><!DOCTYPE hibernate mapping SYSTEM hibernate mapping 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由 个字节( 位)的数字组成 遵守标准格式 UUID的String版本看起来类似如下

   cdb cee f d a c ae c

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

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

  

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

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

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

  

  

  import 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表示 数据库的主键大小将会是 或 字节 数字也可以直接以字节存储 这样大小就减少一半 但是如果直接查询数据库 标识符将变得难以理解 这些方法对您的项目是否可行取决于您的需求

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

  结束语

cha138/Article/program/Java/ky/201311/28001

相关参考

知识大全 别让Hibernate偷走了你的标识符[4]

别让Hibernate偷走了你的标识符[4]  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!  下面

知识大全 别让Hibernate偷走了你的标识符[3]

别让Hibernate偷走了你的标识符[3]  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!  当你

知识大全 别让Hibernate偷走了你的标识符[1]

别让Hibernate偷走了你的标识符[1]  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!  当对

知识大全 别让Hibernate偷走了你的标识符[5]

别让Hibernate偷走了你的标识符[5]  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!  Pe

知识大全 别让Hibernate偷走了你的标识符[2]

别让Hibernate偷走了你的标识符[2]  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!  Hi

别让情绪影响了您的牛皮癣

近年来由于各种原因,导致各式各样的疾病大肆泛滥,牛皮癣也不列外,发病率在不断攀升,而且涉及的范围也越来越广,就连小孩和青少年也没能逃脱牛皮癣的魔掌。牛皮癣是一种比较难缠的皮肤病,发病原因比较复杂,到目

我想握住您的手

这幅场景经常在我的梦里出现:和煦的阳光下,一个上海市郊的农家小院里,我看见了您—我的恩师,一个质朴的农民的儿子,眼睛里闪烁着刚毅。我上前紧紧握住您的手,热切地叫一声:“老师!”您眼里的刚毅变成了温情,

知识大全 华为手机总是弹出请输入您的身份进行验证

华为手机总是弹出请输入您的身份进行验证那就是无网密码不对了HG的无网密码在设备后面的标签上。急!iphone4s"请输入您的身份信息完成注册"我的也面临同样的问题,请问你的问题解决了吗?我感觉很烦!为

知识大全 中国农业银行您的信用卡申请已通过初审,请您本人尽快携带身份证

【中国农业银行】您的信用卡申请已通过初审,请您本人尽快携带身份证信用卡申请方法:1,柜台申请:携带资信证明到柜面申请:需要申请人提交比较完备的资产证明文件,向银行提交材料供银行后台审核部门审核;审核通

分红险的优势

1.分红型保险从某种意义上来说是最“便宜”的保险。保险公司收了您和其他人的保费如果去掉赔付和公司运营成本后来有剩余的话,剩余的钱会通过红利返还给您。保险公司拿您的保费进行投资,投资收益的七成以上也会通