REST 版的 GraphQL:一行代码终结你的 if-else 地狱

简介: 产品加一个筛选条件,你要改四层代码。这不是 MyBatis 的锅,也不是 JPA 的锅——是你把"声明"和"执行"混在了一起。如果列表查询有 GraphQL,会是什么样?

一个后端工程师的自白:为什么你写了 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 &lt;= #{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 &lt;= #{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 都能共存。

如果你觉得这玩意确实解决了你的痛点,点个 Star,让更多被列表查询折磨的 Java 工程师看到它。

毕竟——你把生命花在写 if-else 上,不如花在更有价值的事情上。

相关文章
|
1月前
|
网络协议 Java 测试技术
网站开发性能测试-JMeter 压测 Tomcat 接口的并发吞吐量(QPS)
在企业级网站开发、Java Web 系统上线与后端架构调优实战中,**性能压力测试(Stress / Load Testing)** 是验证系统高可用性、探测吞吐量极限与排除潜在并发死锁、内存泄露(Memory Leak)的必经之路。 对于采用 Apache Tomcat 作为 Servlet 容器的企业级应用(如 Spring Boot 内嵌 Tomcat 或独立 Tomcat 9/10 集群),单台服务器在面对每秒数百乃至数万次的高频请求时,其底层线程池调度、I/O 多路复用模型(NIO/NIO2)、JVM 垃圾回收机制以及操作系统内核 TCP 队列均会承受极限考验。
|
13天前
|
运维 网络性能优化 数据库
异地组网的带宽与时延如何估算?面向业务场景的链路规划方法
本文提供异地组网链路规划的实用方法论:以业务SLA为起点,通过业务画像分级→时延/带宽反推→冗余系数叠加→链路选型匹配四步法,解决视频卡顿、ERP慢等痛点。涵盖跨运营商暗线识别、时延四段拆解、三层带宽估算及常见失误避坑,助力IT负责人科学决策。(239字)
374 2
异地组网的带宽与时延如何估算?面向业务场景的链路规划方法
|
13天前
|
人工智能 自然语言处理 API
觉得阿里云大模型价格太高怎么办?四种便宜购买与使用方法分享
本文针对阿里云大模型“能力强但调用成本高”的普遍痛点,梳理了一套可落地的全链路省钱方案。从开通百炼领取超7000万Tokens新人免费额度起步,叠加夜间错峰4折起的Night Plan活动,再搭配Token Plan订阅与AI通用型节省计划,四层优惠叠加后,实际调用成本可降至普通按量付费的三到五成,帮助个人开发者与团队大幅降低大模型落地的综合开支。
|
15天前
|
存储 缓存 算法
AKF扩展立方体和AKF可用性立方体
很多人知道AKF扩展立方体是从《架构即未来》这本书开始。实际上akfpartners官方写过4篇关于AKF扩展立方体的文章,还有一篇介绍AKF可用性立方体。akfpartners官方在高可用、扩展性方面有很多专业技术文章,建议有空就翻翻看。
|
22天前
|
人工智能 JSON 前端开发
⑤ 消费格式差异:同一份契约的四角色消费格式
同一份YAML契约编译为Prompt前缀、JSON Schema、走查清单、CI规则四种格式。AI工程师注入对话约束,前端校验Props,设计师走查,负责人看消费数据。不是语法差异,而是工作流入口差异——同一语义嵌入四种习惯,变更自动同步。
|
24天前
|
NoSQL JavaScript 前端开发
【Azure Function】NodeJS Function大批量写入到Redis遇见丢失数据情况的分析
Azure Functions 中批量写 Redis 时,看到 Invocation 已完成,不代表每个 Redis SET 都完成了。如果代码用 callback 调用 client.set(),随后立即执行 client.quit() 或直接返回,Function Runtime 可能认为本次执行已经结束,但 Redis 写入还在事件循环里排队。我的建议很明确:所有外部依赖调用都必须 await,批量写入要么顺序等待,要么用受控并发等待全部 Promise 完成。
119 1
|
8天前
|
监控 Linux Docker
OFNA:用 Python 从零构建一个内核驱动的场态操作系统
OFNA 是一个内核驱动的场态操作系统,为应用提供身份、环流、审计、时序对齐、算力调度等底层能力。本文介绍其 9 个内核模块的架构设计、A/B/C 三层防护体系、Docker 多阶段构建方案,以及 716 条测试、317K events/s 吞吐背后的工程实践。项目已在 Gitee 开源。
|
10天前
|
存储 编译器 Swift
从 OC 到 Swift,老 iOS 开发者踩过的 10 个语法大坑
本文总结老iOS开发者从Objective-C转向Swift时必踩的10个典型语法与思维陷阱:nil直觉差异、强制解包滥用、混编隐式解包、值/引用语义混淆、Any泛滥、Bool/BOOL误用、初始化顺序混乱、闭包循环引用、OptionSet误当枚举、动态调用失效等。直击OC“运行时宽容”与Swift“编译期严控”的根本冲突,助你真正切换编程范式。(239字)
85 0
|
21天前
|
机器学习/深度学习 人工智能 数据挖掘
从 AI 写作到科研工作台,Textira 正在重新连接论文从实验到投稿的流程
随着大模型逐渐进入文献阅读、实验分析、科研绘图和论文写作,AI 科研工具正在从单点的“AI Writer”走向更完整的 Research Workspace。本文以 Textira 为例,讨论如何以一项 Research 为基本单位,连接知识库、文献、实验、数据、论文与投稿,并进一步探讨 MCP、API 与 Research Agent 可能带来的科研工作流变化。
|
3月前
|
人工智能 运维 安全
【新版】阿里云 轻量应用服务器 功能介绍及配置价格表
阿里云轻量应用服务器是面向个人开发者、学生、小微企业及初创团队打造的轻量化云服务器产品,主打**简单易用、开箱即用、高性价比**,无需复杂的云服务运维知识,即可快速搭建网站、应用、测试环境、小程序后端、AI应用等各类业务。新版产品在原有基础上完成全面升级,整合一站式应用部署、大带宽、多镜像、安全防护、自动化运维等能力,彻底降低上云门槛,让用户专注于业务开发而非基础设施管理。
366 3
【新版】阿里云  轻量应用服务器 功能介绍及配置价格表