Hibernate集合映射
<set>,<list>,<map>,<bag>,<array>,<primitive-array>
集合映射-Set
先介绍当Set中包括的对象为非实体(Entity)时的映射方式,简单的说,也就是所包括的对象没有对象识别(Identity),只是纯粹的值型态(Value type)对象)。
假设您有一个User类别,当中除了名称属性之外,另一个就是使用者的电子邮件地址,同一个使用者可能有多个不同的邮件地址,所以在User类别中使用 Set对象来加以记录,在这边使用String来记录每一笔邮件地址,为了不允许重复的邮件地址记录,所以使用Set对象,User类别如下:
public class User {
private Integer id;
private String name;
private Set emails;
// 必须要有一个预设的建构方法
// 以使得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 Set getEmails() {
return emails;
}
public void setEmails(Set emails) {
this.emails = emails;
}
public void addEmail(String email) {
this.emails.add(email);
}
public void removeEmail(String email) {
this.emails.remove(email);
}
}
要映像Set容器,您可以使用另一个表格来储存Set容器中的数据,例如您可以分别建立user与email表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);
CREATE TABLE email (
id INT(11) NOT NULL,
address VARCHAR(100) NOT NULL
);
接着定义映像文件,使用<set>标签来定义Set映像:
User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.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"/>
<set name="emails" table="email">
<key column="id"/>
<element type="java.lang.String"
column="address"/>
</set>
</class>
</hibernate-mapping>
假设您如下储存对象:
User user1 = new User();
user1.setEmails(new HashSet());
user1.setName(“hhp");
user1.addEmail(“hhp@gmail.com");
user1.addEmail(“hhp@yahoo.com");
User user2 = new User();
user2.setEmails(new HashSet());
user2.setName(“tiantian");
user2.addEmail(“tian@gmail.com");
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
集合映射-List
List是有序的结构,所以在储存List容器中的对象时,要一并储存其顺序信息,例如若您设计了以下的类别:
User.java
public class User {
private Integer id;
private String name;
private List items;
// 必须要有一个预设的建构方法
// 以使得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 List getItems() {
return items;
}
public void setItems(List items) {
this.items = items;
}
public void addItem(String item) {
items.add(item);
}
public void removeItem(String item) {
items.remove(item);
}
}
在设计表格时,使用一个item表格来记录List容器信息,item表格必须包括索引信息,例如您可以如下建立user与item表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);
CREATE TABLE item (
id INT(11) NOT NULL,
position INT(11) NOT NULL,
name VARCHAR(100) NOT NULL default ''
);
其中position字段要用来储存List的索引信息,可以使用<list>标签如下定义映像文件:
User.hbm.xml
<hibernate-mapping>
<class name=“hhp.com.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"/>
<list name="items" table="item">
<key column="id"/>
<index column="position"/>
<element type="java.lang.String" column="name"/>
</list>
</class>
</hibernate-mapping>
假设您如下储存对象:
User user1 = new User();
user1.setItems(new ArrayList());
user1.setName(“hhp");
user1.addItem("DC");
user1.addItem("CF Card");
User user2 = new User();
user2.setItems(new ArrayList());
user2.setName(“tiantian");
user2.addItem("comics");
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
集合映射-Map
Map的特性是key/value对,容器中的每一个对象都有一个key与之对应,所以将Map容器的数据储存至数据库时,必须一同储存它的key信息。
假设您设计了以下的类别: User.java
import java.util.Map;
public class User {
private Integer id;
private String name;
private Map items;
// 必须要有一个预设的建构方法
// 以使得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 Map getItems() {
return items;
}
public void setItems(Map items) {
this.items = items;
}
public void addItem(String name, String description) {
items.put(name, description);
}
public void removeItem(String name) {
items.remove(name);
}
}
您可以建立以下的表格来分别储存user信息与当中的items信息:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);
CREATE TABLE item (
id INT(11) NOT NULL,
name VARCHAR(100) NOT NULL default '',
description VARCHAR(100) NOT NULL default ''
);
在映像文件方面,使用<map>标签来定义Map的映像,如下:User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.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"/>
<map name="items" table="item">
<key column="id"/>
<index column="name" type="java.lang.String"/>
<element column="description" type="java.lang.String"/>
</map>
</class>
</hibernate-mapping>
假设您使用以下的程序片段来储存User实例:
User user1 = new User();
user1.setItems(new HashMap());
user1.setName(“hhp");
user1.addItem("Book", "Java Gossip");
user1.addItem("DC", "Caxxx A80");
User user2 = new User();
user2.setItems(new HashMap());
user2.setName(“zhangy");
user2.addItem("Doll", "Snoppy world");
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user2);
tx.commit();
session.close();
集合映射-Bag
Bag是集合,与Set不同的是,Bag允许重复的元素,在Java的标准API中并没有提供Bag容器,Hibernate提供自己的Bag实现,允许您将List映射为Bag。
您可以如下定义User类别,其中的List成员将被用作Bag来使用,而不管对象在List容器中的顺序: User.java
import java.util.List;
public class User {
private Integer id;
private String name;
private List items;
// 必须要有一个预设的建构方法
// 以使得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 List getItems() {
return items;
}
public void setItems(List items) {
this.items = items;
}
public void addItem(String item) {
items.add(item);
}
public void removeItem(String name) {
items.remove(name);
}
}
最简单的Bag映像是使用<bag>标签,在这之前,假设您如下建立表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);
CREATE TABLE item (
id INT(11) NOT NULL,
name VARCHAR(100) NOT NULL
);
接着定义映射文件,如下所示:User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.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"/>
<bag name="items" table="item">
<key column="id"/>
<element column="name" type="java.lang.String"/>
</bag>
</class>
</hibernate-mapping>
假设您如下储存对象:
User user1 = new User();
user1.setItems(new ArrayList());
user1.setName(“hhp");
user1.addItem("Java Gossip");
user1.addItem("Java Gossip");
user1.addItem("Caxxx A80");
User user2 = new User();
user2.setItems(new ArrayList());
user2.setName(“zhangy");
user2.addItem("Snoppy world");
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
您可以如下更新数据:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
User user = (User) session.load(User.class, new Integer(1));
user.removeItem("Java Gossip");
tx.commit();
session.close();
然而注意观察在更新数据时所使用的SQL:
Hibernate: delete from item where id=?
Hibernate: insert into item (id, name) values (?, ?)
Hibernate: insert into item (id, name) values (?, ?)
由于Bag的数据允许重复,当必须更新数据时,无法确定要更新的是哪一笔数据,因而采取的方式是删除集合对象对应的所有数据,然后重新将集合对象中的数据写入数据库,显然的这种作法相当的没有效率。
作为Bag的一种扩充,Hibernate提供idbag,藉由在定义Bag映像时加上"collection-id",让Hibernate可以直接确定所要更新的数据,提高数据库操作的效率,
您可以先如下建立表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);
CREATE TABLE item (
cid CHAR(32) NOT NULL,
id INT(11) NOT NULL,
name VARCHAR(100) NOT NULL
);
其中item表格的cid就用于数据更新时定位之用,接着在映像文件中使用<idbag>标签加以定义:User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.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"/>
<idbag name="items" table="item">
<collection-id column="cid" type="java.lang.String">
<generator class="uuid.hex"/>
</collection-id>
<key column="id"/>
<element column="name" type="java.lang.String"/>
</idbag>
</class>
</hibernate-mapping>
如果使用上面提到过的程序片段来更新对象的话,则实际上Hibernate会使用以下的SQL来进行更新:
Hibernate: delete from item where cid=?
这一次并不是整个删除集合中的数据,而是直接藉由cid来确定所要更新的数据,比起只使用Bag,idbag的效率好了许多
假设您建立了以下的表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);
CREATE TABLE email (
id INT(11) NOT NULL,
address VARCHAR(100) NOT NULL
);
一个user可以有多个email,但不可重复,这可以使用 Set 来映像,在对应的对象方法,您可以如下设计对象:
package onlyfun.caterpillar;
import java.util.Set;
public class User {
private Integer id;
private Set emails;
....
}
假设您原先预定在Set中储存的是String型态,而后设计时考虑独立设计一个MailAddress类别,而Set中将储存MailAddress的实例,例如:
User.java
import java.util.Set;
public class User {
private Integer id;
private String name;
private Set emails;
// 必须要有一个预设的建构方法
// 以使得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 Set getEmails() {
return emails;
}
public void setEmails(Set emails) {
this.emails = emails;
}
public void addEmail(MailAddress mailAddress) {
this.emails.add(mailAddress);
}
public void removeEmail(MailAddress mailAddress) {
this.emails.remove(mailAddress);
}
}
MailAddress.java 类
public class MailAddress {
private String address;
public MailAddress() {}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void sendMail() {
System.out.println("Send mail to " + address);
}
}
在映射文件方面,可以使用<composite-element>来为MailAddress作映射,如下:
User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.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"/>
<set name="emails" table="email">
<key column="id"/>
<composite-element class=“com.cstp.MailAddress">
<property name="address" column="address"/>
</composite-element>
</set>
</class>
</hibernate-mapping>
您可以如下储存对象:
User user = new User();
user.setName(“hhp");
user.setEmails(new HashSet());
MailAddress mailAddress = new MailAddress();
mailAddress.setAddress(“hhp@gmail.com");
user.addEmail(mailAddress);
mailAddress = new MailAddress();
mailAddress.setAddress(“hhp@yahoo.com");
user.addEmail(mailAddress);
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();
session.close();
在查询时,address表格的数据会封装为MailAddress的实例,一个范例如下:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
MailAddress mailAddress = (MailAddress) iterator.next();
mailAddress.sendMail();
}
session.close();
从数据库的观点来看,Set、Map、Bag是无序的,而List是有序的,这边所谓的无序或有序,是指将容器中对象储存至数据库时,是否依容器对象中的顺序来储存。
然而从数据库取得数据之后,您也许会希望Set、Map等容器中的对象可以依一定的顺序来排列,您可以从两个层次来容器中的对象排序,
一是在加载数据后于 JVM中排序,
另一是在数据库中直接使用order by子句来排序。
以 Set 这篇文章中的范例来作说明,要在JVM中就数据进行排序,您可以在映像文件中使用sort属性来定义容器的排序,这适用于Set与Map,例如:
User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.User" table="user">
....
<set name="emails" table="email" sort="natural">
<key column="id"/>
<element type="java.lang.String"
column="address"/>
</set>
</class>
</hibernate-mapping>
sort=“natural”表示使用对象的comparaTo()方法来进行排序,容器中的对象上必须有实例java.lang.Comparable 接口,例如String就有实例java.lang.Comparable接口,结果会使用字典顺序来排列容器中的对象。
您可以实现自己的排序方式,只要定义一个类别来实例java.util.Comparator接口,例如:
CustomComparator.java
import java.util.Comparator;
public class CustomComparator implements Comparator {
public int compare(Object o1, Object o2) {
if (((String) o1).equals(o2))
return 0;
return ((Comparable) o1).compareTo(o2) * -1;
}
}
在自定义的Comparator中,如果两个对象的顺序相同会传回0,而为了方便比较对象,要求传入的对象必须实例Comparable接口(例如 String对象就有实例Comparable接口),范例中只是简单的将原来compareTo()传回的值乘以负一,如此就可以简单的让排列顺序相反,接着可以在映射文件中指定自定的Comparator类别:
User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.User" table="user">
....
<set name="emails" table="email"
sort=“com.cstp.CustomComparator">
<key column="id"/>
<element type="java.lang.String"
column="address"/>
</set>
</class>
</hibernate-mapping>
注意:Bag与List并不适用于这种方式。
另一个排序的方式则是在数据库中进行,直接使用order by子句来排序,这可以在映像文件中使用order-by属性来指定,例如:
User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.User" table="user">
....
<set name="emails" table="email" order-by="address desc">
<key column="id"/>
<element type="java.lang.String"
column="address"/>
</set>
</class>
</hibernate-mapping>
观察Hibernate所使用的SQL可以看到order by子句:
Hibernate: select emails0_.id as id0_, emails0_.address as address0_ from email emails0_ where emails0_.id=? order by emails0_.address desc
有时候您只是想要获得对象中某个属性的数据,如果您的对象中包括Set等容器对象,若从数据库中加载数据时全部加载所有的对象,却只是为了取得某个属性,显然的这样很没有效率。
以Set 中的范例来说,如果您只是想取得对象之后,显示对象的某些属性,例如name属性:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());
session.close();
在这个例子中,email的信息不必要从数据库中全部加载,在Hibernate中支持容器的延迟初始(Lazy onitialization),只有在真正需要容器对象中的数据时,才从数据库中取得数据,预设容器类会使用延迟加载的功能,例如上面的程序实际上会使用以下的SQL:
Hibernate: select user0_.id as id0_, user0_.name as name0_0_ from user user0_ where user0_.id=?
可以藉由映像文件中的lazy属性来设定是否使用延迟初始,例如在映射文件中如下设定:
User.hbm.xml
<hibernate-mapping>
<class name=“com.cstp.User" table="user">
....
<set name="emails" table="email" lazy="false">
<key column="id"/>
<element type="java.lang.String"
column="address"/>
</set>
</class>
</hibernate-mapping>
由于lazy属性被设定为false,延迟初始的功能被关闭,所以上面的程序会使用以下的SQL来查询:
Hibernate: select user0_.id as id0_, user0_.name as name0_0_ from user user0_ where user0_.id=?
Hibernate: select emails0_.id as id0_, emails0_.address as address0_ from email emails0_ where emails0_.id=?
所有的容器对象之数据一并被查询了,即使程序中还不会使用到容器中的对象信息。
在启用延迟初始的情况下,如果如下查询数据:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());
Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
session.close();
可以看到,只有在需要查询容器中对象时,才会向数据库索取数据。
使用延迟初始时,由于在需要数据时会向数据库进行查询,所以session不能关闭,如果关闭会丢出 LazyInitializationException 例外,例如下面的程序就会丢出例外:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());
session.close();
Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
如果您使用了延迟初始,而在某些时候仍有需要在session关闭之后取得相关对象,则可以使用Hibernate.initialize()来先行加载相关对象,例如:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());
Hibernate.initialize(user); // 先加载容器中的对象
session.close();
Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
即使启用延迟初始,在Hibernate.initialize()该行,email容器中的对象已经被加载,所以即使关闭session也无所谓了,这种情况发生在某个情况下,您启用了延迟初始,而使用者操作过程中,会在稍后一阵子才用到email容器,您不能浪费session在这段时间的等待上,所以您先行加载容器对象,直接关闭session以节省资源。