一个 LightDao 的自我修养:SqlToy ORM 能力全景
很多人第一次见 SqlToy 的反应是:"动态 SQL 还能这么写?"这篇文章把它近二十年攒下的家底一次性摊开。
Java 圈的 ORM,常见的站队是"JPA 派"和"MyBatis 派":前者对象操作顺手但复杂查询憋屈,后者 SQL 自由但单表 CRUD 写到怀疑人生。SqlToy 的答案是不站队——它是 JPA 式对象操作与 SQL 优先查询的融合体:简单的走对象,复杂的写 SQL,两者共用同一个 LightDao(约 110 个方法),背后是同一套事务、缓存、方言与安全体系。
这个框架不是新面孔:2007 年起源银行管理类项目,经历金融企业项目、拉卡拉日均千万级大数据平台(累计百亿规模)、SaaS 化多租户 ERP 的连续锤炼后开源,当前版本 6.0.9。下面按七个能力面展开。
一、三层 API 哲学:简单与复杂各得其所
理解 SqlToy 只需记住一张分工表,复杂度递增、各不越界:
| 层 | 写法 | 适用场景 |
|---|---|---|
| 对象操作 | lightDao.save/update/load(...) |
主键定位的单对象 CRUD |
| Lambda 条件 | lightDao.lambdaQuery(User.class).eq(User::getStatus,1) |
单表/多表 join 的常规条件查询(6.0.8 起内置) |
| SQL 模式 | sqlId + #[...] 动态段 |
复杂查询、报表、多分支动态条件 |
三层可以在一个 Service 里自由混用,没有"选型包袱"——这是它和单形态框架最大的差异。
二、对象操作:像 JPA,但补了 JPA 的坑
快速上手是标志性的三步曲:quickvo 生成 POJO → yml 配置 → Service 注入 LightDao,不需要手写任何 Dao:
@Autowired
LightDao lightDao;
lightDao.save(staffInfo); // 保存
lightDao.update(staffInfo, "photo"); // 弹性更新:null 字段跳过,photo 强制更新
lightDao.loadByIds(StaffInfoVO.class, "S2007"); // 按主键批量加载
lightDao.isUnique(staffInfo, "staffCode"); // 唯一性验证
真正拉开差距的是面向强事务高并发场景的两个方法:
弹性更新——不同于 Hibernate"先 load 再改"的两步走,SqlToy 一次数据库交互完成修改,null 字段不参与 UPDATE,高并发下不会把别人刚写入的字段冲掉。
updateSaveFetch(台账场景独门利器)——一次数据库交互完成:锁查询 → 存在则修改、不存在则插入 → 返回最新记录。"存在则更新、不存在则初始化"的 upsert 语义天然规避并发首查竞争:两个请求同时发现记录不存在,只有一个 insert 成功,另一个自动走修改。仓库出库扣减的写法——先判断是否够减,不够直接抛异常整体回滚:
StockLedgerVO ledger = new StockLedgerVO(); // 以 产品+仓库 唯一定位台账记录
ledger.setProductId("P0001");
ledger.setWarehouseId("WH01");
int deductQty = 30;
StockLedgerVO latest = lightDao.updateSaveFetch(ledger, (entity, rowIndex) -> {
// entity 是代理对象:get 直接读数据库当前值,set 自动更新行,全程不碰 ResultSet
int available = entity.getQuantity();
if (available < deductQty) {
// 先判断是否够减:不够则抛异常,本次操作整体回滚
throw new IllegalArgumentException("库存不足! 可用: " + available + ", 需扣: " + deductQty);
}
entity.setQuantity(available - deductQty); // 够减才扣减
}, "productId", "warehouseId");
库存台账、客户资金账这类高并发强事务环节一个方法闭环(台账记录不存在时以实体当前值自动补插,入库初始化与出库扣减共用同一方法;实体回调形态依赖 byte-buddy 生成代理实体,另有直接操作 ResultSet 的 UpdateRowHandler 形态)。与之互补的 updateFetch 面向"记录必然存在、且需要自由编写查询条件与指定锁策略"的场景。
对象操作面还包括:@OneToOne/@OneToMany 级联保存加载修改、一条 join SQL 自动封装父子对象的层次封装、非递归查询整棵子树的树形表路由、22/26 位有序数字/雪花/UUIDv7/ULID/redis 业务单据号等十余种主键策略、创建人修改人等公共字段统一赋值、@DataVersion 乐观锁。
三、SQL 查询:看家本领
动态 SQL:#[...] 一个符号讲完条件逻辑
#[...] 内只要有一个参数为 null,整段剔除——条件组织原理一句话讲完,支持多层嵌套和 @if() 万能钥匙。同样功能的对比:
<!-- SqlToy -->
<sql id="show_case">
<filters>
<!-- statusAry 含 -1(全部) 则置 null 不参与检索 -->
<eq params="statusAry" value="-1" />
</filters>
<value><![CDATA[
select * from sqltoy_device_order_info t
where #[t.status in (:statusAry)]
#[and t.ORDER_ID=:orderId]
#[and t.ORGAN_ID in (:authedOrganIds)]
]]>
</value>
</sql>
<!-- MyBatis:同样的逻辑 -->
<select id="show_case" resultMap="BaseResultMap">
select * from sqltoy_device_order_info t
<where>
<if test="statusAry!=null">
and t.status in
<foreach collection="statusAry" item="status" separator="," open="(" close=")">#{status}</foreach>
</if>
<if test="orderId!=null">and t.ORDER_ID=#{orderId}</if>
<if test="authedOrganIds!=null">
and t.ORGAN_ID in
<foreach collection="authedOrganIds" item="organId" separator="," open="(" close=")">#{organId}</foreach>
</if>
</where>
</select>
SqlToy 版本可以直接 copy 到数据库客户端执行调试(#[] 剥掉就是合法 SQL);参数全部走 PreparedStatement 绑定,天然防注入。中等复杂度还可以用 Java 文本块把 SQL 写在代码里,与 xml 完全同构。
缓存翻译:把 join 消灭在查询之前
码值转名称不用关联表:员工表里的 staffId 出结果时直接翻译成姓名,字典 SEX_TYPE 翻译成"男/女",靠的是可增量刷新的本地缓存(ehcache/caffeine)。对象属性上打注解即可:
@Translate(cacheName = "staffIdName", keyField = "staffId")
private String staffName;
配套能力同样值钱:cache-arg 反向用缓存把"名称模糊搜索"变成精准 key 匹配(替代 like);面向平台型电商百万级 SKU 的 FIFO 动态缓存不做全量加载,未命中按需批量拉取,本地只留最热数据。
四层分页优化
四层可叠加使用:1)自动优化 count 语句(不是简单 count(1) from (原sql),智能剔除外层 order by);2)page-optimize 缓存优化,相同条件一定周期内不重复 count,分页由两次查询变 1.3~1.5 次;3)@fast() 快速分页,先取单页数据再关联;4)parallel 并行分页,count 与单页数据同时查。
一个员工分页查询,把快速分页和缓存翻译揉在一起——机构名称、性别字典从缓存直取,两个关联表的 join 直接省掉:
<sql id="sqltoy_staff_page">
<!-- 第 2 层:相同条件 120 秒内 count 走缓存;第 4 层:count 与单页数据并行查询 -->
<page-optimize parallel="true" alive-max="100" alive-seconds="120" />
<!-- 缓存翻译:关联查询变缓存直取,出结果时代码值自动变成名称 -->
<translate cache="organIdName" columns="organName" />
<translate cache="dictKeyName" cache-type="SEX_TYPE" columns="sexTypeName" />
<value><![CDATA[
select t.STAFF_ID, t.STAFF_NAME,
t.ORGAN_ID organName, -- 机构ID → 机构名称(缓存翻译)
t.SEX_TYPE sexTypeName, -- 性别字典 → 名称(缓存翻译)
t.ENTRY_DATE
-- 第 3 层:@fast 快速分页,limit 先在内层取数时生效
from @fast(select * from sqltoy_staff_info t
where t.STATUS = 1
#[and t.STAFF_NAME like :staffName]
#[and t.ENTRY_DATE >= :beginDate]
order by t.ENTRY_DATE desc) t
]]></value>
</sql>
// 第 1 层无需任何配置:count 语句由框架自动改写优化
Page<StaffInfoVO> result = lightDao.findPage(
new Page(20, 1), "sqltoy_staff_page", staffVO, StaffInfoVO.class);
查询全家桶
并行查询(一个页面多个无依赖 SQL 同时执行)、流式查询(fetchStream 超大数据逐行回调不撑爆内存)、存储过程(含 out 参数与多结果集)、取 Top/随机记录、query()/save()/store() 等链式入口、20+ 种 filters 参数预处理器——管理类项目的高频需求基本都有原生答案。
四、数据分析:报表零 ETL
这类能力是"用算法代替 SQL"的思路:在内存中完成、跨库行为一致,一份查询结果加几行配置直接出报表。用同一份水果销售明细演示:
| 品类 | 销售月份 | 销售笔数 | 销售数量(吨) | 销售金额(万元) |
|---|---|---|---|---|
| 苹果 | 2019年5月 | 12 | 2000 | 2400 |
| 苹果 | 2019年4月 | 11 | 1900 | 2600 |
| 苹果 | 2019年3月 | 13 | 2000 | 2500 |
| 香蕉 | 2019年5月 | 10 | 2000 | 2000 |
| 香蕉 | 2019年4月 | 12 | 2400 | 2700 |
| 香蕉 | 2019年3月 | 13 | 2300 | 2700 |
行转列(pivot):月份旋转成横向标题,三个指标随月份展开
<pivot start-column="sale_count" end-column="total_amt"
group-columns="fruit_name" category-columns="order_month" />
| 品类 | 3月笔数 | 3月数量 | 3月金额 | 4月笔数 | 4月数量 | 4月金额 | 5月笔数 | 5月数量 | 5月金额 |
|---|---|---|---|---|---|---|---|---|---|
| 香蕉 | 13 | 2300 | 2700 | 12 | 2400 | 2700 | 10 | 2000 | 2000 |
| 苹果 | 13 | 2000 | 2500 | 11 | 1900 | 2600 | 12 | 2000 | 2400 |
分组汇总(summary):任意层级小计/总计直接算好
<summary sum-columns="sale_count,sale_quantity,total_amt" reverse="true">
<global sum-label="总计" label-column="fruit_name" />
<group group-column="fruit_name" sum-label="小计" label-column="fruit_name" />
</summary>
reverse="true" 让汇总行排在组前,直接输出财务要的带合计行结果:
| 品类 | 销售月份 | 销售笔数 | 销售数量(吨) | 销售金额(万元) |
|---|---|---|---|---|
| 总计 | 71 | 12600 | 14900 | |
| 小计 | 36 | 5900 | 7500 | |
| 苹果 | 2019年5月 | 12 | 2000 | 2400 |
| 苹果 | 2019年4月 | 11 | 1900 | 2600 |
| 苹果 | 2019年3月 | 13 | 2000 | 2500 |
| 小计 | 35 | 6700 | 7400 | |
| 香蕉 | 2019年5月 | 10 | 2000 | 2000 |
| 香蕉 | 2019年4月 | 12 | 2400 | 2700 |
| 香蕉 | 2019年3月 | 13 | 2300 | 2700 |
同比环比(cols-chain-relative):先旋转再算"比上月"
<pivot start-column="sale_count" end-column="total_amt"
group-columns="fruit_name" category-columns="order_month" />
<cols-chain-relative group-size="3" relative-indexs="1,2" start-column="1" format="#.00%" />
环比列自动插入、百分比按 format 格式化(首月无上期留空):
| 品类 | 4月数量 | 4月比上月 | 4月金额 | 4月比上月 | 5月数量 | 5月比上月 | 5月金额 | 5月比上月 |
|---|---|---|---|---|---|---|---|---|
| 香蕉 | 2400 | 4.30% | 2700 | 0.00% | 2000 | -16.70% | 2000 | -26.00% |
| 苹果 | 1900 | -5.10% | 2600 | 4.00% | 2000 | 5.20% | 2400 | -7.70% |
同一族能力还包括:列转行(unpivot,反向旋转)、行环比(rows-chain-relative)、树形排序汇总(tree-sort:地区树自动排序、叶子节点销量逐层向上汇总)、分组拼接(link:代替各库方言不一的 group_concat/listagg)、以及支持中文大写金额等财务格式化——管理类报表的高频诉求,基本都有现成答案。
五、跨数据库:25 种方言,产品化项目的底气
- 25 种方言覆盖 MySQL、Oracle、PostgreSQL、SQL Server、DB2、SAP HANA、达梦、金仓、瀚高、OceanBase、TiDB、ClickHouse、Doris、StarRocks、TDengine 等——常规 OLTP、国产数据库、OLAP 全都接;
- 函数自动替换:
functionConverts=default一行配置,nvl/concat/to_char/datediff 等 28 个函数按目标库自动转换,Oracle 写法的 SQL 在 MySQL 上照常跑(6.0.9 刚完成一轮真库实证修正:HH 时间口径、to_date 截断、trim 形态等十余处静默错值被真库反证修复); - 多方言 sqlId:
sqlId_mysql/sqlId_oracle按当前库自动匹配,个别库的特化 SQL 有地方放; - redoDataSources 多库重放:开发期让同一查询在其他库上重放执行,跨库适配天天验证而不是上线赌命;
- 虚表宏
@virtualTable():"常量行 union 真实查询"的 dual 差异一次写对。
六、企业级特性:中大型系统的标配一次给齐
分库分表(注解 + xml 双模式策略声明,自动路由)、多租户(@Tenant 统一过滤与赋值)、数据权限(参数注入 + 越权访问校验)、数据安全(RSA 字段加解密存储、手机号/卡号脱敏含离散脱敏)、SQL 拦截器(集中改写 SQL)、慢 SQL 阈值告警与收集上报、JSON/空间几何等扩展类型的 TypeHandler。这些能力对单表 lambda 查询同样自动生效——因为都长在内核执行链路上。
七、工程化:从代码生成到 Native 镜像
- quickvo 代码生成(独立工具 + maven 插件):连库生成带注解 POJO/VO,支持 lombok/swagger;
- autoDDL:POJO 直接建表,含分区、MPP 库专用生成器;
- debug 模式:SQL/参数/耗时全打印,sql 文件改动热加载;
- GraalVM Native:6.0.8 重写 AOT 支持,showcase 实测 native 镜像启动 0.05 秒,CRUD/缓存翻译/lambda 全链路可用,云原生与信创场景直接受益;
- 四种接入形态:Spring Boot starter / 传统 Spring / Solon / 纯 Java(无容器直接跑);
- JSON 库可插拔(6.0.7 起):fastjson2/jackson2/jackson3 自动探测,不强绑任何一家。
写在最后
把 SqlToy 的能力面铺开后会发现,它的主线始终是同一个:让简单的事保持简单(对象操作、lambda 条件),让复杂的事有尊严(SQL 模式、算法化分析),让产品化的事不焦虑(25 种方言、多库重放、native 镜像)。近二十年的金融与大数据场景锤炼,换来的是一份"遇到什么问题,这里已经有答案"的从容。
三分钟看懂:在线文档 | 十分钟跑通:sqltoy-quickstart(clone 后 mvn test,内置 H2 + 9 节课程)| 全功能演示:sqltoy-showcase
| 资源 | 地址 |
|---|---|
| GitHub | https://github.com/sagframe/sagacity-sqltoy |
| Gitee | https://gitee.com/sagacity/sagacity-sqltoy |
| 在线文档(128 页中英双语) | https://sagframe.github.io/sqltoy-docs/ |
<dependency>
<groupId>com.sagframe</groupId>
<artifactId>sagacity-sqltoy-spring-starter</artifactId>
<version>6.0.9</version>
</dependency>