上一篇聊到,LLM 很适合帮 Spring 企业项目生成 SQL 模板。
它擅长 SQL,也能比较快理解"列表页有哪些筛选条件""哪些字段要返回""哪些条件为空时跳过"。如果我们让它输出一段完整 SQL 模板,而不是输出 Java 里的 StringBuilder 拼接,人工审查成本会低很多。
但这里还有一个问题:既然已经是 SQL 模板了,为什么还要规定 sqlExp、sqlInExp、toLikeStr 这些 helper?
原因很简单:AI 能写 SQL,不代表它每次都会用同一种方式处理动态条件。
如果不给规则,它很可能写出这样的模板:
javascript
export const sql = `
SELECT *
FROM orders o
WHERE o.deleted = 0
${form.param.teamId ? `AND o.team_id = ${form.param.teamId}` : ''}
${form.param.keyword ? `AND o.order_no LIKE '%${form.param.keyword}%'` : ''}
`;
这段看起来也像模板,但问题很多。
第一,参数被直接拼进 SQL,注入风险不用多说。第二,字符串、数字、时间、IN 列表要各自处理转义和格式,模型很容易漏掉细节。第三,每个开发者、每次对话、每个模型版本都可能生成不同风格,最后团队维护的不是一套模板,而是一堆写法。
所以 helper 的作用,不是把 SQL 变复杂,而是把容易出错的部分固定下来。
比如单值条件:
javascript
${sqlExp(form.param.teamId, 'AND o.team_id = ?')}
这条规则很小,但它同时约定了几件事:
- 值为空时不输出这段条件;
- 值不为空时输出
AND o.team_id = ?; - 参数进入 evaluator 的参数列表;
- SQL 仍然保持 PreparedStatement 风格。
这样 LLM 不需要自己判断"空值怎么跳过""参数放在哪里""要不要拼引号"。它只需要选对字段和条件。
再看 IN:
javascript
${sqlInExp(form.param.statusList, 'AND o.status IN ')}
IN 是动态 SQL 里很容易写乱的地方。列表为空怎么办?有 1 个、3 个、20 个值时,占位符怎么展开?参数顺序怎么和占位符对应?
如果每次都让人或 LLM 现场写,就会反复出现这种代码:
java
sql.append(" and o.status in (");
sql.append(statusList.stream().map(x -> "?").collect(joining(",")));
sql.append(")");
args.addAll(statusList);
它不难,但很烦,而且容易在复制修改时漏掉边界。sqlInExp 把这个模式收掉以后,模板里只剩业务含义:这里要按状态列表筛选。
LIKE 也是一样。
javascript
${sqlExp(toLikeStr(form.param.keyword), 'AND o.order_no LIKE ?')}
${sqlExp(toLikeStrR(form.param.mobile), 'AND c.mobile LIKE ?')}
模糊查询看起来简单,实际很容易不统一。有的地方写 %keyword%,有的地方写 keyword%,有的地方忘了处理空字符串。固定成 toLikeStr、toLikeStrL、toLikeStrR 以后,模型只需要表达"这是两端模糊"还是"这是右侧模糊"。
对人也是一样。
代码评审时,不需要重新判断这段模板有没有手工拼参数,只要看几个点:
- 可选条件是否都用了
sqlExp; - 多值条件是否用了
sqlInExp; - 模糊匹配方向是否符合业务;
- 必带条件是否用了明确的强制规则;
- 字段、表、JOIN 和排序是否符合查询目标。
这比审一段自由发挥的动态 SQL 轻很多。
更关键的是,这套规则对 LLM 很友好。
提示词里不用解释一整套框架,只要给几条短规则:
text
可选单值条件:sqlExp(value, 'AND field = ?')
多值 IN 条件:sqlInExp(list, 'AND field IN ')
LIKE 条件:先用 toLikeStr / toLikeStrL / toLikeStrR 处理值
不要直接把参数拼进 SQL 字符串
这类规则短、稳定、容易举例。模型在生成第一个模板以后,后续改查询条件、加字段、拆时间范围,基本都会沿着同一种写法继续补。
这就是固定 helper 的价值:减少自由度。
听起来好像"减少自由度"不够酷,但企业开发里很多质量问题,恰恰来自不必要的自由度。一个订单列表页、一个导出接口、一个运营报表,不需要十种动态 SQL 写法。它需要的是一套团队看得懂、AI 也容易遵守的规则。
当然,helper 也不是万能的。
它解决的是动态 SQL 生成过程里的工程问题:空值跳过、参数收集、IN 展开、LIKE 处理、调试可读性。它不能替你判断业务字段口径,也不能替你判断某个用户该不该看到这批数据。
所以这一阶段的边界要说清楚:我们不是在做一个让 AI 自动决定业务含义的系统,也不是让 AI 绕过权限去查库。我们只是先把"复杂动态 SQL 怎么生成、怎么审、怎么调试"这件事变得规整一点。
对一个已经长期使用 Spring JPA / Hibernate 的项目来说,这个收益很直接。
简单查询继续用 Repository。复杂列表、报表、导出这些 SQL 味很重的地方,可以让 LLM 帮忙生成模板初稿,再用固定 helper 把动态部分收束住。人最后审的是一段完整 SQL 和一组稳定规则,而不是一堆 Java 拼接分支。
这一步仍然很小,但它让 AI 辅助开发从"生成一段看起来能跑的代码",变成"生成一段团队能接住的模板"。
相关代码:foggy-dataset Java 引擎模块
相关文档:FSScript SQL helper functions