28 Commits

Author SHA1 Message Date
7de2a7eec1 refactor: 将单列查询 Class 重载标记为过时,新增语义明确的方法
- 新增 queryValues(sql, args, Class) 替代 queryList(sql, args, Class)
- 新增 queryValue(sql, args, Class) 替代 queryFirst(sql, args, Class)
- 新增 queryValueOrDefault(sql, args, Class, defaultVal) 聚合查询便捷方法
- 旧方法标记 @Deprecated,委托至新方法,后续版本移除
- 更新 README 方法列表和示例代码
- 补充 queryValueOrDefault 单元测试 6 个
2026-06-18 02:58:43 +08:00
76bfff2d90 docs: 更新设计考量章节标题并补充内容 2026-06-18 02:52:37 +08:00
6f2882cd08 refactor!: 将工厂方法异常类型从 SQLException 改为 IllegalStateException
DefaultBeanRowMapper.of() 及 RowMapper.beanRowMapper() 在反射异常时
不再抛出受检异常 SQLException,改为抛出非受检异常 IllegalStateException。
同时优化相关 Javadoc 文档并同步更新测试断言。

BREAKING CHANGE: beanRowMapper() 和 of() 方法不再抛出 SQLException。
调用方 catch (SQLException e) 将静默失效,建议移除相关 catch 块或
改为 catch (IllegalStateException)。
2026-06-18 02:41:10 +08:00
5100fa5f1d docs: 添加示例代码 2026-06-17 23:27:43 +08:00
9abbef8b86 prepare 1.1.0 2026-06-17 21:24:03 +08:00
1f566ca488 release/1.0.0 [plusone/simple-jdbc#10 (Gitea)] 2026-06-17 21:16:38 +08:00
bb79777f0c docs: 修复文档错误 2026-06-17 21:06:40 +08:00
cf911c3756 release 1.0.0 2026-06-17 20:57:43 +08:00
f471c1a489 docs: 调整文档描述 2026-06-17 20:41:09 +08:00
f35cfac913 docs: 更新 DefaultBeanRowMapper 类文档注释,强调使用场景 2026-06-17 20:27:26 +08:00
76255bf3eb docs: 修改 README.md
- 移除标题中的emoji
- 新增“设计考量与边界”章节
2026-06-17 18:47:34 +08:00
aeb57dcdba refactor: 将中断控制逻辑内化到 BatchUpdateResult 中
- BatchUpdateResult 新增 quietly 字段,由 recordErrorBatch() 自行判断
  状态设置为 INTERRUPTED 或 COMPLETED_WITH_ERRORS
- 移除 interrupt() 方法,消除 recordErrorBatch → interrupt 的时序耦合
- 外部调用方仅需 break 控制循环,不再干预结果对象的状态流转
2026-06-17 17:27:13 +08:00
a0df4136f4 refactor!: 移除 plusone-commons 及 Guava 依赖,内化工具方法
完全移除对外部工具库 plusone-commons 和 Guava 的编译期依赖,
将所需功能内化实现,使项目成为真正的零依赖轻量级 JDBC 封装库。

- 新增 AssertTools: 断言工具(checkArgument / checkNotNull / checkState / checkCondition)
- 新增 NamingTools: 命名转换(camelToSnake)
- 新增 ThrowingConsumer / ThrowingPredicate: 可抛受检异常的函数式接口
- 新增 AssertToolsTests: 完整覆盖断言工具所有方法及边界场景
- pom.xml 移除 plusone-dependencies BOM,直接声明 jsr305 / test 依赖版本
- DefaultBeanRowMapper 使用 NamingTools.camelToSnake 替代 Guava CaseFormat
- ParamBuilder 内联 Optional 展开逻辑,去除 OptionalTools / CollectionTools
- JdbcOperationSupport 用 null/长度判断替代 ArrayTools.isEmpty/isNotEmpty
- NOTICE 移除第三方依赖声明;README 移除无依赖分支说明
- 测试:新增缩写映射(URL/XML/ID/HTML/HTTP)覆盖、TransactionException 构造器测试

BREAKING CHANGE: BatchUpdateStatus 不再实现 IWithIntCode 接口;
plusone-commons 和 Guava 不再作为传递依赖提供。
2026-06-17 17:21:31 +08:00
ad0fbb788e docs: 添加对无依赖分支(feature/no-dependencies)的特性和适用情况的说明 2026-06-06 00:59:40 +08:00
d740ad8467 docs: 优化 README 文档结构与内容
- 调整章节顺序
- 为所有二级及以下标题添加序号
- 移除示例代码注释中的序号,避免与标题序号冲突
- 新增「连接池集成」章节(HikariCP/Druid/DBCP 2),快速开始中添加引导提示
- 快速开始各小节末尾添加跳转指引,关联至对应详细章节
- 补充事务管理章节的内容
2026-06-06 00:43:06 +08:00
e577f72d4f docs: 新增 CHANGELOG.md 文件记录各版本更新内容 2026-06-05 22:31:27 +08:00
ef39c4323c test: 补充测试数据库初始化脚本的注释 2026-06-05 22:31:27 +08:00
d70e12e254 docs: 补充 ParamBuilderSimpleJdbcTemplate 的文档注释 2026-06-05 22:31:27 +08:00
721d0bbd5e test: 添加事务异常测试用例 2026-06-05 22:31:27 +08:00
0b3675d3a4 test: 添加 buildParamsTemporal 测试方法验证时间类型参数构建 2026-06-05 22:31:27 +08:00
8b4f5bac65 test: 重构测试类 UpdateTest,补全 Statement 路径覆盖
- 按 update / updateAndReturnKeys 分组,内部再按 PreparedStatement
  (有参数)和 Statement(无参数)子路径组织,提升可维护性
- 重命名 3 个测试方法以消除歧义:
    testUpdateWithNullParams → testUpdateWithNullElement
    testUpdateWithParamsIsNull → testUpdateWithParamsNull
    testUpdateAndReturnKeysWithParamsIsNull → testUpdateAndReturnKeysWithParamsNull
- 新增 2 个测试用例补全 params=null 的 Statement 路径覆盖:
    testUpdateWithParamsNull
    testUpdateAndReturnKeysWithParamsNull
2026-06-05 21:17:12 +08:00
486d0c98c7 fix: 修复 updateAndReturnKeys 执行无参 SQL 无法获取生成的 key 的问题 2026-06-05 21:12:25 +08:00
f5909818c3 refactor: JdbcOperationSupport 中无参数 SQL 改用 Statement 执行
update、updateAndReturnKeys、queryInternal 三个方法根据是否有参数,分别走 PreparedStatement(有参数)或 Statement(无参数),避免无参数时不必要的预编译开销
2026-06-05 20:49:17 +08:00
3753aafd61 refactor: 提取 ResultHandler.mapToList 消除 ResultSet 遍历重复代码 2026-06-05 20:28:16 +08:00
f323d04d57 refactor: ParamBuilder#buildParams 中为常用数据类型添加短路处理
提取 buildParams 中的 lambda 为 handleItem 方法,并在 Optional 系列检测之前,为 null、CharSequence、Number、Boolean、Temporal 等高频类型增加提前返回逻辑,提升批量参数构建时的处理效率。

附带行为变化:CharSequence 实现类(如 StringBuilder/StringBuffer)现在会通过 toString() 转为 String 后再传递。
2026-06-05 20:08:08 +08:00
eabd5d7f77 docs: 更新项目简介 2026-06-02 23:31:24 +08:00
152094029e docs: 更新 DefaultBeanRowMapper 的描述以避免歧义 2026-06-02 23:21:26 +08:00
5b643291eb docs: 更新 README.md 2026-06-02 23:21:12 +08:00
26 changed files with 2174 additions and 376 deletions

141
CHANGELOG.md Normal file
View File

@@ -0,0 +1,141 @@
# Changelog
## [1.1.0] - Unreleased
### ⚠️ 破坏性变更
- **`DefaultBeanRowMapper.of()` 不再抛出 `SQLException`**:工厂方法在反射异常时改为抛出非受检异常 `IllegalStateException`。调用方如果 `catch (SQLException e)` 包裹 `of()` 调用,该捕获将失效,需移除相关 `catch` 块或改为捕获 `IllegalStateException`
### 新增
- `queryValueOrDefault(sql, params, Class<T>, T defaultValue)`:查询单行单列,结果为空时返回指定默认值
- `queryValueOrDefault(sql, Class<T>, T defaultValue)`:无参数重载
- 补充 `QueryTest``queryValueOrDefault` 单元测试 6 个
### 重构
**将单列查询方法标记为过时,消除 Class 参数重载歧义**
- `queryList(sql, params, Class<T>)` → 已过时,请使用 `queryValues(sql, params, Class<T>)`
语义明确为"多行单列 → 值列表",不与整行 `RowMapper` 重载混淆
- `queryFirst(sql, params, Class<T>)` → 已过时,请使用 `queryValue(sql, params, Class<T>)`
语义明确为"单行单列 → 单值",不与整行 `RowMapper` 重载混淆
- 同时将对应的无参数重载标记为过时:
- `queryList(sql, Class<T>)` → 请使用 `queryValues(sql, Class<T>)`
- `queryFirst(sql, Class<T>)` → 请使用 `queryValue(sql, Class<T>)`
- 旧方法将在后续版本中移除
### 文档
- 优化 `DefaultBeanRowMapper` 类注释,明确性能限制和使用建议
---
## [1.0.0] - 2026-06-17
### 重构
- 移除 `plusone-commons` 及 Guava 依赖,内化 `AssertTools``NamingTools``ThrowingConsumer``ThrowingPredicate` 工具类
-`batchUpdate` 中断时对 `BatchUpdateResult` 的变更逻辑内化到 `BatchUpdateResult`
### 测试
- 添加 `ParamBuilderTest#buildParamsTemporal` 测试方法验证时间类型参数构建
- 添加 `TransactionTest` 事务异常测试用例
- 补充测试数据库初始化脚本注释
### 文档
- 新增 `CHANGELOG.md` 文件记录各版本更新内容
- 优化 README 文档结构与内容
- 补充 `ParamBuilder``SimpleJdbcTemplate` 的文档注释
- 更新 `DefaultBeanRowMapper` 类文档注释,强调使用场景
---
## [1.0.0-RC3] - 2026-06-05
### 新增
- `ResultHandler` 新增静态工厂方法 `mapToList(RowMapper)`,消除各处 `ResultSet` 遍历重复代码
### 重构
- `JdbcOperationSupport` 中无参数 SQL 统一改用 `Statement` 执行,避免创建空参数 `PreparedStatement`
- `ParamBuilder#buildParams` 为常用数据类型(`CharSequence`/`Number`/`Boolean`/`Temporal`)添加短路处理,优化参数构建性能
### 测试
- 重构 `UpdateTest`,补全 `Statement` 路径覆盖
### 文档
- 更新 README.md 使用说明和示例
- 更新 `DefaultBeanRowMapper` 类文档以避免歧义
- 更新项目简介
---
## [1.0.0-RC2] - 2026-05-31
### 新增
- `TransactionTemplate``SimpleJdbcTemplate` 中分离为独立类,封装事务生命周期管理(开启/提交/回滚/恢复自动提交)
- `SimpleJdbcTemplate.transaction()` 暴露 `TransactionTemplate` 实例
- 新增 `ParamBuilderTest` 单元测试
- 新增 `Instant` 类型参数端到端测试
### 重构
- 简化 `ParamBuilder` 参数构建逻辑
- 优化批量更新批次内索引计算逻辑
### 文档
- 完善 `SimpleJdbcTemplate` 类文档注释
- 更新批量更新相关类(`BatchUpdateResult`/`BatchUpdateStatus`/`BatchUpdateErrorInfo`)的 JavaDoc
- 更新 README 使用说明和示例
### 其他
- 版权年份由固定范围更新为包含当前时间的表述
---
## [1.0.0-RC1] - 2026-05-29
项目首个正式发布的候选版本2023-07 从 plusone-commons 独立开发)。
### 新增
**核心框架**
- `SimpleJdbcTemplate` 核心模板类,封装 JDBC 连接管理、异常处理和资源释放
- `JdbcOperations` 接口规范,定义统一的数据库操作 API
- `JdbcOperationSupport` 静态工具类,封装底层 `PreparedStatement` / `Statement` 操作
**查询**
- `query` + `ResultHandler`:自定义 `ResultSet` 处理
- `queryList`:返回列表(支持 `RowMapper``Class``Map<String, Object>` 三种变体)
- `queryFirst`:返回 `Optional<T>`(支持 `RowMapper``Class``Map<String, Object>` 三种变体)
- `queryBoolean`:返回 `boolean`,结果集为空时返回 `false`
- 全部查询方法提供无参数重载
**更新**
- `update`:执行 INSERT / UPDATE / DELETE返回影响行数
- `updateAndReturnKeys`:执行 DML 并返回自动生成的主键,支持 `RowMapper` 映射
- `batchUpdate`:分批执行 DML支持非静默模式遇错中断和静默模式遇错继续
**批量更新结果**
- `BatchUpdateResult`:封装批次级粒度的执行结果(总数据量、批次计数、成功/失败/剩余批次)
- `BatchUpdateStatus` 枚举SUCCESS / COMPLETED_WITH_ERRORS / INTERRUPTED
- `BatchUpdateErrorInfo`:封装错误批次的异常详情
**参数构建**
- `ParamBuilder.buildParams`:构建 `Object[]` 参数数组,支持 `Optional` / `OptionalInt` / `OptionalLong` / `OptionalDouble` 自动拆箱
- `ParamBuilder.buildBatchParams`:批量构建 `List<Object[]>` 参数列表
**映射策略**
- `RowMapper` 函数式接口:`ResultSet` 单行映射
- `RowMapper.HASH_MAP_MAPPER`:将行数据映射为 `Map<String, Object>`
- `DefaultBeanRowMapper`:默认 Bean 映射实现,自动匹配 属性名(小驼峰)↔ 列名(小写蛇形),通过反射调用 setter
- `RowMapper.beanRowMapper(Class)` / `beanRowMapper(Class, Map)` 静态工厂方法
**事务**
- `TransactionException`:包装事务执行中的原始异常
- `commitIfTrue` 方法根据业务逻辑返回值true 提交 / false 回滚)控制事务
**测试**
- 基于 H2 内存数据库的集成测试体系
- 覆盖查询 / 更新 / 批量 / 事务 / RowMapper 全场景
**许可**
- Apache License 2.0

24
NOTICE
View File

@@ -5,36 +5,20 @@ This product is licensed under the Apache License, Version 2.0.
You may obtain a copy of the License at: You may obtain a copy of the License at:
https://www.apache.org/licenses/LICENSE-2.0 https://www.apache.org/licenses/LICENSE-2.0
================================================================================
Third-Party Dependencies
================================================================================
1. plusone-commons (xyz.zhouxy.plusone:plusone-commons)
Copyright ZhouXY
Licensed under the Apache License, Version 2.0
2. Google Guava (com.google.guava:guava)
Copyright (C) The Guava Authors
Licensed under the Apache License, Version 2.0
3. JSR-305 Annotations (com.google.code.findbugs:jsr305)
Copyright (C) FindBugs
Licensed under the Apache License, Version 2.0
================================================================================ ================================================================================
Test Dependencies (Not included in distribution) Test Dependencies (Not included in distribution)
================================================================================ ================================================================================
4. JUnit Jupiter (org.junit.jupiter:junit-jupiter) 1. JUnit Jupiter (org.junit.jupiter:junit-jupiter)
Copyright 2015-2026 JUnit Team Copyright 2015-2026 JUnit Team
Licensed under the Eclipse Public License 2.0 Licensed under the Eclipse Public License 2.0
5. Logback (ch.qos.logback:logback-classic) 2. Logback (ch.qos.logback:logback-classic)
Copyright (C) 1999-2026, QOS.ch Copyright (C) 1999-2026, QOS.ch
Licensed under the Eclipse Public License 1.0 Licensed under the Eclipse Public License 1.0
and GNU Lesser General Public License 2.1 and GNU Lesser General Public License 2.1
6. H2 Database (com.h2database:h2) 3. H2 Database (com.h2database:h2)
Copyright 1999-2026 H2 Database Project Copyright 1999-2026 H2 Database Project
Licensed under the MPL 2.0 and EPL 1.0 Licensed under the MPL 2.0 and EPL 1.0
@@ -42,6 +26,6 @@ Test Dependencies (Not included in distribution)
Notes Notes
================================================================================ ================================================================================
- This project is a lightweight JDBC wrapper designed for legacy projects - This project is a lightweight JDBC wrapper designed for legacy projects
without ORM frameworks. without ORM frameworks.
- For learning and reference purposes only. - For learning and reference purposes only.

