列表查询的 GraphQL —— 一行代码终结你的 if-else 地狱!
一个后端工程师的自白:为什么你写了 100 行 Java 代码,其实只做了一件事——把前端传进来的几个参数,拼成一条 SQL。
一页纸的需求
产品经理小王给我发了一张原型图。很普通的后台管理页面:
- 表格分页展示
- 顶部四个筛选条件:姓名、年龄区间、部门、入职时间
- 可以按任意列排序
- 底部统计:总人数、平均年龄
"这个简单吧?下午能上线吗?"他问。
我说:"能。"
然后默默打开 IDEA,开始写代码。
如果你也用 MyBatis,接下来 30 分钟你会做什么
// 首先,你要写一条动态 SQL
<select id="searchUsers" resultType="UserVO">
SELECT u.*, d.name as deptName
FROM user u LEFT JOIN dept d ON u.dept_id = d.id
<where>
<if test="name != null and name != ''">
AND u.name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="ageMin != null">
AND u.age >= #{ageMin}
</if>
<if test="ageMax != null">
AND u.age <= #{ageMax}
</if>
<if test="dept != null and dept != ''">
AND d.name = #{dept}
</if>
<if test="entryDateStart != null">
AND u.entry_date >= #{entryDateStart}
</if>
<if test="entryDateEnd != null">
AND u.entry_date <= #{entryDateEnd}
</if>
</where>
<if test="sortField != null and sortOrder != null">
ORDER BY ${sortField} ${sortOrder}
</if>
LIMIT #{offset}, #{size}
</select>
还没完。你还需要一个统计 SQL、一个 Controller 方法解析参数、一个 Service 层做分页计算、一个 Mapper 接口、一个 VO 类专门承接查询结果——还要小心 ${} SQL 注入。
需求只说了四个筛选条件,代码已经快 100 行了。
然后小王说:"对了,把部门筛选改成支持多选。还有,加一个性别筛选。"
你深吸一口气,继续写。
这不是 MyBatis 的问题——换 JPA,你也不会更轻松:
Specification<User> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 联表:要查部门名,先 JOIN department 表
Join<User, Department> deptJoin = root.join("dept", JoinType.LEFT);
if (StringUtils.isNotBlank(name)) {
predicates.add(cb.like(root.get("name"), "%" + name + "%"));
}
if (ageMin != null) {
predicates.add(cb.ge(root.get("age"), ageMin));
}
if (ageMax != null) {
predicates.add(cb.le(root.get("age"), ageMax));
}
if (StringUtils.isNotBlank(dept)) {
predicates.add(cb.equal(deptJoin.get("name"), dept));
}
// ... 同样的 if 地狱,只是换了语法
return cb.and(predicates.toArray(new Predicate[0]));
};
// 除此之外,还有 排序、分页、总条数、字段统计,每一项都都需要你不断的堆砌代码 ...
MyBatis 在 XML 里写 <if>,JPA 在 Java 里拼 Join + Predicate。本质没变——你还是在用手工方式把参数翻译成查询条件。
这个问题存在的唯一原因,就是你把"声明"和"执行"混在了一起。
如果换一种思想
我们换一个视角看这个问题。
数据库表,你已经定义好了。前端要查什么,用户说了算。
那为什么中间的翻译工作——把 HTTP 参数翻译成 SQL——需要你一行一行写 if-else?
有没有可能,让一个聪明的中间层来做这件事?它知道:
- 你的实体有哪些字段 → 这就是检索边界
- 前端传了哪些参数 → 这就是检索意图
- 两者一结合 → 生成 SQL
这个思路不是我的发明。在 API 领域,它有一个如雷贯耳的名字:
GraphQL — 客户端指定要什么字段,服务端返回什么字段。一次请求,替代多次 REST 调用。
那在列表查询领域,能不能有同样的东西?
| 维度 | GraphQL | Bean Searcher |
|---|---|---|
| 领域 | API 数据查询 | 数据库列表检索 |
| 客户端控制什么 | 返回哪些字段 | 返回哪些字段 + 按什么筛选 + 按什么排序 + 分页多少 |
| 协议 | POST + GraphQL body | 标准 HTTP 参数(GET/POST 均可) |
| 核心思想 | 声明你要什么数据 | 声明检索边界,参数驱动查询 |
| 接入成本 | 改 API 层、加 Schema | 一个依赖,零代码改造 |
一句话:GraphQL 让前端在一次请求中自由控制返回数据;Bean Searcher 让前端在一次请求中自由控制筛选、排序、分页和统计——用 REST 最熟悉的 URL 参数方式。
列表检索领域的 GraphQL
你只需要定义一个实体:
@SearchBean(tables = "user u, dept d", where = "u.dept_id = d.id", autoMapTo = "u")
public class UserVO {
private Long id;
private String name;
private Integer age;
private String gender;
@DbField("d.name")
private String deptName;
private LocalDate entryDate;
// getters & setters...
}
然后,整个检索接口一行代码:
@GetMapping("/user/search")
public SearchResult<UserVO> search(HttpServletRequest request) {
return beanSearcher.search(UserVO.class, MapUtils.flat(request.getParameterMap()));
}
前端直接 GET 请求:
GET /user/search?name=张&age-0=20&age-1=30&age-op=bt&sort=age&order=desc
这一行代码返回的数据长这样:
{
"dataList": [
{ "id": 1, "name": "张三", "age": 25, "deptName": "技术部", "entryDate": "2023-03-01" },
{ "id": 2, "name": "张小明", "age": 28, "deptName": "产品部", "entryDate": "2022-11-15" }
],
"totalCount": 47,
"summaries": [ 1350 ]
}
分页、联表、多条件筛选、排序、统计——一个接口全搞定。 没有 XML。没有 if-else。没有 VO 转换代码。
这就是 Bean Searcher——一个我用了三年、忍不住想安利给所有后端工程师的框架。
它到底做了什么
让我用一句话解释它的核心原理,因为理解了这一点,你就理解了它为什么能省掉 90% 的代码:
实体类声明检索边界,HTTP 参数驱动查询逻辑。
| 传统方式(MyBatis / JPA) | Bean Searcher | |
|---|---|---|
| 筛选条件怎么定义 | XML 里写 <if> / Java 里拼 QueryWrapper |
参数名直接映射字段名 |
| 加一个新筛选条件 | 改 XML / 改 Java 代码 → 重新编译部署 | 前端直接传新参数,后端零改动 |
| 多表联查 | 手写 JOIN SQL | 实体类声明关联关系 |
| 返回结果 | 需要 VO 转换层 | SearchBean 就是 VO |
| 安全性 | 自己写校验 | 防注入、防大页、防深度偏移,全部默认开启 |
打个比方:传统方式是"命令式"的——你告诉框架每一步怎么做。Bean Searcher 是"声明式"的——你声明"能查什么"(实体定义边界),然后前端通过参数表达"想查什么"(驱动查询逻辑)。
你不是在写查询。你是在声明检索边界。
一个会被问到的问题
"这难道不会让前端传太多参数吗?"
这个问题我被问了无数次。答案是:前端传多少参数,只和产品需求的复杂度有关,和后端用的什么框架毫无关系。
如果产品只需要一个模糊搜索框,前端就只传 ?name=张,不需要 name-op 和 name-ic。你甚至可以零注解使用——一个单纯的 POJO,字段名默认映射为数据库列名(驼峰转下划线)。
记住一件事:Annotation 是用来约束和精细化控制的,不是必须的。单表实体什么注解都不用加,天生可搜。
它和 MyBatis/JPA 是敌人吗?
绝对不是。
MyBatis 管增删改,Bean Searcher 管列表查。各司其职,和谐共存。
就像 GraphQL 不是为了取代 REST 而生的——它只是让 API 查询更灵活。Bean Searcher 也不是为了取代 MyBatis——它只是让列表检索不再痛苦。
| 什么时候用 | |
|---|---|
| MyBatis / JPA | 增删改、事务性操作、复杂业务逻辑 |
| Bean Searcher | 后台管理列表、数据导出、报表查询、任何"多条件动态筛选"场景 |
| 两者的关系 | 互补,不是替代。加一个依赖就够了。 |
真实项目里的体验
我在三个项目里深度使用 Bean Searcher(分别是 Spring Boot 2/3/4 和 Solon 3/4),最大的感受不是"代码少了"——而是思维方式变了。
以前接到列表查询需求,脑子里想的是"这个 SQL 怎么写、参数怎么拼、排序怎么处理、分页传什么对象"。
现在接到列表查询需求,脑子里想的是"这个页面需要哪些字段,它们来自哪些表,哪些字段允许前端筛选"。
你不再是一个 SQL 拼接工。你是一个领域建模者。
而当你把这种体验告诉同事时,他们的第一反应通常是:"这不就是……Java 后端版的 GraphQL?"
"对。"
试试看
如果你读到这里,发现上面说的痛点都是你每天在经历的——那你应该试一下。
不用重构项目,不用替换 ORM,不用改变任何架构。它是一个完全不侵入的框架,和 MyBatis/JPA/Spring Data JDBC 都能共存。
- 📖 完整文档
- 🖥 在线 Demo(零部署体验)
- ⭐ GitHub | Gitee
如果你觉得这玩意确实解决了你的痛点,点个 Star,让更多被列表查询折磨的 Java 工程师看到它。
毕竟 —— 你把生命花在写 if-else 上,不如花在更有价值的事情上。

浙公网安备 33010602011771号