Java 动态 SQL 构建新选择:告别繁琐 XML,轻量、强类型的 dynamic-sql2 框架来了!
在日常开发中,动态 SQL 一直是老生带的问题:
- XML 拼接 SQL,开发体验差、易出错
- 直接字符串拼接 SQL,存在严重安全障碍
- ORM 框架虽好,但复杂查询时往往换不起来
有没有一种方案,既能:
✅ 保留原生 SQL 的灵活性
✅ 摆脱 XML、字符串的繁琐与不安全
✅ 提供类型安全、链式流畅的开发体验
答案是:dynamic-sql2
,一款专注 SQL 构建的轻量级 Java 框架!
💡 dynamic-sql2 简介
dynamic-sql2
是我在实战项目中总结提炼的一套 SQL 动态构建方案,专注于:
✅ DSL 风格链式 API,干净利落,无需 XML 配置
✅ 类型安全表达式,避免拼写错误,IDE 友好提示
✅ 多数据库方言支持,目前已兼容 MySQL、Oracle、DB2
✅ 子查询、窗口函数、CTE、递归查询 一应俱全
✅ Spring Boot Starter 集成,开盘即用
目标是让开发者轻松、安全地构建复杂 SQL,同时不被 ORM 绑定,保持最大灵活性。
🛠 快速体验示例
dynamic-sql2 提供强类型、链式、安全、灵活的 SQL 构建能力,支持:
✅ 子查询、JSON_TABLE、窗口函数、动态排序
✅ 一对多结果映射、集合分页、Map结构输出
✅ 自定义 SQL 函数,任意嵌套组合拓展
以下通过典型案例,快速了解其强大特性。
添加Maven依赖
目前0.1.1是最新版,0.1.2正在开发中。
<!-- springboot 引用-->
<dependency>
<groupId>com.dynamic-sql</groupId>
<artifactId>dynamic-sql2-spring-boot-starter</artifactId>
<version>0.1.1</version>
</dependency>
<!-- 单体项目引用-->
<dependency>
<groupId>com.dynamic-sql</groupId>
<artifactId>dynamic-sql2</artifactId>
<version>0.1.1</version>
</dependency>
🔍 查询
本框架支持链式、类型安全、任意函数嵌套、分页、子查询、窗口函数、一对多映射等能力,适合复杂业务场景。
1. 子查询结合 JSON_TABLE 解析嵌套数据
适用场景:从订单JSON字段中解析商品信息,并进行联表查询。
List<UserOrderView> list = sqlContext.select()
.column(User::getUserId)
.column(User::getName)
.column("total.total_amount")
.column("p.product_name", "product")
.from(User.class)
.join(select -> select
.column(Order::getUserId)
.column(new Sum(Order::getTotalAmount), "total_amount")
.from(Order.class)
.groupBy(Order::getUserId),
"total",
on -> on.andEqualTo(User::getUserId, bindAlias("total", Order::getUserId)))
.leftJoin(select -> select
.column(Product::getProductId)
.column("jt.order_id")
.from(Product.class)
.join(() -> new JsonTable("o", "order_details", "$.items[*]",
JsonColumn.builder().column("product").dataType("VARCHAR(100)").jsonPath("$.productName").build()),
"jt",
on -> on.andEqualTo(bindAlias("jt", "product"), Product::getProductName)),
"p",
on -> on.andEqualTo(Order::getOrderId, bindAlias("p", Order::getOrderId)))
.fetch(UserOrderView.class)
.toList();
2. 集合分页查询(子查询集合)
适用场景:针对集合类结果,进行分页截取,避免数据过大传输。
CollectionPage<List<ProductView>, ProductView> page = PageHelper.ofCollection(1, 10)
.selectPage(() -> sqlContext.select()
.column(Product::getProductId)
.column(Product::getProductName)
.from(Product.class)
.fetch(ProductView.class)
.toList());
3. 一对多集合映射
适用场景:将一对多关联结果,映射为集合字段返回,常见于主表附属数据展示。
List<CategoryView> list = sqlContext.select()
.column(Category::getCategoryId)
.column(Category::getCategoryName)
.collectionColumn(
KeyMapping.of(Category::getCategoryId, Product::getCategoryId),
mapper -> mapper.column(Product::getProductId).column(Product::getProductName),
"productList")
.from(Category.class)
.join(Product.class, on -> on.andEqualTo(Category::getCategoryId, Product::getCategoryId))
.fetch(CategoryView.class)
.toList();
4. 动态排序(前端字段控制)
适用场景:用户指定排序字段,安全构建 SQL,防止 SQL 注入。
String sortField = "createTime";
List<User> list = sqlContext.select()
.allColumn()
.from(User.class)
.orderBy(sortField, SortOrder.DESC)
.fetch()
.toList();
5. 窗口函数 DenseRank 示例
适用场景:分组内排名,支持复杂统计分析。
List<Map<String, Object>> list = sqlContext.select()
.column(Product::getProductId)
.column(Product::getProductName)
.column(new DenseRank(), over -> over
.partitionBy(Product::getCategoryId)
.orderBy(new Sum(Product::getPrice), SortOrder.DESC),
"rank")
.from(Product.class)
.groupBy(Product::getProductId, Product::getCategoryId)
.fetchOriginalMap()
.toList();
6. 分页 + 聚合 + 窗口函数组合查询
适用场景:结合分页、统计、排名,适用于排行榜、分析报表。
PageInfo<UserView> page = PageHelper.of(1, 10).selectPage(() -> sqlContext.select()
.column(User::getUserId)
.column(User::getName)
.column(new Sum(Order::getTotalAmount), "totalSpent")
.column(new DenseRank(), over -> over
.orderBy(new Sum(Order::getTotalAmount), SortOrder.DESC),
"rank")
.from(User.class)
.join(Order.class, on -> on.andEqualTo(User::getUserId, Order::getUserId))
.groupBy(User::getUserId)
.fetch(UserView.class));
7. 日期格式化分组(年月聚合)
适用场景:基于日期字段,按指定格式聚合,适合按月统计。
List<UserDateView> list = sqlContext.select()
.column(User::getUserId)
.column(new DateFormat(User::getRegistrationDate, "%Y-%m"))
.from(User.class)
.groupBy(User::getUserId)
.groupBy(new DateFormat(User::getRegistrationDate, "%Y-%m"))
.limit(10)
.fetch(UserDateView.class)
.toList();
8. 算术函数任意嵌套拓展
适用场景:复杂计算需求,函数无限组合。
Map<String, Object> result = sqlContext.select()
.column(new Round(new Sum(User::getUserId), 3).divide(2))
.column(new Round(new Sum(User::getUserId).divide(2), 3))
.column(new Round(new Sum(User::getUserId).divide(new Count(User::getUserId)), 3))
.from(User.class)
.fetchOriginalMap()
.toOne();
💾 新增
1. 仅插入非空字段
适用场景:字段较多,部分字段可为 null,仅插入有值字段,避免空值写入。
Product product = new Product();
product.setProductName("菠萝手机-insertSelective");
product.setPrice(BigDecimal.valueOf(6.66));
product.setStock(666);
product.setCreatedAt(new Date());
product.setCategoryId(1);
// 仅插入非空字段,保持 SQL 简洁
int rows = sqlContext.insertSelective(product);
System.out.println("影响行数:" + rows);
2. 字段即使为空也要插入
适用场景:部分字段即使为空也要插入,即使该字段为null,这在特定场景下特别有用
Product product = new Product();
product.setProductName("菠萝手机-insertSelective2");
product.setPrice(BigDecimal.valueOf(6.66));
product.setStock(666);
product.setCreatedAt(new Date());
product.setCategoryId(1);
// 强制更新Attributes字段,即使该字段为null,这在特定场景下特别有用
int rows = sqlContext.insertSelective(product, Arrays.asList(Product::getAttributes, Product::getProductId));
System.out.println("影响行数:" + rows);
3. 全字段插入
适用场景:所有字段均已赋值,直接全字段写入,确保数据完整性。
Product product = new Product();
product.setProductName("菠萝手机-insert");
product.setPrice(BigDecimal.valueOf(6.66));
product.setStock(666);
product.setCreatedAt(new Date());
product.setCategoryId(1);
// 全字段插入
int rows = sqlContext.insert(product);
System.out.println("影响行数:" + rows);
4. 单条多次插入(分批发送)
适用场景:大批量数据写入,兼容不支持多值插入的数据库,适合海量数据分批。
List<Product> products = new ArrayList<>();
for (int i = 1; i <= 10_000; i++) {
Product product = new Product();
product.setProductName("菠萝手机-insertBatch/" + i);
product.setPrice(BigDecimal.valueOf(6.66));
product.setStock(666);
product.setCreatedAt(new Date());
product.setCategoryId(1);
products.add(product);
}
long start = System.currentTimeMillis();
int rows = sqlContext.insertBatch(products);
long duration = System.currentTimeMillis() - start;
System.out.println("耗时:" + duration + "ms");
System.out.println("总插入行数:" + rows);
5. 单 SQL 多值插入(高性能)
适用场景:数据库支持多值插入,批量写入性能更优,减少网络和 SQL 解析开销。
List<Product> products = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
Product product = new Product();
product.setProductName("菠萝手机-insertMultiple/" + i);
product.setPrice(BigDecimal.valueOf(6.66));
product.setStock(666);
product.setCreatedAt(new Date());
product.setCategoryId(1);
products.add(product);
}
long start = System.currentTimeMillis();
int rows = sqlContext.insertMultiple(products);
long duration = System.currentTimeMillis() - start;
System.out.println("耗时:" + duration + "ms");
System.out.println("总插入行数:" + rows);
6. 插入视图对象
适用场景:视图、联表映射对象存储,框架自动识别可插入字段,忽略虚拟属性。
UserAndOrderView view = new UserAndOrderView();
view.setPrice(BigDecimal.ONE);
// 插入映射视图对象,仅写入真实存在的物理字段
sqlContext.insertMultiple(Collections.singleton(view));
🔧 更新
1. 根据主键全字段更新
适用场景:明确指定主键,更新所有字段,确保数据完整覆盖。
Product product = new Product();
product.setProductId(20);
product.setProductName("New Coffee Maker");
product.setCategoryId(4);
product.setCreatedAt(new Date());
product.setPrice(BigDecimal.TEN);
product.setStock(123);
// 主键全字段更新
int rows = sqlContext.updateByPrimaryKey(product);
System.out.println(rows);
2. 主键更新,忽略 null 字段
适用场景:更新有值字段,指定字段即使为空也会强制更新为null。
Product product = new Product();
product.setProductId(20);
product.setProductName("New Coffee Maker3");
product.setCategoryId(4);
product.setCreatedAt(new Date());
product.setPrice(BigDecimal.valueOf(879));
product.setStock(222);
// 强制 attributes 字段更新,其他字段根据条件更新
int rows = sqlContext.updateSelective(product, Collections.singletonList(Product::getAttributes),
w -> w.andEqualTo(Product::getProductId, 20));
System.out.println(rows);
4. 自动插入或更新(全字段)
适用场景:数据存在则更新,不存在则新增,适合唯一约束表,防止重复数据。
Product product = new Product();
product.setProductName("New Coffee Maker");
product.setCategoryId(4);
product.setCreatedAt(new Date());
product.setPrice(BigDecimal.TEN);
product.setStock(123);
// 自动插入或更新
int rows = sqlContext.upsert(product);
System.out.println(rows);
5. 自动插入或更新,部分字段控制
适用场景:Upsert 同时,强制部分字段,灵活控制写入范围。
Product product = sqlContext.select().allColumn().from(Product.class)
.where(w -> w.andEqualTo(Product::getProductId, 7))
.fetch().toOne();
product.setProductName("MacBook Pro2");
product.setCreatedAt(new Date());
product.setAttributes("{\"a\":1}");
product.setProductId(null); // 强制模拟新增
// 强制 attributes 字段更新,其他字段根据条件更新
int rows = sqlContext.upsertSelective(product, Collections.singletonList(Product::getAttributes));
System.out.println(rows);
6. 自动插入或更新
适用场景:大批量数据同步,存在更新,不存在新增,提升效率。
List<Product> products = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
Product product = new Product();
product.setProductName("New Coffee Maker " + i);
product.setCategoryId(4);
product.setCreatedAt(new Date());
product.setPrice(BigDecimal.TEN);
product.setStock(123);
products.add(product);
}
// 批量自动插入或更新
int rows = sqlContext.upsertMultiple(products);
System.out.println(rows);
⚙️ 删除
1. 按单个主键删除
测试通过 deleteByPrimaryKey
方法,删除 Product
表中指定主键的记录。
void deleteByPrimaryKey() {
int pkValue = 5011;
int i = sqlContext.deleteByPrimaryKey(Product.class, pkValue);
System.out.println(i);
}
2. 按多个主键批量删除
测试通过 deleteByPrimaryKey
方法,删除 Product
表中指定主键集合的多条记录。
void deleteByPrimaryKey2() {
int i = sqlContext.deleteByPrimaryKey(Product.class, Arrays.asList(5001, 5002, 5003, 5004, 5005));
System.out.println(i);
}
3. 根据条件删除
根据条件删除,可组合任意条件
void delete() {
int i = sqlContext.delete(Product.class, where -> {
where.andEqualTo(Product::getProductId, 1);
where.orCondition(nestedWhere -> {
nestedWhere.andEqualTo(Product::getProductId, 3);
nestedWhere.orEqualTo(Product::getProductId, 4);
});
});
System.out.println(i);
}
4. 删除全部
直接删除表中所有的数据
void deleteAll() {
int i = sqlContext.delete(Product.class, null);
System.out.println(i);
}
这里列取了常见的操作,因篇幅过长不便书写,更多内容请直接访问 GitHub项目地址
🔥 为什么选择 dynamic-sql2?
相比传统拼接、XML 配置,dynamic-sql2
的优势十分明显:
特性 | 说明 |
---|---|
轻量级 | 无需繁琐配置,纯 Java 链式调用,快速上手 |
强类型安全 | 基于实体类属性表达,IDE 全程语法提示,避免拼写错误 |
多方言兼容 | 内置 MySQL、Oracle、DB2,未来支持更多数据库 |
高级 SQL 支持 | 子查询、CTE、递归、窗口函数等复杂 SQL 全面覆盖 |
良好扩展性 | 内置拦截器机制,方便自定义 SQL 日志、权限控制 |
Spring Boot 适配 | Starter 集成,Spring 项目无缝接入 |
特别适合对 SQL 灵活性、安全性、数据库兼容性有较高要求的系统。
🏢 典型适用场景
✔ 金融系统:动态报表、权限控制、复杂统计 SQL 构建
✔ 数据中台:多数据源环境下灵活拼接 SQL、动态过滤条件
✔ 传统项目:不想写繁琐 XML,又想控制 SQL 细节的场景
✔ 需要支持递归、窗口函数、子查询的中大型系统
🗺️ 未来规划
项目仍在持续优化,规划包括:
- PostgreSQL、SQL Server 等更多数据库支持
- 泛型推导进一步优化,提升 IDE 语法提示体验
- 官方文档、示例项目完善,降低学习成本
- 提供性能基准测试与优化方案
- 吸引更多社区开发者共同参与,打造国产开源 SQL 构建利器
📦 项目地址
欢迎试用、Star、Fork、提交反馈:
👉 GitHub(推荐): https://github.com/pengweizhong/dynamic-sql2
👉 Gitee(加速地址): https://gitee.com/pengweizhong/dynamic-sql2
项目适用于任何 Java 8+ 环境,Spring Boot 用户可通过 Starter 快速集成。
🗣️ 交流 & 加入社区
我已建立技术交流群,欢迎交流:
- 实战经验分享
- 动态 SQL 场景讨论
- 需求建议、功能共建
- 遇到 Bug,群内及时响应
扫码加群 / 私信我获取群二维码:
_(这里预留微信群二维码位置或联系信息)_TODO
💬 结语
SQL 构建应该是简单、安全、灵活的。
dynamic-sql2 正是为此而生,专注帮助 Java 开发者用更优雅、更可靠的方式管理动态 SQL,摆脱 XML、字符串拼接带来的痛点。
如果你也在为动态 SQL 头疼,欢迎体验 dynamic-sql2,让开发回归本质,让 SQL 更加清晰高效。
🎯 下一步推荐
如果你对 dynamic-sql2 感兴趣,可以继续关注:
- 下一篇:《5分钟快速上手 dynamic-sql2 实战教程》
- 后续更新:复杂分页、动态排序、子查询、窗口函数实战
- 社区发布:定期分享技术干货,邀请你一起完善框架
欢迎评论、点赞、关注,一起打造更优秀的国产动态 SQL 框架!
声明:该文章由Chat GPT 自动生成!难免有疏漏之处,敬请见谅~
声明:该文章由Chat GPT 自动生成!难免有疏漏之处,敬请见谅~
声明:该文章由Chat GPT 自动生成!难免有疏漏之处,敬请见谅~