9.容器映射
容器常用来储存对象,这边来了解一下如何将容器的关系映像至表格。
9.1 Set
关于Set的特性,您可以先参考 HashSet、TreeSet 这两篇文件的介绍,这边先介绍当Set中包括的对象为非实体(Entiy)时的映射方式,简单的说,也就是所包括的对象没有对象识别(Identity),只是纯綷的值型态(Value type)对象)。
假设您有一个User类别,当中除了名称属性之外,另一个就是使用者的电子邮件地址,同一个使用者可能有多个不同的邮件地址,所以在User类别中使用 Set对象来加以记录,在这边使用String来记录每一笔邮件地址,为了不允许重复的邮件地址记录,所以使用Set对象,User类别如下:
User.java
package onlyfun.caterpillar;
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(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
<?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"/>
<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("caterpillar");
user1.addEmail("caterpillar.onlyfun@gmail.com");
user1.addEmail("caterpillar.onlyfun@yahoo.com");
User user2 = new User();
user2.setEmails(new HashSet());
user2.setName("momor");
user2.addEmail("momor@gmail.com");
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
则数据库中的表格储存内容将如下:
mysql> select * from user;
+----+----------------+
| id | name |
+----+----------------+
| 1 | caterpillar |
| 2 | momor |
+----+----------------+
2 rows in set (0.00 sec)
mysql> select * from email;
+----+-------------------------------------------+
| id | address |
+----+-------------------------------------------+
| 1 | caterpillar.onlyfun@yahoo.com |
| 1 | caterpillar.onlyfun@gmail.com |
| 2 | momor@gmail.com |
+----+-------------------------------------------+
3 rows in set (0.00 sec)
注意到:在Set的功能说明中,并没有用到Email的Email..hbm.Xml文件和Email.java。在User.hbm.xml文件中声明的
<set name="emails" table="email">
<key column="id"/>
<element type="java.lang.String"
column="address"/>
</set>
Set中key为email的id,元素为address 于是更新的时候便对应的进行操作;
9.2 List
关于List的特性,可以先看一下[ArrayList]、[LinkedList]这两篇文件。
List是有序的结构,所以在储存List容器中的对象时,要一并储存其顺序信息,例如若您设计了以下的类别:
User.java
package onlyfun.caterpillar;
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 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
<?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"/>
<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("caterpillar");
user1.addItem("DC");
user1.addItem("CF Card");
User user2 = new User();
user2.setItems(new ArrayList());
user2.setName("momor");
user2.addItem("comics");
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
则数据库中的储存状况如下:
mysql> select * from user;
+----+--------------+
| id | name |
+----+--------------+
| 1 | caterpillar |
| 2 | momor |
+----+--------------+
2 rows in set (0.00 sec)
mysql> select * from item;
+----+------------+-----------+
| id | position | name |
+----+------------+-----------+
| 1 | 0 | DC |
| 1 | 1 | CF Card |
| 2 | 0 | comics |
+----+------------+-----------+
3 rows in set (0.00 sec)
注意到:在List的功能说明中,item表中出现position字段原因就是便于区分有序的list的准确定位;而我们对posetion的index取值也是自动排号的;
9.3 Map
Definition:
A map is a simple name-value pair list stored on a first rank collection.
Scenario:
Map Foo.getAges() // returns a collection of String name-value pairs
Hibernate Mapping
在hbm.xml中设定如下:
<class name="Foo" table="foo">
...
<map role="ages">
<key column="id"/>
<index column="name" type="string"/>
<element column="age" type="string"/>
</map>
</class>
Table Schema
Foo |
id |
Ages |
|
|
id | name | age |
A simple extra table, Ages, is used to store the name and age string-value pair. Note that the map needs its own identity column too: id
Bidirectionality:
Bidirectionality has no meaning for a map.
9.4 Bag
Bag是集合,与Set不同的是,Bag允许重复的元素,在Java的标准API中并没有提供Bag容器,Hibernate提供自己的Bag实现,允许您将List映射为Bag。
您可以如下定义User类别,其中的List成员将被用作Bag来使用,而不管对象在List容器中的顺序:
User.java
package onlyfun.caterpillar;
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
<?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"/>
<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("caterpillar");
user1.addItem("Java Gossip");
user1.addItem("Java Gossip");
user1.addItem("Caxxx A80");
User user2 = new User();
user2.setItems(new ArrayList());
user2.setName("momor");
user2.addItem("Snoppy world");
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
则数据库中会有如下的数据:
mysql> select * from user;
+----+-------------+
| id | name |
+----+-------------+
| 1 | caterpillar |
| 2 | momor |
+----+-------------+
2 rows in set (0.00 sec)
mysql> select * from item;
+----+-------------------+
| id | name |
+----+-------------------+
| 1 | Java Gossip |
| 1 | Java Gossip |
| 1 | Caxxx A80 |
| 2 | Snoppy world |
+----+-------------------+
4 rows in set (0.00 sec)
您可以如下更新数据:
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
<?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"/>
<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>
使用上面用过的程序片段来储存对象的话,数据库中会有如下的数据:
mysql> select * from user;
+----+-------------+
| id | name |
+----+-------------+
| 1 | caterpillar |
| 2 | momor |
+----+-------------+
2 rows in set (0.00 sec)
mysql> select * from item;
+----------------------------------+------+--------------------+
| cid | id | name |
+----------------------------------+------+--------------------+
| 297eba61056726030105672605df0001 | 1 | Java Gossip |
| 297eba61056726030105672605df0002 | 1 | Java Gossip |
| 297eba61056726030105672605df0003 | 1 | Caxxx A80 |
| 297eba61056726030105672605df0004 | 2 | Snoppy world |
+----------------------------------+------+--------------------+
4 rows in set (0.00 sec)
如果使用上面提到过的程序片段来更新对象的话,则实际上Hibernate会使用以下的SQL来进行更新:
Hibernate: delete from item where cid=?
这一次并不是整个删除集合中的数据,而是直接藉由cid来确定所要更新的数据,比起只使用Bag,idbag的效率好了许多。
9.5内含 Component 的容器
假设您建立了以下的表格:
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
package onlyfun.caterpillar;
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
package onlyfun.caterpillar;
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
<?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"/>
<set name="emails" table="email">
<key column="id"/>
<composite-element class="onlyfun.caterpillar.MailAddress">
<property name="address" column="address"/>
</composite-element>
</set>
</class>
</hibernate-mapping>
您可以如下储存对象:
User user = new User();
user.setName("caterpillar");
user.setEmails(new HashSet());
MailAddress mailAddress = new MailAddress();
mailAddress.setAddress("caterpillar.onlyfun@gmail.com");
user.addEmail(mailAddress);
mailAddress = new MailAddress();
mailAddress.setAddress("caterpillar.onlyfun@yahoo.com");
user.addEmail(mailAddress);
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();
session.close();
则数据库中会储存如下的数据:
mysql> select * from user;
+----+-------------+
| id | name |
+----+-------------+
| 1 | caterpillar |
+----+-------------+
1 row in set (0.00 sec)
mysql> select * from email;
+----+-------------------------------------------+
| id | address |
+----+-------------------------------------------+
| 1 | caterpillar.onlyfun@yahoo.com |
| 1 | caterpillar.onlyfun@gmail.com |
+----+-------------------------------------------+
2 rows in set (0.00 sec)
在查询时,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();
9.6容器的排序
从数据库的观点来看,Set、Map、Bag是无序的,而List是有序的,这边所谓的无序或有序,是指将容器中对象储存至数据库时,是否依容器对象中的顺序来储存。
然而从数据库取得数据之后,您也许会希望Set、Map等容器中的对象可以依一定的顺序来排列,您可以从两个层次来容器中的对象排序,一是在加载数据后于JVM中排序,另一是在数据库中直接使用order by子句来排序。
以 Set 这篇文章中的范例来作说明,要在JVM中就数据进行排序,您可以在映像文件中使用sort属性来定义容器的排序,这适用于Set与Map,例如:
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">
....
<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
package onlyfun.caterpillar;
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
<?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">
....
<set name="emails" table="email"
sort="onlyfun.caterpillar.CustomComparator">
<key column="id"/>
<element type="java.lang.String"
column="address"/>
</set>
</class>
</hibernate-mapping>
Bag与List并不适用于这种方式。
另一个排序的方式则是在数据库中进行,直接使用order by子句来排序,这可以在映像文件中使用order-by属性来指定,例如:
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">
....
<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
9.7容器的延迟初始(Lazy Initialization)
有时候您只是想要获得对象中某个属性的数据,如果您的对象中包括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
<?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">
....
<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();
在开启SQL显示的情况下,会显示以下的内容:
Hibernate:
select user0_.id as id0_, user0_.name as name0_0_
from user user0_ where user0_.id=?
caterpillar
Hibernate:
select emails0_.id as id0_, emails0_.address as address0_
from email emails0_ where emails0_.id=?
caterpillar.onlyfun@yahoo.com
caterpillar.onlyfun@gmail.com
可以看到,只有在需要查询容器中对象时,才会向数据库索取数据。
使用延迟初始时,由于在需要数据时会向数据库进行查询,所以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.getEmails()); // 先加载容器中的对象
session.close();
Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
即使启用延迟初始,在Hibernate.initialize()该行,email容器中的对象已经被加载,所以即使关闭session也无所谓了,这种情况发生在某个情况下,您启用了延迟初始,而使用者操作过程中,会在稍后一阵子才用到email容器,您不能浪费session在这段时间的等待上,所以您先行加载容器对象,直接关闭session以节省资源。