首页 > 开源 > SqlToy-ORM 能力全景介绍

SqlToy-ORM 能力全景介绍

OSChina资讯 2026-10-23 11:40 3 阅读 查看原文

一个 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>