熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java開源技術 >> 正文

別讓Hibernate偷走了您的身份

2022-06-13   來源: Java開源技術 

  企業級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(SetMap和List)的正確工作更是尤為重要

  為了闡明實現equal()和hashCode()的不同途徑讓我們考慮一個准備持久存儲到數據庫中的簡單對象Person

  

  

  public class Person {
private Long id;
private Integer version;

  public Long getId() {
return id;
}
public void setId(Long id) {
thisid = id;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
thisversion = version;
}

  // personspecific properties and behavior

  }

  在這個例子中我們遵循了同時持有id字段和version字段的最佳實踐Id字段保存了在數據庫中作為主鍵使用的值而version字段則是一個從開始增長的增量隨著對象的每次更新而變化(這幫助我們避免並發更新的問題)為了更清楚一些讓我們看看允許Hibernate把這個對象持久存儲到數據庫的Hibernate映射文件

  

  

  <?xml version=?>
<!DOCTYPE hibernatemapping SYSTEM

hibernatemappingdtd>

  <hibernatemapping package=mypackage>

  <class name=Person table=PERSON>

  <id name=id column=ID
unsavedvalue=null>
<generator class=sequence>
<param name=sequence>PERSON_SEQ</param>
</generator>
</id>

  <version name=version column=VERSION />

  <! Map Personspecific properties here >

  </class>

  </hibernatemapping>

  Hibernate映射文件指明了Person的id字段代表數據庫中的ID列(也就是說它是PERSON表的主鍵)包含在id標簽中的unsavedvalue=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 == othergetId()) return true;
if (id == null) return false;

  // equivalence by id
return idequals(othergetId());
}

  public int hashCode() {
if (id != null) {
return idhashCode();
} else {
return superhashCode();
}
}

  不幸的是這個實現存在著問題當我們首次創建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 equal
if (id == null || othergetId() == null)
return false;

  return idequals(othergetId());
}

  這裡有個隱含的問題Java Collection框架在Collection的生命周期中需要基於不變字段的equals()和hashCode()方法換句話來說當一個對象處在Collection中的時候不可以改變equals()和hashCode()的值舉個例子下面這段程序

  

  Person p = new Person();
Set set = new HashSet();
setadd(p);
Systemoutprintln(ntains(p));
psetId(new Long());
Systemoutprintln(ntains(p));
輸出結果true false

  對ntains(p)的第次調用返回false這是因為Set再也找不到p了用專業術語來講就是Set丟失了這個對象!這是因為當對象在集合中時我們改變了hashCode()的值

  當您想要創建一個將其他域對象保存在SetMap或是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 possible
private String id = IdGeneratorcreateId();
private Integer version;

  public String getId() {
return id;
}
public void setId(String id) {
thisid = id;
}

  public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
thisversion = version;
}

  // Personspecific 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 idequals(othergetId());
}

  public int hashCode() {
if (id != null) {
return idhashCode();
} else {
return superhashCode();
}
}
}

  這個例子使用對象id作為equals()方法判斷相等的標准以及hashCode()返回哈希碼的來源這就簡單了許多但是要讓它正常工作我們需要兩樣東西首先我們需要保證每個對象在被保存之前都有一個id值在這個例子裡當id變量被聲明的時候它就被指派了一個值其次我們需要一種判斷這個對象是新生成的還是之前保存過的的手段在我們最早的例子中Hibernate通過檢查id字段是否為null來判斷對象是否為新的既然對象id永不為null很顯然這種方法不再有效通過配置Hibernate讓它檢查version字段而不是id字段是否為null 我們可以很容易地解決這個問題version字段是一個更恰當的用來判斷對象是否被保存過的指示符

  下面是我們改進過的Person類的Hibernate映射文件

  

  

  <?xml version=?>
<!DOCTYPE hibernatemapping SYSTEM

hibernatemappingdtd>

  <hibernatemapping package=mypackage>

  <class name=Person table=PERSON>

  <id name=id column=ID>
<generator class=assigned />
</id>

  <version name=version column=VERSION
unsavedvalue=null />

  <! Map Personspecific properties here >

  </class>

  </hibernatemapping>

  注意id下面的generator標簽包含了屬性class=assigned這個屬性告訴Hibernate我們不是從數據庫指派id值而是在代碼中指派id值Hibernate會簡單地認為即使是新的未保存的對象也有id值我們也給version標簽新增了一個屬性unsavedvalue=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 AbstractPersistentObject
implements PersistentObject {

  private String id = IdGeneratorcreateId();
private Integer version;

  public String getId() {
return id;
}
public void setId(String id) {
thisid = id;
}

  public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
thisversion = 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 idequals(othergetId());
}

  public int hashCode() {
if (id != null) {
return idhashCode();
} else {
return superhashCode();
}
}

  public String toString() {
return thisgetClass()getName()
+ [id= + id + ];
}
}

  現在我們有了一個簡單而高效的方法來創建域對象它們擴展了AbstractPersistentObject該父類能在它們第一次創建時自動賦予一個id並且恰當地實現了equals()和hashCode()域對象也得到了一個toString()方法的合理默認實現這個方法可以有選擇地被重寫如果這是一個查詢例子的測試對象或者示例對象id可以被修改或者被設為null否則它是不應當被改變的如果因為某些原因我們需要創建一個擴展其他類的域對象這個對象就應當實現PersistentObject接口而不是擴展抽象類

  Person類現在就簡單多了

  

  

  public class Person
