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

如何为所有Spring JPA Repository方法添加通用查询条件?

需求背景与问题

我们的目标是什么?

Spring Data JPA允许我们定义如下仓库方法:

Optional<User> findByEmail(String email);

框架会根据方法名称自动生成执行的查询语句,比如此方法对应的语句类似select * from user where email = ?。

我们希望为仓库中所有此类find...By方法添加一个额外的通用条件:and deleted_at is null。本质上,要让findByEmail的行为等效于findByEmailAndDeletedAtNull。

为什么要实现这个需求?

此前我们在响应式Spring Data MongoDB中,通过继承ReactivePartTreeMongoQuery并重写createQuery方法实现了类似功能,代码如下:

@Override
protected Mono<Query> createQuery(ConvertingParameterAccessor accessor) {
    return super.createQuery(accessor).map(query -> {
        query.addCriteria(where("deletedAt").is(null));
        return query;
    });
}

该方案运行良好。现在我们迁移到关系型数据库(RDBMS),需要类似的实现方式。

我们已尝试的方法?

我们发现PartTreeJpaQuery.QueryPreparer.createQuery()方法会基于CriteriaQuery调用EntityManager.createQuery,并返回TypedQuery对象。

我们尝试介入这个流程,但无法像操作CriteriaQuery那样给TypedQuery添加额外条件,也无法访问QueryPreparer对象进行扩展。此外,PartTreeJpaQuery类没有公共构造函数,无法直接继承扩展。

请问是否存在可行的实现方案?


可行实现方案

方案1:拦截CriteriaQuery构建流程(贴近MongoDB实现思路)

通过自定义PartTreeJpaQuery子类和RepositoryQueryFactory,拦截查询构建过程,自动添加软删除条件:

  1. 自定义PartTreeJpaQuery子类(通过反射调用父类包级私有构造),替换内部QueryPreparer逻辑:
public class CustomPartTreeJpaQuery extends PartTreeJpaQuery {

    // 通过反射调用父类构造
    public CustomPartTreeJpaQuery(Method method, RepositoryMetadata metadata, EntityManager em,
                                  JpaQueryMethod queryMethod, EvaluationContextProvider evaluationContextProvider,
                                  SpelExpressionParser parser) {
        super(method, metadata, em, queryMethod, evaluationContextProvider, parser);
    }

    @Override
    protected QueryPreparer createQueryPreparer() {
        return new CustomQueryPreparer(super.createQueryPreparer(), getEntityManager());
    }

    private static class CustomQueryPreparer implements QueryPreparer {

        private final QueryPreparer delegate;
        private final EntityManager em;

        public CustomQueryPreparer(QueryPreparer delegate, EntityManager em) {
            this.delegate = delegate;
            this.em = em;
        }

        @Override
        public TypedQuery<?> createQuery() {
            CriteriaQuery<?> criteriaQuery = delegate.getCriteriaQuery();
            Root<?> root = criteriaQuery.getRoots().iterator().next();
            // 添加deleted_at is null条件
            CriteriaBuilder cb = em.getCriteriaBuilder();
            Predicate existingPredicate = criteriaQuery.getRestriction();
            Predicate softDeletePredicate = cb.isNull(root.get("deletedAt"));
            
            if (existingPredicate != null) {
                criteriaQuery.where(cb.and(existingPredicate, softDeletePredicate));
            } else {
                criteriaQuery.where(softDeletePredicate);
            }
            return em.createQuery(criteriaQuery);
        }

        @Override
        public CriteriaQuery<?> getCriteriaQuery() {
            return delegate.getCriteriaQuery();
        }

        @Override
        public TypedQuery<?> countQuery() {
            CriteriaQuery<?> countQuery = delegate.countQuery().getCriteriaQuery();
            Root<?> root = countQuery.getRoots().iterator().next();
            CriteriaBuilder cb = em.getCriteriaBuilder();
            Predicate existingPredicate = countQuery.getRestriction();
            Predicate softDeletePredicate = cb.isNull(root.get("deletedAt"));
            
            if (existingPredicate != null) {
                countQuery.where(cb.and(existingPredicate, softDeletePredicate));
            } else {
                countQuery.where(softDeletePredicate);
            }
            return em.createQuery(countQuery);
        }
    }
}
  1. 配置自定义RepositoryQueryFactory替换默认实现:
@Configuration
public class JpaConfig extends JpaRepositoryConfigExtension {

    @Override
    protected RepositoryQueryFactory getRepositoryQueryFactory(EntityManager em,
                                                              QueryMethodEvaluationContextProvider evaluationContextProvider,
                                                              SpelExpressionParser parser) {
        return new RepositoryQueryFactory() {
            @Override
            public RepositoryQuery getQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) {
                JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, getBean(JpaMetamodelMappingContext.class));
                if (queryMethod.isQueryAnnotationPresent()) {
                    return new SimpleJpaQuery(queryMethod, em, evaluationContextProvider, parser);
                } else {
                    return new CustomPartTreeJpaQuery(method, metadata, em, queryMethod, evaluationContextProvider, parser);
                }
            }
        };
    }
}

方案2:使用Specification统一过滤(更简洁)

让仓库继承JpaSpecificationExecutor,通过全局Specification统一添加软删除条件:

  1. 定义软删除Specification:
public class SoftDeleteSpecification<T> implements Specification<T> {
    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        return cb.isNull(root.get("deletedAt"));
    }
}
  1. 自定义仓库基类,重写默认查询方法并注入Specification:
@NoRepositoryBean
public interface SoftDeleteJpaRepository<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {

    @Override
    default Optional<T> findById(ID id) {
        return findOne((root, query, cb) -> cb.and(
                cb.equal(root.get("id"), id),
                cb.isNull(root.get("deletedAt"))
        ));
    }

    @Override
    default List<T> findAll() {
        return findAll(new SoftDeleteSpecification<>());
    }

    // 覆盖其他需要过滤的默认方法,自定义find...By方法也可通过默认方法自动添加条件
}
  1. 业务仓库直接继承SoftDeleteJpaRepository即可生效。

方案3:数据库视图过滤(只读场景首选)

创建过滤已删除数据的视图,实体类映射到视图上,所有查询自动过滤:

CREATE VIEW active_users AS SELECT * FROM user WHERE deleted_at IS NULL;

这种方式无需修改代码,但仅适合只读场景,因为视图通常不支持写入操作。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 01:35:28