JPA 中的 FilterDef 与伪删除机制:从理念到实战

在企业级应用中,数据删除往往不是"真的删除"。在多数情况下,我们更倾向于**伪删除(Soft Delete)**通过标记字段的方式,将记录逻辑上设为删除状态,而非从数据库物理移除。

本文将从伪删除与真实删除的对比出发,带你深入理解 JPA 中的 @FilterDef@Filter 注解如何优雅地实现伪删除逻辑,同时介绍 Hibernate 6.4+ 引入的 @SoftDelete,帮助你构建更安全、可维护的数据层。

一、伪删除 vs 实际删除

实际删除(Hard 直接执行 简单、节省存储空间 数据无法恢复;外键约束可能被破坏;历史审计困难)

伪删除(Soft 添加一个状态字段(如 deleted 或 可恢复、可审计、安全性高 查询逻辑需额外过滤;容易遗忘过滤导致数据"穿透")

举个例子:
用户表 user 中,有字段 is_deleted
当我们调用"删除用户"接口时,实际执行的 SQL 不是 DELETE,而是:

UPDATE user SET is_deleted = true WHERE id = ?;

同时,我们希望所有查询都自动加上 WHERE is_deleted = false

这正是 @FilterDef 派上用场的地方。

二、Hibernate Filter 的设计思想

Hibernate 提供了一种优雅的机制:动态 SQL 过滤(Dynamic Filtering)

通过 @FilterDef 定义过滤器参数,再通过 @Filter 为实体应用过滤条件。
当过滤器开启后,Hibernate 会自动在生成的 SQL 中附加相应的条件。

优势: - 不需要在每个 Repository 查询中手写过滤条件; - 灵活启用或关闭; - 不影响实体模型结构。

三、FilterDef 实战示例

1. 实体定义

import jakarta.persistence.*;
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;

@Entity
@Table(name = "users")
@FilterDef(
    name = "deletedFilter",
    parameters = @ParamDef(name = "isDeleted", type = Boolean.class)
)
@Filter(
    name = "deletedFilter",
    condition = "is_deleted = :isDeleted"
)
public class User {

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

    private String username;

    @Column(name = "is_deleted")
    private Boolean isDeleted = false;

    // getter/setter
}

2. 启用过滤器

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.hibernate.Session;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {

    private final EntityManager entityManager;

    public List<User> findAllActiveUsers() {
        Session session = entityManager.unwrap(Session.class);
        session.enableFilter("deletedFilter")
               .setParameter("isDeleted", false);

        List<User> users = session.createQuery("from User", User.class).list();

        session.disableFilter("deletedFilter");
        return users;
    }

    public void softDelete(Long userId) {
        Session session = entityManager.unwrap(Session.class);
        User user = session.get(User.class, userId);
        if (user != null) {
            user.setIsDeleted(true);
            session.merge(user);
        }
    }
}

💡 使用 autoEnabled 自动启用过滤器

Hibernate 6+ 支持 autoEnabled 属性,可以自动激活过滤器。

@Entity
@Table(name = "users")
@FilterDef(
    name = "deletedFilter",
    parameters = @ParamDef(name = "isDeleted", type = Boolean.class)
)
@Filter(
    name = "deletedFilter",
    condition = "is_deleted = :isDeleted",
    autoEnabled = true
)
public class User {
    ...
}

无需再手动启用 session.enableFilter(),Hibernate 会自动附加过滤条件。


💡 组合使用伪删除与多租户 Filter

@Entity
@Table(name = "users")
@FilterDef(
    name = "deletedFilter",
    parameters = @ParamDef(name = "isDeleted", type = Boolean.class)
)
@Filter(
    name = "deletedFilter",
    condition = "is_deleted = :isDeleted",
    autoEnabled = true
)
@FilterDef(
    name = "tenantFilter",
    parameters = @ParamDef(name = "tenantId", type = Long.class)
)
@Filter(
    name = "tenantFilter",
    condition = "tenant_id = :tenantId",
    autoEnabled = true
)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    @Column(name = "tenant_id")
    private Long tenantId;

    @Column(name = "is_deleted")
    private Boolean isDeleted = false;

    // getter/setter
}
Session session = entityManager.unwrap(Session.class);
session.enableFilter("deletedFilter").setParameter("isDeleted", false);
session.enableFilter("tenantFilter").setParameter("tenantId", tenantId);

