如何为所有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,拦截查询构建过程,自动添加软删除条件:
- 自定义
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); } } }
- 配置自定义
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统一添加软删除条件:
- 定义软删除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")); } }
- 自定义仓库基类,重写默认查询方法并注入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方法也可通过默认方法自动添加条件 }
- 业务仓库直接继承
SoftDeleteJpaRepository即可生效。
方案3:数据库视图过滤(只读场景首选)
创建过滤已删除数据的视图,实体类映射到视图上,所有查询自动过滤:
CREATE VIEW active_users AS SELECT * FROM user WHERE deleted_at IS NULL;
这种方式无需修改代码,但仅适合只读场景,因为视图通常不支持写入操作。
内容的提问来源于stack exchange,提问作者sharat87

