已开启
add: DBCP 连接池适配 openGauss B 兼容模式示例与测试 #104
liuhongwei创建于 8月7日
add: DBCP 连接池适配 openGauss B 兼容模式示例与测试 #104
已开启
共 26 个文件变更+3255-0
| @@ -0,0 +1,205 @@ | |||
| 1 | +# DBCP 连接 openGauss B 兼容模式示例 | ||
| 2 | + | ||
| 3 | +本项目演示 Apache Commons DBCP2 通过 MySQL Connector/J 和 Dolphin MySQL 协议端口 | ||
| 4 | +访问 openGauss B 兼容模式数据库,并提供连接池、CRUD、事务、并发和 SQL 分类测试。 | ||
| 5 | + | ||
| 6 | +## 环境要求 | ||
| 7 | + | ||
| 8 | +| 组件 | 版本或要求 | | ||
| 9 | +|---|---| | ||
| 10 | +| JDK | 8+ | | ||
| 11 | +| Maven | 3.6+ | | ||
| 12 | +| Apache Commons DBCP2 | 2.9.0 | | ||
| 13 | +| Apache Commons Pool2 | 2.10.0(DBCP2 传递依赖) | | ||
| 14 | +| MySQL Connector/J | 8.0.28 | | ||
| 15 | +| openGauss | 包含 Dolphin 插件并启用 MySQL 协议 | | ||
| 16 | + | ||
| 17 | +DBCP2 2.9.0 和 Connector/J 8.0.28 的核心类使用 Java 8 字节码,因此最低运行版本为 | ||
| 18 | +JDK 8。openGauss 服务端与 Dolphin 应使用同一官方安装包,无需单独编译 Dolphin。 | ||
| 19 | + | ||
| 20 | +本文命令默认使用 Dolphin 端口 `3308`。自测报告使用独立实例端口 `36443`,通过 | ||
| 21 | +`-Ddbcp.url` 覆盖连接地址;端口不同只代表实例隔离,连接链路和测试内容相同。 | ||
| 22 | + | ||
| 23 | +## 连接链路 | ||
| 24 | + | ||
| 25 | +```text | ||
| 26 | +dbcp.properties | ||
| 27 | + -> BasicDataSourceFactory | ||
| 28 | + -> BasicDataSource.getConnection() | ||
| 29 | + -> GenericObjectPool.borrowObject() | ||
| 30 | + -> PoolableConnectionFactory.makeObject()(池中无空闲连接时) | ||
| 31 | + -> DriverConnectionFactory.createConnection() | ||
| 32 | + -> com.mysql.cj.jdbc.Driver.connect() | ||
| 33 | + -> com.mysql.cj.jdbc.ConnectionImpl | ||
| 34 | + -> TCP / Dolphin MySQL 协议端口 | ||
| 35 | + -> openGauss B 兼容库 | ||
| 36 | + -> ResultSet 或 updateCount | ||
| 37 | + -> PoolableConnection.close() | ||
| 38 | + -> GenericObjectPool.returnObject() 将连接归还 DBCP | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +`BasicDataSource` 管理池参数和连接生命周期。仅当池内没有可复用的空闲连接且未达到 | ||
| 42 | +`maxTotal` 时,DBCP 才调用 Connector/J 创建物理连接;Connector/J 负责 MySQL 协议通信, | ||
| 43 | +Dolphin 将协议请求交给 openGauss 执行。业务代码调用 `Connection.close()` 时,DBCP | ||
| 44 | +归还逻辑连接;物理连接是否保留由连接池配置决定。 | ||
| 45 | + | ||
| 46 | +## Spring Boot 快速开始 | ||
| 47 | + | ||
| 48 | +工程同时提供一个可直接运行的 Spring Boot 入口,用于验证真实的应用链路: | ||
| 49 | + | ||
| 50 | +```text | ||
| 51 | +SpringApplication.run | ||
| 52 | + -> Spring Boot DataSourceAutoConfiguration | ||
| 53 | + -> BasicDataSource(DBCP2) | ||
| 54 | + -> JdbcTemplate / TransactionTemplate | ||
| 55 | + -> MySQL Connector/J | ||
| 56 | + -> Dolphin MySQL 协议 | ||
| 57 | + -> openGauss B 兼容库 | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +`spring-boot-starter-jdbc` 默认会选择 HikariCP。本工程在 `pom.xml` 排除了 HikariCP, | ||
| 61 | +并在 `application.properties` 显式指定 `BasicDataSource`,因此运行时不会误用其他连接池。 | ||
| 62 | +完成数据库初始化后执行: | ||
| 63 | + | ||
| 64 | +```bash | ||
| 65 | +export DBCP_PASSWORD='实际密码' | ||
| 66 | +mvn spring-boot:run -Dspring-boot.run.jvmArguments="-DDBCP_PASSWORD=$DBCP_PASSWORD" | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +也可以通过环境变量覆盖地址和用户: | ||
| 70 | + | ||
| 71 | +```bash | ||
| 72 | +DBCP_URL='jdbc:mysql://127.0.0.1:3308/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC' \ | ||
| 73 | +DBCP_USER=opengauss DBCP_PASSWORD='实际密码' mvn spring-boot:run | ||
| 74 | +``` | ||
| 75 | + | ||
| 76 | +启动日志应包含 `BasicDataSource`、连接池参数、`JdbcTemplate` 查询行数以及 | ||
| 77 | +`TransactionTemplate COMMIT/ROLLBACK` 结果。示例会删除自己插入的临时数据。 | ||
| 78 | +在 VM master 转测包实例(Dolphin 36443)上已验证通过;使用专用账号时请确保同时授予 | ||
| 79 | +schema、表和自增序列权限。 | ||
| 80 | + | ||
| 81 | +## 普通 JDBC 示例与测试 | ||
| 82 | + | ||
| 83 | +### 1. 配置 Dolphin | ||
| 84 | + | ||
| 85 | +在 `postgresql.conf` 中设置: | ||
| 86 | + | ||
| 87 | +```ini | ||
| 88 | +shared_preload_libraries = 'dolphin' | ||
| 89 | +enable_dolphin_proto = on | ||
| 90 | +dolphin_server_port = 3308 | ||
| 91 | +dolphin.default_database_name = 'proto_test_db' | ||
| 92 | +``` | ||
| 93 | + | ||
| 94 | +重启实例并确认端口监听: | ||
| 95 | + | ||
| 96 | +```bash | ||
| 97 | +gs_ctl restart -D "$PGDATA" -Z single_node -l "$GAUSSLOG/restart.log" | ||
| 98 | +ss -tlnp | grep 3308 | ||
| 99 | +``` | ||
| 100 | + | ||
| 101 | +### 2. 配置客户端认证 | ||
| 102 | + | ||
| 103 | +以下配置仅允许本机访问。远程连接时应改为实际客户端所在的受限网段。 | ||
| 104 | + | ||
| 105 | +```ini | ||
| 106 | +local all all trust | ||
| 107 | +host all all 127.0.0.1/32 sha256 | ||
| 108 | +host all all ::1/128 sha256 | ||
| 109 | +``` | ||
| 110 | + | ||
| 111 | +修改 `pg_hba.conf` 后执行: | ||
| 112 | + | ||
| 113 | +```bash | ||
| 114 | +gs_ctl reload -D "$PGDATA" | ||
| 115 | +``` | ||
| 116 | + | ||
| 117 | +### 3. 初始化数据库 | ||
| 118 | + | ||
| 119 | +先将 `src/main/resources/create_b_database.sql` 中的 `<DBCP_PASSWORD>` 替换为测试密码, | ||
| 120 | +再按以下顺序执行脚本: | ||
| 121 | + | ||
| 122 | +```bash | ||
| 123 | +gsql -d postgres -h "$PGDATA" -p 5432 \ | ||
| 124 | + -f src/main/resources/create_b_database.sql | ||
| 125 | +gsql -d proto_test_db -h "$PGDATA" -p 5432 \ | ||
| 126 | + -f src/main/resources/init.sql | ||
| 127 | +``` | ||
| 128 | + | ||
| 129 | +脚本创建 B 兼容库 `proto_test_db`、schema `mysql_test_db`、`user` 表和初始数据。 | ||
| 130 | + | ||
| 131 | +### 4. 配置连接池 | ||
| 132 | + | ||
| 133 | +编辑 `src/main/resources/dbcp.properties`: | ||
| 134 | + | ||
| 135 | +```properties | ||
| 136 | +driverClassName=com.mysql.cj.jdbc.Driver | ||
| 137 | +url=jdbc:mysql://127.0.0.1:3308/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL | ||
| 138 | +username=opengauss | ||
| 139 | +password=<DBCP_PASSWORD> | ||
| 140 | + | ||
| 141 | +initialSize=2 | ||
| 142 | +maxTotal=8 | ||
| 143 | +maxIdle=8 | ||
| 144 | +minIdle=2 | ||
| 145 | +maxWaitMillis=5000 | ||
| 146 | + | ||
| 147 | +validationQuery=SELECT 1 | ||
| 148 | +testOnBorrow=true | ||
| 149 | +testWhileIdle=true | ||
| 150 | +``` | ||
| 151 | + | ||
| 152 | +### 5. 运行测试和示例 | ||
| 153 | + | ||
| 154 | +测试密码也可以通过系统属性传入,避免修改配置文件: | ||
| 155 | + | ||
| 156 | +```bash | ||
| 157 | +mvn clean test -Ddbcp.password="$DBCP_PASSWORD" | ||
| 158 | +mvn exec:java -Dexec.mainClass=org.opengauss.DbcpConnectDemo | ||
| 159 | +``` | ||
| 160 | + | ||
| 161 | +全量测试的预期结果为: | ||
| 162 | + | ||
| 163 | +```text | ||
| 164 | +Tests run: 46, Failures: 0, Errors: 0, Skipped: 0 | ||
| 165 | +BUILD SUCCESS | ||
| 166 | +``` | ||
| 167 | + | ||
| 168 | +## 连接池边界 | ||
| 169 | + | ||
| 170 | +`maxTotal=8` 表示单个数据源最多同时借出 8 条连接。连接池满后,新请求最多等待 | ||
| 171 | +`maxWaitMillis`;等待期间不得突破 `maxTotal`。连接归还后,排队请求应继续执行;等待 | ||
| 172 | +超时则抛出 `SQLException`。 | ||
| 173 | + | ||
| 174 | +`ConnectionPoolTest#testMaxTotalConcurrentCrudAndQueuedBorrow` 使用 8 条连接分别执行 | ||
| 175 | +`INSERT -> SELECT -> UPDATE -> SELECT -> DELETE -> COMMIT` 并保持占用,同时验证第 9 条 | ||
| 176 | +请求等待、超时和释放后的恢复行为。 | ||
| 177 | + | ||
| 178 | +`StabilityTest#testConcurrentCrudLoad` 使用 16 个并发连接执行完整事务 CRUD,并在结束时 | ||
| 179 | +断言活动连接数归零。更高负载测试固定 `maxTotal=128`,验证了 128 条同时持有连接和最多 | ||
| 180 | +5000 个排队业务请求;请求数不等于物理连接数。 | ||
| 181 | + | ||
| 182 | +## 测试范围 | ||
| 183 | + | ||
| 184 | +| 测试类 | 用例数 | 主要内容 | | ||
| 185 | +|---|---:|---| | ||
| 186 | +| `DbcpDataSourceTest` | 5 | 配置装配、驱动加载、初始连接及错误连接参数 | | ||
| 187 | +| `ConnectionPoolTest` | 10 | 借出归还、池参数、并发、满池等待与恢复 | | ||
| 188 | +| `ConnectionValidationTest` | 3 | `testOnBorrow`、`testWhileIdle` 和断连恢复 | | ||
| 189 | +| `CrudTest` | 8 | 查询、插入、更新、删除、分页和批处理 | | ||
| 190 | +| `TransactionTest` | 5 | 自动提交、提交、回滚、隔离级别和池化连接事务 | | ||
| 191 | +| `SqlCategoryTest` | 5 | DDL、DML、DQL、DCL、TCL | | ||
| 192 | +| `DataTypeCharsetTest` | 3 | 中文、emoji、常用类型和 NULL | | ||
| 193 | +| `ConstraintTest` | 3 | 多语句限制、游标参数和非 SSL 连接 | | ||
| 194 | +| `StabilityTest` | 4 | 重复建池、并发事务、长连接和池状态 | | ||
| 195 | +| **合计** | **46** | | | ||
| 196 | + | ||
| 197 | +DDL、DML、DQL、DCL、TCL 用例分别校验对象状态、影响行数、查询结果、授权状态以及 | ||
| 198 | +事务提交和回滚后的数据可见性。examples 的 DCL 测试使用具备 `CREATEROLE` 权限的 | ||
| 199 | +`opengauss` 测试账号;生产部署应改用专用业务账号,并按实际权限调整 DCL 用例。 | ||
| 200 | + | ||
| 201 | +## 相关文档 | ||
| 202 | + | ||
| 203 | +- `docs/初始化步骤.md`:完整初始化命令。 | ||
| 204 | +- `docs/测试思维导图.md`:测试范围思维导图。 | ||
| 205 | +- `docs/自测报告.md`:测试环境、结果和用例映射。 | ||
| @@ -0,0 +1,55 @@ | |||
| 1 | +# Spring Boot + DBCP 连接 openGauss B 兼容模式快速开始 | ||
| 2 | + | ||
| 3 | +## 环境要求 | ||
| 4 | + | ||
| 5 | +- JDK 8+、Maven 3.6+ | ||
| 6 | +- Apache Commons DBCP2 2.9.0、Commons Pool2 2.10.0 | ||
| 7 | +- MySQL Connector/J 8.0.28 | ||
| 8 | +- 包含 Dolphin 插件并已启用 MySQL 协议的 openGauss | ||
| 9 | + | ||
| 10 | +## 操作步骤 | ||
| 11 | + | ||
| 12 | +1. 在 `postgresql.conf` 中配置并重启实例: | ||
| 13 | + | ||
| 14 | + ```ini | ||
| 15 | + shared_preload_libraries = 'dolphin' | ||
| 16 | + enable_dolphin_proto = on | ||
| 17 | + dolphin_server_port = 3308 | ||
| 18 | + dolphin.default_database_name = 'proto_test_db' | ||
| 19 | + ``` | ||
| 20 | + | ||
| 21 | +2. 确认 Dolphin 端口监听: | ||
| 22 | + | ||
| 23 | + ```bash | ||
| 24 | + ss -tlnp | grep 3308 | ||
| 25 | + ``` | ||
| 26 | + | ||
| 27 | +3. 将 `src/main/resources/create_b_database.sql` 中的 `<DBCP_PASSWORD>` 替换为测试密码, | ||
| 28 | + 然后初始化数据库: | ||
| 29 | + | ||
| 30 | + ```bash | ||
| 31 | + gsql -d postgres -h "$PGDATA" -p 5432 \ | ||
| 32 | + -f src/main/resources/create_b_database.sql | ||
| 33 | + gsql -d proto_test_db -h "$PGDATA" -p 5432 \ | ||
| 34 | + -f src/main/resources/init.sql | ||
| 35 | + ``` | ||
| 36 | + | ||
| 37 | +4. 运行 Spring Boot 链路示例: | ||
| 38 | + | ||
| 39 | + ```bash | ||
| 40 | + DBCP_PASSWORD='实际密码' mvn spring-boot:run | ||
| 41 | + ``` | ||
| 42 | + | ||
| 43 | + 日志应显示实际数据源为 `org.apache.commons.dbcp2.BasicDataSource`,并显示 | ||
| 44 | + `JdbcTemplate` 查询、事务提交和回滚结果。 | ||
| 45 | + | ||
| 46 | +5. 运行 46 项低层测试和普通 JDBC 示例: | ||
| 47 | + | ||
| 48 | + ```bash | ||
| 49 | + mvn clean test -Ddbcp.password="$DBCP_PASSWORD" | ||
| 50 | + mvn exec:java -Dexec.mainClass=org.opengauss.DbcpConnectDemo | ||
| 51 | + ``` | ||
| 52 | + | ||
| 53 | +预期结果:`Tests run: 46, Failures: 0, Errors: 0, Skipped: 0`,`BUILD SUCCESS`。 | ||
| 54 | + | ||
| 55 | +完整说明见 `../README.md`、`../docs/初始化步骤.md` 和 `../docs/自测报告.md`。 | ||
| @@ -0,0 +1,95 @@ | |||
| 1 | +# 初始化步骤 | ||
| 2 | + | ||
| 3 | +本文说明如何为 DBCP 示例准备 openGauss B 兼容数据库。openGauss 应使用包含 Dolphin | ||
| 4 | +插件的官方安装包。 | ||
| 5 | + | ||
| 6 | +## 前置条件 | ||
| 7 | + | ||
| 8 | +- openGauss 实例已使用 `gs_initdb` 初始化。 | ||
| 9 | +- 当前用户可以执行 `gs_ctl` 和 `gsql`。 | ||
| 10 | +- 已设置 `PGDATA` 和 `GAUSSLOG`。 | ||
| 11 | + | ||
| 12 | +## 1. 配置 Dolphin | ||
| 13 | + | ||
| 14 | +编辑 `$PGDATA/postgresql.conf`: | ||
| 15 | + | ||
| 16 | +```ini | ||
| 17 | +shared_preload_libraries = 'dolphin' | ||
| 18 | +enable_dolphin_proto = on | ||
| 19 | +dolphin_server_port = 3308 | ||
| 20 | +dolphin.default_database_name = 'proto_test_db' | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +重启实例并确认两个协议端口均正常: | ||
| 24 | + | ||
| 25 | +```bash | ||
| 26 | +gs_ctl restart -D "$PGDATA" -Z single_node -l "$GAUSSLOG/restart.log" | ||
| 27 | +gs_ctl query -D "$PGDATA" | ||
| 28 | +ss -tlnp | grep -E '5432|3308' | ||
| 29 | +``` | ||
| 30 | + | ||
| 31 | +## 2. 配置客户端认证 | ||
| 32 | + | ||
| 33 | +编辑 `$PGDATA/pg_hba.conf`。以下示例只允许本机连接: | ||
| 34 | + | ||
| 35 | +```ini | ||
| 36 | +local all all trust | ||
| 37 | +host all all 127.0.0.1/32 sha256 | ||
| 38 | +host all all ::1/128 sha256 | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +远程连接时应将地址改为实际客户端所在的受限网段。修改后重新加载配置: | ||
| 42 | + | ||
| 43 | +```bash | ||
| 44 | +gs_ctl reload -D "$PGDATA" | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +## 3. 初始化数据库 | ||
| 48 | + | ||
| 49 | +1. 将 `src/main/resources/create_b_database.sql` 中的 `<DBCP_PASSWORD>` 替换为测试密码。 | ||
| 50 | +2. 使用相同密码配置 `src/main/resources/dbcp.properties`。 | ||
| 51 | +3. 依次执行建库和建表脚本: | ||
| 52 | + | ||
| 53 | +```bash | ||
| 54 | +gsql -d postgres -h "$PGDATA" -p 5432 \ | ||
| 55 | + -f src/main/resources/create_b_database.sql | ||
| 56 | +gsql -d proto_test_db -h "$PGDATA" -p 5432 \ | ||
| 57 | + -f src/main/resources/init.sql | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +第一个脚本创建 B 兼容库 `proto_test_db`、schema `mysql_test_db` 并设置 MySQL native | ||
| 61 | +密码;第二个脚本创建 `user` 表并写入三条初始数据。 | ||
| 62 | + | ||
| 63 | +如果 Spring Boot 使用非表 owner 的专用业务账号,需额外授予 schema、表和自增序列权限: | ||
| 64 | + | ||
| 65 | +```sql | ||
| 66 | +GRANT ALL ON SCHEMA mysql_test_db TO <DBCP_USER>; | ||
| 67 | +GRANT ALL ON ALL TABLES IN SCHEMA mysql_test_db TO <DBCP_USER>; | ||
| 68 | +GRANT ALL ON ALL SEQUENCES IN SCHEMA mysql_test_db TO <DBCP_USER>; | ||
| 69 | +``` | ||
| 70 | + | ||
| 71 | +## 4. Spring Boot 链路验证 | ||
| 72 | + | ||
| 73 | +完成初始化后,在工程目录执行: | ||
| 74 | + | ||
| 75 | +```bash | ||
| 76 | +DBCP_PASSWORD='实际密码' mvn spring-boot:run | ||
| 77 | +``` | ||
| 78 | + | ||
| 79 | +程序会由 Spring Boot 创建 DBCP `BasicDataSource`,通过 `JdbcTemplate` 查询业务表, | ||
| 80 | +再用 `TransactionTemplate` 分别验证提交和回滚。日志中的数据源类型必须为 | ||
| 81 | +`org.apache.commons.dbcp2.BasicDataSource`,否则说明依赖或配置未按文档设置。 | ||
| 82 | + | ||
| 83 | +## 5. 低层测试和 JDBC 示例 | ||
| 84 | + | ||
| 85 | +```bash | ||
| 86 | +mvn clean test -Ddbcp.password="$DBCP_PASSWORD" | ||
| 87 | +mvn exec:java -Dexec.mainClass=org.opengauss.DbcpConnectDemo | ||
| 88 | +``` | ||
| 89 | + | ||
| 90 | +全量测试应输出: | ||
| 91 | + | ||
| 92 | +```text | ||
| 93 | +Tests run: 46, Failures: 0, Errors: 0, Skipped: 0 | ||
| 94 | +BUILD SUCCESS | ||
| 95 | +``` | ||
| @@ -0,0 +1,95 @@ | |||
| 1 | +# DBCP 连接 openGauss B 兼容模式测试思维导图 | ||
| 2 | + | ||
| 3 | +```text | ||
| 4 | +DBCP 连接 openGauss B 兼容模式测试 | ||
| 5 | +│ | ||
| 6 | +├── 1. 环境与依赖 | ||
| 7 | +│ ├── openGauss B 兼容库 | ||
| 8 | +│ ├── Dolphin MySQL 协议端口 | ||
| 9 | +│ ├── JDK 8+ / Maven 3.6+ | ||
| 10 | +│ ├── DBCP2 2.9.0 / Pool2 2.10.0 | ||
| 11 | +│ └── MySQL Connector/J 8.0.28 | ||
| 12 | +│ | ||
| 13 | +├── 2. 数据源配置 | ||
| 14 | +│ ├── dbcp.properties 参数装配 | ||
| 15 | +│ ├── 驱动类、JDBC URL、用户名与密码 | ||
| 16 | +│ ├── initialSize / maxTotal | ||
| 17 | +│ ├── maxIdle / minIdle / maxWaitMillis | ||
| 18 | +│ ├── validationQuery / testOnBorrow / testWhileIdle | ||
| 19 | +│ └── 错误账号、密码和端口 | ||
| 20 | +│ | ||
| 21 | +├── 3. 连接池行为 | ||
| 22 | +│ ├── 连接借出与归还 | ||
| 23 | +│ ├── 物理会话复用 | ||
| 24 | +│ ├── 池化连接状态复位 | ||
| 25 | +│ ├── 空闲连接驱逐与补足 | ||
| 26 | +│ ├── 满池等待、超时与释放后恢复 | ||
| 27 | +│ └── 失效连接淘汰与重建 | ||
| 28 | +│ | ||
| 29 | +├── 4. CRUD 操作 | ||
| 30 | +│ ├── INSERT 与自增主键回填 | ||
| 31 | +│ ├── SELECT 全量、按主键、LIKE、分页 | ||
| 32 | +│ ├── UPDATE | ||
| 33 | +│ ├── DELETE | ||
| 34 | +│ └── JDBC batch 批量插入 | ||
| 35 | +│ | ||
| 36 | +├── 5. 事务操作 | ||
| 37 | +│ ├── autoCommit 默认值 | ||
| 38 | +│ ├── COMMIT 后跨连接可见 | ||
| 39 | +│ ├── ROLLBACK 后数据不可见 | ||
| 40 | +│ ├── READ_COMMITTED / REPEATABLE_READ | ||
| 41 | +│ └── 池化连接归还后的事务状态复位 | ||
| 42 | +│ | ||
| 43 | +├── 6. 标准 SQL 分类 | ||
| 44 | +│ ├── DDL:CREATE / ALTER / TRUNCATE / DROP | ||
| 45 | +│ ├── DML:INSERT / UPDATE / DELETE | ||
| 46 | +│ ├── DQL:WHERE / ORDER BY / COUNT | ||
| 47 | +│ ├── DCL:GRANT / REVOKE | ||
| 48 | +│ └── TCL:COMMIT / ROLLBACK | ||
| 49 | +│ | ||
| 50 | +├── 7. 数据与协议兼容性 | ||
| 51 | +│ ├── 中文与 emoji | ||
| 52 | +│ ├── 常用数值、日期和文本类型 | ||
| 53 | +│ ├── NULL 与 wasNull | ||
| 54 | +│ ├── 多语句限制 | ||
| 55 | +│ ├── useCursorFetch 配置 | ||
| 56 | +│ └── useSSL=false 连接 | ||
| 57 | +│ | ||
| 58 | +├── 8. 并发与稳定性 | ||
| 59 | +│ ├── 并发借出连接并同时持有 | ||
| 60 | +│ ├── 16 个工作线程执行完整事务 CRUD | ||
| 61 | +│ ├── 128 条连接同时持有 | ||
| 62 | +│ ├── 256~5000 个业务请求排队复用连接池 | ||
| 63 | +│ ├── 重复创建和关闭连接池 | ||
| 64 | +│ ├── 长连接连续查询 | ||
| 65 | +│ └── 负载结束 active=0 | ||
| 66 | +│ | ||
| 67 | +└── 9. 预期结果 | ||
| 68 | + ├── 9 个测试类、46 个测试方法全部通过 | ||
| 69 | + ├── Failures=0 / Errors=0 / Skipped=0 | ||
| 70 | + └── 测试数据、临时角色和临时表全部清理 | ||
| 71 | +``` | ||
| 72 | + | ||
| 73 | +## 测试主链路 | ||
| 74 | + | ||
| 75 | +```text | ||
| 76 | +dbcp.properties | ||
| 77 | + │ | ||
| 78 | + ▼ | ||
| 79 | +BasicDataSourceFactory | ||
| 80 | + │ | ||
| 81 | + ▼ | ||
| 82 | +BasicDataSource / GenericObjectPool | ||
| 83 | + │ 借出、校验、复用、归还 | ||
| 84 | + ▼ | ||
| 85 | +MySQL Connector/J | ||
| 86 | + │ MySQL 协议握手、认证、SQL | ||
| 87 | + ▼ | ||
| 88 | +Dolphin MySQL 协议端口 | ||
| 89 | + │ | ||
| 90 | + ▼ | ||
| 91 | +openGauss B 兼容库 | ||
| 92 | + │ | ||
| 93 | + ▼ | ||
| 94 | +CRUD / 事务 / 五类 SQL / 并发与稳定性断言 | ||
| 95 | +``` | ||
| @@ -0,0 +1,172 @@ | |||
| 1 | +# DBCP 连接 openGauss B 兼容模式自测报告 | ||
| 2 | + | ||
| 3 | +## 1. 测试环境 | ||
| 4 | + | ||
| 5 | +| 项目 | 配置 | | ||
| 6 | +|---|---| | ||
| 7 | +| 操作系统 | openEuler 24.03 x86_64 | | ||
| 8 | +| openGauss | 7.0.0 master 转测包,`SELECT VERSION()` 显示 7.0.0-RC3 build a2d43dcd | | ||
| 9 | +| Dolphin | MySQL 协议地址 `127.0.0.1:36443`(隔离自测实例;部署示例默认使用 `3308`) | | ||
| 10 | +| JDK | BiSheng JDK 8u472 | | ||
| 11 | +| Maven | 3.6.3 | | ||
| 12 | +| DBCP2 / Pool2 | 2.9.0 / 2.10.0 | | ||
| 13 | +| MySQL Connector/J | 8.0.28 | | ||
| 14 | +| 数据库 / schema | `proto_test_db` / `mysql_test_db` | | ||
| 15 | + | ||
| 16 | +测试使用官方配套安装包中的 openGauss-server 和 Dolphin,连接账号为具备 DCL 测试权限的 | ||
| 17 | +`opengauss`。数据库密码通过 Maven 系统属性传入,未写入测试日志。`36443` 是隔离实例 | ||
| 18 | +端口,初始化步骤和示例配置默认使用 `3308`;运行测试时通过 `-Ddbcp.url` 覆盖为 `36443`。 | ||
| 19 | + | ||
| 20 | +## 2. 执行命令 | ||
| 21 | + | ||
| 22 | +```bash | ||
| 23 | +mvn clean test \ | ||
| 24 | + -Ddbcp.url='jdbc:mysql://127.0.0.1:36443/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL' \ | ||
| 25 | + -Ddbcp.username=opengauss \ | ||
| 26 | + -Ddbcp.password="$DBCP_PASSWORD" | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +## 3. 测试结果 | ||
| 30 | + | ||
| 31 | +```text | ||
| 32 | +Tests run: 46, Failures: 0, Errors: 0, Skipped: 0 | ||
| 33 | +BUILD SUCCESS | ||
| 34 | +``` | ||
| 35 | + | ||
| 36 | +| 测试类 | 用例数 | 结果 | 验证内容 | | ||
| 37 | +|---|---:|---|---| | ||
| 38 | +| `DbcpDataSourceTest` | 5 | PASS | 配置装配、驱动加载、初始连接和错误连接参数 | | ||
| 39 | +| `ConnectionPoolTest` | 10 | PASS | 借出归还、池参数、并发、满池等待与恢复 | | ||
| 40 | +| `ConnectionValidationTest` | 3 | PASS | 连接校验、失效连接淘汰和物理连接重建 | | ||
| 41 | +| `CrudTest` | 8 | PASS | 查询、插入、更新、删除、分页和批处理 | | ||
| 42 | +| `TransactionTest` | 5 | PASS | 自动提交、提交、回滚、隔离级别和池化连接事务 | | ||
| 43 | +| `SqlCategoryTest` | 5 | PASS | DDL、DML、DQL、DCL、TCL | | ||
| 44 | +| `DataTypeCharsetTest` | 3 | PASS | 中文、emoji、常用类型和 NULL | | ||
| 45 | +| `ConstraintTest` | 3 | PASS | 多语句限制、游标参数和非 SSL 连接 | | ||
| 46 | +| `StabilityTest` | 4 | PASS | 重复建池、并发事务、长连接和池状态 | | ||
| 47 | +| **合计** | **46** | **PASS** | Failures、Errors、Skipped 均为 0 | | ||
| 48 | + | ||
| 49 | +## 4. 关键场景 | ||
| 50 | + | ||
| 51 | +### 4.1 配置文件到连接池实例 | ||
| 52 | + | ||
| 53 | +`DbcpDataSourceTest#testPoolConfigLoaded` 验证以下链路: | ||
| 54 | + | ||
| 55 | +```text | ||
| 56 | +dbcp.properties | ||
| 57 | + -> TestConfig.load() | ||
| 58 | + -> BasicDataSourceFactory.create(Properties) | ||
| 59 | + -> BasicDataSource | ||
| 60 | +``` | ||
| 61 | + | ||
| 62 | +测试逐项读取并断言 URL、用户名、`initialSize`、`maxTotal`、`maxIdle`、`minIdle`、 | ||
| 63 | +`maxWaitMillis`、`validationQuery`、`testOnBorrow` 和 `testWhileIdle`,确认配置文件中的参数 | ||
| 64 | +已应用到实际连接池实例。 | ||
| 65 | + | ||
| 66 | +### 4.2 满池等待与恢复 | ||
| 67 | + | ||
| 68 | +`ConnectionPoolTest#testMaxTotalConcurrentCrudAndQueuedBorrow` 设置 `maxTotal=8`: | ||
| 69 | + | ||
| 70 | +1. 同时借出 8 条连接。 | ||
| 71 | +2. 每条连接执行 `INSERT -> SELECT -> UPDATE -> SELECT -> DELETE -> COMMIT` 并保持占用。 | ||
| 72 | +3. 第 9 条请求进入等待,活动连接数始终不超过 8。 | ||
| 73 | +4. 不释放连接时,第 9 条请求在 `maxWaitMillis` 后超时。 | ||
| 74 | +5. 释放连接后,新请求可以重新借到连接并执行 `SELECT 1`。 | ||
| 75 | +6. 测试结束时 `numActive=0`。 | ||
| 76 | + | ||
| 77 | +### 4.3 并发业务 | ||
| 78 | + | ||
| 79 | +`StabilityTest#testConcurrentCrudLoad` 使用 16 个工作线程,每个线程持有独立连接完成一次 | ||
| 80 | +事务 CRUD,所有线程提交后均清理测试数据,最终活动连接数为 0。 | ||
| 81 | + | ||
| 82 | +高负载验证固定 `maxTotal=128`: | ||
| 83 | + | ||
| 84 | +| 场景 | 结果 | | ||
| 85 | +|---|---| | ||
| 86 | +| 128 条连接同时执行事务 CRUD | 全部完成,结束 `active=0` | | ||
| 87 | +| 256~5000 个业务请求排队复用 128 条连接 | 各档均完成,结束 `active=0` | | ||
| 88 | + | ||
| 89 | +请求数表示业务任务数量,连接池创建的物理连接数始终受 `maxTotal` 限制。 | ||
| 90 | + | ||
| 91 | +### 4.4 事务 | ||
| 92 | + | ||
| 93 | +| 场景 | 断言 | | ||
| 94 | +|---|---| | ||
| 95 | +| 自动提交 | 新连接的 `autoCommit` 为 `true` | | ||
| 96 | +| 提交 | `commit()` 后,其他连接可以查询到已提交数据 | | ||
| 97 | +| 回滚 | `rollback()` 后,其他连接查询不到回滚数据 | | ||
| 98 | +| 隔离级别 | 可以设置并读取 `TRANSACTION_READ_COMMITTED` 和 `TRANSACTION_REPEATABLE_READ` | | ||
| 99 | +| 池化连接 | 通过 DBCP 包装连接执行事务,归还后连接状态恢复 | | ||
| 100 | + | ||
| 101 | +### 4.5 DDL、DML、DQL、DCL、TCL | ||
| 102 | + | ||
| 103 | +| 分类 | 操作 | 断言 | | ||
| 104 | +|---|---|---| | ||
| 105 | +| DDL | `CREATE TABLE`、`ALTER TABLE`、`TRUNCATE TABLE`、`DROP TABLE` | 对象创建、结构变更、清空和删除成功 | | ||
| 106 | +| DML | `INSERT`、`UPDATE`、`DELETE` | 影响行数和最终数据正确 | | ||
| 107 | +| DQL | 条件查询、排序、`COUNT(*)` | 结果顺序、行数和聚合值正确 | | ||
| 108 | +| DCL | `GRANT SELECT`、`REVOKE SELECT` | 授权后权限存在,撤销后权限消失 | | ||
| 109 | +| TCL | `COMMIT`、`ROLLBACK` | 提交数据可见,回滚数据不可见 | | ||
| 110 | + | ||
| 111 | +DCL 用例使用临时角色和临时表,执行账号需要管理员权限或 `CREATEROLE` 权限。所有临时 | ||
| 112 | +对象均在测试结束时清理。 | ||
| 113 | + | ||
| 114 | +### 4.6 断连恢复 | ||
| 115 | + | ||
| 116 | +`ConnectionValidationTest` 通过独立管理连接终止被测服务端会话,然后验证: | ||
| 117 | + | ||
| 118 | +- `testOnBorrow=true` 时,借连接前发现失效连接并创建新的物理连接。 | ||
| 119 | +- `testWhileIdle=true` 时,维护线程从空闲池中清除失效连接。 | ||
| 120 | +- 新连接可以继续执行 `SELECT 1`。 | ||
| 121 | + | ||
| 122 | +## 5. Spring Boot 连接链路验证 | ||
| 123 | + | ||
| 124 | +examples 工程新增 Spring Boot 2.5.6、DBCP2 2.9.0、Connector/J 8.0.28 的可运行链路示例。 | ||
| 125 | +代码在本地完成编译,并在 VM 的 master 转测包实例中完成启动验证: | ||
| 126 | + | ||
| 127 | +```text | ||
| 128 | +SpringApplication.run() | ||
| 129 | + -> DataSourceAutoConfiguration | ||
| 130 | + -> BasicDataSource(DBCP2)/ PoolableConnection | ||
| 131 | + -> com.mysql.cj.jdbc.ConnectionImpl | ||
| 132 | + -> Dolphin MySQL 协议端口 | ||
| 133 | + -> openGauss B 兼容库 | ||
| 134 | + -> JdbcTemplate / TransactionTemplate | ||
| 135 | +``` | ||
| 136 | + | ||
| 137 | +运行时确认数据源类型和连接池参数;`JdbcTemplate` 完成查询, | ||
| 138 | +`TransactionTemplate` 完成提交和回滚,临时数据清理成功。 | ||
| 139 | + | ||
| 140 | +VM 内执行命令(密码通过环境变量传入,以下账号为临时测试账号): | ||
| 141 | + | ||
| 142 | +```bash | ||
| 143 | +DBCP_URL='jdbc:mysql://127.0.0.1:36443/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL' \ | ||
| 144 | +DBCP_USER='<DBCP_USER>' DBCP_PASSWORD='<DBCP_PASSWORD>' mvn spring-boot:run | ||
| 145 | +``` | ||
| 146 | + | ||
| 147 | +VM 验证实例:openGauss 7.0.0 master 转测包(版本显示 7.0.0-RC3 build a2d43dcd), | ||
| 148 | +PostgreSQL 端口 `36432`,Dolphin MySQL 协议端口 `36443`,JDK 1.8.0_472。 | ||
| 149 | +执行结果: | ||
| 150 | + | ||
| 151 | +```text | ||
| 152 | +Spring 注入数据源: org.apache.commons.dbcp2.BasicDataSource | ||
| 153 | +DBCP 参数: initialSize=2, maxTotal=8, maxWaitMillis=5000 | ||
| 154 | +JdbcTemplate 查询 user 表: 3 行 | ||
| 155 | +TransactionTemplate COMMIT 后查询到 1 行 | ||
| 156 | +TransactionTemplate ROLLBACK 后查询到 0 行 | ||
| 157 | +Spring Boot DBCP 链路验证完成,临时数据已清理 | ||
| 158 | +``` | ||
| 159 | + | ||
| 160 | +该验证确认 Spring Boot 自动配置实际创建的是 DBCP `BasicDataSource`,并通过 | ||
| 161 | +`JdbcTemplate` 完成查询、`TransactionTemplate` 完成提交和回滚;不计入 DBCP 项目的 | ||
| 162 | +46 个低层测试方法。VM 验证使用专用测试账号,并为自增列授予了 schema 下序列权限: | ||
| 163 | + | ||
| 164 | +```sql | ||
| 165 | +GRANT ALL ON ALL SEQUENCES IN SCHEMA mysql_test_db TO <DBCP_USER>; | ||
| 166 | +``` | ||
| 167 | + | ||
| 168 | +## 6. 结论 | ||
| 169 | + | ||
| 170 | +在本报告所列环境中,DBCP2 2.9.0 可以通过 Connector/J 8.0.28 和 Dolphin MySQL 协议 | ||
| 171 | +连接 openGauss B 兼容库。连接池配置、CRUD、事务、满池等待、并发业务、断连恢复以及 | ||
| 172 | +DDL、DML、DQL、DCL、TCL 操作均通过验证。 | ||
| @@ -0,0 +1,88 @@ | |||
| 1 | + | ||
| 2 | +<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
| 3 | + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| 4 | + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| 5 | + <modelVersion>4.0.0</modelVersion> | ||
| 6 | + | ||
| 7 | + <groupId>org.opengauss</groupId> | ||
| 8 | + <artifactId>dbcp-connect-opengauss-b</artifactId> | ||
| 9 | + <version>1.0.0</version> | ||
| 10 | + <packaging>jar</packaging> | ||
| 11 | + | ||
| 12 | + <name>dbcp-connect-opengauss-b</name> | ||
| 13 | + <description>Apache Commons DBCP 连接池适配 openGauss B 兼容模式数据库示例</description> | ||
| 14 | + | ||
| 15 | + <properties> | ||
| 16 | + <maven.compiler.source>1.8</maven.compiler.source> | ||
| 17 | + <maven.compiler.target>1.8</maven.compiler.target> | ||
| 18 | + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
| 19 | + <commons.dbcp2.version>2.9.0</commons.dbcp2.version> | ||
| 20 | + <mysql.connector.version>8.0.28</mysql.connector.version> | ||
| 21 | + <slf4j.version>1.7.36</slf4j.version> | ||
| 22 | + <junit.version>4.13.2</junit.version> | ||
| 23 | + <spring-boot.version>2.5.6</spring-boot.version> | ||
| 24 | + </properties> | ||
| 25 | + | ||
| 26 | + <dependencies> | ||
| 27 | + <!-- Apache Commons DBCP2 连接池(含 commons-pool2 依赖) --> | ||
| 28 | + <dependency> | ||
| 29 | + <groupId>org.apache.commons</groupId> | ||
| 30 | + <artifactId>commons-dbcp2</artifactId> | ||
| 31 | + <version>${commons.dbcp2.version}</version> | ||
| 32 | + </dependency> | ||
| 33 | + | ||
| 34 | + <!-- Spring Boot JDBC;排除默认 HikariCP,明确使用 DBCP2 --> | ||
| 35 | + <dependency> | ||
| 36 | + <groupId>org.springframework.boot</groupId> | ||
| 37 | + <artifactId>spring-boot-starter-jdbc</artifactId> | ||
| 38 | + <version>${spring-boot.version}</version> | ||
| 39 | + <exclusions> | ||
| 40 | + <exclusion> | ||
| 41 | + <groupId>com.zaxxer</groupId> | ||
| 42 | + <artifactId>HikariCP</artifactId> | ||
| 43 | + </exclusion> | ||
| 44 | + </exclusions> | ||
| 45 | + </dependency> | ||
| 46 | + | ||
| 47 | + <!-- MySQL JDBC 驱动:本示例固定使用已验证的 8.0.28 --> | ||
| 48 | + <dependency> | ||
| 49 | + <groupId>mysql</groupId> | ||
| 50 | + <artifactId>mysql-connector-java</artifactId> | ||
| 51 | + <version>${mysql.connector.version}</version> | ||
| 52 | + </dependency> | ||
| 53 | + | ||
| 54 | + <!-- 日志 --> | ||
| 55 | + <dependency> | ||
| 56 | + <groupId>org.slf4j</groupId> | ||
| 57 | + <artifactId>slf4j-api</artifactId> | ||
| 58 | + <version>${slf4j.version}</version> | ||
| 59 | + </dependency> | ||
| 60 | + <!-- 测试框架 --> | ||
| 61 | + <dependency> | ||
| 62 | + <groupId>junit</groupId> | ||
| 63 | + <artifactId>junit</artifactId> | ||
| 64 | + <version>${junit.version}</version> | ||
| 65 | + <scope>test</scope> | ||
| 66 | + </dependency> | ||
| 67 | + </dependencies> | ||
| 68 | + | ||
| 69 | + <build> | ||
| 70 | + <plugins> | ||
| 71 | + <plugin> | ||
| 72 | + <groupId>org.apache.maven.plugins</groupId> | ||
| 73 | + <artifactId>maven-compiler-plugin</artifactId> | ||
| 74 | + <version>3.11.0</version> | ||
| 75 | + </plugin> | ||
| 76 | + <plugin> | ||
| 77 | + <groupId>org.apache.maven.plugins</groupId> | ||
| 78 | + <artifactId>maven-surefire-plugin</artifactId> | ||
| 79 | + <version>3.1.2</version> | ||
| 80 | + </plugin> | ||
| 81 | + <plugin> | ||
| 82 | + <groupId>org.springframework.boot</groupId> | ||
| 83 | + <artifactId>spring-boot-maven-plugin</artifactId> | ||
| 84 | + <version>${spring-boot.version}</version> | ||
| 85 | + </plugin> | ||
| 86 | + </plugins> | ||
| 87 | + </build> | ||
| 88 | +</project> | ||
| @@ -0,0 +1,85 @@ | |||
| 1 | +package org.opengauss; | ||
| 2 | + | ||
| 3 | +import org.apache.commons.dbcp2.BasicDataSource; | ||
| 4 | +import org.opengauss.dao.UserDao; | ||
| 5 | +import org.opengauss.datasource.BasicDataSourceFactory; | ||
| 6 | +import org.opengauss.entity.User; | ||
| 7 | +import org.slf4j.Logger; | ||
| 8 | +import org.slf4j.LoggerFactory; | ||
| 9 | + | ||
| 10 | +import java.util.Arrays; | ||
| 11 | +import java.util.List; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * DBCP 连接池连接 openGauss B 兼容模式数据库 —— 主示例程序。 | ||
| 15 | + * | ||
| 16 | + * <p>运行前提: | ||
| 17 | + * <ol> | ||
| 18 | + * <li>openGauss 已开启 MySQL 协议兼容(enable_dolphin_proto=on),dolphin 监听端口可用</li> | ||
| 19 | + * <li>已创建 B 兼容库、业务用户并完成建表(见 src/main/resources/init.sql)</li> | ||
| 20 | + * <li>src/main/resources/dbcp.properties 中连接信息正确</li> | ||
| 21 | + * </ol> | ||
| 22 | + * | ||
| 23 | + * <p>演示流程:建池 → 查询 → 新增(自增主键回填)→ 更新 → 模糊/分页查询 → 批量插入 → 删除 → 关池。 | ||
| 24 | + */ | ||
| 25 | +public class DbcpConnectDemo { | ||
| 26 | + | ||
| 27 | + private static final Logger log = LoggerFactory.getLogger(DbcpConnectDemo.class); | ||
| 28 | + | ||
| 29 | + public static void main(String[] args) throws Exception { | ||
| 30 | + // 1. 创建 DBCP 连接池 | ||
| 31 | + BasicDataSource dataSource = BasicDataSourceFactory.create(); | ||
| 32 | + log.info("连接池创建成功, initialSize={}, maxTotal={}", | ||
| 33 | + dataSource.getInitialSize(), dataSource.getMaxTotal()); | ||
| 34 | + | ||
| 35 | + UserDao userDao = new UserDao(dataSource); | ||
| 36 | + | ||
| 37 | + try { | ||
| 38 | + // 2. 查询:全表 | ||
| 39 | + log.info("===== 全表查询 ====="); | ||
| 40 | + userDao.findAll().forEach(u -> log.info("{}", u)); | ||
| 41 | + | ||
| 42 | + // 3. 新增:自增主键回填 | ||
| 43 | + log.info("===== 新增 ====="); | ||
| 44 | + User newUser = new User("赵六", 21); | ||
| 45 | + int rows = userDao.insert(newUser); | ||
| 46 | + log.info("插入 {} 行, 回填主键 id={}", rows, newUser.getId()); | ||
| 47 | + | ||
| 48 | + // 4. 按 id 查询 | ||
| 49 | + User found = userDao.findById(newUser.getId()); | ||
| 50 | + log.info("按 id={} 查询结果: {}", newUser.getId(), found); | ||
| 51 | + | ||
| 52 | + // 5. 更新 | ||
| 53 | + log.info("===== 更新 ====="); | ||
| 54 | + newUser.setAge(22); | ||
| 55 | + log.info("更新 {} 行", userDao.update(newUser)); | ||
| 56 | + | ||
| 57 | + // 6. 模糊查询 | ||
| 58 | + log.info("===== 模糊查询 LIKE '%张%' ====="); | ||
| 59 | + userDao.findByNameLike("张").forEach(u -> log.info("{}", u)); | ||
| 60 | + | ||
| 61 | + // 7. 分页查询 | ||
| 62 | + log.info("===== 分页查询 LIMIT 2 OFFSET 1 ====="); | ||
| 63 | + userDao.findByPage(2, 1).forEach(u -> log.info("{}", u)); | ||
| 64 | + | ||
| 65 | + // 8. 批量插入 | ||
| 66 | + log.info("===== 批量插入 ====="); | ||
| 67 | + List<User> batch = Arrays.asList(new User("钱七", 23), new User("孙八", 24)); | ||
| 68 | + int[] batchRows = userDao.batchInsert(batch); | ||
| 69 | + log.info("批量插入影响行数: {}", Arrays.toString(batchRows)); | ||
| 70 | + batch.forEach(u -> log.info("回填主键: {}", u)); | ||
| 71 | + | ||
| 72 | + // 9. 删除 | ||
| 73 | + log.info("===== 删除 ====="); | ||
| 74 | + log.info("删除 {} 行", userDao.delete(newUser.getId())); | ||
| 75 | + | ||
| 76 | + // 10. 最终数据 | ||
| 77 | + log.info("===== 最终全表数据 ====="); | ||
| 78 | + userDao.findAll().forEach(u -> log.info("{}", u)); | ||
| 79 | + } finally { | ||
| 80 | + // 11. 关闭连接池(释放全部连接) | ||
| 81 | + dataSource.close(); | ||
| 82 | + log.info("连接池已关闭"); | ||
| 83 | + } | ||
| 84 | + } | ||
| 85 | +} | ||
| @@ -0,0 +1,84 @@ | |||
| 1 | +package org.opengauss; | ||
| 2 | + | ||
| 3 | +import org.apache.commons.dbcp2.BasicDataSource; | ||
| 4 | +import org.slf4j.Logger; | ||
| 5 | +import org.slf4j.LoggerFactory; | ||
| 6 | +import org.springframework.boot.CommandLineRunner; | ||
| 7 | +import org.springframework.boot.SpringApplication; | ||
| 8 | +import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
| 9 | +import org.springframework.context.annotation.Bean; | ||
| 10 | +import org.springframework.jdbc.core.JdbcTemplate; | ||
| 11 | +import org.springframework.transaction.PlatformTransactionManager; | ||
| 12 | +import org.springframework.transaction.support.TransactionTemplate; | ||
| 13 | + | ||
| 14 | +import javax.sql.DataSource; | ||
| 15 | +import java.util.UUID; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * Spring Boot + DBCP2 + MySQL Connector/J 连接 openGauss B 兼容库的可运行示例。 | ||
| 19 | + * | ||
| 20 | + * <p>启动时会输出实际数据源类型,并通过 JdbcTemplate 执行查询、提交和回滚, | ||
| 21 | + * 用完连接后由 Spring/DBCP 自动归还连接池。</p> | ||
| 22 | + */ | ||
| 23 | + | ||
| 24 | +public class SpringBootDbcpApplication { | ||
| 25 | + | ||
| 26 | + private static final Logger log = LoggerFactory.getLogger(SpringBootDbcpApplication.class); | ||
| 27 | + | ||
| 28 | + public static void main(String[] args) { | ||
| 29 | + SpringApplication.run(SpringBootDbcpApplication.class, args); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + | ||
| 33 | + public CommandLineRunner verifyDbcp(DataSource dataSource, JdbcTemplate jdbcTemplate, | ||
| 34 | + PlatformTransactionManager transactionManager) { | ||
| 35 | + return args -> { | ||
| 36 | + if (!(dataSource instanceof BasicDataSource)) { | ||
| 37 | + throw new IllegalStateException("实际数据源不是 DBCP BasicDataSource: " | ||
| 38 | + + dataSource.getClass().getName()); | ||
| 39 | + } | ||
| 40 | + BasicDataSource dbcp = (BasicDataSource) dataSource; | ||
| 41 | + log.info("Spring 注入数据源: {}", dataSource.getClass().getName()); | ||
| 42 | + log.info("DBCP 参数: initialSize={}, maxTotal={}, maxWaitMillis={}", | ||
| 43 | + dbcp.getInitialSize(), dbcp.getMaxTotal(), dbcp.getMaxWaitMillis()); | ||
| 44 | + | ||
| 45 | + Integer initialRows = jdbcTemplate.queryForObject( | ||
| 46 | + "SELECT COUNT(*) FROM `user`", Integer.class); | ||
| 47 | + log.info("JdbcTemplate 查询 user 表: {} 行", initialRows); | ||
| 48 | + | ||
| 49 | + String marker = "spring-" + UUID.randomUUID().toString().replace("-", ""); | ||
| 50 | + String rollbackMarker = marker + "-rollback"; | ||
| 51 | + TransactionTemplate transactions = new TransactionTemplate(transactionManager); | ||
| 52 | + try { | ||
| 53 | + // 提交事务:插入后由另一个连接可见,证明 Spring 事务管理器已生效。 | ||
| 54 | + transactions.execute(status -> { | ||
| 55 | + jdbcTemplate.update("INSERT INTO `user`(`name`,`age`) VALUES (?,?)", marker, 30); | ||
| 56 | + return null; | ||
| 57 | + }); | ||
| 58 | + Integer committed = jdbcTemplate.queryForObject( | ||
| 59 | + "SELECT COUNT(*) FROM `user` WHERE `name` = ?", Integer.class, marker); | ||
| 60 | + if (!Integer.valueOf(1).equals(committed)) { | ||
| 61 | + throw new IllegalStateException("提交事务后的数据行数应为 1,实际为 " + committed); | ||
| 62 | + } | ||
| 63 | + log.info("TransactionTemplate COMMIT 后查询到 {} 行", committed); | ||
| 64 | + | ||
| 65 | + // 回滚事务:标记回滚后不得留下数据。 | ||
| 66 | + transactions.execute(status -> { | ||
| 67 | + jdbcTemplate.update("INSERT INTO `user`(`name`,`age`) VALUES (?,?)", | ||
| 68 | + rollbackMarker, 31); | ||
| 69 | + status.setRollbackOnly(); | ||
| 70 | + return null; | ||
| 71 | + }); | ||
| 72 | + Integer rolledBack = jdbcTemplate.queryForObject( | ||
| 73 | + "SELECT COUNT(*) FROM `user` WHERE `name` = ?", Integer.class, rollbackMarker); | ||
| 74 | + if (!Integer.valueOf(0).equals(rolledBack)) { | ||
| 75 | + throw new IllegalStateException("回滚事务后的数据行数应为 0,实际为 " + rolledBack); | ||
| 76 | + } | ||
| 77 | + log.info("TransactionTemplate ROLLBACK 后查询到 {} 行", rolledBack); | ||
| 78 | + } finally { | ||
| 79 | + jdbcTemplate.update("DELETE FROM `user` WHERE `name` IN (?, ?)", marker, rollbackMarker); | ||
| 80 | + log.info("Spring Boot DBCP 链路验证完成,临时数据已清理"); | ||
| 81 | + } | ||
| 82 | + }; | ||
| 83 | + } | ||
| 84 | +} | ||
| @@ -0,0 +1,174 @@ | |||
| 1 | +package org.opengauss.dao; | ||
| 2 | + | ||
| 3 | +import org.opengauss.entity.User; | ||
| 4 | + | ||
| 5 | +import javax.sql.DataSource; | ||
| 6 | +import java.sql.Connection; | ||
| 7 | +import java.sql.PreparedStatement; | ||
| 8 | +import java.sql.ResultSet; | ||
| 9 | +import java.sql.SQLException; | ||
| 10 | +import java.sql.Statement; | ||
| 11 | +import java.util.ArrayList; | ||
| 12 | +import java.util.List; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * 基于 JDBC 手写 SQL 的 DAO,演示 DBCP 连接池下的 CRUD / 分页 / 批量操作。 | ||
| 16 | + * 所有连接均从连接池(DataSource)获取,用完即还,由 DBCP 统一管理。 | ||
| 17 | + */ | ||
| 18 | +public class UserDao { | ||
| 19 | + | ||
| 20 | + private final DataSource dataSource; | ||
| 21 | + | ||
| 22 | + public UserDao(DataSource dataSource) { | ||
| 23 | + this.dataSource = dataSource; | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 新增用户,并回填自增主键(AUTO_INCREMENT) | ||
| 28 | + */ | ||
| 29 | + public int insert(User user) throws SQLException { | ||
| 30 | + String sql = "INSERT INTO `user`(`name`,`age`) VALUES (?,?)"; | ||
| 31 | + try (Connection conn = dataSource.getConnection(); | ||
| 32 | + PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { | ||
| 33 | + ps.setString(1, user.getName()); | ||
| 34 | + ps.setInt(2, user.getAge()); | ||
| 35 | + int rows = ps.executeUpdate(); | ||
| 36 | + try (ResultSet keys = ps.getGeneratedKeys()) { | ||
| 37 | + if (keys.next()) { | ||
| 38 | + user.setId(keys.getInt(1)); | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + return rows; | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 按 id 查询 | ||
| 47 | + */ | ||
| 48 | + public User findById(int id) throws SQLException { | ||
| 49 | + String sql = "SELECT id,name,age FROM `user` WHERE id=?"; | ||
| 50 | + try (Connection conn = dataSource.getConnection(); | ||
| 51 | + PreparedStatement ps = conn.prepareStatement(sql)) { | ||
| 52 | + ps.setInt(1, id); | ||
| 53 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 54 | + if (rs.next()) { | ||
| 55 | + return toUser(rs); | ||
| 56 | + } | ||
| 57 | + } | ||
| 58 | + } | ||
| 59 | + return null; | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + /** | ||
| 63 | + * 全表查询 | ||
| 64 | + */ | ||
| 65 | + public List<User> findAll() throws SQLException { | ||
| 66 | + String sql = "SELECT id,name,age FROM `user` ORDER BY id"; | ||
| 67 | + List<User> list = new ArrayList<>(); | ||
| 68 | + try (Connection conn = dataSource.getConnection(); | ||
| 69 | + PreparedStatement ps = conn.prepareStatement(sql); | ||
| 70 | + ResultSet rs = ps.executeQuery()) { | ||
| 71 | + while (rs.next()) { | ||
| 72 | + list.add(toUser(rs)); | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | + return list; | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + /** | ||
| 79 | + * 模糊查询(LIKE) | ||
| 80 | + */ | ||
| 81 | + public List<User> findByNameLike(String keyword) throws SQLException { | ||
| 82 | + String sql = "SELECT id,name,age FROM `user` WHERE name LIKE ? ORDER BY id"; | ||
| 83 | + List<User> list = new ArrayList<>(); | ||
| 84 | + try (Connection conn = dataSource.getConnection(); | ||
| 85 | + PreparedStatement ps = conn.prepareStatement(sql)) { | ||
| 86 | + ps.setString(1, "%" + keyword + "%"); | ||
| 87 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 88 | + while (rs.next()) { | ||
| 89 | + list.add(toUser(rs)); | ||
| 90 | + } | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + return list; | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + /** | ||
| 97 | + * 分页查询(LIMIT/OFFSET) | ||
| 98 | + */ | ||
| 99 | + public List<User> findByPage(int limit, int offset) throws SQLException { | ||
| 100 | + String sql = "SELECT id,name,age FROM `user` ORDER BY id LIMIT ? OFFSET ?"; | ||
| 101 | + List<User> list = new ArrayList<>(); | ||
| 102 | + try (Connection conn = dataSource.getConnection(); | ||
| 103 | + PreparedStatement ps = conn.prepareStatement(sql)) { | ||
| 104 | + ps.setInt(1, limit); | ||
| 105 | + ps.setInt(2, offset); | ||
| 106 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 107 | + while (rs.next()) { | ||
| 108 | + list.add(toUser(rs)); | ||
| 109 | + } | ||
| 110 | + } | ||
| 111 | + } | ||
| 112 | + return list; | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + /** | ||
| 116 | + * 更新用户 | ||
| 117 | + */ | ||
| 118 | + public int update(User user) throws SQLException { | ||
| 119 | + String sql = "UPDATE `user` SET name=?, age=? WHERE id=?"; | ||
| 120 | + try (Connection conn = dataSource.getConnection(); | ||
| 121 | + PreparedStatement ps = conn.prepareStatement(sql)) { | ||
| 122 | + ps.setString(1, user.getName()); | ||
| 123 | + ps.setInt(2, user.getAge()); | ||
| 124 | + ps.setInt(3, user.getId()); | ||
| 125 | + return ps.executeUpdate(); | ||
| 126 | + } | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + /** | ||
| 130 | + * 删除用户 | ||
| 131 | + */ | ||
| 132 | + public int delete(int id) throws SQLException { | ||
| 133 | + String sql = "DELETE FROM `user` WHERE id=?"; | ||
| 134 | + try (Connection conn = dataSource.getConnection(); | ||
| 135 | + PreparedStatement ps = conn.prepareStatement(sql)) { | ||
| 136 | + ps.setInt(1, id); | ||
| 137 | + return ps.executeUpdate(); | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + /** | ||
| 142 | + * 批量插入(批量执行 + 自增主键回填) | ||
| 143 | + */ | ||
| 144 | + public int[] batchInsert(List<User> users) throws SQLException { | ||
| 145 | + String sql = "INSERT INTO `user`(`name`,`age`) VALUES (?,?)"; | ||
| 146 | + try (Connection conn = dataSource.getConnection(); | ||
| 147 | + PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { | ||
| 148 | + for (User user : users) { | ||
| 149 | + ps.setString(1, user.getName()); | ||
| 150 | + ps.setInt(2, user.getAge()); | ||
| 151 | + ps.addBatch(); | ||
| 152 | + } | ||
| 153 | + int[] rows = ps.executeBatch(); | ||
| 154 | + try (ResultSet keys = ps.getGeneratedKeys()) { | ||
| 155 | + int i = 0; | ||
| 156 | + while (keys.next()) { | ||
| 157 | + if (i < users.size()) { | ||
| 158 | + users.get(i).setId(keys.getInt(1)); | ||
| 159 | + } | ||
| 160 | + i++; | ||
| 161 | + } | ||
| 162 | + } | ||
| 163 | + return rows; | ||
| 164 | + } | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + private User toUser(ResultSet rs) throws SQLException { | ||
| 168 | + User user = new User(); | ||
| 169 | + user.setId(rs.getInt("id")); | ||
| 170 | + user.setName(rs.getString("name")); | ||
| 171 | + user.setAge(rs.getInt("age")); | ||
| 172 | + return user; | ||
| 173 | + } | ||
| 174 | +} | ||
| @@ -0,0 +1,84 @@ | |||
| 1 | +package org.opengauss.datasource; | ||
| 2 | + | ||
| 3 | +import org.apache.commons.dbcp2.BasicDataSource; | ||
| 4 | +import org.slf4j.Logger; | ||
| 5 | +import org.slf4j.LoggerFactory; | ||
| 6 | + | ||
| 7 | +import java.io.IOException; | ||
| 8 | +import java.io.InputStream; | ||
| 9 | +import java.util.Properties; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * DBCP 连接池工厂。 | ||
| 13 | + * 支持两种创建方式: | ||
| 14 | + * 1. 编程式:直接调用 { #create(Properties)} 传入配置 | ||
| 15 | + * 2. 配置式:从 classpath 下的 dbcp.properties 加载配置 { #create()} | ||
| 16 | + */ | ||
| 17 | +public class BasicDataSourceFactory { | ||
| 18 | + | ||
| 19 | + private static final Logger log = LoggerFactory.getLogger(BasicDataSourceFactory.class); | ||
| 20 | + | ||
| 21 | + private BasicDataSourceFactory() { | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 从 classpath 加载 dbcp.properties 并创建连接池 | ||
| 26 | + */ | ||
| 27 | + public static BasicDataSource create() { | ||
| 28 | + return create(loadProperties()); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 根据配置创建连接池 | ||
| 33 | + */ | ||
| 34 | + public static BasicDataSource create(Properties props) { | ||
| 35 | + BasicDataSource ds = new BasicDataSource(); | ||
| 36 | + | ||
| 37 | + // ---- 驱动与连接 ---- | ||
| 38 | + ds.setDriverClassName(props.getProperty("driverClassName")); | ||
| 39 | + ds.setUrl(props.getProperty("url")); | ||
| 40 | + ds.setUsername(props.getProperty("username")); | ||
| 41 | + ds.setPassword(props.getProperty("password")); | ||
| 42 | + | ||
| 43 | + // ---- 池容量参数 ---- | ||
| 44 | + ds.setInitialSize(Integer.parseInt(props.getProperty("initialSize", "2"))); | ||
| 45 | + ds.setMaxTotal(Integer.parseInt(props.getProperty("maxTotal", "8"))); | ||
| 46 | + ds.setMaxIdle(Integer.parseInt(props.getProperty("maxIdle", "8"))); | ||
| 47 | + ds.setMinIdle(Integer.parseInt(props.getProperty("minIdle", "2"))); | ||
| 48 | + ds.setMaxWaitMillis(Long.parseLong(props.getProperty("maxWaitMillis", "5000"))); | ||
| 49 | + | ||
| 50 | + // ---- 连接校验 ---- | ||
| 51 | + ds.setValidationQuery(props.getProperty("validationQuery", "SELECT 1")); | ||
| 52 | + ds.setTestOnBorrow(Boolean.parseBoolean(props.getProperty("testOnBorrow", "true"))); | ||
| 53 | + ds.setTestWhileIdle(Boolean.parseBoolean(props.getProperty("testWhileIdle", "true"))); | ||
| 54 | + ds.setTimeBetweenEvictionRunsMillis( | ||
| 55 | + Long.parseLong(props.getProperty("timeBetweenEvictionRunsMillis", "60000"))); | ||
| 56 | + ds.setMinEvictableIdleTimeMillis( | ||
| 57 | + Long.parseLong(props.getProperty("minEvictableIdleTimeMillis", "300000"))); | ||
| 58 | + | ||
| 59 | + // ---- 防连接泄漏 ---- | ||
| 60 | + ds.setRemoveAbandonedOnBorrow(Boolean.parseBoolean(props.getProperty("removeAbandonedOnBorrow", "false"))); | ||
| 61 | + ds.setRemoveAbandonedTimeout(Integer.parseInt(props.getProperty("removeAbandonedTimeout", "300"))); | ||
| 62 | + | ||
| 63 | + log.info("DBCP 连接池配置完成: url={}, initialSize={}, maxTotal={}", | ||
| 64 | + ds.getUrl(), ds.getInitialSize(), ds.getMaxTotal()); | ||
| 65 | + return ds; | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + /** | ||
| 69 | + * 加载 dbcp.properties 配置文件 | ||
| 70 | + */ | ||
| 71 | + private static Properties loadProperties() { | ||
| 72 | + Properties props = new Properties(); | ||
| 73 | + try (InputStream in = BasicDataSourceFactory.class.getClassLoader() | ||
| 74 | + .getResourceAsStream("dbcp.properties")) { | ||
| 75 | + if (in == null) { | ||
| 76 | + throw new IllegalStateException("找不到配置文件 dbcp.properties,请确认它在 classpath 下"); | ||
| 77 | + } | ||
| 78 | + props.load(in); | ||
| 79 | + } catch (IOException e) { | ||
| 80 | + throw new IllegalStateException("加载 dbcp.properties 失败", e); | ||
| 81 | + } | ||
| 82 | + return props; | ||
| 83 | + } | ||
| 84 | +} | ||
| @@ -0,0 +1,51 @@ | |||
| 1 | +package org.opengauss.entity; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * 用户实体类,对应 B 兼容库 mysql_test_db 下的 user 表 | ||
| 5 | + */ | ||
| 6 | +public class User { | ||
| 7 | + | ||
| 8 | + /** 自增主键 */ | ||
| 9 | + private Integer id; | ||
| 10 | + /** 用户名 */ | ||
| 11 | + private String name; | ||
| 12 | + /** 年龄 */ | ||
| 13 | + private Integer age; | ||
| 14 | + | ||
| 15 | + public User() { | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | + public User(String name, Integer age) { | ||
| 19 | + this.name = name; | ||
| 20 | + this.age = age; | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + public Integer getId() { | ||
| 24 | + return id; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + public void setId(Integer id) { | ||
| 28 | + this.id = id; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + public String getName() { | ||
| 32 | + return name; | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + public void setName(String name) { | ||
| 36 | + this.name = name; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + public Integer getAge() { | ||
| 40 | + return age; | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + public void setAge(Integer age) { | ||
| 44 | + this.age = age; | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + public String toString() { | ||
| 49 | + return "User{id=" + id + ", name='" + name + "', age=" + age + "}"; | ||
| 50 | + } | ||
| 51 | +} | ||
| @@ -0,0 +1,21 @@ | |||
| 1 | +spring.application.name=dbcp-connect-opengauss-b | ||
| 2 | + | ||
| 3 | +# Spring Boot 默认会优先选择 HikariCP;pom.xml 已排除 HikariCP,下面再显式指定 DBCP2。 | ||
| 4 | +spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource | ||
| 5 | +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver | ||
| 6 | +spring.datasource.url=${DBCP_URL:jdbc:mysql://127.0.0.1:3308/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL} | ||
| 7 | +spring.datasource.username=${DBCP_USER:opengauss} | ||
| 8 | +spring.datasource.password=${DBCP_PASSWORD:change_me} | ||
| 9 | + | ||
| 10 | +# DBCP2 连接池参数 | ||
| 11 | +spring.datasource.dbcp2.initial-size=2 | ||
| 12 | +spring.datasource.dbcp2.max-total=8 | ||
| 13 | +spring.datasource.dbcp2.max-idle=8 | ||
| 14 | +spring.datasource.dbcp2.min-idle=2 | ||
| 15 | +spring.datasource.dbcp2.max-wait-millis=5000 | ||
| 16 | +spring.datasource.dbcp2.validation-query=SELECT 1 | ||
| 17 | +spring.datasource.dbcp2.test-on-borrow=true | ||
| 18 | +spring.datasource.dbcp2.test-while-idle=true | ||
| 19 | + | ||
| 20 | +# 业务示例由代码显式执行 SQL,不让 Spring Boot 自动执行未审核的脚本。 | ||
| 21 | +spring.sql.init.mode=never | ||
| @@ -0,0 +1,31 @@ | |||
| 1 | +-- ===================================================================== | ||
| 2 | +-- 第 1 步:创建 B 兼容数据库 + 业务 schema + MySQL 协议 native 密码 | ||
| 3 | +-- 用法(在 postgres 默认库中执行,当前用户为超级用户,如 opengauss): | ||
| 4 | +-- gsql -d postgres -h <unix_socket目录> -p 5432 -f create_b_database.sql | ||
| 5 | +-- 执行顺序:本脚本必须在 init.sql 之前执行(先建库,再在库里建表)。 | ||
| 6 | +-- | ||
| 7 | +-- 幂等说明: | ||
| 8 | +-- - 首次在空环境执行:全部语句一次成功(CREATE DATABASE / schema / 授权)。 | ||
| 9 | +-- - 重复执行:仅 CREATE DATABASE 可能报 “already exists”(gsql 不支持 \gexec, | ||
| 10 | +-- 且 CREATE DATABASE 不能放入 DO 块);CREATE SCHEMA 使用 IF NOT EXISTS, | ||
| 11 | +-- 重复执行不报错。重复建表由 init.sql 的 IF NOT EXISTS 与条件插入保证幂等。 | ||
| 12 | +-- ===================================================================== | ||
| 13 | + | ||
| 14 | +-- 1. 创建 B 兼容数据库(DBCOMPATIBILITY 'B',提供 utf8mb4 / MySQL 语义) | ||
| 15 | +CREATE DATABASE proto_test_db WITH DBCOMPATIBILITY = 'B'; | ||
| 16 | + | ||
| 17 | +-- 2. 连接进入 B 兼容库(后续 schema / 密码均在该库上下文执行) | ||
| 18 | +\connect proto_test_db | ||
| 19 | + | ||
| 20 | +-- 3. 设置 MySQL 协议 native 密码。执行前将 <DBCP_PASSWORD> 替换为实际密码, | ||
| 21 | +-- 并与 dbcp.properties 中的 password 保持一致。第三个参数是原 native 密码; | ||
| 22 | +-- 首次设置或重复使用同一密码时可保持如下写法,修改已有密码时应改为当前密码。 | ||
| 23 | +SELECT set_native_password('opengauss', '<DBCP_PASSWORD>', '<DBCP_PASSWORD>'); | ||
| 24 | + | ||
| 25 | +-- 4. 创建业务 schema(MySQL 协议下 schema 等价于 database, | ||
| 26 | +-- jdbc:mysql://<host>:3308/mysql_test_db 中的库名即映射到该 schema) | ||
| 27 | +CREATE SCHEMA IF NOT EXISTS mysql_test_db; | ||
| 28 | + | ||
| 29 | +-- 5. 表权限:schema 与其中表对象默认属于当前超级用户 opengauss; | ||
| 30 | +-- 显式授权给 opengauss,保证 DBCP 连接(opengauss 账号)可读写 | ||
| 31 | +GRANT ALL ON SCHEMA mysql_test_db TO opengauss; | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | +# ============ 驱动与连接 ============ | ||
| 2 | +# MySQL JDBC 驱动(本示例固定使用已验证的 8.0.28) | ||
| 3 | +driverClassName=com.mysql.cj.jdbc.Driver | ||
| 4 | +# dolphin 协议监听端口 + B 兼容库下的 schema 名 | ||
| 5 | +# 本地示例的 Dolphin MySQL 协议端口未启用 SSL,因此设置 useSSL=false | ||
| 6 | +url=jdbc:mysql://127.0.0.1:3308/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL | ||
| 7 | +username=opengauss | ||
| 8 | +password=<DBCP_PASSWORD> | ||
| 9 | + | ||
| 10 | +# ============ 池容量参数 ============ | ||
| 11 | +initialSize=2 | ||
| 12 | +maxTotal=8 | ||
| 13 | +maxIdle=8 | ||
| 14 | +minIdle=2 | ||
| 15 | +maxWaitMillis=5000 | ||
| 16 | + | ||
| 17 | +# ============ 连接校验 ============ | ||
| 18 | +validationQuery=SELECT 1 | ||
| 19 | +testOnBorrow=true | ||
| 20 | +testWhileIdle=true | ||
| 21 | +timeBetweenEvictionRunsMillis=60000 | ||
| 22 | +minEvictableIdleTimeMillis=300000 | ||
| 23 | + | ||
| 24 | +# ============ 防连接泄漏 ============ | ||
| 25 | +removeAbandonedOnBorrow=false | ||
| 26 | +removeAbandonedTimeout=300 | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | +-- ===================================================================== | ||
| 2 | +-- 第 2 步:在 B 兼容库 proto_test_db 中创建业务表并初始化数据 | ||
| 3 | +-- 用法(需先执行 create_b_database.sql,再连接进入 proto_test_db): | ||
| 4 | +-- gsql -d proto_test_db -h <unix_socket目录> -p 5432 -f init.sql | ||
| 5 | +-- | ||
| 6 | +-- 幂等:建表使用 IF NOT EXISTS,初始数据按姓名去重,可重复执行。 | ||
| 7 | +-- ===================================================================== | ||
| 8 | + | ||
| 9 | +-- 1. 切换业务 schema(MySQL 协议下 schema 等价于 database, | ||
| 10 | +-- 与 jdbc:mysql://<host>:3308/mysql_test_db 的 URL 库名一致) | ||
| 11 | +SET current_schema TO mysql_test_db; | ||
| 12 | + | ||
| 13 | +-- 2. 创建业务表(与官网 Mybatis 示例保持一致,便于横向对比) | ||
| 14 | +CREATE TABLE IF NOT EXISTS `user` ( | ||
| 15 | + `id` INT AUTO_INCREMENT PRIMARY KEY, | ||
| 16 | + `name` VARCHAR(50) NOT NULL COMMENT '用户名', | ||
| 17 | + `age` INT COMMENT '年龄' | ||
| 18 | +) DEFAULT CHARSET=utf8mb4; | ||
| 19 | + | ||
| 20 | +-- 3. 初始化数据(幂等:仅当姓名不存在时插入) | ||
| 21 | +INSERT INTO `user` (`name`,`age`) | ||
| 22 | +SELECT '张三', 18 WHERE NOT EXISTS (SELECT 1 FROM `user` WHERE `name` = '张三'); | ||
| 23 | +INSERT INTO `user` (`name`,`age`) | ||
| 24 | +SELECT '李四', 19 WHERE NOT EXISTS (SELECT 1 FROM `user` WHERE `name` = '李四'); | ||
| 25 | +INSERT INTO `user` (`name`,`age`) | ||
| 26 | +SELECT '王五', 20 WHERE NOT EXISTS (SELECT 1 FROM `user` WHERE `name` = '王五'); | ||
| @@ -0,0 +1,229 @@ | |||
| 1 | +package org.opengauss.test; | ||
| 2 | + | ||
| 3 | +import org.apache.commons.dbcp2.BasicDataSource; | ||
| 4 | +import org.opengauss.datasource.BasicDataSourceFactory; | ||
| 5 | +import org.opengauss.test.util.TestConfig; | ||
| 6 | +import org.junit.AfterClass; | ||
| 7 | +import org.junit.BeforeClass; | ||
| 8 | + | ||
| 9 | +import java.sql.Connection; | ||
| 10 | +import java.sql.ResultSet; | ||
| 11 | +import java.sql.SQLException; | ||
| 12 | +import java.sql.Statement; | ||
| 13 | +import java.util.Properties; | ||
| 14 | +import java.util.concurrent.ExecutorService; | ||
| 15 | +import java.util.concurrent.TimeUnit; | ||
| 16 | + | ||
| 17 | +import static org.junit.Assert.assertTrue; | ||
| 18 | +import static org.junit.Assert.fail; | ||
| 19 | + | ||
| 20 | +/** | ||
| 21 | + * 测试基类: | ||
| 22 | + * 1. 统一创建 / 关闭连接池(每个测试类各自建池,互不影响) | ||
| 23 | + * 2. 提供把连接参数应用到临时 BasicDataSource 的工具方法 | ||
| 24 | + * 3. 提供数据库连通性预检(失败时给出明确的排查提示) | ||
| 25 | + * 4. 提供测试共用的连接池/物理连接/会话断言工具方法 | ||
| 26 | + */ | ||
| 27 | +public abstract class BaseDbTest { | ||
| 28 | + | ||
| 29 | + protected static BasicDataSource dataSource; | ||
| 30 | + | ||
| 31 | + | ||
| 32 | + public static void initPool() throws Exception { | ||
| 33 | + closeIfNeeded(); | ||
| 34 | + dataSource = BasicDataSourceFactory.create(TestConfig.load()); | ||
| 35 | + // 连通性预检:能借出连接并执行 SELECT 1 才继续,否则给出明确提示 | ||
| 36 | + try (Connection conn = dataSource.getConnection(); | ||
| 37 | + Statement st = conn.createStatement()) { | ||
| 38 | + st.execute("SELECT 1"); | ||
| 39 | + } catch (Exception e) { | ||
| 40 | + closeIfNeeded(); | ||
| 41 | + throw new IllegalStateException( | ||
| 42 | + "数据库不可达,请检查:① openGauss 实例是否已启动并开启 enable_dolphin_proto;" | ||
| 43 | + + "② dolphin_server_port 与 dbcp.properties 中 url 端口是否一致;" | ||
| 44 | + + "③ 用户/密码/权限是否正确。原始异常: " + e.getMessage(), e); | ||
| 45 | + } | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + | ||
| 49 | + public static void closePool() throws Exception { | ||
| 50 | + closeIfNeeded(); | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + private static void closeIfNeeded() { | ||
| 54 | + if (dataSource != null) { | ||
| 55 | + try { | ||
| 56 | + dataSource.close(); | ||
| 57 | + } catch (SQLException ignored) { | ||
| 58 | + // 关闭失败不影响用例结果 | ||
| 59 | + } | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 把 dbcp.properties(含系统属性覆盖)的连接参数应用到临时连接池 | ||
| 65 | + */ | ||
| 66 | + protected static void applyUrl(BasicDataSource ds) { | ||
| 67 | + Properties p = TestConfig.load(); | ||
| 68 | + ds.setDriverClassName(p.getProperty("driverClassName")); | ||
| 69 | + ds.setUrl(p.getProperty("url")); | ||
| 70 | + ds.setUsername(p.getProperty("username")); | ||
| 71 | + ds.setPassword(p.getProperty("password")); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + /** | ||
| 75 | + * 新建一个仅带连接参数(驱动/URL/账号/密码)的测试专用连接池。 | ||
| 76 | + * 其余池参数由各用例显式设置,避免依赖 dbcp.properties 的全局取值。 | ||
| 77 | + */ | ||
| 78 | + protected static BasicDataSource newPool() { | ||
| 79 | + BasicDataSource ds = new BasicDataSource(); | ||
| 80 | + applyUrl(ds); | ||
| 81 | + return ds; | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 获取当前 MySQL 会话的服务端会话标识(SELECT CONNECTION_ID())。 | ||
| 86 | + * 注意:dolphin 会在会话结束后复用该数值,故只用于“同一会话复用”判断, | ||
| 87 | + * 不能用于区分“重建后的新会话”是否不同。 | ||
| 88 | + */ | ||
| 89 | + protected static long connectionId(Connection conn) throws SQLException { | ||
| 90 | + try (Statement st = conn.createStatement(); | ||
| 91 | + ResultSet rs = st.executeQuery("SELECT CONNECTION_ID()")) { | ||
| 92 | + if (!rs.next()) { | ||
| 93 | + throw new SQLException("SELECT CONNECTION_ID() 无返回结果"); | ||
| 94 | + } | ||
| 95 | + return rs.getLong(1); | ||
| 96 | + } | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + /** | ||
| 100 | + * 获取被 DBCP 包装的真实 MySQL 物理连接。 | ||
| 101 | + * 不使用 unwrap(Connection.class)——它只会返回 DBCP 包装对象本身。 | ||
| 102 | + */ | ||
| 103 | + protected static com.mysql.cj.jdbc.ConnectionImpl physicalConnection(Connection conn) throws SQLException { | ||
| 104 | + com.mysql.cj.jdbc.ConnectionImpl phys = conn.unwrap(com.mysql.cj.jdbc.ConnectionImpl.class); | ||
| 105 | + if (phys == null) { | ||
| 106 | + throw new SQLException("无法取得真实 MySQL 物理连接(unwrap(ConnectionImpl.class) 返回 null)"); | ||
| 107 | + } | ||
| 108 | + return phys; | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + /** | ||
| 112 | + * 服务端断连:从独立管理连接(共享池 dataSource 的另一个会话)执行 KILL <CONNECTION_ID>。 | ||
| 113 | + * 不调用被测连接的 ConnectionImpl.close(),被测 JDBC/物理连接本地仍报告 isClosed()==false, | ||
| 114 | + * 从而证明是服务端终止会话,而非客户端关闭。 | ||
| 115 | + * 说明:dolphin 的 MySQL 协议实测支持 KILL(本环境已验证:执行后服务端会话从 | ||
| 116 | + * SHOW PROCESSLIST 消失,客户端 isClosed() 仍为 false,下次查询才报断连)。 | ||
| 117 | + */ | ||
| 118 | + protected static void killServerSession(long id) throws SQLException { | ||
| 119 | + try (Connection mgmt = dataSource.getConnection(); | ||
| 120 | + Statement st = mgmt.createStatement()) { | ||
| 121 | + st.execute("KILL " + id); | ||
| 122 | + } | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + /** | ||
| 126 | + * 通过 SHOW PROCESSLIST 确认某个服务端会话是否仍存在(用于核验服务端断连是否真正生效)。 | ||
| 127 | + */ | ||
| 128 | + protected static boolean serverSessionExists(long id) throws SQLException { | ||
| 129 | + try (Connection c = dataSource.getConnection(); | ||
| 130 | + Statement st = c.createStatement(); | ||
| 131 | + ResultSet rs = st.executeQuery("SHOW PROCESSLIST")) { | ||
| 132 | + while (rs.next()) { | ||
| 133 | + if (rs.getLong(1) == id) { | ||
| 134 | + return true; | ||
| 135 | + } | ||
| 136 | + } | ||
| 137 | + } | ||
| 138 | + return false; | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + /** | ||
| 142 | + * 停止并等待线程池终止(带超时):shutdown 后 awaitTermination 指定时长, | ||
| 143 | + * 超时仍未终止则 shutdownNow 再补等一次;仍无法终止视为测试失败。 | ||
| 144 | + * 保证所有并发用例的线程终止路径都有明确超时,不会无限挂起。 | ||
| 145 | + */ | ||
| 146 | + protected static void shutdownExecutor(ExecutorService pool, long timeoutMillis, String message) | ||
| 147 | + throws InterruptedException { | ||
| 148 | + if (pool == null) { | ||
| 149 | + return; | ||
| 150 | + } | ||
| 151 | + pool.shutdown(); | ||
| 152 | + if (!pool.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS)) { | ||
| 153 | + pool.shutdownNow(); | ||
| 154 | + if (!pool.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS)) { | ||
| 155 | + fail(message + "(线程池在超时内未终止)"); | ||
| 156 | + } | ||
| 157 | + } | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + /** | ||
| 161 | + * 先带超时停止线程池,随后无论线程池终止是否失败都关闭连接池; | ||
| 162 | + * 线程池终止失败/被中断不会阻止 ds.close(),其失败会在连接池关闭后重新抛出。 | ||
| 163 | + */ | ||
| 164 | + protected static void shutdownExecutorAndClosePool(ExecutorService pool, BasicDataSource ds, | ||
| 165 | + long timeoutMillis, String message) { | ||
| 166 | + Throwable failure = null; | ||
| 167 | + try { | ||
| 168 | + shutdownExecutor(pool, timeoutMillis, message); | ||
| 169 | + } catch (Throwable t) { | ||
| 170 | + failure = t; | ||
| 171 | + } | ||
| 172 | + closeDataSourceQuietly(ds); | ||
| 173 | + if (failure != null) { | ||
| 174 | + if (failure instanceof InterruptedException) { | ||
| 175 | + Thread.currentThread().interrupt(); | ||
| 176 | + } | ||
| 177 | + if (failure instanceof RuntimeException) { | ||
| 178 | + throw (RuntimeException) failure; | ||
| 179 | + } | ||
| 180 | + if (failure instanceof Error) { | ||
| 181 | + throw (Error) failure; | ||
| 182 | + } | ||
| 183 | + throw new RuntimeException(failure); | ||
| 184 | + } | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + /** | ||
| 188 | + * 关闭测试专用连接池(忽略关闭异常,确保 finally 中不因关闭失败掩盖原断言失败)。 | ||
| 189 | + */ | ||
| 190 | + protected static void closeDataSourceQuietly(BasicDataSource ds) { | ||
| 191 | + if (ds != null) { | ||
| 192 | + try { | ||
| 193 | + ds.close(); | ||
| 194 | + } catch (SQLException ignored) { | ||
| 195 | + } | ||
| 196 | + } | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + /** | ||
| 200 | + * 关闭单个连接(忽略关闭异常)。finally 中逐个调用:某个连接关闭失败不影响其余连接与连接池关闭。 | ||
| 201 | + */ | ||
| 202 | + protected static void closeConnectionQuietly(Connection c) { | ||
| 203 | + if (c != null) { | ||
| 204 | + try { | ||
| 205 | + c.close(); | ||
| 206 | + } catch (SQLException ignored) { | ||
| 207 | + } | ||
| 208 | + } | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + /** | ||
| 212 | + * 轮询等待条件成立(带超时,避免测试无限阻塞)。 | ||
| 213 | + */ | ||
| 214 | + protected static void waitForCondition(Condition cond, long timeoutMillis, String message) throws InterruptedException { | ||
| 215 | + long deadline = System.currentTimeMillis() + timeoutMillis; | ||
| 216 | + while (System.currentTimeMillis() < deadline) { | ||
| 217 | + if (cond.eval()) { | ||
| 218 | + return; | ||
| 219 | + } | ||
| 220 | + Thread.sleep(200); | ||
| 221 | + } | ||
| 222 | + fail(message); | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + | ||
| 226 | + protected interface Condition { | ||
| 227 | + boolean eval(); | ||
| 228 | + } | ||
| 229 | +} | ||
| @@ -0,0 +1,498 @@ | |||
| 1 | +package org.opengauss.test; | ||
| 2 | + | ||
| 3 | +import org.apache.commons.dbcp2.BasicDataSource; | ||
| 4 | +import org.junit.Test; | ||
| 5 | + | ||
| 6 | +import java.sql.Connection; | ||
| 7 | +import java.sql.PreparedStatement; | ||
| 8 | +import java.sql.ResultSet; | ||
| 9 | +import java.sql.SQLException; | ||
| 10 | +import java.sql.Statement; | ||
| 11 | +import java.util.ArrayList; | ||
| 12 | +import java.util.List; | ||
| 13 | +import java.util.NoSuchElementException; | ||
| 14 | +import java.util.concurrent.CountDownLatch; | ||
| 15 | +import java.util.concurrent.ExecutorService; | ||
| 16 | +import java.util.concurrent.Executors; | ||
| 17 | +import java.util.concurrent.Future; | ||
| 18 | +import java.util.concurrent.TimeUnit; | ||
| 19 | + | ||
| 20 | +import static org.junit.Assert.assertEquals; | ||
| 21 | +import static org.junit.Assert.assertFalse; | ||
| 22 | +import static org.junit.Assert.assertTrue; | ||
| 23 | +import static org.junit.Assert.fail; | ||
| 24 | + | ||
| 25 | +/** | ||
| 26 | + * 连接池核心行为测试: | ||
| 27 | + * 并发借出(两阶段屏障 + 全部同时持有)、maxTotal/maxWaitMillis 上下界与池耗尽异常、 | ||
| 28 | + * maxIdle 对空闲连接数上限(精确收敛)、minIdle 由维护线程补足、归还后物理会话复用、 | ||
| 29 | + * 连接状态复位(autoCommit/隔离级别)、removeAbandoned 泄漏回收、按空闲时长驱逐。 | ||
| 30 | + */ | ||
| 31 | +public class ConnectionPoolTest extends BaseDbTest { | ||
| 32 | + | ||
| 33 | + | ||
| 34 | + public void testBorrowAndReturn() throws Exception { | ||
| 35 | + try (Connection conn = dataSource.getConnection()) { | ||
| 36 | + assertFalse(conn.isClosed()); | ||
| 37 | + assertEquals(1, dataSource.getNumActive()); | ||
| 38 | + } | ||
| 39 | + assertEquals("归还后 active 应为 0", 0, dataSource.getNumActive()); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + /** | ||
| 43 | + * 并发借出:使用测试专用连接池 + 两阶段屏障。 | ||
| 44 | + * 所有线程先各自借到连接并一直持有,主线程确认 active 达到线程数(同时持有的峰值), | ||
| 45 | + * 再统一放行执行查询。所有 await/Future 均带超时。 | ||
| 46 | + */ | ||
| 47 | + | ||
| 48 | + public void testConcurrentBorrowAllThreadsHoldSimultaneously() throws Exception { | ||
| 49 | + int threads = 6; | ||
| 50 | + BasicDataSource ds = newPool(); | ||
| 51 | + ds.setMaxTotal(threads); | ||
| 52 | + ds.setMaxWaitMillis(5000); | ||
| 53 | + ExecutorService pool = Executors.newFixedThreadPool(threads); | ||
| 54 | + try { | ||
| 55 | + CountDownLatch held = new CountDownLatch(threads); // 阶段1:所有线程已借到连接 | ||
| 56 | + CountDownLatch release = new CountDownLatch(1); // 阶段2:放行执行查询 | ||
| 57 | + CountDownLatch done = new CountDownLatch(threads); | ||
| 58 | + List<Future<Boolean>> results = new ArrayList<>(); | ||
| 59 | + for (int i = 0; i < threads; i++) { | ||
| 60 | + results.add(pool.submit(() -> { | ||
| 61 | + try (Connection conn = ds.getConnection()) { | ||
| 62 | + held.countDown(); | ||
| 63 | + if (!release.await(15, TimeUnit.SECONDS)) { | ||
| 64 | + return false; | ||
| 65 | + } | ||
| 66 | + try (Statement st = conn.createStatement(); | ||
| 67 | + ResultSet rs = st.executeQuery("SELECT 1")) { | ||
| 68 | + return rs.next() && rs.getInt(1) == 1; | ||
| 69 | + } | ||
| 70 | + } finally { | ||
| 71 | + done.countDown(); | ||
| 72 | + } | ||
| 73 | + })); | ||
| 74 | + } | ||
| 75 | + assertTrue("所有线程应在 15s 内借到连接", held.await(15, TimeUnit.SECONDS)); | ||
| 76 | + | ||
| 77 | + // 所有线程此刻都持有连接且阻塞在屏障上,active 应正好等于线程数(即并发峰值) | ||
| 78 | + int peakActive = 0; | ||
| 79 | + for (int i = 0; i < 50; i++) { | ||
| 80 | + peakActive = Math.max(peakActive, ds.getNumActive()); | ||
| 81 | + if (ds.getNumActive() >= threads) { | ||
| 82 | + break; | ||
| 83 | + } | ||
| 84 | + Thread.sleep(20); | ||
| 85 | + } | ||
| 86 | + assertEquals("所有线程同时持有连接时 peak active 应等于线程数", threads, peakActive); | ||
| 87 | + assertEquals("屏障阶段 active 应等于线程数(全部同时持有)", threads, ds.getNumActive()); | ||
| 88 | + | ||
| 89 | + release.countDown(); | ||
| 90 | + assertTrue("所有线程应在 15s 内完成查询并归还", done.await(15, TimeUnit.SECONDS)); | ||
| 91 | + for (Future<Boolean> f : results) { | ||
| 92 | + assertTrue("并发借出执行失败", f.get(5, TimeUnit.SECONDS)); | ||
| 93 | + } | ||
| 94 | + assertEquals("全部归还后 active 应为 0", 0, ds.getNumActive()); | ||
| 95 | + } finally { | ||
| 96 | + shutdownExecutorAndClosePool(pool, ds, 15000, "并发借出线程未在超时内终止"); | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + /** | ||
| 101 | + * maxTotal/maxWaitMillis:验证等待时长上下界与明确的池耗尽异常。 | ||
| 102 | + * 若第二次借出意外成功,必须先关闭该连接再 fail;连接池在 finally 中可靠关闭。 | ||
| 103 | + */ | ||
| 104 | + | ||
| 105 | + public void testMaxTotalAndMaxWaitTimeout() throws Exception { | ||
| 106 | + BasicDataSource ds = newPool(); | ||
| 107 | + ds.setMaxTotal(1); | ||
| 108 | + ds.setMaxWaitMillis(2000); | ||
| 109 | + Connection c2 = null; | ||
| 110 | + try { | ||
| 111 | + try (Connection c1 = ds.getConnection()) { | ||
| 112 | + long begin = System.currentTimeMillis(); | ||
| 113 | + try { | ||
| 114 | + c2 = ds.getConnection(); | ||
| 115 | + fail("超过 maxTotal 且超过 maxWaitMillis 应抛出 SQLException"); | ||
| 116 | + } catch (SQLException e) { | ||
| 117 | + long cost = System.currentTimeMillis() - begin; | ||
| 118 | + assertTrue("等待时长应接近 maxWaitMillis(2000ms),实际: " + cost + "ms", cost >= 1500); | ||
| 119 | + assertTrue("等待时长不应远超 maxWaitMillis(2000ms),实际: " + cost + "ms", cost <= 5000); | ||
| 120 | + assertTrue("应给出明确的池耗尽异常,实际: " + e.getMessage(), | ||
| 121 | + e.getMessage() != null | ||
| 122 | + && e.getMessage().contains("Timeout waiting for idle object")); | ||
| 123 | + assertTrue("池耗尽异常应链式包含 NoSuchElementException,实际 cause: " | ||
| 124 | + + (e.getCause() == null ? null : e.getCause()), | ||
| 125 | + e.getCause() instanceof NoSuchElementException | ||
| 126 | + && e.getCause().getMessage() != null | ||
| 127 | + && e.getCause().getMessage().contains("Timeout waiting for idle object")); | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + } finally { | ||
| 131 | + closeConnectionQuietly(c2); | ||
| 132 | + closeDataSourceQuietly(ds); | ||
| 133 | + } | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + /** | ||
| 137 | + * 满池真实业务行为:8 条连接分别在事务中完成 CRUD 后继续持有; | ||
| 138 | + * 第 9 条请求先在无释放时超时,再验证释放任一连接后的恢复执行。 | ||
| 139 | + */ | ||
| 140 | + | ||
| 141 | + public void testMaxTotalConcurrentCrudAndQueuedBorrow() throws Exception { | ||
| 142 | + final int maxTotal = 8; | ||
| 143 | + final long waitMillis = 5000; | ||
| 144 | + final long workerHoldMillis = 15000; | ||
| 145 | + BasicDataSource ds = newPool(); | ||
| 146 | + ds.setInitialSize(0); | ||
| 147 | + ds.setMinIdle(0); | ||
| 148 | + ds.setMaxTotal(maxTotal); | ||
| 149 | + ds.setMaxWaitMillis(waitMillis); | ||
| 150 | + | ||
| 151 | + ExecutorService pool = Executors.newFixedThreadPool(maxTotal + 1); | ||
| 152 | + CountDownLatch workersHoldingConnections = new CountDownLatch(maxTotal); | ||
| 153 | + CountDownLatch releaseWorkers = new CountDownLatch(1); | ||
| 154 | + List<Future<Boolean>> workers = new ArrayList<>(); | ||
| 155 | + String runId = "pool_max_" + System.nanoTime(); | ||
| 156 | + try { | ||
| 157 | + for (int i = 0; i < maxTotal; i++) { | ||
| 158 | + final String name = runId + "_" + i; | ||
| 159 | + workers.add(pool.submit(() -> { | ||
| 160 | + boolean completedCrud = false; | ||
| 161 | + try (Connection conn = ds.getConnection()) { | ||
| 162 | + executeCrudInTransaction(conn, name, 20); | ||
| 163 | + completedCrud = true; | ||
| 164 | + workersHoldingConnections.countDown(); | ||
| 165 | + return releaseWorkers.await(workerHoldMillis, TimeUnit.MILLISECONDS); | ||
| 166 | + } finally { | ||
| 167 | + // 失败路径同样唤醒主线程,后续 Future.get 会保留原始失败原因。 | ||
| 168 | + if (!completedCrud) { | ||
| 169 | + workersHoldingConnections.countDown(); | ||
| 170 | + } | ||
| 171 | + } | ||
| 172 | + })); | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + assertTrue("所有 worker 应在 20s 内完成事务并保持借出连接", | ||
| 176 | + workersHoldingConnections.await(20, TimeUnit.SECONDS)); | ||
| 177 | + assertEquals("8 条连接均应处于 active,不能只验证建连成功", | ||
| 178 | + maxTotal, ds.getNumActive()); | ||
| 179 | + | ||
| 180 | + Future<Boolean> timedOutOverflow = pool.submit(() -> { | ||
| 181 | + try (Connection ignored = ds.getConnection()) { | ||
| 182 | + return false; | ||
| 183 | + } catch (SQLException e) { | ||
| 184 | + return e.getMessage() != null | ||
| 185 | + && e.getMessage().contains("Timeout waiting for idle object"); | ||
| 186 | + } | ||
| 187 | + }); | ||
| 188 | + assertTrue("8 条连接均不释放时,第 9 条请求应在 maxWaitMillis 后超时", | ||
| 189 | + timedOutOverflow.get(waitMillis + 3000, TimeUnit.MILLISECONDS)); | ||
| 190 | + assertEquals("第 9 条请求超时后,8 条 worker 仍应处于 active", | ||
| 191 | + maxTotal, ds.getNumActive()); | ||
| 192 | + | ||
| 193 | + CountDownLatch overflowRequested = new CountDownLatch(1); | ||
| 194 | + long overflowBegin = System.nanoTime(); | ||
| 195 | + Future<Boolean> overflow = pool.submit(() -> { | ||
| 196 | + overflowRequested.countDown(); | ||
| 197 | + try (Connection conn = ds.getConnection(); | ||
| 198 | + Statement st = conn.createStatement(); | ||
| 199 | + ResultSet rs = st.executeQuery("SELECT 1")) { | ||
| 200 | + return rs.next() && rs.getInt(1) == 1; | ||
| 201 | + } | ||
| 202 | + }); | ||
| 203 | + | ||
| 204 | + assertTrue("第 9 条连接请求未发起", overflowRequested.await(2, TimeUnit.SECONDS)); | ||
| 205 | + Thread.sleep(300); | ||
| 206 | + assertFalse("达到 maxTotal 后,第 9 条请求应等待空闲连接而不能立即成功", overflow.isDone()); | ||
| 207 | + assertEquals("排队期间 active 不得超过 maxTotal", maxTotal, ds.getNumActive()); | ||
| 208 | + | ||
| 209 | + releaseWorkers.countDown(); | ||
| 210 | + for (Future<Boolean> worker : workers) { | ||
| 211 | + assertTrue("worker 未在满池释放后正常结束", worker.get(10, TimeUnit.SECONDS)); | ||
| 212 | + } | ||
| 213 | + assertTrue("释放连接后,第 9 条请求应恢复并执行 SQL", overflow.get(10, TimeUnit.SECONDS)); | ||
| 214 | + long overflowCost = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - overflowBegin); | ||
| 215 | + assertTrue("第 9 条请求应实际经历排队等待,耗时: " + overflowCost + "ms", | ||
| 216 | + overflowCost >= 250); | ||
| 217 | + assertEquals("全部归还后不应遗留 active 连接", 0, ds.getNumActive()); | ||
| 218 | + } finally { | ||
| 219 | + releaseWorkers.countDown(); | ||
| 220 | + shutdownExecutorAndClosePool(pool, ds, 15000, "满池并发 CRUD 线程未在超时内终止"); | ||
| 221 | + } | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + /** | ||
| 225 | + * maxIdle:并发创建 maxTotal 个物理连接,全部归还后空闲连接数应精确收敛到 maxIdle。 | ||
| 226 | + * 采用 held/release 两阶段屏障:工作线程内部 try-with-resources 自行归还, | ||
| 227 | + * 主线程确认 8 个连接同时 active 后统一放行;Future/await 全部带超时, | ||
| 228 | + * 任何失败路径都由 finally 停止线程池并关闭 ds,不会丢失借出的连接。 | ||
| 229 | + */ | ||
| 230 | + | ||
| 231 | + public void testMaxIdleLimitsIdleConnections() throws Exception { | ||
| 232 | + int connCount = 8; | ||
| 233 | + int maxIdle = 2; | ||
| 234 | + BasicDataSource ds = newPool(); | ||
| 235 | + ds.setMaxTotal(connCount); | ||
| 236 | + ds.setMaxIdle(maxIdle); | ||
| 237 | + ds.setMaxWaitMillis(10000); | ||
| 238 | + ExecutorService pool = Executors.newFixedThreadPool(connCount); | ||
| 239 | + try { | ||
| 240 | + CountDownLatch held = new CountDownLatch(connCount); // 阶段1:全部线程已借到连接 | ||
| 241 | + CountDownLatch release = new CountDownLatch(1); // 阶段2:放行归还 | ||
| 242 | + CountDownLatch done = new CountDownLatch(connCount); | ||
| 243 | + List<Future<Boolean>> results = new ArrayList<>(); | ||
| 244 | + for (int i = 0; i < connCount; i++) { | ||
| 245 | + results.add(pool.submit(() -> { | ||
| 246 | + try (Connection c = ds.getConnection()) { | ||
| 247 | + held.countDown(); | ||
| 248 | + if (!release.await(15, TimeUnit.SECONDS)) { | ||
| 249 | + return false; | ||
| 250 | + } | ||
| 251 | + return true; | ||
| 252 | + } finally { | ||
| 253 | + done.countDown(); | ||
| 254 | + } | ||
| 255 | + })); | ||
| 256 | + } | ||
| 257 | + assertTrue("8 个线程应在 15s 内全部借到连接", held.await(15, TimeUnit.SECONDS)); | ||
| 258 | + assertEquals("8 个连接应同时 active(全部持有)", connCount, ds.getNumActive()); | ||
| 259 | + | ||
| 260 | + release.countDown(); | ||
| 261 | + assertTrue("8 个线程应在 15s 内全部归还", done.await(15, TimeUnit.SECONDS)); | ||
| 262 | + for (Future<Boolean> f : results) { | ||
| 263 | + assertTrue("借出/归还执行失败", f.get(10, TimeUnit.SECONDS)); | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + // 全部归还后:active 必须为 0(无泄漏),空闲数必须精确收敛到 maxIdle=2 | ||
| 267 | + waitForCondition(() -> ds.getNumIdle() == maxIdle, 10000, | ||
| 268 | + "全部归还后空闲连接数应精确收敛到 maxIdle=" + maxIdle); | ||
| 269 | + assertEquals("全部归还后 active 应为 0", 0, ds.getNumActive()); | ||
| 270 | + assertEquals("归还后空闲连接数应恰好等于 maxIdle,实际: " + ds.getNumIdle(), | ||
| 271 | + maxIdle, ds.getNumIdle()); | ||
| 272 | + } finally { | ||
| 273 | + shutdownExecutorAndClosePool(pool, ds, 15000, "maxIdle 借出线程未在超时内终止"); | ||
| 274 | + } | ||
| 275 | + } | ||
| 276 | + | ||
| 277 | + /** | ||
| 278 | + * minIdle:启动维护线程,轮询断言空闲连接被补足到 minIdle; | ||
| 279 | + * 且在被借走期间维护线程仍继续补足。 | ||
| 280 | + */ | ||
| 281 | + | ||
| 282 | + public void testMinIdleReplenishedByMaintenanceThread() throws Exception { | ||
| 283 | + int minIdle = 2; | ||
| 284 | + BasicDataSource ds = newPool(); | ||
| 285 | + ds.setInitialSize(0); | ||
| 286 | + ds.setMinIdle(minIdle); | ||
| 287 | + ds.setMaxIdle(5); | ||
| 288 | + ds.setMaxTotal(5); | ||
| 289 | + ds.setTimeBetweenEvictionRunsMillis(300); | ||
| 290 | + Connection a = null; | ||
| 291 | + Connection b = null; | ||
| 292 | + try { | ||
| 293 | + try (Connection c = ds.getConnection()) { | ||
| 294 | + // 首次借出触发建池 | ||
| 295 | + } | ||
| 296 | + waitForCondition(() -> ds.getNumIdle() >= minIdle, 15000, | ||
| 297 | + "维护线程应在池建成后把空闲连接补足到 minIdle=" + minIdle); | ||
| 298 | + assertEquals("空闲补足期间不应有借出的连接", 0, ds.getNumActive()); | ||
| 299 | + assertTrue("minIdle 补足后空闲连接数应 >= " + minIdle + ",实际: " + ds.getNumIdle(), | ||
| 300 | + ds.getNumIdle() >= minIdle); | ||
| 301 | + | ||
| 302 | + // 借走全部空闲连接后,维护线程仍应把空闲补足回 minIdle(active 保持被借出状态) | ||
| 303 | + a = ds.getConnection(); | ||
| 304 | + b = ds.getConnection(); | ||
| 305 | + assertEquals("借走 2 个空闲连接后 idle 应为 0", 0, ds.getNumIdle()); | ||
| 306 | + waitForCondition(() -> ds.getNumIdle() >= minIdle, 15000, | ||
| 307 | + "持有连接期间维护线程仍应补足空闲连接到 minIdle"); | ||
| 308 | + assertEquals("补足期间被借出的连接不应被释放", 2, ds.getNumActive()); | ||
| 309 | + } finally { | ||
| 310 | + if (a != null) { | ||
| 311 | + try { | ||
| 312 | + a.close(); | ||
| 313 | + } catch (SQLException ignored) { | ||
| 314 | + } | ||
| 315 | + } | ||
| 316 | + if (b != null) { | ||
| 317 | + try { | ||
| 318 | + b.close(); | ||
| 319 | + } catch (SQLException ignored) { | ||
| 320 | + } | ||
| 321 | + } | ||
| 322 | + closeDataSourceQuietly(ds); | ||
| 323 | + } | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + /** | ||
| 327 | + * 连接归还复用:maxTotal=1,close 归还后重新借出应复用同一物理连接(同一会话、同一对象)。 | ||
| 328 | + */ | ||
| 329 | + | ||
| 330 | + public void testReturnAndReborrowReusesPhysicalSession() throws Exception { | ||
| 331 | + BasicDataSource ds = newPool(); | ||
| 332 | + ds.setMaxTotal(1); | ||
| 333 | + ds.setMaxWaitMillis(3000); | ||
| 334 | + try { | ||
| 335 | + com.mysql.cj.jdbc.ConnectionImpl phys1; | ||
| 336 | + long id1; | ||
| 337 | + try (Connection c = ds.getConnection()) { | ||
| 338 | + phys1 = physicalConnection(c); | ||
| 339 | + id1 = connectionId(c); | ||
| 340 | + } | ||
| 341 | + com.mysql.cj.jdbc.ConnectionImpl phys2; | ||
| 342 | + long id2; | ||
| 343 | + try (Connection c = ds.getConnection()) { | ||
| 344 | + phys2 = physicalConnection(c); | ||
| 345 | + id2 = connectionId(c); | ||
| 346 | + } | ||
| 347 | + assertTrue("close 归还后重新借出应复用同一物理连接对象", phys1 == phys2); | ||
| 348 | + assertEquals("close 归还后重新借出应复用同一服务端会话(CONNECTION_ID 相同)", id1, id2); | ||
| 349 | + } finally { | ||
| 350 | + closeDataSourceQuietly(ds); | ||
| 351 | + } | ||
| 352 | + } | ||
| 353 | + | ||
| 354 | + /** | ||
| 355 | + * 连接状态复位:归还前修改 autoCommit/isolation,重新借出后应恢复默认值。 | ||
| 356 | + * 注:dolphin 的 MySQL 协议实测不支持 readOnly(setReadOnly(true) 后 isReadOnly() | ||
| 357 | + * 仍为 false),故本用例只断言 autoCommit 与隔离级别的复位,不声明 readOnly 复位。 | ||
| 358 | + */ | ||
| 359 | + | ||
| 360 | + public void testStateResetOnReborrow() throws Exception { | ||
| 361 | + BasicDataSource ds = newPool(); | ||
| 362 | + ds.setMaxTotal(1); | ||
| 363 | + ds.setMaxWaitMillis(3000); | ||
| 364 | + ds.setDefaultAutoCommit(true); | ||
| 365 | + ds.setDefaultTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); | ||
| 366 | + | ||
| 367 | + Connection c1 = null; | ||
| 368 | + try { | ||
| 369 | + c1 = ds.getConnection(); | ||
| 370 | + c1.setAutoCommit(false); | ||
| 371 | + c1.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); | ||
| 372 | + assertEquals("归还前 autoCommit 应已被修改为 false", false, c1.getAutoCommit()); | ||
| 373 | + assertEquals("归还前隔离级别应已被修改为 READ_COMMITTED", | ||
| 374 | + Connection.TRANSACTION_READ_COMMITTED, c1.getTransactionIsolation()); | ||
| 375 | + c1.close(); | ||
| 376 | + c1 = null; | ||
| 377 | + | ||
| 378 | + try (Connection c2 = ds.getConnection()) { | ||
| 379 | + assertEquals("重新借出后 autoCommit 应复位为默认 true", true, c2.getAutoCommit()); | ||
| 380 | + assertEquals("重新借出后隔离级别应复位为默认 REPEATABLE_READ", | ||
| 381 | + Connection.TRANSACTION_REPEATABLE_READ, c2.getTransactionIsolation()); | ||
| 382 | + } | ||
| 383 | + } finally { | ||
| 384 | + if (c1 != null) { | ||
| 385 | + try { | ||
| 386 | + c1.close(); | ||
| 387 | + } catch (SQLException ignored) { | ||
| 388 | + } | ||
| 389 | + } | ||
| 390 | + closeDataSourceQuietly(ds); | ||
| 391 | + } | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + /** | ||
| 395 | + * 连接泄漏回收:removeAbandoned 通过维护线程在等待期间周期回收废弃连接。 | ||
| 396 | + * leak/second/third 均在 try 内获取,finally 逐个 quiet close(单个失败不阻断其余清理), | ||
| 397 | + * ds 使用 closeDataSourceQuietly;等待上界按 maxWaitMillis + 合理调度容差断言。 | ||
| 398 | + */ | ||
| 399 | + | ||
| 400 | + public void testLeakReclamationWithRemoveAbandoned() throws Exception { | ||
| 401 | + BasicDataSource ds = newPool(); | ||
| 402 | + ds.setMaxTotal(2); | ||
| 403 | + ds.setMaxWaitMillis(8000); | ||
| 404 | + ds.setRemoveAbandonedOnBorrow(true); | ||
| 405 | + ds.setRemoveAbandonedTimeout(5); // 借出超过 5 秒未使用视为泄漏 | ||
| 406 | + ds.setRemoveAbandonedOnMaintenance(true); | ||
| 407 | + ds.setTimeBetweenEvictionRunsMillis(1000); | ||
| 408 | + Connection leak = null; // 故意不归还(模拟泄漏) | ||
| 409 | + Connection second = null; | ||
| 410 | + Connection third = null; | ||
| 411 | + try { | ||
| 412 | + leak = ds.getConnection(); | ||
| 413 | + second = ds.getConnection(); | ||
| 414 | + long begin = System.currentTimeMillis(); | ||
| 415 | + try { | ||
| 416 | + third = ds.getConnection(); | ||
| 417 | + } catch (SQLException e) { | ||
| 418 | + fail("等待 removeAbandonedTimeout 后应能借出第三个连接: " + e.getMessage()); | ||
| 419 | + } | ||
| 420 | + assertFalse("回收后借出的连接应可用", third.isClosed()); | ||
| 421 | + long cost = System.currentTimeMillis() - begin; | ||
| 422 | + assertTrue("回收应等待 removeAbandonedTimeout(5s) 而非立即成功,实际: " + cost + "ms", | ||
| 423 | + cost >= 4000); | ||
| 424 | + assertTrue("回收等待不应超过 maxWaitMillis(8000ms) + 调度容差,实际: " + cost + "ms", | ||
| 425 | + cost <= 8000 + 3000); | ||
| 426 | + } finally { | ||
| 427 | + closeConnectionQuietly(third); | ||
| 428 | + closeConnectionQuietly(second); | ||
| 429 | + closeConnectionQuietly(leak); // 已被回收,close 幂等 | ||
| 430 | + closeDataSourceQuietly(ds); | ||
| 431 | + } | ||
| 432 | + } | ||
| 433 | + | ||
| 434 | + /** | ||
| 435 | + * 空闲驱逐:空闲连接超过 minEvictableIdleTimeMillis 后被维护线程驱逐到 minIdle。 | ||
| 436 | + */ | ||
| 437 | + | ||
| 438 | + public void testIdleEvictionByMinEvictableIdleTime() throws Exception { | ||
| 439 | + BasicDataSource ds = newPool(); | ||
| 440 | + ds.setInitialSize(2); | ||
| 441 | + ds.setMinIdle(0); | ||
| 442 | + ds.setMinEvictableIdleTimeMillis(3000); | ||
| 443 | + ds.setTimeBetweenEvictionRunsMillis(1000); | ||
| 444 | + try { | ||
| 445 | + try (Connection c = ds.getConnection()) { | ||
| 446 | + // 首次借出触发建池,归还后 2 个空闲 | ||
| 447 | + } | ||
| 448 | + waitForCondition(() -> ds.getNumIdle() < 2, 15000, | ||
| 449 | + "空闲连接应在超过 minEvictableIdleTimeMillis 后被驱逐到 minIdle=0"); | ||
| 450 | + assertEquals(0, ds.getNumActive()); | ||
| 451 | + } finally { | ||
| 452 | + closeDataSourceQuietly(ds); | ||
| 453 | + } | ||
| 454 | + } | ||
| 455 | + | ||
| 456 | + private static void executeCrudInTransaction(Connection conn, String name, int initialAge) throws SQLException { | ||
| 457 | + conn.setAutoCommit(false); | ||
| 458 | + try { | ||
| 459 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 460 | + "INSERT INTO `user`(`name`,`age`) VALUES (?,?)")) { | ||
| 461 | + ps.setString(1, name); | ||
| 462 | + ps.setInt(2, initialAge); | ||
| 463 | + assertEquals("事务内 INSERT 应影响一行", 1, ps.executeUpdate()); | ||
| 464 | + } | ||
| 465 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 466 | + "SELECT `age` FROM `user` WHERE `name`=?")) { | ||
| 467 | + ps.setString(1, name); | ||
| 468 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 469 | + assertTrue("事务内 INSERT 后应可查询到数据", rs.next()); | ||
| 470 | + assertEquals(initialAge, rs.getInt(1)); | ||
| 471 | + } | ||
| 472 | + } | ||
| 473 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 474 | + "UPDATE `user` SET `age`=? WHERE `name`=?")) { | ||
| 475 | + ps.setInt(1, initialAge + 1); | ||
| 476 | + ps.setString(2, name); | ||
| 477 | + assertEquals("事务内 UPDATE 应影响一行", 1, ps.executeUpdate()); | ||
| 478 | + } | ||
| 479 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 480 | + "SELECT `age` FROM `user` WHERE `name`=?")) { | ||
| 481 | + ps.setString(1, name); | ||
| 482 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 483 | + assertTrue("事务内 UPDATE 后应可查询到数据", rs.next()); | ||
| 484 | + assertEquals(initialAge + 1, rs.getInt(1)); | ||
| 485 | + } | ||
| 486 | + } | ||
| 487 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 488 | + "DELETE FROM `user` WHERE `name`=?")) { | ||
| 489 | + ps.setString(1, name); | ||
| 490 | + assertEquals("事务内 DELETE 应影响一行", 1, ps.executeUpdate()); | ||
| 491 | + } | ||
| 492 | + conn.commit(); | ||
| 493 | + } catch (SQLException | RuntimeException e) { | ||
| 494 | + conn.rollback(); | ||
| 495 | + throw e; | ||
| 496 | + } | ||
| 497 | + } | ||
| 498 | +} | ||
| @@ -0,0 +1,156 @@ | |||
| 1 | +package org.opengauss.test; | ||
| 2 | + | ||
| 3 | +import org.apache.commons.dbcp2.BasicDataSource; | ||
| 4 | +import org.junit.Test; | ||
| 5 | + | ||
| 6 | +import java.sql.Connection; | ||
| 7 | +import java.sql.ResultSet; | ||
| 8 | +import java.sql.SQLException; | ||
| 9 | +import java.sql.Statement; | ||
| 10 | + | ||
| 11 | +import static org.junit.Assert.assertEquals; | ||
| 12 | +import static org.junit.Assert.assertFalse; | ||
| 13 | +import static org.junit.Assert.assertTrue; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * 连接校验与自愈测试: | ||
| 17 | + * testOnBorrow 校验、testWhileIdle 后台校验、连接池对失效连接(服务端断连)的自愈能力。 | ||
| 18 | + * 服务端断连通过**独立管理连接执行 KILL <CONNECTION_ID>** 实现(不再调用被测 | ||
| 19 | + * ConnectionImpl.close()),并先用 SHOW PROCESSLIST 复核服务端会话确实已失效, | ||
| 20 | + * 再验证 testOnBorrow / testWhileIdle 丢弃死连接并重建。 | ||
| 21 | + * 均显式 testOnReturn=false,避免归还路径的校验干扰对“死连接已进入空闲池”的判定。 | ||
| 22 | + */ | ||
| 23 | +public class ConnectionValidationTest extends BaseDbTest { | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + public void testValidationQueryOnBorrow() throws Exception { | ||
| 27 | + // 基类连接池 testOnBorrow=true:借出即执行 SELECT 1 校验 | ||
| 28 | + try (Connection conn = dataSource.getConnection()) { | ||
| 29 | + assertFalse(conn.isClosed()); | ||
| 30 | + assertTrue("testOnBorrow=true 时借出的连接应通过校验并可用", conn.isValid(3)); | ||
| 31 | + } | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 服务端断连后,testOnBorrow 应丢弃死连接并重建新物理连接。 | ||
| 36 | + * 流程:KILL 会话 → SHOW PROCESSLIST 确认消失 → 本地 isClosed() 仍为 false → | ||
| 37 | + * 逻辑归还(testOnReturn=false)后 idle==1 证明死连接进入空闲池 → 再次借出时 | ||
| 38 | + * testOnBorrow 校验失败才丢弃并重建。 | ||
| 39 | + */ | ||
| 40 | + | ||
| 41 | + public void testOnBorrowDiscardsKilledConnectionAndRebuilds() throws Exception { | ||
| 42 | + BasicDataSource ds = newPool(); | ||
| 43 | + ds.setMaxTotal(1); | ||
| 44 | + ds.setMaxWaitMillis(5000); | ||
| 45 | + ds.setTestOnBorrow(true); | ||
| 46 | + ds.setTestWhileIdle(false); | ||
| 47 | + ds.setTestOnReturn(false); | ||
| 48 | + ds.setValidationQuery("SELECT 1"); | ||
| 49 | + | ||
| 50 | + Connection c1 = null; | ||
| 51 | + try { | ||
| 52 | + c1 = ds.getConnection(); | ||
| 53 | + com.mysql.cj.jdbc.ConnectionImpl phys1 = physicalConnection(c1); | ||
| 54 | + long killedId = connectionId(c1); | ||
| 55 | + // 服务端断连:独立管理连接执行 KILL,不调用被测 ConnectionImpl.close() | ||
| 56 | + killServerSession(killedId); | ||
| 57 | + // 复核:服务端会话确实消失(而非仅客户端标记关闭) | ||
| 58 | + waitForCondition(() -> { | ||
| 59 | + try { | ||
| 60 | + return !serverSessionExists(killedId); | ||
| 61 | + } catch (SQLException e) { | ||
| 62 | + return false; // 查询失败视作会话尚未消失,继续轮询 | ||
| 63 | + } | ||
| 64 | + }, 5000, "KILL 后服务端会话应消失,id=" + killedId); | ||
| 65 | + // 本地视图:被测 JDBC/物理连接仍报告未关闭,证明是服务端终止而非客户端 close | ||
| 66 | + assertFalse("服务端 KILL 后本地 isClosed() 仍应为 false", c1.isClosed()); | ||
| 67 | + | ||
| 68 | + c1.close(); // 逻辑归还(testOnReturn=false,归还不做校验) | ||
| 69 | + c1 = null; | ||
| 70 | + assertEquals("死连接应进入空闲池(testOnReturn=false 归还未校验)", 1, ds.getNumIdle()); | ||
| 71 | + | ||
| 72 | + // 再次借出:testOnBorrow 校验失败 → 丢弃死连接 → 新建可用连接(自愈) | ||
| 73 | + try (Connection c2 = ds.getConnection(); | ||
| 74 | + Statement st = c2.createStatement(); | ||
| 75 | + ResultSet rs = st.executeQuery("SELECT 1")) { | ||
| 76 | + assertTrue("重建后的连接应可执行查询", rs.next() && rs.getInt(1) == 1); | ||
| 77 | + assertFalse("池应自愈并提供可用连接", c2.isClosed()); | ||
| 78 | + com.mysql.cj.jdbc.ConnectionImpl phys2 = physicalConnection(c2); | ||
| 79 | + assertTrue("testOnBorrow 应丢弃死连接并创建新物理连接(对象应不同)", | ||
| 80 | + phys2 != phys1); | ||
| 81 | + } | ||
| 82 | + } finally { | ||
| 83 | + if (c1 != null) { | ||
| 84 | + try { | ||
| 85 | + c1.close(); | ||
| 86 | + } catch (SQLException ignored) { | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + closeDataSourceQuietly(ds); | ||
| 90 | + } | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + /** | ||
| 94 | + * 服务端断连后,testWhileIdle 应通过后台校验清除死连接。 | ||
| 95 | + * 流程:KILL 会话 → SHOW PROCESSLIST 确认消失 → 本地 isClosed() 仍为 false → | ||
| 96 | + * 逻辑归还(testOnReturn=false)后、维护线程首轮前 idle==1 → 后台校验轮询到 idle==0 | ||
| 97 | + * → 再次借出得到新物理对象。 | ||
| 98 | + */ | ||
| 99 | + | ||
| 100 | + public void testWhileIdleEvictsKilledConnection() throws Exception { | ||
| 101 | + BasicDataSource ds = newPool(); | ||
| 102 | + ds.setMaxTotal(2); | ||
| 103 | + ds.setMaxIdle(1); | ||
| 104 | + ds.setMinIdle(0); | ||
| 105 | + ds.setInitialSize(1); | ||
| 106 | + ds.setTestOnBorrow(false); | ||
| 107 | + ds.setTestWhileIdle(true); | ||
| 108 | + ds.setTestOnReturn(false); | ||
| 109 | + ds.setValidationQuery("SELECT 1"); | ||
| 110 | + // 首轮维护跑批在 10s 后,保证有足够时间完成“KILL→归还→断言 idle==1” | ||
| 111 | + ds.setTimeBetweenEvictionRunsMillis(10000); | ||
| 112 | + // 调大空闲时长阈值:确保只有 testWhileIdle 的校验路径会移除死连接(而非按空闲时长驱逐) | ||
| 113 | + ds.setMinEvictableIdleTimeMillis(60000); | ||
| 114 | + | ||
| 115 | + Connection c1 = null; | ||
| 116 | + try { | ||
| 117 | + c1 = ds.getConnection(); | ||
| 118 | + com.mysql.cj.jdbc.ConnectionImpl phys1 = physicalConnection(c1); | ||
| 119 | + long killedId = connectionId(c1); | ||
| 120 | + killServerSession(killedId); | ||
| 121 | + waitForCondition(() -> { | ||
| 122 | + try { | ||
| 123 | + return !serverSessionExists(killedId); | ||
| 124 | + } catch (SQLException e) { | ||
| 125 | + return false; | ||
| 126 | + } | ||
| 127 | + }, 5000, "KILL 后服务端会话应消失,id=" + killedId); | ||
| 128 | + assertFalse("服务端 KILL 后本地 isClosed() 仍应为 false", c1.isClosed()); | ||
| 129 | + | ||
| 130 | + c1.close(); // 逻辑归还(testOnReturn=false) | ||
| 131 | + c1 = null; | ||
| 132 | + // 维护线程首轮(10s)之前,死连接应已在空闲池中 | ||
| 133 | + assertEquals("维护线程首轮前死连接应进入空闲池(idle==1)", 1, ds.getNumIdle()); | ||
| 134 | + | ||
| 135 | + // 后台维护线程通过 testWhileIdle 校验清除死连接:空闲数应回到 0 | ||
| 136 | + waitForCondition(() -> ds.getNumIdle() == 0, 15000, | ||
| 137 | + "testWhileIdle 应清除死连接(minIdle=0 且不会按时间驱逐)"); | ||
| 138 | + | ||
| 139 | + // 再次借出应得到全新的物理会话 | ||
| 140 | + try (Connection c2 = ds.getConnection()) { | ||
| 141 | + assertFalse("重新借出的连接应可用", c2.isClosed()); | ||
| 142 | + com.mysql.cj.jdbc.ConnectionImpl phys2 = physicalConnection(c2); | ||
| 143 | + assertTrue("testWhileIdle 清除死连接后应创建新物理连接(对象应不同)", | ||
| 144 | + phys2 != phys1); | ||
| 145 | + } | ||
| 146 | + } finally { | ||
| 147 | + if (c1 != null) { | ||
| 148 | + try { | ||
| 149 | + c1.close(); | ||
| 150 | + } catch (SQLException ignored) { | ||
| 151 | + } | ||
| 152 | + } | ||
| 153 | + closeDataSourceQuietly(ds); | ||
| 154 | + } | ||
| 155 | + } | ||
| 156 | +} | ||
| @@ -0,0 +1,85 @@ | |||
| 1 | +package org.opengauss.test; | ||
| 2 | + | ||
| 3 | +import org.opengauss.test.util.TestConfig; | ||
| 4 | +import org.apache.commons.dbcp2.BasicDataSource; | ||
| 5 | +import org.junit.Test; | ||
| 6 | + | ||
| 7 | +import java.sql.Connection; | ||
| 8 | +import java.sql.ResultSet; | ||
| 9 | +import java.sql.SQLException; | ||
| 10 | +import java.sql.Statement; | ||
| 11 | +import java.util.Properties; | ||
| 12 | + | ||
| 13 | +import static org.junit.Assert.assertEquals; | ||
| 14 | +import static org.junit.Assert.assertTrue; | ||
| 15 | +import static org.junit.Assert.fail; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * JDBC 连接参数和受限行为验证: | ||
| 19 | + * <ul> | ||
| 20 | + * <li>allowMultiQueries 默认 off:分号多查询必须报含 "Multi-statement is not allowed" 的明确错误。</li> | ||
| 21 | + * <li>useCursorFetch=true + setFetchSize(10):仅验证该配置下查询可执行并精确返回 3 行; | ||
| 22 | + * <b>不证明使用了服务端游标,也不证明 dolphin 支持/不支持服务端游标</b>。</li> | ||
| 23 | + * <li>useSSL=false:验证本地未启用 SSL 的 Dolphin MySQL 协议端口可以连接。</li> | ||
| 24 | + * </ul> | ||
| 25 | + */ | ||
| 26 | +public class ConstraintTest extends BaseDbTest { | ||
| 27 | + | ||
| 28 | + /** 约束:allowMultiQueries 默认 off —— 分号分隔多查询必须报明确的 "Multi-statement is not allowed" */ | ||
| 29 | + | ||
| 30 | + public void testAllowMultiQueriesDefaultOff() throws Exception { | ||
| 31 | + try (Connection conn = dataSource.getConnection(); | ||
| 32 | + Statement st = conn.createStatement()) { | ||
| 33 | + try { | ||
| 34 | + st.execute("SELECT 1; SELECT 2"); | ||
| 35 | + fail("allowMultiQueries 默认关闭,分号分隔多查询应报错"); | ||
| 36 | + } catch (SQLException e) { | ||
| 37 | + assertTrue("应给出明确的 Multi-statement 报错,实际: " + e.getMessage(), | ||
| 38 | + e.getMessage() != null && e.getMessage().contains("Multi-statement is not allowed")); | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 配置兼容性验证:useCursorFetch=true + setFetchSize(10) 下查询应可执行并精确返回 3 行。 | ||
| 45 | + * 注意:这只是配置兼容性 / 查询可用性验证,<b>不证明使用了服务端游标</b>, | ||
| 46 | + * <b>也不证明 dolphin 支持或不支持服务端游标</b>。 | ||
| 47 | + */ | ||
| 48 | + | ||
| 49 | + public void testCursorFetchConfigCompatibility() throws Exception { | ||
| 50 | + Properties p = TestConfig.load(); | ||
| 51 | + String baseUrl = p.getProperty("url"); | ||
| 52 | + String cursorUrl = baseUrl + (baseUrl.contains("?") ? "&" : "?") | ||
| 53 | + + "useCursorFetch=true&defaultFetchSize=10"; | ||
| 54 | + BasicDataSource ds = new BasicDataSource(); | ||
| 55 | + ds.setDriverClassName(p.getProperty("driverClassName")); | ||
| 56 | + ds.setUrl(cursorUrl); | ||
| 57 | + ds.setUsername(p.getProperty("username")); | ||
| 58 | + ds.setPassword(p.getProperty("password")); | ||
| 59 | + ds.setMaxTotal(1); | ||
| 60 | + ds.setMaxWaitMillis(5000); | ||
| 61 | + try (Connection conn = ds.getConnection(); | ||
| 62 | + Statement st = conn.createStatement()) { | ||
| 63 | + st.setFetchSize(10); | ||
| 64 | + try (ResultSet rs = st.executeQuery("SELECT id,name FROM `user` ORDER BY id LIMIT 3")) { | ||
| 65 | + int n = 0; | ||
| 66 | + while (rs.next()) { | ||
| 67 | + n++; | ||
| 68 | + } | ||
| 69 | + assertEquals("useCursorFetch=true 配置下查询应可执行并精确返回 3 行", 3, n); | ||
| 70 | + } | ||
| 71 | + } finally { | ||
| 72 | + closeDataSourceQuietly(ds); | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + /** 使用 useSSL=false 的 URL 可正常连接(其余用例均基于此配置) */ | ||
| 77 | + | ||
| 78 | + public void testUseSslFalseConnectionWorks() throws Exception { | ||
| 79 | + String url = TestConfig.load().getProperty("url"); | ||
| 80 | + assertTrue("连接串应显式包含 useSSL=false(文档要求)", url.contains("useSSL=false")); | ||
| 81 | + try (Connection conn = dataSource.getConnection()) { | ||
| 82 | + assertTrue(conn.isValid(3)); | ||
| 83 | + } | ||
| 84 | + } | ||
| 85 | +} | ||
| @@ -0,0 +1,101 @@ | |||
| 1 | +package org.opengauss.test; | ||
| 2 | + | ||
| 3 | +import org.opengauss.dao.UserDao; | ||
| 4 | +import org.opengauss.entity.User; | ||
| 5 | +import org.junit.Before; | ||
| 6 | +import org.junit.Test; | ||
| 7 | + | ||
| 8 | +import java.util.Arrays; | ||
| 9 | +import java.util.List; | ||
| 10 | + | ||
| 11 | +import static org.junit.Assert.assertEquals; | ||
| 12 | +import static org.junit.Assert.assertNotNull; | ||
| 13 | +import static org.junit.Assert.assertNull; | ||
| 14 | +import static org.junit.Assert.assertTrue; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * CRUD 功能测试:增删改查、自增主键回填、模糊查询、分页、批量 | ||
| 18 | + * 依赖 mysql_test_db 库中的 user 表(见 src/main/resources/init.sql) | ||
| 19 | + */ | ||
| 20 | +public class CrudTest extends BaseDbTest { | ||
| 21 | + | ||
| 22 | + private UserDao userDao; | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + public void setUp() { | ||
| 26 | + userDao = new UserDao(dataSource); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + public void testInsertBackfillAutoIncrementKey() throws Exception { | ||
| 31 | + User u = new User("crud_测试_" + System.currentTimeMillis(), 18); | ||
| 32 | + assertEquals(1, userDao.insert(u)); | ||
| 33 | + assertNotNull("自增主键应回填", u.getId()); | ||
| 34 | + User db = userDao.findById(u.getId()); | ||
| 35 | + assertNotNull(db); | ||
| 36 | + assertEquals(u.getName(), db.getName()); | ||
| 37 | + assertEquals(u.getAge(), db.getAge()); | ||
| 38 | + userDao.delete(u.getId()); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + | ||
| 42 | + public void testFindAll() throws Exception { | ||
| 43 | + List<User> all = userDao.findAll(); | ||
| 44 | + assertTrue("初始化数据应至少有 3 行,实际: " + all.size(), all.size() >= 3); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + public void testFindByIdNotExistReturnsNull() throws Exception { | ||
| 49 | + assertNull(userDao.findById(-1)); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + | ||
| 53 | + public void testFindByNameLike() throws Exception { | ||
| 54 | + List<User> like = userDao.findByNameLike("张"); | ||
| 55 | + assertTrue("应能模糊查到一个以上用户,实际: " + like.size(), like.size() >= 1); | ||
| 56 | + like.forEach(u -> assertTrue(u.getName().contains("张"))); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + | ||
| 60 | + public void testFindByPage() throws Exception { | ||
| 61 | + List<User> page = userDao.findByPage(2, 0); | ||
| 62 | + assertEquals("LIMIT 2 应返回 2 行", 2, page.size()); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + | ||
| 66 | + public void testUpdate() throws Exception { | ||
| 67 | + User u = new User("crud_update_" + System.currentTimeMillis(), 20); | ||
| 68 | + userDao.insert(u); | ||
| 69 | + u.setAge(30); | ||
| 70 | + assertEquals(1, userDao.update(u)); | ||
| 71 | + User db = userDao.findById(u.getId()); | ||
| 72 | + assertEquals(Integer.valueOf(30), db.getAge()); | ||
| 73 | + userDao.delete(u.getId()); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + | ||
| 77 | + public void testDelete() throws Exception { | ||
| 78 | + User u = new User("crud_del_" + System.currentTimeMillis(), 20); | ||
| 79 | + userDao.insert(u); | ||
| 80 | + assertEquals(1, userDao.delete(u.getId())); | ||
| 81 | + assertNull(userDao.findById(u.getId())); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + | ||
| 85 | + public void testBatchInsert() throws Exception { | ||
| 86 | + List<User> users = Arrays.asList( | ||
| 87 | + new User("批_一号", 21), | ||
| 88 | + new User("批_二号", 22), | ||
| 89 | + new User("批_三号", 23)); | ||
| 90 | + int[] rows = userDao.batchInsert(users); | ||
| 91 | + assertEquals("批量插入应返回每行影响行数", 3, rows.length); | ||
| 92 | + users.forEach(u -> assertNotNull("批量插入应回填主键", u.getId())); | ||
| 93 | + users.forEach(u -> { | ||
| 94 | + try { | ||
| 95 | + userDao.delete(u.getId()); | ||
| 96 | + } catch (Exception e) { | ||
| 97 | + throw new RuntimeException(e); | ||
| 98 | + } | ||
| 99 | + }); | ||
| 100 | + } | ||
| 101 | +} | ||
| @@ -0,0 +1,119 @@ | |||
| 1 | +package org.opengauss.test; | ||
| 2 | + | ||
| 3 | +import org.junit.After; | ||
| 4 | +import org.junit.Before; | ||
| 5 | +import org.junit.Test; | ||
| 6 | + | ||
| 7 | +import java.math.BigDecimal; | ||
| 8 | +import java.sql.Connection; | ||
| 9 | +import java.sql.Date; | ||
| 10 | +import java.sql.PreparedStatement; | ||
| 11 | +import java.sql.ResultSet; | ||
| 12 | +import java.sql.Statement; | ||
| 13 | +import java.sql.Timestamp; | ||
| 14 | +import java.sql.Types; | ||
| 15 | + | ||
| 16 | +import static org.junit.Assert.assertEquals; | ||
| 17 | +import static org.junit.Assert.assertNotNull; | ||
| 18 | +import static org.junit.Assert.assertTrue; | ||
| 19 | + | ||
| 20 | +/** | ||
| 21 | + * 数据类型与字符集测试:中文/emoji(utf8mb4)、常用类型往返读写、NULL 处理。 | ||
| 22 | + * 用例自建测试表,互不依赖,执行完自动清理。 | ||
| 23 | + */ | ||
| 24 | +public class DataTypeCharsetTest extends BaseDbTest { | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + public void createTable() throws Exception { | ||
| 28 | + try (Connection conn = dataSource.getConnection(); | ||
| 29 | + Statement st = conn.createStatement()) { | ||
| 30 | + st.execute("DROP TABLE IF EXISTS test_datatype"); | ||
| 31 | + st.execute("CREATE TABLE test_datatype (" | ||
| 32 | + + "id INT AUTO_INCREMENT PRIMARY KEY, " | ||
| 33 | + + "c_varchar VARCHAR(100), " | ||
| 34 | + + "c_int INT, " | ||
| 35 | + + "c_bigint BIGINT, " | ||
| 36 | + + "c_decimal DECIMAL(10,2), " | ||
| 37 | + + "c_date DATE, " | ||
| 38 | + + "c_datetime DATETIME, " | ||
| 39 | + + "c_text TEXT, " | ||
| 40 | + + "c_nullable VARCHAR(20))"); | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + | ||
| 45 | + public void dropTable() throws Exception { | ||
| 46 | + try (Connection conn = dataSource.getConnection(); | ||
| 47 | + Statement st = conn.createStatement()) { | ||
| 48 | + st.execute("DROP TABLE IF EXISTS test_datatype"); | ||
| 49 | + } | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + | ||
| 53 | + public void testChineseAndEmojiRoundTrip() throws Exception { | ||
| 54 | + String value = "中文😀emoji测试_" + System.currentTimeMillis(); | ||
| 55 | + try (Connection conn = dataSource.getConnection(); | ||
| 56 | + PreparedStatement ps = conn.prepareStatement( | ||
| 57 | + "INSERT INTO test_datatype(c_varchar) VALUES (?)")) { | ||
| 58 | + ps.setString(1, value); | ||
| 59 | + ps.executeUpdate(); | ||
| 60 | + } | ||
| 61 | + try (Connection conn = dataSource.getConnection(); | ||
| 62 | + PreparedStatement ps = conn.prepareStatement( | ||
| 63 | + "SELECT c_varchar FROM test_datatype WHERE c_varchar=?")) { | ||
| 64 | + ps.setString(1, value); | ||
| 65 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 66 | + assertTrue("插入的数据应能查到", rs.next()); | ||
| 67 | + assertEquals("中文与 emoji 应无损读写", value, rs.getString(1)); | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + | ||
| 73 | + public void testCommonTypesRoundTrip() throws Exception { | ||
| 74 | + long bigValue = 9223372036854775807L; | ||
| 75 | + try (Connection conn = dataSource.getConnection(); | ||
| 76 | + PreparedStatement ps = conn.prepareStatement( | ||
| 77 | + "INSERT INTO test_datatype(c_int,c_bigint,c_decimal,c_date,c_datetime,c_text) " | ||
| 78 | + + "VALUES (?,?,?,?,?,?)")) { | ||
| 79 | + ps.setInt(1, 123); | ||
| 80 | + ps.setLong(2, bigValue); | ||
| 81 | + ps.setBigDecimal(3, new BigDecimal("12345.67")); | ||
| 82 | + ps.setDate(4, Date.valueOf("2026-08-06")); | ||
| 83 | + ps.setTimestamp(5, Timestamp.valueOf("2026-08-06 12:30:00")); | ||
| 84 | + ps.setString(6, "TEXT内容"); | ||
| 85 | + ps.executeUpdate(); | ||
| 86 | + } | ||
| 87 | + try (Connection conn = dataSource.getConnection(); | ||
| 88 | + PreparedStatement ps = conn.prepareStatement( | ||
| 89 | + "SELECT c_int,c_bigint,c_decimal,c_date,c_datetime,c_text FROM test_datatype WHERE c_int=123"); | ||
| 90 | + ResultSet rs = ps.executeQuery()) { | ||
| 91 | + assertTrue(rs.next()); | ||
| 92 | + assertEquals(123, rs.getInt(1)); | ||
| 93 | + assertEquals(bigValue, rs.getLong(2)); | ||
| 94 | + assertEquals(0, new BigDecimal("12345.67").compareTo(rs.getBigDecimal(3))); | ||
| 95 | + assertEquals(Date.valueOf("2026-08-06"), rs.getDate(4)); | ||
| 96 | + assertNotNull("DATETIME 应正常读写", rs.getTimestamp(5)); | ||
| 97 | + assertEquals("TEXT内容", rs.getString(6)); | ||
| 98 | + } | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + | ||
| 102 | + public void testNullHandling() throws Exception { | ||
| 103 | + try (Connection conn = dataSource.getConnection(); | ||
| 104 | + PreparedStatement ps = conn.prepareStatement( | ||
| 105 | + "INSERT INTO test_datatype(c_varchar, c_nullable) VALUES (?, ?)")) { | ||
| 106 | + ps.setString(1, "null_test"); | ||
| 107 | + ps.setNull(2, Types.VARCHAR); | ||
| 108 | + ps.executeUpdate(); | ||
| 109 | + } | ||
| 110 | + try (Connection conn = dataSource.getConnection(); | ||
| 111 | + PreparedStatement ps = conn.prepareStatement( | ||
| 112 | + "SELECT c_nullable FROM test_datatype WHERE c_varchar='null_test'"); | ||
| 113 | + ResultSet rs = ps.executeQuery()) { | ||
| 114 | + assertTrue(rs.next()); | ||
| 115 | + rs.getString(1); | ||
| 116 | + assertTrue("NULL 应通过 wasNull() 识别", rs.wasNull()); | ||
| 117 | + } | ||
| 118 | + } | ||
| 119 | +} | ||
| @@ -0,0 +1,104 @@ | |||
| 1 | +package org.opengauss.test; | ||
| 2 | + | ||
| 3 | +import org.apache.commons.dbcp2.BasicDataSource; | ||
| 4 | +import org.opengauss.test.util.TestConfig; | ||
| 5 | +import org.junit.Test; | ||
| 6 | + | ||
| 7 | +import java.sql.Connection; | ||
| 8 | +import java.sql.SQLException; | ||
| 9 | +import java.util.Properties; | ||
| 10 | + | ||
| 11 | +import static org.junit.Assert.assertEquals; | ||
| 12 | +import static org.junit.Assert.assertNotNull; | ||
| 13 | +import static org.junit.Assert.assertTrue; | ||
| 14 | +import static org.junit.Assert.fail; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * 连接池初始化与配置相关测试: | ||
| 18 | + * 驱动加载、配置项、初始连接数、错误配置容错 | ||
| 19 | + */ | ||
| 20 | +public class DbcpDataSourceTest extends BaseDbTest { | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + public void testPoolConfigLoaded() { | ||
| 24 | + Properties props = TestConfig.load(); | ||
| 25 | + assertEquals(props.getProperty("url"), dataSource.getUrl()); | ||
| 26 | + assertEquals(props.getProperty("username"), dataSource.getUsername()); | ||
| 27 | + assertTrue("password 应从配置文件映射到连接池", | ||
| 28 | + props.getProperty("password").equals(dataSource.getPassword())); | ||
| 29 | + assertEquals(props.getProperty("driverClassName"), dataSource.getDriverClassName()); | ||
| 30 | + assertEquals(Integer.parseInt(props.getProperty("initialSize")), dataSource.getInitialSize()); | ||
| 31 | + assertEquals(Integer.parseInt(props.getProperty("maxTotal")), dataSource.getMaxTotal()); | ||
| 32 | + assertEquals(Integer.parseInt(props.getProperty("maxIdle")), dataSource.getMaxIdle()); | ||
| 33 | + assertEquals(Integer.parseInt(props.getProperty("minIdle")), dataSource.getMinIdle()); | ||
| 34 | + assertEquals(Long.parseLong(props.getProperty("maxWaitMillis")), dataSource.getMaxWaitMillis()); | ||
| 35 | + assertEquals(props.getProperty("validationQuery"), dataSource.getValidationQuery()); | ||
| 36 | + assertEquals(Boolean.parseBoolean(props.getProperty("testOnBorrow")), dataSource.getTestOnBorrow()); | ||
| 37 | + assertEquals(Boolean.parseBoolean(props.getProperty("testWhileIdle")), dataSource.getTestWhileIdle()); | ||
| 38 | + assertEquals(Long.parseLong(props.getProperty("timeBetweenEvictionRunsMillis")), | ||
| 39 | + dataSource.getTimeBetweenEvictionRunsMillis()); | ||
| 40 | + assertEquals(Long.parseLong(props.getProperty("minEvictableIdleTimeMillis")), | ||
| 41 | + dataSource.getMinEvictableIdleTimeMillis()); | ||
| 42 | + assertEquals(Boolean.parseBoolean(props.getProperty("removeAbandonedOnBorrow")), | ||
| 43 | + dataSource.getRemoveAbandonedOnBorrow()); | ||
| 44 | + assertEquals(Integer.parseInt(props.getProperty("removeAbandonedTimeout")), | ||
| 45 | + dataSource.getRemoveAbandonedTimeout()); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + | ||
| 49 | + public void testDriverExplicitlyLoadable() throws Exception { | ||
| 50 | + // DBCP2 基于 JDBC4 SPI 自动加载驱动,显式 Class.forName 也应可用 | ||
| 51 | + Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver"); | ||
| 52 | + assertNotNull(clazz); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + | ||
| 56 | + public void testPoolStartedWithInitialSize() throws Exception { | ||
| 57 | + // 预检已触发建池:initialSize=2,连接全部归还后 idle >= 2 | ||
| 58 | + assertEquals(0, dataSource.getNumActive()); | ||
| 59 | + assertTrue("idle 应不少于 initialSize,实际: " + dataSource.getNumIdle(), | ||
| 60 | + dataSource.getNumIdle() >= dataSource.getInitialSize()); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + | ||
| 64 | + public void testWrongPasswordFails() { | ||
| 65 | + BasicDataSource bad = new BasicDataSource(); | ||
| 66 | + applyUrl(bad); | ||
| 67 | + bad.setPassword("definitely-wrong-password"); | ||
| 68 | + bad.setMaxTotal(1); | ||
| 69 | + bad.setMaxWaitMillis(3000); | ||
| 70 | + try (Connection conn = bad.getConnection()) { | ||
| 71 | + fail("密码错误时应抛出 SQLException,却成功获取连接: " + conn); | ||
| 72 | + } catch (SQLException e) { | ||
| 73 | + // 预期:认证失败 | ||
| 74 | + } finally { | ||
| 75 | + try { | ||
| 76 | + bad.close(); | ||
| 77 | + } catch (SQLException ignored) { | ||
| 78 | + } | ||
| 79 | + } | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + | ||
| 83 | + public void testWrongPortFails() { | ||
| 84 | + BasicDataSource bad = new BasicDataSource(); | ||
| 85 | + bad.setDriverClassName("com.mysql.cj.jdbc.Driver"); | ||
| 86 | + // 故意用未监听的端口(原端口 + 1) | ||
| 87 | + String url = TestConfig.load().getProperty("url"); | ||
| 88 | + bad.setUrl(url.replaceFirst(":[0-9]+/", ":1/")); | ||
| 89 | + bad.setUsername("mysql_test_db"); | ||
| 90 | + bad.setPassword("whatever"); | ||
| 91 | + bad.setMaxTotal(1); | ||
| 92 | + bad.setMaxWaitMillis(3000); | ||
| 93 | + try (Connection conn = bad.getConnection()) { | ||
| 94 | + fail("端口不通时应抛出 SQLException,却成功获取连接"); | ||
| 95 | + } catch (SQLException e) { | ||
| 96 | + // 预期:连接被拒绝 | ||
| 97 | + } finally { | ||
| 98 | + try { | ||
| 99 | + bad.close(); | ||
| 100 | + } catch (SQLException ignored) { | ||
| 101 | + } | ||
| 102 | + } | ||
| 103 | + } | ||
| 104 | +} | ||
| @@ -0,0 +1,243 @@ | |||
| 1 | +package org.opengauss.test; | ||
| 2 | + | ||
| 3 | +import org.junit.Test; | ||
| 4 | + | ||
| 5 | +import java.math.BigDecimal; | ||
| 6 | +import java.sql.Connection; | ||
| 7 | +import java.sql.PreparedStatement; | ||
| 8 | +import java.sql.ResultSet; | ||
| 9 | +import java.sql.SQLException; | ||
| 10 | +import java.sql.Statement; | ||
| 11 | + | ||
| 12 | +import static org.junit.Assert.assertEquals; | ||
| 13 | +import static org.junit.Assert.assertFalse; | ||
| 14 | +import static org.junit.Assert.assertNull; | ||
| 15 | +import static org.junit.Assert.assertTrue; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * Standard SQL category coverage through DBCP and the Dolphin MySQL protocol. | ||
| 19 | + * | ||
| 20 | + * Each test owns a uniquely named table. The DCL test also creates a temporary | ||
| 21 | + * role, verifies the privilege transition, and removes the role in finally so | ||
| 22 | + * repeated mvn test runs do not leave database objects behind. | ||
| 23 | + */ | ||
| 24 | +public class SqlCategoryTest extends BaseDbTest { | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * Executes CREATE, ALTER, TRUNCATE, and DROP through one pooled connection. | ||
| 28 | + * JDBC update counts and the inserted row prove that each schema transition | ||
| 29 | + * completed before the next statement; finally provides retry-safe cleanup. | ||
| 30 | + */ | ||
| 31 | + | ||
| 32 | + public void testDdlCreateAlterTruncateDrop() throws Exception { | ||
| 33 | + String table = tableName("ddl"); | ||
| 34 | + try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) { | ||
| 35 | + assertEquals(0, st.executeUpdate("CREATE TABLE " + table | ||
| 36 | + + " (id INT PRIMARY KEY, name VARCHAR(64) NOT NULL)")); | ||
| 37 | + assertEquals(0, st.executeUpdate("ALTER TABLE " + table | ||
| 38 | + + " ADD COLUMN amount DECIMAL(10,2)")); | ||
| 39 | + assertEquals(1, st.executeUpdate("INSERT INTO " + table | ||
| 40 | + + " (id, name, amount) VALUES (1, 'ddl-row', 10.50)")); | ||
| 41 | + assertEquals(0, st.executeUpdate("TRUNCATE TABLE " + table)); | ||
| 42 | + assertEquals(0, st.executeUpdate("DROP TABLE " + table)); | ||
| 43 | + } finally { | ||
| 44 | + dropTableQuietly(table); | ||
| 45 | + } | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * Verifies INSERT, UPDATE, and DELETE by checking affected-row counts, then | ||
| 50 | + * reads the final state through a separate pooled connection so success is | ||
| 51 | + * not inferred only from the driver's return value. | ||
| 52 | + */ | ||
| 53 | + | ||
| 54 | + public void testDmlInsertUpdateDelete() throws Exception { | ||
| 55 | + String table = tableName("dml"); | ||
| 56 | + try { | ||
| 57 | + createTable(table); | ||
| 58 | + try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) { | ||
| 59 | + assertEquals(2, st.executeUpdate("INSERT INTO " + table | ||
| 60 | + + " (id, name, amount) VALUES (1, 'one', 1.00), (2, 'two', 2.00)")); | ||
| 61 | + assertEquals(1, st.executeUpdate("UPDATE " + table | ||
| 62 | + + " SET amount = 20.00, name = 'two-updated' WHERE id = 2")); | ||
| 63 | + assertEquals(1, st.executeUpdate("DELETE FROM " + table + " WHERE id = 1")); | ||
| 64 | + } | ||
| 65 | + assertEquals(1, countRows(table)); | ||
| 66 | + assertEquals("two-updated", findName(table, 2)); | ||
| 67 | + } finally { | ||
| 68 | + dropTableQuietly(table); | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + /** | ||
| 73 | + * Covers parameterized filtering, ORDER BY, and COUNT aggregation. The test | ||
| 74 | + * asserts result order, cardinality, and end-of-result-set behavior to catch | ||
| 75 | + * both missing and unexpected rows. | ||
| 76 | + */ | ||
| 77 | + | ||
| 78 | + public void testDqlSelectFilteringOrderingAndAggregate() throws Exception { | ||
| 79 | + String table = tableName("dql"); | ||
| 80 | + try { | ||
| 81 | + createTable(table); | ||
| 82 | + try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) { | ||
| 83 | + st.executeUpdate("INSERT INTO " + table | ||
| 84 | + + " (id, name, amount) VALUES (1, 'alpha', 3.00), (2, 'beta', 8.00), (3, 'beta-2', 5.00)"); | ||
| 85 | + } | ||
| 86 | + try (Connection conn = dataSource.getConnection(); | ||
| 87 | + PreparedStatement ps = conn.prepareStatement("SELECT name FROM " + table | ||
| 88 | + + " WHERE amount >= ? ORDER BY amount DESC"); | ||
| 89 | + ResultSet rs = setAndQuery(ps, 5.00)) { | ||
| 90 | + assertTrue(rs.next()); | ||
| 91 | + assertEquals("beta", rs.getString(1)); | ||
| 92 | + assertTrue(rs.next()); | ||
| 93 | + assertEquals("beta-2", rs.getString(1)); | ||
| 94 | + assertFalse(rs.next()); | ||
| 95 | + } | ||
| 96 | + assertEquals(3, countRows(table)); | ||
| 97 | + } finally { | ||
| 98 | + dropTableQuietly(table); | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + /** | ||
| 103 | + * Creates an isolated login role, grants SELECT, checks the effective table | ||
| 104 | + * privilege, revokes it, and checks again. The generated password and unique | ||
| 105 | + * role name prevent credentials or shared database state from entering tests. | ||
| 106 | + */ | ||
| 107 | + | ||
| 108 | + public void testDclGrantAndRevoke() throws Exception { | ||
| 109 | + String table = tableName("dcl"); | ||
| 110 | + String role = "dbcp_sql_role_" + Long.toUnsignedString(System.nanoTime(), 36); | ||
| 111 | + String rolePassword = "Dbcp!Sql9" + Long.toString(System.nanoTime(), 36); | ||
| 112 | + try { | ||
| 113 | + createTable(table); | ||
| 114 | + try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) { | ||
| 115 | + // openGauss requires a password when a role is created through Dolphin. | ||
| 116 | + // Generate a test-only password at runtime; the role is dropped in finally below. | ||
| 117 | + st.executeUpdate("CREATE ROLE " + role + " LOGIN PASSWORD '" + rolePassword + "'"); | ||
| 118 | + st.executeUpdate("GRANT SELECT ON TABLE " + table + " TO " + role); | ||
| 119 | + assertTrue(hasTablePrivilege(conn, role, table, "SELECT")); | ||
| 120 | + st.executeUpdate("REVOKE SELECT ON TABLE " + table + " FROM " + role); | ||
| 121 | + assertFalse(hasTablePrivilege(conn, role, table, "SELECT")); | ||
| 122 | + } | ||
| 123 | + } finally { | ||
| 124 | + // Drop the owned table first so a failure between GRANT and REVOKE | ||
| 125 | + // cannot leave dependencies that prevent the temporary role cleanup. | ||
| 126 | + dropTableQuietly(table); | ||
| 127 | + dropRoleQuietly(role); | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + /** | ||
| 132 | + * Executes explicit START TRANSACTION/COMMIT and START TRANSACTION/ROLLBACK. | ||
| 133 | + * Follow-up reads use new pooled connections, proving committed data is | ||
| 134 | + * visible and rolled-back data is absent outside the original transaction. | ||
| 135 | + */ | ||
| 136 | + | ||
| 137 | + public void testTclCommitAndRollback() throws Exception { | ||
| 138 | + String table = tableName("tcl"); | ||
| 139 | + try { | ||
| 140 | + createTable(table); | ||
| 141 | + String committed = "committed"; | ||
| 142 | + try (Connection conn = dataSource.getConnection(); Statement tx = conn.createStatement()) { | ||
| 143 | + tx.execute("START TRANSACTION"); | ||
| 144 | + try (PreparedStatement ps = conn.prepareStatement("INSERT INTO " + table | ||
| 145 | + + " (id, name, amount) VALUES (?, ?, ?)")) { | ||
| 146 | + ps.setInt(1, 1); | ||
| 147 | + ps.setString(2, committed); | ||
| 148 | + ps.setBigDecimal(3, new BigDecimal("1.00")); | ||
| 149 | + assertEquals(1, ps.executeUpdate()); | ||
| 150 | + } | ||
| 151 | + tx.execute("COMMIT"); | ||
| 152 | + } | ||
| 153 | + assertEquals(1, countRows(table)); | ||
| 154 | + | ||
| 155 | + try (Connection conn = dataSource.getConnection(); Statement tx = conn.createStatement()) { | ||
| 156 | + tx.execute("START TRANSACTION"); | ||
| 157 | + try (PreparedStatement ps = conn.prepareStatement("INSERT INTO " + table | ||
| 158 | + + " (id, name, amount) VALUES (?, ?, ?)")) { | ||
| 159 | + ps.setInt(1, 2); | ||
| 160 | + ps.setString(2, "rolled-back"); | ||
| 161 | + ps.setBigDecimal(3, new BigDecimal("2.00")); | ||
| 162 | + assertEquals(1, ps.executeUpdate()); | ||
| 163 | + } | ||
| 164 | + tx.execute("ROLLBACK"); | ||
| 165 | + } | ||
| 166 | + assertEquals(1, countRows(table)); | ||
| 167 | + assertEquals("committed", findName(table, 1)); | ||
| 168 | + assertNull(findName(table, 2)); | ||
| 169 | + } finally { | ||
| 170 | + dropTableQuietly(table); | ||
| 171 | + } | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + private String tableName(String category) { | ||
| 175 | + return "dbcp_sql_" + category + "_" + Long.toUnsignedString(System.nanoTime(), 36); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + private void createTable(String table) throws SQLException { | ||
| 179 | + try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) { | ||
| 180 | + st.executeUpdate("CREATE TABLE " + table | ||
| 181 | + + " (id INT PRIMARY KEY, name VARCHAR(64) NOT NULL, amount DECIMAL(10,2))"); | ||
| 182 | + } | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + private int countRows(String table) throws SQLException { | ||
| 186 | + try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement(); | ||
| 187 | + ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM " + table)) { | ||
| 188 | + assertTrue(rs.next()); | ||
| 189 | + return rs.getInt(1); | ||
| 190 | + } | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + private String findName(String table, int id) throws SQLException { | ||
| 194 | + try (Connection conn = dataSource.getConnection(); | ||
| 195 | + PreparedStatement ps = conn.prepareStatement("SELECT name FROM " + table + " WHERE id = ?")) { | ||
| 196 | + ps.setInt(1, id); | ||
| 197 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 198 | + return rs.next() ? rs.getString(1) : null; | ||
| 199 | + } | ||
| 200 | + } | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + private ResultSet setAndQuery(PreparedStatement ps, double amount) throws SQLException { | ||
| 204 | + ps.setDouble(1, amount); | ||
| 205 | + return ps.executeQuery(); | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + private boolean hasTablePrivilege(Connection conn, String role, String table, String privilege) | ||
| 209 | + throws SQLException { | ||
| 210 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 211 | + "SELECT has_table_privilege(?, ?, ?)")) { | ||
| 212 | + ps.setString(1, role); | ||
| 213 | + ps.setString(2, table); | ||
| 214 | + ps.setString(3, privilege); | ||
| 215 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 216 | + assertTrue(rs.next()); | ||
| 217 | + return rs.getBoolean(1); | ||
| 218 | + } | ||
| 219 | + } | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + private void dropRoleQuietly(String role) { | ||
| 223 | + if (role == null) { | ||
| 224 | + return; | ||
| 225 | + } | ||
| 226 | + try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) { | ||
| 227 | + st.executeUpdate("DROP ROLE " + role); | ||
| 228 | + } catch (SQLException ignored) { | ||
| 229 | + // Cleanup is best effort; preserve the original test failure. | ||
| 230 | + } | ||
| 231 | + } | ||
| 232 | + | ||
| 233 | + private void dropTableQuietly(String table) { | ||
| 234 | + if (table == null) { | ||
| 235 | + return; | ||
| 236 | + } | ||
| 237 | + try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) { | ||
| 238 | + st.executeUpdate("DROP TABLE IF EXISTS " + table); | ||
| 239 | + } catch (SQLException ignored) { | ||
| 240 | + // Cleanup is best effort; preserve the original test failure. | ||
| 241 | + } | ||
| 242 | + } | ||
| 243 | +} | ||