Hibernate 自动生成:

select * from users where is_deleted = false and tenant_id = 1001;

四、BaseEntity 统一管理伪删除与租户过滤

我们可以将伪删除与租户字段统一放在 BaseEntity 中。

@MappedSuperclass
@Filter(name = "deletedFilter")
@Filter(name = "tenantFilter")
public abstract class BaseEntity {

    @Column(name = "is_deleted", nullable = false)
    protected Boolean isDeleted = false;

    @Column(name = "tenant_id", nullable = false)
    protected Long tenantId;

    // getters/setters
}

package-info.java

@FilterDef(
    name = "deletedFilter",
    condition = "is_deleted = false",
    autoEnabled = true
)
@FilterDef(
    name = "tenantFilter",
    parameters = @ParamDef(name = "tenantId", type = Long.class),
    condition = "tenant_id = :tenantId",
    autoEnabled = true
)
package com.demo.domain;

这样所有继承自 BaseEntity 的实体都自动具备伪删除与多租户过滤。

五、Hibernate 6.4+ 的 @SoftDelete

Hibernate 6.4 开始提供了官方注解 @SoftDelete,用于自动管理软删除:

import org.hibernate.annotations.SoftDelete;
import jakarta.persistence.*;

@Entity
@Table(name = "company_base_grade")
@SoftDelete // 默认 deleted=true 代表已删除
public class CompanyBaseGrade {

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

    @ManyToOne(fetch = FetchType.LAZY)
    private Company company;

    private Long gradeId;
    private Short type;
    private Short state;
}

优点

  • 无需手动定义 isDeleted 字段或过滤器;
  • delete() 自动改写为 UPDATE ... SET deleted=true
  • 所有 JPQL 查询自动附加 deleted=false

注意

  • 若需查询包含已删除数据,必须使用 nativeQuery=true,这个其实是使用原生的 SQL 进行查询;
  • 若需"恢复",使用普通 UPDATEdeleted=false
  • 若需多租户或复杂条件,仍建议使用 FilterDef 机制。

六、Spring Data JPA 下的实践

在 Spring Data JPA 中,直接使用 Repository 也可与 @SoftDelete 无缝结合:

@Repository
public interface CompanyBaseGradeRepository<GRADE extends CompanyBaseGrade>
        extends JpaRepository<GRADE, Long> {

    @Query("from CompanyBaseGrade WHERE company = ?1 AND gradeId = ?2 AND state = 1")
    Optional<GRADE> findByCompanyAndId(Company company, Long gradeId);

    Integer findMaxLevelByCompanyAndType(Company company, Short type);

    // 查询包含已删除
    @Query(value = "SELECT * FROM company_base_grade WHERE company_id = :companyId", nativeQuery = true)
    List<Map<String, Object>> findAllRowsByCompany(@Param("companyId") Long companyId);

    // 恢复
    @Modifying
    @Query(value = "UPDATE company_base_grade SET deleted = false WHERE id = :id", nativeQuery = true)
    int restoreById(@Param("id") Long id);
}

七、何时使用 SoftDelete,何时使用 Filter

场景 推荐方案

只需默认隐藏删除数据 @SoftDelete 需要可开关过滤、多租户 @FilterDef + @Filter 想集中管理所有实体过滤 BaseEntity + package-info + autoEnabled 需要访问被删数据 原生 SQL 查询

八、总结

  • 对核心业务数据(用户、订单、资金等),推荐伪删除 + FilterDef
  • 对非核心或简单表,可以直接使用 @SoftDelete
  • 若系统有多租户、审计等需求,优先设计通用的 BaseEntity + package-info 全局过滤方案
  • Spring Data JPA 场景下,可透明兼容上述两种模式。

参考资料