6.映射基础议题
一边是对象,一边是数据表格,两者在映像时有一些过渡的基础议题必须了解。
6.1实体对象生命周期
Hibernate中的实体对象可以分为三种状态:Transient、Persistent、Detached。
- Transient
当您直接使用new创建出对象,例如在之前的例子中,User类别所衍生出之对象,在还没有使用save()之前都是暂存对象,这些对象还没有与数据库发生任何的关系,不对应于数据库中的任一笔数据。
- Persistent
当对象与数据库中的数据有对应关系,并且与Session实例有关联而Session 实例尚未关闭(close),则它是在Persistent状态,具体而言,如果您将Transient状态的对象使用Session的save()方法加以储存,或是使用Hibernate从数据库加载数据并封装为对象(例如使用get()、load()),则该对象为Persistent状态。
Persistent状态的对象对应于数据库中的一笔数据,对象的id值与数据的主键值相同,并且Session实例尚未失效,在这期间您对对象的任何状态变动,在Session实例关闭(close)或Transaction实例执行commit()之后,数据库中对应的数据也会跟着更新。
如果您将Session实例关闭(close),则Persistent状态的对象会成为Detached状态。
如果您使用Session的实例delete()方法删除数据,Persistent状态的对象由于失去了对应的数据,则它会成为Transient状态。
- Detached
Detached状态的对象,其id与数据库的主键值对应,但脱离Session实例的管理,例如在使用load()方法查询到数据并封装为对象之后,将Session实例关闭,则对象由Persistent状态变为Detached状态,Detached状态的对象之任何属性变动,不会对数据库中的数据造成任何的影响。Detached状态的对象可以使用update()方法使之与数据库中的对应数据再度发生关联,此时Detached状态的对象会变为Persistent状态。
简单的说,Transient与Detached状态的对象未受Hibernate持久层管理员管理,对这两个状态的对象作任何属性变动,不会对数据库中的数据有任何的影响,而Persistent状态的对象受Hibernate持久层管理,对对象的属性变动,在Session实例关闭(close)或 Transaction实例执行commit()之后,数据库中对应的数据也会跟着更新。
Transient与Detached状态的对象是非管理状态,而Persistent状态的对象是管理状态,又称为Persistent Object。
在对象为Persistent时,如果对象的属性发生变化,并且尚未提交之前,对象所携带的数据称之为Dirty Data,Hibernate会在持久层维护对象的最近读取版本,并在数据提交时检查两个版本的属性是否有变化,如果有的话,则将数据库中的数据进行更新。
6.2数据识别(Data Identity)
讨论一下对象识别问题,对数据库而言,其识别一笔数据唯一性的方式是根据主键值,如果手上有两份数据,它们拥有同样的主键值,则它们在数据库中代表同一个字段的数据。对Java而言,要识别两个对象是否为同一个对象有两种方式,一种是根据对象是否拥有同样的内存位置来决定,在Java语法中就是透过== 运算来比较,一种是根据equals()、hasCode()中的定义。
先探讨第一种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 obj1 = session2.load(User.class, new Integer(1));
session2.close();
System.out.println(obj1 == obj2);
原因可以参考 简介快取(Session Level) 。
Hibernate并不保证不同时间所取得的数据对象,其是否参考至内存的同一位置,使用==来比较两个对象的数据是否代表数据库中的同一笔数据是不可行的,事实上就算在Java程序中也不会这么比较两个对象是否相同,要比较两个对象是否相同,会透过equals()方法,而Object预设的 equals()本身即比较对象的内存参考,如果您要有必要比较透过查询后两个对象的数据是否相同(例如当对象被储存至Set时)您必须实作 equals()与hashCode()。
一个实作equals()与hashCode()的方法是根据数据库的identity,方法之一是透过getId()方法取得对象的id值并加以比较,例如若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 hasCode() {
return id == null ? System.identityHashCode(this) : id.hashcode();
}
}
这个例子取自于Hibernate in Action第123页的范例,然而这是个不被鼓励的例子,因为当一个对象被new出来而还没有save()时,它并不会被赋予id值,这时候就不适用这个方法。
另一个比较被采用的方法是根据对象中真正包括的的属性值来作比较,在 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;
}
}
这个例子不是简单的比较id属性,而是一个根据商务键值(Business key)实作equals()与hasCode()的例子,当然留下的问题就是您如何在实作时利用相关的商务键值,这就要根据您实际的商务需求来决定了。
愿意的话,还可以使用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();
}
}
二.物件关联映像(Object/Relational Mapping, ORM)
学习 Hibernate,大部份的时间都在了解如何实现映射,而从中您也可以了解到不少关系型数据库的表格设计方式。
7.实体映像
来看看一些进阶的实体映像议题。
7.1复合主键(一)
基于业务需求,您会需要使用两个字段来作复合主键,例如在User数据表中,您也许会使用"name"与"phone"两个字段来定义复合主键。
假设您这么建立User表格:
CREATE TABLE user (
name VARCHAR(100) NOT NULL,
phone VARCHAR(50) NOT NULL,
age INT,
PRIMARY KEY(name, phone)
);
在表格中,"name"与"age"被定义为复合主键,在映像时,您可以让User类别直接带有"name"与"age"这两个属性,而Hibernate要求复合主键类别要实作Serializable接口,并定义equals()与hashCode()方法:
User.java
package onlyfun.caterpillar;
import java.io.Serializable;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
// 复合主键类的对应类别必须实作Serializable接口
public class User implements Serializable {
private String name;
private String phone;
private Integer age;
public User() {
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
// 必须重新定义equals()与hashCode()
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();
}
}
equals()与hashCode()方法被用作两笔不同数据的识别依据;接着您可以使用<composite-id>在映射文件中定义复合主键与对象的属性对应:
User.hbm.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="user">
<composite-id>
<key-property name="name"
column="name"
type="java.lang.String"/>
<key-property name="phone"
column="phone"
type="java.lang.String"/>
</composite-id>
<property name="age" column="age" type="java.lang.Integer"/>
</class>
</hibernate-mapping>
在储存数据方面,复合主键的储存没什么区别,现在的问题在于如何依据复合主键来查询数据,例如使用load()方法,您可以创建一个User实例,并设定复合主键对应的属性,接着再透过load()查询对应的数据,例如:
User user = new User();
user.setName("bush");
user.setPhone("0970123456");
Session session = sessionFactory.openSession();
// 以实例设定复合主键并加载对应的数据
user = (User) session.load(User.class, user);
System.out.println(user.getAge() + "/t" +
user.getName() + "/t" +
user.getPhone());
session.close();
7.2复合主键(二)
可以将主键的信息独立为一个类别,例如:
UserPK.java
package onlyfun.caterpillar;
import java.io.Serializable;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
public class UserPK implements Serializable {
private String name;
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public boolean equals(Object obj) {
if(obj == this) {
return true;
}
if(!(obj instanceof User)) {
return false;
}
UserPK pk = (UserPK) obj;
return new EqualsBuilder()
.append(this.name, pk.getName())
.append(this.phone, pk.getPhone())
.isEquals();
}
public int hashCode() {
return new HashCodeBuilder()
.append(this.name)
.append(this.phone)
.toHashCode();
}
}
现在User类别的主键信息被分离出来了,例如:
User.java
package onlyfun.caterpillar;
import java.io.Serializable;
public class User implements Serializable {
private UserPK userPK; // 主键
private Integer age;
public User() {
}
public UserPK getUserPK() {
return userPK;
}
public void setUserPK(UserPK userPK) {
this.userPK = userPK;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
在映像文件方面,需要指定主键类的信息,例如:
User.hbm.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="user">
<composite-id name="userPK"
class="onlyfun.caterpillar.UserPK"
unsaved-value="any">
<key-property name="name"
column="name"
type="java.lang.String"/>
<key-property name="phone"
column="phone"
type="java.lang.String"/>
</composite-id>
<property name="age" column="age" type="java.lang.Integer"/>
</class>
</hibernate-mapping>
在查询数据时,必须指定主键信息,例如:
UserPK pk = new UserPK();
pk.setName("bush");
pk.setPhone("0970123456");
Session session = sessionFactory.openSession();
// 以主键类实例设定复合主键并加载对应的数据
User user = (User) session.load(User.class, pk);
System.out.println(user.getAge() + "/t" +
user.getUserPK().getName() + "/t" +
user.getUserPK().getPhone());
session.close();
7.3 Blob、Clob
在Hibernate中,您可以直接对Blob、Clob作映像,例如在MySQL中,您的数据库表格若是这么建立的:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
age INT,
photo BLOB,
resume TEXT
);
您可以定义一个User类别,并让属性包括java.sql.Blob与java.sql.Clob,如下:
User.java
package onlyfun.caterpillar;
import java.sql.Blob;
import java.sql.Clob;
public class User {
private Integer id;
private String name;
private Integer age;
private Blob photo;
private Clob resume;
// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Blob getPhoto() {
return photo;
}
public void setPhoto(Blob photo) {
this.photo = photo;
}
public Clob getResume() {
return resume;
}
public void setResume(Clob resume) {
this.resume = resume;
}
}
接着在映射文件中,可以如下定义:
User.hbm.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="user">
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"/>
</id>
<property name="name" column="name" type="java.lang.String"/>
<property name="age" column="age" type="java.lang.Integer"/>
<property name="photo" column="photo" type="java.sql.Blob"/>
<property name="resume" column="resume" type="java.sql.Clob"/>
</class>
</hibernate-mapping>
在进行数据储存时,可以使用Hibernate.createBlob()与Hibernate.createClob()从来源数据建立Blob与Clob实例,例如:
FileInputStream fileInputStream = new FileInputStream("c://workspace//photo.jpg");
Blob photo = Hibernate.createBlob(fileInputStream);
Clob resume = Hibernate.createClob("Bla....Bla....resume text!!");
User user = new User();
user.setName("caterpillar");
user.setAge(new Integer(30));
user.setPhoto(photo);
user.setResume(resume);
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();
session.close();
如果打算从数据库中取得数据,则一个范例如下所示:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.print(user.getAge() + "/t" +
user.getName() + "/t");
String resume = user.getResume().getSubString(1, (int) user.getResume().length());
System.out.println(resume);
// 将Blob数据写到档案
InputStream inputStream = user.getPhoto().getBinaryStream();
FileOutputStream fileOutputStream = new FileOutputStream("c://workspace//photo_save.jpg");
byte[] buf = new byte[1];
int len = 0;
while((len = inputStream.read(buf)) != -1) {
fileOutputStream.write(buf, 0, len);
}
inputStream.close();
fileOutputStream.close();
System.out.println("save photo to c://workspace//photo_save.jpg");
session.close();
在MySQL中对Blob与Clob是比较简单的,如果在Oracle DB中则复杂一些,您可以参考 Using Clobs/Blobs with Oracle and Hibernate。
7.4 Component
假设您设计了这么一个user表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
age INT,
email VARCHAR(100) NOT NULL
);
最基本的映像策略中,您可以设计一个如下的User类别与之对应:
package onlyfun.caterpillar;
public class User {
private Integer id;
private String name;
private int age;
private String email;
........
}
现在假设您基于业务上的设计需求,您需要将email信息提升为一个MailAddress对象,让它携带更多信息或负有特定职责,例如:
MailAddress.java
package onlyfun.caterpillar;
public class MailAddress {
private String email;
public MailAddress() {
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public void sendMail() {
System.out.println("Send mail to " + email);
}
}
而User类别中有(has a)MailAddress,例如:
User.java
package onlyfun.caterpillar;
public class User {
private Integer id;
private String name;
private Integer age;
private MailAddress mailAddress;
// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public MailAddress getMailAddress() {
return mailAddress;
}
public void setMailAddress(MailAddress mailAddress) {
this.mailAddress = mailAddress;
}
}
在数据库表格方面并没有任何的改变,这是基于程序设计上的考虑,增加对象设计上的粒度,MailAddress为User的一个Component,在映射文件上,您可以使用<component>标签来完成这样的映像:
User.hbm.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="user">
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"/>
</id>
<property name="name" column="name" type="java.lang.String"/>
<property name="age" column="age" type="java.lang.Integer"/>
<component name="mailAddress" class="onlyfun.caterpillar.MailAddress">
<property name="email"
column="email"
type="java.lang.String"
not-null="true"/>
</component>
</class>
</hibernate-mapping>
在对象储存时的一个示范如下:
MailAddress mailAddress = new MailAddress();
mailAddress.setEmail("caterpillar.onlyfun@gmail.com");
User user = new User();
user.setName("caterpillar");
user.setAge(new Integer(30));
user.setMailAddress(mailAddress);
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
session.flush();
tx.commit();
session.close();
在对象查询与使用上的一个例子如下:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getAge() + "/t" +
user.getName() + "/t" +
user.getMailAddress().getEmail());
user.getMailAddress().sendMail();
session.close();
7.5动态模型(Dynamic Model)
在构造系统原型(Prototype)阶段时,由于需求尚未确定,应用程序模型中的Java对象会有相当大的变动,在Hibernate 3中引入了动态模式,可以使用对象容器充当Java对象,在构造系统原型时灵活变化,而不必实际定义Java对象。
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
age INT
);
使用动态模式来作映像时,无需定义Java对象,直接在映像文件的<class>卷标上使用entity-name属性:
User.hbm.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class entity-name="onlyfun.caterpillar.DynamicUserModel"
table="user">
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"/>
</id>
<property name="name"
column="name"
type="java.lang.String"/>
<property name="age"
column="age"
type="java.lang.Integer"/>
</class>
</hibernate-mapping>
entity-name属性设定的名称将在储存或加载时使用,例如可以如下储存数据:
Map userMap = new HashMap();
userMap.put("name", "caterpillar");
userMap.put("age", new Integer(30));
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save("onlyfun.caterpillar.DynamicUserModel", userMap);
tx.commit();
session.close();
Map容器的key用来表示属性名称,而value用来表示储存之对象,它们将对应至数据表中的字段与值,上面的程序片段储存数据后,数据表内容如下:
mysql> select * from user;
+----+-------------+------+
| id | name | age |
+----+-------------+------+
| 1 | caterpillar | 30 |
+----+-------------+------+
1 row in set (0.00 sec)
如果要加载数据,范例如下所示:
Session session = sessionFactory.openSession();
Map userMap = (Map) session.load("onlyfun.caterpillar.DynamicUserModel", new Integer(1));
System.out.println(userMap.get("name"));
System.out.println(userMap.get("age"));
session.close();
Hibernate 3引入动态模型的目的,在于更灵活的构造原型系统,在系统架构与对象模式确定之后,仍是要设计专用的Java对象,以获得编译时期的型态检查等好处。