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

別讓Hibernate偷走了你的標識符

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

  當對象持久化到數據庫中時對象的標識符總時很難被恰當的實現盡管如此問題其實完全是由存在著在保存之前不持有ID的對象的現象衍生而來的我們可以通過從諸如Hibernate這樣的對象—關系映像框架手中取走指派對象ID的職責來解決這個問題相對的一旦對象被實例化它就應該被指派一個ID這使對象標識符變成簡單而不易出錯也減少了領域模型中需要的代碼量

  企業級Java應用程序常常把數據在java對象和關系型數據庫之間來回移動從手動編寫SQL代碼到使用諸如hibernate這樣的成熟的對象——關系映像(ORM)解決方案有很多種方法可以實現這個過程無論你采用什麼樣的技術一旦你開始將java對象持久化到數據庫中對象標識符都將成為一個復雜而且難以管理的課題可能出現的情況是你實例化了兩個不同的對象而它們卻代表了數據庫中的同一行為了解決這個問題你可能采取的措施是在你的持久化對象中實現equals() 和hashCode()這兩個方法可是要恰當的實現這兩個方法比乍看之下要有技巧一些讓問題更糟糕的是那些傳統的思路(包括hibernate官方文檔所提倡的那些)對於新的工程並不一定能提出最實用的解決方案

  對象標識在虛擬機(VM)中和在數據庫中的差異是問題滋生的溫床在虛擬機中你並不會得到對象的id你只是簡單的持有對象的直接引用而在幕後虛擬機確實給每個對象指派了一個字節大小的id這個id才是對象的真實引用當你將對象持久化到數據庫中的時候問題開始產生了假定你創建了一個Person對象並將它存入數據庫(我們可以叫它person而你的其它某段代碼從數據庫中讀取了這個Person對象的數據並將它實例化為另一個新的Person對象(我們可以叫它Person現在你的內存中有了兩個映像到數據庫中同一行的對象一個對象引用只能指向它們倆的其中一個可是我們需要一種方法來表示這兩個對象實際上表示著同一個實體這就是(在虛擬機中)引入對象標識符的原因

  在java語言中對象標識符是由每個對象都持有的equals()方法(以及相關的hashCode()方法)來定義的無論兩個對象(引用)是否為同一個實例equals()方法都應該能夠判別出它們是否表示同一個實體hashCode()方法和equals()方法有關聯是因為所有被判斷等價(equal)的對象都應該返回相同的哈希值(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把這個對象持久化到數據庫的映像文件

  <?XML version=?> <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將會執行樂觀鎖方案(optimistic locking scheme)根據這個方案Hibernate在保存對象之前會檢查對比對象的version值和數據庫中相應數據的version值

  我們的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的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框架在它的生命周期中需要基於不變字段的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丟失了這個對象!這是因為當對象在Set中時我們改變了hashCode()的返回值

  當你想要創建一個將其它域對象保存在SetMap或是List裡面的域對象時這是一個問題為了解決這個問題你必須為你的所有對象提供一種equals()和hashCode()的實現這種實現能夠保證在它們在對象保存前後正確工作並且當對象在內存中時(返回值)不會改變Hibernate參考文檔提供了以下的建議

  不要使用數據庫標識符來實現等價的判斷而應該使用商業鍵值(business key)一種唯一的通常不改變的屬性的結合體當一個buk不可序列化對象(transient object)被持久化的時候數據庫標識符會發生改變當一個不可序列化實例(常常和detached instances在一起)被包含在一個Set裡面時哈希值的改變會破壞Set的從屬關系商業鍵值的屬性並不要求和數據庫主鍵一樣穩定你只要保證當對象在某個Set中時它們的穩定性

  我們推薦判斷商業鍵值的等價性來實現equals()和hashCode()兩個方法這意味著equals()方法只比較能夠區分現實世界中的實例的商業鍵值(某個候選碼)的屬性(Hibernate 參考文檔 v )

  換句話說equals()和hashCode()使用商業鍵值進行處理而對象使用Hibernate生成的鍵值作為id值這要求對於每個對象有一個相關的不會改變的商業鍵值可是並不是每個對象類型都有這樣的一種鍵這時候你可能會嘗試使用會改變但不時常改變的字段這和商業鍵值不必和數據庫主鍵一樣穩定的思想相吻合當對象在Collection中時候如果這種鍵不改變那它們似乎就足夠好這是一種危險的主張這意味著你的應用程序可能不會崩潰但是前提是沒有人在特定的情況下更新了特定的字段所以應當有一種更好的解決方案而它確實也存在 試圖創建和維護在對象和數據庫行兩者間有著分離的定義的標識符是目前為止討論的所有問題的根源如果我們統一所有標識符的形式這些問題都將不復存在也就時說作為以數據庫為中心和以對象為中心的標識符的替代品我們應該創建一種通用的特定於實體的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字段是否為空來判斷對象是否時新生成的既然我們的對象id永遠不為空這個方法顯然不再有效為了解決這個問題我們可以很容易的配置Hibernate讓它檢查version字段而不是id字段是否為空version字段是一個更為恰當的用來判斷你的對象是否被保存過的指示器

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

  

  <?XML version=?> <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去了解抽象父類相對的我們只要保證每個持久化對象的映射文件包含一個id項(和一個被指派的生成器)和一個帶有unsavedvalue=null屬性的version標簽機敏的讀者可能已經注意到每當一個持久化對象被實例化的時候它的id值得到了指派這意味著當Hibernate在內存中創建一個已經保存過的對象時雖然這個對象是已經存在並從數據庫中讀取的它也會得到一個新的id這不會產生問題因為Hibernate會接著調用對象的setId()方法用保存的真實id來替換新分配的id剩下的id生成器並不是問題因為實現它的算法是輕量級的(也就是說它並不牽扯到數據庫)

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

  ● 鍵值可以不牽扯到數據庫而很輕量級的產生

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

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

  我們需要的是通用唯一標識符(UUID)UUID是由標准格式化的個字節大小的(位)數字組成的UUID的字符串版本是像這樣的
cdbceefdacaec(大家應該可以注意到 Jmatrix目前就是使用的UUID)

  裡面的字符是數字簡單的按字節的進制表示橫線把數字的不同部分分割開來這種格式簡單而且易於處理只是個字符有點兒太長了因為橫線總是被安置在相同的位置所以可以把它們去掉而把字符的數目減少到用一種更為簡潔的表示方法你可以創建一個byte[]的數組或是兩個字節大小的長整型(long)來保存這些數字如果你使用的是Java或更高版本你可以直接使用UUID類雖然這不是它在內存中最簡潔的格式如果你要獲得更多的信息請參閱Wikipedia 的UUID條目 或 Java 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 project 和 Java UUID Generator(JUG) project它們都在Apache的旗下(在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表示數據庫的主鍵大小將會是字節Id也可以直接使用位存儲這樣將減少一半的占用空間但是如果你直接查詢數據庫id將變得難以理解這些方法對你的工程是否可行取決於你的需求 如果你的數據庫不接受UUID作為主鍵你可以考慮使用數據庫序列但總是應該讓新對象創建的時候被指派一個ID而不是讓Hibernate管理你的ID在這種情況下創建新的域對象的商業對象可以調用一個使用data Access object(DAO)從數據庫序列中獲取數據庫id的服務如果你使用一個長整型來表示你的對象id一個單獨的數據庫序列(以及服務方法)對你的域對象來說已經足夠了

  小結

  當對象持久化到數據庫中時對象的標識符總時很難被恰當的實現盡管如此問題其實完全是由存在著在保存之前不持有ID的對象的現象衍生而來的我們可以通過從諸如Hibernate這樣的對象—關系映像框架手中取走指派對象ID的職責來解決這個問題相對的一旦對象被實例化它就應該被指派一個ID這使對象標識符變成簡單而不易出錯也減少了領域模型中需要的代碼量


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