Spring Boot如何结合原生查询使用Criteria Builder或JPA Specifications做后端过滤?
结合Spring Boot实现PrimeNG表格懒加载的后端数据过滤方案
针对你用Angular+PrimeNG懒加载表格、后端已有原生查询的场景,下面提供几种实用的实现方式,适配不同的查询复杂度:
方案1:JPA Specifications(推荐,适配简单到中等复杂度查询)
Specifications可以动态构建JPQL查询条件,完美整合Spring Data JPA的分页、排序能力,和PrimeNG的懒加载参数天然匹配。
步骤1:定义Specification过滤逻辑
假设你的实体是User,针对前端传来的过滤字段封装动态条件:
public class UserSpecs { public static Specification<User> withFilters(Map<String, String> filters) { return (root, query, cb) -> { List<Predicate> predicates = new ArrayList<>(); // 遍历前端过滤条件,按需添加匹配规则 filters.forEach((field, value) -> { switch(field) { case "username": predicates.add(cb.like(cb.lower(root.get("username")), "%" + value.toLowerCase() + "%")); break; case "status": predicates.add(cb.equal(root.get("status"), value)); break; case "createTime": // 日期范围过滤示例(假设前端传的是yyyy-MM-dd格式) LocalDate date = LocalDate.parse(value); predicates.add(cb.equal(cb.function("DATE", LocalDate.class, root.get("createTime")), date)); break; // 其他字段的过滤规则按需添加 } }); return cb.and(predicates.toArray(new Predicate[0])); }; } }
步骤2:扩展Repository接口
让你的Repository继承JpaSpecificationExecutor,获得动态查询能力:
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> { // 保留你已有的原生查询方法,比如: // @Query(nativeQuery = true, value = "SELECT * FROM users WHERE department_id = ?1") // List<User> findByDepartmentId(Long deptId); }
步骤3:Service层处理懒加载请求
接收PrimeNG传来的分页、排序、过滤参数,组装查询并返回分页结果:
@Service public class UserService { @Autowired private UserRepository userRepo; public Page<User> getLazyUserData(LazyLoadRequest request) { // 构建过滤条件 Specification<User> spec = UserSpecs.withFilters(request.getFilters()); // 处理排序 Sort sort = Sort.by(request.getSortField()); if (request.getSortOrder() == -1) { // PrimeNG中sortOrder=-1表示降序 sort = sort.descending(); } else { sort = sort.ascending(); } // 组装分页参数 Pageable pageable = PageRequest.of(request.getPage(), request.getSize(), sort); // 执行查询 return userRepo.findAll(spec, pageable); } }
方案2:动态拼接原生SQL(适配复杂多表关联的原生查询)
如果你的现有原生查询是复杂多表关联,无法转为JPQL,可以通过动态拼接WHERE条件实现过滤,同时做好SQL注入防护。
步骤1:定义基础原生SQL
private static final String BASE_SQL = "SELECT u.id, u.username, u.status, d.dept_name " + "FROM users u " + "JOIN departments d ON u.dept_id = d.id";
步骤2:动态构建查询语句
@Service public class UserService { @Autowired private EntityManager em; public Page<UserDTO> getLazyUserDataWithNativeQuery(LazyLoadRequest request) { StringBuilder sqlBuilder = new StringBuilder(BASE_SQL); List<Object> params = new ArrayList<>(); // 添加过滤条件 if (!request.getFilters().isEmpty()) { sqlBuilder.append(" WHERE 1=1"); request.getFilters().forEach((field, value) -> { switch(field) { case "username": sqlBuilder.append(" AND LOWER(u.username) LIKE ?"); params.add("%" + value.toLowerCase() + "%"); break; case "deptName": sqlBuilder.append(" AND LOWER(d.dept_name) LIKE ?"); params.add("%" + value.toLowerCase() + "%"); break; case "status": sqlBuilder.append(" AND u.status = ?"); params.add(value); break; } }); } // 添加排序 if (request.getSortField() != null) { sqlBuilder.append(" ORDER BY ").append(request.getSortField()); sqlBuilder.append(request.getSortOrder() == -1 ? " DESC" : " ASC"); } // 统计总条数 String countSql = "SELECT COUNT(*) FROM (" + BASE_SQL + (sqlBuilder.indexOf("WHERE") != -1 ? sqlBuilder.substring(sqlBuilder.indexOf("WHERE")) : "") + ") AS temp"; Query countQuery = em.createNativeQuery(countSql); for (int i = 0; i < params.size(); i++) { countQuery.setParameter(i + 1, params.get(i)); } long total = ((Number) countQuery.getSingleResult()).longValue(); // 查询分页数据 Query dataQuery = em.createNativeQuery(sqlBuilder.toString(), UserDTO.class); for (int i = 0; i < params.size(); i++) { dataQuery.setParameter(i + 1, params.get(i)); } dataQuery.setFirstResult(request.getPage() * request.getSize()); dataQuery.setMaxResults(request.getSize()); List<UserDTO> content = dataQuery.getResultList(); return new PageImpl<>(content, PageRequest.of(request.getPage(), request.getSize()), total); } }
方案3:直接使用Criteria Builder(灵活度最高)
如果需要更精细的查询控制,可以直接用JPA Criteria Builder手动构建动态查询,底层和Specifications一致,但更灵活:
@Service public class UserService { @Autowired private EntityManager em; public Page<User> getLazyUserDataWithCriteria(LazyLoadRequest request) { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<User> cq = cb.createQuery(User.class); Root<User> root = cq.from(User.class); // 构建过滤条件 List<Predicate> predicates = new ArrayList<>(); request.getFilters().forEach((field, value) -> { switch(field) { case "username": predicates.add(cb.like(cb.lower(root.get("username")), "%" + value.toLowerCase() + "%")); break; case "status": predicates.add(cb.equal(root.get("status"), value)); break; } }); cq.where(cb.and(predicates.toArray(new Predicate[0]))); // 处理排序 if (request.getSortField() != null) { Order order = request.getSortOrder() == -1 ? cb.desc(root.get(request.getSortField())) : cb.asc(root.get(request.getSortField())); cq.orderBy(order); } // 分页查询数据 TypedQuery<User> dataQuery = em.createQuery(cq); dataQuery.setFirstResult(request.getPage() * request.getSize()); dataQuery.setMaxResults(request.getSize()); List<User> content = dataQuery.getResultList(); // 统计总条数 CriteriaQuery<Long> countQuery = cb.createQuery(Long.class); countQuery.select(cb.count(countQuery.from(User.class))); countQuery.where(cb.and(predicates.toArray(new Predicate[0]))); long total = em.createQuery(countQuery).getSingleResult(); return new PageImpl<>(content, PageRequest.of(request.getPage(), request.getSize()), total); } }
核心注意事项
- 参数映射:PrimeNG的
LazyLoadEvent中,first是起始索引,rows是每页数量,后端要转换为page = first / rows;sortOrder为1是升序,-1是降序。 - SQL注入防护:所有过滤参数必须通过占位符传递,绝对不能直接拼接字符串到SQL中。
- 性能优化:确保过滤字段、排序字段都有数据库索引,避免大数据量下的全表扫描。
- 复杂结果映射:如果原生查询返回多表字段,用DTO接收,可通过
@SqlResultSetMapping或构造函数映射结果。
内容的提问来源于stack exchange,提问作者Karim Hossam
相关产品推荐
相关产品推荐

