forked from plusone/simple-jdbc
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7de2a7eec1 | |||
| 76bfff2d90 | |||
| 6f2882cd08 | |||
| 5100fa5f1d | |||
| 9abbef8b86 | |||
| 1f566ca488 | |||
| bb79777f0c | |||
| cf911c3756 | |||
| f471c1a489 | |||
| f35cfac913 | |||
| 76255bf3eb | |||
| aeb57dcdba | |||
| a0df4136f4 | |||
| ad0fbb788e | |||
| d740ad8467 | |||
| e577f72d4f | |||
| ef39c4323c | |||
| d70e12e254 | |||
| 721d0bbd5e | |||
| 0b3675d3a4 | |||
| 8b4f5bac65 | |||
| 486d0c98c7 | |||
| f5909818c3 | |||
| 3753aafd61 | |||
| f323d04d57 | |||
| eabd5d7f77 | |||
| 152094029e | |||
| 5b643291eb |
141
CHANGELOG.md
Normal file
141
CHANGELOG.md
Normal 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
24
NOTICE
@@ -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
433
README.md
@@ -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)` | 执行 DML(INSERT / 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)` | 执行 DML(INSERT / 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
30
pom.xml
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定批次更新结果
|
* 获取指定批次更新结果
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/main/java/xyz/zhouxy/jdbc/ThrowingConsumer.java
Normal file
41
src/main/java/xyz/zhouxy/jdbc/ThrowingConsumer.java
Normal 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;
|
||||||
|
}
|
||||||
42
src/main/java/xyz/zhouxy/jdbc/ThrowingPredicate.java
Normal file
42
src/main/java/xyz/zhouxy/jdbc/ThrowingPredicate.java
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
334
src/main/java/xyz/zhouxy/jdbc/util/AssertTools.java
Normal file
334
src/main/java/xyz/zhouxy/jdbc/util/AssertTools.java
Normal 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),
|
||||||
|
* () -> 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
|
||||||
|
// ================================
|
||||||
|
}
|
||||||
96
src/main/java/xyz/zhouxy/jdbc/util/NamingTools.java
Normal file
96
src/main/java/xyz/zhouxy/jdbc/util/NamingTools.java
Normal 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
|
||||||
|
// ================================
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 - buildParams:Temporal 时间类型
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("buildParams:Temporal 类型(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
|
||||||
// --------------------------------------------------------------------
|
// --------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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("queryValueOrDefault:COUNT 聚合查询")
|
||||||
|
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("queryValueOrDefault:SUM 聚合查询")
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("TransactionException:null cause")
|
||||||
|
void testTransactionExceptionNullCause() {
|
||||||
|
TransactionException ex = new TransactionException(null);
|
||||||
|
assertEquals("Transaction failed during execution", ex.getMessage());
|
||||||
|
assertNull(ex.getCause());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("update:INSERT 操作返回影响行数 1")
|
@DisplayName("update:INSERT 操作返回影响行数 1")
|
||||||
@@ -121,54 +124,18 @@ class UpdateTest extends BaseH2Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("update:DELETE 全表")
|
@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("update:null 参数数组")
|
|
||||||
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("update:DELETE 全表(无参数)")
|
||||||
|
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("update:params 为 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("updateAndReturnKeys:INSERT 返回自增主键")
|
@DisplayName("updateAndReturnKeys:INSERT 返回自增主键")
|
||||||
@@ -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("updateAndReturnKeys:params 为 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
522
src/test/java/xyz/zhouxy/jdbc/test/util/AssertToolsTests.java
Normal file
522
src/test/java/xyz/zhouxy/jdbc/test/util/AssertToolsTests.java
Normal 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
|
||||||
|
// ================================
|
||||||
|
}
|
||||||
@@ -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 为 null,created_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');
|
||||||
|
|||||||
Reference in New Issue
Block a user