已合并
【实习#28】新增 SpringDataJPA 配合 MySQL JDBC 连接 openGauss B 库示例与自测 #111
wsrym创建于 29 天前
【实习#28】新增 SpringDataJPA 配合 MySQL JDBC 连接 openGauss B 库示例与自测 #111
已合并
共 22 个文件变更+1828-0
| @@ -0,0 +1,94 @@ | |||
| 1 | +# Spring Data JPA 连接 openGauss B 兼容库 | ||
| 2 | + | ||
| 3 | +对应任务:Issue #28 — openGauss B 库 B 协议适配 MySQL Java 框架 SpringDataJPA。 | ||
| 4 | + | ||
| 5 | +使用 Spring Boot 3.x + Spring Data JPA(Hibernate 6)配合 MySQL JDBC 驱动连接 openGauss B 兼容模式数据库的示例工程,包含 19 个测试用例、7 档驱动版本矩阵,以及测试思维导图与自测报告。 | ||
| 6 | + | ||
| 7 | +## 目录结构 | ||
| 8 | + | ||
| 9 | +``` | ||
| 10 | +springdatajpa-connect-opengauss-b | ||
| 11 | +├── pom.xml # 含 7 档驱动 profile(版本矩阵用) | ||
| 12 | +├── README.md | ||
| 13 | +├── docs | ||
| 14 | +│ ├── 测试思维导图-SpringDataJPA连接openGaussB库.{mm,md,html} | ||
| 15 | +│ └── 基于SpringDataJPA框架的openGauss数据库测试.md # 自测报告 | ||
| 16 | +└── src | ||
| 17 | + ├── main/java/org/opengauss/smoke # 实体/仓储/方言/示例应用 | ||
| 18 | + └── test/java/org/opengauss/smoke/jpacases # 测试套件(19 用例) | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +## 前置条件 | ||
| 22 | + | ||
| 23 | +与 `springboot-connect-opengauss-b`(Issue #27 工程)相同:JDK 17+、openGauss MySQL 协议已开(3308)、B 库/schema/`` `user` `` 表/用户已就绪。环境搭建步骤详见 docs 仓《[SpringDataJPA 连接示例](https://docs.opengauss.org/zh/docs/latest/mysql_compatibility_developer_guide/springdatajpa_development.html)》。 | ||
| 24 | + | ||
| 25 | +## 配置 | ||
| 26 | + | ||
| 27 | +| 变量 | 默认值 | 说明 | | ||
| 28 | +|---|---|---| | ||
| 29 | +| DB_HOST | 127.0.0.1 | openGauss 所在机器的 IP | | ||
| 30 | +| DB_PORT | 3308 | dolphin 协议端口 | | ||
| 31 | +| DB_NAME | mysql_test_db | B 库下 schema(等价 MySQL database) | | ||
| 32 | +| DB_USER / DB_PASSWORD | mysql_test_db / ChangeMe_123 | MySQL 认证用户 | | ||
| 33 | + | ||
| 34 | +## 运行 | ||
| 35 | + | ||
| 36 | +```bash | ||
| 37 | +# 运行全部测试(19 用例) | ||
| 38 | +mvn test | ||
| 39 | + | ||
| 40 | +# 运行示例主程序 | ||
| 41 | +mvn spring-boot:run | ||
| 42 | +``` | ||
| 43 | + | ||
| 44 | +## 测试内容(19 用例) | ||
| 45 | + | ||
| 46 | +| 类别 | 用例 | 覆盖点 | | ||
| 47 | +|---|---|---| | ||
| 48 | +| 已知差异回归 | K1 / K2 | K1 复现转义语义差异(派生 LIKE escape 报错,回归断言,与 Plugin 仓 issue #504 同源);K2 验证 workaround(JPQL 显式 `escape '!'`) | | ||
| 49 | +| ddl-auto 专项 | D1 / D2 | D1 update 模式(FINDING E 静默失效);D2 validate 模式 | | ||
| 50 | +| 类型映射 | M1 | BigDecimal/LocalDate/LocalDateTime/Boolean/String 往返 + IDENTITY 主键回填 | | ||
| 51 | +| 分页与批量 | P1 / P2 | PageRequest 多页遍历 / saveAll(50) 批量回填 | | ||
| 52 | +| 冒烟 | 3 | saveAndFind / pagination / transactionRollback | | ||
| 53 | +| 协议命令 | C1-C9 | COM_QUERY/INIT_DB/PING/STMT_PREPARE/STMT_EXECUTE/STMT_CLOSE/STMT_SEND_LONG_DATA/STMT_RESET/STMT_FETCH/SET_OPTION/QUIT(评审意见落地,与 #27 报告 7.1-7.9 同设计) | | ||
| 54 | + | ||
| 55 | +## 驱动版本矩阵(7 档 × 19 用例,三档行为) | ||
| 56 | + | ||
| 57 | +| 驱动版本 | 结果 | | ||
| 58 | +|---|---| | ||
| 59 | +| 8.0.28(文档基准) | 协议类 1 失败 + 6 错误(FINDING I 崩溃 + FINDING H 丢 8KB),其余 12 通过(含 K1/D1 复现) | | ||
| 60 | +| 8.0.33 / 8.1.0 / 8.3.0 / 8.4.0 | 18/19(仅 FINDING H;RESET 正常、服务端不崩) | | ||
| 61 | +| 9.7.0(Boot 默认)/ 26.7.0(最新) | 19/19 全部通过 | | ||
| 62 | + | ||
| 63 | +切换档位重跑: | ||
| 64 | + | ||
| 65 | +```bash | ||
| 66 | +mvn test -Pdriver-8.0.28 | ||
| 67 | +mvn test -Pdriver-8.0.33 | ||
| 68 | +mvn test -Pdriver-8.1.0 | ||
| 69 | +mvn test -Pdriver-8.3.0 | ||
| 70 | +mvn test -Pdriver-8.4.0 | ||
| 71 | +mvn test -Pdriver-9.7.0 | ||
| 72 | +mvn test -Pdriver-26.7.0 | ||
| 73 | +``` | ||
| 74 | + | ||
| 75 | +> 三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常。核心链路与 K1/D1/FINDING E/F/G 均与驱动版本无关;FINDING H/I 为服务端缺陷、触发条件依赖驱动版本。 | ||
| 76 | +> | ||
| 77 | +> **注意:8.0.28 档跑协议命令测试会打崩 openGauss 服务端进程(FINDING I),需重启数据库后才可继续。** | ||
| 78 | + | ||
| 79 | +## 已知要点与限制 | ||
| 80 | + | ||
| 81 | +## 已知要点与限制 | ||
| 82 | + | ||
| 83 | +| 项 | 说明 | | ||
| 84 | +|---|---| | ||
| 85 | +| 转义语义差异(issue #504) | Hibernate 6.6 默认给 LIKE 追加 `escape '\\'`(两个反斜杠),B 模式视 `'\\'` 为 2 字符 → `invalid escape string`。根源是 B 模式不支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)。规避:JPQL 显式 `escape '!'`(见 UserRepository.searchByName)。与驱动版本无关 | | ||
| 86 | +| FINDING E | ddl-auto=update 静默失效(HHH000339:information_schema.KEYWORDS 不存在(默认配置 `dolphin.lower_case_table_names=0` 下大小写敏感:大写报错、小写可查)→ 不建表也不报错)。**设置 `dolphin.lower_case_table_names=1` 并持久化后实测 Hibernate 元数据探测恢复**;未配置时建议手工建表 + `ddl-auto: none` | | ||
| 87 | +| FINDING F | COM_STMT_SEND_LONG_DATA(服务端预编译)写入含 0x00 的二进制被按 C 字符串截断为前缀(无零字节 1MB 完整往返 MD5 一致;客户端预编译路径无此问题) | | ||
| 88 | +| FINDING G | allowMultiQueries 多语句后续 SELECT 结果集经 getMoreResults() 不可得(仅首语句结果 + 末 DML update count;DML 均落库) | | ||
| 89 | +| FINDING H | COM_STMT_SEND_LONG_DATA 无零字节 1MB:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,丢末块 8KB);9.7.0/26.7.0 档完整 | | ||
| 90 | +| FINDING I | COM_STMT_RESET(clearParameters 后复用):仅 8.0.28 档,第二次 executeUpdate 直接崩溃 openGauss 服务端进程(EOFException 断连、3308 不可达、可复现 3 次);8.0.33 及以上档正常 | | ||
| 91 | +| 方言 | `database-platform: org.opengauss.smoke.OpenGaussBMySQLDialect`(继承 MySQLDialect,声明无反斜杠转义语义,作为适配点) | | ||
| 92 | +| jakarta | Spring Boot 3.x 实体类 import 用 `jakarta.persistence.*`(不是 javax) | | ||
| 93 | + | ||
| 94 | +完整分析见 `docs/基于SpringDataJPA框架的openGauss数据库测试.md`。 | ||
| @@ -0,0 +1,300 @@ | |||
| 1 | +# 基于SpringDataJPA框架的openGauss数据库测试 | ||
| 2 | + | ||
| 3 | +版权所有 © 2026 openGauss社区 | ||
| 4 | + 您对“本文档”的复制、使用、修改及分发受知识共享(Creative Commons)署名—相同方式共享4.0国际公共许可协议(以下简称“CC BY-SA 4.0”)的约束。为了方便用户理解,您可以通过访问[*https://creativecommons.org/licenses/by-sa/4.0/*](https://creativecommons.org/licenses/by-sa/4.0/) 了解CC BY-SA 4.0的概要 (但不是替代)。CC BY-SA 4.0的完整协议内容您可以访问如下网址获取:[*https://creativecommons.org/licenses/by-sa/4.0/legalcode*](https://creativecommons.org/licenses/by-sa/4.0/legalcode)。 | ||
| 5 | + | ||
| 6 | +修订记录 | ||
| 7 | + | ||
| 8 | +| 日期 | 修订版本 | 修改描述 | 作者 | | ||
| 9 | +| --------- | -------- | ------------------------------------------------- | ---- | | ||
| 10 | +| 2026-08-28 | 1.0 | openGauss 对 Spring Data JPA + MySQL JDBC 的兼容性测试 | lele-2026 | | ||
| 11 | +| 2026-09-04 | 1.1 | 协议命令专项 9 用例实测结果回填(新增 FINDING F/G);LIKE escape 转义语义差异表述与 docs 仓指导文档对齐(escape '\\' 两个反斜杠、与 Plugin 仓 issue #504 同源);FINDING E 定位修正(默认配置 `dolphin.lower_case_table_names=0` 下大小写敏感,=1 持久化可恢复) | lele-2026 | | ||
| 12 | +| 2026-09-04 | 1.2 | 驱动矩阵全量补测:协议命令行为按驱动版本分三档(新增 FINDING H/I,与 #27 报告同源)——SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档)、COM_STMT_RESET 崩溃服务端进程(仅 8.0.28 档,可复现);9.7.0/26.7.0 档 19/19 全绿;矩阵结论由"行为逐项一致"改写为三档行为 | lele-2026 | | ||
| 13 | +| 2026-09-04 | 1.3 | 转义字符问题不再以 Bug 描述:全文统一为"转义语义差异(已知差异,与 Plugin 仓 issue #504 同源)",与 docs 仓指导文档口径一致(涉及摘要、3.1/3.3/4.1.1/4.2/5.1/5.2/6.1/6.2/7.1/7.2/7.3、思维导图、README 与测试代码注释) | lele-2026 | | ||
| 14 | +| 2026-09-04 | 1.4 | 删除 7.1 遗留问题列表(内部跟踪事项不再在公开报告列出,各差异/FINDING 的现状与规避已完整记录于 3.3/5.2;与 #27 报告同步处理);附件 7.2/7.3 重编号为 7.1/7.2;环境表移除内网 IP;附件表移除"原始日志备存"行 | lele-2026 | | ||
| 15 | + | ||
| 16 | +[TOC] | ||
| 17 | + | ||
| 18 | +**关键词**: | ||
| 19 | + | ||
| 20 | +Spring Data JPA,Hibernate,MySQL JDBC,openGauss 数据库,B 兼容模式,dolphin 插件,兼容性测试 | ||
| 21 | + | ||
| 22 | +**Abstract 摘要**: | ||
| 23 | + | ||
| 24 | +本测试验证使用 MySQL JDBC 驱动(mysql-connector-j)连接 openGauss B 兼容模式数据库,在 Spring Data JPA(Hibernate 6.6)框架下的兼容性。测试覆盖已知差异回归、ddl-auto 行为、类型映射、分页与批量、冒烟、协议命令六个方面共 19 个用例,并扫描 7 个驱动版本验证版本边界。结果显示:Spring Data JPA 在 openGauss B 模式下类型映射、主键回填、分页、批量、事务等核心链路均可正常工作;发现并定位 6 个问题——LIKE 模糊查询 escape 转义语义差异(已有 workaround,与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)、FINDING E(ddl-auto=update 静默失效,根因为默认配置 `dolphin.lower_case_table_names=0` 下 information_schema 表名大小写敏感(Hibernate 发大写 KEYWORDS 报不存在,小写 keywords 实测可查),设置 =1 并持久化后实测恢复)、FINDING F(COM_STMT_SEND_LONG_DATA 二进制参数遇 0x00 截断)、FINDING G(多语句结果返回结构不完整)、FINDING H(SEND_LONG_DATA 1MB 丢末块 8KB,8.0.28~8.4.0 档)与 FINDING I(COM_STMT_RESET 崩溃服务端进程,仅 8.0.28 档)。该转义差异与 FINDING E/F/G 均与驱动版本无关,根源在服务端;FINDING H/I 为服务端缺陷、触发条件依赖驱动版本。 | ||
| 25 | + | ||
| 26 | +**缩略语清单:** | ||
| 27 | + | ||
| 28 | +| 缩略语 | 英文全名 | 中文解释 | | ||
| 29 | +| ------ | --------------------------- | -------------- | | ||
| 30 | +| JPA | Jakarta Persistence API | Java 持久化 API | | ||
| 31 | +| ORM | Object Relational Mapping | 对象关系映射 | | ||
| 32 | +| JPQL | Jakarta Persistence Query Language | JPA 查询语言 | | ||
| 33 | +| ddl-auto | Hibernate DDL 自动管理配置 | 建表策略配置 | | ||
| 34 | + | ||
| 35 | +*** | ||
| 36 | + | ||
| 37 | +# 1 概述 | ||
| 38 | + | ||
| 39 | +本报告针对 Spring Data JPA 框架(Hibernate 6 实现)配合 MySQL JDBC 驱动与 openGauss B 兼容模式数据库的兼容性测试活动进行总结。Spring Data JPA 是 Spring Boot 生态最常用的 ORM 方案,其兼容性风险集中在方言适配、元数据探测、主键生成与查询渲染四个环节,本测试针对性地覆盖了这些风险点。 | ||
| 40 | + | ||
| 41 | +# 2 测试版本说明 | ||
| 42 | + | ||
| 43 | +## 2.1 测试版本信息 | ||
| 44 | + | ||
| 45 | +### 2.1.1 被测版本 | ||
| 46 | + | ||
| 47 | +| 版本名称 | 软件包名称 | 测试起始时间 | 测试结束时间 | 测试人员 | | ||
| 48 | +| ---------- | ----------------------------- | ------------ | ------------ | -------- | | ||
| 49 | +| Spring Boot | spring-boot-starter-data-jpa 3.5.16 | 2026-08-28 | 2026-08-28 | lele-2026 | | ||
| 50 | +| Hibernate | hibernate-core 6.6.53(Boot BOM 管理) | 2026-08-28 | 2026-08-28 | lele-2026 | | ||
| 51 | +| mysql-connector-j | 9.7.0(Boot BOM 默认) | 2026-08-28 | 2026-08-28 | lele-2026 | | ||
| 52 | +| openGauss | openGauss 7.0.0-RC3(源码编译,commit fe6f6e4b) | 2026-08-28 | 2026-08-28 | lele-2026 | | ||
| 53 | + | ||
| 54 | +> 说明:任务要求基于 openGauss 7.0.0-LTS,实测环境为 7.0.0-RC3(与 issue 开发指导文档同版本),正式版发布后建议回归。 | ||
| 55 | +> | ||
| 56 | +> 说明:被测二进制基于 7.0.0-RC3(commit fe6f6e4b)的本地源码工作副本编译:dolphin 插件为本地 contrib/dolphin 副本,包含此前 round() 支持 blob 任务的本地改动(已核验二进制中存在 round(binary/blob) 系列函数签名),contrib/timescaledb 未参与编译。上述差异均不在本测试覆盖的 MySQL 协议 / JDBC / 类型映射 / 事务路径上,不影响测试结论。 | ||
| 57 | + | ||
| 58 | +### 2.1.2 配套测试的版本 | ||
| 59 | + | ||
| 60 | +| 版本名称 | 配套版本 | 版本说明 | | ||
| 61 | +| -------- | --------------- | ----------------------------------------------- | | ||
| 62 | +| JDK | 17.0.15(Liberica) | Spring Boot 3.x 要求 JDK 17+ | | ||
| 63 | +| Maven | 3.9.14 | 构建工具 | | ||
| 64 | +| 驱动矩阵 | 7 个版本 | 8.0.28(旧坐标)/ 8.0.33 / 8.1.0 / 8.3.0 / 8.4.0 / 9.7.0 / 26.7.0 | | ||
| 65 | + | ||
| 66 | +## 2.2 测试环境描述 | ||
| 67 | + | ||
| 68 | +### 2.2.1 环境硬件信息 | ||
| 69 | + | ||
| 70 | +| 环境信息 | 硬件配置信息 | 备注 | | ||
| 71 | +| -------- | ------------------------------------------------------------ | ----------------- | | ||
| 72 | +| 开发机 | OS:Windows 11<br />JDK 17.0.15<br />Maven 3.9.14 | 测试代码执行环境 | | ||
| 73 | +| 虚拟机 | OS:openEuler 20.03 LTS(x86_64)<br />openGauss 源码编译安装(debug) | 数据库环境 | | ||
| 74 | + | ||
| 75 | +### 2.2.2 虚拟化平台 | ||
| 76 | + | ||
| 77 | +| 虚拟化平台 | 版本说明 | | ||
| 78 | +| ------------------ | -------- | | ||
| 79 | +| VMware Workstation Pro | NAT 网络模式,Windows 开发机可直连虚拟机 | | ||
| 80 | + | ||
| 81 | +### 2.2.3 OS版本 | ||
| 82 | + | ||
| 83 | +| 操作系统 | OS版本 | 版本说明 | | ||
| 84 | +| --------- | ----------- | ------------------------ | | ||
| 85 | +| openEuler | 20.03 (LTS) | x86_64 版本 | | ||
| 86 | + | ||
| 87 | +### 2.2.4 数据库环境配置 | ||
| 88 | + | ||
| 89 | +| 配置项 | 值 | | ||
| 90 | +| ----------------------------- | ----------------------------------------- | | ||
| 91 | +| 协议开关 | `enable_dolphin_proto = on` | | ||
| 92 | +| MySQL 协议端口 | `dolphin_server_port = 3308` | | ||
| 93 | +| 默认数据库 | `dolphin.default_database_name = 'proto_test_db'` | | ||
| 94 | +| 测试数据库 | `proto_test_db`(DBCOMPATIBILITY 'B') | | ||
| 95 | +| 测试用户 | `mysql_test_db`(已执行 `set_native_password` 原生密码认证) | | ||
| 96 | +| 客户端接入认证 | pg_hba.conf:`host all mysql_test_db 0.0.0.0/0 sha256` | | ||
| 97 | +| 测试表 | `` `user` ``(预置 3 行)、t_jpa_map(类型映射)、t_jpa_ddl(ddl-auto 专项)、jpa_cmd(协议命令专项,含 LONGBLOB 列,预置 500 行) | | ||
| 98 | + | ||
| 99 | +# 3 版本概要测试结论、关键风险和规避措施 | ||
| 100 | + | ||
| 101 | +## 3.1 测试结论总结 | ||
| 102 | + | ||
| 103 | +本测试共设计 19 个测试用例,覆盖六个方面:已知差异回归(2)、ddl-auto 专项(2)、类型映射(1)、分页与批量(2)、冒烟(3)、协议命令专项(9)。**19 个用例在 BOM 默认档(9.7.0)全部通过**(已知差异用例的"通过"= 复现了预期中的当前差异行为)。 | ||
| 104 | + | ||
| 105 | +核心结论: | ||
| 106 | + | ||
| 107 | +1. Spring Data JPA 在 openGauss B 模式下的核心链路——类型映射、IDENTITY 主键回填、分页、批量保存、事务回滚——全部可用; | ||
| 108 | +2. **LIKE 模糊查询 escape 转义语义差异**:Hibernate 6.6 默认在所有 LIKE 谓词后追加 `escape '\\'`(两个反斜杠),MySQL 中 `'\\'` 是 1 个字符,而 B 模式按标准 SQL 把 `'\\'` 视为 2 个字符,导致 `invalid escape string`。规避:JPQL 显式 `escape '!'`。该差异的根源是 B 模式不支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin 仓 issue #504),与驱动版本无关; | ||
| 109 | +3. **FINDING E(ddl-auto 静默失效)**:Hibernate 启动时查询 `information_schema.KEYWORDS` 失败(仅警告)→ ddl-auto=update 不做任何建表动作且不报错,运行时才暴露 `relation does not exist`。根因为默认配置(`dolphin.lower_case_table_names=0`)下大小写敏感(视图存在、小写可查)。**设置 `dolphin.lower_case_table_names=1` 并持久化后实测大写 KEYWORDS 可查、Hibernate 元数据探测恢复**;未配置时建议 B 模式手工建表 + `ddl-auto=none`; | ||
| 110 | +4. **FINDING F / G(协议命令专项)**:COM_STMT_SEND_LONG_DATA 写入含 0x00 的二进制被按 C 字符串截断为前缀;allowMultiQueries 多语句后续 SELECT 结果集经 getMoreResults() 不可得(均详见 5.2.2,回归断言); | ||
| 111 | +5. **协议命令行为按驱动版本分三档**:8.0.28 档 COM_STMT_RESET 可复现崩溃服务端进程(FINDING I)、8.0.28~8.4.0 档 SEND_LONG_DATA 1MB 丢末块 8KB(FINDING H)、9.7.0/26.7.0 档 19/19 全部通过;该转义差异与 FINDING E/F/G 在 7 档下行为一致、与驱动版本无关(详见 4.2)。 | ||
| 112 | + | ||
| 113 | +## 3.2 约束说明 | ||
| 114 | + | ||
| 115 | +1. ddl-auto=create 模式未测(会 drop+create 全部映射表,破坏现有数据);其行为预计与 update 一致(同样依赖元数据探测); | ||
| 116 | +2. Hibernate 批量参数(hibernate.jdbc.batch_size / order_inserts)未专项计时,saveAll 仅验证功能正确性; | ||
| 117 | +3. 测试基于 openGauss 7.0.0-RC3,正式 7.0.0-LTS 发布后建议用同一套用例回归。 | ||
| 118 | + | ||
| 119 | +## 3.3 关键风险和规避措施 | ||
| 120 | + | ||
| 121 | +| 风险 | 说明 | 规避措施 | | ||
| 122 | +| ---- | ---- | -------- | | ||
| 123 | +| 转义语义差异:LIKE 模糊查询报错 | 派生查询 / 隐式 escape 渲染 `escape '\\'`(两个反斜杠)→ `invalid escape string` | JPQL 显式 `escape '!'`(本工程 searchByName 为参考实现);根源是 B 模式不支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin 仓 issue #504);若服务端调整反斜杠转义语义(B 模式字符串字面量支持反斜杠转义)该差异可消除 | | ||
| 124 | +| FINDING E:ddl-auto=update 静默失效 | 容器启动成功但不建表,运行时才报错,排查困难 | **设置 `dolphin.lower_case_table_names=1` 并持久化后实测 Hibernate 元数据探测恢复**;未配置时 B 模式手工建表 + `ddl-auto=none`(或 validate) | | ||
| 125 | +| FINDING A 联动:元数据探测受限 | getTables / prepareCall 依赖 information_schema.TABLES/PARAMETERS | 同 #27 报告 FINDING A:默认配置(`dolphin.lower_case_table_names=0`)下大小写敏感,设置 =1 并持久化后实测 `DatabaseMetaData.getTables` 恢复正常;ORM 场景尽量避免依赖元数据探测的功能 | | ||
| 126 | +| FINDING F:SEND_LONG_DATA 二进制参数遇 0x00 截断 | 服务端预编译 + setBlob 流式写入 LONGBLOB 时,二进制遇第一个 0x00 字节被按 C 字符串截断为前缀(无零字节 1MB 完整往返 MD5 一致;客户端预编译文本协议路径无此问题) | 服务端预编译路径避免写入含 0x00 的二进制(用客户端预编译文本协议路径);服务端修复后回归 | | ||
| 127 | +| FINDING G:多语句结果返回结构不完整 | allowMultiQueries=true 多语句执行时 getMoreResults() 取不到后续 SELECT 结果集(仅首语句结果 + 末 DML update count;纯 SELECT 序列逐个正常;DML 均落库) | 逐条执行或仅依赖末条 update count;服务端修复后回归 | | ||
| 128 | +| FINDING H:SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档,与 #27 报告同源) | 服务端预编译 + setBlob 写入 1MB 全 0x41(无零字节)数据时,8.0.28/8.0.33/8.1.0/8.3.0/8.4.0 档读回仅 1040384 字节 = 127×8192(恰少末块 8192),MD5 校验失败;9.7.0/26.7.0 档完整往返 | 8.0.28~8.4.0 档服务端预编译路径避免写入 8192 倍数大小的大二进制(改用 9.7.0+ 驱动或客户端预编译文本协议路径);服务端修复后回归 | | ||
| 129 | +| FINDING I:COM_STMT_RESET 崩溃服务端进程(仅 8.0.28 档,与 #27 报告同源) | 8.0.28 档 clearParameters 后二次 executeUpdate(驱动先发 COM_STMT_RESET)直接崩溃 openGauss 服务端进程:客户端报 `EOFException: read 0 bytes before connection was unexpectedly lost`,3308 端口不可达需人工重启;单测可复现(两个工程同构);8.0.33 及以上档同一序列正常 | 8.0.28 档避免服务端预编译语句 clearParameters 后复用(新建语句或换 8.0.33+ 驱动);服务端修复后回归 | | ||
| 130 | + | ||
| 131 | +# 4 版本详细测试结论 | ||
S | |||
| 132 | + | ||
| 133 | +## 4.1 特性测试结论 | ||
| 134 | + | ||
| 135 | +### 4.1.1 已知差异回归(2 用例,符合预期) | ||
| 136 | + | ||
| 137 | +| 用例 | 结果 | | ||
| 138 | +| ---- | ---- | | ||
| 139 | +| K1:派生 LIKE(findByNameLike) | ✓ 复现转义语义差异:`invalid escape string`(Hibernate 6.6 默认追加 `escape '\\'`(两个反斜杠),MySQL 中 `'\\'`=1 字符,B 模式按标准 SQL 视 `'\\'` 为 2 字符;根源是 B 模式不支持反斜杠作为字符串转义字符,与 Plugin 仓 issue #504 同源)。**本用例为回归断言**:服务端行为调整后测试将自动失败,提醒移除 workaround 并更新文档 | | ||
| 140 | +| K2:JPQL 显式 `escape '!'`(searchByName) | ✓ workaround 稳定可用(分页版模糊查询) | | ||
| 141 | + | ||
| 142 | +### 4.1.2 ddl-auto 专项(2 用例) | ||
| 143 | + | ||
| 144 | +| 用例 | 结果 | | ||
| 145 | +| ---- | ---- | | ||
| 146 | +| D1:ddl-auto=update | **FINDING E**:容器启动成功,但缺失表 t_jpa_ddl 未被创建,不报错不警告(静默失效)。日志证据:`HHH000339: Could not obtain connection metadata ... relation "information_schema.KEYWORDS" does not exist on primary`(默认配置 `dolphin.lower_case_table_names=0` 下大小写敏感:视图存在、小写 keywords 实测可查)→ 元数据获取失败 → 无法做"现有 schema vs 实体"对比 → update 什么都不做。设置 `dolphin.lower_case_table_names=1` 并持久化后实测大写 KEYWORDS 可查、元数据探测恢复 | | ||
| 147 | +| D2:ddl-auto=validate | 启动校验未报错(但元数据同样缺失,校验深度存疑,如实表述"未报错") | | ||
| 148 | + | ||
| 149 | +### 4.1.3 类型映射(1 用例,通过) | ||
| 150 | + | ||
| 151 | +| 用例 | 结果 | | ||
| 152 | +| ---- | ---- | | ||
| 153 | +| M1:五类型往返 + 主键回填 | ✓ BigDecimal / LocalDate / LocalDateTime / Boolean↔TINYINT(1) / String 写入读出一致;save 后 IDENTITY 主键回填正常(getGeneratedKeys 链路可用) | | ||
| 154 | + | ||
| 155 | +### 4.1.4 分页与批量(2 用例,通过) | ||
| 156 | + | ||
| 157 | +| 用例 | 结果 | | ||
| 158 | +| ---- | ---- | | ||
| 159 | +| P1:PageRequest 多页遍历 | ✓ LIMIT/OFFSET 与 count 查询正常 | | ||
| 160 | +| P2:saveAll(50) | ✓ 批量保存 50 条主键逐个回填 | | ||
| 161 | + | ||
| 162 | +### 4.1.5 冒烟(3 用例,通过) | ||
| 163 | + | ||
| 164 | +| 用例 | 结果 | | ||
| 165 | +| ---- | ---- | | ||
| 166 | +| saveAndFind / pagination / transactionRollback | ✓ 全链路正常;@Transactional 异常回滚正常 | | ||
| 167 | + | ||
| 168 | +### 4.1.6 协议命令专项(9 用例) | ||
| 169 | + | ||
| 170 | +> 背景:评审意见要求增加协议命令基础功能测试(COM_STMT_PREPARE / COM_STMT_SEND_LONG_DATA / COM_STMT_EXECUTE,最好覆盖到每个 command)。MySQL 协议命令按"JDBC 是否有触发路径"处理:可触发的逐条实测(用例 C1-C9),无触发路径的在下方说明。与 #27 报告(springboot)7.1-7.9 同一套设计。 | ||
| 171 | + | ||
| 172 | +| 协议命令 | JDBC 触发方式 | 用例 | 结果 | | ||
| 173 | +| -------- | ------------- | ---- | ---- | | ||
| 174 | +| COM_QUERY | 普通 Statement 执行 SQL(文本协议,建表/插入/查询均走此命令) | C1 | ✓ 插入 2 行后计数 +2(文本协议插入/查询/计数正常) | | ||
| 175 | +| COM_INIT_DB | url 指定 database 建立连接(握手携带);setCatalog + useLocalSessionState=false | C2 | ✓ 连接 catalog=测试库;setCatalog 切换后查询正常 | | ||
| 176 | +| COM_PING | Connection.isValid(2) | C3 | ✓ isValid=true | | ||
| 177 | +| COM_STMT_PREPARE | useServerPrepStmts=true 下 prepareStatement | C4 | ✓ 服务端预编译插入+查询往返正常(cachePrepStmts=false 确保真实发出) | | ||
| 178 | +| COM_STMT_EXECUTE | 服务端预编译语句 executeQuery/executeUpdate | C4 | ✓ 同 C4 | | ||
| 179 | +| COM_STMT_CLOSE | 服务端预编译语句 close | C4 | ✓ 同 C4(语句关闭无异常) | | ||
| 180 | +| COM_STMT_SEND_LONG_DATA | 服务端预编译 + setBlob 流式写入 1MB LONGBLOB(MD5 往返校验) | C5 | ✓ 链路可用(9.7.0 基线),但发现 FINDING F:无零字节 1MB(全 0x41)完整往返 MD5 一致;含 0x00 的 200 字节读回仅 150 字节(= 第一个 0x00 前的前缀)——服务端对二进制参数按 C 字符串截断(与 #27 报告 FINDING F 同源,详见 5.2.2,回归断言)。驱动矩阵补测发现 FINDING H:8.0.28~8.4.0 档无零字节 1MB 读回仅 1040384 字节(丢末块 8KB,详见 4.2/5.2.2) | | ||
| 181 | +| COM_STMT_RESET | clearParameters 后复用同一预编译语句 | C6 | ✓(8.0.33 及以上档)clearParameters 后复用同一语句插入 2 行均生效;但 8.0.28 档此序列(COM_STMT_RESET)可复现崩溃服务端进程(FINDING I,详见 5.2.2) | | ||
| 182 | +| COM_STMT_FETCH | useCursorFetch=true + setFetchSize(100) | C7 | ✓ 驱动层无报错、完整读回 500 行;协议层是否真逐批返回需服务端日志佐证(口径同 #27 报告 6.1,按"驱动层可用、协议层待确认"记录) | | ||
| 183 | +| COM_SET_OPTION | allowMultiQueries=true 多语句执行 | C8 | ✓ 参数生效、多语句 DML 全部落库,但发现 FINDING G:execute() 仅返回首语句 update count,getMoreResults() 取不到后续 SELECT 结果集(后续 SELECT 被丢弃;纯 SELECT 序列逐个正常)(与 #27 报告 FINDING G 同源,详见 5.2.2,回归断言) | | ||
| 184 | +| COM_QUIT | Connection.close | C9 | ✓ close 后 isClosed=true | | ||
| 185 | + | ||
| 186 | +JDBC 无触发路径的命令:COM_FIELD_LIST(已被查询结果集元数据取代)、COM_PROCESS_INFO / COM_STATISTICS / COM_PROCESS_KILL(管理类命令)、COM_CHANGE_USER、COM_REFRESH 等,MySQL JDBC 驱动不提供对应 API,无法从应用层触发,不纳入用例;本专项已覆盖**驱动可触发的全部协议命令**。 | ||
| 187 | + | ||
| 188 | +## 4.2 兼容性测试结论(驱动版本矩阵) | ||
| 189 | + | ||
| 190 | +同一套 19 用例扫描 7 档驱动(Maven profile 切换,同一服务端环境): | ||
| 191 | + | ||
| 192 | +| 驱动版本 | 坐标 | 结果 | | ||
| 193 | +| -------- | ---- | ---- | | ||
| 194 | +| 8.0.28(文档基准) | mysql:mysql-connector-java(旧坐标) | 协议命令类 1 失败 + 6 错误:SEND_LONG_DATA 丢末块 8KB(FINDING H)、COM_STMT_RESET 崩溃服务端进程(FINDING I,后续用例全部失联,需人工重启服务端);其余 12 用例通过(含 K1/D1 复现) | | ||
| 195 | +| 8.0.33 | com.mysql:mysql-connector-j(新坐标) | 19 用例中 1 失败(FINDING H:SEND_LONG_DATA 丢末块 8KB),其余 18 通过(COM_STMT_RESET 正常、服务端未崩溃) | | ||
| 196 | +| 8.1.0 | 同上 | 同上(1 失败 + 18 通过) | | ||
| 197 | +| 8.3.0 | 同上 | 同上(1 失败 + 18 通过) | | ||
| 198 | +| 8.4.0 | 同上 | 同上(1 失败 + 18 通过) | | ||
| 199 | +| 9.7.0(Boot 3.5.16 BOM 默认) | 同上 | 19/19 通过 | | ||
| 200 | +| 26.7.0(最新) | 同上 | 19/19 通过 | | ||
| 201 | + | ||
| 202 | +7 档驱动的核心链路(类型映射/主键回填/分页/批量/事务)与 K1 复现转义语义差异、D1 复现 FINDING E、多语句结果结构(FINDING G)**逐项一致**——这些问题均与驱动版本无关,根源在服务端;协议命令专项按驱动版本分**三档**:8.0.28 崩溃 / 8.0.33~8.4.0 丢 8KB / 9.7.0 及以上正常(FINDING H/I,详见 5.2.2)。与 #27 报告 8 档矩阵结论互相印证。 | ||
| 203 | + | ||
| 204 | +# 5 测试对象质量评估 | ||
| 205 | + | ||
| 206 | +## 5.1 覆盖率分析 | ||
| 207 | + | ||
| 208 | +| 覆盖维度 | 覆盖情况 | | ||
| 209 | +| -------- | -------- | | ||
| 210 | +| 功能覆盖 | 已知差异回归 / ddl-auto / 类型映射+主键回填 / 分页 / 批量 / 事务回滚 / 协议命令,共 19 用例 | | ||
| 211 | +| 风险点覆盖 | ORM 四大风险环节全覆盖:方言(MySQLDialect+自定义适配点)、元数据探测(D1 捕获 HHH000339)、主键生成(IDENTITY 回填)、查询渲染(K1 捕获 escape 渲染差异) | | ||
| 212 | +| 版本覆盖 | 驱动 7 档(8.0.28 ~ 26.7.0,覆盖新旧两套 Maven 坐标) | | ||
| 213 | +| 未覆盖项 | ddl-auto=create(破坏性)、Hibernate 批量参数性能、二级缓存、锁模式、JDBC 无触发路径的协议命令(见 4.1.6) | | ||
| 214 | + | ||
| 215 | +## 5.2 缺陷统计和分析 | ||
| 216 | + | ||
| 217 | +### 5.2.1 缺陷统计 | ||
| 218 | + | ||
| 219 | +| 类别 | 数量 | 说明 | | ||
| 220 | +| ---- | ---- | ---- | | ||
| 221 | +| 服务端差异(FINDING/已知差异) | 6 | LIKE escape 转义语义差异(与 issue #504 同源)+ FINDING E(ddl-auto 静默失效)+ FINDING F(SEND_LONG_DATA 0x00 截断)+ FINDING G(多语句结果结构不完整)+ FINDING H(SEND_LONG_DATA 1MB 丢末块 8KB,8.0.28~8.4.0 档)+ FINDING I(COM_STMT_RESET 崩溃,仅 8.0.28 档) | | ||
| 222 | +| 崩溃类缺陷 | 1 | FINDING I(8.0.28 档 COM_STMT_RESET 可复现崩溃 openGauss 服务端进程,与 #27 报告同源) | | ||
| 223 | + | ||
| 224 | +### 5.2.2 缺陷列表 | ||
| 225 | + | ||
| 226 | +| 编号 | 现象 | 定位 | 严重程度 | 规避措施 | | ||
| 227 | +| ---- | ---- | ---- | -------- | -------- | | ||
| 228 | +| 转义语义差异(#504) | 派生 LIKE 查询报 `invalid escape string` | Hibernate 6.6 默认渲染 `escape '\\'`(两个反斜杠);MySQL 中 `'\\'`=1 字符,B 模式按标准 SQL 把 `'\\'` 当 2 字符。根源是 B 模式不支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)。与驱动版本无关(8.0.28/9.7.0 同错) | 高(模糊查询是高频功能) | JPQL 显式 `escape '!'`;服务端候选调整:B 模式字符串字面量支持反斜杠转义(已 gsql 取证,见 7.2 附件) | | ||
| 229 | +| FINDING E | ddl-auto=update 容器启动成功但表未创建,静默失效 | Hibernate 查 information_schema.KEYWORDS 失败(HHH000339 仅警告)→ schema 对比无法进行。与 FINDING A(TABLES/PARAMETERS)同源:默认配置(`dolphin.lower_case_table_names=0`)下大小写敏感(视图本身均存在,小写可查) | 高(对按 MySQL 教程配置 ddl-auto=update 的用户是隐性陷阱) | **`dolphin.lower_case_table_names=1` 并持久化(实测 Hibernate 元数据探测恢复)**;未配置时手工建表 + ddl-auto=none | | ||
| 230 | +| FINDING F | COM_STMT_SEND_LONG_DATA 写入含 0x00 的二进制被截断 | 服务端预编译路径对二进制参数按 C 字符串处理:遇第一个 0x00 截断为前缀(200 字节读回 150 = 0x00 位置);无零字节 1MB 完整往返(MD5 一致);客户端预编译(文本协议)路径无此问题 | 中(影响服务端预编译 + 二进制大对象写入的正确性) | 服务端预编译路径避免写入含 0x00 的二进制(用客户端预编译文本协议路径) | | ||
| 231 | +| FINDING G | 多语句后续 SELECT 结果集经 getMoreResults() 不可得 | `allowMultiQueries=true` 下结果返回结构不完整:仅回传首语句结果 + 末 DML update count,中间结果全部丢弃(纯 SELECT 序列逐个正常);各语句 DML 均已落库 | 中(影响依赖多结果集解析的应用/框架) | 逐条执行或仅依赖末条 update count | | ||
| 232 | +| FINDING H | SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档,与 #27 报告同源) | 服务端预编译 + setBlob 写入 1MB 全 0x41(无零字节)数据时,8.0.28~8.4.0 档读回仅 1040384 字节 = 127×8192(恰少末块 8192),MD5 失败(断言行固定 `expected: 1048576 but was: 1040384`);9.7.0/26.7.0 档完整往返。与驱动分块方式(8192 对齐)相关,缺陷在服务端 | 高(服务端预编译 + 大二进制写入数据静默丢失) | 8.0.28~8.4.0 档避免服务端预编译写入 8192 倍数大小的大二进制(改用 9.7.0+ 驱动或客户端预编译文本协议路径) | | ||
| 233 | +| FINDING I | COM_STMT_RESET 崩溃服务端进程(仅 8.0.28 档,与 #27 报告同源) | 8.0.28 档 clearParameters 后二次 executeUpdate(驱动先发 COM_STMT_RESET)直接崩溃 openGauss 服务端:客户端报 `EOFException: read 0 bytes before connection was unexpectedly lost`,服务端日志戛然而止(无 PANIC、无正常关闭流程),3308 不可达需人工重启;单测可复现(两个工程同构);8.0.33+ 档同一序列正常 | 严重(单次 JDBC 操作即可打崩整个服务端,可被任意客户端触发拒绝服务) | 8.0.28 档避免服务端预编译语句 clearParameters 后复用(新建语句或换 8.0.33+ 驱动);服务端修复后回归 | | ||
| 234 | + | ||
| 235 | +# 6 测试过程评估 | ||
| 236 | + | ||
| 237 | +## 6.1 测试策略回顾 | ||
| 238 | + | ||
| 239 | +先以 Boot BOM 默认驱动跑通冒烟建立基线,再针对 ORM 高风险环节(方言/元数据/主键/查询渲染)设计专项用例;对发现的转义语义差异,先在 8.0.28 文档基准驱动上复现以排除驱动因素,再下钻到 SQL 层用 gsql 取证定位服务端语义差异;最后以驱动矩阵确认问题归属。 | ||
| 240 | + | ||
| 241 | +## 6.2 测试设计评估 | ||
| 242 | + | ||
| 243 | +1. **已知差异回归测试方法论**:K1 把"当前已知的差异行为"写成断言——将来服务端行为调整后测试自动失败,提示更新文档、移除 workaround,让测试成为文档的守护者; | ||
| 244 | +2. D1 采用手工拉起独立 Spring 容器的写法(SpringApplicationBuilder),避免 @SpringBootTest 在容器异常时把测试直接标 ERROR,从而能观察"启动成功但表未创建"的中间状态——这是捕获 FINDING E 的关键设计; | ||
| 245 | +3. 驱动矩阵与 #27 报告共享同一方法论,两个任务结论互相印证; | ||
| 246 | +4. 协议命令专项(C1-C9)与 #27 报告 7.1-7.9 同一套设计,覆盖 JDBC 可触发的全部协议命令(COM_QUERY/INIT_DB/PING/STMT_*/SET_OPTION/QUIT),无触发路径命令在 4.1.6 说明原因;C5/C8 将 FINDING F/G 的"当前错误行为"写成回归断言(服务端修复后自动失败提醒更新报告),与 K1/D1 同一方法论。 | ||
| 247 | + | ||
| 248 | +## 6.3 测试执行评估 | ||
| 249 | + | ||
| 250 | +### 6.3.1 测试执行统计数据 | ||
| 251 | + | ||
| 252 | +| 项目 | 数据 | | ||
| 253 | +| ---- | ---- | | ||
| 254 | +| 用例总数 | 19 | | ||
| 255 | +| 驱动档位 | 7(含文档基准 8.0.28 与最新 26.7.0) | | ||
| 256 | +| 执行轮次 | 7 档各一轮全量回归(8.0.28 档因 FINDING I 服务端崩溃分两段执行:协议类 9 + 其余 10)+ 若干轮开发调试与单用例复测 | | ||
| 257 | +| 总执行用例次 | 19 × 7 = 133(含 8.0.28 档两段) | | ||
| 258 | + | ||
| 259 | +### 6.3.2 测试用例执行结果统计数据 | ||
| 260 | + | ||
| 261 | +| 结果 | 数量 | | ||
| 262 | +| ---- | ---- | | ||
| 263 | +| 通过 | 122(8.0.28 档 12 / 8.0.33~8.4.0 档各 18 / 9.7.0 档 19 / 26.7.0 档 19) | | ||
| 264 | +| 失败 | 5(FINDING H 断言失败:8.0.28 档 1 + 8.0.33~8.4.0 档各 1) | | ||
| 265 | +| 错误 | 6(FINDING I 服务端崩溃后失联:8.0.28 档 6) | | ||
| 266 | + | ||
| 267 | +# 7 附件 | ||
| 268 | + | ||
| 269 | +## 7.1 附件1:相关产物 | ||
| 270 | + | ||
| 271 | +| 产物 | 位置 | | ||
| 272 | +| ---- | ---- | | ||
| 273 | +| 测试代码(19 用例) | 本目录 `src/test/java/org/opengauss/smoke/jpacases/` | | ||
| 274 | +| 测试思维导图 | 本目录 `docs/测试思维导图-SpringDataJPA连接openGaussB库.{mm,md,html}` | | ||
| 275 | +| 转义语义差异取证材料 | 本报告 7.2 节(gsql 三条取证 SQL 输出,服务端直连复现,不依赖 Java 组件) | | ||
| 276 | +| 指导文档 | docs 仓 `docs/zh/mysql_compatibility_developer_guide/springdatajpa_development.md`(轻量版 `docs-lite/zh/...` 同路径) | | ||
| 277 | + | ||
| 278 | +## 7.2 附件2:LIKE escape 转义语义差异 gsql 取证输出(与 Plugin 仓 issue #504 同源) | ||
| 279 | + | ||
| 280 | +> 取证环境:openGauss 7.0.0-RC3(commit fe6f6e4b,debug 编译),B 库 proto_test_db,通过 gsql(postgres 协议 5432)直连复现——不依赖任何 Java 组件,与驱动版本无关。 | ||
| 281 | + | ||
| 282 | +```sql | ||
| 283 | +proto_test_db=# SELECT '\\'; | ||
| 284 | + \\ | ||
| 285 | +---- | ||
| 286 | + \\ | ||
| 287 | +(1 row) | ||
| 288 | + | ||
| 289 | +proto_test_db=# SELECT 'abc' LIKE 'a%' ESCAPE '\\'; | ||
| 290 | +ERROR: invalid escape string | ||
| 291 | +HINT: Escape string must be empty or one character. | ||
| 292 | + | ||
| 293 | +proto_test_db=# SELECT 'abc' LIKE 'a%' ESCAPE '!'; | ||
| 294 | + ?column? | ||
| 295 | +---------- | ||
| 296 | + t | ||
| 297 | +(1 row) | ||
| 298 | +``` | ||
| 299 | + | ||
| 300 | +三条 SQL 说明:① openGauss B 模式按标准 SQL 解释字符串字面量,`'\\'` 是两个字符的反斜杠(MySQL 反斜杠转义语义下是单字符);② LIKE 的 ESCAPE 子句要求转义符为空或单字符,两字符的 `'\\'` 触发 `invalid escape string`;③ 换用单字符 `!` 作为转义符即可正常执行。Hibernate 6.6 默认给 LIKE 追加 `escape '\\'`,故派生模糊查询(如 `findByNameLike`)在 B 模式下全部报错(该差异的回归用例见 4.1.1 节),JPQL 显式 `escape '!'` 可规避。该差异的根源是 B 模式不支持反斜杠作为字符串转义字符,与社区已记录的已知差异同源(Plugin 仓 issue #504)。 | ||
| @@ -0,0 +1,123 @@ | |||
| 1 | + | ||
| 2 | +<html lang="zh-CN"> | ||
| 3 | +<head> | ||
| 4 | +<meta charset="UTF-8"> | ||
| 5 | +<title>测试思维导图:SpringDataJPA + MySQL JDBC 连接 openGauss B 库</title> | ||
| 6 | +<style> | ||
| 7 | + body { font-family: "Microsoft YaHei", "PingFang SC", sans-serif; background: #f4f6f8; margin: 0; padding: 24px; } | ||
| 8 | + h1 { font-size: 18px; text-align: center; color: #2c3e50; margin: 8px 0 4px; } | ||
| 9 | + p.sub { text-align: center; color: #7f8c8d; font-size: 13px; margin: 0 0 20px; } | ||
| 10 | + ul.map-tree, ul.map-tree ul { list-style: none; } | ||
| 11 | + ul.map-tree { padding-left: 8px; } | ||
| 12 | + ul.map-tree ul { border-left: 2px solid #c8d1da; margin-left: 16px; padding-left: 18px; } | ||
| 13 | + ul.map-tree li { margin: 7px 0; position: relative; } | ||
| 14 | + ul.map-tree ul li::before { | ||
| 15 | + content: ""; position: absolute; left: -18px; top: 11px; | ||
| 16 | + width: 16px; border-top: 2px solid #c8d1da; | ||
| 17 | + } | ||
| 18 | + .root > .label { | ||
| 19 | + display: inline-block; background: #1f3a5f; color: #fff; font-weight: bold; | ||
| 20 | + padding: 10px 18px; border-radius: 22px; font-size: 16px; | ||
| 21 | + box-shadow: 0 3px 8px rgba(31,58,95,.25); | ||
| 22 | + } | ||
| 23 | + .branch > .label { | ||
| 24 | + display: inline-block; color: #fff; font-weight: bold; | ||
| 25 | + padding: 7px 14px; border-radius: 16px; font-size: 14px; | ||
| 26 | + } | ||
| 27 | + .leaf > .label { font-size: 13.5px; color: #34495e; padding: 2px 0; } | ||
| 28 | + .leaf-note > .label { font-size: 13px; color: #b03a2e; background: #fdecea; padding: 2px 8px; border-radius: 6px; } | ||
| 29 | + .b-env > .label { background: #7f8c8d; } | ||
| 30 | + .b-issue > .label { background: #2980b9; } | ||
| 31 | + .b-ddl > .label { background: #c0392b; } | ||
| 32 | + .b-map > .label { background: #27ae60; } | ||
| 33 | + .b-page > .label { background: #e67e22; } | ||
| 34 | + .b-smoke > .label { background: #16a085; } | ||
| 35 | + .b-proto > .label { background: #d35400; } | ||
| 36 | + .b-matrix > .label { background: #2c3e50; } | ||
| 37 | + .b-concl > .label { background: #1e8449; } | ||
| 38 | + .pass { color: #27ae60; font-weight: bold; } | ||
| 39 | + .foot { text-align: center; color: #95a5a6; font-size: 12px; margin-top: 24px; } | ||
| 40 | +</style> | ||
| 41 | +</head> | ||
| 42 | +<body> | ||
| 43 | +<h1>测试思维导图:SpringDataJPA + MySQL JDBC 连接 openGauss B 库(Issue #28)</h1> | ||
| 44 | +<p class="sub">19 个测试用例(BOM 默认 9.7.0 基线全通过)· 7 档驱动矩阵协议命令分三档 · 转义语义差异(#504)/ FINDING E / F / G / H / I 已记录</p> | ||
| 45 | +<ul class="map-tree"> | ||
| 46 | + <li class="root"><span class="label">SpringDataJPA + MySQL JDBC × openGauss B 库 测试</span> | ||
| 47 | + <ul> | ||
| 48 | + <li class="branch b-env"><span class="label">测试环境</span> | ||
| 49 | + <ul> | ||
| 50 | + <li class="leaf"><span class="label">openGauss 7.0.0-RC3(fe6f6e4b)B 库 proto_test_db / schema mysql_test_db(协议 3308)</span></li> | ||
| 51 | + <li class="leaf"><span class="label">Spring Boot 3.5.16 + Hibernate 6.6.53 + mysql-connector-j 9.7.0(BOM 默认)</span></li> | ||
| 52 | + <li class="leaf"><span class="label">方言 MySQLDialect(自定义 OpenGaussBMySQLDialect 保留适配点)</span></li> | ||
| 53 | + </ul> | ||
| 54 | + </li> | ||
| 55 | + <li class="branch b-issue"><span class="label">一、已知差异回归(2 用例 <span class="pass">✓</span>)</span> | ||
| 56 | + <ul> | ||
| 57 | + <li class="leaf-note"><span class="label">K1:派生 LIKE(findByNameLike)复现转义语义差异 —— invalid escape string(Hibernate 6.6 默认 escape '\\'(两个反斜杠),B 模式视 '\\' 为 2 字符;根源是 B 模式不支持反斜杠作为字符串转义字符,与 Plugin 仓 issue #504 同源)</span></li> | ||
| 58 | + <li class="leaf"><span class="label">K2:JPQL 显式 escape '!'(searchByName)可用 —— workaround 稳定,与驱动版本无关</span></li> | ||
| 59 | + <li class="leaf"><span class="label">方法论:K1 写成回归断言 —— 服务端行为调整后测试自动失败,提醒移除 workaround 并更新文档</span></li> | ||
| 60 | + </ul> | ||
| 61 | + </li> | ||
| 62 | + <li class="branch b-ddl"><span class="label">二、ddl-auto 专项(2 用例)</span> | ||
| 63 | + <ul> | ||
| 64 | + <li class="leaf-note"><span class="label">D1 FINDING E:ddl-auto=update 容器启动成功但表未创建(静默失效,不建表也不报错)</span></li> | ||
| 65 | + <li class="leaf"><span class="label">根因:默认配置 dolphin.lower_case_table_names=0 下 information_schema 表名大小写敏感(KEYWORDS 大写报不存在,小写可查)→ schema 对比失败</span></li> | ||
| 66 | + <li class="leaf"><span class="label">解法:dolphin.lower_case_table_names=1 并持久化后实测大写 KEYWORDS 可查、Hibernate 元数据探测恢复</span></li> | ||
| 67 | + <li class="leaf"><span class="label">实际影响:应用启动正常,运行时才报 relation does not exist</span></li> | ||
| 68 | + <li class="leaf"><span class="label">D2:ddl-auto=validate 未报错(元数据同样缺失,校验深度存疑,如实表述)</span></li> | ||
| 69 | + </ul> | ||
| 70 | + </li> | ||
| 71 | + <li class="branch b-map"><span class="label">三、类型映射(1 用例 <span class="pass">✓</span>)</span> | ||
| 72 | + <ul> | ||
| 73 | + <li class="leaf"><span class="label">BigDecimal / LocalDate / LocalDateTime / Boolean↔TINYINT(1) / String 往返一致</span></li> | ||
| 74 | + <li class="leaf"><span class="label">IDENTITY 主键回填(getGeneratedKeys 链路正常)</span></li> | ||
| 75 | + </ul> | ||
| 76 | + </li> | ||
| 77 | + <li class="branch b-page"><span class="label">四、分页与批量(2 用例 <span class="pass">✓</span>)</span> | ||
| 78 | + <ul> | ||
| 79 | + <li class="leaf"><span class="label">P1:PageRequest 多页遍历(LIMIT/OFFSET 与 count 查询正常)</span></li> | ||
| 80 | + <li class="leaf"><span class="label">P2:saveAll(50) 批量主键逐个回填</span></li> | ||
| 81 | + </ul> | ||
| 82 | + </li> | ||
| 83 | + <li class="branch b-smoke"><span class="label">五、冒烟(3 用例 <span class="pass">✓</span>)</span> | ||
| 84 | + <ul> | ||
| 85 | + <li class="leaf"><span class="label">saveAndFind / pagination / transactionRollback(@Transactional 回滚正常)</span></li> | ||
| 86 | + </ul> | ||
| 87 | + </li> | ||
| 88 | + <li class="branch b-proto"><span class="label">六、协议命令专项(9 用例 <span class="pass">✓</span>,评审意见落地,与 #27 报告 7.1-7.9 同设计)</span> | ||
| 89 | + <ul> | ||
| 90 | + <li class="leaf"><span class="label">C1 COM_QUERY / C2 COM_INIT_DB / C3 COM_PING</span></li> | ||
| 91 | + <li class="leaf"><span class="label">C4 COM_STMT_PREPARE / COM_STMT_EXECUTE / COM_STMT_CLOSE(useServerPrepStmts=true 全生命周期)</span></li> | ||
| 92 | + <li class="leaf"><span class="label">C5 COM_STMT_SEND_LONG_DATA:无零字节 1MB 完整往返(MD5 一致,9.7.0 基线)</span></li> | ||
| 93 | + <li class="leaf-note"><span class="label">FINDING H:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,丢末块 8KB);9.7.0/26.7.0 档完整</span></li> | ||
| 94 | + <li class="leaf-note"><span class="label">FINDING F:含 0x00 二进制被按 C 字符串截断为前缀(200 字节读回 150;客户端预编译路径无此问题)</span></li> | ||
| 95 | + <li class="leaf"><span class="label">C6 COM_STMT_RESET(clearParameters 后复用,8.0.33 及以上档正常)</span></li> | ||
| 96 | + <li class="leaf-note"><span class="label">FINDING I:8.0.28 档此序列直接崩溃 openGauss 服务端进程(EOFException 断连、可复现 3 次)</span></li> | ||
| 97 | + <li class="leaf"><span class="label">C7 COM_STMT_FETCH:驱动层可用、协议层待确认</span></li> | ||
| 98 | + <li class="leaf"><span class="label">C8 COM_SET_OPTION(allowMultiQueries 多语句)</span></li> | ||
| 99 | + <li class="leaf-note"><span class="label">FINDING G:多语句后续 SELECT 结果集 getMoreResults() 不可得(DML 均落库)</span></li> | ||
| 100 | + <li class="leaf"><span class="label">C9 COM_QUIT;无 JDBC 触发路径的命令在报告 4.1.6 说明</span></li> | ||
| 101 | + </ul> | ||
| 102 | + </li> | ||
| 103 | + <li class="branch b-matrix"><span class="label">驱动版本矩阵(7 档 × 19 用例)</span> | ||
| 104 | + <ul> | ||
| 105 | + <li class="leaf-note"><span class="label">8.0.28(文档基准):FINDING I 崩溃 + FINDING H 丢 8KB,其余 12 通过(含 K1/D1 复现)</span></li> | ||
| 106 | + <li class="leaf"><span class="label">8.0.33 / 8.1.0 / 8.3.0 / 8.4.0:18/19,仅 FINDING H(RESET 正常、服务端不崩)</span></li> | ||
| 107 | + <li class="leaf"><span class="label">9.7.0(Boot 默认)/ 26.7.0(最新):19/19 全部通过</span></li> | ||
| 108 | + <li class="leaf"><span class="label">三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常 → <b>核心链路与 K1/D1/FINDING E/F/G 均与驱动版本无关;FINDING H/I 为服务端缺陷、触发条件依赖驱动版本</b></span></li> | ||
| 109 | + </ul> | ||
| 110 | + </li> | ||
| 111 | + <li class="branch b-concl"><span class="label">结论</span> | ||
| 112 | + <ul> | ||
| 113 | + <li class="leaf"><span class="label">JPA 核心链路(类型映射 / 主键回填 / 分页 / 批量 / 事务)可用</span></li> | ||
| 114 | + <li class="leaf"><span class="label">六个问题/差异根源在服务端:① 反斜杠转义语义差异(已知差异,与 #504 同源)② FINDING E 大小写敏感(配置可恢复)③ FINDING F 二进制 0x00 截断 ④ FINDING G 多语句结果结构 ⑤ FINDING H 丢末块 8KB(8.0.28~8.4.0 档)⑥ FINDING I 崩溃(仅 8.0.28 档)</span></li> | ||
| 115 | + <li class="leaf"><span class="label">指导文档建议:LIKE 显式 escape '!';ddl-auto 建议 lower_case_table_names=1(或 none + 手工建表);服务端预编译 + SEND_LONG_DATA/RESET 场景建议 9.7.0 及以上驱动</span></li> | ||
| 116 | + </ul> | ||
| 117 | + </li> | ||
| 118 | + </ul> | ||
| 119 | + </li> | ||
| 120 | +</ul> | ||
| 121 | +<p class="foot">测试代码与自测报告见同目录 · 2026-09-04(v1.3)</p> | ||
| 122 | +</body> | ||
| 123 | +</html> | ||
| @@ -0,0 +1,89 @@ | |||
| 1 | +# 测试思维导图:SpringDataJPA + MySQL JDBC 连接 openGauss B 库(Issue #28) | ||
| 2 | + | ||
| 3 | +> 大纲版思维导图。可视化版见同名 .html,可编辑版见同名 .mm(XMind / FreeMind / MindMaster 均可打开)。 | ||
| 4 | + | ||
| 5 | +```text | ||
| 6 | +SpringDataJPA + MySQL JDBC 连接 openGauss B 库 测试(19 用例;BOM 默认 9.7.0 基线全通过) | ||
| 7 | +│ | ||
| 8 | +├── 测试环境 | ||
| 9 | +│ ├── openGauss 7.0.0-RC3(fe6f6e4b,debug)B 兼容库 proto_test_db / schema mysql_test_db | ||
| 10 | +│ ├── MySQL 协议端口 3308(enable_dolphin_proto = on) | ||
| 11 | +│ └── Spring Boot 3.5.16 + Hibernate 6.6.53 + mysql-connector-j 9.7.0(BOM 默认) | ||
| 12 | +│ 方言 MySQLDialect(自定义 OpenGaussBMySQLDialect 保留适配点) | ||
| 13 | +│ | ||
| 14 | +├── 一、已知差异回归(2 用例 ✓) | ||
| 15 | +│ ├── K1:派生 LIKE(findByNameLike)复现转义语义差异(#504) | ||
| 16 | +│ │ invalid escape string —— Hibernate 6.6 默认追加 escape '\\' | ||
| 17 | +│ │ (两个反斜杠;MySQL 中 '\\'=1 字符),B 模式按标准 SQL 把 '\\' | ||
| 18 | +│ │ 视为 2 字符;根源是 B 模式不支持反斜杠作为字符串转义字符 | ||
| 19 | +│ │ (与 Plugin 仓 issue #504 同源) | ||
| 20 | +│ ├── K2:JPQL 显式 escape '!'(searchByName)可用 ✓ | ||
| 21 | +│ │ workaround 稳定,且与驱动版本无关(8.0.28 与 9.7.0 同行为) | ||
| 22 | +│ └── 方法论:K1 写成回归断言 —— 服务端行为调整后测试自动失败, | ||
| 23 | +│ 提醒移除 workaround 并更新文档 | ||
| 24 | +│ | ||
| 25 | +├── 二、ddl-auto 专项(2 用例) | ||
| 26 | +│ ├── D1:ddl-auto=update 容器启动成功但表未创建 → FINDING E | ||
| 27 | +│ │ HHH000339:information_schema.KEYWORDS 不存在 | ||
| 28 | +│ │ (默认配置 dolphin.lower_case_table_names=0 下大小写敏感:小写可查) | ||
| 29 | +│ │ → schema 对比失败 → update 静默失效(不建表也不报错) | ||
| 30 | +│ │ → 实际项目:启动正常,运行时才报 relation does not exist | ||
| 31 | +│ │ → 解法:dolphin.lower_case_table_names=1 并持久化后实测大写 KEYWORDS | ||
| 32 | +│ │ 可查、Hibernate 元数据探测恢复 | ||
| 33 | +│ └── D2:ddl-auto=validate 未报错(元数据同样缺失,校验深度存疑,如实表述) | ||
| 34 | +│ | ||
| 35 | +├── 三、类型映射(1 用例 ✓) | ||
| 36 | +│ └── M1:BigDecimal / LocalDate / LocalDateTime / Boolean↔TINYINT(1) / String 往返 | ||
| 37 | +│ + IDENTITY 主键回填(getGeneratedKeys 链路正常) | ||
| 38 | +│ | ||
| 39 | +├── 四、分页与批量(2 用例 ✓) | ||
| 40 | +│ ├── P1:PageRequest 多页遍历(LIMIT/OFFSET 与 count 查询正常) | ||
| 41 | +│ └── P2:saveAll(50) 批量主键逐个回填 | ||
| 42 | +│ | ||
| 43 | +├── 五、冒烟(3 用例 ✓) | ||
| 44 | +│ └── saveAndFind / pagination / transactionRollback(@Transactional 回滚正常) | ||
| 45 | +│ | ||
| 46 | +├── 六、协议命令专项(9 用例 ✓,评审意见落地,与 #27 报告 7.1-7.9 同设计) | ||
| 47 | +│ ├── C1 COM_QUERY / C2 COM_INIT_DB / C3 COM_PING ✓ | ||
| 48 | +│ ├── C4 COM_STMT_PREPARE / COM_STMT_EXECUTE / COM_STMT_CLOSE | ||
| 49 | +│ │ (useServerPrepStmts=true 全生命周期)✓ | ||
| 50 | +│ ├── C5 COM_STMT_SEND_LONG_DATA(服务端预编译 + 1MB LONGBLOB) | ||
| 51 | +│ │ ├── 无零字节 1MB 完整往返(MD5 一致,9.7.0 基线)✓ | ||
| 52 | +│ │ ├── FINDING H:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192, | ||
| 53 | +│ │ │ 丢末块 8KB,MD5 失败;9.7.0/26.7.0 档完整) | ||
| 54 | +│ │ └── FINDING F:含 0x00 二进制被按 C 字符串截断为前缀 | ||
| 55 | +│ │ (200 字节读回 150 = 首个 0x00 位置;客户端预编译路径无此问题) | ||
| 56 | +│ ├── C6 COM_STMT_RESET(clearParameters 后复用)✓(8.0.33 及以上档) | ||
| 57 | +│ │ └── FINDING I:8.0.28 档此序列直接崩溃 openGauss 服务端进程 | ||
| 58 | +│ │ (EOFException 断连、3308 不可达、可复现 3 次) | ||
| 59 | +│ ├── C7 COM_STMT_FETCH(useCursorFetch + setFetchSize)✓ | ||
| 60 | +│ │ 驱动层可用、协议层待确认 | ||
| 61 | +│ ├── C8 COM_SET_OPTION(allowMultiQueries 多语句) | ||
| 62 | +│ │ └── FINDING G:后续 SELECT 结果集 getMoreResults() 不可得 | ||
| 63 | +│ │ (仅首语句结果 + 末 DML update count;DML 均落库) | ||
| 64 | +│ └── C9 COM_QUIT ✓;无 JDBC 触发路径的命令在报告 4.1.6 说明 | ||
| 65 | +│ | ||
| 66 | +├── 驱动版本矩阵(7 档 × 19 用例) | ||
| 67 | +│ ├── 8.0.28(文档基准):FINDING I 崩溃 + FINDING H 丢 8KB, | ||
| 68 | +│ │ 其余 12 通过(含 K1/D1 复现) | ||
| 69 | +│ ├── 8.0.33 / 8.1.0 / 8.3.0 / 8.4.0:18/19(仅 FINDING H, | ||
| 70 | +│ │ RESET 正常、服务端不崩) | ||
| 71 | +│ ├── 9.7.0(Boot 默认)/ 26.7.0(最新):19/19 全部通过 | ||
| 72 | +│ └── 三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常 → | ||
| 73 | +│ 核心链路与 K1/D1/FINDING E/F/G 均与驱动版本无关; | ||
| 74 | +│ FINDING H/I 为服务端缺陷、触发条件依赖驱动版本 | ||
| 75 | +│ | ||
| 76 | +└── 结论 | ||
| 77 | + ├── JPA 核心链路(类型映射 / 主键回填 / 分页 / 批量 / 事务)可用 | ||
| 78 | + ├── 六个问题/差异根源在服务端: | ||
| 79 | + │ ① 反斜杠转义语义差异(LIKE escape,已知差异,与 #504 同源) | ||
| 80 | + │ ② FINDING E information_schema 表名大小写敏感(ddl-auto 失效, | ||
| 81 | + │ 配置 lower_case_table_names=1 持久化可恢复) | ||
| 82 | + │ ③ FINDING F SEND_LONG_DATA 二进制 0x00 截断 | ||
| 83 | + │ ④ FINDING G 多语句结果返回结构不完整 | ||
| 84 | + │ ⑤ FINDING H SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档) | ||
| 85 | + │ ⑥ FINDING I COM_STMT_RESET 崩溃服务端(仅 8.0.28 档) | ||
| 86 | + └── 指导文档建议:LIKE 显式 escape '!';ddl-auto 建议 | ||
| 87 | + lower_case_table_names=1(或 none + 手工建表); | ||
| 88 | + 服务端预编译 + SEND_LONG_DATA/RESET 场景建议 9.7.0 及以上驱动 | ||
| 89 | +``` | ||
| @@ -0,0 +1,57 @@ | |||
| 1 | +<map version="1.0.1"> | ||
| 2 | +<node TEXT="SpringDataJPA + MySQL JDBC 连接 openGauss B 库 测试(19 用例;BOM 默认 9.7.0 基线全通过)"> | ||
| 3 | +<node TEXT="测试环境" POSITION="right"> | ||
| 4 | +<node TEXT="openGauss 7.0.0-RC3(fe6f6e4b)B 库 proto_test_db / schema mysql_test_db" POSITION="right"/> | ||
| 5 | +<node TEXT="MySQL 协议端口 3308(enable_dolphin_proto = on)" POSITION="right"/> | ||
| 6 | +<node TEXT="Spring Boot 3.5.16 + Hibernate 6.6.53 + mysql-connector-j 9.7.0" POSITION="right"/> | ||
| 7 | +<node TEXT="方言 MySQLDialect(自定义 OpenGaussBMySQLDialect 保留适配点)" POSITION="right"/> | ||
| 8 | +</node> | ||
| 9 | +<node TEXT="一、已知差异回归(2 用例,符合预期)" POSITION="right"> | ||
| 10 | +<node TEXT="K1:派生 LIKE 复现转义语义差异 —— invalid escape string(Hibernate 6.6 默认 escape '\\'(两个反斜杠),B 模式视 '\\' 为 2 字符;与 Plugin 仓 issue #504 同源)" POSITION="right"/> | ||
| 11 | +<node TEXT="K2:JPQL 显式 escape '!'(searchByName)可用,workaround 稳定" POSITION="right"/> | ||
| 12 | +<node TEXT="方法论:K1 写成回归断言 —— 服务端行为调整后自动失败,提醒更新文档" POSITION="right"/> | ||
| 13 | +</node> | ||
| 14 | +<node TEXT="二、ddl-auto 专项(2 用例)" POSITION="right"> | ||
| 15 | +<node TEXT="D1 FINDING E:ddl-auto=update 容器启动成功但表未创建(静默失效)" POSITION="right"/> | ||
| 16 | +<node TEXT="根因:默认配置 lower_case_table_names=0 下 information_schema 表名大小写敏感(KEYWORDS 大写报不存在,小写可查)→ schema 对比失败" POSITION="right"/> | ||
| 17 | +<node TEXT="解法:dolphin.lower_case_table_names=1 并持久化后实测大写 KEYWORDS 可查、Hibernate 元数据探测恢复" POSITION="right"/> | ||
| 18 | +<node TEXT="实际影响:启动正常,运行时才报 relation does not exist" POSITION="right"/> | ||
| 19 | +<node TEXT="D2:ddl-auto=validate 未报错(校验深度存疑,如实表述)" POSITION="right"/> | ||
| 20 | +</node> | ||
| 21 | +<node TEXT="三、类型映射(1 用例,通过)" POSITION="right"> | ||
| 22 | +<node TEXT="BigDecimal / LocalDate / LocalDateTime / Boolean↔TINYINT(1) / String 往返一致" POSITION="right"/> | ||
| 23 | +<node TEXT="IDENTITY 主键回填(getGeneratedKeys 链路正常)" POSITION="right"/> | ||
| 24 | +</node> | ||
| 25 | +<node TEXT="四、分页与批量(2 用例,通过)" POSITION="right"> | ||
| 26 | +<node TEXT="P1:PageRequest 多页遍历(LIMIT/OFFSET 与 count 查询正常)" POSITION="right"/> | ||
| 27 | +<node TEXT="P2:saveAll(50) 批量主键逐个回填" POSITION="right"/> | ||
| 28 | +</node> | ||
| 29 | +<node TEXT="五、冒烟(3 用例,通过)" POSITION="right"> | ||
| 30 | +<node TEXT="saveAndFind / pagination / transactionRollback(@Transactional 回滚正常)" POSITION="right"/> | ||
| 31 | +</node> | ||
| 32 | +<node TEXT="六、协议命令专项(9 用例,评审意见落地,与 #27 报告 7.1-7.9 同设计)" POSITION="right"> | ||
| 33 | +<node TEXT="C1 COM_QUERY / C2 COM_INIT_DB / C3 COM_PING" POSITION="right"/> | ||
| 34 | +<node TEXT="C4 COM_STMT_PREPARE / EXECUTE / CLOSE(useServerPrepStmts=true 全生命周期)" POSITION="right"/> | ||
| 35 | +<node TEXT="C5 COM_STMT_SEND_LONG_DATA:无零字节 1MB 完整往返(MD5 一致,9.7.0 基线)" POSITION="right"/> | ||
| 36 | +<node TEXT="FINDING H:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,丢末块 8KB);9.7.0/26.7.0 档完整" POSITION="right"/> | ||
| 37 | +<node TEXT="FINDING F:含 0x00 二进制被按 C 字符串截断为前缀(200 字节读回 150;客户端预编译路径无此问题)" POSITION="right"/> | ||
| 38 | +<node TEXT="C6 COM_STMT_RESET(clearParameters 后复用,8.0.33 及以上档正常)" POSITION="right"/> | ||
| 39 | +<node TEXT="FINDING I:8.0.28 档 COM_STMT_RESET 直接崩溃 openGauss 服务端进程(EOFException 断连、可复现 3 次)" POSITION="right"/> | ||
| 40 | +<node TEXT="C7 COM_STMT_FETCH:驱动层可用、协议层待确认" POSITION="right"/> | ||
| 41 | +<node TEXT="C8 COM_SET_OPTION(allowMultiQueries)" POSITION="right"/> | ||
| 42 | +<node TEXT="FINDING G:多语句后续 SELECT 结果集 getMoreResults() 不可得(DML 均落库)" POSITION="right"/> | ||
| 43 | +<node TEXT="C9 COM_QUIT;无 JDBC 触发路径的命令在报告 4.1.6 说明" POSITION="right"/> | ||
| 44 | +</node> | ||
| 45 | +<node TEXT="驱动版本矩阵(7 档 × 19 用例)" POSITION="right"> | ||
| 46 | +<node TEXT="8.0.28(文档基准):FINDING I 崩溃 + FINDING H 丢 8KB,其余 12 通过(含 K1/D1 复现)" POSITION="right"/> | ||
| 47 | +<node TEXT="8.0.33 / 8.1.0 / 8.3.0 / 8.4.0:18/19,仅 FINDING H(RESET 正常、服务端不崩)" POSITION="right"/> | ||
| 48 | +<node TEXT="9.7.0(Boot 默认)/ 26.7.0(最新):19/19 全部通过" POSITION="right"/> | ||
| 49 | +<node TEXT="三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常 → 核心链路与 K1/D1/FINDING E/F/G 与驱动版本无关;FINDING H/I 为服务端缺陷、触发条件依赖驱动版本" POSITION="right"/> | ||
| 50 | +</node> | ||
| 51 | +<node TEXT="结论" POSITION="right"> | ||
| 52 | +<node TEXT="JPA 核心链路(类型映射 / 主键回填 / 分页 / 批量 / 事务)可用" POSITION="right"/> | ||
| 53 | +<node TEXT="问题/差异根源在服务端:反斜杠转义语义差异(已知差异,与 #504 同源)/ FINDING E 大小写敏感(配置可恢复)/ FINDING F 二进制 0x00 截断 / FINDING G 多语句结果结构 / FINDING H 丢末块 8KB(8.0.28~8.4.0 档)/ FINDING I 崩溃(仅 8.0.28 档)" POSITION="right"/> | ||
| 54 | +<node TEXT="指导文档建议:LIKE 显式 escape '!';ddl-auto 建议 lower_case_table_names=1(或 none + 手工建表);服务端预编译 + SEND_LONG_DATA/RESET 场景建议 9.7.0 及以上驱动" POSITION="right"/> | ||
| 55 | +</node> | ||
| 56 | +</node> | ||
| 57 | +</map> | ||
| @@ -0,0 +1,129 @@ | |||
| 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 | + <parent> | ||
| 8 | + <groupId>org.springframework.boot</groupId> | ||
| 9 | + <artifactId>spring-boot-starter-parent</artifactId> | ||
| 10 | + <version>3.5.16</version> | ||
| 11 | + <relativePath/> | ||
| 12 | + </parent> | ||
| 13 | + | ||
| 14 | + <groupId>org.opengauss</groupId> | ||
| 15 | + <artifactId>springdatajpa-connect-opengauss-b</artifactId> | ||
| 16 | + <version>1.0.0</version> | ||
| 17 | + <name>springdatajpa-connect-opengauss-b</name> | ||
| 18 | + <description>Spring Data JPA 连接 openGauss B 兼容库 — Issue #28</description> | ||
| 19 | + | ||
| 20 | + <properties> | ||
| 21 | + <java.version>17</java.version> | ||
| 22 | + </properties> | ||
| 23 | + | ||
| 24 | + <dependencies> | ||
| 25 | + <dependency> | ||
| 26 | + <groupId>org.springframework.boot</groupId> | ||
| 27 | + <artifactId>spring-boot-starter-data-jpa</artifactId> | ||
| 28 | + </dependency> | ||
| 29 | + <dependency> | ||
| 30 | + <groupId>org.springframework.boot</groupId> | ||
| 31 | + <artifactId>spring-boot-starter-test</artifactId> | ||
| 32 | + <scope>test</scope> | ||
| 33 | + </dependency> | ||
| 34 | + </dependencies> | ||
| 35 | + | ||
| 36 | + <!-- 驱动扫测 profile:与 springboot-connect-opengauss-b 保持同一套档位 --> | ||
| 37 | + <profiles> | ||
| 38 | + <profile> | ||
| 39 | + <id>driver-boot-default</id> | ||
| 40 | + <activation> | ||
| 41 | + <activeByDefault>true</activeByDefault> | ||
| 42 | + </activation> | ||
| 43 | + <dependencies> | ||
| 44 | + <dependency> | ||
| 45 | + <groupId>com.mysql</groupId> | ||
| 46 | + <artifactId>mysql-connector-j</artifactId> | ||
| 47 | + <scope>runtime</scope> | ||
| 48 | + </dependency> | ||
| 49 | + </dependencies> | ||
| 50 | + </profile> | ||
| 51 | + <profile> | ||
| 52 | + <id>driver-8.0.28</id> | ||
| 53 | + <dependencies> | ||
| 54 | + <dependency> | ||
| 55 | + <groupId>mysql</groupId> | ||
| 56 | + <artifactId>mysql-connector-java</artifactId> | ||
| 57 | + <version>8.0.28</version> | ||
| 58 | + <scope>runtime</scope> | ||
| 59 | + </dependency> | ||
| 60 | + </dependencies> | ||
| 61 | + </profile> | ||
| 62 | + <profile> | ||
| 63 | + <id>driver-8.0.33</id> | ||
| 64 | + <dependencies> | ||
| 65 | + <dependency> | ||
| 66 | + <groupId>com.mysql</groupId> | ||
| 67 | + <artifactId>mysql-connector-j</artifactId> | ||
| 68 | + <version>8.0.33</version> | ||
| 69 | + <scope>runtime</scope> | ||
| 70 | + </dependency> | ||
| 71 | + </dependencies> | ||
| 72 | + </profile> | ||
| 73 | + <profile> | ||
| 74 | + <id>driver-8.1.0</id> | ||
| 75 | + <dependencies> | ||
| 76 | + <dependency> | ||
| 77 | + <groupId>com.mysql</groupId> | ||
| 78 | + <artifactId>mysql-connector-j</artifactId> | ||
| 79 | + <version>8.1.0</version> | ||
| 80 | + <scope>runtime</scope> | ||
| 81 | + </dependency> | ||
| 82 | + </dependencies> | ||
| 83 | + </profile> | ||
| 84 | + <profile> | ||
| 85 | + <id>driver-8.3.0</id> | ||
| 86 | + <dependencies> | ||
| 87 | + <dependency> | ||
| 88 | + <groupId>com.mysql</groupId> | ||
| 89 | + <artifactId>mysql-connector-j</artifactId> | ||
| 90 | + <version>8.3.0</version> | ||
| 91 | + <scope>runtime</scope> | ||
| 92 | + </dependency> | ||
| 93 | + </dependencies> | ||
| 94 | + </profile> | ||
| 95 | + <profile> | ||
| 96 | + <id>driver-8.4.0</id> | ||
| 97 | + <dependencies> | ||
| 98 | + <dependency> | ||
| 99 | + <groupId>com.mysql</groupId> | ||
| 100 | + <artifactId>mysql-connector-j</artifactId> | ||
| 101 | + <version>8.4.0</version> | ||
| 102 | + <scope>runtime</scope> | ||
| 103 | + </dependency> | ||
| 104 | + </dependencies> | ||
| 105 | + </profile> | ||
| 106 | + <profile> | ||
| 107 | + <id>driver-9.7.0</id> | ||
| 108 | + <dependencies> | ||
| 109 | + <dependency> | ||
| 110 | + <groupId>com.mysql</groupId> | ||
| 111 | + <artifactId>mysql-connector-j</artifactId> | ||
| 112 | + <version>9.7.0</version> | ||
| 113 | + <scope>runtime</scope> | ||
| 114 | + </dependency> | ||
| 115 | + </dependencies> | ||
| 116 | + </profile> | ||
| 117 | + <profile> | ||
| 118 | + <id>driver-26.7.0</id> | ||
| 119 | + <dependencies> | ||
| 120 | + <dependency> | ||
| 121 | + <groupId>com.mysql</groupId> | ||
| 122 | + <artifactId>mysql-connector-j</artifactId> | ||
| 123 | + <version>26.7.0</version> | ||
| 124 | + <scope>runtime</scope> | ||
| 125 | + </dependency> | ||
| 126 | + </dependencies> | ||
| 127 | + </profile> | ||
| 128 | + </profiles> | ||
| 129 | +</project> | ||
Aspringdatajpa-connect-opengauss-b/src/main/java/org/opengauss/smoke/OpenGaussBMySQLDialect.java+25-0
| @@ -0,0 +1,25 @@ | |||
| 1 | +package org.opengauss.smoke; | ||
| 2 | + | ||
| 3 | +import org.hibernate.dialect.MySQLDialect; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * openGauss B 兼容库方言(应用经 MySQL JDBC 驱动 + dolphin 协议连接)。 | ||
| 7 | + * | ||
| 8 | + * openGauss B 模式解析器按标准 SQL 解释字符串字面量(无反斜杠转义, | ||
| 9 | + * '\\' 是两个字符),与 MySQL 不同,故在此声明"无反斜杠转义"语义。 | ||
| 10 | + * | ||
| 11 | + * 已知兼容性问题:Hibernate 6.6 默认给 LIKE 谓词追加 escape '\\' | ||
| 12 | + * (两个反斜杠),B 模式解析器因 '\\' 是两字符而报 "invalid escape string" | ||
| 13 | + * (与驱动版本无关,8.0.28 与 9.7.0 表现一致)。该差异的根源是 B 模式不 | ||
| 14 | + * 支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin | ||
| 15 | + * 仓 issue #504)。实测 6.6.53 的 LIKE 渲染不读取本 isNoBackslashEscapesEnabled | ||
| 16 | + * 开关,规避方式是在 JPQL 中显式指定单字符转义符:like ?1 escape '!' | ||
| 17 | + * (见 UserRepository.searchByName)。 | ||
| 18 | + */ | ||
| 19 | +public class OpenGaussBMySQLDialect extends MySQLDialect { | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + public boolean isNoBackslashEscapesEnabled() { | ||
| 23 | + return true; | ||
| 24 | + } | ||
| 25 | +} | ||
| @@ -0,0 +1,52 @@ | |||
| 1 | +package org.opengauss.smoke; | ||
| 2 | + | ||
| 3 | +import org.springframework.boot.CommandLineRunner; | ||
| 4 | +import org.springframework.boot.SpringApplication; | ||
| 5 | +import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
| 6 | +import org.springframework.data.domain.Page; | ||
| 7 | +import org.springframework.data.domain.PageRequest; | ||
| 8 | +import org.springframework.stereotype.Component; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * JPA 冒烟程序:验证 Hibernate 6 在 MySQL 协议链路上的行为。 | ||
| 12 | + * 重点观察:启动元数据查询(show-sql 可见)、IDENTITY 主键回填、分页 SQL。 | ||
| 13 | + */ | ||
| 14 | + | ||
| 15 | +public class SmokeApplication { | ||
| 16 | + | ||
| 17 | + public static void main(String[] args) { | ||
| 18 | + SpringApplication.run(SmokeApplication.class, args); | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + public static class SmokeRunner implements CommandLineRunner { | ||
| 23 | + | ||
| 24 | + private final UserRepository repo; | ||
| 25 | + | ||
| 26 | + public SmokeRunner(UserRepository repo) { | ||
| 27 | + this.repo = repo; | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + | ||
| 31 | + public void run(String... args) { | ||
| 32 | + // 1. 查询 | ||
| 33 | + long count = repo.count(); | ||
| 34 | + System.out.println("[SMOKE] 连接成功,User 总数 = " + count); | ||
| 35 | + | ||
| 36 | + // 2. 保存:验证 IDENTITY 主键回填(getGeneratedKeys) | ||
| 37 | + User saved = repo.save(new User("smoke", 99)); | ||
| 38 | + System.out.println("[SMOKE] 保存成功,主键回填 id = " + saved.getId()); | ||
| 39 | + | ||
| 40 | + // 3. 分页:Hibernate 生成 LIMIT/OFFSET | ||
| 41 | + Page<User> page = repo.findAll(PageRequest.of(0, 2)); | ||
| 42 | + System.out.println("[SMOKE] 分页成功,本页 " + page.getContent().size() | ||
| 43 | + + " 条,共 " + page.getTotalElements() + " 条"); | ||
| 44 | + | ||
| 45 | + // 4. 模糊查询(显式 escape '!' 规避 B 模式反斜杠转义问题)+ 删除 | ||
| 46 | + Page<User> found = repo.searchByName("%smoke%", PageRequest.of(0, 5)); | ||
| 47 | + System.out.println("[SMOKE] 模糊查询成功,命中 " + found.getTotalElements() + " 条"); | ||
| 48 | + repo.delete(saved); | ||
| 49 | + System.out.println("[SMOKE] 删除成功,JPA 全链路通过"); | ||
| 50 | + } | ||
| 51 | + } | ||
| 52 | +} | ||
| @@ -0,0 +1,21 @@ | |||
| 1 | +package org.opengauss.smoke; | ||
| 2 | + | ||
| 3 | +import org.springframework.stereotype.Service; | ||
| 4 | +import org.springframework.transaction.annotation.Transactional; | ||
| 5 | + | ||
| 6 | +/** 事务行为验证用服务。 */ | ||
| 7 | + | ||
| 8 | +public class TxService { | ||
| 9 | + | ||
| 10 | + private final UserRepository repo; | ||
| 11 | + | ||
| 12 | + public TxService(UserRepository repo) { | ||
| 13 | + this.repo = repo; | ||
| 14 | + } | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + public void saveThenThrow(String name, int age) { | ||
| 18 | + repo.save(new User(name, age)); | ||
| 19 | + throw new RuntimeException("事务回滚验证"); | ||
| 20 | + } | ||
| 21 | +} | ||
| @@ -0,0 +1,63 @@ | |||
| 1 | +package org.opengauss.smoke; | ||
| 2 | + | ||
| 3 | +import jakarta.persistence.Entity; | ||
| 4 | +import jakarta.persistence.GeneratedValue; | ||
| 5 | +import jakarta.persistence.GenerationType; | ||
| 6 | +import jakarta.persistence.Id; | ||
| 7 | +import jakarta.persistence.Table; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 业务表 `user` 的实体类。 | ||
| 11 | + * 注意:Spring Boot 3.x / Hibernate 6 使用 jakarta.* 包(Boot 2.x 是 javax.*)。 | ||
| 12 | + * user 在 MySQL 中是保留字,Hibernate 6 默认自动加反引号(auto_quote_keyword)。 | ||
| 13 | + */ | ||
| 14 | + | ||
| 15 | + | ||
| 16 | +public class User { | ||
| 17 | + | ||
| 18 | + /** IDENTITY 策略对应 MySQL 的 AUTO_INCREMENT,回填依赖 JDBC getGeneratedKeys。 */ | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + private Integer id; | ||
| 22 | + | ||
| 23 | + private String name; | ||
| 24 | + | ||
| 25 | + private Integer age; | ||
| 26 | + | ||
| 27 | + public User() { | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + public User(String name, Integer age) { | ||
| 31 | + this.name = name; | ||
| 32 | + this.age = age; | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + public Integer getId() { | ||
| 36 | + return id; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + public void setId(Integer id) { | ||
| 40 | + this.id = id; | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + public String getName() { | ||
| 44 | + return name; | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + public void setName(String name) { | ||
| 48 | + this.name = name; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + public Integer getAge() { | ||
| 52 | + return age; | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + public void setAge(Integer age) { | ||
| 56 | + this.age = age; | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + | ||
| 60 | + public String toString() { | ||
| 61 | + return "User{id=" + id + ", name='" + name + "', age=" + age + "}"; | ||
| 62 | + } | ||
| 63 | +} | ||
| @@ -0,0 +1,25 @@ | |||
| 1 | +package org.opengauss.smoke; | ||
| 2 | + | ||
| 3 | +import org.springframework.data.domain.Page; | ||
| 4 | +import org.springframework.data.domain.Pageable; | ||
| 5 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 6 | +import org.springframework.data.jpa.repository.Query; | ||
| 7 | + | ||
| 8 | +/** JpaRepository 自带 CRUD 与 findAll(Pageable)(分页生成 LIMIT/OFFSET)。 */ | ||
| 9 | +public interface UserRepository extends JpaRepository<User, Integer> { | ||
| 10 | + | ||
| 11 | + /** | ||
| 12 | + * 派生 LIKE 查询:Hibernate 6.6 默认在所有 LIKE 谓词后追加 escape '\\' | ||
| 13 | + * (两个反斜杠),B 模式按标准 SQL 把 '\\' 视为 2 个字符,报 | ||
| 14 | + * invalid escape string(转义语义差异;根源是 B 模式不支持反斜杠作为字符串 | ||
| 15 | + * 转义字符,与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)。 | ||
| 16 | + * 保留此方法作为"已知差异"回归用例。 | ||
| 17 | + */ | ||
| 18 | + Page<User> findByNameLike(String name, Pageable pageable); | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * 显式指定非反斜杠转义符(!)的 LIKE 查询,规避上述问题。 | ||
| 22 | + */ | ||
| 23 | + | ||
| 24 | + Page<User> searchByName(String pattern, Pageable pageable); | ||
| 25 | +} | ||
| @@ -0,0 +1,14 @@ | |||
| 1 | +spring: | ||
| 2 | + datasource: | ||
| 3 | + url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3308}/${DB_NAME:mysql_test_db}?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8 | ||
| 4 | + username: ${DB_USER:mysql_test_db} | ||
| 5 | + password: ${DB_PASSWORD:ChangeMe_123} | ||
| 6 | + driver-class-name: com.mysql.cj.jdbc.Driver | ||
| 7 | + jpa: | ||
| 8 | + # 表由 SQL 脚本创建,不让 Hibernate 建表/改表;ddl-auto 行为是后续专项测试点 | ||
| 9 | + hibernate: | ||
| 10 | + ddl-auto: none | ||
| 11 | + # 走 MySQL 协议。自定义方言声明 B 模式"无反斜杠转义"语义,见 OpenGaussBMySQLDialect。 | ||
| 12 | + # LIKE 查询的规避:JPQL 显式 escape '!'(B 模式不认 '\\' 单字符转义,见 UserRepository) | ||
| 13 | + database-platform: org.opengauss.smoke.OpenGaussBMySQLDialect | ||
| 14 | + show-sql: true | ||
| @@ -0,0 +1,54 @@ | |||
| 1 | +package org.opengauss.smoke; | ||
| 2 | + | ||
| 3 | +import org.junit.jupiter.api.Test; | ||
| 4 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 5 | +import org.springframework.boot.test.context.SpringBootTest; | ||
| 6 | +import org.springframework.data.domain.Page; | ||
| 7 | +import org.springframework.data.domain.PageRequest; | ||
| 8 | + | ||
| 9 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 10 | +import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| 11 | + | ||
| 12 | +/** JPA 冒烟用例:需真实 openGauss B 库环境,与 driver profile 配合做版本扫测。 */ | ||
| 13 | + | ||
| 14 | +class SmokeTest { | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + private UserRepository repo; | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + private TxService txService; | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + void saveAndFind() { | ||
| 24 | + User saved = repo.save(new User("t_jpa", 1)); | ||
| 25 | + // IDENTITY 主键回填 | ||
| 26 | + assertNotNull(saved.getId(), "save 后应回填自增主键"); | ||
| 27 | + | ||
| 28 | + User found = repo.findById(saved.getId()).orElseThrow(); | ||
| 29 | + assertEquals("t_jpa", found.getName()); | ||
| 30 | + repo.delete(saved); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + | ||
| 34 | + void pagination() { | ||
| 35 | + Page<User> page = repo.findAll(PageRequest.of(0, 2)); | ||
| 36 | + assertEquals(2, page.getContent().size(), "每页应返回 2 条(LIMIT 2)"); | ||
| 37 | + repo.searchByName("%t_%", PageRequest.of(0, 5)); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + | ||
| 41 | + void transactionRollback() { | ||
| 42 | + try { | ||
| 43 | + txService.saveThenThrow("t_tx", 1); | ||
| 44 | + } catch (RuntimeException expected) { | ||
| 45 | + // 预期异常:触发回滚 | ||
| 46 | + } | ||
| 47 | + long count = repo.count(); | ||
| 48 | + repo.findAll().forEach(u -> { | ||
| 49 | + if ("t_tx".equals(u.getName())) { | ||
| 50 | + throw new AssertionError("事务应已回滚,不应存在 t_tx 记录"); | ||
| 51 | + } | ||
| 52 | + }); | ||
| 53 | + } | ||
| 54 | +} | ||
Aspringdatajpa-connect-opengauss-b/src/test/java/org/opengauss/smoke/jpacases/DdlAutoTest.java+100-0
| @@ -0,0 +1,100 @@ | |||
| 1 | +package org.opengauss.smoke.jpacases; | ||
| 2 | + | ||
| 3 | +import org.junit.jupiter.api.DisplayName; | ||
| 4 | +import org.junit.jupiter.api.Test; | ||
| 5 | +import org.opengauss.smoke.SmokeApplication; | ||
| 6 | +import org.springframework.boot.builder.SpringApplicationBuilder; | ||
| 7 | +import org.springframework.context.ConfigurableApplicationContext; | ||
| 8 | + | ||
| 9 | +import java.sql.Connection; | ||
| 10 | +import java.sql.PreparedStatement; | ||
| 11 | +import java.sql.ResultSet; | ||
| 12 | +import java.sql.SQLException; | ||
| 13 | + | ||
| 14 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 15 | +import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| 16 | +import static org.opengauss.smoke.jpacases.JpaDbUtil.*; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * ddl-auto 行为专项。 | ||
| 20 | + * 手工拉起独立 Spring 容器(避免 在容器启动失败时把测试直接标 ERROR), | ||
| 21 | + * 分别验证 update / validate 两种模式在 B 模式下的实际行为。 | ||
| 22 | + */ | ||
| 23 | +class DdlAutoTest { | ||
| 24 | + | ||
| 25 | + private static boolean tableExists(String table) throws SQLException { | ||
| 26 | + try (Connection c = raw(); | ||
| 27 | + PreparedStatement ps = c.prepareStatement("SELECT COUNT(*) FROM " + table); | ||
| 28 | + ResultSet rs = ps.executeQuery()) { | ||
| 29 | + rs.next(); | ||
| 30 | + return true; | ||
| 31 | + } | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + | ||
| 35 | + | ||
| 36 | + void ddlAutoUpdate() throws SQLException { | ||
| 37 | + // 先确保目标表不存在(从干净状态出发) | ||
| 38 | + try (Connection c = raw()) { | ||
| 39 | + exec(c, "DROP TABLE IF EXISTS t_jpa_ddl"); | ||
| 40 | + } | ||
| 41 | + boolean contextOk = true; | ||
| 42 | + String failure = null; | ||
| 43 | + try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder(SmokeApplication.class) | ||
| 44 | + .properties("spring.jpa.hibernate.ddl-auto=update") | ||
| 45 | + .run()) { | ||
| 46 | + log("D1 ddl-auto=update 容器启动成功"); | ||
| 47 | + } catch (Exception e) { | ||
| 48 | + contextOk = false; | ||
| 49 | + failure = rootMessage(e); | ||
| 50 | + log("D1 FINDING: ddl-auto=update 容器启动失败: %s", failure); | ||
| 51 | + } | ||
| 52 | + boolean created; | ||
| 53 | + try { | ||
| 54 | + created = tableExists("t_jpa_ddl"); | ||
| 55 | + } catch (SQLException e) { | ||
| 56 | + created = false; | ||
| 57 | + } | ||
| 58 | + if (contextOk && !created) { | ||
| 59 | + log("D1 FINDING: 容器启动成功但 t_jpa_ddl 未被创建。原因:连接元数据获取失败" | ||
| 60 | + + "(information_schema.KEYWORDS 不存在,见日志 HHH000339)," | ||
| 61 | + + "Hibernate 无法做 schema 对比 -> ddl-auto=update 静默失效(不建表也不报错)。" | ||
| 62 | + + "实际项目影响:应用启动正常,运行时才报 relation does not exist"); | ||
| 63 | + } else if (created) { | ||
| 64 | + log("D1 t_jpa_ddl 已被 Hibernate 自动创建(ddl-auto=update 可用,可能服务端已修复元数据)"); | ||
| 65 | + // 新表上做一次真实读写,确认生成的表结构可用(IDENTITY 回填) | ||
| 66 | + try (Connection c = raw()) { | ||
| 67 | + exec(c, "INSERT INTO t_jpa_ddl (name) VALUES ('ddl_ok')"); | ||
| 68 | + } | ||
| 69 | + try (Connection c = raw(); | ||
| 70 | + PreparedStatement ps = c.prepareStatement("SELECT name FROM t_jpa_ddl WHERE name='ddl_ok'"); | ||
| 71 | + ResultSet rs = ps.executeQuery()) { | ||
| 72 | + assertTrue(rs.next()); | ||
| 73 | + assertEquals("ddl_ok", rs.getString(1)); | ||
| 74 | + log("D1 生成的表上读写正常"); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + // 两种结果都不做硬断言:当前 B 模式实际行为是"不建表"(作为 FINDING 记录), | ||
| 78 | + // 将来服务端修复 information_schema 后,本用例会自动观察到 created=true | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + | ||
| 82 | + | ||
| 83 | + void ddlAutoValidate() { | ||
| 84 | + try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder(SmokeApplication.class) | ||
| 85 | + .properties("spring.jpa.hibernate.ddl-auto=validate") | ||
| 86 | + .run()) { | ||
| 87 | + log("D2 ddl-auto=validate 通过:Hibernate 元数据校验未报错"); | ||
| 88 | + } catch (Exception e) { | ||
| 89 | + log("D2 FINDING: ddl-auto=validate 启动失败: %s", rootMessage(e)); | ||
| 90 | + } | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + private static String rootMessage(Throwable t) { | ||
| 94 | + Throwable cur = t; | ||
| 95 | + while (cur.getCause() != null) { | ||
| 96 | + cur = cur.getCause(); | ||
| 97 | + } | ||
| 98 | + return cur.getMessage(); | ||
| 99 | + } | ||
| 100 | +} | ||
| @@ -0,0 +1,45 @@ | |||
| 1 | +package org.opengauss.smoke.jpacases; | ||
| 2 | + | ||
| 3 | +import jakarta.persistence.Entity; | ||
| 4 | +import jakarta.persistence.GeneratedValue; | ||
| 5 | +import jakarta.persistence.GenerationType; | ||
| 6 | +import jakarta.persistence.Id; | ||
| 7 | +import jakarta.persistence.Table; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * ddl-auto 行为测试专用实体,对应表 t_jpa_ddl(表本身不存在,由 Hibernate 生成)。 | ||
| 11 | + * 结构刻意简单(id + name),便于观察 Hibernate 生成的建表 SQL 在 B 模式下的表现。 | ||
| 12 | + */ | ||
| 13 | + | ||
| 14 | + | ||
| 15 | +public class DdlEntity { | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + private Integer id; | ||
| 20 | + | ||
| 21 | + private String name; | ||
| 22 | + | ||
| 23 | + public DdlEntity() { | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + public DdlEntity(String name) { | ||
| 27 | + this.name = name; | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + public Integer getId() { | ||
| 31 | + return id; | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + public void setId(Integer id) { | ||
| 35 | + this.id = id; | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + public String getName() { | ||
| 39 | + return name; | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + public void setName(String name) { | ||
| 43 | + this.name = name; | ||
| 44 | + } | ||
| 45 | +} | ||
| @@ -0,0 +1,58 @@ | |||
| 1 | +package org.opengauss.smoke.jpacases; | ||
| 2 | + | ||
| 3 | +import java.sql.Connection; | ||
| 4 | +import java.sql.DriverManager; | ||
| 5 | +import java.sql.SQLException; | ||
| 6 | +import java.sql.Statement; | ||
| 7 | + | ||
| 8 | +/** JPA 用例公共工具:原生 JDBC 连接(建测试表用)与统一日志。 */ | ||
| 9 | +public final class JpaDbUtil { | ||
| 10 | + | ||
| 11 | + public static final String HOST = env("DB_HOST", "127.0.0.1"); | ||
| 12 | + public static final String PORT = env("DB_PORT", "3308"); | ||
| 13 | + public static final String DB = env("DB_NAME", "mysql_test_db"); | ||
| 14 | + public static final String USER = env("DB_USER", "mysql_test_db"); | ||
| 15 | + public static final String PASSWORD = env("DB_PASSWORD", "ChangeMe_123"); | ||
| 16 | + | ||
| 17 | + private JpaDbUtil() { | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + public static String url() { | ||
| 21 | + return "jdbc:mysql://" + HOST + ":" + PORT + "/" + DB | ||
| 22 | + + "?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8"; | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + /** 原生连接(不走连接池),用于建表/DDL 与元数据检查。 */ | ||
| 26 | + public static Connection raw() throws SQLException { | ||
| 27 | + return DriverManager.getConnection(url(), USER, PASSWORD); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + /** 拼 MySQL JDBC URL;extraParams 形如 "useServerPrepStmts=true&...",null 表示不带额外参数。 */ | ||
| 31 | + public static String url(String extraParams) { | ||
| 32 | + String base = url(); | ||
| 33 | + return extraParams == null || extraParams.isEmpty() ? base : base + "&" + extraParams; | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + /** 原生连接(带驱动参数),用于协议命令专项。 */ | ||
| 37 | + public static Connection raw(String extraParams) throws SQLException { | ||
| 38 | + return DriverManager.getConnection(url(extraParams), USER, PASSWORD); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + public static void exec(Connection conn, String... sqls) throws SQLException { | ||
| 42 | + try (Statement st = conn.createStatement()) { | ||
| 43 | + for (String sql : sqls) { | ||
| 44 | + st.execute(sql); | ||
| 45 | + } | ||
| 46 | + } | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + /** 统一日志前缀:[CASE] 开头的行会被整理进自测报告。 */ | ||
| 50 | + public static void log(String fmt, Object... args) { | ||
| 51 | + System.out.println("[CASE] " + String.format(fmt, args)); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + private static String env(String key, String def) { | ||
| 55 | + String v = System.getenv(key); | ||
| 56 | + return (v == null || v.isEmpty()) ? def : v; | ||
| 57 | + } | ||
| 58 | +} | ||
Aspringdatajpa-connect-opengauss-b/src/test/java/org/opengauss/smoke/jpacases/JpaMapEntity.java+99-0
| @@ -0,0 +1,99 @@ | |||
| 1 | +package org.opengauss.smoke.jpacases; | ||
| 2 | + | ||
| 3 | +import jakarta.persistence.Column; | ||
| 4 | +import jakarta.persistence.Entity; | ||
| 5 | +import jakarta.persistence.GeneratedValue; | ||
| 6 | +import jakarta.persistence.GenerationType; | ||
| 7 | +import jakarta.persistence.Id; | ||
| 8 | +import jakarta.persistence.Table; | ||
| 9 | + | ||
| 10 | +import java.math.BigDecimal; | ||
| 11 | +import java.time.LocalDate; | ||
| 12 | +import java.time.LocalDateTime; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * JPA 类型映射测试实体,对应表 t_jpa_map(由 MappingTest 的 用 SQL 创建)。 | ||
| 16 | + * 覆盖 JPA/Hibernate 6 常用映射:BigDecimal / LocalDate / LocalDateTime / Boolean / String。 | ||
| 17 | + */ | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +public class JpaMapEntity { | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + private Integer id; | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + private BigDecimal dec; | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + private LocalDate date; | ||
| 31 | + | ||
| 32 | + | ||
| 33 | + private LocalDateTime datetime; | ||
| 34 | + | ||
| 35 | + | ||
| 36 | + private Boolean bool; | ||
| 37 | + | ||
| 38 | + | ||
| 39 | + private String name; | ||
| 40 | + | ||
| 41 | + public JpaMapEntity() { | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + public JpaMapEntity(BigDecimal dec, LocalDate date, LocalDateTime datetime, Boolean bool, String name) { | ||
| 45 | + this.dec = dec; | ||
| 46 | + this.date = date; | ||
| 47 | + this.datetime = datetime; | ||
| 48 | + this.bool = bool; | ||
| 49 | + this.name = name; | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + public Integer getId() { | ||
| 53 | + return id; | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + public void setId(Integer id) { | ||
| 57 | + this.id = id; | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + public BigDecimal getDec() { | ||
| 61 | + return dec; | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + public void setDec(BigDecimal dec) { | ||
| 65 | + this.dec = dec; | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + public LocalDate getDate() { | ||
| 69 | + return date; | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + public void setDate(LocalDate date) { | ||
| 73 | + this.date = date; | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + public LocalDateTime getDatetime() { | ||
| 77 | + return datetime; | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + public void setDatetime(LocalDateTime datetime) { | ||
| 81 | + this.datetime = datetime; | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + public Boolean getBool() { | ||
| 85 | + return bool; | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + public void setBool(Boolean bool) { | ||
| 89 | + this.bool = bool; | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + public String getName() { | ||
| 93 | + return name; | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + public void setName(String name) { | ||
| 97 | + this.name = name; | ||
| 98 | + } | ||
| 99 | +} | ||
Aspringdatajpa-connect-opengauss-b/src/test/java/org/opengauss/smoke/jpacases/JpaMapRepository.java+7-0
| @@ -0,0 +1,7 @@ | |||
| 1 | +package org.opengauss.smoke.jpacases; | ||
| 2 | + | ||
| 3 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
| 4 | + | ||
| 5 | +/** t_jpa_map 的仓储(MappingTest 用)。 */ | ||
| 6 | +public interface JpaMapRepository extends JpaRepository<JpaMapEntity, Integer> { | ||
| 7 | +} | ||
Aspringdatajpa-connect-opengauss-b/src/test/java/org/opengauss/smoke/jpacases/KnownIssueTest.java+61-0
| @@ -0,0 +1,61 @@ | |||
| 1 | +package org.opengauss.smoke.jpacases; | ||
| 2 | + | ||
| 3 | +import org.junit.jupiter.api.DisplayName; | ||
| 4 | +import org.junit.jupiter.api.Test; | ||
| 5 | +import org.opengauss.smoke.User; | ||
| 6 | +import org.opengauss.smoke.UserRepository; | ||
| 7 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 8 | +import org.springframework.boot.test.context.SpringBootTest; | ||
| 9 | +import org.springframework.data.domain.Page; | ||
| 10 | +import org.springframework.data.domain.PageRequest; | ||
| 11 | +import org.springframework.orm.jpa.JpaSystemException; | ||
| 12 | + | ||
| 13 | +import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| 14 | +import static org.opengauss.smoke.jpacases.JpaDbUtil.log; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * 已知差异回归用例(LIKE escape 转义语义差异)。 | ||
| 18 | + * Hibernate 6.6 派生 LIKE 查询默认生成 escape '\\'(两个反斜杠),B 模式 | ||
| 19 | + * 按标准 SQL 把 '\\' 当两字符,报 invalid escape string(与驱动版本无关, | ||
| 20 | + * gsql 已取证)。该差异的根源是 B 模式不支持反斜杠作为字符串转义字符 | ||
| 21 | + * (与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)。 | ||
| 22 | + * | ||
| 23 | + * 这类用例的写法:把"当前已知的差异行为"写成断言。将来服务端行为调整后, | ||
| 24 | + * 第一个用例会失败,提醒我们移除 workaround 并更新文档——这是回归测试的标准用法。 | ||
| 25 | + */ | ||
| 26 | + | ||
| 27 | +class KnownIssueTest { | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + private UserRepository repo; | ||
| 31 | + | ||
| 32 | + | ||
| 33 | + | ||
| 34 | + void derivedLikeFailsAsExpected() { | ||
| 35 | + try { | ||
| 36 | + Page<User> page = repo.findByNameLike("%t_%", PageRequest.of(0, 5)); | ||
| 37 | + log("K1 注意:派生 LIKE 不再报错(命中 %d 条)!服务端可能已调整转义语义," | ||
| 38 | + + "应移除 searchByName 的 escape '!' workaround 并更新文档", page.getTotalElements()); | ||
| 39 | + } catch (JpaSystemException e) { | ||
| 40 | + String msg = rootMessage(e); | ||
| 41 | + log("K1 已知差异复现(符合当前预期): %s", msg); | ||
| 42 | + assertTrue(msg.contains("invalid escape string"), | ||
| 43 | + "应报 invalid escape string,实际: " + msg); | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + | ||
| 49 | + void explicitEscapeWorks() { | ||
| 50 | + Page<User> page = repo.searchByName("%t_%", PageRequest.of(0, 5)); | ||
| 51 | + log("K2 searchByName(escape '!')命中 %d 条,规避方案有效", page.getTotalElements()); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + private static String rootMessage(Throwable t) { | ||
| 55 | + Throwable cur = t; | ||
| 56 | + while (cur.getCause() != null) { | ||
| 57 | + cur = cur.getCause(); | ||
| 58 | + } | ||
| 59 | + return cur.getMessage(); | ||
| 60 | + } | ||
| 61 | +} | ||
| @@ -0,0 +1,67 @@ | |||
| 1 | +package org.opengauss.smoke.jpacases; | ||
| 2 | + | ||
| 3 | +import org.junit.jupiter.api.BeforeAll; | ||
| 4 | +import org.junit.jupiter.api.DisplayName; | ||
| 5 | +import org.junit.jupiter.api.Test; | ||
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 7 | +import org.springframework.boot.test.context.SpringBootTest; | ||
| 8 | + | ||
| 9 | +import java.math.BigDecimal; | ||
| 10 | +import java.sql.Connection; | ||
| 11 | +import java.sql.SQLException; | ||
| 12 | +import java.time.LocalDate; | ||
| 13 | +import java.time.LocalDateTime; | ||
| 14 | + | ||
| 15 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 16 | +import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| 17 | +import static org.opengauss.smoke.jpacases.JpaDbUtil.*; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * JPA 类型映射:BigDecimal / LocalDate / LocalDateTime / Boolean / String | ||
| 21 | + * 实体-表往返(save → findById 值一致),IDENTITY 主键回填。 | ||
| 22 | + */ | ||
| 23 | + | ||
| 24 | +class MappingTest { | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + private JpaMapRepository repo; | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + static void createTable() throws SQLException { | ||
| 31 | + try (Connection c = raw()) { | ||
| 32 | + exec(c, | ||
| 33 | + "DROP TABLE IF EXISTS t_jpa_map", | ||
| 34 | + "CREATE TABLE t_jpa_map (" | ||
| 35 | + + " id INT AUTO_INCREMENT PRIMARY KEY," | ||
| 36 | + + " n_dec DECIMAL(10,2)," | ||
| 37 | + + " n_date DATE," | ||
| 38 | + + " n_datetime DATETIME," | ||
| 39 | + + " n_bool TINYINT(1)," | ||
| 40 | + + " n_name VARCHAR(50)" | ||
| 41 | + + ")"); | ||
| 42 | + } | ||
| 43 | + log("M0 建表 t_jpa_map 成功"); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + void roundtrip() { | ||
| 49 | + BigDecimal dec = new BigDecimal("3.14"); | ||
| 50 | + LocalDate date = LocalDate.of(2026, 8, 28); | ||
| 51 | + LocalDateTime datetime = LocalDateTime.of(2026, 8, 28, 12, 34, 56); | ||
| 52 | + JpaMapEntity saved = repo.save(new JpaMapEntity(dec, date, datetime, Boolean.TRUE, "jpa_map")); | ||
| 53 | + | ||
| 54 | + assertNotNull(saved.getId(), "save 后应回填自增主键"); | ||
| 55 | + log("M1 保存成功,IDENTITY 回填 id=%d", saved.getId()); | ||
| 56 | + | ||
| 57 | + JpaMapEntity found = repo.findById(saved.getId()).orElseThrow(); | ||
| 58 | + assertEquals(dec, found.getDec(), "DECIMAL 往返"); | ||
| 59 | + assertEquals(date, found.getDate(), "DATE 往返"); | ||
| 60 | + assertEquals(datetime, found.getDatetime(), "DATETIME 往返"); | ||
| 61 | + assertEquals(Boolean.TRUE, found.getBool(), "TINYINT(1) ↔ Boolean 往返"); | ||
| 62 | + assertEquals("jpa_map", found.getName(), "VARCHAR 往返"); | ||
| 63 | + log("M1 五种类型映射往返一致:DECIMAL/DATE/DATETIME/Boolean/String"); | ||
| 64 | + | ||
| 65 | + repo.delete(found); | ||
| 66 | + } | ||
| 67 | +} | ||
| @@ -0,0 +1,70 @@ | |||
| 1 | +package org.opengauss.smoke.jpacases; | ||
| 2 | + | ||
| 3 | +import org.junit.jupiter.api.DisplayName; | ||
| 4 | +import org.junit.jupiter.api.Test; | ||
| 5 | +import org.opengauss.smoke.User; | ||
| 6 | +import org.opengauss.smoke.UserRepository; | ||
| 7 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 8 | +import org.springframework.boot.test.context.SpringBootTest; | ||
| 9 | +import org.springframework.data.domain.Page; | ||
| 10 | +import org.springframework.data.domain.PageRequest; | ||
| 11 | + | ||
| 12 | +import java.util.ArrayList; | ||
| 13 | +import java.util.List; | ||
| 14 | + | ||
| 15 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 16 | +import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| 17 | +import static org.opengauss.smoke.jpacases.JpaDbUtil.log; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * 分页(LIMIT/OFFSET 生成)与批量保存。 | ||
| 21 | + * 清理用 searchByName(escape '!'),刻意避开转义语义差异(#504)的派生 LIKE。 | ||
| 22 | + */ | ||
| 23 | + | ||
| 24 | +class PaginationBatchTest { | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + private UserRepository repo; | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + | ||
| 31 | + void pagination() { | ||
| 32 | + long total = repo.count(); | ||
| 33 | + assertTrue(total >= 3, "user 表至少应有 3 条初始数据"); | ||
| 34 | + | ||
| 35 | + int size = 2; | ||
| 36 | + long pages = (total + size - 1) / size; | ||
| 37 | + int seen = 0; | ||
| 38 | + for (int i = 0; i < pages; i++) { | ||
| 39 | + Page<User> page = repo.findAll(PageRequest.of(i, size)); | ||
| 40 | + seen += page.getContent().size(); | ||
| 41 | + } | ||
| 42 | + assertEquals(total, seen, "逐页遍历应覆盖全部记录"); | ||
| 43 | + log("P1 分页遍历 OK:共 %d 条 / 页大小 2 / %d 页(LIMIT/OFFSET 与 count 查询均正常)", total, pages); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + void saveAll() { | ||
| 49 | + List<User> users = new ArrayList<>(); | ||
| 50 | + for (int i = 1; i <= 50; i++) { | ||
| 51 | + users.add(new User("t_jpa_batch_" + i, i)); | ||
| 52 | + } | ||
| 53 | + long before = repo.count(); | ||
| 54 | + repo.saveAll(users); | ||
| 55 | + long after = repo.count(); | ||
| 56 | + assertEquals(before + 50, after, "saveAll 应新增 50 条"); | ||
| 57 | + | ||
| 58 | + // 抽查回填的主键均非空 | ||
| 59 | + for (User u : users) { | ||
| 60 | + assertTrue(u.getId() != null, "批量保存后主键应回填"); | ||
| 61 | + } | ||
| 62 | + log("P2 saveAll(50) 成功:count %d -> %d,主键全部回填", before, after); | ||
| 63 | + | ||
| 64 | + // 清理:用显式 escape '!' 的查询(避开转义语义差异) | ||
| 65 | + List<User> toDelete = repo.searchByName("%t_jpa_batch_%", PageRequest.of(0, 100)).getContent(); | ||
| 66 | + repo.deleteAll(toDelete); | ||
| 67 | + assertEquals(before, repo.count(), "清理后应恢复原行数"); | ||
| 68 | + log("P2 清理完成,恢复 %d 行", before); | ||
| 69 | + } | ||
| 70 | +} | ||
| @@ -0,0 +1,275 @@ | |||
| 1 | +package org.opengauss.smoke.jpacases; | ||
| 2 | + | ||
| 3 | +import org.junit.jupiter.api.BeforeAll; | ||
| 4 | +import org.junit.jupiter.api.DisplayName; | ||
| 5 | +import org.junit.jupiter.api.Test; | ||
| 6 | + | ||
| 7 | +import java.io.ByteArrayInputStream; | ||
| 8 | +import java.security.MessageDigest; | ||
| 9 | +import java.sql.Connection; | ||
| 10 | +import java.sql.PreparedStatement; | ||
| 11 | +import java.sql.ResultSet; | ||
| 12 | +import java.sql.SQLException; | ||
| 13 | +import java.sql.Statement; | ||
| 14 | +import java.util.Arrays; | ||
| 15 | + | ||
| 16 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 17 | +import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| 18 | +import static org.opengauss.smoke.jpacases.JpaDbUtil.*; | ||
| 19 | + | ||
| 20 | +/** | ||
| 21 | + * 协议命令专项(评审意见:COM_STMT_PREPARE / COM_STMT_SEND_LONG_DATA / | ||
| 22 | + * COM_STMT_EXECUTE 最好覆盖到每个 command)。 | ||
| 23 | + * 与 #27 报告(springboot-connect-opengauss-b)7.1-7.9 同一套设计,按"JDBC 是否有触发路径"逐条实测: | ||
| 24 | + * C1 COM_QUERY(普通 Statement,文本协议) | ||
| 25 | + * C2 COM_INIT_DB(连接建立携带 database;setCatalog + useLocalSessionState=false) | ||
| 26 | + * C3 COM_PING(isValid) | ||
| 27 | + * C4 COM_STMT_PREPARE / COM_STMT_EXECUTE / COM_STMT_CLOSE(服务端预编译) | ||
| 28 | + * C5 COM_STMT_SEND_LONG_DATA(服务端预编译 + 1MB LONGBLOB 流式写入) | ||
| 29 | + * C6 COM_STMT_RESET(clearParameters 后复用同一预编译语句) | ||
| 30 | + * C7 COM_STMT_FETCH(useCursorFetch + setFetchSize) | ||
| 31 | + * C8 COM_SET_OPTION(allowMultiQueries 多语句) | ||
| 32 | + * C9 COM_QUIT(close) | ||
| 33 | + * COM_FIELD_LIST / COM_PROCESS_INFO / COM_STATISTICS / COM_PROCESS_KILL / COM_CHANGE_USER | ||
| 34 | + * 等命令 JDBC 驱动无触发路径(不提供对应 API),在自测报告 4.1.6 说明,不在此处用例化。 | ||
| 35 | + */ | ||
| 36 | +class ProtocolCommandTest { | ||
| 37 | + | ||
| 38 | + | ||
| 39 | + static void createData() throws SQLException { | ||
| 40 | + try (Connection c = raw()) { | ||
| 41 | + exec(c, | ||
| 42 | + "DROP TABLE IF EXISTS jpa_cmd", | ||
| 43 | + "CREATE TABLE jpa_cmd (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), data LONGBLOB)"); | ||
| 44 | + try (PreparedStatement ps = c.prepareStatement( | ||
| 45 | + "INSERT INTO jpa_cmd (name) VALUES (?)")) { | ||
| 46 | + for (int i = 1; i <= 500; i++) { | ||
| 47 | + ps.setString(1, "cmd_" + i); | ||
| 48 | + ps.addBatch(); | ||
| 49 | + } | ||
| 50 | + ps.executeBatch(); | ||
| 51 | + } | ||
| 52 | + log("C0 准备协议命令测试数据(jpa_cmd 500 行,含 LONGBLOB 列)"); | ||
| 53 | + } | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + private static int count(Connection c) throws SQLException { | ||
| 57 | + try (Statement st = c.createStatement(); | ||
| 58 | + ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM jpa_cmd")) { | ||
| 59 | + rs.next(); | ||
| 60 | + return rs.getInt(1); | ||
| 61 | + } | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + | ||
| 65 | + | ||
| 66 | + void comQuery() throws SQLException { | ||
| 67 | + try (Connection c = raw()) { | ||
| 68 | + int before = count(c); | ||
| 69 | + try (Statement st = c.createStatement()) { | ||
| 70 | + st.executeUpdate("INSERT INTO jpa_cmd (name) VALUES ('query_a')"); | ||
| 71 | + st.executeUpdate("INSERT INTO jpa_cmd (name) VALUES ('query_b')"); | ||
| 72 | + } | ||
| 73 | + int after = count(c); | ||
| 74 | + log("C1 COM_QUERY 文本协议插入/查询正常:%d -> %d 行", before, after); | ||
| 75 | + assertEquals(before + 2, after); | ||
| 76 | + } | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + | ||
| 80 | + | ||
| 81 | + void comInitDb() throws SQLException { | ||
| 82 | + // 连接建立阶段:url 指定 database,驱动握手时即发出 COM_INIT_DB; | ||
| 83 | + // useLocalSessionState=false 关闭本地状态缓存,setCatalog 按协议重新确认 database。 | ||
| 84 | + try (Connection c = raw("useLocalSessionState=false")) { | ||
| 85 | + assertEquals(DB, c.getCatalog()); | ||
| 86 | + c.setCatalog(DB); | ||
| 87 | + assertTrue(count(c) >= 500, "切换 catalog 后查询应正常"); | ||
| 88 | + log("C2 COM_INIT_DB 连接初始化/切换 database 正常:catalog=%s", c.getCatalog()); | ||
| 89 | + } | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + | ||
| 93 | + | ||
| 94 | + void comPing() throws SQLException { | ||
| 95 | + try (Connection c = raw()) { | ||
| 96 | + boolean ok = c.isValid(2); | ||
| 97 | + log("C3 COM_PING 心跳正常:isValid=%s", ok); | ||
| 98 | + assertTrue(ok); | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + | ||
| 103 | + | ||
| 104 | + void comStmtPrepareExecuteClose() throws SQLException { | ||
| 105 | + // cachePrepStmts=false 确保每次 prepareStatement 真实发出 COM_STMT_PREPARE | ||
| 106 | + try (Connection c = raw("useServerPrepStmts=true&cachePrepStmts=false")) { | ||
| 107 | + try (PreparedStatement ps = c.prepareStatement( | ||
| 108 | + "INSERT INTO jpa_cmd (name) VALUES (?)")) { | ||
| 109 | + ps.setString(1, "prep_c4"); | ||
| 110 | + assertEquals(1, ps.executeUpdate()); | ||
| 111 | + } // 语句 close -> COM_STMT_CLOSE | ||
| 112 | + try (PreparedStatement ps = c.prepareStatement( | ||
| 113 | + "SELECT name FROM jpa_cmd WHERE name = ?")) { | ||
| 114 | + ps.setString(1, "prep_c4"); | ||
| 115 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 116 | + assertTrue(rs.next(), "服务端预编译语句执行后应查回已插入行"); | ||
| 117 | + assertEquals("prep_c4", rs.getString(1)); | ||
| 118 | + } | ||
| 119 | + } | ||
| 120 | + log("C4 COM_STMT_PREPARE/EXECUTE/CLOSE 服务端预编译插入+查询往返正常"); | ||
| 121 | + } | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + | ||
| 125 | + | ||
| 126 | + void comStmtSendLongData() throws SQLException { | ||
| 127 | + try (Connection c = raw("useServerPrepStmts=true")) { | ||
| 128 | + // ① 链路基线:无零字节 1MB(全 0x41)应完整往返(MD5 一致)。 | ||
| 129 | + byte[] blob = new byte[1024 * 1024]; | ||
| 130 | + Arrays.fill(blob, (byte) 0x41); | ||
| 131 | + String expectMd5 = md5(blob); | ||
| 132 | + try (PreparedStatement ps = c.prepareStatement( | ||
| 133 | + "INSERT INTO jpa_cmd (name, data) VALUES (?, ?)")) { | ||
| 134 | + ps.setString(1, "long_c5"); | ||
| 135 | + ps.setBlob(2, new ByteArrayInputStream(blob)); | ||
| 136 | + assertEquals(1, ps.executeUpdate()); | ||
| 137 | + } | ||
| 138 | + byte[] read; | ||
| 139 | + try (PreparedStatement ps = c.prepareStatement( | ||
| 140 | + "SELECT data FROM jpa_cmd WHERE name = ?")) { | ||
| 141 | + ps.setString(1, "long_c5"); | ||
| 142 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 143 | + assertTrue(rs.next()); | ||
| 144 | + read = rs.getBytes(1); | ||
| 145 | + } | ||
| 146 | + } | ||
| 147 | + log("C5a COM_STMT_SEND_LONG_DATA 1MB 无零字节 LONGBLOB 流式写入并读回:%d -> %d 字节,MD5 %s", | ||
| 148 | + blob.length, read.length, expectMd5.equals(md5(read)) ? "一致" : "不一致"); | ||
| 149 | + assertEquals(blob.length, read.length); | ||
| 150 | + assertEquals(expectMd5, md5(read)); | ||
| 151 | + | ||
| 152 | + // ② FINDING F 回归断言:二进制内容遇 0x00 被服务端按 C 字符串截断为前缀 | ||
| 153 | + // (与 #27 报告 FINDING F 同源;MySQL 原生完整存储)。 | ||
| 154 | + // 服务端修复后本断言自动失败,提醒移除规避说明并更新自测报告。 | ||
| 155 | + byte[] mixed = new byte[200]; | ||
| 156 | + Arrays.fill(mixed, (byte) 0x41); | ||
| 157 | + mixed[150] = 0x00; | ||
| 158 | + Arrays.fill(mixed, 151, mixed.length, (byte) 0x42); | ||
| 159 | + try (PreparedStatement ps = c.prepareStatement( | ||
| 160 | + "INSERT INTO jpa_cmd (name, data) VALUES (?, ?)")) { | ||
| 161 | + ps.setString(1, "long_c5_mixed"); | ||
| 162 | + ps.setBlob(2, new ByteArrayInputStream(mixed)); | ||
| 163 | + assertEquals(1, ps.executeUpdate()); | ||
| 164 | + } | ||
| 165 | + byte[] mixedRead; | ||
| 166 | + try (PreparedStatement ps = c.prepareStatement( | ||
| 167 | + "SELECT data FROM jpa_cmd WHERE name = ?")) { | ||
| 168 | + ps.setString(1, "long_c5_mixed"); | ||
| 169 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 170 | + assertTrue(rs.next()); | ||
| 171 | + mixedRead = rs.getBytes(1); | ||
| 172 | + } | ||
| 173 | + } | ||
| 174 | + log("C5b FINDING F:含 0x00 的 200 字节读回 %d 字节(=第一个 0x00 前的前缀长度,MySQL 原生应回 200)", | ||
| 175 | + mixedRead.length); | ||
| 176 | + assertEquals(150, mixedRead.length, | ||
| 177 | + "FINDING F 回归断言:dolphin 遇 0x00 截断为前缀;服务端修复后应回 200 字节"); | ||
| 178 | + } | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + | ||
| 182 | + | ||
| 183 | + void comStmtReset() throws SQLException { | ||
| 184 | + try (Connection c = raw("useServerPrepStmts=true")) { | ||
| 185 | + int before = count(c); | ||
| 186 | + try (PreparedStatement ps = c.prepareStatement( | ||
| 187 | + "INSERT INTO jpa_cmd (name) VALUES (?)")) { | ||
| 188 | + ps.setString(1, "reset_a"); | ||
| 189 | + assertEquals(1, ps.executeUpdate()); | ||
| 190 | + ps.clearParameters(); // 参数置空,下次执行时驱动发 COM_STMT_RESET | ||
| 191 | + ps.setString(1, "reset_b"); | ||
| 192 | + assertEquals(1, ps.executeUpdate()); | ||
| 193 | + } | ||
| 194 | + int after = count(c); | ||
| 195 | + log("C6 COM_STMT_RESET clearParameters 后复用预编译语句正常:%d -> %d 行", before, after); | ||
| 196 | + assertEquals(before + 2, after); | ||
| 197 | + } | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + | ||
| 201 | + | ||
| 202 | + void comStmtFetch() throws SQLException { | ||
| 203 | + try (Connection c = raw("useCursorFetch=true&useServerPrepStmts=true")) { | ||
| 204 | + int n; | ||
| 205 | + try (PreparedStatement ps = c.prepareStatement( | ||
| 206 | + "SELECT id, name FROM jpa_cmd ORDER BY id")) { | ||
| 207 | + ps.setFetchSize(100); | ||
| 208 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 209 | + n = 0; | ||
| 210 | + while (rs.next()) { | ||
| 211 | + n++; | ||
| 212 | + } | ||
| 213 | + } | ||
| 214 | + } | ||
| 215 | + log("C7 COM_STMT_FETCH 游标拉取无报错,读取 %d 行。协议层是否真逐批返回需服务端日志佐证" | ||
| 216 | + + "(同 #27 报告 6.1 口径,报告中按'驱动层可用、协议层待确认'记录)", n); | ||
| 217 | + assertTrue(n >= 500, "应至少读回准备阶段写入的 500 行"); | ||
| 218 | + } | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + | ||
| 222 | + | ||
| 223 | + void comSetOption() throws SQLException { | ||
| 224 | + try (Connection c = raw("allowMultiQueries=true")) { | ||
| 225 | + int before = count(c); | ||
| 226 | + int cnt = -1; | ||
| 227 | + boolean hasResults; | ||
| 228 | + try (Statement st = c.createStatement()) { | ||
| 229 | + hasResults = st.execute("INSERT INTO jpa_cmd (name) VALUES ('mq_1'); " | ||
| 230 | + + "INSERT INTO jpa_cmd (name) VALUES ('mq_2'); " | ||
| 231 | + + "SELECT COUNT(*) FROM jpa_cmd"); | ||
| 232 | + // FINDING G:dolphin 多语句结果返回结构不完整——execute() 只反映首语句 | ||
| 233 | + // 结果,getMoreResults() 取不到后续语句的结果集(与 #27 报告 FINDING G 同源; | ||
| 234 | + // MySQL 原生按语句逐个返回全部结果)。服务端修复后 more=true 且 cnt 能读到 | ||
| 235 | + // COUNT(*),本断言自动失败提醒更新报告。 | ||
| 236 | + hasResults = st.getMoreResults(); | ||
| 237 | + if (hasResults) { | ||
| 238 | + try (ResultSet rs = st.getResultSet()) { | ||
| 239 | + if (rs.next()) { | ||
| 240 | + cnt = rs.getInt(1); | ||
| 241 | + } | ||
| 242 | + } | ||
| 243 | + } | ||
| 244 | + } | ||
| 245 | + int after = count(c); | ||
| 246 | + log("C8 COM_SET_OPTION 多语句执行生效:%d -> %d 行(后续 SELECT 结果集经 getMoreResults " | ||
| 247 | + + "不可得,见 FINDING G)", before, after); | ||
| 248 | + assertEquals(before + 2, after, "多语句中的 DML 应全部生效"); | ||
| 249 | + assertEquals(-1, cnt, | ||
| 250 | + "FINDING G 回归断言:dolphin 当前不回传后续 SELECT 结果集;服务端修复后 cnt 应=落库行数"); | ||
| 251 | + } | ||
| 252 | + } | ||
| 253 | + | ||
| 254 | + | ||
| 255 | + | ||
| 256 | + void comQuit() throws SQLException { | ||
| 257 | + Connection c = raw(); | ||
| 258 | + c.close(); | ||
| 259 | + assertTrue(c.isClosed()); | ||
| 260 | + log("C9 COM_QUIT 连接关闭正常(isClosed=true)"); | ||
| 261 | + } | ||
| 262 | + | ||
| 263 | + private static String md5(byte[] data) { | ||
| 264 | + try { | ||
| 265 | + byte[] digest = MessageDigest.getInstance("MD5").digest(data); | ||
| 266 | + StringBuilder sb = new StringBuilder(); | ||
| 267 | + for (byte b : digest) { | ||
| 268 | + sb.append(String.format("%02x", b)); | ||
| 269 | + } | ||
| 270 | + return sb.toString(); | ||
| 271 | + } catch (Exception e) { | ||
| 272 | + throw new IllegalStateException(e); | ||
| 273 | + } | ||
| 274 | + } | ||
| 275 | +} | ||
【检视意见】增加点基础功能测试,比如COM_STMT_PREPARE、COM_STMT_SEND_LONG_DATA、COM_STMT_EXECUTE 最好覆盖到每个command