對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);
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);
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,一個實作的例子如下:
由於主鍵值是資料庫中的資料唯一識別方式,因此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();
}
}
....
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;
}
}
...
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();
}
}