FetchType: Hibernate & JPA 的 Lazy/Eager 加载
在定义实体映射时,选择正确的 FetchType
是最重要的决定之一。它指定了 JPA 实现(例如 Hibernate)何时从数据库中获取关联实体。你可以在 EAGER 和 LAZY 加载之间进行选择。第一个选项会立即获取关联,另一个仅在使用它时才获取关联。我在本文中解释了这两个选项。
选择正确的 FetchType
时的主要挑战是确保尽可能高效地获取实体,并避免获取任何不需要的东西。但这比看起来要复杂得多。你可以在实体的映射定义中静态指定 FetchType
,Hibernate 每次获取实体时都会使用它。这使得选择一个与你的所有用例都匹配的 FetchType
以及为什么应该将其与特定于查询的获取相结合变得具有挑战性。我将在本文末尾提供更多相关信息。
但是,让我们首先深入了解不同的 FetchType
及其定义。
默认 FetchType 及如何对其进行修改
JPA 规范为所有关联类型定义了默认的 FetchType
。只要不指定 FetchType
,就会使用默认选项。
此默认值取决于关联的基数。所有对一关联使用 FetchType.EAGER
而所有对多关联使用 FetchType.LAZY
。
你可以通过设置 @OneToMany
、@ManyToOne
、@ManyToMany
和 @OneToOne
注释的 fetch
属性来覆盖默认值。
@Entity
@Table(name = "purchaseOrder")
public class Order implements Serializable {
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private Set<OrderItem> items = new HashSet<OrderItem>();
...
}
好了,让我们细看一下不同 FetchType 的详情。
FetchType.EAGER – 立即获取关联,这样你需要的时候就可以直接使用
FetchType.EAGER
告诉 Hibernate 在检索根实体时获取关联的实体。正如我之前解释的那样,这是默认的一对一关联。你可以在以下代码中看到它的效果。
我在 OrderItem
和 Product
实体之间的 @ManyToOne
关联上,使用默认的 FetchType.EAGER
。
@Entity
public class OrderItem implements Serializable
{
@ManyToOne
private Product product;
...
}
现在当我从数据库中获取 OrderItem
实体时,Hibernate 也会同时获取 Product
实体。
OrderItem orderItem = em.find(OrderItem.class, 1L);
log.info("Fetched OrderItem: "+orderItem);
Assert.assertNotNull(orderItem.getProduct());
就像你能在日志输出中看到的,Hibernate 执行了一个获取两个实体的查询。
05:01:24,504 DEBUG SQL:92 - select orderitem0_.id as id1_0_0_, orderitem0_.order_id as order_id4_0_0_, orderitem0_.product_id as product_5_0_0_, orderitem0_.quantity as quantity2_0_0_, orderitem0_.version as version3_0_0_, order1_.id as id1_2_1_, order1_.orderNumber as orderNum2_2_1_, order1_.version as version3_2_1_, product2_.id as id1_1_2_, product2_.name as name2_1_2_, product2_.price as price3_1_2_, product2_.version as version4_1_2_ from OrderItem orderitem0_ left outer join purchaseOrder order1_ on orderitem0_.order_id=order1_.id left outer join Product product2_ on orderitem0_.product_id=product2_.id where orderitem0_.id=?
05:01:24,557 INFO FetchTypes:77 - Fetched OrderItem: OrderItem , quantity: 100
这似乎是一种有效的方法。但请记住,Hibernate 在获取 OrderItem
时总是会获取 Product
实体。
即使你在业务代码中不使用 Product
实体,情况也是如此。如果关联实体不是太大,Hibernate 只获取一个对一的关联,这不是最佳选择,但通常也不是一个大问题。如果你在多关联或者庞大的对多关联上使用 FetchType.EAGE
情况会很快改变。Hibernate 必须获取数十甚至数百个额外的实体,这会产生巨大的开销。
FetchType.LAZY – 需要时才获取关联
FetchType.LAZY
告诉 Hibernate,只有在你首次使用关联时才只从数据库中获取关联的实体。一般来说,这是一个好主意,因为没有理由选择不在业务代码中使用的实体。你可以在以下代码中看到一个延迟获取关联的示例。
Order
和 OrderItem
实体之间的 @OneToMany
关联使用 FetchType.LAZY
。这是对多关联的默认设置。
@Entity
@Table(name = "purchaseOrder")
public class Order implements Serializable {
@OneToMany(mappedBy = "order")
private Set<OrderItem> items = new HashSet<OrderItem>();
...
}
使用的 FetchType 不会影响业务代码。你可以像其他 getter 方法一样调用 getOrderItems() 方法。
Order newOrder = em.find(Order.class, 1L);
log.info("Fetched Order: "+newOrder);
Assert.assertEquals(2, newOrder.getItems().size());
Hibernate 透明地处理延迟初始化,并在第一次调用 getOrderItems()
、方法时获取 OrderItem
实体。
05:03:01,504 DEBUG SQL:92 - select order0_.id as id1_2_0_, order0_.orderNumber as orderNum2_2_0_, order0_.version as version3_2_0_ from purchaseOrder order0_ where order0_.id=?
05:03:01,545 INFO FetchTypes:45 - Fetched Order: Order orderNumber: order1
05:03:01,549 DEBUG SQL:92 - select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_, product1_.id as id1_1_2_, product1_.name as name2_1_2_, product1_.price as price3_1_2_, product1_.version as version4_1_2_ from OrderItem items0_ left outer join Product product1_ on items0_.product_id=product1_.id where items0_.order_id=?
如果你处理单个 Order
实体或一个小的实体列表,则以这种方式处理延迟关联是完全可以的。但是,当你在大的实体列表上执行此操作时,它会成为一个性能问题。正如在以下日志消息中看到的,Hibernate 必须为每个 Order
实体执行额外的 SQL 语句以获取其 OrderItem
。
05:03:40,936 DEBUG ConcurrentStatisticsImpl:411 - HHH000117: HQL: SELECT o FROM Order o, time: 41ms, rows: 3
05:03:40,939 INFO FetchTypes:60 - Fetched all Orders
05:03:40,942 DEBUG SQL:92 - select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_, product1_.id as id1_1_2_, product1_.name as name2_1_2_, product1_.price as price3_1_2_, product1_.version as version4_1_2_ from OrderItem items0_ left outer join Product product1_ on items0_.product_id=product1_.id where items0_.order_id=?
05:03:40,957 DEBUG SQL:92 - select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_, product1_.id as id1_1_2_, product1_.name as name2_1_2_, product1_.price as price3_1_2_, product1_.version as version4_1_2_ from OrderItem items0_ left outer join Product product1_ on items0_.product_id=product1_.id where items0_.order_id=?
05:03:40,959 DEBUG SQL:92 - select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id4_0_1_, items0_.product_id as product_5_0_1_, items0_.quantity as quantity2_0_1_, items0_.version as version3_0_1_, product1_.id as id1_1_2_, product1_.name as name2_1_2_, product1_.price as price3_1_2_, product1_.version as version4_1_2_ from OrderItem items0_ left outer join Product product1_ on items0_.product_id=product1_.id where items0_.order_id=?
这种行为被称为 n+1 查询问题。这是使用 Hibernate 时性能问题最常见的原因。
解决这个问题有几个好方法和一个错误方法。
错误的方法是使用 FetchType.EAGER
。它非但没有解决问题,反而以另一种低效取而代之。如果每次获取 Order
实体时,还需要关联的 OrderItem
,那么这将是一个好主意。但通常情况并非如此。
解决 n+1 查询问题的所有好方法都依赖于 FetchType.LAZY
和特定的查询获取。FetchType.LAZY
只告诉 Hibernate 在使用关联时初始化它。当你知道你的业务代码需要初始化关联时,你可以使用特定于查询的获取来有效地读取它。与执行额外的查询来初始化每个关联不同,特定于查询的获取会获得一个具有指定关联列表的实体。因此,当你获取 Order
实体时,你可以告诉 Hibernate 在同一查询中也获取关联的 OrderItem
。
总结
让我们快速总结一下不同的 `FetchType`
。
FetchType.EAGER
告诉 Hibernate 在初始化查询时获取相关联的实体。这看起来非常高效,因为它只需一个查询即可获取所有实体。但在大多数情况下,它会产生巨大的开销,因为即使业务代码不使用这些实体,Hibernate 也会获取它们。
你可以使用 FetchType.LAZY
来防止这种情况。它告诉 Hibernate 延迟关联的初始化,直到你在业务代码中访问它。
这种方法的缺点是 Hibernate 需要执行一个额外的查询来初始化每个关联。这被称为 n+1 检索问题,是性能问题最常见的原因之一。你可以通过使用特定于查询的获取来避免它。两者的结合能够高效地获取每个用例所需的信息。