内容
活动
关注

终于有人把 Java 动态 SQL 写舒服了!支持任意嵌套、分页、一对多,你还在手搓 SQL 吗?

简介: 基于JDBC的动态SQL

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 自动生成!难免有疏漏之处,敬请见谅~

目录
相关文章
|
4月前
|
JSON 关系型数据库 Apache
十亿 JSON 秒级响应:Apache Doris vs ClickHouse,Elasticsearch,PostgreSQL
JSONBench 是一个为 JSON 数据而生的数据分析 Benchmark,在默认设置下,Doris 的性能表现是 Elasticsearch 的 2 倍,是 PostgreSQL 的 80 倍。调优后,Doris 查询整体耗时降低了 74%,对比原榜单第一的 ClickHouse 产品实现了 39% 的领先优势。本文详细描述了调优思路与 Doris 调优前后的性能表现,欢迎阅读了解~
674 0
十亿 JSON 秒级响应:Apache Doris vs ClickHouse,Elasticsearch,PostgreSQL
|
数据库
mybatisplus返回指定字段的两种方式
mybatisplus返回指定字段的两种方式
931 1
|
小程序 开发工具 开发者
微信开发者工具使用教程
微信开发者工具使用教程
|
4月前
|
JSON 数据可视化 计算机视觉
大语言模型也可以进行图像分割:使用Gemini实现工业异物检测完整代码示例
本文将通过一个实际应用场景——工业传送带异物检测,详细介绍如何利用Gemini的图像分割能力构建完整的解决方案。
171 2
大语言模型也可以进行图像分割:使用Gemini实现工业异物检测完整代码示例
|
4月前
|
数据采集 缓存 监控
八年电商开发血泪史:淘宝评论API的接口处理
本文分享了一位电商开发者八年对接淘宝评论API的实战经验,涵盖接口签名、限流控制、数据清洗、情感分析及实时监控等实用技巧,并附有完整代码示例。
|
SQL 消息中间件 存储
Flink报错问题之Flink报错:Table sink 'a' doesn't support consuming update and delete changes which is produced by node如何解决
Flink报错通常是指在使用Apache Flink进行实时数据处理时遇到的错误和异常情况;本合集致力于收集Flink运行中的报错信息和解决策略,以便开发者及时排查和修复问题,优化Flink作业的稳定性。
|
3月前
|
SQL 关系型数据库 Apache
从 Flink 到 Doris 的实时数据写入实践 —— 基于 Flink CDC 构建更实时高效的数据集成链路
本文将深入解析 Flink-Doris-Connector 三大典型场景中的设计与实现,并结合 Flink CDC 详细介绍了整库同步的解决方案,助力构建更加高效、稳定的实时数据处理体系。
1590 0
从 Flink 到 Doris 的实时数据写入实践 —— 基于 Flink CDC 构建更实时高效的数据集成链路
|
3月前
|
API 数据处理 索引
电商API详解
本内容介绍了主流开放API的电商平台及其可获取的数据类型,如用户、商品、店铺及交易信息等,支持智能选品、极速上架、高效定价等功能。同时详解了API调用步骤,并提供电商应用开发指导。
|
5月前
|
SQL 关系型数据库 MySQL
MySQL 5.6/5.7 DDL 失败残留文件清理指南
通过本文的指南,您可以更安全地处理 MySQL 5.6 和 5.7 版本中 DDL 失败后的残留文件,有效避免数据丢失和数据库不一致的问题。
|
4月前
|
NoSQL Redis
功能最全面最快的redis备份工具
现在使用云的人越来越多,redis数据跨云备份,跨云迁移的需求也越来越多,备份这么重要的东西,肯定要选最好的客户端。现在做数据备份和恢复的产品,也就yunedit-redis这款redis客户端能考虑所有这些场景,而且导出导入速度也是做到客户端导出比在服务端导出还快的效果。实测导出20万数据只用时十多秒。在备份这个领域,yunedit-redis应该是最好的。
下一篇