資料識別(Data Identity)


對Java而言,要識別兩個物件是否為同一個物件有兩種方式,一種是根據物件是否擁有同樣的記憶體位置來決定,在Java語法中就是透過== 運算來比較,這是Java所定義的物件識別(Object identity),一種是根據equals()、hasCode()中的定義,這是Java所定義的物件相等(Object equality)

  • 物件識別
先探討第一種Java的識別方式在Hibernate中該注意的地方,在Hibernate中,如果是在同一個session中根據相同查詢所得到的相同 資料,則它們會擁有相同的Java識別,舉個實際的例子來說明:
Session session = sessions.openSession();
Object obj1 = session.load(User.class, new Integer(1));
Object obj2 = session.load(User.class, new Integer(1));
session.close();

System.out.println(obj1 == obj2);

上面這個程式片段將會顯示true的結果,表示obj1與obj2是參考至同一物件,但如果是以下的情況則會顯示false:
Session session1 = sessions.openSession();
Object obj1 = session1.load(User.class, new new Integer(1));
session1.close();

Session session2 = sessions.openSession();
Object obj2 = session2.load(User.class, new Integer(1));
session2.close();

System.out.println(obj1 == obj2);

原因可以參考 簡介快取(Session Level)

應用程式中基於效能的原因,不會在一個使用者的長時間操作會話階段,持續開始Session,將物件維持在 Persistence狀態,Hibernate並不保證不同時間所取得的資料物件,其是否參考至記憶體的同一位置,使用==來比較兩個物件的資料是否代 表資料庫中的同一筆資料是不可 行的。

  • 物件相等
再來討論物件相等的問題,在Java程式中要比較兩個物件是否相同,會透過equals()方法,而Object預設的 equals()本身是比較物件的記憶體參考,如果您要比較兩個物件的資料內容是否相同,您必須實作 equals()與hashCode(),最簡單的實作方式,是在equals()中,對物件的每個屬性逐一加以比較是否相同,稱之為by value equality

  • 資料識別
討論一下Hibernate中資料識別問題,對資料庫而言,其識別一筆資料唯一性的方式是根據主鍵值,如果手上有兩份資料,它們擁有同樣的主鍵值,則它們在資料庫中代表同一個 欄位的資料

由於主鍵值是資料庫中的資料唯一識別方式,因此Hibernate中的資料物件是否對應於一筆欄位資料,就是根據與主鍵值對應的物件識別屬 性(identifier property),Hibernate會維護物件的識別屬性,必要時,您可以將識別屬性的setter方法設定為private,以避免程式中遭到 修改,您可以藉由Session的getIdentifier()方法取得物件的識別屬性值。

如果要結合equals()、hashCode()來實作物件相等,一個根據資料庫的識別屬性的實作方式,是透過識別屬性的getter方法取得物件的識別屬性值並加以比較, 例如若id的型態是String,一個實作的例子如下:
public class User {
    ....

    public boolean equals(Object o) {
        if(this == o) return true;
        if(id == null || !(o instanceof User)) return false;

        final User user == (User) o;
        return this.id.equals(user.getId());
    }

    public int hashCode() {
        return id == null ? System.identityHashCode(this) : id.hashcode();
    }
}

這個例子取自於Hibernate in Action第123頁的範例,稱之為database identity equality,然而要注意的是,因為當一個物件被new出來而還沒有save()時,它並不會被賦予id值,如果您在物件儲存前,可能就有需求比較物件的相等性,就不適用這 個方法,例如物件若會被加入Set物件之中,該物件在被儲存至資料庫前與後,在Set中的判斷將有所不同,導致明明是同一個物件,卻使得程式出現不同的行為。

較好的方式是根據商務鍵值(Business key)來實作equals()與hashCode(),稱之為business key equality,在 Hibernate 官方參考手冊 中給了一個例子:
public class Cat {

    ...
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof Cat)) return false;

        final Cat cat = (Cat) other;

        if (!getName().equals(cat.getName())) return false;
        if (!getBirthday().equals(cat.getBirthday())) return false;

        return true;
    }

    public int hashCode() {
        int result;
        result = getName().hashCode();
        result = 29 * result + getBirthday().hashCode();
        return result;
    }

}

與by value equality的實作方式類似,但根據性的不同是不再比對所有的屬性,而是只比較商務鍵,商 務鍵是一個屬性或多個屬性的結合,對每個具有相同資料庫識別的物件來說,商務鍵的組合也是唯一的,商務鍵的挑選可以找那些從不為null、 immutable或很少改變且具有唯一性的屬性(例如對應欄位中UNIQUE的屬性),選用識別屬性作為商務屬性之一也是一種選擇。

願意的話,還可以使用org.apache.commons.lang.builder.EqualsBuilder與 org.apache.commons.lang.builder.HashCodeBuilder來協助定義equals()與hashCode(), 例如:
package onlyfun.caterpillar;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

public class User {
     ....
    public boolean equals(Object obj) {
        if(obj == this) {
            return true;
        }
       
        if(!(obj instanceof User)) {
            return false;
        }
       
        User user = (User) obj;
        return new EqualsBuilder()
                 .append(this.name, user.getName())
                 .append(this.phone, user.getPhone())
                 .isEquals();
       
    }
   
    public int hashCode() {
        return new HashCodeBuilder()
                 .append(this.name)
                 .append(this.phone)
                 .toHashCode();
    }
}