Hibernate笔记(抓取计划,策略)

延迟加载和急加载

有些时候,必须决定应该讲那些数据从数据库中加载到内存中,当执行entityManager.find(Item.class,123)时,那些咋内存中是可用的并且被加载到持久化上下文中呢?,如果转而使用EntityManager#getReference()又发生什么呢?
 在域-模型映射中,要在关联和集合上使用FetchType.LAZY和FetchType.EAGER选项来定义全局默认的抓取计划,这个计划是用于所有涉及持久化模型类的操作的默认设置.当通过标识符加载一个实体实例以及通过后续关联导航实体图并且遍历持久化集合时.他总是处于活动状态.
 我们推荐策略是将延迟的默认抓取计划用于所有实体和集合.如果使用FetchType.LAZY映射的所有的关联和集合.那么Hibernate将只在你进行访问的时候加载数据,当导航域模型实例的图时,Hibernate会按需一块一块地加载数据.然后在必要时基于每种情况重写此行为.
 为实现延迟加载.Hibernate借助被称为代理的运行时生成的实体占位符以及用于集合的智能包装器.

选择一个抓取策略

Hibernate会执行SQL SELECT语句讲数据加载到内存中,如果加载一个实体实例,则会执行一个或者多个SELECT.这取决于涉及的表数量以及所应用的抓取策略.你的目标就是最小化SQL语句的数量.并且将会SQL语句,以便查询尽可能提高效率.
 每一个关联和集合都应该按须被延迟加载.这一默认抓取计划很可能造成过多的SQL语句,每个语句都仅加载一小部分数据.这将导致n+1次查询问题.我们首先探讨这个问题,使用急加载这一可选抓取计划,将产生较少的SQL语句,因为每个SQL查询都会将较大快的数据加载到内存中,然后你可能会看到笛卡尔积问题,因为SQL结果集变得过大.
 需要在这个两个极端之间找到平衡.用于应用程序中每个程序和用例的理想抓取策略.就像抓取计划一样.可以在映射中设置一个全局抓取策略.总是生效的默认设置,然后对于某特定程序.可以用JPQL.CriteriaQuery或SQL查询重写默认抓取策略.

n+1查询问题

  1. 1 对多,在1 方,查找得到了n 个对象, 那么又需要将n 个对象关联的集合取出,于是本来的一条sql查询变成了n +1 条
  2. 多对1 ,在多方,查询得到了m个对象,那么也会将m个对象对应的1 方的对象取出, 也变成了m+1

笛卡尔积问题

如果查看域和数据模型并且认为,每次我需要一个Item时.我还需要改Item的seller.那么可以使用FetchType.EAGER而非延迟抓取计划来映射该关联.你希望确保无论何时加载一个Item.seller都会被立即加载.您希望数据在分离Item和关闭持久化上下文时可用.
 为了实现急抓取计划.Hibernate使用了一个SQL JOIN操作在一个SELECT中加载Item和User实例.

select i.*,u.* from t_item i left outer join t_users u on u.id = i.seller_id where i.id=?

将使用默认JOIN策略的急抓取用于@ManyToOne和@OneToOne关联没什么问题.可以使用一个SQL查询和多个JOIN急加载一个Item,其seller,该User的Address以及他们居住的City等,即便你使用FetchType.EAGER映射所有这些关联.结果集也只有一行,现在,Hibernate必须在某个时刻停止继续你的FetchType.EAGER计划,所链接的表的数量取决于全局的Hibernate.max_fetch_depth配置属性.默认情况下,不会设置任何限制,合理值很小,通常介意1到5之间.甚至可以通过该属性设置为0来禁用@ManyToOne和@OneToOne关联的JOIN抓取,如果Hibernate达到了该限制.那么它仍将根据您的抓取计划急加载数据.但会使用额外的SELECT语句,
 另一方面,使用JOINS的急加载集合会导致严重的性能问题.如果也为bids何images集合切换到FetchType.EAGER,就会碰到笛卡尔积问题.
