Spring Boot 用一个接口搞定所有查询
原创大约 3 分钟
Spring Boot 用一个接口搞定所有查询
如果你用过 Spring Boot,一定很熟悉“铁三角”:@Controller
、@Service
和 @Repository
。每加一个实体,通常就会有对应的 Controller 来处理 CRUD 和业务逻辑——至少对于同步操作来说如此。
创建 POST
、PUT
、DELETE
这些接口很简单:校验、业务逻辑一接,接口就能用了。
但 GET
呢?
来看一个简单的 Employee
实体:
public class Employee extends BaseUUIDEntity {
private Long id;
private String name;
private String email;
private String mobile;
private String role;
private String status;
private String address;
}
现在我们要查所有指定状态的员工,很简单:
@GetMapping
public List<Employee> getEmployeesByStatus(@RequestParam String status) {
return employeeRepository.findByStatus(status);
}
清爽利落,对吧?
我们继续下一个需求:
“API 能不能也按角色过滤?”
当然可以,改一下:
@GetMapping
public List<Employee> getEmployeesByStatusAndRole(@RequestParam String status,
@RequestParam String role) {
return employeeRepository.findByStatusAndRole(status, role);
}
又一次上线、又一次版本号递增。
然后又有同事说:
“我们想按 status 或 role 查询,还能不能加个 department 过滤?”
糟了,问题来了:组合太多,维护太累。
Spring Data Specification 登场
Spring Specification 提供了一种动态、可复用的方式,用 JPA Criteria API 构建查询。
可以把 Specification<T>
理解为运行时构建 Predicate
的工具,让你对查询条件有极高的灵活性。
我们来用 Specification<T>
搭一个灵活的查询解析器。
动态搜索构建器
public <T> Specification<T> parseSearchParams(MultiValueMap<String, String> params) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
for (Map.Entry<String, List<String>> param : params.entrySet()) {
String key = param.getKey();
List<String> values = param.getValue();
if (key.equals("page") || key.equals("size") || key.equals("sort")) {
continue; // 跳过分页/排序参数
}
try {
Path<?> path = root.get(key);
if (values.get(0).startsWith("in:")) {
String[] valuesArray = values.get(0).substring(3).split(",");
List<String> trimmedValues = Arrays.stream(valuesArray)
.map(String::trim)
.collect(Collectors.toList());
predicates.add(path.in(trimmedValues));
continue;
}
for (String value : values) {
if (value.startsWith("eq:")) {
predicates.add(criteriaBuilder.equal(path, value.substring(3)));
} else if (value.startsWith("like:")) {
predicates.add(criteriaBuilder.like(path.as(String.class), "%" + value.substring(5) + "%"));
} else if (value.startsWith("gt:")) {
predicates.add(criteriaBuilder.greaterThan(path.as(String.class), value.substring(3)));
} else if (value.startsWith("gte:")) {
predicates.add(criteriaBuilder.greaterThanOrEqualTo(path.as(String.class), value.substring(4)));
} else if (value.startsWith("lt:")) {
predicates.add(criteriaBuilder.lessThan(path.as(String.class), value.substring(3)));
} else if (value.startsWith("lte:")) {
predicates.add(criteriaBuilder.lessThanOrEqualTo(path.as(String.class), value.substring(4)));
}
}
} catch (Exception e) {
log.error("创建 {}:{} 的谓词出错", key, values, e);
}
}
return predicates.isEmpty()
? criteriaBuilder.conjunction()
: criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
最终 API 接口
@GetMapping
public List<Employee> getEmployees(@RequestParam MultiValueMap<String, String> params) {
Specification<Employee> spec = parseSearchParams(params);
return employeeRepository.findAll(spec);
}
查询示例
现在,这一个接口就能支持所有这些用例:
# 查找所有在职员工
curl -X GET "http://localhost:8080/api/employees?status=eq:ACTIVE"
# 查找所有角色包含 manager 的在职员工
curl -X GET "http://localhost:8080/api/employees?status=eq:ACTIVE&role=like:manager"
# 查找指定角色的员工
curl -X GET "http://localhost:8080/api/employees?role=in:Manager,Engineer"
为什么值得这样做
- 不用为每种过滤组合新建接口
- 不用为每个新字段扩展 repository 接口
- 简单过滤逻辑无需重新部署
- 对业务友好,易扩展
这种模式可以轻松适配任何实体,让你的代码库保持一致性和可维护性。
一个接口,无限查询。如果没有复杂业务逻辑,这就是最终答案。