这个问题最早出现在一个很普通的 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 被拆碎了,参数列表要手工维护,IN、LIKE、时间范围、权限条件、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 引擎模块