You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决Hibernate中@Fetch(SUBSELECT)与EntityGraph的多余关联问题

问题描述

我有一个Employee实体,包含多个关联实体,使用@Fetch(FetchMode.SUBSELECT)加载集合数据。但通过带EntityGraph的查询获取Employee时,原查询的所有JOIN会被加入SUBSELECT,该如何解决?尝试在关联实体上添加@BatchSize注解后,多余JOIN消失但查询次数未变,调整size参数也无效果,恳请指导正确的实体设计方案。


实体代码

主实体Employee

@Data
@Entity
@NoArgsConstructor
@Where(clause = "delete_time is null")
@Table(name = "employee")
@NamedEntityGraphs({
    @NamedEntityGraph(name = "withDepartmentManagerAndOffice", attributeNodes = {
            @NamedAttributeNode("department"),
            @NamedAttributeNode("manager"),
            @NamedAttributeNode("office")
    }),
    @NamedEntityGraph(name = "withUserDepartmentManagerAndOffice", attributeNodes = {
            @NamedAttributeNode("user"),
            @NamedAttributeNode("department"),
            @NamedAttributeNode("manager"),
            @NamedAttributeNode("office")
    })
})
public class Employee extends BaseIdentifiableEntity {

@OneToOne(fetch = LAZY)
@JoinColumn(name = "email", foreignKey = @ForeignKey(name = "user_email_pk"), insertable = false, updatable = false)
@ToString.Exclude
private User user;

private String email;
private String lastName;
private String firstName;
private String patronymic;
private String simpleName;//for ldap manager search

private LocalDate birthday;
private String phoneNumber;
private String telegramId;
private byte[] photo;
private String position;
private LocalDate startDate;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "department_id")
@ToString.Exclude
private Department department;

@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "employee")
@ToString.Exclude
private List<Equipment> equipment;

@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "employee")
@ToString.Exclude
private List<Bonus> bonuses;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "manager_id")
@ToString.Exclude
private Employee manager;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "office_id")
@Cascade(CascadeType.SAVE_UPDATE)
@ToString.Exclude
private Office office;

@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "employee")
@ToString.Exclude
private List<Notification> notifications;

@Formula(value = "concat(last_name, ' ', first_name, ' ', patronymic)")
private String fullName;

@Transient
private String managerName;

@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;
    Employee employee = (Employee) o;
    return Objects.equals(email, employee.email) && Objects.equals(lastName, employee.lastName) &&
           Objects.equals(firstName, employee.firstName) && Objects.equals(patronymic, employee.patronymic) &&
           Objects.equals(phoneNumber, employee.phoneNumber) && Objects.equals(telegramId, employee.telegramId) &&
           Objects.equals(position, employee.position) && Arrays.equals(photo, employee.photo) &&
           Objects.equals(startDate, employee.startDate) && Objects.equals(birthday, employee.birthday) &&
           Objects.equals(simpleName, employee.simpleName);
}

@Override
public int hashCode() {
    return Objects.hash(super.hashCode(), email, lastName, firstName, phoneNumber, telegramId,
            Arrays.hashCode(photo), position, department, simpleName, startDate, birthday);
}
}

Equipment实体

@Data
@Entity
@Table(name = "equipment")
@Where(clause = "delete_time is null")
public class Equipment extends NamedWithDescriptionIdentifiableEntity {

private LocalDate attachmentDate;
private String status;
private String inventoryNumber;
private String devTypeDescription;

@Where(clause = "delete_date is null")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "employee_id")
@ToString.Exclude
private Employee employee;

@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;
    Equipment equipment = (Equipment) o;
    return Objects.equals(attachmentDate, equipment.attachmentDate) && Objects.equals(status, equipment.status) &&
           Objects.equals(inventoryNumber, equipment.inventoryNumber) && Objects.equals(devTypeDescription, equipment.devTypeDescription);
}

@Override
public int hashCode() {
    return Objects.hash(super.hashCode(), attachmentDate, status, inventoryNumber, devTypeDescription);
}
}

Bonus实体

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "bonus")
@Where(clause = "delete_time is null")
public class Bonus extends NamedWithDescriptionIdentifiableEntity {

private LocalDate expirationDate;

@Enumerated(EnumType.STRING)
private BonusType type;
private Long quantity;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "employee_id")
@ToString.Exclude
private Employee employee;

@Builder(builderMethodName = "childBuilder")
public Bonus(String name, LocalDate expirationDate, BonusType type, Long quantity, Employee employee) {
    super(name);
    this.expirationDate = expirationDate;
    this.type = type;
    this.quantity = quantity;
    this.employee = employee;
}

