20.5. 理解集合性能(Understanding Collection performance)
在前面的章节里我们已经讨论了集合和相关应用程序。在本节我么将探索运行时集合的更多问题。
20.5.1. 分类(Taxonomy)
Hibernate 定义了三种基本类型的集合:
值数据集合
一对多关联(One-to-many Associations)
多对多关联
这个分类是区分了不同的表和外键关系类型,但是它没有告诉我们关系模型的所有内容。 要完全理解他们的关系结构和性能特点,我们必须同时考虑“用于 Hibernate 更新或删除集合行数据的主键的结构”。因此得到了如下的分类:
有序集合类
集合(sets)
包(bags)
所有的有序集合类(maps,lists,arrays)都拥有一个由
<key>
和 <index>
组成的主键。这种情况下集合类的更新是非常高效的 — 主键已经被有效的索引,因此当 Hibernate 试图更新或删除一行时,可以迅速找到该行数据。
集合(sets)的主键由
<key>
和其他元素字段构成。对于有些元素类型来说,这很低效,特别是组合元素或者大文本、大二进制字段;数据库可能无法有效的对复杂的主键进行索引。另一方面,对于一对多、多对多关联,特别是合成的标识符来说,集合也可以达到同样的高效性能。( 附注:如果你希望 SchemaExport
为你的 <set>
创建主键,你必须把所有的字段都声明为 not-null="true"
。)
<idbag>
映射定义了代理键,因此它总是可以很高效的被更新。事实上,<idbag>
拥有着最好的性能表现。
Bag 是最差的。因为 bag 允许重复的元素值,也没有索引字段,因此不可能定义主键。 Hibernate 无法判断出重复的行。当这种集合被更改时,Hibernate 将会先完整地移除 (通过一个(in a single
DELETE
))整个集合,然后再重新创建整个集合。因此 Bag 是非常低效的。
请注意:对于一对多关联来说,“主键”很可能并不是数据库表的物理主键。但就算在此情况下,上面的分类仍然是有用的。(它仍然反映了 Hibernate 在集合的各数据行中是如何进行“定位”的。)
20.5.2. Lists,maps 和 sets 用于更新效率最高
根据我们上面的讨论,显然有序集合类型和大多数 set 都可以在增加、删除、修改元素中拥有最好的性能。
可论证的是对于多对多关联、值数据集合而言,有序集合类比集合(set)有一个好处。因为
Set
的内在结构, 如果“改变”了一个元素,Hibernate 并不会更新(UPDATE)
这一行。对于 Set
来说,只有在插入(INSERT)
和删除(DELETE)
操作时“改变”才有效。再次强调:这段讨论对“一对多关联”并不适用。
注意到数组无法延迟载入,我们可以得出结论,list,map 和 idbags 是最高效的(非反向)集合类型,set 则紧随其后。 在 Hibernate 中,set 应该时最通用的集合类型,这时因为“set”的语义在关系模型中是最自然的。
但是,在设计良好的 Hibernate 领域模型中,我们通常可以看到更多的集合事实上是带有
inverse="true"
的一对多的关联。对于这些关联,更新操作将会在多对一的这一端进行处理。因此对于此类情况,无需考虑其集合的更新性能。
20.5.3. Bag 和 list 是反向集合类中效率最高的
在把 bag 扔进水沟之前,你必须了解,在一种情况下,bag 的性能(包括list)要比 set 高得多:对于指明了
inverse="true"
的集合类(比如说,标准的双向的一对多关联),我们可以在未初始化(fetch)包元素的情况下直接向 bag 或 list 添加新元素! 这是因为 Collection.add()
)或者 Collection.addAll()
方法对 bag 或者 List 总是返回 true(这点与与 Set 不同)。因此对于下面的相同代码来说,速度会快得多。
Parent p = (Parent) sess.load(Parent.class, id);
Child c = new Child();
c.setParent(p);
p.getChildren().add(c); //no need to fetch the collection!
sess.flush();
20.5.4. 一次性删除(One shot delete)
偶尔的,逐个删除集合类中的元素是相当低效的。Hibernate 并没那么笨,如果你想要把整个集合都删除(比如说调用 list.clear()),Hibernate 只需要一个 DELETE 就搞定了。
假设我们在一个长度为20的集合类中新增加了一个元素,然后再删除两个。Hibernate 会安排一条
INSERT
语句和两条 DELETE
语句(除非集合类是一个 bag)。这当然是令人满意的。
但是,假设我们删除了 18 个数据,只剩下 2 个,然后新增 3 个。则有两种处理方式:
逐一的删除这 18 个数据,再新增三个;
删除整个集合类(只用一句 DELETE 语句),然后逐一添加 5 个数据。
Hibernate 还没那么聪明,知道第二种选择可能会比较快。(也许让 Hibernate 不这么聪明也是好事,否则可能会引发意外的“数据库触发器”之类的问题。)
幸运的是,你可以强制使用第二种策略。你需要取消原来的整个集合类(解除其引用),然后再返回一个新的实例化的集合类,只包含需要的元素。有些时候这是非常有用的。
显然,一次性删除并不适用于被映射为
inverse="true"
的集合。