111.默认的抓取计划(fetch plan)和抓取策略(fetching strategy)是应用到特定的实体关联或者集合的计划和策略。换句话说,它定义了当加载自己的实体对象,以及访问一个被关联的对象或者集合时,被关联的对象或者集合是否以及如何被加载。每一种获取方法否可能使用不同的计划和策略——也就是说,集合定义了持久化对象网络的哪一部分应该被获取和怎样获取。
(1)HQL和JPA QL
HQL是常见的数据库查询语言SQL的一种面向对象的方言。
HQL常用语对象获取,而不是更新、插入或者删除数据。
HQL支持:
- 对按引用相关或者处于集合中的被关联对象的属性,应用限制的能力(用查询语言导航对象图)。
- 只获取一个或者多个实体的属性的能力,没有加载实体本身到持久化上下文的系统开销。有时这杯称作报告查询,更准确的称呼是投影。
- 对查询结果进行排序的能力。
- 对查询结果进行分页的能力。
- 与group by 、having和聚集函数(如sum、min和max/min)联合使用
- 当每一行获取多个对象时使用外部联结。
- 调用标准的和用户定义的SQL函数的能力。
- 子查询(嵌套查询)。
(2)按条件查询
Hibernate的按条件查询(Query By Criteria,QBC)API允许查询在运行时通过Criteria对象的操作进行创建。
一个Criteria就是一颗Criterion实例的树。Restriction类提供了返回Criterion实例的静态工厂方法。一旦建立了想要的Criteria树,就可以依靠数据库来执行它了。
(3)按示例查询
作为Criteria工具的一部分,Hibernate支持按示例查询(Query By Example,QBE)。按示例查询背后的思想是,应用程序通过某些属性值设置,提供被查询类的实例(到非默认的值)。查询结果返回带有匹配属性值的所有持久化实例。
112.Hibernate给所有的实体和集合默认一个延迟的抓取策略。这意味着Hibernate在默认情况下只加载你正在查询的对象。
代理是在运行时生成的占位符。每当Hibernate返回实体类的实例时,它就检查是否可以返回一个代理来代替,并避免数据库命中。代理是当真实对象第一次被访问时触发其加载的占位符;
只要你只访问数据库标识符属性,就没有必要初始化代理。(注意,如果用直接的字段访问映射标识符属性,就不是这样了;Hibernate当时甚至不知道getId()方法的存在。如果调用它,代理就必须被初始化。)
如果调用get()而不是load(),就触发了一次数据库命中,且没有返回任何代理。get()操作始终命中数据库(如果实例还没有处在持久化上下文中,并且如果没有任何透明的二级高速缓存是活动的),如果无法找到对象就返回null.
JPA提供程序可以利用代理实现延迟加载。EntityManager API中相当于load()和get()这些操作方法的名称是find()(相当于get())和getReference()(相当于load());
113.可以给一个特定的实体类禁用代理生成,通过XML映射元数据中的lazy="false"属性。
JPA标准不要求通过代理实现;“代理”一词甚至没有出现在规范中。Hibernate是默认情况下依赖代理的JPA提供程序。因此禁用Hibernate代理的切换可以用一个供应商扩展:@org.hibernate.annotations.Proxy(lazy=false)
在Hibernate中,FetchType.EAGER提供与lazy="false"同样的保证:被关联的实体实例必须被即时抓取,而不是延迟。虽然Hibernate中的所有关联都是完全延迟的,所有@ManyToOne和@OneToOne关联都默认为FetchType.EAGER!这项默认被标准化,允许Java Persistence提供程序不通过延迟加载实现。建议通过在“对一”的关联映射中设置FetchType.LAZY,默认为Hibernate的延迟加载计划,并且只在必要时才覆盖它。
114.lazy属性默认为proxy。通过设置no-proxy,你在告诉Hibernate对这个关联应用拦截。
也可以延迟加载通过<property>或者<component>映射的属性;早Hibernate XML映射中,这里启用拦截的属性是lazy="true"。利用注解,@Basic(fetch=FetchType.LAZY)是给Hibernate的一个提示:有一个属性或者组件应该通过拦截被延迟加载。
Hibernate执行SQL SELECT语句把对象加载到内存。如果加载一个对象,那么就要执行单个或者几个SELECT,这取决于所涉及的表的数量,以及你所采取的抓取策略。
第一个优化步骤是,减少利用默认的延迟行为时定会看到的额外按需SELECT的数量——例如,通过预抓取数据。
<class name="User" table="USERS" batch-size="10"></class>
另一种并非瞎猜的预抓取算法是使用子查询,通过单个语句初始化多个集合。
<class name="Item" table="ITEM">
<set name="bids" inverse="true" fetch="subselect">
<key column="ITEM_ID"/>
<one-to-many class="Bid"/>
</set>
</class>
在编写本书时,子查询抓取只可用于集合,而不可用于实体代理。还要注意,作为子查询重新运行的原始查询,只被Hibernate为一个特定的Session而记住。如果你分离一个Item实例,而没有初始化bids的集合,那么重附它并启动对该集合的遍历,则不会发生其他集合的预抓取。
延迟加载是一种极好的默认策略。另一方面,你可以经常看看领域模型和数据模型,并说“每次我需要Item的时候,也需要该Item的seller”。如果可以把它变成语句,就应该到映射元数据中去,给seller关联启用即时抓取(eager fetching),并利用SQL联结:
<class name="Item" table="ITEM">
<many-to-one name="seller" class="User" column="SELLER_ID" update="false" fetch="join"/>
</class>
利用Java Persistence注解,通过FetchType注解属性启用即时抓取
最后,必须介绍一个全局的、可以用来控制被联结的实体关联(不是集合)最大数量的Hibernate配置设置;hibernate.max_fetch_depth,默认情况下,没有设置限制。合理的设置很小,通常在1~5之间。你甚至可以通过设置该属性为0,给多对一和一对一的关联禁用联结抓取!
如果涉及继承或者被联结的映射,SQL查询也会变得更加复杂。每当给一个特定的实体类映射二级表时,你需要考虑几个额外的优化选项。
你想要继承层次结构的一些部分通过立即的额外SELECT语句而被抓取,而不是通过初始查询中的OUT JOIN。
启用这种抓取策略的唯一方式是稍微重构映射,作为使用了辨别标识列的每个层次结构一张表和使用了<join>映射的每个子类一张表的混合。
在<many-to-one>或者<one-to-one>关联中设置的fetch="join"是一般的优化,就像@ManyToOne(fetch=FetchType.EAGER)注解(Java Persistence中的默认)一样。单端关联的即时联结抓取,不同于集合的即时外部联结抓取,不会产生笛卡尔积问题。
代理或者集合包装器,每当调用它的任何方法时都会被自动初始化(除了标识符属性获取方法之外,它可能返回标识符值,而不抓取底层的持久化对象)。预抓取和即时联结抓取都是获取你需要的所有数据的可能的解决方案。
Hibernate.initialize()可以传递一个集合包装器或者代理。注意,如果你把一个集合包装器传递到initialize(),它不会初始化这个集合所引用的目标实体对象。
115.这两个配置属性hibernate.format_sql和hibernate.use_sql_comments,使得在日志文件中阅读和分类SQL语句变得容易多了。优化期间启用这两个。
考虑转化到多对一、一对一和(有时)集合映射中的lazy="false"(或者FetchType.EAGER)。全局的抓取计划定义了始终被即时加载的对象。优化你的查询,并且如果你需要即时(不是全局)地加载对象,就启用即时抓取,但要在特定的过程(仅有的一个用例)中。
一旦定义了抓取计划,并且知道一个特定的用例所需要的数据量,就优化这个数据抓取的方式。你可能遇到两个常见的问题:
(1)SQL语句使用太负责且太慢的联结操作。首先和你的数据库管理员一起优化SQL执行计划。如果这样还没有解决问题,就移除集合映射中的fetch="join"(或者先不设置它)。通过考虑它们是真的需要fetch="join"策略,还是被关联的对象应该通过二级查询进行加载,来最优化你的多对一和一对一关联。也可以通过全局的hibernate.max_fetch_depth配置选项来尝试调优,但是记住这个值最好保持在1~5之间。
(2)可能执行过多的SQL语句。在多对一和一对一关联映射中设置fetch="join"。在极少数情况下,如果你绝对确定,就启用fetch="join",禁用对特定集合的延迟加载。记住,每个持久化类都有不止一个被即时抓取的集合会导致乘积。利用批量或者子查询,来评估你的用例是否能从集合的预抓取中受益。使用3~15之间的批量大小。
116.高速缓存基本原理
高速缓存使当前数据库状态的表示接近应用程序,要么在内存中,要么在应用程序服务器的磁盘上。高速缓存是数据的一个本地副本,处在应用程序和数据库之间,可以用来避免数据库命中,每当:
- 应用程序通过标识符(主键)执行查找的时候。
- 持久层延迟解析一个关联或者集合的时候。
(1)高速缓存的概念在对象/关系持久化是如此基本,以致如果你不限知道它使用了哪几种高速缓存策略,就无法理解性能、可伸缩性或者ORM实现的事务语义。有3种主要的高速缓存类型:
- 事务范围高速缓存——添加到当前的工作单元,它可能是一个数据库事务,甚至是一个对话。它只有在工作单元运行时才是有效的,才能被使用。每一个工作单元都有自己的高速缓存。这个高速缓存中的数据不会被并发访问。
- 过程范围高速缓存——在许多(可能并发的)工作单元或者事务之间共享。这意味着过程范围高速缓存中的数据被并发运行的线程访问。显然隐含着事务隔离性。
- 集群范围高速缓存——在同一台机器的多个过程之间或者一个集群中的多台机器上共享。这里,网络通信是一个值得考虑的关键点。
持久层使用的高速缓存类型影响着对象同一性的范围(Java对象同一性和数据库同一性之间的关系)。
利用过程范围高速缓存的持久化机制可能选择实现过程范围的同一性。在这种情况下,对象的同一性就相当于这个过程的数据库同一性。在两个并发运行的工作单元中,使用相同的数据库标识符的两个查找产生了相同的Java实例。另一种方法是,从过程范围的高速缓存中获取的对象可能按值(by value)被返回、此时,每个工作单元获取它自己的状态副本(考虑原始数据),并且生成的持久化实例也不相同。高速缓存的范围和对象同一性的范围就不再相同。
(2)任何允许多个工作单元共享相同持久化实例的ORM实现,都必须提供某些对象级锁定的形式,以确保并发访问的同步。这通常结合使用读/写锁(保存在内存中)与死锁侦测共同实现。像Hibernate这样的实现,给每一个工作单元(工作单元范围的同一性)维护一个独特的实例组,很大程度地避免了这些问题。
(3)高速缓存和事务隔离性
如果应用程序有对数据库的非专有访问,就不应该使用过程范围高速缓存,除了对很少改变且可以被高速缓存期限安全地刷新的数据之外。
117.Hibernate高速缓存架构
Hibernate有一个两级的高速缓存架构。
- 一级高速缓存是持久化上下文高速缓存。一个Hibernate Session的寿命相当于单个请求(通常用一个数据库事务实现)或者单个对话。这是个强制的一级高速缓存,它保证对象的范围和数据库同一性(例外的是StatelessSession,它没有持久化上下文)。
- Hibernate中的二级高速缓存是可插拔的,并且可以被界定到过程或者集群。这是一个状态的高速缓存(按值返回),二不是实际的持久化实例的高速缓存。高速缓存并发策略给特定的数据项目定义了事务隔离细节,而高速缓存提供程序表示了物理高速缓存实现。二级高速缓存的使用是可选的,可以在按类和按集合的基础上配置——每一个这样的高速缓存都利用它自己的物理高速缓存区域。
- Hibernate也给与二级高速缓存密切整合的查询结果集实现高速缓存。这是个可选的特性;它需要两个额外的物理高速缓存区域,当表最后一次被更新时保存被高速缓存的查询结果和时间戳。
(1)Hibernate二级高速缓存
Hibernate的二级高速缓存有过程或者集群范围:已经从特定的SessionFactory开始的(或者与特定持久化单元的EntityManager关联的)所有持久化上下文共享同一个二级高速缓存。
持久化实例以分解的形式保存在二级高速缓存中。把分解当做一个过程,有点像序列化(但这个算法比Java序列化要快很多)。
不同种类的数据需要不同的高速缓存策略:读取与写入之比不同,数据库表的大小就不同,并且有些表与其他外部应用程序共享。二级高速缓存在一个单独的类或者集合角色的粒度中时可配置的。
高速缓存通常只对主要用来读取的类有用。如果你有更新比读取更频繁的数据,就不要启用二级高速缓存,即使所有其他的高速缓存条件都符合!二级高速缓存存在与其他的写入应用程序共享数据库的系统中可能很危险。
Hibernate二级高速缓存的创建分两步。首先必须决定使用哪种并发策略(concurrency strategy)。之后,利用高速缓存提供程序配置高速缓存过期和物理高速缓存属性。
(2)内建的并发策略
并发策略是一个媒介:它负责在高速缓存中保存数据的项目,并从高速缓存中获取它们。
下面4个内建的并发策略表示递减的事务隔离方面的严格级别:
- 事务(transactional)——只可用于脱管环境,如有必要,它还保证完全的事务隔离直到可重复读取(repeatable read)。给主要用于读取的数据使用这种策略,因为在这种数据中,防止并发事务中的废弃数据最为关键,极少数情况下用于更新。
- 读/写(read-write)——这种策略利用时间戳机制,维护读取提交(read committed)隔离,并且只在非集群环境中可用。还是给主要用于读取的数据使用这种策略,因为在这种数据中防止并发事务中的废弃数据最为关键,极少数情况下用于更新。
- 非严格读/写(nonstrict-read-write)——不提供高速缓存和数据库之间的一致性保证。如果有可能并发访问相同的实体,你应该配置一个足够段的超时期限。否则,则可能从高速缓存中读取废弃的数据。如果数据几乎不变(几个小时、几天甚至一周),并且废弃的数据不可能是关键的关注点,那就使用这种策略。
- 只读(read-only)——并发策略适合于从不改变的数据。它只用于引用数据。
(3)选择高速缓存提供程序
Hibernate内建了一下开源产品的提供程序:
EHCache是特意用于单个JVM中简单的过程范围高速缓存的高速缓存提供程序。它可以高速缓存在内存或者磁盘中,并支持可选的Hibernate查询结果高速缓存。
OpenSymphony OSCache是一项服务,它通过一组丰富的过期策略和查询高速缓存支持,支持在单个JVM中高速缓存到内存和磁盘。
SwarmCache是基于JGroups的集群高速缓存。它使用集群的失效,但不支持Hibernate查询高速缓存。
JBoss Cache是一个完全是无复制的集群高速缓存,也基于JGroups多播库。让支持复制或者失效、同步或者不同步的通信,以及乐观锁和悲观锁。支持Hibernate查询高速缓存,假设时钟在集群中同步。
创建高速缓存涉及两个步骤:首先,在映射元数据中看看持久化类和集合,并决定要给每个类和每个集合使用哪种高速缓存并发策略。然后,在全局的Hibernate配置中启用首选的高速缓存提供程序,并定制提供程序特定的设置和物理高速缓存区域。
118.从添加Hibernate高速缓存Category实例所需的映射元素开始:
<class name="auction.model.Category" table="CATEGORY">
<cache usage="read-write"/>
<id...
</class>
usage="read-write"属性告诉Hibernate,给auction.model.Category高速缓存使用一个读/写并发策略。每当导航到Category,或者当按标识符加载Category时,Hibernate现在就命中二级高速缓存。
类高速缓存始终对持久化类的整个层次结构而被启用。你无法只高速缓存一个特定子类的实例。
Hibernate在不同的高速缓存区域(cache region)保存不同的类/集合。区域是一个具名的高速缓存:这个句柄使你可以通过它在高速缓存提供程序配置中引用类和集合,并设置适用于该区域的过期策略。一种更为图形化的描述是,区域是一桶桶的数据,它们有两种类型:一种区域类型包含实体实例的分解数据,另一种类型只包含通过集合而被链接的实体标识符。
对于类高速缓存而言,区域的名称是类名;对于集合高速缓存而言,是类名加属性名。
Hibernate具名hibernate.cache.region_prefix的配置属性可能被用来给特定的SessionFactory或者持久化单元指定区域名前缀。如果应用程序使用多个SessionFactory实例或者持久化单元,这项设置就是必需的。没有它,不同持久化单元的高速缓存区域名称就可能冲突。
(1)设置本地的高速缓存提供程序
需要设置选择了高速缓存提供程序的配置属性:
hibernate.cache.provider_class=org.hibernate.cache.EhCacheProvider
(2)设置重复的高速缓存
JBoss Cache,一个基于TreeCache和JGroups多播库的集群安全的高速缓存系统。JBoss Cache是非常可伸缩的,并且集群通信可以用任何可能的方式进行调优。
对于集群高速缓存提供程序而言,把Hibernate配置选项hibernate.cache.use_minimal_puts设置为true可能更好。当启用这项设置时,Hibernate只在检查以确保该项目还没有被高速缓存之后才把它添加到高速缓存中。如果高速缓存写入比高速缓存读取更贵,则使这种策略执行得更好。
(3)控制二级高速缓存
Hibernate有一些有用的方法,可以帮助你测试和调优高速缓存。给二级高速缓存hibernate.cache.use_second_level_cache考虑全局的配置转换。默认情况下,映射文件(在hibernate.cfg.xml或者注解)中的任何<cache>元素都触发二级高速缓存,并在启动时加载高速缓存提供程序。如果想要全局地禁用二级高速缓存,而不移除高速缓存映射元素或者注解,那么就设置这个配置属性为false.
可以调用evict(),通过指定类和对象标识符值,从二级缓存中移除元素:
SessionFactory.evict(Category.class,new Long(123));
也可以通过指定一个区域名称,来清除一个特定类的所有元素,或者清除一个特定集合角色:
SessionFactory.evict("auction.model.Category");
二级高速缓存的清除是非事务的,也就是说,高速缓存区域在清除期间没有被锁定。
Hibernate还提供CacheMode选项,它可以由特定的Session启用。想象你想要在Session中把许多个对象插入数据库。需要以批量来完成这项工作,以避免内存耗尽——每一个对象都被添加到一级高速缓存中。然而,如果为实体类启用了它,它也被添加到二级高速缓存中。CacheMode控制Hibernate与二级高速缓存的交互;
设置CacheMode.IGNORE高速Hibernate不要在这个特定的Session中与二级缓存交互。可用的选项如下:
CacheMode.NORMAL——默认的行为
CacheMode.IGNORE——Hibernate从来不与二级高速缓存交互,除了更新发生时使被高速缓存的项目失效之外。
CacheMode.GET——Hibernate可能从二级高速缓存中读取项目,但不添加项目,除了更新发生时使项目失效之外。
CacheMode.PUT——Hibernate从来不从二级高速缓存中读取项目,但是当它从数据库中读取项目时,会把项目添加到高速缓存。
CacheMode.REFRESH——Hibernate从来不从二级高速缓存中读取项目,但是当它从数据库中读取项目时,会把项目添加到高速缓存。在这种模式下,hibernate.cache.use_minimal_puts的作用被忽略,以便在一个复制的集群高速缓存中强制高速缓存刷新。
119.在执行之前,必须在应用程序代码中准备一个查询。因此,查询涉及几个独特的步骤:
(1)创建查询,通过任何任意的限制或者你想要获取的数据的投影。
(2)把运行时实参绑定到查询参数,该查询可以通过改变设置被重用。
(3)执行针对数据库的预编译查询和数据获取。你可以控制查询如何执行,以及应该如何把数据获取到内存中。
org.hibernate.Query和org.hibernate.Criteria这两个接口都定义了几种控制查询执行的方法。此外,Query还提供把具体值绑定到查询参数的方法。
Java Persistence指定javax.persistence.Query接口。
120.为了创建新的Hibernate Query实例,在Session上调用createQuery()或者createSQLQuery。createQuery()方法来准备一个HQL查询。通过底层数据库的元素语法,可以用createSQLQuery()来创建SQL查询。
要想获得Criteria实例,就调用createCriteria(),传递你想让查询返回的对象的类。这也称作为条件查询的根实体(root entity)。
利用JPA,你的查询起点是EntityManager。为了给JPA QL创建javax.persistence.Query实例,就调用createQuery();为了创建元素的SQL查询,就用createNativeQuery();