首页>后端>正文

SqlToy ORM 架构拆解:动态SQL与跨库报表

Feng 41 阅读 后端

三层 API 的分工逻辑

SqlToy 不逼你在 JPA 和 MyBatis 之间选边。它把调用面切成三层,复杂度递增,各管一段。

主键定位的单对象增删改查走对象操作,lightDao.save/update/load 即可。单表或简单 join 的条件查询用 lambda 链,6.0.8 起内置。复杂查询、报表、多分支动态条件则落到 sqlId 加动态段。三层能在同一个 Service 里混着用,没有选型包袱。

接入方式有四种:Spring Boot starter、传统 Spring、Solon、纯 Java 无容器。当前版本 6.0.9。Spring Boot 3.x 环境下注意 starter 走 jakarta 命名空间,JDK 17 起步;JDK 21 上 byte-buddy 生成的代理实体在 6.0.9 已做兼容,但升级前建议先用 quickstart 跑一遍 mvn test 验证代理链路,别直接上生产。

对象操作补的两个坑

上手三步: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 = ...;
StockLedgerVO latest = lightDao.updateSaveFetch(ledger, (entity, rowIndex) -> {
 int available = entity.getQuantity();
 if (available < deductQty) {
 throw new IllegalArgumentException("库存不足! 可用: " + available + ", 需扣: " + deductQty);
 }
 entity.setQuantity(available - deductQty);
}, "productId", "warehouseId");

实体回调形态依赖 byte-buddy 生成代理实体,另有直接操作 ResultSet 的 UpdateRowHandler 形态。这里有个生产边界要说清:updateSaveFetch 的锁查询在默认隔离级别下走行锁,若两个事务以不同顺序锁定多行台账,仍可能死锁。批量扣减时按 productId 排序后再逐条执行,或把隔离级别提到 REPEATABLE READ 并配合重试,是更稳的做法。与之互补的 updateFetch 面向记录必然存在、需要自由写查询条件和指定锁策略的场景。

对象面还包括 @OneToOne/@OneToMany 级联、一条 join SQL 自动封装父子对象、树形表路由、十余种主键策略(22/26 位有序数字、雪花、UUIDv7、ULID、redis 单据号)、公共字段统一赋值、@DataVersion 乐观锁。

动态 SQL 的剔除语义

一个符号讲完条件逻辑:#[...] 内只要有一个参数为 null,整段剔除。支持多层嵌套,@if() 是万能钥匙。

<sql id="show_case">
<filters>
 <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 同样逻辑要写一串 if 加 foreach。SqlToy 版本剥掉 #[] 就是合法 SQL,能直接贴进数据库客户端调试;参数全走 PreparedStatement 绑定,防注入是天然的。中等复杂度还可以用 Java 文本块把 SQL 写进代码,与 xml 完全同构。

缓存翻译与分页的四层叠加

码值转名称不用关联表。员工表里的 staffId 出结果时直接翻译成姓名,字典 SEX_TYPE 翻译成男/女,靠可增量刷新的本地缓存(ehcache/caffeine)。对象属性打注解即可:

@Translate(cacheName = "staffIdName", keyField = "staffId")
private String staffName;

配套的 cache-arg 反向用缓存把名称模糊搜索变成精准 key 匹配,替代 like。平台型电商百万级 SKU 场景用 FIFO 动态缓存,不做全量加载,未命中按需批量拉取,本地只留最热数据。

分页优化四层可叠加。第一层自动改写 count 语句,智能剔除外层 order by,不是简单套 count(1) from (原sql)。第二层 page-optimize 缓存,相同条件一定周期内不重复 count,分页从两次查询降到 1.3~1.5 次。第三层 @fast() 先取单页数据再关联。第四层 parallel 让 count 与单页数据并行查。

<sql id="sqltoy_staff_page">
<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,
t.SEX_TYPE sexTypeName,
t.ENTRY_DATE
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>

page-optimize 有个必须知道的副作用:alive-seconds 窗口内 count 走缓存,如果这期间数据有增删,返回的总条数与实际不符。判定方法是关掉 page-optimize 对比两次查询结果,或把 alive-seconds 压到业务可接受的范围。列表页容忍度低,报表页可以放宽。

查询全家桶还有并行查询、流式查询(fetchStream 逐行回调不撑爆内存)、存储过程含 out 参数与多结果集、取 Top/随机记录、链式入口、20 多种 filters 参数预处理器。

报表在内存里算完

这类能力的思路是用算法代替 SQL,内存中完成,跨库行为一致。一份查询结果加几行配置直接出报表,不用 ETL。

行转列 pivot 把月份旋成横向标题,三个指标随月份展开:

<pivot start-column="sale_count" end-column="total_amt"
group-columns="fruit_name" category-columns="order_month"/>

分组汇总 summary 直接算好任意层级小计总计,reverse="true" 让汇总行排在组前,输出财务要的带合计行结果:

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

同比环比先旋转再算比上月,环比列自动插入,百分比按 format 格式化,首月无上期留空:

<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%"/>

同族还有列转行 unpivot、行环比 rows-chain-relative、树形排序汇总 tree-sort(地区树自动排序、叶子销量逐层向上汇总)、分组拼接 link(代替各库方言不一的 group_concat/listagg)、中文大写金额格式化。

跨库与产品化配置

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 写法在 MySQL 上照常跑。6.0.9 刚完成一轮真库实证修正,HH 时间口径、to_date 截断、trim 形态等十余处静默错值被真库反证修复,这类静默错值不报错但结果错,升级时值得关注。

多方言 sqlId 按当前库自动匹配 sqlId_mysql、sqlId_oracle,个别库的特化 SQL 有地方放。redoDataSources 多库重放让开发期同一查询在其他库上重放执行,跨库适配天天验证而不是上线赌命。虚表宏 @virtualTable() 把常量行 union 真实查询的 dual 差异一次写对。

企业级特性一次给齐:分库分表注解加 xml 双模式策略声明自动路由、多租户 @Tenant 统一过滤赋值、数据权限参数注入加越权校验、RSA 字段加解密存储、手机号卡号脱敏含离散脱敏、SQL 拦截器集中改写、慢 SQL 阈值告警收集上报、JSON 与空间几何的 TypeHandler。这些能力对单表 lambda 查询同样生效,因为都长在内核执行链路上。

工程化链路

quickvo 代码生成有独立工具和 maven 插件两种形态,连库生成带注解 POJO/VO,支持 lombok 和 swagger。autoDDL 让 POJO 直接建表,含分区和 MPP 库专用生成器。debug 模式打印 SQL、参数、耗时,sql 文件改动热加载。

GraalVM Native 在 6.0.8 重写 AOT 支持,showcase 实测镜像启动 0.05 秒,CRUD、缓存翻译、lambda 全链路可用。JSON 库 6.0.7 起可插拔,fastjson2/jackson2/jackson3 自动探测,不强绑任何一家。

依赖坐标:

<dependency>
<groupId>com.sagframe</groupId>
<artifactId>sagacity-sqltoy-spring-starter</artifactId>
<version>6.0.9</version>
</dependency>

想快速验证,clone sqltoy-quickstart 后 mvn test,内置 H2 和 9 节课程。全功能演示看 sqltoy-showcase。

SqlToy ORM 架构拆解:对象操作、动态SQL与跨库报

ID:7023

Feng
这位作者很神秘,还没有填写简介。