跳至主要內容

Spring Boot 用一个接口搞定所有查询

DD编辑部原创Spring BootSpring BootSpring Data大约 3 分钟

Spring Boot 用一个接口搞定所有查询

如果你用过 Spring Boot,一定很熟悉“铁三角”:@Controller@Service@Repository。每加一个实体,通常就会有对应的 Controller 来处理 CRUD 和业务逻辑——至少对于同步操作来说如此。

创建 POSTPUTDELETE 这些接口很简单:校验、业务逻辑一接,接口就能用了。

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 接口
  • 简单过滤逻辑无需重新部署
  • 对业务友好,易扩展

这种模式可以轻松适配任何实体,让你的代码库保持一致性和可维护性。

一个接口,无限查询。如果没有复杂业务逻辑,这就是最终答案。

上次编辑于:
贡献者: didi