@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;
    Bonus bonus = (Bonus) o;
    return Objects.equals(expirationDate, bonus.expirationDate) && type == bonus.type && Objects.equals(quantity, bonus.quantity) && Objects.equals(name, bonus.name);
}

@Override
public int hashCode() {
    return Objects.hash(super.hashCode(), expirationDate, type, quantity, name);
}
}

Repository方法
@EntityGraph("withUserDepartmentManagerAndOffice")
@Query("SELECT e FROM Employee e WHERE e.email= :email")
Optional<Employee> findFullEmployeeByEmail(String email);

解决方案

问题根源

使用EntityGraph时,Hibernate会将EntityGraph中指定的关联查询JOIN条件带入SUBSELECT子查询,这是因为@Fetch(FetchMode.SUBSELECT)会复用主查询的所有条件,而EntityGraph触发的主查询包含额外JOIN,导致子查询被污染。

具体解决步骤

1. 移除全局Fetch策略,改为查询级动态指定

不要在实体类上全局设置@Fetch(FetchMode.SUBSELECT),而是在需要批量加载集合的查询方法上,通过@FetchProfile动态指定:

  • 先修改Employee实体,移除集合上的@Fetch(FetchMode.SUBSELECT):
// 移除@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "employee")
@ToString.Exclude
private List<Equipment> equipment;

// 其他集合关联(bonuses、notifications)同理修改
  • 然后在Repository中定义FetchProfile并绑定查询:
@FetchProfile(name = "employee-with-collections", fetchOverrides = {
    @FetchProfile.FetchOverride(entity = Employee.class, association = "equipment", mode = FetchMode.SUBSELECT),
    @FetchProfile.FetchOverride(entity = Employee.class, association = "bonuses", mode = FetchMode.SUBSELECT),
    @FetchProfile.FetchOverride(entity = Employee.class, association = "notifications", mode = FetchMode.SUBSELECT)
})
@Query("SELECT e FROM Employee e")
List<Employee> findAllWithCollections();
  • 使用时激活FetchProfile:
entityManager.enableFetchProfile("employee-with-collections");
List<Employee> employees = employeeRepository.findAllWithCollections();

2. 为EntityGraph显式排除集合属性

如果当前EntityGraph查询不需要加载集合,可通过excludeAttributes排除集合,避免Hibernate自动处理其Fetch策略:

@NamedEntityGraph(name = "withUserDepartmentManagerAndOffice", 
    attributeNodes = {
        @NamedAttributeNode("user"),
        @NamedAttributeNode("department"),
        @NamedAttributeNode("manager"),
        @NamedAttributeNode("office")
    },
    excludeAttributes = {"equipment", "bonuses", "notifications"} // 显式排除集合
)

3. 正确配置@BatchSize

之前@BatchSize未生效,是因为需要配置在集合属性或关联实体类上,同时开启Hibernate批处理配置:

  • 在Employee的集合属性上添加:
@BatchSize(size = 20)
@OneToMany(mappedBy = "employee")
@ToString.Exclude
private List<Equipment> equipment;
  • 或在Equipment实体类上添加:
@BatchSize(size = 20)
@Entity
@Table(name = "equipment")
@Where(clause = "delete_time is null")
public class Equipment extends NamedWithDescriptionIdentifiableEntity {
    // ...
}
  • 同时在Hibernate配置中开启批处理:
hibernate.jdbc.batch_size=20
hibernate.order_inserts=true
hibernate.order_updates=true

4. 拆分查询,避免混合加载策略

如果上述方案不适用,可拆分查询:先通过EntityGraph加载Employee及其单值关联,再单独查询集合并手动赋值:

Optional<Employee> employeeOpt = employeeRepository.findFullEmployeeByEmail(email);
if(employeeOpt.isPresent()){
    Employee employee = employeeOpt.get();
    // 单独查询集合
    List<Equipment> equipment = equipmentRepository.findByEmployeeId(employee.getId());
    employee.setEquipment(equipment);
    // 同理查询bonuses和notifications
}

实体设计建议

  • 单值关联(@ManyToOne、@OneToOne)保持LAZY加载,通过EntityGraph按需加载
  • 集合关联(@OneToMany)默认LAZY,根据业务场景选择:
    • 批量查询时用@Fetch(FetchMode.SUBSELECT)或@BatchSize
    • 单个查询时直接单独查询集合,避免N+1问题
  • 避免在实体类上全局设置Fetch策略,优先在查询级别动态指定,提升灵活性

内容的提问来源于stack exchange,提问作者KripDo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 01:08:10