编程

FetchType: Hibernate & JPA 的 Lazy/Eager 加载

329 2024-07-28 02:31:00

在定义实体映射时,选择正确的 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 在检索根实体时获取关联的实体。正如我之前解释的那样,这是默认的一对一关联。你可以在以下代码中看到它的效果。

我在 OrderItemProduct 实体之间的 @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,只有在你首次使用关联时才只从数据库中获取关联的实体。一般来说,这是一个好主意,因为没有理由选择不在业务代码中使用的实体。你可以在以下代码中看到一个延迟获取关联的示例。

OrderOrderItem 实体之间的 @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 检索问题,是性能问题最常见的原因之一。你可以通过使用特定于查询的获取来避免它。两者的结合能够高效地获取每个用例所需的信息。