为什么 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-One
、One-To-Many
、Many-To-Many
、单向 One-To-One
和 ElementCollection
)。
并且,除非您使用字节码增强,否则您应该避免双向关联。
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
限制。
我有 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-One
、One-To-Many
、Many-To-Many
、单向 One-To-One
和 ElementCollection
)。
并且,除非您使用字节码增强,否则您应该避免双向关联。
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
限制。