这个问题会在用一个SQL查询和一个JOIN操作急加载两个集合时出现.看下面的例子.

@Table(name = "t_item")
public class Item {
    @OneToMany(mappedBy = "Item", fetch = FetchType.EAGER)
    private Set<Bid> bids = new HashSet<>();
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "IMAGE")
    @Column(name = "FILENAME")
    private Set<String> images = new HashSet<>();
}

这两个集合是@OneToMany @ManyToMany还是@ ElementCollection并没有什么关系.使用SQL JOIN操作符一次急抓取多个集合就是根本问题,无论集合内容是什么.如果加载一个Item,那么Hibernate就会执行有问题的SQL语句.

select i.*,b.*,img.* from Item i 
    left outer join Bid b on b.ITEM_ID = i.ID 
    left outer join Image img on img.ITEM_ID = i.ID
where i.ID = ?

Hibernate会服从你的急抓取计划.并且可以访问分离状态中的bids和images集合.问题在于.使用产生一个乘机SQL JOIN,这些集合是如何别加载的.
 该Item具有3个bids和3个images.乘积的大小取决于你正在检索的大小.3*3=9,现在思考一个具有50个bids和5个images的Item的情况.你会看到具有250行的一个结果集.在使用JPQL或CriteriaQuery编写你自己的查询时你甚至会创建更大的SQL乘积.想象一个你在加载500个items并且使用多个JOIN急抓取几十个bids和images时会发生什么么?
 数据库服务器上需要大量的处理时间和内存来创建这样的结果.这些结果还必须跨网络传输.如果寄希望于JDBC驱动法在传输是压缩该数据.你可能对数据库供应商的期望过高了.Hibernate会在将结果集封送到持久化实例和集合中时立即移除所有重复项.显然.无法再SQL级别移除这些重复项.
 接下来,我们要专注于此类优化以及如何找出并且实现最佳的抓取策略.我们还是从默认延迟抓取计划开始并且首先尝试解决n+1查询问题

批量抓取数据

如果Hibernate仅按需抓取每个实体关联和集合.那么可能就需要许多额外的SQL SELECT语句来完成某特定过程.像之前一样,思考一个检查每个Item的seller是否具有一个username的例子.使用延迟加载,就需要一个SELECT得到所有的Item实例以及更多的n个SELECT来初始化每个Item的seller代理.
 Hibernate提供了几个可以预抓取数据的算法.我们套探讨的第一个算法是批量预抓取.它会如下所示的工作.如果Hibernate必须初始化一个User代理,那么就使用相同的SELECT初始化几个User代理,换句话说,如果已经知道持久化上下文中有几个Item实例并且他们都具有一个应用到其seller关联的代理,那么久可以初始化几个代理,而不是在于数据库交互时只初始化一个代理

@Entity
@org.hibernate.annotations.BatchSize(size = 10)
@Table(name = "t_Users")
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

此设置会告知Hibernate,在必须加载一个User代理时它可以加载至10个,所有的代理都使用相同SELECT来加载.批量抓取通常被称为忙猜优化,因为你不知道某特定持久化上下文中会有多少个未初始化的User代理,你不能确定10是否是一个理想值.它只是一个猜测.你清楚相较于n+1个SQL查询.你现在回看到n+1/10个查询.已经显著减少了,合理值通常很小,因为你也不希望过多的数据加载到内存中,尤其是在您不确定是否需要他时,
 注意.Hibernate在您遍历items时执行SQL查询.当首次调用item.getSeller().getUserName()时.Hibernate必须初始化第一个User代理.相较于仅从USERS表中加载单个行.Hibernate会检索多个行,并且加载最多10个User实例.一旦访问第十一个seller.就会在一个批次中加载另外10个.一次类推.
 批量抓取也可用于集合:

@Entity
@Table(name = "t_item")
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "item")
    @org.hibernate.annotations.BatchSize(size = 5)
    private Set<Bid> bids = new HashSet<>();

