<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Scott@JAVA

    Java, 一杯濃濃的咖啡伴你到深夜

    別讓Hibernate偷走了您的身份

    [轉] http://dev2dev.bea.com.cn/techdoc/20061103895.html

       企業(yè)級Java應用程序常常把數(shù)據(jù)在Java對象和相關數(shù)據(jù)庫之間來回移動。從手工編寫SQL代碼到諸如Hibernate這樣成熟的對象關系映射(ORM)解決方案,有很多種方法可以實現(xiàn)這個過程。無論采用什么樣的技術,一旦開始將Java對象持久存儲到數(shù)據(jù)庫中,身份將成為一個復雜且難以管理的課題??赡艹霈F(xiàn)的情況是:您實例化了兩個不同的對象,而它們卻代表數(shù)據(jù)庫中的同一行。為了解決這個問題,您可能采取的措施是在持久性對象中實現(xiàn)equals()和hashCode(),可是要恰當?shù)貙崿F(xiàn)這兩個方法比乍看之下要有技巧一些。讓問題更糟糕的是,那些傳統(tǒng)的思路(包括Hibernate官方文檔所提倡的)對于新的項目并不一定能提出最實用的解決方案。

       對象身份在虛擬機(VM)中和在數(shù)據(jù)庫中的差異是問題滋生的溫床。在虛擬機中,您并不會得到對象的ID,您只是簡單地持有對象的直接引用。而在幕后,虛擬機確實給每個對象指派了一個8字節(jié)大小的ID,這個ID才是對象的真實引用。當您將對象持久存儲到數(shù)據(jù)庫中的時候,問題開始產(chǎn)生了。假定您創(chuàng)建了一個Person對象并將它存入數(shù)據(jù)庫(我們可以叫它person1)。而您的其他某段代碼從數(shù)據(jù)庫中讀取了這個Person對象的數(shù)據(jù),并將它實例化為另一個新的Person對象(我們可以叫它Person2)。現(xiàn)在您的內(nèi)存中有了兩個映射到數(shù)據(jù)庫中同一行的對象。一個對象引用只能指向它們的其中一個,可是我們需要一種方法來表示這兩個對象實際上表示著同一個實體。這就是(在虛擬機中)引入對象身份的原因。

       在Java語言中,對象身份是由每個對象都持有的equals()方法(以及相關的hashCode()方法)來定義的。無論兩個對象是否為同一個實例,equals()方法都應該能夠判別出它們是否表示同一個實體。hashCode()方法和equals()方法有關聯(lián)是因為所有相等的對象都應該返回相同的hashCode。默認情況下,equals()方法僅僅比較對象引用。一個對象和它自身是相等的,而和其他任何實例都不相等。對于持久性對象來說,重寫這兩個方法,讓代表著數(shù)據(jù)庫中同一行的兩個對象被視為相等是很重要的。而這對于Java中Collection(Set、Map和List)的正確工作更是尤為重要。

       為了闡明實現(xiàn)equal()和hashCode()的不同途徑,讓我們考慮一個準備持久存儲到數(shù)據(jù)庫中的簡單對象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字段保存了在數(shù)據(jù)庫中作為主鍵使用的值,而version字段則是一個從0開始增長的增量,隨著對象的每次更新而變化(這幫助我們避免并發(fā)更新的問題)。為了更清楚一些,讓我們看看允許Hibernate把這個對象持久存儲到數(shù)據(jù)庫的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"
            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字段代表數(shù)據(jù)庫中的ID列(也就是說,它是PERSON表的主鍵)。包含在id標簽中的unsaved-value="null"屬性告訴Hibernate使用id字段來判斷一個Person對象之前是否被保存過。ORM框架必須依靠這個來判斷保存一個對象的時候應該使用SQL的INSERT子句還是UPDATE子句。在這個例子中,Hibernate假定一個新對象的id字段一開始為null值,當它第一次被保存時id才被賦予一個值。generator標簽告訴Hibernate當對象第一次保存時,應該從哪里獲得指派的id。在這個例子中,Hibernate使用數(shù)據(jù)庫序列作為唯一ID的來源。最后,version標簽告訴Hibernate使用Person對象的version字段進行并發(fā)控制。Hibernate將會執(zhí)行樂觀鎖定方案,根據(jù)這個方案,Hibernate在保存對象之前會根據(jù)數(shù)據(jù)庫版本號檢查對象的版本號。

       我們的Person對象還缺少的是equals()方法和hashCode()方法的實現(xiàn)。既然這是一個持久性對象,我們并不想依賴于這兩個方法的默認實現(xiàn),因為默認實現(xiàn)并不能分辨代表數(shù)據(jù)庫中同一行的兩個不同實例。一種簡單而又顯然的實現(xiàn)方法是利用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();
        }
    }
    

      不幸的是,這個實現(xiàn)存在著問題。當我們首次創(chuàng)建Person對象時id的值為null,這意味著任何兩個Person對象只要尚未保存,就將被認為是相等的。如果我們想創(chuàng)建一個Person對象并把它放到一個Set中,再創(chuàng)建一個完全不同的Person對象也把它放到同一個Set里面,事實上第二個Person對象并不能被加入。這是因為Set會斷定所有未保存的對象都是相同的。

       您可能會試圖去實現(xiàn)一個使用id(只在已設置id的情況下)的equals()方法。畢竟,如果兩個對象都沒有被保存過,我們可以假定它們是不同的對象。這是因為在它們被保存到數(shù)據(jù)庫的時候,它們會被賦予不同的主鍵。

    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框架在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次調(diào)用返回false,這是因為Set再也找不到p了。用專業(yè)術語來講,就是Set丟失了這個對象!這是因為當對象在集合中時,我們改變了hashCode()的值。

       當您想要創(chuàng)建一個將其他域?qū)ο蟊4嬖赟et、Map或是List中的域?qū)ο髸r,這是一個問題。為了解決這個問題,您必須為所有對象提供一種equals()和hashCode()的實現(xiàn),這種實現(xiàn)能夠保證在它們在對象保存前后正確工作并且當對象在內(nèi)存中時(返回值)不可變。Hibernate Reference Documentation (v. 3)提供了以下的建議:

       “不要使用數(shù)據(jù)庫標識符來實現(xiàn)相等性判斷,而應該使用業(yè)務鍵(business key),這是一個唯一的、通常不改變的屬性的組合體。當一個瞬態(tài)對象(transient object)被持久化的時候,數(shù)據(jù)庫標識符會發(fā)生改變。當一個瞬態(tài)實例(常常與detached實例一起使用)保存在一個Set中時,哈希碼的改變會破壞Set的約定。業(yè)務鍵的屬性并不要求和數(shù)據(jù)庫主鍵一樣穩(wěn)定,只要保證當對象在同一個Set中時它們的穩(wěn)定性?!保?a >Hibernate Reference Documentation v. 3.1.1)。

       “我們推薦通過判斷業(yè)務鍵相等性來實現(xiàn)equals()和hashCode()。業(yè)務鍵相等性意味著equals()方法只比較能夠區(qū)分現(xiàn)實世界中實例的業(yè)務鍵(普通候選鍵)的屬性?!保?a >Hibernate Reference Documentation v. 3.1.1)。

       換句話說,普通鍵用于equals()和hashCode(),而Hibernate生成的代理項鍵用于對象的id。這要求對于每個對象有一個相關的不可變的業(yè)務鍵??墒?,并不是每個對象類型都有這樣的一種鍵,這時候您可能會嘗試使用會改變但不經(jīng)常改變的字段。這和業(yè)務鍵不必與數(shù)據(jù)庫主鍵一樣穩(wěn)定的思想相吻合。如果這種鍵在對象所在集合的生存期中不改變,那這就“足夠好”了。這是一種危險的觀點,因為這意味著您的應用程序可能不會崩潰,但是前提是沒有人在特定的情況下更新了特定的字段。所以,應當有一種更好的解決方案,這種解決方案確實也存在。

       不要讓Hibernate管理您的id。

       試圖創(chuàng)建和維護對象及數(shù)據(jù)庫行的各自身份定義是目前為止所有討論問題的根源。如果我們統(tǒng)一所有身份形式,這些問題都將不復存在。也就是說,作為以數(shù)據(jù)庫為中心和以對象為中心的ID的替代品,我們應該創(chuàng)建一種通用的、特定于實體的ID來代表數(shù)據(jù)實體,這種ID應該在數(shù)據(jù)第一次輸入的時候創(chuàng)建。無論這個唯一數(shù)據(jù)實體是保存在數(shù)據(jù)庫中,是作為對象駐留在內(nèi)存中,還是存儲在其他格式的介質(zhì)中,這個通用ID都應該可以識別它。通過使用數(shù)據(jù)實體第一次創(chuàng)建時指派的實體ID,我們可以安全地回到equals()和hashCode()的原始定義,它們只需使用這個id:

    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字段是否為null來判斷對象是否為新的。既然對象id永不為null,很顯然這種方法不再有效。通過配置Hibernate,讓它檢查version字段,而不是id字段是否為null, 我們可以很容易地解決這個問題。version字段是一個更恰當?shù)挠脕砼袛鄬ο笫欠癖槐4孢^的指示符。

       下面是我們改進過的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我們不是從數(shù)據(jù)庫指派id值,而是在代碼中指派id值。Hibernate會簡單地認為即使是新的未保存的對象也有id值。我們也給version標簽新增了一個屬性:unsaved-value="null"。這個屬性告訴Hibernate應該把version值而不是id值為null作為對象是新創(chuàng)建而成的指示器。我們也可以簡單地告訴Hibernate把負值作為對象未保存的指示符,如果您喜歡把version字段的類型設置為int而不是Integer,這將是很有用的。

       我們已經(jīng)從轉移到純對象id中獲取了不少好處。我們對equals()和hashCode()方法的實現(xiàn)更加簡單而且更易閱讀。這些方法再也不易出錯而且無論在保存對象之前還是之后,它們都能與Collection一起正常工作。Hibernate也變得更快一些,這是因為在保存新的對象之前它再也不需要從數(shù)據(jù)庫讀取一個序列值。此外,新定義的equals()和hashCode()對于所有包含id對象的對象來說是通用的。這意味著我們可以把這些方法移至一個抽象父類。我們不再需要為每個域?qū)ο笾匦聦崿F(xiàn)equals()和hashCode(),而且我們也不再需要考慮對于每個類來說哪些字段組合是唯一且不變的。我們只要簡單地擴展這個抽象父類。當然,我們沒必要強迫域?qū)ο髲母割愔袛U展出來,所以我們定義了一個接口來保證設計的靈活性。

    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 + "]";
        }
    }
    

      現(xiàn)在我們有了一個簡單而高效的方法來創(chuàng)建域?qū)ο?。它們擴展了AbstractPersistentObject,該父類能在它們第一次創(chuàng)建時自動賦予一個id,并且恰當?shù)貙崿F(xiàn)了equals()和hashCode()。域?qū)ο笠驳玫搅艘粋€toString()方法的合理默認實現(xiàn),這個方法可以有選擇地被重寫。如果這是一個查詢例子的測試對象或者示例對象,id可以被修改或者被設為null。否則它是不應當被改變的。如果因為某些原因我們需要創(chuàng)建一個擴展其他類的域?qū)ο螅@個對象就應當實現(xiàn)PersistentObject接口而不是擴展抽象類。

       Person類現(xiàn)在就簡單多了:

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

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

       到現(xiàn)在為止一切都很好,但是我們遺漏了一個重要的細節(jié):如何實現(xiàn)IdGenerator.createId()。我們可以為理想中的鍵生成(key-generation)算法定義一些標準:

    • 鍵可以不牽扯到數(shù)據(jù)庫而很廉價地生成。
    • 即使跨越不同的虛擬機和不同機器,鍵也要保證唯一性。
    • 如果可能,鍵可以由其他程序、編程語言和數(shù)據(jù)庫生成,但是至少要能與它們兼容。

      我們所需的是通用唯一標識符(universally unique identifier,UUID)。UUID由16個字節(jié)(128位)的數(shù)字組成,遵守標準格式。UUID的String版本看起來類似如下:

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

       里面的字符是簡單的字節(jié)16進制表示,橫線把數(shù)字的不同部分分隔開來。這種格式簡單而且易于處理,只是36個字符有點長了。因為橫線總是被安置在相同的位置,所以可以把它們?nèi)サ?,從而把字符的?shù)目減少到32個。為了更為簡潔地表示,可以創(chuàng)建一個byte[16]的數(shù)組或是兩個8字節(jié)大小的long來保存這些數(shù)字。如果您使用的是Java 1.5或更高版本,可以直接使用UUID類,雖然這不是它在內(nèi)存中最簡潔的格式。有關更多信息,請參閱Wikipedia UUID條目JavaDoc UUID類條目

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

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

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

       這是使用JUG庫實現(xiàn)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中內(nèi)置的UUID生成器算法又如何呢?這是獲得對象身份的UUID的適當途徑嗎?如果您想讓對象身份獨立于對象持久性,這就不是一個好方法。雖然Hibernate確實提供了生成UUID的選項,但這樣的話我們又回到了最早的那個問題上:對象ID的獲得并不在它們被創(chuàng)建的時候,而是在它們被保存的時候。

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

       如果數(shù)據(jù)庫不接受UUID作為主鍵,您可以考慮使用數(shù)據(jù)庫序列。但總是應該在新對象創(chuàng)建的時候被指派一個ID而不是讓Hibernate管理ID。在這種情況下,創(chuàng)建新域?qū)ο蟮臉I(yè)務對象可以調(diào)用一個使用數(shù)據(jù)訪問對象(DAO)從數(shù)據(jù)庫序列中檢索id的服務。如果使用一個Long數(shù)據(jù)類型來表示對象id,一個單獨的數(shù)據(jù)庫序列(以及服務方法)對您的域?qū)ο髞碚f就已經(jīng)足夠了。

    結束語

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

    posted on 2006-12-01 18:53 Scott@JAVA 閱讀(307) 評論(0)  編輯  收藏 所屬分類: Java EE 5


    只有注冊用戶登錄后才能發(fā)表評論。


    網(wǎng)站導航:
     
    主站蜘蛛池模板: 国产精品免费观看久久| 日韩在线永久免费播放| 久久电影网午夜鲁丝片免费| 亚洲丝袜美腿视频| 免费国产黄网站在线观看视频| 亚洲∧v久久久无码精品 | 日韩大片在线永久免费观看网站| 最近最新的免费中文字幕| 亚洲人成电影网站久久| 毛片免费视频播放| 亚洲无人区码一二三码区别图片| 最近中文字幕mv免费高清电影| 亚洲国产视频久久| 无码国模国产在线观看免费| 人妻无码中文字幕免费视频蜜桃| 亚洲一级特黄大片在线观看| 国产色爽免费无码视频| 亚洲人成亚洲精品| 99视频在线精品免费观看6| 色综合久久精品亚洲国产| 亚洲国产精品一区二区九九| 成全视成人免费观看在线看| 亚洲色欲或者高潮影院| 女人18毛片特级一级免费视频| 香蕉视频在线观看免费| 国产成人精品日本亚洲| 亚色九九九全国免费视频| 亚洲成AV人影片在线观看| 黑人大战亚洲人精品一区| 性xxxx视频免费播放直播| 亚洲精品456人成在线| 亚洲日韩VA无码中文字幕| 精品无码无人网站免费视频| 午夜在线a亚洲v天堂网2019| 亚洲精品无码av天堂| 免费A级毛片无码A∨中文字幕下载 | 亚洲无线电影官网| 暖暖日本免费在线视频| 国产成人免费ā片在线观看老同学| 亚洲免费在线视频观看| 亚洲第一网站男人都懂|