Contents

Hibernate @LazyCollection注解简介

1. 概述

管理应用程序中的 SQL 语句是我们需要注意的最重要的事情之一,因为它对性能有巨大影响。在处理对象之间的关系时,有两种主要的获取设计模式。第一个是懒惰的方法,而另一个是Eager的方法。

在本文中,我们将对它们进行概述。此外,我们将讨论 Hibernate 中的*@LazyCollection*注解。

2. Lazy获取

当我们想推迟数据初始化直到我们需要它时,我们使用Lazy加载 。让我们看一个例子来更好地理解这个想法。

假设我们有一家在城市有多个分支机构的公司。每个分支机构都有自己的员工。从数据库的角度来看,这意味着我们在分支机构与其员工之间存在一对多的关系。

在惰性获取方法中,一旦获取分支对象,我们就不会获取员工。我们只获取分支对象的数据,我们推迟加载员工列表,直到我们调用*getEmployees()*方法。此时,将执行另一个数据库查询以获取员工。

这种方法的好处是我们减少了最初加载的数据量。原因是我们可能不需要分支机构的员工,加载它们没有意义,因为我们不打算立即使用它们。

3. Eager获取

**当需要立即加载数据时,我们使用Eager获取。**让我们以公司、分支机构和员工的相同示例来解释这个想法。一旦我们从数据库中加载了一些分支对象,我们将立即使用相同的数据库查询加载其员工列表。

使用 Eager获取时的主要问题是我们加载了大量可能不需要的数据。因此,我们应该只在我们确定一旦我们加载它的对象时总是使用Eager获取的数据时才使用它。

4. @LazyCollection注解

当我们需要处理应用程序的性能时,我们会使用*@LazyCollection注释。从 Hibernate 3.0 开始,@LazyCollection默认启用。*使用@LazyCollection的主要思想是控制数据的获取应该使用惰性方法还是Eager方法。**

使用*@LazyCollection时,我们为LazyCollectionOption设置提供了三个配置选项:TRUEFALSEEXTRA*。让我们分别讨论它们。

4.1. 使用LazyCollectionOption.TRUE

此选项为指定字段启用延迟获取方法,并且是从 Hibernate 版本 3.0 开始的默认设置。因此,我们不需要显式设置此选项。但是,为了更好地解释这个想法,我们将举一个设置此选项的示例。

在此示例中,我们有一个Branch实体,该实体由idname和与Employee实体的*@OneToMany关系组成。我们可以注意到我们在这个例子中明确地将@LazyCollection选项设置为true*:

@Entity
public class Branch {
    @Id
    private Long id;
    private String name;
    @OneToMany(mappedBy = "branch")
    @LazyCollection(LazyCollectionOption.TRUE)
    private List<Employee> employees;
    
    // getters and setters
}

现在,让我们看一下由idnameaddress以及与Branch实体的*@ManyToOne关系组成的Employee*实体:

@Entity
public class Employee {
    @Id
    private Long id;
    private String name;
    private String address;
    
    @ManyToOne
    @JoinColumn(name = "BRANCH_ID") 
    private Branch branch; 
    // getters and setters 
}

在上面的例子中,当我们得到一个分支对象时,我们不会立即加载员工列表。相反,这个操作将被推迟,直到我们调用*getEmployees()*方法。

4.2. 使用LazyCollectionOption.FALSE

当我们将此选项设置为FALSE时,我们启用了Eager获取方法。在这种情况下,我们需要显式指定此选项,因为我们将覆盖 Hibernate 的默认值。让我们看另一个例子。

在本例中,我们有Branch实体,其中包含idname以及与Employee实体的*@OneToMany关系。请注意,我们将@LazyCollection的选项设置为FALSE*:

@Entity
public class Branch {
    @Id
    private Long id;
    private String name;
    @OneToMany(mappedBy = "branch")
    @LazyCollection(LazyCollectionOption.FALSE)
    private List<Employee> employees;
    
    // getters and setters
}

在上面的例子中,当我们得到一个分支对象时,我们会立即加载包含员工列表的分支

4.3. 使用LazyCollectionOption.EXTRA

有时,我们只关心集合的属性,并不马上需要其中的对象。

例如,回到BranchEmployee的例子,我们可以只需要 branch 的员工数量,而不关心实际员工的实体。在这种情况下,我们考虑使用EXTRA选项。让我们更新我们的示例来处理这种情况。

与之前的情况类似,Branch实体与Employee实体有一个idname和一个*@OneToMany关系。但是,我们将@LazyCollection的选项设置为EXTRA*:

@Entity
public class Branch {
    @Id
    private Long id;
    private String name;
    @OneToMany(mappedBy = "branch")
    @LazyCollection(LazyCollectionOption.EXTRA)
    @OrderColumn(name = "order_id")
    private List<Employee> employees;
    // getters and setters
    
    public Branch addEmployee(Employee employee) {
        employees.add(employee);
        employee.setBranch(this);
        return this;
    }
}

我们注意到在这种情况下我们使用了@OrderColumn注解。原因是EXTRA选项仅考虑索引列表集合。这意味着如果我们没有使用@OrderColumn*注解该字段,EXTRA选项将为我们提供与惰性相同的行为,并且在第一次访问时将获取集合。*

此外,我们还定义了addEmployee()方法,因为我们需要从两侧同步BranchEmployee 。如果我们添加一个新Employee并为他设置一个分支,我们也需要更新Branch实体中的员工列表。

现在,当持久化一个具有三个关联员工的Branch实体时,我们需要将代码编写为:

entityManager.persist(
  new Branch().setId(1L).setName("Branch-1")
    .addEmployee(
      new Employee()
        .setId(1L)
        .setName("Employee-1")
        .setAddress("Employee-1 address"))
  
    .addEmployee(
      new Employee()
        .setId(2L)
        .setName("Employee-2")
        .setAddress("Employee-2 address"))
  
    .addEmployee(
      new Employee()
        .setId(3L)
        .setName("Employee-3")
        .setAddress("Employee-3 address"))
);

如果我们看一下执行的查询,我们会注意到 Hibernate 将首先为 Branch-1 插入一个新的分支。然后它将插入 Employee-1、Employee-2 和 Employee-3。

我们可以看到这是一种自然的行为。但是,EXTRA选项中的不良行为是,在刷新上述查询之后,它将执行三个额外的查询——我们添加的每个Employee一个:

UPDATE EMPLOYEES
SET
    order_id = 0
WHERE
    id = 1
     
UPDATE EMPLOYEES
SET
    order_id = 1
WHERE
    id = 2
 
UPDATE EMPLOYEES
SET
    order_id = 2
WHERE
    id = 3

执行UPDATE语句来设置列表条目索引。这是一个所谓的 N +1 查询问题 的示例,这意味着我们执行N个额外的 SQL 语句来更新我们创建的相同数据。

正如我们从示例中注意到的,使用EXTRA选项时可能会遇到 N +1 查询问题。

另一方面,使用此选项的好处是当我们需要获取每个分支的员工列表大小时:

int employeesCount = branch.getEmployees().size();

当我们调用这个语句时,它只会执行这个 SQL 语句:

SELECT
    COUNT(ID)
FROM
    EMPLOYEES
WHERE
    BRANCH_ID = :ID

正如我们所见,我们不需要将员工列表存储在内存中即可获得其大小。尽管如此,我们还是建议避免使用EXTRA选项,因为它会执行额外的查询。

这里还值得注意的是,使用其他数据访问技术可能会遇到N +1 查询问题,因为它不仅限于 JPA 和 Hibernate。