Java 里动态 SQL 为什么总是越写越乱

这个问题最早出现在一个很普通的 Spring 项目里。

项目主体是 Spring JPA / Hibernate。由于历史原因,数据访问层已经围绕 Entity、Repository、Session、事务上下文这些东西跑了很久,不太适合再为了少量复杂查询引入另一套持久层架构。

日常的增删改查用 Repository、Entity、JPQL 就够了,也没必要再上一套 XML 动态 SQL 框架。大多数时候这没什么问题,直到遇到列表页、运营报表、复杂筛选和导出。

这类查询经常长这样:按团队、状态、关键字、时间范围筛选;有些条件为空就不拼,有些条件是 IN 列表,有些要做模糊匹配;同一段查询还要支持分页、count 和排序。

在 JPA 项目里,常见做法有几种。

一种是在 Repository 里写 JPQL / native SQL,然后为了动态条件拆成很多方法,或者在业务代码里拼查询片段:

java 复制代码
String hql = "from Order o where o.deleted = false";
if (teamId != null) {
    hql += " and o.teamId = :teamId";
}
if (keyword != null) {
    hql += " and o.orderNo like :keyword";
}

另一种是退回 JdbcTemplate,自己维护 SQL 和参数:

java 复制代码
StringBuilder sql = new StringBuilder("select * from orders where deleted = 0");
List<Object> args = new ArrayList<>();

if (teamId != null) {
    sql.append(" and team_id = ?");
    args.add(teamId);
}
if (statusList != null && !statusList.isEmpty()) {
    sql.append(" and status in (");
    sql.append(statusList.stream().map(x -> "?").collect(joining(",")));
    sql.append(")");
    args.addAll(statusList);
}

这些写法都能跑,也都不稀奇。问题是条件一多,SQL 被拆碎了,参数列表要手工维护,INLIKE、时间范围、权限条件、count 查询都要重复处理。最后维护的不是查询本身,而是一堆拼接规则。

foggy-dataset 早期想补的就是这个空位:在不推翻原有 Hibernate / JPA 架构的前提下,简单整理一套 JavaScript 风格的 SQL 模板能力。复杂查询可以保留 SQL 的直接表达能力,但不要再把动态 SQL 写成 Java 字符串拼装。

我们做了一个接近 JavaScript 风格的 SQL 模板:

javascript 复制代码
// order_query.fsscript
import {getCurrentUser} from '@authService';

const user = getCurrentUser();

export const sql = `
    SELECT
        o.order_id,
        o.order_no,
        o.amount,
        o.status,
        o.create_time
    FROM orders o
    WHERE o.tenant_id = ${sqlExp(user.tenantId, '?', true)}
        ${sqlExp(form.param.teamId, 'AND o.team_id = ?')}
        ${sqlInExp(form.param.statusList, 'AND o.status IN ')}
        ${sqlExp(toLikeStr(form.param.orderNo), 'AND o.order_no LIKE ?')}
        ${sqlExp(form.param.startTime, 'AND o.create_time >= ?')}
        ${sqlExp(form.param.endTime, 'AND o.create_time < ?')}
    ORDER BY o.create_time DESC
`;

这里没有发明一套新的 ORM。SQL 还是 SQL,只是动态部分放回模板里:

  • sqlExp 在值为空时跳过条件,有值时追加 SQL 片段并收集参数。
  • sqlInExp 处理 IN (...) 的占位符和参数列表。
  • toLikeStr 处理模糊匹配的 %
  • import {getCurrentUser} from '@authService' 可以直接拿 Spring Bean,把当前用户、租户、权限片段这类上下文接进来。

Java 侧也不复杂。用带 Spring 上下文的 evaluator 执行脚本,再拿生成后的 SQL 和参数:

java 复制代码
QueryExpEvaluator evaluator = QueryExpEvaluator.newInstance(applicationContext);
evaluator.put("form", form);

SQLKey sqlKey = dataSetModel.getSql(evaluator);
// sqlKey.getSql(): 生成后的 SQL
// sqlKey.getArgs(): PreparedStatement 参数

这一步的目标很小:不是替代 JPA,也不是把所有查询都迁走。Entity 查询、简单 Repository 查询继续留在 JPA;只有那些条件多、报表味重、需要精细控制 SQL 的地方,才交给模板。

后来 TM、QM、DSL 是在这个基础上继续往前长出来的。但最早的起点,就是先把 Spring JPA 项目里越写越乱的动态 SQL 整理干净。

相关代码:foggy-dataset Java 引擎模块

github.com/foggy-proje...

相关推荐
越努力越幸运661 小时前
MAF的塑智能体边界,从AIAgent抽象类开始
后端
404号扳手1 小时前
Java 进阶知识(七)
java·后端
小马爱打代码1 小时前
Spring框架:介绍和快速入门
java·后端·spring
颜进强1 小时前
Claude Code -16 文件引用与加载机制完整实践:从 CLAUDE.md 到 Skills 与 Subagents
前端·后端·ai编程
闪闪发光得欧1 小时前
agent工作模式之ReAct实战
后端
爱折腾的程序员2 小时前
Java 8 Stream 流常用操作:从入门到原理
后端
李小狼lee2 小时前
认识一下枚举类型
后端
卷无止境2 小时前
Jupyter Kernel 是什么?原来notebook不仅可用python
后端