批量抓取是简单的.并且通常智能优化能够显著降低SQL语句的数量.否则初始化所有代理和集合就需要大量的SQL语句,尽管最终可能会预抓取你不需要的数据.并且消耗更多的内存,但数据库交互的减少也会产生很大的差异,内存很便宜.但拓展数据库服务器就并非如此了.
 另一个并非盲猜的预抓取算法会使用子查询在单个语句中初始化多个集合.

使用子查询预抓取集合.

用于加载几个Item实例的所有bids的更好的一个策略是使用一个子查询进行预抓取.要启用此优化.需要将一个Hibernate注解添加到你的集合映射:

@Entity
@Table(name = "t_item")
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "item")
    @org.hibernate.annotations.Fetch(
            FetchMode.SUBSELECT
    )
    private Set<Bid> bids = new HashSet<>();

Hibernate会记住用于加载item的原始查询.然后他会在子查询中嵌入这个初始查询.以便为每个item检索bids的集合.
 如果在映射中坚持使用一个全局的延迟抓取计划.那么批量和子查询就会降低特定过程需要的查询数量,以帮助缓解n+1查询问题.如果相反,你的全局抓取计划具有急加载关联和集合,就必须避免笛卡尔积问题,例如.通过将一个join查询分解成几个SELECT来避免.

使用多个SELECT进行急抓取

当尝试一个SQL查询和多个JOIN抓取几个集合时,就会碰到笛卡尔积问题.将像之前的阐述过的那样,相较于一个JOIN操作,可以告知HIbernate用几个额外的SELECT查询急加载数据.并因而避免大的结果以及具有重复项的SQL乘积.

@Entity
@Table(name = "t_item")
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "item", fetch = FetchType.EAGER)
    @org.hibernate.annotations.Fetch(
            FetchMode.SELECT
    )
    private Set<Bid> bids = new HashSet<>();

    @ManyToOne(fetch = FetchType.EAGER)
    @org.hibernate.annotations.Fetch(
            FetchMode.SELECT
    )
    private User seller;

现在,当加载一个Item时,也必须加载seller和bids:

Item item = em.find(Item.class,ITEM_ID);
//select * from Item where id = ?
//select * from User where id = ?
//select * from Bid where ITEM_ID = ?

Hibernate会使用一个SELECT从ITEM表中加载一行,然后它会立即执行两个SELECT;一个从USER表中加载一行(seller),另一个从BID表中加载几行(bids).
 额外的SELECT查询不会被延迟执行:find()方法会生成几个SQL查询.可以看到Hibernate如何遵循急抓取计划:所有数据在分离状态下都是可用的.

动态急抓取

我们假设你必须检查每个Item#seller的username,使用一个延迟全局抓取计划,加载这个过程所需的数据并且在一个查询中应用动态急抓取策略:

List<Item> items = em
.createQuery("select i from Item i join fetch i.seller");
//select i.*,u.* from Item i inner join User u on 
//u.ID = i.SELLER_ID 
//where i.ID= ?

这个JPQL查询中的重要关键词是join fetch,告知Hibernate使用一个SQL JOIN在相同查询中检索每个Item的Seller,也可以使用CriteriaQuery API而非JPQL字符串来表示相同的查询.

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery criteria = cb.createQuery();
Root<Item> i = criteria.from(Item.class);
i.fetch("seller");
criteria.select(i)
List<Item> items = em.createQuery(criteria).getResultList();

动态急联结抓取也使用与集合.此处要加载每个Item的所有bids:

List<Item> items = em.createQuery("select i from Item i left join fetch i.bids").getResultList();
//select i.*,b.* from Item i left outer join Bid b 
//on b.ITEM_ID = i.ID
//where i.ID = ?

同样也可以使用CriteriaQuery API

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery criteria = cb.createQuery();
Root<Item> i = criteria.from(Item.class);
i.fetch("bids",JoinType.LEFT);
criteria.select(i)
List<Item> items = em.createQuery(criteria).getResultList();

推荐阅读更多精彩内容