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

Hibernate从集合移除实体时生成额外SELECT查询的原因

问题:Spring Data JPA移除Set集合实体时Hibernate生成额外SELECT查询

我正在使用Spring Data JPA,尝试从Set集合中移除实体,但Hibernate生成了超出预期的SELECT查询。

相关代码

CompanyBranch实体

@Entity
@Table(name = "COMPANY_BRANCH")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class CompanyBranch {

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

    @NotNull
    @Convert(converter = MoneyConverter.class)
    @Column(name = "COMPANY_BRANCH_BUDGET", length = 63)
    private Money budget;

    @Embedded
    @NotNull
    @AttributeOverrides({
            @AttributeOverride(name = "city",
                column = @Column(name = "COMPANY_BRANCH_CITY")),
            @AttributeOverride(name = "zipCode",
                column = @Column(name = "COMPANY_BRANCH_ZIP_CODE")),
            @AttributeOverride(name = "street",
                column = @Column(name = "COMPANY_BRANCH_STREET")),
            @AttributeOverride(name = "country",
            column = @Column(name = "COMPANY_BRANCH_COUNTRY"))
    })
    private Address companyBranchAddress;

    @NotNull
    @NotBlank(message = "Номер телефона не должен быть пустым")
    @Pattern(regexp = "^(\\+\\d{1,3}( )?)?((\\(\\d{1,3}\\))|\\d{1,3})[- .]?\\d{3,4}[- .]?\\d{4}$",
            message = "Неверный формат номера!")
    @Column(name = "COMPANY_BRANCH_PHONE_NUMBER")
    private String phoneNumber;


    @OneToMany(mappedBy = "companyBranch", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Department> departments = new HashSet<>();

    public void addDepartment(Department department) {
        this.departments.add(department);
        department.setCompanyBranch(this);
    }

    public void removeDepartment(Department department) {
        department.setCompanyBranch(null);
        departments.remove(department);
    }
}

Department实体

@Entity
@Table(name = "DEPARTMENT")
@Getter
@Setter
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @NotBlank(message = "Название отдела не может быть пустым!")
    @Column(name = "DEPARTMENT_NAME")
    private String departmentName;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "LAST_MODIFIED", insertable = false, updatable = false)
    private Date lastModified;

    @NotNull
    @NotBlank(message = "Номер телефона не должен быть пустым")
    @Pattern(regexp = "^(\\+\\d{1,3}( )?)?((\\(\\d{1,3}\\))|\\d{1,3})[- .]?\\d{3,4}[- .]?\\d{4}$",
            message = "Неверный формат номера!")
    @Column(name = "DEPARTMENT_PHONE_NUMBER")
    private String phoneNumber;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "COMPANY_BRANCH_ID")
    private CompanyBranch companyBranch;

    @OneToMany(mappedBy = "department", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private Set<Employee> employees = new HashSet<>();

    public void addEmployee(Employee employee) {
        this.employees.add(employee);
        employee.setDepartment(this);
    }

    public void removeEmployee(Employee employee) {
        this.employees.remove(employee);
        employee.setDepartment(null);
    }

    @PreUpdate
    @PrePersist
    protected void setDefaultLastModified(){
        lastModified = new Date();
    }

    @PreRemove
    protected void changeEmploysStatus() {
        this.employees.forEach(employee -> employee.getEmployeeStatus().setEmployeeStatus(EEmployeeStatus.FIRED));
    }
}

移除操作代码

CompanyBranch companyBranch = companyBranchService.getCompanyBranchById(companyBranchId);
Department department = departmentService.getDepartmentById(depId);
companyBranchService.removeDepartment(companyBranch, department);

移除方法

public void removeDepartment(CompanyBranch companyBranch, Department department) {
        companyBranch.removeDepartment(department);
        companyBranchRepository.saveAndFlush(companyBranch);
    }

Hibernate生成的查询

Hibernate: 
    select
        c1_0.id,
        c1_0.company_branch_budget,
        c1_0.company_branch_city,
        c1_0.company_branch_country,
        c1_0.company_branch_street,
        c1_0.company_branch_zip_code,
        c1_0.company_branch_phone_number 
    from
        company_branch c1_0 
    where
        c1_0.id=?
Hibernate: 
    select
        d1_0.id,
        d1_0.company_branch_id,
        d1_0.department_name,
        d1_0.last_modified,
        d1_0.department_phone_number 
    from
        department d1_0 
    where
        d1_0.id=?
Hibernate: 
    select
        d1_0.company_branch_id,
        d1_0.id,
        d1_0.department_name,
        d1_0.last_modified,
        d1_0.department_phone_number 
    from
        department d1_0 
    where
        d1_0.company_branch_id=?

疑问

请问Hibernate为何会生成最后这条SELECT查询?是否与save方法有关,或是其他原因?我猜测可能和保存操作相关,但不确定。


解答

最后这条SELECT查询是Hibernate处理orphanRemoval=true时触发的,核心原因是集合懒加载导致的状态校验:

  1. orphanRemoval的工作机制:当你给@OneToMany设置orphanRemoval=true时,Hibernate需要确保从关联集合中移除的实体被彻底删除。为了做到这一点,它需要对比内存中集合的当前状态和数据库中实际存在的关联数据,找出被移除的实体。

  2. 懒加载的影响:你加载CompanyBranch时,departments集合默认是懒加载的(@OneToMany默认fetch类型为LAZY),此时内存中并没有完整的集合数据。当你修改集合并调用saveAndFlush时,Hibernate无法仅通过内存中的部分数据判断哪些实体被移除,因此会执行这条查询,拉取该CompanyBranch关联的所有Department,再和内存中的集合做比对。

  3. 额外查询的触发时机:这条查询是在saveAndFlush执行时触发的,因为Hibernate需要在刷新持久化上下文前完成集合状态的一致性校验,进而确定是否要执行删除操作。

解决建议

如果你想避免这条额外查询,可以提前初始化departments集合:

  • 使用JOIN FETCH查询加载集合:修改getCompanyBranchById方法,通过JOIN FETCH一次性加载CompanyBranch和关联的departments,这样内存中就有完整的集合数据,Hibernate无需再去数据库查询。
    @Query("SELECT cb FROM CompanyBranch cb JOIN FETCH cb.departments WHERE cb.id = :id")
    CompanyBranch getCompanyBranchByIdWithDepartments(@Param("id") Long id);
    
  • 手动初始化集合:在加载CompanyBranch后,调用Hibernate.initialize(companyBranch.getDepartments())强制初始化懒加载集合,同样可以让Hibernate直接使用内存中的数据做比对。

另外,你的removeDepartment方法逻辑是正确的,只要集合被提前初始化,Hibernate就能直接识别出被移除的元素,不会再触发额外的SELECT查询。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 00:24:52