为什么 Spring JPA 仍然检索与 FetchMode.LAZY 关联的实体?

Why Spring JPA still retrieves associated entities with FetchMode.LAZY?

我有 Account 实体,里面有 3 个关联实体,配置如下:

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )

我还有 JpaRepository 方法

  Optional<Account> findByEmail(String email);

执行 findByEmail 时,它会使用分隔的 SELECT 子句查询帐户内的所有实体。 为什么会这样? 我不在服务逻辑中使用吸气剂,它恰好发生在代码中:

Account account = accountRepository.findByEmail(email).orElseThrow(
        () -> new ResourceNotFoundException("Account with %s email is not found", email));

父实体:

@Entity
@Table(name = "account")
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@Builder(toBuilder = true)
@Getter
public class Account extends BaseEntity {

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

  @Column(name = "encoded_password")
  @Setter
  private String password;

  @Column(name = "email")
  @Setter
  private String email;

  @Column(name = "first_name")
  @Setter
  private String firstName;

  @Column(name = "last_name")
  @Setter
  private String lastName;

  @Column(name = "avatar_file_name")
  private String avatarFileName;

  @Column(name = "last_logged_in_time")
  private LocalDateTime lastLoggedInTime;

  @Column(name = "role", updatable = false)
  @Enumerated(EnumType.STRING)
  private AccountRoleEnum role;

  @Column(name = "status")
  @Enumerated(EnumType.STRING)
  @Setter
  private AccountStatusEnum status;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private Social social;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private Location location;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private PaymentInfo paymentInfo;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private Activation activation;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private Company Company;

  @OneToMany(
      fetch = FetchType.LAZY,
      mappedBy = "account",
      cascade = CascadeType.ALL
  )
  @ToString.Exclude
  private final Set<Resume> resumeSet = new HashSet<>();

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;

    Account account = (Account) o;
    return Objects.equals(id, account.id)
        && Objects.equals(password, account.password)
        && Objects.equals(email, account.email)
        && Objects.equals(firstName, account.firstName)
        && Objects.equals(lastName, account.lastName)
        && Objects.equals(avatarFileName, account.avatarFileName)
        && Objects.equals(lastLoggedInTime, account.lastLoggedInTime)
        && role == account.role
        && status == account.status
        && Objects.equals(social, account.social)
        && Objects.equals(location, account.location)
        && Objects.equals(paymentInfo, account.paymentInfo)
        && Objects.equals(activation, account.activation)
        && Objects.equals(Company, account.Company)
        && Objects.equals(resumeSet, account.resumeSet);
  }

  @Override
  public int hashCode() {
    return Objects.hash(super.hashCode(),
        id, password, email, firstName, lastName, avatarFileName, lastLoggedInTime,
        role, status, social, location, paymentInfo, activation, Company, resumeSet);
  }
}

子实体:

@Entity
@Table(name = "location")
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@Builder(toBuilder = true)
@Getter
public class Location extends BaseEntity {

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

  @Column(name = "country")
  private String country;

  @Column(name = "city")
  private String city;

  @OneToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "account_id", updatable = false)
  @ToString.Exclude
  private Account account;

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;
    Location location = (Location) o;
    return Objects.equals(id, location.id)
        && Objects.equals(country, location.country)
        && Objects.equals(city, location.city)
        && Objects.equals(account, location.account);
  }

  @Override
  public int hashCode() {
    return Objects.hash(super.hashCode(), id, country, city, account);
  }
}

有一件事绝对是个问题

private final Set<Resume> resumeSet = new HashSet<>();

在您的父实体中。由于您使用的是 Set,因此 Java 必须计算内部元素的唯一性。这通常是通过调用子对象内的 equals() 方法来完成的。然而,这会导致(子)实体及其所有子实体被加载(取决于“等于”实现)。一个简单的解决方法是使用列表而不是集合(如果可能)。

您的 equals() 和 hashCode() 函数仍在使用子实体(例如位置)。根据父实体的调用,调用 equals() 或 hashCode() 函数并加载 Chiulds。

使用调试器可以“操纵”您的 sql 提取的结果。当调用调试器内的断点时,IDE 通常会调用 toString 方法来显示调试器内的对象 window。这也可以触发加载子项,因为它们在 toString 实现中是必需的。

你有 bi-direction One-To-One 关系。默认情况下,Hibernate 忽略双向 One-To-One 父端的获取策略,但它正确地将其应用于其他关联(Many-To-OneOne-To-ManyMany-To-Many、单向 One-To-OneElementCollection)。
并且,除非您使用字节码增强,否则您应该避免双向关联。
What is the difference between Unidirectional and Bidirectional JPA and Hibernate associations?


解决方案 1:改用 Many-To-One 关系

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "account_id")
  @ToString.Exclude
  private Account account;

方案二:Bytecode Enhancement
使用增强实体字节码的字节码增强插件 类 并允许我们利用 No-proxy 延迟获取策略

<build>
<plugins>
    <plugin>
        <groupId>org.hibernate.orm.tooling</groupId
        <artifactId>hibernate-enhance-maven-plugin</artifactId>
        <version>${hibernate.version}</version>
        <executions>
        <execution>
        <configuration>
           <enableLazyInitialization>true</enableLazyInitialization>         
        </configuration>
        <goals>
            <goal>enhance</goal>
        </goals>
        </execution>
        </executions>
    </plugin>
</plugins>
</build>

在实体 类 中添加 @LazyToOne 注释,让 hibernate 知道我们要为关联实体启用无代理延迟获取。

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  @LazyToOne(LazyToOneOption.NO_PROXY)
  private Location location;

添加到配置:

hibernate.ejb.use_class_enhancer=true

在 Hibernate 5.5 之前,您必须将 @LazyToOne(LazyToOneOption.NO_PROXY) 添加到 @OneToOne(fetch=FetchType.LAZY, mappedBy="x")

从Hibernate 5.5开始,不再需要,只需启用字节码增强。
请注意在 Hibenrate 5.5 之前启用字节码增强可能会导致副作用:
HHH-13134 – JOIN FETCH 不能与增强实体一起正常工作
HHH-14450 – 删除禁用“增强代理”的能力

解决方案 3:强制关联关系
此解决方案仅供参考。您不应该仅仅为了延迟加载而更改实体限制

@OneToOne(optional = false, fetch = FetchType.LAZY)

详情:Hibernate: one-to-one lazy loading, optional = false
请注意, 可选技巧并不适用于每个版本的 Hibernate,因此如果您升级它可能会损坏。它还对关系进行了额外的 Not-Null 限制。