433
README.md
View File

@@ -1,12 +1,42 @@
# SimpleJDBC # Simple JDBC
SimpleJDBC 是一个轻量级 JDBC 工具库,提供简洁的 API 用于执行 SQL 查询、更新、批量操作及事务管理,适用于未引入 ORM 框架、直接使用原生 JDBC 的项目 `Simple JDBC` 提供了一套轻量级 JDBC 封装工具类,是作者在对传统遗留项目进行改造时设计。该项目未引入 ORM 框架,原本的数据库交互高度依赖原生 JDBC API导致存在大量冗余的样板代码Boilerplate Code。本项目通过抽象底层数据库操作简化了连接管理、SQL 执行与结果集处理流程,提升数据访问层的开发效率与代码可维护性
## 1. 快速开始 > 注:本项目基于 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) 开源协议发布。
**要求 JDK 8+。** ---
Maven 依赖: ## 1. 核心特性
- **轻量无依赖**:基于原生 JDBC 封装,无第三方重量级依赖。
- **API 简洁**:提供丰富的快捷方法,大幅减少样板代码。
- **灵活的映射**:支持自定义 `ResultHandler``RowMapper`,内置默认 Bean 映射策略。
- **事务与批处理**:提供声明式的事务模板与完善的批量更新错误处理机制。
- **线程安全**:核心模板类无状态设计,天然支持多线程环境。
---
## 2. 设计考量与取舍
`Simple JDBC` 不追求大而全,在功能设计上保持克制。以下是本项目的一些设计考量与取舍:
- 明确**不支持存储过程**:专注于基础 CRUD。
- **不提供分页 API**:不同数据库的分页方言差异巨大,且实际业务中的分页查询往往不是简单的 `LIMIT OFFSET`(存在如游标分页、延迟关联等深度优化空间)。为了保持轻量与灵活,分页 SQL 交由开发者根据具体数据库与业务场景自行编写。
- **不提供缓存支持**:数据缓存应当被视为一个独立的关注点,通常交由更高层的抽象模块来处理。
- **不支持直接传入 Connection**为确保数据库连接资源的规范获取与安全释放Simple JDBC 舍弃了一定的连接管理灵活性,不支持直接传入 Connection而是需要通过 `DataSource` 来获取连接(详见[8. 连接池集成](#8-连接池集成))。常规操作由 `SimpleJdbcTemplate` 自动完成连接的获取与归还;事务场景下,由 `TransactionTemplate` 实现连接绑定,保障同一事务内所有操作的连接一致性。
- **有限但灵活的结果映射**为应对多样化的数据处理需求Simple JDBC 提供了 `ResultHandler``RowMapper` 两层抽象机制。前者负责整体结果集的统筹处理,后者专注于单行数据的解析与映射(详见 [4.2 结果映射策略](#42-结果映射策略))。两者均设计为函数式接口,支持通过 Lambda 表达式快速定制映射逻辑。
---
## 3. 快速开始
### 3.1 环境要求
- **JDK 8** 或更高版本
### 3.2 添加 Maven 依赖
将以下配置添加到您的 `pom.xml` 中:
```xml ```xml
<dependency> <dependency>
@@ -16,106 +46,18 @@ Maven 依赖:
</dependency> </dependency>
``` ```
> 本项目基于 **Apache License 2.0** 开源。 ### 3.3 初始化
## 2. 查询
### 2.1 查询方法
所有查询方法均使用 `Object[]` 作为参数,并提供了无参便捷重载(适用于不含占位符的 SQL
| 方法 | 说明 |
|---|---|
| `query(sql, params, resultHandler)` | 最基础的查询,通过 `ResultHandler` 自定义映射逻辑 |
| `queryList(sql, params, rowMapper)` | 查询列表,通过 `RowMapper` 逐行映射 |
| `queryList(sql, params, Class)` | 单列查询列表,每行取第一列转为指定类型 |
| `queryList(sql, params)` | 查询列表,每行转为 `Map<String, Object>` |
| `queryFirst(sql, params, rowMapper)` | 查询第一行,通过 `RowMapper` 映射,返回 `Optional` |
| `queryFirst(sql, params, Class)` | 查询第一行第一列,返回 `Optional<T>` |
| `queryFirst(sql, params)` | 查询第一行,返回 `Optional<Map<String, Object>>` |
| `queryBoolean(sql, params)` | 查询第一行第一列并转为 boolean结果为空返回 `false` |
> 以上方法均有不含 `params` 的便捷重载,例如 `queryList(sql, rowMapper)`、`queryFirst(sql, Class)` 等,适用于无参数 SQL。
### 2.2 结果映射
- **`ResultHandler`**:处理完整的 `ResultSet`,自定义逻辑将结果映射为任意类型(包括集合)。
- **`RowMapper`**:将 `ResultSet` 中的一行数据映射为 Java 对象。
- `RowMapper.HASH_MAP_MAPPER`:每行映射为 `HashMap<String, Object>`
- `RowMapper.beanRowMapper(Class)`:默认的 Bean 映射,属性名(小驼峰) ↔ 列名(小写蛇形)。
- `RowMapper.beanRowMapper(Class, Map<String, String>)`:自定义属性名与列名映射的 Bean 映射。
## 3. 更新
所有更新方法同样提供了无参便捷重载。
| 方法 | 说明 |
|---|---|
| `update(sql, params)` | 执行 DMLINSERT / UPDATE / DELETE返回受影响行数 |
| `updateAndReturnKeys(sql, params, rowMapper)` | 执行 DML 并返回自动生成的键,通过 `RowMapper` 映射 |
| `batchUpdate(sql, params, batchSize)` | 分批执行 DML遇错即中断 |
| `batchUpdate(sql, params, batchSize, quietly)` | 分批执行 DML`quietly=true` 遇错不中断,全部执行完毕 |
### BatchUpdateResult
`batchUpdate` 返回 `BatchUpdateResult`,包含:
- `getStatus()`:批次状态(`SUCCESS` / `COMPLETED_WITH_ERRORS` / `INTERRUPTED`
- `getTotal()`:总数据量
- `getBatchCount()`:总批次数
- `getSuccessBatchCount()` / `getErrorBatchCount()`:成功/失败批次数
- `getBatchUpdateErrorInfo(batchIndex)`:获取指定批次的错误详情
## 4. 事务
通过 `TransactionTemplate` 管理事务,可直接创建或通过 `SimpleJdbcTemplate.transaction()` 获取。
- **`execute(consumer)`**:执行事务。传入 `ThrowingConsumer<JdbcOperations>`,若内部无异常则提交,有异常则回滚。
- **`commitIfTrue(predicate)`**:执行事务。传入 `ThrowingPredicate<JdbcOperations>`,返回 `true` 提交,返回 `false` 或抛异常则回滚。
## 5. 参数构建
此项目中所有方法都**不使用可变长参数**,避免强制将参数列表放在 SQL 语句末尾,也避免与数组产生歧义。
### 5.1 构建参数列表
使用 `ParamBuilder.buildParams(...)` 构建 `Object[]` 作为 SQL 参数。该方法会自动将 `Optional` 值拆箱。
```java
import static xyz.zhouxy.jdbc.ParamBuilder.buildParams;
buildParams("admin%", "0000"); // → Object[]{"admin%", "0000"}
buildParams(Optional.of("hello")); // → Object[]{"hello"}
buildParams(Optional.empty()); // → Object[]{null}
```
### 5.2 批量构建参数列表
使用 `ParamBuilder.buildBatchParams(collection, func)` 将集合中每个元素转为 `Object[]`,返回 `List<Object[]>`
```java
import static xyz.zhouxy.jdbc.ParamBuilder.buildBatchParams;
import static xyz.zhouxy.jdbc.ParamBuilder.buildParams;
buildBatchParams(accountList, account -> buildParams(
account.getUsername(),
account.getPassword(),
account.getOrgNo()
));
```
## 6. 示例
创建 `SimpleJdbcTemplate` 对象:
```java ```java
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource); SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
``` ```
### 6.1 查询 > 💡 关于与数据库连接池(如 HikariCP、Druid、DBCP 2 等)的集成方式,请参见「[连接池集成](#8-连接池集成)」章节。
### 3.4 查询操作
```java ```java
// 查询(使用 ResultHandler 处理全部结果) // 基础查询(使用 ResultHandler 处理全部结果)
List<Account> accounts = jdbcTemplate.query( List<Account> accounts = jdbcTemplate.query(
"SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?", "SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"), buildParams("admin%", "0000"),
@@ -136,21 +78,21 @@ List<Account> accounts = jdbcTemplate.query(
); );
// 查询列表(单列) // 查询列表(单列)
List<String> usernames = jdbcTemplate.queryList( List<String> usernames = jdbcTemplate.queryValues(
"SELECT username FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?", "SELECT username FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"), buildParams("admin%", "0000"),
String.class String.class
); );
// 查询列表(使用 DefaultBeanRowMapper 进行映射) // 查询列表(使用内置 Bean 映射)
List<Account> accounts = jdbcTemplate.queryList( List<Account> mappedAccounts = jdbcTemplate.queryList(
"SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?", "SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"), buildParams("admin%", "0000"),
RowMapper.beanRowMapper(Account.class) RowMapper.beanRowMapper(Account.class)
); );
// 查询列表(使用自定义 RowMapper 进行映射) // 查询列表(使用自定义 RowMapper 映射)
List<Account> accounts = jdbcTemplate.queryList( List<Account> customMappedAccounts = jdbcTemplate.queryList(
"SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?", "SELECT * FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"), buildParams("admin%", "0000"),
(rs, rowNum) -> new Account( (rs, rowNum) -> new Account(
@@ -163,57 +105,73 @@ List<Account> accounts = jdbcTemplate.queryList(
) )
); );
// 查询行数据 // 查询行数据
Optional<Account> account = jdbcTemplate.queryFirst( Optional<Account> account = jdbcTemplate.queryFirst(
"SELECT * FROM account WHERE deleted = 0 AND id = ?", "SELECT * FROM account WHERE deleted = 0 AND id = ?",
buildParams(10000L), buildParams(10000L),
(rs, rowNum) -> new Account( (rs, rowNum) -> new Account(
rs.getLong("id"), rs.getLong("id"),
rs.getString("username"), rs.getString("username")
rs.getString("password"), // ... 省略其他字段
rs.getString("org_no"),
rs.getTimestamp("create_time"),
rs.getTimestamp("update_time")
) )
); );
// 查询 boolean // 查询单个值(所有 ResultSet.getObject 支持的类型)
Long count = jdbcTemplate.queryValue(
"SELECT COUNT(*) FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"),
Long.class
).orElse(0L);
// 或者
Long count = jdbcTemplate.queryValueOrDefault(
"SELECT COUNT(*) FROM account WHERE deleted = 0 AND username LIKE ? AND org_no = ?",
buildParams("admin%", "0000"),
Long.class,
0L
);
// 查询 Boolean 值
boolean exists = jdbcTemplate.queryBoolean( boolean exists = jdbcTemplate.queryBoolean(
"SELECT EXISTS(SELECT 1 FROM account WHERE deleted = 0 AND id = ?)", "SELECT EXISTS(SELECT 1 FROM account WHERE deleted = 0 AND id = ?)",
buildParams(10000L) buildParams(10000L)
); );
// 无参数 SQL 可直接省略 params // 无参数 SQL 可直接省略 params 参数
List<Account> allAccounts = jdbcTemplate.queryList( List<Account> allAccounts = jdbcTemplate.queryList(
"SELECT * FROM account WHERE deleted = 0", "SELECT * FROM account WHERE deleted = 0",
RowMapper.beanRowMapper(Account.class) RowMapper.beanRowMapper(Account.class)
); );
``` ```
### 6.2 更新 > 📖 完整的方法列表与映射策略说明请参见「[4. 数据查询](#4-数据查询-query)」章节。
### 3.5 更新操作
```java ```java
// 执行 DML // 执行常规 DML
int affectedRows = jdbcTemplate.update( int affectedRows = jdbcTemplate.update(
"UPDATE account SET deleted = 1 WHERE id = ?", "UPDATE account SET deleted = 1 WHERE id = ?",
buildParams(10000L) buildParams(10000L)
); );
// 执行 DML并获取生成的主键 // 执行 DML 并获取生成的主键
// 注:按 JDBC 规范可获取自增 ID能否获取其他值取决于具体数据库及其 JDBC Driver 实现。
List<Pair<Long, LocalDateTime>> keys = jdbcTemplate.updateAndReturnKeys( List<Pair<Long, LocalDateTime>> keys = jdbcTemplate.updateAndReturnKeys(
"INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)", "INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)",
buildParams("admin", "123456", "0000"), buildParams("admin", "123456", "0000"),
(rs, rowNum) -> Pair.of( (rs, rowNum) -> Pair.of(
rs.getLong("id"), rs.getLong("id"),
rs.getObject("create_time", LocalDateTime.class) rs.getObject("create_time", LocalDateTime.class)
) )
); );
``` ```
### 6.3 批量更新 > 📖 完整的方法列表与批量更新结果说明请参见「[5. 数据更新](#5-数据更新-update)」章节。
### 3.6 批量更新操作
```java ```java
// 默认:遇错即中断 // 默认模式:遇错即中断
BatchUpdateResult result = jdbcTemplate.batchUpdate( BatchUpdateResult result = jdbcTemplate.batchUpdate(
"INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)", "INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)",
buildBatchParams(accountList, account -> buildParams( buildBatchParams(accountList, account -> buildParams(
@@ -221,11 +179,11 @@ BatchUpdateResult result = jdbcTemplate.batchUpdate(
account.getPassword(), account.getPassword(),
account.getOrgNo() account.getOrgNo()
)), )),
100 // 每 100 条数据一个批次 100 // 每 100 条数据一个批次
); );
// 静默模式:遇错不中断,全部执行完毕后统一检查结果 // 静默模式:遇错不中断,全部执行完毕后统一检查
BatchUpdateResult result = jdbcTemplate.batchUpdate( BatchUpdateResult quietResult = jdbcTemplate.batchUpdate(
"INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)", "INSERT INTO account (username, password, org_no) VALUES (?, ?, ?)",
buildBatchParams(accountList, account -> buildParams( buildBatchParams(accountList, account -> buildParams(
account.getUsername(), account.getUsername(),
@@ -237,26 +195,30 @@ BatchUpdateResult result = jdbcTemplate.batchUpdate(
); );
// 检查批量更新结果 // 检查批量更新结果
if (result.getStatus() == BatchUpdateStatus.COMPLETED_WITH_ERRORS) { if (quietResult.getStatus() == BatchUpdateStatus.COMPLETED_WITH_ERRORS) {
for (int idx : result.getErrorBatchIndexes()) { for (int idx : quietResult.getErrorBatchIndexes()) {
BatchUpdateErrorInfo err = result.getBatchUpdateErrorInfo(idx); BatchUpdateErrorInfo err = quietResult.getBatchUpdateErrorInfo(idx);
System.err.println("批次 " + idx + " 失败: " + err.getCause().getMessage()); System.err.println("批次 " + idx + " 失败: " + err.getCause().getMessage());
} }
} }
``` ```
### 6.4 事务 > 📖 批量更新的详细结果说明请参见「[5.2 批量更新结果](#52-批量更新结果-batchupdateresult)」章节。
### 3.7 事务管理
```java ```java
// 自动提交/回滚事务
jdbcTemplate.transaction().execute(jdbc -> { jdbcTemplate.transaction().execute(jdbc -> {
... ...
jdbc.update(...); jdbc.update(...);
... ...
jdbc.update(...); jdbc.update(...);
... ...
// 无异常则自动提交 // 内部无异常抛出则自动提交,抛出异常则自动回滚
}); });
// 根据返回值控制事务
jdbcTemplate.transaction().commitIfTrue(jdbc -> { jdbcTemplate.transaction().commitIfTrue(jdbc -> {
... ...
jdbc.update(...); jdbc.update(...);
@@ -278,8 +240,217 @@ jdbcTemplate.transaction().commitIfTrue(jdbc -> {
}); });
``` ```
> **!!!本项目不比成熟的工具,如若使用请自行承担风险。** > 📖 事务方法的详细说明请参见「[6. 事务管理](#6-事务管理-transaction)」章节。
>
> - **线程安全**`SimpleJdbcTemplate` 无内部状态,线程安全。但所依赖的 `DataSource` 需自行保证线程安全。 ---
> - **连接管理**:每次操作自动从 `DataSource` 获取连接并在操作完成后关闭,无需手动管理。
> - **适用场景**:不适合高并发或大数据量场景,建议仅用于学习参考或小型项目。 ## 4. 数据查询 (Query)
### 4.1 查询方法列表
| 方法签名 | 说明 |
| :--- | :--- |
| `query(sql, params, resultHandler)` | 最基础的查询,通过 `ResultHandler` 自定义完整的映射逻辑。 |
| `queryList(sql, params, rowMapper)` | 查询列表,通过 `RowMapper` 逐行映射。 |
| `queryList(sql, params)` | 查询列表,每行自动转换为 `Map<String, Object>`。 |
| `queryFirst(sql, params, rowMapper)` | 查询第一行,通过 `RowMapper` 映射,返回 `Optional<T>`。 |
| `queryFirst(sql, params)` | 查询第一行,返回 `Optional<Map<String, Object>>`。 |
| `queryValues(sql, params, Class)` | 单列查询列表,每行提取第一列并转换为指定类型。 |
| `queryValue(sql, params, Class)` | 查询第一行第一列,返回 `Optional<T>`。 |
| `queryValueOrDefault(sql, params, Class, default)` | 查询第一行第一列,结果为空时返回默认值。适用于 COUNT/SUM 等聚合查询。 |
| `queryBoolean(sql, params)` | 查询第一行第一列并转换为 `boolean`,若结果为空则返回 `false`。 |
*💡 提示:以上方法均有省略 `params` 的重载(如 `queryList(sql, rowMapper)`),适用于不含占位符的 SQL 语句。`queryValues`、`queryValue`、`queryValueOrDefault` 同理。*
### 4.2 结果映射策略
- **`ResultHandler`**:处理完整的 `ResultSet`,允许自定义逻辑将结果集映射为任意类型(包括集合)。
- **`RowMapper`**:将 `ResultSet` 中的单行数据映射为 Java 对象。内置以下默认实现:
- `RowMapper.HASH_MAP_MAPPER`:将每行数据映射为 `HashMap<String, Object>`
- `DefaultBeanRowMapper`:将 `ResultSet` 中的一行数据映射为 Java Bean 的默认实现。使用反射获取类型信息、调用无参构造器和 `setter` 方法。**(注:实际生产中更建议针对目标类型自定义 `RowMapper` 以提升性能)**
- `RowMapper.beanRowMapper(Class)`:自动匹配 **属性名(小驼峰) ↔ 列名(小写蛇形)**
- `RowMapper.beanRowMapper(Class, Map<String, String>)`:通过 `Map` 自定义属性名与列名映射关系。
---
## 5. 数据更新 (Update)
所有更新方法同样提供了无参重载。
### 5.1 更新方法列表
| 方法签名 | 说明 |
| :--- | :--- |
| `update(sql, params)` | 执行 DMLINSERT / UPDATE / DELETE返回受影响的行数。 |
| `updateAndReturnKeys(sql, params, rowMapper)` | 执行 DML 并返回自动生成的键(如自增 ID通过 `RowMapper` 进行映射。 |
| `batchUpdate(sql, params, batchSize)` | 分批执行 DML遇到错误立即中断。 |
| `batchUpdate(sql, params, batchSize, quietly)` | 分批执行 DML`quietly=true`,则遇到错误不中断,直至全部执行完毕。 |
### 5.2 批量更新结果 (BatchUpdateResult)
`batchUpdate` 方法返回 `BatchUpdateResult` 对象,包含以下信息:
- `getStatus()`:批量更新的总状态(`SUCCESS` / `COMPLETED_WITH_ERRORS` / `INTERRUPTED`)。
- `getTotal()`:总数据量。
- `getBatchSize()`:批次大小
- `getBatchCount()`:总批次数。
- `getCompleteBatchCount()`:已完成的批次数。
- `getSuccessBatchCount()` / `getErrorBatchCount()`:成功 / 失败的批次数。
- `getRemainingBatchCount()`:(中断后)未执行的剩余批次数。
- `getUpdateCounts(batchIndex)`:获取指定批次更新结果。
- `getErrorBatchIndexes()`:获取所有出错的批次号.
- `getBatchUpdateErrorInfo(batchIndex)`:获取指定批次的详细错误信息。
- `getAllErrorsInfo()`:获取所有出错的批次的错误信息。
---
## 6. 事务管理 (Transaction)
通过 `TransactionTemplate` 管理事务,可直接实例化或通过 `SimpleJdbcTemplate.transaction()` 获取。
事务模板的核心机制如下:
- **共享连接**:事务内的所有 JDBC 操作共享同一个数据库连接,确保操作在同一事务上下文中执行。
- **自动提交管理**:进入事务时关闭连接的自动提交模式,事务结束后恢复原始状态。
- **异常处理**:操作中抛出异常时自动执行回滚,并将原始异常包装为 `TransactionException` 抛出。
### 6.1 获取事务模板
```java
// 方式一:通过 SimpleJdbcTemplate 获取
TransactionTemplate tx = jdbcTemplate.transaction();
// 方式二:直接实例化
TransactionTemplate tx = new TransactionTemplate(dataSource);
```
### 6.2 事务方法
- **`execute(consumer)`**:执行事务。传入 `ThrowingConsumer<JdbcOperations>`,若内部代码无异常抛出则自动提交,发生异常则回滚。
- **`commitIfTrue(predicate)`**:执行事务。传入 `ThrowingPredicate<JdbcOperations>`,根据返回值决定事务走向:返回 `true` 提交,返回 `false` 或抛出异常则回滚。
---
## 7. 参数构建
为避免与数组产生歧义并规范 API 设计,`JdbcOperations` 中的所有方法均不使用可变长参数Varargs而是统一使用 `Object[]` 作为参数传递。您可以使用内置的 `ParamBuilder` 快速构建参数。
### 7.1 构建单条参数列表
使用 `ParamBuilder.buildParams(...)` 构建 `Object[]`。该方法会自动将 `Optional` 值进行拆箱处理。
```java
import static xyz.zhouxy.jdbc.ParamBuilder.buildParams;
buildParams("admin%", "0000"); // 返回 Object[]{"admin%", "0000"}
buildParams(Optional.of("hello")); // 返回 Object[]{"hello"}
buildParams(Optional.empty()); // 返回 Object[]{null}
```
### 7.2 批量构建参数列表
使用 `ParamBuilder.buildBatchParams(collection, func)` 将集合中的每个元素转换为 `Object[]`,最终返回 `List<Object[]>`
```java
import static xyz.zhouxy.jdbc.ParamBuilder.buildBatchParams;
import static xyz.zhouxy.jdbc.ParamBuilder.buildParams;
List<Object[]> batchParams = buildBatchParams(accountList, account -> buildParams(
account.getUsername(),
account.getPassword(),
account.getOrgNo()
));
```
---
## 8. 连接池集成
`SimpleJdbcTemplate` 仅依赖标准的 `javax.sql.DataSource` 接口,可与任何主流数据库连接池集成。以下为常用连接池的配置示例。
### 8.1 使用 HikariCP
> [HikariCP](https://github.com/brettwooldridge/HikariCP) 是目前性能最优的数据库连接池之一,推荐在新项目中优先使用。
```xml
<!-- Maven 依赖 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>
```
```java
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
// 配置 HikariCP 连接池
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/your_database");
config.setUsername("your_username");
config.setPassword("your_password");
// 其他连接池参数按需配置
DataSource dataSource = new HikariDataSource(config);
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
```
### 8.2 使用 Druid
> [Druid](https://github.com/alibaba/druid) 是阿里巴巴开源的数据库连接池,提供了内置的监控和扩展能力,在中文社区中广泛采用。
```xml
<!-- Maven 依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
```
```java
import com.alibaba.druid.pool.DruidDataSource;
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/your_database");
dataSource.setUsername("your_username");
dataSource.setPassword("your_password");
// 其他连接池参数按需配置
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
```
### 8.3 使用 DBCP 2
> [DBCP 2](https://commons.apache.org/proper/commons-dbcp/) 是 Apache Commons 提供的数据库连接池,适用于需要依赖轻量的场景。
```xml
<!-- Maven 依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>${dbcp2.version}</version>
</dependency>
```
```java
import org.apache.commons.dbcp2.BasicDataSource;
BasicDataSource dataSource = new BasicDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/your_database");
dataSource.setUsername("your_username");
dataSource.setPassword("your_password");
// 其他连接池参数按需配置
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
```
---
## 9. 注意事项与适用场景
1. **风险提示**:本项目定位为轻量级工具,相较于成熟的 ORM 框架(如 MyBatis、Hibernate其功能覆盖面和生态相对有限。**在生产环境使用前,请务必进行充分的测试,使用风险自行承担。**
2. **线程安全**`SimpleJdbcTemplate` 本身无内部状态,是**线程安全**的。但请确保其底层依赖的 `DataSource`(如 HikariCP、Druid 等连接池)已正确配置并保证线程安全。
3. **连接管理**:每次数据库操作均会自动从 `DataSource` 获取连接,并在操作完成(或发生异常)后自动关闭,开发者无需手动管理连接的释放。
4. **适用场景**:中小型项目、内部工具、快速原型开发、未引入 ORM 框架的遗留系统改造、学习 JDBC 原理。

30
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>xyz.zhouxy.jdbc</groupId> <groupId>xyz.zhouxy.jdbc</groupId>
<artifactId>simple-jdbc</artifactId> <artifactId>simple-jdbc</artifactId>
<version>1.0.0-SNAPSHOT</version> <version>1.1.0-SNAPSHOT</version>
<name>Simple JDBC</name> <name>Simple JDBC</name>
<description>对 JDBC 的简单封装。</description> <description>对 JDBC 的简单封装。</description>
@@ -16,7 +16,11 @@
<maven.compiler.target>8</maven.compiler.target> <maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plusone-commons.version>1.1.0-RC2</plusone-commons.version> <!-- Dependency Versions -->
<jsr305.version>3.0.2</jsr305.version>
<junit-jupiter.version>5.14.4</junit-jupiter.version>
<logback.version>1.3.16</logback.version>
<h2.version>2.2.224</h2.version>
</properties> </properties>
<licenses> <licenses>
@@ -40,43 +44,35 @@
<url>https://gitea.zhouxy.xyz/plusone/simple-jdbc</url> <url>https://gitea.zhouxy.xyz/plusone/simple-jdbc</url>
</scm> </scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>xyz.zhouxy.plusone</groupId>
<artifactId>plusone-dependencies</artifactId>
<version>${plusone-commons.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>xyz.zhouxy.plusone</groupId> <groupId>com.google.code.findbugs</groupId>
<artifactId>plusone-commons</artifactId> <artifactId>jsr305</artifactId>
<version>${plusone-commons.version}</version> <version>${jsr305.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId> <artifactId>junit-jupiter</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ch.qos.logback</groupId> <groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
<version>${h2.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<profiles> <profiles>
<profile> <profile>
<id>release</id> <id>release</id>

View File

@@ -51,6 +51,11 @@ public class BatchUpdateResult {
*/ */
private final int batchSize; private final int batchSize;
/**
* 是否静默模式
*/
private final boolean quietly;
/** /**
* 本次分批更新的状态 * 本次分批更新的状态
*/ */
@@ -75,10 +80,11 @@ public class BatchUpdateResult {
*/ */
private int completeBatchCount; private int completeBatchCount;
BatchUpdateResult(int total, int batchCount, int batchSize) { BatchUpdateResult(int total, int batchCount, int batchSize, boolean quietly) {
this.total = total; this.total = total;
this.batchCount = batchCount; this.batchCount = batchCount;
this.batchSize = batchSize; this.batchSize = batchSize;
this.quietly = quietly;
this.allUpdateCounts = new HashMap<>(batchCount); this.allUpdateCounts = new HashMap<>(batchCount);
this.allErrorsInfo = new HashMap<>(batchCount); this.allErrorsInfo = new HashMap<>(batchCount);
@@ -101,17 +107,15 @@ public class BatchUpdateResult {
this.allUpdateCounts.put(batchIndex, updateCounts); this.allUpdateCounts.put(batchIndex, updateCounts);
this.allErrorsInfo.put(batchIndex, new BatchUpdateErrorInfo(batchIndex, cause)); this.allErrorsInfo.put(batchIndex, new BatchUpdateErrorInfo(batchIndex, cause));
if (this.status == BatchUpdateStatus.SUCCESS) { if (this.status == BatchUpdateStatus.SUCCESS) {
this.status = BatchUpdateStatus.COMPLETED_WITH_ERRORS; if (this.quietly) {
this.status = BatchUpdateStatus.COMPLETED_WITH_ERRORS;
}
else {
this.status = BatchUpdateStatus.INTERRUPTED;
}
} }
} }
/**
* 中断
*/
void interrupt() {
this.status = BatchUpdateStatus.INTERRUPTED;
}
/** /**
* 获取指定批次更新结果 * 获取指定批次更新结果
* *

View File

@@ -15,8 +15,6 @@
*/ */
package xyz.zhouxy.jdbc; package xyz.zhouxy.jdbc;
import xyz.zhouxy.plusone.commons.base.IWithIntCode;
/** /**
* 批量更新状态 * 批量更新状态
* *
@@ -27,7 +25,7 @@ import xyz.zhouxy.plusone.commons.base.IWithIntCode;
* @see BatchUpdateResult * @see BatchUpdateResult
* @see BatchUpdateResult#getStatus() * @see BatchUpdateResult#getStatus()
*/ */
public enum BatchUpdateStatus implements IWithIntCode { public enum BatchUpdateStatus {
/** /**
* 成功 * 成功
@@ -62,9 +60,10 @@ public enum BatchUpdateStatus implements IWithIntCode {
} }
/** /**
* {@inheritDoc} * 获取状态码
*
* @return 状态码
*/ */
@Override
public int getCode() { public int getCode() {
return code; return code;
} }

View File

@@ -34,28 +34,27 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import com.google.common.base.CaseFormat; import xyz.zhouxy.jdbc.util.NamingTools;
import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod;
/** /**
* DefaultBeanRowMapper * DefaultBeanRowMapper
* *
* <p> * <p>
* 默认实现的将 {@link ResultSet} 转换为 Java Bean 的 {@link RowMapper}。 * 将 {@link ResultSet} 转换为 Java Bean 的 {@link RowMapper} 的基础实现
* </p> * <p>
* <i>性能和规则上的限制都比较大,仅在对性能不敏感的场景下便捷使用,
* 一般情况下你应该自定义 {@link RowMapper}。</i>
* *
* <p> * <p>
* 说明: * 说明:
* <ul> * <ul>
* <li>使用反射获取类型信息,也是使用反射调用无参构造器和 {@code setter} 方法。</li> * <li>使用反射获取类型信息,也是使用反射调用无参构造器和 {@code setter} 方法。</li>
* <li>{@code propertyColMap} 未指定的列名和属性名的映射时,默认 JavaBean 的属性名为小驼峰,列名为小写蛇形命名。</li> * <li>支持自定义列名和属性名的映射,当未指定 {@code propertyColMap} 时,默认 JavaBean 的属性名为小驼峰,列名为小写蛇形命名。</li>
* <li>从{@link ResultSet} 中获取属性值时,使用 {@link ResultSet#getObject(String, Class)} 获取。</li> * <li>使用 {@link ResultSet#getObject(String, Class)} 从 {@link ResultSet} 中获取属性值。</li>
* <li>JavaBean 属性仅支持引用类型,不支持基本数据类型。</li> * <li>JavaBean 属性仅支持引用类型,不支持基本数据类型。</li>
* <li>实际使用中还是建议针对目标类型自定义 {@link RowMapper}。</li>
* </ul> * </ul>
*
* @author ZhouXY * @author ZhouXY
* @since 1.0.0 * @since 1.0.0
*/ */
public class DefaultBeanRowMapper<T> implements RowMapper<T> { public class DefaultBeanRowMapper<T> implements RowMapper<T> {
@@ -88,10 +87,9 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
* @param <T> Bean 类型 * @param <T> Bean 类型
* @param beanType Bean 类型 * @param beanType Bean 类型
* @return DefaultBeanRowMapper 对象 * @return DefaultBeanRowMapper 对象
* @throws SQLException 创建 {@code DefaultBeanRowMapper} 出现错误的异常时抛出 * @throws IllegalStateException 创建 {@code DefaultBeanRowMapper} 出现错误的异常时抛出
*/ */
@StaticFactoryMethod(DefaultBeanRowMapper.class) public static <T> DefaultBeanRowMapper<T> of(Class<T> beanType) {
public static <T> DefaultBeanRowMapper<T> of(Class<T> beanType) throws SQLException {
return of(beanType, null); return of(beanType, null);
} }
@@ -102,11 +100,10 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
* @param beanType Bean 类型 * @param beanType Bean 类型
* @param propertyColMap Bean 字段与列名的映射关系。key 是字段value 是列名。 * @param propertyColMap Bean 字段与列名的映射关系。key 是字段value 是列名。
* @return {@code DefaultBeanRowMapper} 对象 * @return {@code DefaultBeanRowMapper} 对象
* @throws SQLException 创建 {@code DefaultBeanRowMapper} 出现错误的异常时抛出 * @throws IllegalStateException 创建 {@code DefaultBeanRowMapper} 出现错误的异常时抛出
*/ */
@StaticFactoryMethod(DefaultBeanRowMapper.class) public static <T> DefaultBeanRowMapper<T> of(Class<T> beanType,
public static <T> DefaultBeanRowMapper<T> of(Class<T> beanType, @Nullable Map<String, String> propertyColMap) @Nullable Map<String, String> propertyColMap) {
throws SQLException {
try { try {
// 获取无参构造器 // 获取无参构造器
Constructor<T> constructor = beanType.getDeclaredConstructor(); Constructor<T> constructor = beanType.getDeclaredConstructor();
@@ -117,10 +114,10 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
return new DefaultBeanRowMapper<>(beanType, constructor, colPropertyMap, colSetterMap); return new DefaultBeanRowMapper<>(beanType, constructor, colPropertyMap, colSetterMap);
} }
catch (IntrospectionException e) { catch (IntrospectionException e) {
throw new SQLException("There is an exception occurs during introspection.", e); throw new IllegalStateException("There is an exception occurs during introspection.", e);
} }
catch (NoSuchMethodException e) { catch (NoSuchMethodException e) {
throw new SQLException("Could not find a no-args constructor in " + beanType.getName(), e); throw new IllegalStateException("Could not find a no-args constructor in " + beanType.getName(), e);
} }
} }
@@ -145,7 +142,7 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
return newInstance; return newInstance;
} }
catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
throw new SQLException("Could not map row to " + beanType.getName(), e); throw new IllegalStateException("Could not map row to " + beanType.getName(), e);
} }
} }
@@ -167,14 +164,14 @@ public class DefaultBeanRowMapper<T> implements RowMapper<T> {
// Bean 的属性名为小驼峰,对应的列名为下划线 // Bean 的属性名为小驼峰,对应的列名为下划线
Function<? super PropertyDescriptor, String> keyMapper; Function<? super PropertyDescriptor, String> keyMapper;
if (propertyColMap == null || propertyColMap.isEmpty()) { if (propertyColMap == null || propertyColMap.isEmpty()) {
keyMapper = p -> CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, p.getName()); keyMapper = p -> NamingTools.camelToSnake(p.getName());
} }
else { else {
keyMapper = p -> { keyMapper = p -> {
String propertyName = p.getName(); String propertyName = p.getName();
String colName = propertyColMap.get(propertyName); String colName = propertyColMap.get(propertyName);
return colName != null ? colName return colName != null ? colName
: CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, propertyName); : NamingTools.camelToSnake(propertyName);
}; };
} }
return Arrays.stream(propertyDescriptors) return Arrays.stream(propertyDescriptors)

View File

@@ -16,8 +16,8 @@
package xyz.zhouxy.jdbc; package xyz.zhouxy.jdbc;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgument; import static xyz.zhouxy.jdbc.util.AssertTools.checkArgument;
import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull; import static xyz.zhouxy.jdbc.util.AssertTools.checkArgumentNotNull;
import java.sql.BatchUpdateException; import java.sql.BatchUpdateException;
import java.sql.Connection; import java.sql.Connection;
@@ -37,8 +37,6 @@ import java.util.List;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import com.google.common.collect.Lists;
/** /**
* JdbcOperationSupport * JdbcOperationSupport
* *
@@ -95,14 +93,14 @@ class JdbcOperationSupport {
} }
/** /**
* 执行查询,返回结果映射为指定类型。当结果为单列时使用 * 执行查询,只取结果集每行第一列的值,映射为指定类型并返回列表
* *
* @param conn 数据库连接 * @param conn 数据库连接
* @param sql SQL * @param sql SQL
* @param params 参数 * @param params 参数
* @param clazz 将结果映射为指定的类型 * @param clazz 将结果映射为指定的类型
*/ */
static <T> List<T> queryList(Connection conn, String sql, Object[] params, Class<T> clazz) static <T> List<T> queryValues(Connection conn, String sql, Object[] params, Class<T> clazz)
throws SQLException { throws SQLException {
assertConnectionNotNull(conn); assertConnectionNotNull(conn);
assertSqlNotNull(sql); assertSqlNotNull(sql);
@@ -131,14 +129,15 @@ class JdbcOperationSupport {
} }
/** /**
* 查询第一行第一列,并转换为指定类型 * 执行查询,只取结果集第一行第一列的值,映射为指定类型并返回
* *
* @param conn 数据库连接
* @param <T> 目标类型 * @param <T> 目标类型
* @param sql SQL * @param sql SQL
* @param params 参数 * @param params 参数
* @param clazz 目标类型 * @param clazz 目标类型
*/ */
static <T> T queryFirst(Connection conn, String sql, Object[] params, Class<T> clazz) static <T> T queryValue(Connection conn, String sql, Object[] params, Class<T> clazz)
throws SQLException { throws SQLException {
assertConnectionNotNull(conn); assertConnectionNotNull(conn);
assertSqlNotNull(sql); assertSqlNotNull(sql);
@@ -162,9 +161,16 @@ class JdbcOperationSupport {
throws SQLException { throws SQLException {
assertConnectionNotNull(conn); assertConnectionNotNull(conn);
assertSqlNotNull(sql); assertSqlNotNull(sql);
try (PreparedStatement stmt = conn.prepareStatement(sql)) { if (params != null && params.length > 0) {
fillStatement(stmt, params); try (PreparedStatement stmt = conn.prepareStatement(sql)) {
return stmt.executeUpdate(); fillStatement(stmt, params);
return stmt.executeUpdate();
}
}
else {
try (Statement stmt = conn.createStatement()) {
return stmt.executeUpdate(sql);
}
} }
} }
@@ -184,18 +190,24 @@ class JdbcOperationSupport {
assertConnectionNotNull(conn); assertConnectionNotNull(conn);
assertSqlNotNull(sql); assertSqlNotNull(sql);
assertRowMapperNotNull(rowMapper); assertRowMapperNotNull(rowMapper);
final List<T> result = Lists.newArrayListWithCapacity(4); if (params != null && params.length > 0) {
try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
fillStatement(stmt, params); fillStatement(stmt, params);
stmt.executeUpdate(); stmt.executeUpdate();
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) { try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
int rowNumber = 0; final ResultHandler<List<T>> resultHandler = ResultHandler.mapToList(rowMapper);
while (generatedKeys.next()) { return resultHandler.handle(generatedKeys);
T e = rowMapper.mapRow(generatedKeys, rowNumber++); }
result.add(e); }
}
else {
try (Statement stmt = conn.createStatement()) {
stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
final ResultHandler<List<T>> resultHandler = ResultHandler.mapToList(rowMapper);
return resultHandler.handle(generatedKeys);
} }
} }
return result;
} }
} }
@@ -221,13 +233,13 @@ class JdbcOperationSupport {
assertSqlNotNull(sql); assertSqlNotNull(sql);
checkArgument(batchSize > 0, "The batch size must be greater than 0."); checkArgument(batchSize > 0, "The batch size must be greater than 0.");
if (params == null || params.isEmpty()) { if (params == null || params.isEmpty()) {
return new BatchUpdateResult(0, 0, batchSize); return new BatchUpdateResult(0, 0, batchSize, quietly);
} }
final int paramsSize = params.size(); final int paramsSize = params.size();
final int batchCount = (paramsSize + batchSize - 1) / batchSize; final int batchCount = (paramsSize + batchSize - 1) / batchSize;
final BatchUpdateResult result = new BatchUpdateResult(paramsSize, batchCount, batchSize); final BatchUpdateResult result = new BatchUpdateResult(paramsSize, batchCount, batchSize, quietly);
try (PreparedStatement stmt = conn.prepareStatement(sql)) { try (PreparedStatement stmt = conn.prepareStatement(sql)) {
// 表示第几条数据1, 2, 3, ..., paramsSize // 表示第几条数据1, 2, 3, ..., paramsSize
@@ -252,7 +264,6 @@ class JdbcOperationSupport {
final int[] updateCounts = getUpdateCountsOnError(indexInBatch, e); final int[] updateCounts = getUpdateCountsOnError(indexInBatch, e);
result.recordErrorBatch(batchIndex, updateCounts, e); result.recordErrorBatch(batchIndex, updateCounts, e);
if (!quietly) { if (!quietly) {
result.interrupt();
break; break;
} }
} }
@@ -295,9 +306,17 @@ class JdbcOperationSupport {
@Nullable Object[] params, @Nullable Object[] params,
@Nonnull ResultHandler<T> resultHandler) @Nonnull ResultHandler<T> resultHandler)
throws SQLException { throws SQLException {
try (PreparedStatement stmt = createPreparedStatementInternal(conn, sql, params); if (params != null && params.length > 0) {
ResultSet rs = stmt.executeQuery()) { try (PreparedStatement stmt = createPreparedStatementInternal(conn, sql, params);
return resultHandler.handle(rs); ResultSet rs = stmt.executeQuery()) {
return resultHandler.handle(rs);
}
}
else {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
return resultHandler.handle(rs);
}
} }
} }
@@ -324,15 +343,7 @@ class JdbcOperationSupport {
@Nullable Object[] params, @Nullable Object[] params,
@Nonnull RowMapper<T> rowMapper) @Nonnull RowMapper<T> rowMapper)
throws SQLException { throws SQLException {
return queryInternal(conn, sql, params, rs -> { return queryInternal(conn, sql, params, ResultHandler.mapToList(rowMapper));
List<T> result = Lists.newArrayList();
int rowNumber = 0;
while (rs.next()) {
T e = rowMapper.mapRow(rs, rowNumber++);
result.add(e);
}
return result;
});
} }
/** /**

View File

@@ -87,17 +87,18 @@ public interface JdbcOperations {
throws SQLException; throws SQLException;
/** /**
* 执行查询,返回结果映射为指定类型。当结果为单列时使用 * 执行查询,只取结果集每行第一列的值,映射为指定类型并返回列表
* 适用于 {@code SELECT single_column FROM ...} 单列查询场景。
* *
* @param <T> 目标类型 * @param <T> 目标类型(对应结果集第一列的 Java 类型)
* @param sql SQL * @param sql SQL
* @param params 参数 * @param params 参数
* @param clazz 目标类型 * @param clazz 目标类型
* *
* @return 映射结果。如果查询结果为空,则返回空列表 * @return 每一行第一列的值列表。如果查询结果为空,则返回空列表
* @throws SQLException SQL异常 * @throws SQLException SQL异常
*/ */
<T> List<T> queryList(String sql, Object[] params, Class<T> clazz) <T> List<T> queryValues(String sql, Object[] params, Class<T> clazz)
throws SQLException; throws SQLException;
/** /**
@@ -128,18 +129,39 @@ public interface JdbcOperations {
} }
/** /**
* 执行查询,返回结果映射为指定类型。当结果为单列时使用 * 执行查询,只取结果集每行第一列的值,映射为指定类型并返回列表
* 适用于 {@code SELECT single_column FROM ...} 单列查询场景。
* *
* @param <T> 目标类型 * @param <T> 目标类型
* @param sql SQL * @param sql SQL
* @param clazz 将结果映射为指定的类型 * @param clazz 将结果映射为指定的类型
* *
* @return 查询结果 * @return 每一行第一列的值列表。如果查询结果为空,则返回空列表
* @throws SQLException SQL 异常 * @throws SQLException SQL 异常
*/ */
default <T> List<T> queryValues(String sql, Class<T> clazz)
throws SQLException {
return queryValues(sql, ParamBuilder.EMPTY_OBJECT_ARRAY, clazz);
}
/**
* @deprecated 自 1.1.0 起,请使用 {@link #queryValues(String, Object[], Class)}。
* 此方法将在后续版本中移除。
*/
@Deprecated
default <T> List<T> queryList(String sql, Object[] params, Class<T> clazz)
throws SQLException {
return queryValues(sql, params, clazz);
}
/**
* @deprecated 自 1.1.0 起,请使用 {@link #queryValues(String, Class)}。
* 此方法将在后续版本中移除。
*/
@Deprecated
default <T> List<T> queryList(String sql, Class<T> clazz) default <T> List<T> queryList(String sql, Class<T> clazz)
throws SQLException { throws SQLException {
return queryList(sql, ParamBuilder.EMPTY_OBJECT_ARRAY, clazz); return queryValues(sql, clazz);
} }
/** /**
@@ -174,17 +196,18 @@ public interface JdbcOperations {
throws SQLException; throws SQLException;
/** /**
* 查询第一行第一列,并转换为指定类型 * 执行查询,只取结果集第一行第一列的值,映射为指定类型并返回。
* 适用于 {@code SELECT single_column FROM ... WHERE ...} 单列单行查询场景。
* *
* @param <T> 目标类型 * @param <T> 目标类型
* @param sql SQL * @param sql SQL
* @param params 参数 * @param params 参数
* @param clazz 目标类型 * @param clazz 目标类型
* *
* @return 查询结果 * @return 第一行第一列的值。如果查询结果为空,则返回 {@code Optional.empty()}
* @throws SQLException SQL 异常 * @throws SQLException SQL 异常
*/ */
<T> Optional<T> queryFirst(String sql, Object[] params, Class<T> clazz) <T> Optional<T> queryValue(String sql, Object[] params, Class<T> clazz)
throws SQLException; throws SQLException;
/** /**
@@ -215,18 +238,39 @@ public interface JdbcOperations {
} }
/** /**
* 查询第一行第一列,并转换为指定类型 * 执行查询,只取结果集第一行第一列的值,映射为指定类型并返回。
* 适用于 {@code SELECT single_column FROM ... WHERE ...} 单列单行查询场景。
* *
* @param <T> 目标类型 * @param <T> 目标类型
* @param sql SQL * @param sql SQL
* @param clazz 目标类型 * @param clazz 目标类型
* *
* @return 第一行第一列的值,如果查询结果为空,则返回 {@code Optional#empty()} * @return 第一行第一列的值,如果查询结果为空,则返回 {@code Optional.empty()}
* @throws SQLException SQL 异常 * @throws SQLException SQL 异常
*/ */
default <T> Optional<T> queryValue(String sql, Class<T> clazz)
throws SQLException {
return queryValue(sql, ParamBuilder.EMPTY_OBJECT_ARRAY, clazz);
}
/**
* @deprecated 自 1.1.0 起,请使用 {@link #queryValue(String, Object[], Class)}。
* 此方法将在后续版本中移除。
*/
@Deprecated
default <T> Optional<T> queryFirst(String sql, Object[] params, Class<T> clazz)
throws SQLException {
return queryValue(sql, params, clazz);
}
/**
* @deprecated 自 1.1.0 起,请使用 {@link #queryValue(String, Class)}。
* 此方法将在后续版本中移除。
*/
@Deprecated
default <T> Optional<T> queryFirst(String sql, Class<T> clazz) default <T> Optional<T> queryFirst(String sql, Class<T> clazz)
throws SQLException { throws SQLException {
return queryFirst(sql, ParamBuilder.EMPTY_OBJECT_ARRAY, clazz); return queryValue(sql, clazz);
} }
/** /**
@@ -242,6 +286,43 @@ public interface JdbcOperations {
return queryFirst(sql, ParamBuilder.EMPTY_OBJECT_ARRAY); return queryFirst(sql, ParamBuilder.EMPTY_OBJECT_ARRAY);
} }
/**
* 执行查询,只取结果集第一行第一列的值,映射为指定类型并返回。
* 如果查询结果为空,则返回指定的默认值。
* 适用于 {@code SELECT COUNT(*)}、{@code SELECT MAX(...)} 等聚合查询场景。
*
* @param <T> 目标类型
* @param sql SQL
* @param params 参数
* @param clazz 目标类型
* @param defaultValue 查询结果为空时返回的默认值
*
* @return 第一行第一列的值,如果查询结果为空则返回 {@code defaultValue}
* @throws SQLException SQL 异常
*/
default <T> T queryValueOrDefault(String sql, Object[] params, Class<T> clazz, T defaultValue)
throws SQLException {
return queryValue(sql, params, clazz).orElse(defaultValue);
}
/**
* 执行查询,只取结果集第一行第一列的值,映射为指定类型并返回。
* 如果查询结果为空,则返回指定的默认值。
* 适用于 {@code SELECT COUNT(*)}、{@code SELECT MAX(...)} 等聚合查询场景。
*
* @param <T> 目标类型
* @param sql SQL
* @param clazz 目标类型
* @param defaultValue 查询结果为空时返回的默认值
*
* @return 第一行第一列的值,如果查询结果为空则返回 {@code defaultValue}
* @throws SQLException SQL 异常
*/
default <T> T queryValueOrDefault(String sql, Class<T> clazz, T defaultValue)
throws SQLException {
return queryValueOrDefault(sql, ParamBuilder.EMPTY_OBJECT_ARRAY, clazz, defaultValue);
}
/** /**
* 查询第一行第一列并转换为 boolean * 查询第一行第一列并转换为 boolean
* *

View File

@@ -17,6 +17,7 @@
package xyz.zhouxy.jdbc; package xyz.zhouxy.jdbc;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.time.temporal.Temporal;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -28,10 +29,7 @@ import java.util.OptionalLong;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import xyz.zhouxy.plusone.commons.collection.CollectionTools; import xyz.zhouxy.jdbc.util.AssertTools;
import xyz.zhouxy.plusone.commons.util.ArrayTools;
import xyz.zhouxy.plusone.commons.util.AssertTools;
import xyz.zhouxy.plusone.commons.util.OptionalTools;
/** /**
* ParamBuilder * ParamBuilder
@@ -44,35 +42,83 @@ import xyz.zhouxy.plusone.commons.util.OptionalTools;
* @since 1.0.0 * @since 1.0.0
*/ */
public class ParamBuilder { public class ParamBuilder {
/**
* 空参数数组常量
*
* <p>
* 用于表示无参数的 SQL 操作
*/
public static final Object[] EMPTY_OBJECT_ARRAY = {}; public static final Object[] EMPTY_OBJECT_ARRAY = {};
/**
* 构建 SQL 参数数组
*
* <p>
* 将传入的参数转换为 {@code Object[]},用于 {@link PreparedStatement} 的参数填充。
* 支持自动拆箱 {@link Optional}、{@link OptionalInt}、{@link OptionalLong}、{@link OptionalDouble}。
* 对于 {@link CharSequence}、{@link Number}、{@link Boolean}、{@link Temporal} 类型不做转换直接透传。
* 如果传入的 {@code params} 为 {@code null} 或空,则返回 {@link #EMPTY_OBJECT_ARRAY}。
*
* @param params SQL 参数列表(可变参数)
* @return 参数数组
*/
public static Object[] buildParams(final Object... params) { public static Object[] buildParams(final Object... params) {
if (ArrayTools.isEmpty(params)) { if (params == null || params.length == 0) {
return EMPTY_OBJECT_ARRAY; return EMPTY_OBJECT_ARRAY;
} }
return Arrays.stream(params) return Arrays.stream(params)
.map(param -> { .map(ParamBuilder::handleItem)
if (param instanceof Optional) {
return OptionalTools.orElseNull((Optional<?>) param);
}
if (param instanceof OptionalInt) {
return OptionalTools.toInteger((OptionalInt) param);
}
if (param instanceof OptionalLong) {
return OptionalTools.toLong((OptionalLong) param);
}
if (param instanceof OptionalDouble) {
return OptionalTools.toDouble((OptionalDouble) param);
}
return param;
})
.toArray(); .toArray();
} }
private static Object handleItem(Object param) {
if (param == null) {
return null;
}
if (param instanceof CharSequence) {
return param.toString();
}
if (param instanceof Number) {
return param;
}
if (param instanceof Boolean) {
return param;
}
if (param instanceof Temporal) {
return param;
}
if (param instanceof Optional) {
return ((Optional<?>) param).orElse(null);
}
if (param instanceof OptionalInt) {
return ((OptionalInt) param).isPresent() ? ((OptionalInt) param).getAsInt() : null;
}
if (param instanceof OptionalLong) {
return ((OptionalLong) param).isPresent() ? ((OptionalLong) param).getAsLong() : null;
}
if (param instanceof OptionalDouble) {
return ((OptionalDouble) param).isPresent() ? ((OptionalDouble) param).getAsDouble() : null;
}
return param;
}
/**
* 批量构建参数列表
*
* <p>
* 将集合中的每个元素通过 {@code func} 映射为 {@code Object[]}
* 最终返回 {@code List<Object[]>},用于 {@code batchUpdate} 批量操作。
*
* @param <T> 集合元素类型
* @param c 待转换的集合
* @param func 转换函数,将集合元素转换为参数数组
* @return 参数数组列表
* @throws NullPointerException 如果 {@code c} 或 {@code func} 为 {@code null}
*/
public static <T> List<Object[]> buildBatchParams(final Collection<T> c, final Function<T, Object[]> func) { public static <T> List<Object[]> buildBatchParams(final Collection<T> c, final Function<T, Object[]> func) {
AssertTools.checkNotNull(c, "The collection can not be null."); AssertTools.checkNotNull(c, "The collection can not be null.");
AssertTools.checkNotNull(func, "The func can not be null."); AssertTools.checkNotNull(func, "The func can not be null.");
if (CollectionTools.isEmpty(c)) { if (c.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
return c.stream().map(func).collect(Collectors.toList()); return c.stream().map(func).collect(Collectors.toList());

View File

@@ -18,6 +18,8 @@ package xyz.zhouxy.jdbc;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/** /**
* ResultHandler * ResultHandler
@@ -41,4 +43,26 @@ public interface ResultHandler<T> {
* @throws SQLException 数据库执行异常 * @throws SQLException 数据库执行异常
*/ */
T handle(ResultSet resultSet) throws SQLException; T handle(ResultSet resultSet) throws SQLException;
/**
* 创建一个返回 {@link List} 的 {@link ResultHandler},将 {@link ResultSet} 中的每一行
* 通过指定的 {@link RowMapper} 映射为对象,最终收集为一个 {@link List}。
*
* @param <T> 列表元素类型
* @param rowMapper 行映射器,用于将 {@link ResultSet} 的单行转换为对象
* @return 返回 {@code List<T>} 的 {@code ResultHandler}
* @since 1.0.0
* @see RowMapper
*/
static <T> ResultHandler<List<T>> mapToList(RowMapper<T> rowMapper) {
return resultSet -> {
List<T> result = new ArrayList<>();
int rowNumber = 0;
while (resultSet.next()) {
T e = rowMapper.mapRow(resultSet, rowNumber++);
result.add(e);
}
return result;
};
}
} }

View File

@@ -60,9 +60,9 @@ public interface RowMapper<T> {
* @param <T> Java Bean 的类型 * @param <T> Java Bean 的类型
* *
* @return {@link DefaultBeanRowMapper} * @return {@link DefaultBeanRowMapper}
* @throws SQLException 如果创建 {@link DefaultBeanRowMapper} 失败 * @throws IllegalStateException 如果创建 {@link DefaultBeanRowMapper} 失败
*/ */
static <T> RowMapper<T> beanRowMapper(Class<T> beanType) throws SQLException { static <T> RowMapper<T> beanRowMapper(Class<T> beanType) {
return DefaultBeanRowMapper.of(beanType); return DefaultBeanRowMapper.of(beanType);
} }
@@ -74,10 +74,9 @@ public interface RowMapper<T> {
* @param <T> Java Bean 的类型 * @param <T> Java Bean 的类型
* *
* @return {@link DefaultBeanRowMapper} * @return {@link DefaultBeanRowMapper}
* @throws SQLException 如果创建 {@link DefaultBeanRowMapper} 失败 * @throws IllegalStateException 如果创建 {@link DefaultBeanRowMapper} 失败
*/ */
static <T> RowMapper<T> beanRowMapper(Class<T> beanType, Map<String, String> propertyColMap) static <T> RowMapper<T> beanRowMapper(Class<T> beanType, Map<String, String> propertyColMap) {
throws SQLException {
return DefaultBeanRowMapper.of(beanType, propertyColMap); return DefaultBeanRowMapper.of(beanType, propertyColMap);
} }
} }

View File

@@ -26,7 +26,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.sql.DataSource; import javax.sql.DataSource;
import xyz.zhouxy.plusone.commons.util.AssertTools; import xyz.zhouxy.jdbc.util.AssertTools;
/** /**
* JDBC 操作的模板类,对原生 JDBC 进行轻量封装,提供查询、更新、批量操作等便捷方法。 * JDBC 操作的模板类,对原生 JDBC 进行轻量封装,提供查询、更新、批量操作等便捷方法。
@@ -59,6 +59,11 @@ public class SimpleJdbcTemplate implements JdbcOperations {
@Nonnull @Nonnull
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
/**
* 构造一个 {@code SimpleJdbcTemplate} 实例
*
* @param dataSource 数据源,用于获取数据库连接;不可为 {@code null}
*/
public SimpleJdbcTemplate(@Nonnull DataSource dataSource) { public SimpleJdbcTemplate(@Nonnull DataSource dataSource) {
AssertTools.checkNotNull(dataSource); AssertTools.checkNotNull(dataSource);
this.dataSource = dataSource; this.dataSource = dataSource;
@@ -91,10 +96,10 @@ public class SimpleJdbcTemplate implements JdbcOperations {
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public <T> List<T> queryList(String sql, Object[] params, Class<T> clazz) public <T> List<T> queryValues(String sql, Object[] params, Class<T> clazz)
throws SQLException { throws SQLException {
try (Connection conn = this.dataSource.getConnection()) { try (Connection conn = this.dataSource.getConnection()) {
return JdbcOperationSupport.queryList(conn, sql, params, clazz); return JdbcOperationSupport.queryValues(conn, sql, params, clazz);
} }
} }
@@ -123,10 +128,10 @@ public class SimpleJdbcTemplate implements JdbcOperations {
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public <T> Optional<T> queryFirst(String sql, Object[] params, Class<T> clazz) public <T> Optional<T> queryValue(String sql, Object[] params, Class<T> clazz)
throws SQLException { throws SQLException {
try (Connection conn = this.dataSource.getConnection()) { try (Connection conn = this.dataSource.getConnection()) {
final T result = JdbcOperationSupport.queryFirst(conn, sql, params, clazz); final T result = JdbcOperationSupport.queryValue(conn, sql, params, clazz);
return Optional.ofNullable(result); return Optional.ofNullable(result);
} }
} }
@@ -148,7 +153,7 @@ public class SimpleJdbcTemplate implements JdbcOperations {
throws SQLException { throws SQLException {
try (Connection conn = this.dataSource.getConnection()) { try (Connection conn = this.dataSource.getConnection()) {
final Boolean result = JdbcOperationSupport final Boolean result = JdbcOperationSupport
.queryFirst(conn, sql, params, Boolean.class); .queryValue(conn, sql, params, Boolean.class);
return Boolean.TRUE.equals(result); return Boolean.TRUE.equals(result);
} }
} }
@@ -199,6 +204,14 @@ public class SimpleJdbcTemplate implements JdbcOperations {
// #region - transaction // #region - transaction
/**
* 获取事务模板
*
* <p>
* 返回的 {@link TransactionTemplate} 与当前模板共享同一个 {@link DataSource}。
*
* @return 事务模板
*/
public TransactionTemplate transaction() { public TransactionTemplate transaction() {
return this.transactionTemplate; return this.transactionTemplate;
} }

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2026-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc;
/**
* 可抛出受检异常的函数式接口。
*
* <p>
* 类似于 {@link java.util.function.Consumer},但 {@code accept} 方法允许抛出受检异常。
* </p>
*
* @param <T> 输入类型
* @param <E> 允许抛出的异常类型
* @author ZhouXY
* @since 1.1.0
*/
@FunctionalInterface
public interface ThrowingConsumer<T, E extends Exception> {
/**
* 对给定参数执行此操作。
*
* @param t 输入参数
* @throws E 异常
*/
void accept(T t) throws E;
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2026-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc;
/**
* 可抛出受检异常的谓词函数式接口。
*
* <p>
* 类似于 {@link java.util.function.Predicate},但 {@code test} 方法允许抛出受检异常。
* </p>
*
* @param <T> 输入类型
* @param <E> 允许抛出的异常类型
* @author ZhouXY
* @since 1.1.0
*/
@FunctionalInterface
public interface ThrowingPredicate<T, E extends Exception> {
/**
* 对给定参数执行此谓词判断。
*
* @param t 输入参数
* @return 谓词判断结果
* @throws E 异常
*/
boolean test(T t) throws E;
}

View File

@@ -26,9 +26,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.sql.DataSource; import javax.sql.DataSource;
import xyz.zhouxy.plusone.commons.function.ThrowingConsumer; import xyz.zhouxy.jdbc.util.AssertTools;
import xyz.zhouxy.plusone.commons.function.ThrowingPredicate;
import xyz.zhouxy.plusone.commons.util.AssertTools;
/** /**
* 事务模板,提供事务执行能力。 * 事务模板,提供事务执行能力。
@@ -64,7 +62,12 @@ public class TransactionTemplate {
@Nonnull @Nonnull
private final DataSource dataSource; private final DataSource dataSource;
public TransactionTemplate(@Nonnull DataSource dataSource) { /**
* 构造一个 {@code TransactionTemplate} 实例
*
* @param dataSource 数据源,用于获取数据库连接;不可为 {@code null}
*/
public TransactionTemplate(DataSource dataSource) {
AssertTools.checkNotNull(dataSource); AssertTools.checkNotNull(dataSource);
this.dataSource = dataSource; this.dataSource = dataSource;
} }
@@ -178,9 +181,9 @@ public class TransactionTemplate {
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public <T> List<T> queryList(String sql, Object[] params, Class<T> clazz) public <T> List<T> queryValues(String sql, Object[] params, Class<T> clazz)
throws SQLException { throws SQLException {
return JdbcOperationSupport.queryList(this.conn, sql, params, clazz); return JdbcOperationSupport.queryValues(this.conn, sql, params, clazz);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@@ -204,9 +207,9 @@ public class TransactionTemplate {
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public <T> Optional<T> queryFirst(String sql, Object[] params, Class<T> clazz) public <T> Optional<T> queryValue(String sql, Object[] params, Class<T> clazz)
throws SQLException { throws SQLException {
final T result = JdbcOperationSupport.queryFirst(this.conn, sql, params, clazz); final T result = JdbcOperationSupport.queryValue(this.conn, sql, params, clazz);
return Optional.ofNullable(result); return Optional.ofNullable(result);
} }
@@ -224,7 +227,7 @@ public class TransactionTemplate {
public boolean queryBoolean(String sql, Object[] params) public boolean queryBoolean(String sql, Object[] params)
throws SQLException { throws SQLException {
final Boolean result = JdbcOperationSupport final Boolean result = JdbcOperationSupport
.queryFirst(this.conn, sql, params, Boolean.class); .queryValue(this.conn, sql, params, Boolean.class);
return Boolean.TRUE.equals(result); return Boolean.TRUE.equals(result);
} }

View File

@@ -0,0 +1,334 @@
/*
* Copyright 2024-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc.util;
import java.util.function.Supplier;
/**
* 断言工具
*
* <p>
* 本工具类不封装过多判断逻辑,鼓励充分使用项目中的工具类进行逻辑判断。
*
* <pre>
* checkArgument(StringUtils.hasText(str), "The argument cannot be blank.");
* checkState(ArrayUtils.isNotEmpty(result), "The result cannot be empty.");
* checkCondition(!CollectionUtils.isEmpty(roles),
* () -&gt; new InvalidInputException("The roles cannot be empty."));
* checkCondition(RegexTools.matches(email, PatternConsts.EMAIL),
* "must be a well-formed email address");
* </pre>
*
* @author ZhouXY
*/
public class AssertTools {
// ================================
// #region - Argument
// ================================
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition) {
if (!condition) {
throw new IllegalArgumentException();
}
}
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @param errorMessage 异常信息
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition, String errorMessage) {
if (!condition) {
throw new IllegalArgumentException(errorMessage);
}
}
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @param errorMessageSupplier 异常信息
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition, Supplier<String> errorMessageSupplier) {
if (!condition) {
throw new IllegalArgumentException(errorMessageSupplier.get());
}
}
/**
* 检查实参
*
* @param condition 判断参数是否符合条件的结果
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @throws IllegalArgumentException 当条件不满足时抛出
*/
public static void checkArgument(boolean condition,
String errorMessageTemplate, Object... errorMessageArgs) {
if (!condition) {
throw new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
// #endregion - Argument
// ================================
// ================================
// #region - ArgumentNotNull
// ================================
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(T obj) {
if (obj == null) {
throw new IllegalArgumentException();
}
return obj;
}
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessage 异常信息
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(T obj, String errorMessage) {
if (obj == null) {
throw new IllegalArgumentException(errorMessage);
}
return obj;
}
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageSupplier 异常信息
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(T obj, Supplier<String> errorMessageSupplier) {
if (obj == null) {
throw new IllegalArgumentException(errorMessageSupplier.get());
}
return obj;
}
/**
* 判断入参不为 {@code null}
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @return 校验通过时返回入参
* @throws IllegalArgumentException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> T checkArgumentNotNull(T obj,
String errorMessageTemplate, Object... errorMessageArgs) {
if (obj == null) {
throw new IllegalArgumentException(String.format(errorMessageTemplate, errorMessageArgs));
}
return obj;
}
// ================================
// #endregion - ArgumentNotNull
// ================================
// ================================
// #region - State
// ================================
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition) {
if (!condition) {
throw new IllegalStateException();
}
}
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @param errorMessage 异常信息
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition, String errorMessage) {
if (!condition) {
throw new IllegalStateException(errorMessage);
}
}
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @param errorMessageSupplier 异常信息
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition, Supplier<String> errorMessageSupplier) {
if (!condition) {
throw new IllegalStateException(errorMessageSupplier.get());
}
}
/**
* 检查状态
*
* @param condition 判断状态是否符合条件的结果
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @throws IllegalStateException 当条件不满足时抛出
*/
public static void checkState(boolean condition,
String errorMessageTemplate, Object... errorMessageArgs) {
if (!condition) {
throw new IllegalStateException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
// #endregion - State
// ================================
// ================================
// #region - NotNull
// ================================
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(T obj) {
if (obj == null) {
throw new NullPointerException();
}
}
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessage 异常信息
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(T obj, String errorMessage) {
if (obj == null) {
throw new NullPointerException(errorMessage);
}
}
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageSupplier 异常信息
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(T obj, Supplier<String> errorMessageSupplier) {
if (obj == null) {
throw new NullPointerException(errorMessageSupplier.get());
}
}
/**
* 判空
*
* @param <T> 入参类型
* @param obj 入参
* @param errorMessageTemplate 异常信息模板
* @param errorMessageArgs 异常信息参数
* @throws NullPointerException 当 {@code obj} 为 {@code null} 时抛出
*/
public static <T> void checkNotNull(T obj,
String errorMessageTemplate, Object... errorMessageArgs) {
if (obj == null) {
throw new NullPointerException(String.format(errorMessageTemplate, errorMessageArgs));
}
}
// ================================
// #endregion - NotNull
// ================================
// ================================
// #region - Condition
// ================================
/**
* 当条件不满足时抛出异常。
*
* @param <T> 异常类型
* @param condition 条件
* @param e 异常
* @throws T 当条件不满足时抛出异常
*/
public static <T extends Exception> void checkCondition(boolean condition, Supplier<T> e)
throws T {
if (!condition) {
throw e.get();
}
}
// ================================
// #endregion
// ================================
// ================================
// #region - constructor
// ================================
private AssertTools() {
throw new IllegalStateException("Utility class");
}
// ================================
// #endregion
// ================================
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2026-present ZhouXY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc.util;
/**
* 字符串工具
*
* @author ZhouXY
* @since 1.0.0
*/
public class NamingTools {
/**
* 将小驼峰命名转换为小写下划线命名snake_case
*
* <p>转换规则:
* <ul>
* <li>小写→大写边界插入下划线:{@code userName → user_name}</li>
* <li>连续大写缩写视为整体,在其末尾小写边界插入下划线:{@code XMLParser → xml_parser}</li>
* <li>纯小写保持不变:{@code username → username}</li>
* <li>{@code null} 或空字符串返回原值</li>
* </ul>
*
* @param camelCase 小驼峰命名字符串,可空
* @return snake_case 命名字符串;{@code null} 输入返回 {@code null}
*/
public static String camelToSnake(String camelCase) {
if (camelCase == null || camelCase.isEmpty()) {
return camelCase;
}
StringBuilder sb = new StringBuilder(camelCase.length() * 2);
int len = camelCase.length();
for (int i = 0; i < len; i++) {
char c = camelCase.charAt(i);
if (isUpperCaseAscii(c)) {
if (shouldInsertUnderscore(camelCase, i)) {
sb.append('_');
}
sb.append((char) (c + 32)); // 转小写
} else {
sb.append(c);
}
}
return sb.toString();
}
private static boolean shouldInsertUnderscore(String str, int index) {
if (index == 0) {
return false;
}
char prev = str.charAt(index - 1);
char next = (index + 1 < str.length()) ? str.charAt(index + 1) : 0;
boolean prevIsBoundary = !isUpperCaseAscii(prev);
boolean nextIsLower = isLowerCaseAscii(next);
return prevIsBoundary || nextIsLower;
}
private static boolean isUpperCaseAscii(char c) {
return c >= 'A' && c <= 'Z';
}
private static boolean isLowerCaseAscii(char c) {
return c >= 'a' && c <= 'z';
}
// ================================
// #region - constructor
// ================================
private NamingTools() {
throw new IllegalStateException("Utility class");
}
// ================================
// #endregion
// ================================
}

View File

@@ -6,6 +6,7 @@ import static xyz.zhouxy.jdbc.ParamBuilder.*;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -16,8 +17,6 @@ import org.junit.jupiter.api.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
import xyz.zhouxy.jdbc.BatchUpdateErrorInfo; import xyz.zhouxy.jdbc.BatchUpdateErrorInfo;
import xyz.zhouxy.jdbc.BatchUpdateResult; import xyz.zhouxy.jdbc.BatchUpdateResult;
import xyz.zhouxy.jdbc.BatchUpdateStatus; import xyz.zhouxy.jdbc.BatchUpdateStatus;
@@ -170,7 +169,7 @@ class BatchUpdateTest extends BaseH2Test {
// #region - 包含错误数据 // #region - 包含错误数据
// ================================ // ================================
final List<User> userListContainingInvalidData = Lists.newArrayList( final List<User> userListContainingInvalidData = Arrays.asList(
// batch 0 // batch 0
new User("test_0001", "test_0001@example.com", 1, 1L, true), new User("test_0001", "test_0001@example.com", 1, 1L, true),
new User("test_0002", "test_0002@example.com", 1, 1L, true), new User("test_0002", "test_0002@example.com", 1, 1L, true),
@@ -200,7 +199,7 @@ class BatchUpdateTest extends BaseH2Test {
void testBatchUpdateQuietlyFalseInterrupted() throws SQLException { void testBatchUpdateQuietlyFalseInterrupted() throws SQLException {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
int count0 = template.queryFirst("SELECT COUNT(*) FROM users", Integer.class) int count0 = template.queryValue("SELECT COUNT(*) FROM users", Integer.class)
.orElse(0); .orElse(0);
List<Object[]> params = buildBatchParams(userListContainingInvalidData, a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() }); List<Object[]> params = buildBatchParams(userListContainingInvalidData, a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() });
@@ -232,7 +231,7 @@ class BatchUpdateTest extends BaseH2Test {
assertNull(result.getUpdateCounts(3)); assertNull(result.getUpdateCounts(3));
assertNull(result.getUpdateCounts(4)); assertNull(result.getUpdateCounts(4));
Optional<Integer> count8 = template.queryFirst("SELECT COUNT(*) FROM users", Integer.class); Optional<Integer> count8 = template.queryValue("SELECT COUNT(*) FROM users", Integer.class);
assertEquals(count0 + 8, count8.get().intValue()); assertEquals(count0 + 8, count8.get().intValue());
} }
@@ -243,7 +242,7 @@ class BatchUpdateTest extends BaseH2Test {
void testBatchUpdateQuietlyTrue() throws SQLException { void testBatchUpdateQuietlyTrue() throws SQLException {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
int count0 = template.queryFirst("SELECT COUNT(*) FROM users", Integer.class) int count0 = template.queryValue("SELECT COUNT(*) FROM users", Integer.class)
.orElse(0); .orElse(0);
List<Object[]> params = buildBatchParams(userListContainingInvalidData, a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() }); List<Object[]> params = buildBatchParams(userListContainingInvalidData, a -> new Object[] { a.getUsername(), a.getEmail(), a.getAge(), a.getBalance(), a.getActive() });
@@ -262,7 +261,7 @@ class BatchUpdateTest extends BaseH2Test {
assertArrayEquals(new int[] { Statement.EXECUTE_FAILED, 1, 1 }, result.getUpdateCounts(3)); assertArrayEquals(new int[] { Statement.EXECUTE_FAILED, 1, 1 }, result.getUpdateCounts(3));
assertArrayEquals(new int[] { 1 }, result.getUpdateCounts(4)); assertArrayEquals(new int[] { 1 }, result.getUpdateCounts(4));
Optional<Integer> count11 = template.queryFirst("SELECT COUNT(*) FROM users", Integer.class); Optional<Integer> count11 = template.queryValue("SELECT COUNT(*) FROM users", Integer.class);
assertEquals(count0 + 11, count11.get().intValue()); assertEquals(count0 + 11, count11.get().intValue());
} }

View File

@@ -10,6 +10,9 @@ import java.util.Optional;
import java.util.OptionalDouble; import java.util.OptionalDouble;
import java.util.OptionalInt; import java.util.OptionalInt;
import java.util.OptionalLong; import java.util.OptionalLong;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.function.Function; import java.util.function.Function;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
@@ -160,6 +163,29 @@ class ParamBuilderTest {
// #endregion // #endregion
// ==================================================================== // ====================================================================
// ====================================================================
// #region - buildParamsTemporal 时间类型
// --------------------------------------------------------------------
@Test
@DisplayName("buildParamsTemporal 类型LocalDate / LocalTime / LocalDateTime")
void testBuildParamsTemporal() {
LocalDate date = LocalDate.of(2024, 6, 15);
LocalTime time = LocalTime.of(14, 30, 0);
LocalDateTime dateTime = LocalDateTime.of(date, time);
Object[] result = buildParams(date, time, dateTime);
assertEquals(3, result.length);
assertSame(date, result[0]);
assertSame(time, result[1]);
assertSame(dateTime, result[2]);
}
// --------------------------------------------------------------------
// #endregion
// ====================================================================
// ==================================================================== // ====================================================================
// #region - buildBatchParams // #region - buildBatchParams
// -------------------------------------------------------------------- // --------------------------------------------------------------------

View File

@@ -18,7 +18,7 @@ import xyz.zhouxy.jdbc.ResultHandler;
import xyz.zhouxy.jdbc.SimpleJdbcTemplate; import xyz.zhouxy.jdbc.SimpleJdbcTemplate;
/** /**
* 查询 API 测试query、queryList、queryFirst、queryBoolean。 * 查询 API 测试query、queryList、queryFirst、queryValues、queryValue、queryBoolean。
*/ */
@DisplayName("SimpleJdbcTemplate 查询操作") @DisplayName("SimpleJdbcTemplate 查询操作")
class QueryTest extends BaseH2Test { class QueryTest extends BaseH2Test {
@@ -119,28 +119,28 @@ class QueryTest extends BaseH2Test {
assertEquals(5, users.size()); assertEquals(5, users.size());
} }
// ==================== queryList(Class) ==================== // ==================== queryValues(Class) ====================
@Test @Test
@DisplayName("queryList(Class):单列查询返回 String 列表") @DisplayName("queryValues(Class):单列查询返回 String 列表")
void testQueryListWithClassString() throws SQLException { void testQueryValuesWithClassString() throws SQLException {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
List<String> usernames = template.queryList( List<String> usernames = template.queryValues(
"SELECT username FROM users ORDER BY id", "SELECT username FROM users ORDER BY id",
String.class); String.class);
logger.info("queryList(Class) 返回用户名: {}", usernames); logger.info("queryValues(Class) 返回用户名: {}", usernames);
assertEquals(5, usernames.size()); assertEquals(5, usernames.size());
assertTrue(usernames.contains("alice")); assertTrue(usernames.contains("alice"));
} }
@Test @Test
@DisplayName("queryList(Class):空结果集返回空列表") @DisplayName("queryValues(Class):空结果集返回空列表")
void testQueryListEmptyResult() throws SQLException { void testQueryValuesEmptyResult() throws SQLException {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
List<String> result = template.queryList( List<String> result = template.queryValues(
"SELECT username FROM users WHERE id = ?", "SELECT username FROM users WHERE id = ?",
buildParams(999), String.class); buildParams(999), String.class);
@@ -215,14 +215,14 @@ class QueryTest extends BaseH2Test {
assertTrue(user.isPresent()); assertTrue(user.isPresent());
} }
// ==================== queryFirst(Class) ==================== // ==================== queryValue(Class) ====================
@Test @Test
@DisplayName("queryFirst(Class):查询第一行第一列") @DisplayName("queryValue(Class):查询第一行第一列")
void testQueryFirstWithClass() throws SQLException { void testQueryValueWithClass() throws SQLException {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
Optional<String> username = template.queryFirst( Optional<String> username = template.queryValue(
"SELECT username FROM users ORDER BY id", "SELECT username FROM users ORDER BY id",
String.class); String.class);
@@ -231,11 +231,11 @@ class QueryTest extends BaseH2Test {
} }
@Test @Test
@DisplayName("queryFirst(Class):空结果返回 Optional.empty()") @DisplayName("queryValue(Class):空结果返回 Optional.empty()")
void testQueryFirstClassEmpty() throws SQLException { void testQueryValueClassEmpty() throws SQLException {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
Optional<String> result = template.queryFirst( Optional<String> result = template.queryValue(
"SELECT username FROM users WHERE id = ?", "SELECT username FROM users WHERE id = ?",
buildParams(999), String.class); buildParams(999), String.class);
@@ -244,11 +244,11 @@ class QueryTest extends BaseH2Test {
@Test @Test
@DisplayName("queryFirst + Class统计总行数") @DisplayName("queryValue + Class统计总行数")
void testQueryFirstWithClass_queryCount() throws SQLException { void testQueryValueWithClass_queryCount() throws SQLException {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
int count = template.queryFirst( int count = template.queryValue(
"SELECT COUNT(*) FROM users", "SELECT COUNT(*) FROM users",
new Object[0], new Object[0],
Integer.class) Integer.class)
@@ -259,11 +259,11 @@ class QueryTest extends BaseH2Test {
} }
@Test @Test
@DisplayName("queryFirst + Class聚合求和") @DisplayName("queryValue + Class聚合求和")
void testQueryFirstWithClass_queryAggregation() throws SQLException { void testQueryValueWithClass_queryAggregation() throws SQLException {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
Long totalBalance = template.queryFirst( Long totalBalance = template.queryValue(
"SELECT SUM(balance) FROM users", "SELECT SUM(balance) FROM users",
new Object[0], new Object[0],
Long.class) Long.class)
@@ -273,6 +273,81 @@ class QueryTest extends BaseH2Test {
assertNotNull(totalBalance); assertNotNull(totalBalance);
} }
// ==================== queryValueOrDefault ====================
@Test
@DisplayName("queryValueOrDefault有结果时返回值")
void testQueryValueOrDefaultWithResult() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
String username = template.queryValueOrDefault(
"SELECT username FROM users WHERE id = ?",
new Object[]{1}, String.class, "default");
assertEquals("alice", username);
}
@Test
@DisplayName("queryValueOrDefault无结果时返回默认值")
void testQueryValueOrDefaultWithDefault() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
String username = template.queryValueOrDefault(
"SELECT username FROM users WHERE id = ?",
new Object[]{999}, String.class, "unknown");
assertEquals("unknown", username);
}
@Test
@DisplayName("queryValueOrDefaultCOUNT 聚合查询")
void testQueryValueOrDefaultCount() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
long count = template.queryValueOrDefault(
"SELECT COUNT(*) FROM users",
new Object[0], Long.class, 0L);
assertEquals(5L, count);
}
@Test
@DisplayName("queryValueOrDefaultSUM 聚合查询")
void testQueryValueOrDefaultSum() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
long totalBalance = template.queryValueOrDefault(
"SELECT SUM(balance) FROM users",
new Object[0], Long.class, 0L);
assertTrue(totalBalance > 0);
logger.info("queryValueOrDefault SUM 结果: {}", totalBalance);
}
@Test
@DisplayName("queryValueOrDefault无参数重载")
void testQueryValueOrDefaultNoParams() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
long count = template.queryValueOrDefault(
"SELECT COUNT(*) FROM users",
Long.class, 0L);
assertEquals(5L, count);
}
@Test
@DisplayName("queryValueOrDefault空表 COUNT 返回默认值 0")
void testQueryValueOrDefaultEmptyTable() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
long count = template.queryValueOrDefault(
"SELECT COUNT(*) FROM users WHERE id = ?",
new Object[]{999}, Long.class, 0L);
assertEquals(0L, count);
}
// ==================== queryFirst(Map) ==================== // ==================== queryFirst(Map) ====================
@Test @Test
@@ -385,7 +460,7 @@ class QueryTest extends BaseH2Test {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
assertThrows(SQLException.class, () -> assertThrows(SQLException.class, () ->
template.queryList("SELECT * FROM non_existent_table", template.queryValues("SELECT * FROM non_existent_table",
new Object[0], String.class)); new Object[0], String.class));
} }
@@ -395,7 +470,7 @@ class QueryTest extends BaseH2Test {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
assertThrows(SQLException.class, () -> assertThrows(SQLException.class, () ->
template.queryList("SELEC * FROM users", template.queryValues("SELEC * FROM users",
new Object[0], String.class)); new Object[0], String.class));
} }

View File

@@ -137,9 +137,9 @@ class RowMapperTest extends BaseH2Test {
} }
@Test @Test
@DisplayName("DefaultBeanRowMapper无无参构造器的 Bean 抛出 SQLException") @DisplayName("DefaultBeanRowMapper无无参构造器的 Bean 抛出 IllegalStateException")
void testDefaultBeanRowMapperNoNoArgConstructor() { void testDefaultBeanRowMapperNoNoArgConstructor() {
assertThrows(SQLException.class, () -> assertThrows(IllegalStateException.class, () ->
DefaultBeanRowMapper.of(BeanWithoutNoArgConstructor.class)); DefaultBeanRowMapper.of(BeanWithoutNoArgConstructor.class));
} }
@@ -157,6 +157,63 @@ class RowMapperTest extends BaseH2Test {
assertEquals("alice", user.get().getUsername()); assertEquals("alice", user.get().getUsername());
} }
// ==================== DefaultBeanRowMapper 连续大写缩写映射 ====================
@Test
@DisplayName("DefaultBeanRowMapper连续大写缩写属性正确映射为 snake_case")
void testDefaultBeanRowMapperAcronymMapping() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
// 创建测试表,列名使用 snake_case
template.update("CREATE TABLE acronym_test ("
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY,"
+ "home_url VARCHAR(100),"
+ "xml_parser VARCHAR(100),"
+ "parse_url VARCHAR(100),"
+ "user_id VARCHAR(100),"
+ "parse_html VARCHAR(100),"
+ "multi_http_client VARCHAR(100))");
template.update(
"INSERT INTO acronym_test (home_url, xml_parser, parse_url, user_id, parse_html, multi_http_client)"
+ " VALUES (?, ?, ?, ?, ?, ?)",
new Object[]{"https://example.com", "SAXParser", "/api/v1",
"user-001", "<div>test</div>", "ApacheHttpClient"});
RowMapper<AcronymBean> rowMapper = RowMapper.beanRowMapper(AcronymBean.class);
Optional<AcronymBean> result = template.queryFirst(
"SELECT * FROM acronym_test WHERE id = ?",
new Object[]{1L}, rowMapper);
assertTrue(result.isPresent());
AcronymBean bean = result.get();
assertEquals("https://example.com", bean.getHomeURL());
assertEquals("SAXParser", bean.getXmlParser());
assertEquals("/api/v1", bean.getParseURL());
assertEquals("user-001", bean.getUserID());
assertEquals("<div>test</div>", bean.getParseHTML());
assertEquals("ApacheHttpClient", bean.getMultiHttpClient());
logger.info("缩写映射: homeURL={}, xmlParser={}, parseURL={}, userID={}, parseHTML={}, multiHttpClient={}",
bean.getHomeURL(), bean.getXmlParser(), bean.getParseURL(),
bean.getUserID(), bean.getParseHTML(), bean.getMultiHttpClient());
}
@Test
@DisplayName("DefaultBeanRowMapper纯小写属性名映射为同名列")
void testDefaultBeanRowMapperAllLowercaseMapping() throws SQLException {
// 通过 User Bean 验证纯小写属性映射username → username, email → email
SimpleJdbcTemplate template = createTemplate();
RowMapper<User> rowMapper = RowMapper.beanRowMapper(User.class);
Optional<User> user = template.queryFirst(
"SELECT username, email FROM users WHERE username = ?",
new Object[]{"alice"}, rowMapper);
assertTrue(user.isPresent());
assertEquals("alice", user.get().getUsername());
assertEquals("alice@example.com", user.get().getEmail());
}
// ==================== HASH_MAP_MAPPER ==================== // ==================== HASH_MAP_MAPPER ====================
@Test @Test
@@ -253,4 +310,74 @@ class RowMapperTest extends BaseH2Test {
this.name = name; this.name = name;
} }
} }
/**
* 包含连续大写缩写属性的 Bean用于验证 camelToSnake 的缩写处理。
*
* <p>覆盖场景:
* <ul>
* <li>homeURL — 缩写在末尾(三字母 URL</li>
* <li>xmlParser — 缩写在前(三字母 XML</li>
* <li>parseURL — 缩写在末尾</li>
* <li>userID — 两字母缩写在末尾ID</li>
* <li>parseHTML — 四字母缩写在末尾HTML</li>
* <li>multiHttpClient — 缩写夹在词中HTTP</li>
* </ul>
*/
public static class AcronymBean {
private String homeURL;
private String xmlParser;
private String parseURL;
private String userID;
private String parseHTML;
private String multiHttpClient;
public String getHomeURL() {
return homeURL;
}
public void setHomeURL(String homeURL) {
this.homeURL = homeURL;
}
public String getXmlParser() {
return xmlParser;
}
public void setXmlParser(String xmlParser) {
this.xmlParser = xmlParser;
}
public String getParseURL() {
return parseURL;
}
public void setParseURL(String parseURL) {
this.parseURL = parseURL;
}
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getParseHTML() {
return parseHTML;
}
public void setParseHTML(String parseHTML) {
this.parseHTML = parseHTML;
}
public String getMultiHttpClient() {
return multiHttpClient;
}
public void setMultiHttpClient(String multiHttpClient) {
this.multiHttpClient = multiHttpClient;
}
}
} }

View File

@@ -44,12 +44,12 @@ class TransactionTest extends BaseH2Test {
}); });
// 验证事务已提交 // 验证事务已提交
Optional<String> newUser = template.queryFirst( Optional<String> newUser = template.queryValue(
"SELECT username FROM users WHERE username = ?", "SELECT username FROM users WHERE username = ?",
buildParams("txUser1"), String.class); buildParams("txUser1"), String.class);
assertTrue(newUser.isPresent()); assertTrue(newUser.isPresent());
Optional<Long> balance = template.queryFirst( Optional<Long> balance = template.queryValue(
"SELECT balance FROM users WHERE username = ?", "SELECT balance FROM users WHERE username = ?",
buildParams("alice"), Long.class); buildParams("alice"), Long.class);
assertEquals(Long.valueOf(99999L), balance.orElse(null)); assertEquals(Long.valueOf(99999L), balance.orElse(null));
@@ -65,7 +65,7 @@ class TransactionTest extends BaseH2Test {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
// 记录原始 balance // 记录原始 balance
Optional<Long> originalBalance = template.queryFirst( Optional<Long> originalBalance = template.queryValue(
"SELECT balance FROM users WHERE username = ?", "SELECT balance FROM users WHERE username = ?",
buildParams("alice"), Long.class); buildParams("alice"), Long.class);
@@ -85,13 +85,13 @@ class TransactionTest extends BaseH2Test {
assertEquals("模拟业务异常", ex.getCause().getMessage()); assertEquals("模拟业务异常", ex.getCause().getMessage());
// 验证更新已回滚 // 验证更新已回滚
Optional<Long> currentBalance = template.queryFirst( Optional<Long> currentBalance = template.queryValue(
"SELECT balance FROM users WHERE username = ?", "SELECT balance FROM users WHERE username = ?",
buildParams("alice"), Long.class); buildParams("alice"), Long.class);
assertEquals(originalBalance.orElse(null), currentBalance.orElse(null)); assertEquals(originalBalance.orElse(null), currentBalance.orElse(null));
// 验证插入已回滚 // 验证插入已回滚
Optional<String> rolledBackUser = template.queryFirst( Optional<String> rolledBackUser = template.queryValue(
"SELECT username FROM users WHERE username = ?", "SELECT username FROM users WHERE username = ?",
buildParams("txUser2"), String.class); buildParams("txUser2"), String.class);
assertFalse(rolledBackUser.isPresent()); assertFalse(rolledBackUser.isPresent());
@@ -114,7 +114,7 @@ class TransactionTest extends BaseH2Test {
// 验证插入已回滚 // 验证插入已回滚
assertDoesNotThrow(() -> { assertDoesNotThrow(() -> {
Optional<String> user = template.queryFirst( Optional<String> user = template.queryValue(
"SELECT username FROM users WHERE username = ?", "SELECT username FROM users WHERE username = ?",
buildParams("validUser"), String.class); buildParams("validUser"), String.class);
assertFalse(user.isPresent()); assertFalse(user.isPresent());
@@ -135,7 +135,7 @@ class TransactionTest extends BaseH2Test {
}); });
// 验证数据已持久化 // 验证数据已持久化
Optional<String> user = template.queryFirst( Optional<String> user = template.queryValue(
"SELECT username FROM users WHERE username = ?", "SELECT username FROM users WHERE username = ?",
buildParams("cftUser"), String.class); buildParams("cftUser"), String.class);
assertTrue(user.isPresent()); assertTrue(user.isPresent());
@@ -155,7 +155,7 @@ class TransactionTest extends BaseH2Test {
}); });
// 验证数据已回滚 // 验证数据已回滚
Optional<String> user = template.queryFirst( Optional<String> user = template.queryValue(
"SELECT username FROM users WHERE username = ?", "SELECT username FROM users WHERE username = ?",
buildParams("cffUser"), String.class); buildParams("cffUser"), String.class);
assertFalse(user.isPresent()); assertFalse(user.isPresent());
@@ -177,7 +177,7 @@ class TransactionTest extends BaseH2Test {
// 验证回滚 // 验证回滚
assertDoesNotThrow(() -> { assertDoesNotThrow(() -> {
Optional<String> user = template.queryFirst( Optional<String> user = template.queryValue(
"SELECT username FROM users WHERE username = ?", "SELECT username FROM users WHERE username = ?",
buildParams("exUser"), String.class); buildParams("exUser"), String.class);
assertFalse(user.isPresent()); assertFalse(user.isPresent());
@@ -196,7 +196,7 @@ class TransactionTest extends BaseH2Test {
buildParams("visible", "visible@test.com")); buildParams("visible", "visible@test.com"));
// 在同一事务内可以查询到刚插入的数据 // 在同一事务内可以查询到刚插入的数据
Optional<String> user = ops.queryFirst( Optional<String> user = ops.queryValue(
"SELECT username FROM users WHERE username = ?", "SELECT username FROM users WHERE username = ?",
buildParams("visible"), String.class); buildParams("visible"), String.class);
assertTrue(user.isPresent()); assertTrue(user.isPresent());
@@ -231,4 +231,32 @@ class TransactionTest extends BaseH2Test {
assertThrows(Exception.class, () -> assertThrows(Exception.class, () ->
template.transaction().execute(null)); template.transaction().execute(null));
} }
// ==================== TransactionException ====================
@Test
@DisplayName("TransactionException单参构造器仅 cause")
void testTransactionExceptionSingleArg() {
RuntimeException cause = new RuntimeException("原始异常");
TransactionException ex = new TransactionException(cause);
assertEquals("Transaction failed during execution", ex.getMessage());
assertSame(cause, ex.getCause());
}
@Test
@DisplayName("TransactionException双参构造器")
void testTransactionExceptionWithMessage() {
RuntimeException cause = new RuntimeException("原始异常");
TransactionException ex = new TransactionException("自定义消息", cause);
assertEquals("自定义消息", ex.getMessage());
assertSame(cause, ex.getCause());
}
@Test
@DisplayName("TransactionExceptionnull cause")
void testTransactionExceptionNullCause() {
TransactionException ex = new TransactionException(null);
assertEquals("Transaction failed during execution", ex.getMessage());
assertNull(ex.getCause());
}
} }

View File

@@ -21,6 +21,8 @@ import xyz.zhouxy.jdbc.SimpleJdbcTemplate;
/** /**
* 更新 API 测试update、updateAndReturnKeys。 * 更新 API 测试update、updateAndReturnKeys。
*
* <p>内部按 PreparedStatement有参数和 Statement无参数路径组织。</p>
*/ */
@DisplayName("SimpleJdbcTemplate 更新操作") @DisplayName("SimpleJdbcTemplate 更新操作")
class UpdateTest extends BaseH2Test { class UpdateTest extends BaseH2Test {
@@ -33,6 +35,7 @@ class UpdateTest extends BaseH2Test {
} }
// ==================== update ==================== // ==================== update ====================
// --- PreparedStatement 路径(有参数) ---
@Test @Test
@DisplayName("updateINSERT 操作返回影响行数 1") @DisplayName("updateINSERT 操作返回影响行数 1")
@@ -121,54 +124,18 @@ class UpdateTest extends BaseH2Test {
} }
@Test @Test
@DisplayName("updateDELETE 全表") @DisplayName("update参数中包含 null 元素")
void testUpdateDeleteAll() throws SQLException { void testUpdateWithNullElement() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
int rows = template.update("DELETE FROM users");
logger.info("DELETE 全表影响行数: {}", rows);
assertEquals(5, rows);
// 验证表为空
int count = template.query("SELECT COUNT(*) FROM users",
rs -> { rs.next(); return rs.getInt(1); });
assertEquals(0, count);
}
@Test
@DisplayName("updatenull 参数数组")
void testUpdateWithNullParams() throws SQLException {
SimpleJdbcTemplate template = createTemplate(); SimpleJdbcTemplate template = createTemplate();
int rows = template.update( int rows = template.update(
"DELETE FROM users WHERE username = ?", "DELETE FROM users WHERE username = ?",
new Object[]{ null }); new Object[]{ null });
// 因为 DELETE ? 中参数 null 不匹配任何行 // 参数 null 不匹配任何行
assertEquals(0, rows); assertEquals(0, rows);
} }
@Test
@DisplayName("update无参数重载")
void testUpdateNoParams() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
int rows = template.update(
"UPDATE users SET balance = 9999 WHERE username = 'alice'");
assertEquals(1, rows);
}
@Test
@DisplayName("update语法错误抛出 SQLException")
void testUpdateInvalidSql() {
SimpleJdbcTemplate template = createTemplate();
assertThrows(SQLException.class, () ->
template.update("UPDAT users SET x = 1"));
}
@Test @Test
@DisplayName("update使用 Instant 类型参数填充 TIMESTAMP 列") @DisplayName("update使用 Instant 类型参数填充 TIMESTAMP 列")
void testUpdateWithInstantParam() throws SQLException { void testUpdateWithInstantParam() throws SQLException {
@@ -195,7 +162,56 @@ class UpdateTest extends BaseH2Test {
assertEquals(java.sql.Timestamp.from(now), stored); assertEquals(java.sql.Timestamp.from(now), stored);
} }
// --- update / Statement 路径(无参数) ---
@Test
@DisplayName("update无参数重载")
void testUpdateNoParams() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
int rows = template.update(
"UPDATE users SET balance = 9999 WHERE username = 'alice'");
assertEquals(1, rows);
}
@Test
@DisplayName("updateDELETE 全表(无参数)")
void testUpdateDeleteAll() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
int rows = template.update("DELETE FROM users");
logger.info("DELETE 全表影响行数: {}", rows);
assertEquals(5, rows);
// 验证表为空
int count = template.query("SELECT COUNT(*) FROM users",
rs -> { rs.next(); return rs.getInt(1); });
assertEquals(0, count);
}
@Test
@DisplayName("updateparams 为 null走 Statement 路径")
void testUpdateWithParamsNull() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
int rows = template.update("DELETE FROM users", (Object[]) null);
assertEquals(5, rows);
}
@Test
@DisplayName("update语法错误抛出 SQLException")
void testUpdateInvalidSql() {
SimpleJdbcTemplate template = createTemplate();
assertThrows(SQLException.class, () ->
template.update("UPDAT users SET x = 1"));
}
// ==================== updateAndReturnKeys ==================== // ==================== updateAndReturnKeys ====================
// --- PreparedStatement 路径(有参数) ---
@Test @Test
@DisplayName("updateAndReturnKeysINSERT 返回自增主键") @DisplayName("updateAndReturnKeysINSERT 返回自增主键")
@@ -231,6 +247,8 @@ class UpdateTest extends BaseH2Test {
assertEquals(2, keys.size()); assertEquals(2, keys.size());
} }
// --- updateAndReturnKeys / Statement 路径(无参数) ---
@Test @Test
@DisplayName("updateAndReturnKeys无参数重载") @DisplayName("updateAndReturnKeys无参数重载")
void testUpdateAndReturnKeysNoParams() throws SQLException { void testUpdateAndReturnKeysNoParams() throws SQLException {
@@ -243,4 +261,18 @@ class UpdateTest extends BaseH2Test {
assertEquals(1, keys.size()); assertEquals(1, keys.size());
assertTrue(keys.get(0) > 0); assertTrue(keys.get(0) > 0);
} }
@Test
@DisplayName("updateAndReturnKeysparams 为 null走 Statement 路径")
void testUpdateAndReturnKeysWithParamsNull() throws SQLException {
SimpleJdbcTemplate template = createTemplate();
RowMapper<Long> rowMapper = (rs, rowNumber) -> rs.getLong(1);
List<Long> keys = template.updateAndReturnKeys(
"INSERT INTO users (username) VALUES ('null_test')",
null, rowMapper);
assertEquals(1, keys.size());
assertTrue(keys.get(0) > 0);
}
} }

View File

@@ -0,0 +1,522 @@
/*
* Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.zhouxy.jdbc.test.util;
import static org.junit.jupiter.api.Assertions.*;
import static xyz.zhouxy.jdbc.util.AssertTools.*;
import java.lang.reflect.Constructor;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import xyz.zhouxy.jdbc.util.AssertTools;
class AssertToolsTests {
// #region - Argument
@Test
void testCheckArgument_true() {
checkArgument(true);
}
@Test
void testCheckArgument_true_withMessage() {
final String IGNORE_ME = "IGNORE_ME"; // NOSONAR
checkArgument(true, IGNORE_ME);
}
@Test
void testCheckArgument_true_withNullMessage() {
final String IGNORE_ME = null; // NOSONAR
checkArgument(true, IGNORE_ME);
}
@Test
void testCheckArgument_true_withMessageSupplier() {
checkArgument(true, () -> "Error message: " + LocalDate.now());
}
@Test
void testCheckArgument_true_withNullMessageSupplier() {
final Supplier<String> IGNORE_ME = null; // NOSONAR
checkArgument(true, IGNORE_ME);
}
@Test
void testCheckArgument_true_withMessageFormat() {
LocalDate today = LocalDate.now();
checkArgument(true, "String format: %s", today);
}
@Test
void testCheckArgument_true_withNullMessageFormat() {
checkArgument(true, null, LocalDate.now());
}
@Test
void testCheckArgument_false() {
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgument_false_withMessage() {
final String message = "testCheckArgument_false_withMessage";
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false, message));
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgument_false_withNullMessage() {
final String message = null;
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false, message));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgument_false_withMessageSupplier() {
final LocalDate today = LocalDate.now();
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false, () -> "Error message: " + today));
assertEquals("Error message: " + today, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgument_false_withNullMessageSupplier() {
Supplier<String> messageSupplier = null;
assertThrows(NullPointerException.class,
() -> checkArgument(false, messageSupplier));
}
@Test
void testCheckArgument_false_withMessageFormat() {
LocalDate today = LocalDate.now();
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgument(false, "String format: %s", today));
assertEquals(String.format("String format: %s", today), e.getMessage());
}
@Test
void testCheckArgument_false_withNullMessageFormat() {
LocalDate today = LocalDate.now();
assertThrows(NullPointerException.class,
() -> checkArgument(false, null, today));
}
// #endregion - Argument
// #region - ArgumentNotNull
@Test
void testCheckArgumentNotNull_notNull() {
final Object object = new Object();
assertEquals(object, checkArgumentNotNull(object));
}
@Test
void testCheckArgumentNotNull_notNull_withMessage() {
final Object object = new Object();
final String IGNORE_ME = "IGNORE_ME"; // NOSONAR
assertEquals(object, checkArgumentNotNull(object, IGNORE_ME));
}
@Test
void testCheckArgumentNotNull_notNull_withNullMessage() {
final Object object = new Object();
final String IGNORE_ME = null; // NOSONAR
assertEquals(object, checkArgumentNotNull(object, IGNORE_ME));
}
@Test
void testCheckArgumentNotNull_notNull_withMessageSupplier() {
final Object object = new Object();
assertEquals(object, checkArgumentNotNull(object, () -> "Error message: " + LocalDate.now()));
}
@Test
void testCheckArgumentNotNull_notNull_withNullMessageSupplier() {
final Object object = new Object();
final Supplier<String> IGNORE_ME = null; // NOSONAR
assertEquals(object, checkArgumentNotNull(object, IGNORE_ME));
}
@Test
void testCheckArgumentNotNull_notNull_withMessageFormat() {
final Object object = new Object();
LocalDate today = LocalDate.now();
assertEquals(object, checkArgumentNotNull(object, "String format: %s", today));
}
@Test
void testCheckArgumentNotNull_notNull_withNullMessageFormat() {
final Object object = new Object();
assertEquals(object, checkArgumentNotNull(object, null, LocalDate.now()));
}
@Test
void testCheckArgumentNotNull_null() {
final Object object = null;
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgumentNotNull_null_withMessage() {
final Object object = null;
final String message = "testCheckArgumentNotNull_null_withMessage";
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object, message));
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgumentNotNull_null_withNullMessage() {
final Object object = null;
final String message = null;
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object, message));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgumentNotNull_null_withMessageSupplier() {
final Object object = null;
final LocalDate today = LocalDate.now();
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object, () -> "Error message: " + today));
assertEquals("Error message: " + today, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckArgumentNotNull_null_withNullMessageSupplier() {
final Object object = null;
Supplier<String> messageSupplier = null;
assertThrows(NullPointerException.class,
() -> checkArgumentNotNull(object, messageSupplier));
}
@Test
void testCheckArgumentNotNull_null_withMessageFormat() {
final Object object = null;
LocalDate today = LocalDate.now();
final IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> checkArgumentNotNull(object, "String format: %s", today));
assertEquals(String.format("String format: %s", today), e.getMessage());
}
@Test
void testCheckArgumentNotNull_null_withNullMessageFormat() {
final Object object = null;
LocalDate today = LocalDate.now();
assertThrows(NullPointerException.class,
() -> checkArgumentNotNull(object, null, today));
}
// #endregion - ArgumentNotNull
// #region - State
@Test
void testCheckState_true() {
checkState(true);
}
@Test
void testCheckState_true_withMessage() {
final String IGNORE_ME = "IGNORE_ME"; // NOSONAR
checkState(true, IGNORE_ME);
}
@Test
void testCheckState_true_withNullMessage() {
final String IGNORE_ME = null; // NOSONAR
checkState(true, IGNORE_ME);
}
@Test
void testCheckState_true_withMessageSupplier() {
checkState(true, () -> "Error message: " + LocalDate.now());
}
@Test
void testCheckState_true_withNullMessageSupplier() {
final Supplier<String> IGNORE_ME = null; // NOSONAR
checkState(true, IGNORE_ME);
}
@Test
void testCheckState_true_withMessageFormat() {
LocalDate today = LocalDate.now();
checkState(true, "String format: %s", today);
}
@Test
void testCheckState_true_withNullMessageFormat() {
checkState(true, null, LocalDate.now());
}
@Test
void testCheckState_false() {
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckState_false_withMessage() {
final String message = "testCheckState_false_withMessage";
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false, message));
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckState_false_withNullMessage() {
final String message = null;
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false, message));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckState_false_withMessageSupplier() {
final LocalDate today = LocalDate.now();
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false, () -> "Error message: " + today));
assertEquals("Error message: " + today, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckState_false_withNullMessageSupplier() {
Supplier<String> messageSupplier = null;
assertThrows(NullPointerException.class,
() -> checkState(false, messageSupplier));
}
@Test
void testCheckState_false_withMessageFormat() {
LocalDate today = LocalDate.now();
final IllegalStateException e = assertThrows(IllegalStateException.class,
() -> checkState(false, "String format: %s", today));
assertEquals(String.format("String format: %s", today), e.getMessage());
}
@Test
void testCheckState_false_withNullMessageFormat() {
LocalDate today = LocalDate.now();
assertThrows(NullPointerException.class,
() -> checkState(false, null, today));
}
// #endregion - State
// #region - NotNull
@Test
void testCheckNotNull_notNull() {
final Object object = new Object();
checkNotNull(object);
}
@Test
void testCheckNotNull_notNull_withMessage() {
final Object object = new Object();
final String IGNORE_ME = "IGNORE_ME"; // NOSONAR
checkNotNull(object, IGNORE_ME);
}
@Test
void testCheckNotNull_notNull_withNullMessage() {
final Object object = new Object();
final String IGNORE_ME = null; // NOSONAR
checkNotNull(object, IGNORE_ME);
}
@Test
void testCheckNotNull_notNull_withMessageSupplier() {
final Object object = new Object();
checkNotNull(object, () -> "Error message: " + LocalDate.now());
}
@Test
void testCheckNotNull_notNull_withNullMessageSupplier() {
final Object object = new Object();
final Supplier<String> IGNORE_ME = null; // NOSONAR
checkNotNull(object, IGNORE_ME);
}
@Test
void testCheckNotNull_notNull_withMessageFormat() {
final Object object = new Object();
LocalDate today = LocalDate.now();
checkNotNull(object, "String format: %s", today);
}
@Test
void testCheckNotNull_notNull_withNullMessageFormat() {
final Object object = new Object();
checkNotNull(object, null, LocalDate.now());
}
@Test
void testCheckNotNull_null() {
final Object object = null;
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckNotNull_null_withMessage() {
final Object object = null;
final String message = "testCheckNotNull_null_withMessage";
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object, message));
assertEquals(message, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckNotNull_null_withNullMessage() {
final Object object = null;
final String message = null;
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object, message));
assertNull(e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckNotNull_null_withMessageSupplier() {
final Object object = null;
final LocalDate today = LocalDate.now();
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object, () -> "Error message: " + today));
assertEquals("Error message: " + today, e.getMessage());
assertNull(e.getCause());
}
@Test
void testCheckNotNull_null_withNullMessageSupplier() {
final Object object = null;
Supplier<String> messageSupplier = null;
assertThrows(NullPointerException.class,
() -> checkNotNull(object, messageSupplier));
}
@Test
void testCheckNotNull_null_withMessageFormat() {
final Object object = null;
LocalDate today = LocalDate.now();
final NullPointerException e = assertThrows(NullPointerException.class,
() -> checkNotNull(object, "String format: %s", today));
assertEquals(String.format("String format: %s", today), e.getMessage());
}
@Test
void testCheckNotNull_null_withNullMessageFormat() {
final Object object = null;
LocalDate today = LocalDate.now();
assertThrows(NullPointerException.class,
() -> checkNotNull(object, null, today));
}
// #endregion - NotNull
// #region - Condition
static final class MyException extends RuntimeException {}
@Test
void testCheckCondition() {
checkCondition(true, MyException::new);
final MyException me = new MyException();
MyException e = assertThrows(MyException.class, () -> checkCondition(false, () -> me));
assertEquals(me, e);
}
// #endregion - Condition
// ================================
// #region - invoke constructor
// ================================
@Test
void test_constructor_isNotAccessible_ThrowsIllegalStateException() {
Constructor<?>[] constructors = AssertTools.class.getDeclaredConstructors();
Arrays.stream(constructors)
.forEach(constructor -> {
assertFalse(constructor.isAccessible());
constructor.setAccessible(true);
Throwable cause = assertThrows(Exception.class, constructor::newInstance)
.getCause();
assertInstanceOf(IllegalStateException.class, cause);
assertEquals("Utility class", cause.getMessage());
});
}
// ================================
// #endregion - invoke constructor
// ================================
}

View File

@@ -1,3 +1,4 @@
-- 测试用户表,覆盖全部列类型(数值/字符串/布尔/时间),用于验证 RowMapper 和查询功能
DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS users;
CREATE TABLE users ( CREATE TABLE users (
@@ -12,19 +13,25 @@ CREATE TABLE users (
work_start_time TIME work_start_time TIME
); );
-- 唯一约束用于测试 batchUpdate 的重复键错误场景
ALTER TABLE users ADD CONSTRAINT uk_username UNIQUE (username); ALTER TABLE users ADD CONSTRAINT uk_username UNIQUE (username);
-- 完整数据行(全部字段非空)
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('alice', 'alice@example.com', 28, 15000, TRUE, '2024-01-15 09:30:00', '1996-05-20', '08:00:00'); VALUES ('alice', 'alice@example.com', 28, 15000, TRUE, '2024-01-15 09:30:00', '1996-05-20', '08:00:00');
-- 完整数据行
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('bob', 'bob@example.com', 35, 25000, TRUE, '2023-11-01 14:00:00', '1989-03-12', '09:30:00'); VALUES ('bob', 'bob@example.com', 35, 25000, TRUE, '2023-11-01 14:00:00', '1989-03-12', '09:30:00');
-- null 数据行email、age、birth_date、work_start_time 均为 null用于测试空字段映射
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('charlie', NULL, NULL, 5000, FALSE, '2025-03-10 11:15:00', NULL, NULL); VALUES ('charlie', NULL, NULL, 5000, FALSE, '2025-03-10 11:15:00', NULL, NULL);
-- 部分 null 数据行balance 为 nullcreated_at 为 null用于测试字段级 null 映射
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('diana', 'diana@example.com', 42, NULL, TRUE, NULL, '1982-11-08', '07:45:00'); VALUES ('diana', 'diana@example.com', 42, NULL, TRUE, NULL, '1982-11-08', '07:45:00');
-- 完整数据行
INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time) INSERT INTO users (username, email, age, balance, active, created_at, birth_date, work_start_time)
VALUES ('eve', 'eve@example.com', 31, 8000, TRUE, '2024-07-22 16:45:00', '1993-01-30', '10:00:00'); VALUES ('eve', 'eve@example.com', 31, 8000, TRUE, '2024-07-22 16:45:00', '1993-01-30', '10:00:00');