extends AbstractPersistentObject {

  // Personspecific fields and behavior here

  }

  從上一個例子開始Hibernate映射文件就不會再改變了我們不想麻煩Hibernate去了解抽象父類我們只要保證每個PersistentObject映射文件包含一個id項(和一個被指派的生成器)和一個帶有unsavedvalue=null屬性的version標簽機敏的讀者可能已經注意到每當一個持久性對象被實例化的時候它的id得到了指派這意味著當Hibernate在內存中創建一個已保存對象的實例時雖然這個對象是已經存在並從數據庫中讀取的它也會得到一個新的id這說好了然後Hibernate會接著調用對象的setId()方法用保存的id來替換新分配的id額外的id生成並不是什麼問題因為id生成算法是廉價的(也就是說它並不牽扯到數據庫)

  到現在為止一切都很好但是我們遺漏了一個重要的細節如何實現IdGeneratorcreateId()我們可以為理想中的鍵生成(keygeneration)算法定義一些標准

  鍵可以不牽扯到數據庫而很廉價地生成

  即使跨越不同的虛擬機和不同機器鍵也要保證唯一性

  如果可能鍵可以由其他程序編程語言和數據庫生成但是至少要能與它們兼容

  我們所需的是通用唯一標識符(universally unique identifierUUID)UUID由個字節(位)的數字組成遵守標准格式UUID的String版本看起來類似如下

  cdbceefdacaec

  裡面的字符是簡單的字節進制表示橫線把數字的不同部分分隔開來這種格式簡單而且易於處理只是個字符有點長了因為橫線總是被安置在相同的位置所以可以把它們去掉從而把字符的數目減少到為了更為簡潔地表示可以創建一個byte[]的數組或是兩個字節大小的long來保存這些數字如果您使用的是Java 或更高版本可以直接使用UUID類雖然這不是它在內存中最簡潔的格式有關更多信息請參閱Wikipedia UUID條目和JavaDoc UUID類條目

  UUID生成算法有多種實現既然最終UUID是一種標准格式我們在IdGenerator類中采用哪一種實現都沒有關系既然無論采用什麼算法每個id都會被保證唯一我們甚至可以在任何時候改變算法的實現或是混合匹配不同的實現如果您使用的是Java 或更高版本最方便的實現是javautilUUID類

  

  public class IdGenerator {
public static String createId() {
UUID uuid = javautilUUIDrandomUUID();
return uuidtoString();
}
}

  對不使用Java 或更高版本的人來說至少有兩種擴展庫實現了UUID並且與之前的Java版本兼容Apache Commons ID項目和Java UUID Generator (JUG)項目它們在Apache License之下都是可用的(在LGPL之下JUG也是可用的)

  這是使用JUG庫實現IdGenerator的例子

  

  

  import orgsafehausuuidUUIDGenerator;
public class IdGenerator {

  public static final UUIDGenerator uuidGen
= UUIDGeneratorgetInstance();

  public static String createId() {
UUID uuid
= uuidGengenerateRandomBasedUUID();
return uuidtoString();
}
}

  Hibernate中內置的UUID生成器算法又如何呢?這是獲得對象身份的UUID的適當途徑嗎?如果您想讓對象身份獨立於對象持久性這就不是一個好方法雖然Hibernate確實提供了生成UUID的選項但這樣的話我們又回到了最早的那個問題上對象ID的獲得並不在它們被創建的時候而是在它們被保存的時候

  使用UUID作為數據庫主鍵的最大障礙是它們在數據庫中(而不是在內存中)的大小在數據庫中索引和外鍵的復合會促使主鍵大小的增加您必須在不同情況下使用不同的表示方法使用String表示數據庫的主鍵大小將會是字節數字也可以直接以字節存儲這樣大小就減少一半但是如果直接查詢數據庫標識符將變得難以理解這些方法對您的項目是否可行取決於您的需求

  如果數據庫不接受UUID作為主鍵您可以考慮使用數據庫序列但總是應該在新對象創建的時候被指派一個ID而不是讓Hibernate管理ID在這種情況下創建新域對象的業務對象可以調用一個使用數據訪問對象(DAO)從數據庫序列中檢索id的服務如果使用一個Long數據類型來表示對象id一個單獨的數據庫序列(以及服務方法)對您的域對象來說就已經足夠了

  結束語

  當對象持久存儲到數據庫中時對象身份總是很難被恰當地實現盡管如此問題其實完全在於對象在保存之前允許對象沒有id就存在我們可以通過從諸如Hibernate這樣的對象關系映射框架中獲得指派對象ID的職責來解決這個問題一旦對象被實例化它就應該被指派一個ID這使對象身份變得簡單而不易出錯也減少了域模型中需要的代碼量


From:http://tw.wingwit.com/Article/program/Java/ky/201311/28001.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.