1. 概述

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

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

2. 惰性获取

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

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

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

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

3. 渴望获取

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

使用急切获取时的主要问题是我们加载了大量可能不需要的数据。因此,只有当我们确定一旦加载其对象后,急切获取的数据将始终被使用时,我们才应该使用它。

4. @LazyCollection 注解

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

使用 @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
}

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

@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 时,我们启用急切获取方法。在这种情况下,我们需要显式指定此选项,因为我们将覆盖 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 的例子,我们可能只需要分支中的员工数量,而不关心实际员工的实体。在这种情况下,我们考虑使用 EXTRA 选项。让我们更新我们的示例来处理这种情况。

与之前的情况类似, Branch 实体具有 idname 以及与 Employee 实体的 @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 。如果我们添加一个新 员工 并为他设置一个分支,我们还需要更新 分支 实体内的员工列表。

现在,当持久保存具有三个关联员工的一个 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。

5. 结论

在本文中,我们讨论了使用 Hibernate 从数据库获取对象属性的不同方法。

首先,我们通过一个例子讨论了延迟获取。然后,我们更新了示例以使用急切获取并讨论了差异。

最后,我们展示了一种额外的获取数据的方法,并解释了它的优点和缺点。

与往常一样,本文中提供的代码可以在 GitHub 上获取。