如何用Spring Boot 3 + Hibernate 6实现Product数据可选过滤?
Spring Boot 3 + Hibernate 6 实现Product可选字段过滤的最佳实践
网传Hibernate 6无法使用Criteria API是错误的——它完全支持JPA Criteria API,同时还有多种更灵活的方案可选。下面针对你的Product实体,给出几种落地性强的可选字段过滤实践:
方案1:JPA Criteria API(原生支持,无额外依赖)
先定义一个过滤参数类,用来接收前端传来的可选过滤条件:
public class ProductFilter { private String title; private BigDecimal minPrice; private BigDecimal maxPrice; private Boolean featured; private Long brandId; private Long categoryId; private StatusZeroAndOne status; // 省略getter、setter }
然后在Repository实现类中,用CriteriaBuilder动态构建查询:
@Repository public class ProductCustomRepositoryImpl { @PersistenceContext private EntityManager entityManager; public List<Product> findProductsByFilter(ProductFilter filter) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Product> cq = cb.createQuery(Product.class); Root<Product> productRoot = cq.from(Product.class); List<Predicate> predicates = new ArrayList<>(); // 标题模糊匹配(忽略大小写) if (filter.getTitle() != null && !filter.getTitle().isBlank()) { predicates.add(cb.like(cb.lower(productRoot.get("title")), "%" + filter.getTitle().toLowerCase() + "%")); } // 价格区间过滤 if (filter.getMinPrice() != null) { predicates.add(cb.greaterThanOrEqualTo(productRoot.get("price"), filter.getMinPrice())); } if (filter.getMaxPrice() != null) { predicates.add(cb.lessThanOrEqualTo(productRoot.get("price"), filter.getMaxPrice())); } // 精选商品过滤 if (filter.getFeatured() != null) { predicates.add(cb.equal(productRoot.get("featured"), filter.getFeatured())); } // 品牌ID匹配 if (filter.getBrandId() != null) { predicates.add(cb.equal(productRoot.get("brand").get("id"), filter.getBrandId())); } // 分类ID匹配 if (filter.getCategoryId() != null) { predicates.add(cb.equal(productRoot.get("category").get("id"), filter.getCategoryId())); } // 状态匹配 if (filter.getStatus() != null) { predicates.add(cb.equal(productRoot.get("status"), filter.getStatus())); } cq.where(predicates.toArray(new Predicate[0])); return entityManager.createQuery(cq).getResultList(); } }
方案2:Spring Data JPA Specification(优雅的动态查询封装)
让你的ProductRepository继承JpaSpecificationExecutor:
public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> { }
编写Specification工具类,封装过滤逻辑:
public class ProductSpecifications { public static Specification<Product> withFilter(ProductFilter filter) { return (root, query, cb) -> { List<Predicate> predicates = new ArrayList<>(); if (filter.getTitle() != null && !filter.getTitle().isBlank()) { predicates.add(cb.like(cb.lower(root.get("title")), "%" + filter.getTitle().toLowerCase() + "%")); } if (filter.getMinPrice() != null) { predicates.add(cb.greaterThanOrEqualTo(root.get("price"), filter.getMinPrice())); } if (filter.getMaxPrice() != null) { predicates.add(cb.lessThanOrEqualTo(root.get("price"), filter.getMaxPrice())); } if (filter.getFeatured() != null) { predicates.add(cb.equal(root.get("featured"), filter.getFeatured())); } if (filter.getBrandId() != null) { predicates.add(cb.equal(root.get("brand").get("id"), filter.getBrandId())); } if (filter.getCategoryId() != null) { predicates.add(cb.equal(root.get("category").get("id"), filter.getCategoryId())); } if (filter.getStatus() != null) { predicates.add(cb.equal(root.get("status"), filter.getStatus())); } return cb.and(predicates.toArray(new Predicate[0])); }; } }
在Service中直接调用:
// 注入ProductRepository @Autowired private ProductRepository productRepository; public List<Product> getProducts(ProductFilter filter) { return productRepository.findAll(ProductSpecifications.withFilter(filter)); }
方案3:Querydsl(类型安全的动态查询)
Querydsl能避免硬编码字段名带来的错误,提供编译期类型检查。
第一步:添加依赖(Maven)
<dependencies> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>5.0.0</version> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>5.0.0</version> <scope>provided</scope> </dependency> </dependencies>
第二步:配置APT插件生成Q类
在pom.xml的build节点下添加插件:
<build> <plugins> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> </plugins> </build>
执行mvn compile后,会自动生成QProduct类。
第三步:编写查询逻辑
让ProductRepository继承QuerydslJpaRepository:
public interface ProductRepository extends JpaRepository<Product, Long>, QuerydslJpaRepository<Product, Long> { }
然后在Service中实现过滤:
@Autowired private ProductRepository productRepository; public List<Product> getProducts(ProductFilter filter) { QProduct product = QProduct.product; BooleanBuilder builder = new BooleanBuilder(); if (filter.getTitle() != null && !filter.getTitle().isBlank()) { builder.and(product.title.toLowerCase().contains(filter.getTitle().toLowerCase())); } if (filter.getMinPrice() != null) { builder.and(product.price.goe(filter.getMinPrice())); } if (filter.getMaxPrice() != null) { builder.and(product.price.loe(filter.getMaxPrice())); } if (filter.getFeatured() != null) { builder.and(product.featured.eq(filter.getFeatured())); } if (filter.getBrandId() != null) { builder.and(product.brand.id.eq(filter.getBrandId())); } if (filter.getCategoryId() != null) { builder.and(product.category.id.eq(filter.getCategoryId())); } if (filter.getStatus() != null) { builder.and(product.status.eq(filter.getStatus())); } return productRepository.findAll(builder); }
方案4:动态JPQL拼接(适合熟悉JPQL的场景)
直接拼接JPQL语句,但必须用参数绑定防止SQL注入:
@Repository public class ProductCustomRepositoryImpl { @PersistenceContext private EntityManager entityManager; public List<Product> findProductsByFilter(ProductFilter filter) { StringBuilder jpql = new StringBuilder("SELECT p FROM Product p WHERE 1=1"); Map<String, Object> params = new HashMap<>(); if (filter.getTitle() != null && !filter.getTitle().isBlank()) { jpql.append(" AND LOWER(p.title) LIKE :title"); params.put("title", "%" + filter.getTitle().toLowerCase() + "%"); } if (filter.getMinPrice() != null) { jpql.append(" AND p.price >= :minPrice"); params.put("minPrice", filter.getMinPrice()); } if (filter.getMaxPrice() != null) { jpql.append(" AND p.price <= :maxPrice"); params.put("maxPrice", filter.getMaxPrice()); } if (filter.getFeatured() != null) { jpql.append(" AND p.featured = :featured"); params.put("featured", filter.getFeatured()); } if (filter.getBrandId() != null) { jpql.append(" AND p.brand.id = :brandId"); params.put("brandId", filter.getBrandId()); } if (filter.getCategoryId() != null) { jpql.append(" AND p.category.id = :categoryId"); params.put("categoryId", filter.getCategoryId()); } if (filter.getStatus() != null) { jpql.append(" AND p.status = :status"); params.put("status", filter.getStatus()); } TypedQuery<Product> query = entityManager.createQuery(jpql.toString(), Product.class); params.forEach(query::setParameter); return query.getResultList(); } }
方案选择建议
- 追求标准JPA兼容性,无额外依赖:选JPA Criteria API或Spring Data Specification
- 看重类型安全,避免字段名硬编码错误:选Querydsl
- 熟悉JPQL语法,喜欢手写查询语句:选动态JPQL(务必用参数绑定)
内容的提问来源于stack exchange,提问作者ttarikose
相关产品推荐
相关产品推荐

