diff --git a/README.md b/README.md index 3770fbb..f5fd597 100644 --- a/README.md +++ b/README.md @@ -67,12 +67,12 @@ |----------|-----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| | 国产操作系统 | 银河麒麟、统信 UOS 等 Linux 系统 | 参考 [《Linux 部署》](https://doc.iocoder.cn/deployment-linux/),选择与 CPU 架构匹配的 JDK | | 关系型数据库 | MySQL、MariaDB、Oracle、PostgreSQL、SQL Server | 默认使用 MySQL | -| 国产关系型数据库 | 达梦 DM、人大金仓 KingbaseES、openGauss、瀚高 HighGo、OceanBase、TiDB | 参考 [《国产信创数据库》](https://doc.iocoder.cn/xinchuang-db/) | +| 国产关系型数据库 | 达梦 DM、人大金仓 KingbaseES、openGauss、瀚高 HighGo、OceanBase、TiDB、华为 GaussDB、云和恩墨 MogDB、海量 Vastbase G100、PolarDB PostgreSQL 版 | 参考 [《国产信创数据库》](https://doc.iocoder.cn/xinchuang-db/)。GaussDB、MogDB、Vastbase G100 使用 PG 兼容模式;神通数据库提供实测差异说明,待专门适配 | | 国产缓存 | 东方通 TongRDS、宝兰德 BES CacheDB | 替换 Redis,参考 [《国产信创数据库》](https://doc.iocoder.cn/xinchuang-db/)。BES CacheDB 提供接入说明,待介质验证 | | 国产应用服务器 | 东方通 TongWeb、宝兰德 BES AppServer | 替换 Tomcat,参考 [《东方通服务器部署》](https://doc.iocoder.cn/deployment-tong/)、[《宝兰德服务器部署》](https://doc.iocoder.cn/deployment-bes/)。BES AppServer 提供接入说明,待介质验证 | | 国产 Web 服务器 | 东方通 TongHttpServer | 替换 Nginx,提供静态资源、反向代理,参考 [《东方通服务器部署》](https://doc.iocoder.cn/deployment-tong/) | -* 数据库脚本:[`sql`](sql/) 目录提供 MySQL、Oracle、PostgreSQL、SQL Server、达梦、人大金仓、openGauss、瀚高、OceanBase 的项目初始化脚本(含 Quartz),优先使用对应目录;MariaDB、TiDB 直接使用 MySQL 脚本 +* 数据库脚本:[`sql`](sql/) 目录提供 MySQL、Oracle、PostgreSQL、SQL Server、达梦、人大金仓、openGauss、瀚高、OceanBase 的项目初始化脚本(含 Quartz),优先使用对应目录;MariaDB、TiDB 直接使用 MySQL 脚本;GaussDB、MogDB、Vastbase G100 基于 openGauss 脚本导入,PolarDB PostgreSQL 版直接使用 PostgreSQL 脚本 * 其它数据库、后续表结构变更,可参考 [`sql/tools`](sql/tools/README.md) 的转换工具及 [《表结构变更(版本升级)》](https://doc.iocoder.cn/sql-update/#_3-数据库转换),转换后仍需按实际数据库调整、测试 ## 🐳 项目关系 diff --git a/sql/tools/README.md b/sql/tools/README.md index 02469f7..096ca91 100644 --- a/sql/tools/README.md +++ b/sql/tools/README.md @@ -6,7 +6,7 @@ ## 1. 测试数据库的快速启动 -基于 Docker Compose,快速启动 MySQL、Oracle、PostgreSQL、SQL Server、OceanBase、TiDB 等数据库。 +基于 Docker Compose,快速启动 MySQL、Oracle、PostgreSQL、SQL Server、OceanBase、TiDB、MogDB 等数据库。 注意!使用 Docker Compose 启动完测试数据后,因为会自动导入项目的 SQL 脚本,所以可能需要等待 1-2 分钟。 @@ -175,6 +175,50 @@ password: 123456 > 注意:端口只绑定本机 `127.0.0.1:4000`。数据保存在 `tidb_pd`、`tidb_tikv` 数据卷中,重启不需要重新导入 SQL。如需重建,执行 `docker compose down tidb tidb-tikv tidb-pd` 后,删除 `ruoyi-vue-pro_tidb_pd`、`ruoyi-vue-pro_tidb_tikv` 数据卷。该环境仅用于开发测试,生产部署见 [TiDB 官方文档](https://docs.pingcap.com/zh/tidb/v8.5/production-deployment-using-tiup/)。 +### 1.11 华为 GaussDB + +GaussDB 没有官方 Docker 镜像,需要自行安装,例如华为云官网的 [GaussDB 混合云开发者版](https://www.huaweicloud.com/intl/zh-cn/product/gaussdb/download.html)(官方声明仅供个人学习、适配验证或测试,不得用于商业用途)。项目复用 `sql/opengauss` 脚本,导入到 PG 兼容模式的数据库中。 + +① 初始化数据库:在安装了 GaussDB `gsql` 客户端的机器上(例如以数据库安装用户登录 GaussDB 服务器,并复制项目的 `sql` 目录),使用具有 CREATEDB 权限的账号执行: + +```Bash +GAUSSDB_PORT='数据库端口' GAUSSDB_USER='数据库账号' GAUSSDB_PASSWORD='数据库密码' bash gaussdb/create_schema.sh +``` + +> 注意:脚本创建 PG 兼容模式(`DBCOMPATIBILITY = 'PG'`)、UTF8 编码的 `ruoyi-vue-pro` 数据库,依次导入 `sql/opengauss/quartz.sql`、`sql/opengauss/ruoyi-vue-pro.sql`。GaussDB 默认创建的是 A(Oracle)兼容模式的数据库,项目脚本需要使用 PG 兼容模式。GaussDB 已内置 `dual` 视图,原脚本开头的 `DROP TABLE IF EXISTS dual` 会报 `"dual" is not a table` 错误,所以脚本只跳过创建 `dual` 表的一段,不修改内置视图,其余 SQL 保持不变;该段格式与预期不一致,或者未检测到内置 `dual` 视图时,拒绝导入。初始化只允许创建全新的数据库:数据库已存在(包括空库)或重复执行时,脚本拒绝导入并返回非零状态;导入失败时保留数据库便于排查。 + +> 注意:`GAUSSDB_HOST` 为空时,使用本机 Unix Socket 连接;远程连接时,需要在 `gs_hba.conf`(不是 `pg_hba.conf`)中为该账号、客户端地址配置密码认证(例如 sha256),不要使用 trust。GaussDB 的初始用户不允许远程连接,项目请使用单独创建的账号。 + +② 项目使用 GaussDB 原生 JDBC 驱动(`com.huawei.gaussdb.jdbc.Driver`,`jdbc:gaussdb://127.0.0.1:端口/ruoyi-vue-pro`),按 PostgreSQL 处理序列主键、Quartz、工作流等差异,驱动需要引入到 `yudao-server/pom.xml` 的 `` 中。详见 [《国产信创数据库》](https://doc.iocoder.cn/xinchuang-db/#_7-华为-gaussdb-数据库) 文档。 + +### 1.12 MogDB + +使用云和恩墨官方 MogDB 5.0.0 容器镜像(官方说明仅用于测试),支持 x86_64 和 MacBook Apple Silicon。`mogdb` 服务属于 `mogdb` profile,执行 `docker compose up -d` 时不会启动。 + +① 启动数据库: + +```Bash +docker compose up -d mogdb +``` + +② 初始化数据库:创建 PG 兼容模式(`DBCOMPATIBILITY = 'PG'`)、UTF8 编码的 `ruoyi-vue-pro` 数据库,所有者为 `mogdb` 用户,并依次导入 `sql/opengauss/quartz.sql`、`sql/opengauss/ruoyi-vue-pro.sql`: + +```Bash +docker compose exec -u omm mogdb bash /tmp/create_schema.sh +``` + +> 注意:MogDB 默认创建的是 A(Oracle)兼容模式的数据库,`varchar(n)` 按字节计算长度,导入项目脚本时会在中文数据处报 `value too long for type character varying(32)` 错误,所以需要使用 PG 兼容模式。初始化只允许创建全新的数据库:数据库已存在(包括空库)或重复执行时,脚本拒绝导入并返回非零状态;导入失败时保留数据库便于排查,重新初始化需要执行 `docker compose down mogdb` 后,删除 `ruoyi-vue-pro_mogdb` 数据卷。 + +③ 项目使用 openGauss JDBC 驱动(`org.opengauss.Driver`),将数据源配置中的地址、账号和密码修改为: + +```yaml +url: jdbc:opengauss://127.0.0.1:15432/ruoyi-vue-pro +username: mogdb +password: Yudao@2024 +``` + +> 注意:端口只绑定本机 `127.0.0.1:15432`;`omm` 初始用户不允许远程连接,项目使用 `mogdb` 用户。`jdbc:opengauss:` 按 PostgreSQL 处理序列主键、Quartz 等差异,驱动需要引入到 `yudao-server/pom.xml` 的 `` 中。详见 [《国产信创数据库》](https://doc.iocoder.cn/xinchuang-db/#_11-云和恩墨-mogdb-数据库) 文档。 + ## 1.X 容器的销毁重建 开发测试过程中,有时候需要创建全新干净的数据库。由于测试数据 Docker 容器采用数据卷 Volume 挂载数据库实例的数据目录,因此销毁数据需要停止容器后,删除数据卷,然后再重新创建容器。 diff --git a/sql/tools/docker-compose.yaml b/sql/tools/docker-compose.yaml index 84c83bd..08c6d5f 100644 --- a/sql/tools/docker-compose.yaml +++ b/sql/tools/docker-compose.yaml @@ -12,6 +12,7 @@ volumes: oceanbase_config: { } tidb_pd: { } tidb_tikv: { } + mogdb: { } services: mysql: @@ -272,3 +273,23 @@ services: - ../mysql/ruoyi-vue-pro.sql:/tmp/schema.sql:ro - ../mysql/quartz.sql:/tmp/quartz.sql:ro - ./tidb/create_schema.sh:/tmp/create_schema.sh:ro + + # MogDB 测试库:云和恩墨官方容器镜像(仅用于测试),属于 mogdb profile,默认不启动 + # 启动:docker compose up -d mogdb + # 初始化:docker compose exec -u omm mogdb bash /tmp/create_schema.sh(创建 PG 兼容模式的 ruoyi-vue-pro 数据库,并导入 sql/opengauss 脚本) + mogdb: + image: swr.cn-north-4.myhuaweicloud.com/mogdb/mogdb:5.0.0 + profiles: ["mogdb"] + restart: unless-stopped + shm_size: 1g + environment: + # omm 超级用户、mogdb 用户的密码:至少 8 位,包含大小写字母、数字及特殊字符 + GS_PASSWORD: Yudao@2024 + ports: + # 只绑定本机,应用使用 mogdb 用户连接(omm 初始用户不允许远程连接) + - "127.0.0.1:15432:5432" + volumes: + - mogdb:/var/lib/mogdb + - ../opengauss/quartz.sql:/tmp/quartz.sql:ro + - ../opengauss/ruoyi-vue-pro.sql:/tmp/schema.sql:ro + - ./mogdb/create_schema.sh:/tmp/create_schema.sh:ro diff --git a/sql/tools/gaussdb/create_schema.sh b/sql/tools/gaussdb/create_schema.sh new file mode 100644 index 0000000..c85b54a --- /dev/null +++ b/sql/tools/gaussdb/create_schema.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# 初始化 GaussDB 数据库:使用 GaussDB 的 gsql 客户端,创建 PG 兼容模式(DBCOMPATIBILITY = 'PG')、UTF8 编码的全新数据库,并导入 sql/opengauss 脚本 +# GaussDB 已内置 dual 视图(sys.dual、pg_catalog.dual),导入时只跳过 sql/opengauss/ruoyi-vue-pro.sql 开头创建 dual 表的一段,不修改内置视图,其余 SQL 保持不变 +# 数据库已存在(包括空库)时拒绝导入;任一步骤失败都以非零状态退出 +# 执行:GAUSSDB_PORT=8000 GAUSSDB_USER='数据库账号' GAUSSDB_PASSWORD='数据库密码' bash sql/tools/gaussdb/create_schema.sh +set -euo pipefail + +host="${GAUSSDB_HOST:-}" # 为空时,使用本机的 Unix Socket 连接 +port="${GAUSSDB_PORT:?GAUSSDB_PORT 未设置,请设置为 GaussDB 的端口}" +user="${GAUSSDB_USER:?GAUSSDB_USER 未设置,请设置为具有 CREATEDB 权限的数据库账号}" +password="${GAUSSDB_PASSWORD:?GAUSSDB_PASSWORD 未设置,请设置为数据库账号的实际密码}" +maintenance_db="${GAUSSDB_MAINTENANCE_DB:-postgres}" +database="${GAUSSDB_DATABASE:-ruoyi-vue-pro}" +sql_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../opengauss" && pwd)" + +if [[ ! "${database}" =~ ^[A-Za-z0-9_-]+$ ]]; then + echo "数据库名 ${database} 不合法,只允许字母、数字、下划线和中划线" >&2 + exit 1 +fi +if ! command -v gsql >/dev/null 2>&1; then + echo "未找到 gsql 客户端:请在安装了 GaussDB gsql 客户端的机器上执行,例如以数据库安装用户登录 GaussDB 服务器" >&2 + exit 1 +fi + +client=(gsql -p "${port}" -U "${user}" -W "${password}" -X -q -v ON_ERROR_STOP=1) +if [ -n "${host}" ]; then + client+=(-h "${host}") +fi + +# 1. 生成跳过 dual 段的临时脚本:只允许跳过「-- Table structure for dual」到其后第一个「-- @formatter:on」之间的 dual 建表、注释及插入语句 +schema_file="$(mktemp)" +trap 'rm -f "${schema_file}"' EXIT +# 注释语句只允许单个字符串常量(内部的单引号写作 ''),字符串后只能是行尾的分号,避免同一行中的其它 SQL 被一起跳过 +if ! awk -v q="'" ' + BEGIN { comment = "^COMMENT ON TABLE dual IS " q "([^" q "]|" q q ")*" q ";$" } + /^-- Table structure for dual$/ { skipping = 1; blocks++ } + skipping { + if ($0 ~ /^(DROP TABLE IF EXISTS dual;|CREATE TABLE dual|INSERT INTO dual VALUES \(1\);)$/) { + statements++ + } else if ($0 !~ /^(--.*|\(|[[:space:]]*id int2|\);)?$/ && $0 !~ comment) { + unexpected = 1 + } + if ($0 ~ /^-- @formatter:on$/) { + skipping = 0 + } + next + } + { print } + END { exit (blocks == 1 && statements == 3 && !skipping && !unexpected) ? 0 : 1 } +' "${sql_dir}/ruoyi-vue-pro.sql" > "${schema_file}" \ + || grep -Eq '^(DROP TABLE IF EXISTS dual;|CREATE TABLE dual|INSERT INTO dual )' "${schema_file}"; then + echo "${sql_dir}/ruoyi-vue-pro.sql 中 dual 表一段的格式与预期不一致,拒绝导入;请检查脚本是否由 sql/tools/convertor.py 生成" >&2 + exit 1 +fi + +# 2. 校验账号、密码及认证方式 +if ! "${client[@]}" -d "${maintenance_db}" -c 'SELECT 1' >/dev/null; then + echo "无法使用账号 ${user} 登录 ${maintenance_db} 数据库:请检查 GAUSSDB_HOST、GAUSSDB_PORT、GAUSSDB_USER、GAUSSDB_PASSWORD,以及 gs_hba.conf 中该账号、客户端地址的认证配置" >&2 + exit 1 +fi + +# 3. 校验 GaussDB 内置的 dual 视图:openGauss、MogDB 没有该视图,请直接导入 sql/opengauss 脚本 +dual_count=$("${client[@]}" -d "${maintenance_db}" -At -c "SELECT COUNT(*) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 'dual' AND c.relkind = 'v' AND n.nspname IN ('sys', 'pg_catalog');") +if [ "${dual_count}" = "0" ]; then + echo "未检测到 GaussDB 内置的 dual 视图,拒绝导入:该脚本只适用于 GaussDB,openGauss、MogDB 请直接导入 sql/opengauss 脚本" >&2 + exit 1 +fi + +# 4. 创建全新数据库:不判断是否存在,数据库已存在时直接失败,避免项目 SQL 中的 DROP TABLE 重建已有的同名表 +# 注意:GaussDB 默认创建的是 A(Oracle)兼容模式的数据库,项目脚本需要使用 PG 兼容模式 +if ! "${client[@]}" -d "${maintenance_db}" -c "CREATE DATABASE \"${database}\" ENCODING 'UTF8' DBCOMPATIBILITY = 'PG';"; then + echo "创建数据库 ${database} 失败:只允许初始化全新的数据库,数据库已存在(包括空库)时拒绝导入;如需重新初始化,请确认数据可以删除后,手动删除该数据库" >&2 + exit 1 +fi +compatibility=$("${client[@]}" -d "${database}" -At -c 'SHOW sql_compatibility;') +if [ "${compatibility}" != "PG" ]; then + echo "数据库 ${database} 的兼容模式为 ${compatibility},不是 PG,拒绝导入" >&2 + exit 1 +fi + +# 5. 按 quartz、ruoyi-vue-pro 的顺序导入脚本(ON_ERROR_STOP 遇到错误立即退出,失败时保留数据库,便于排查) +"${client[@]}" -d "${database}" -f "${sql_dir}/quartz.sql" +"${client[@]}" -d "${database}" -f "${schema_file}" + +# 只统计普通表、分区表;dual 使用 GaussDB 内置视图,不计入 +table_count=$("${client[@]}" -d "${database}" -At -c "SELECT COUNT(*) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND c.relkind IN ('r', 'p');") +echo "GaussDB 初始化完成:数据库 ${database} 共 ${table_count} 张表" diff --git a/sql/tools/mogdb/create_schema.sh b/sql/tools/mogdb/create_schema.sh new file mode 100644 index 0000000..e7d15e7 --- /dev/null +++ b/sql/tools/mogdb/create_schema.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# 初始化 MogDB 测试库:在 mogdb 容器内以 omm 用户执行,创建 PG 兼容模式(DBCOMPATIBILITY = 'PG')、UTF8 编码的全新数据库,并导入 sql/opengauss 脚本 +# 数据库已存在(包括空库)时拒绝导入;任一步骤失败都以非零状态退出 +# 执行:docker compose exec -u omm mogdb bash /tmp/create_schema.sh +set -euo pipefail + +# 官方镜像只在 omm 用户的 .profile 中设置 gsql 的 PATH,这里显式设置 +export GAUSSHOME="${GAUSSHOME:-/usr/local/mogdb}" +export PATH="${GAUSSHOME}/bin:${PATH}" +export LD_LIBRARY_PATH="${GAUSSHOME}/lib:${LD_LIBRARY_PATH:-}" + +port="${GS_PORT:-5432}" +owner="${MOGDB_OWNER:-mogdb}" # 应用连接使用的账号,官方镜像默认创建 mogdb 用户;omm 初始用户不允许远程连接 +password="${GS_PASSWORD:?GS_PASSWORD 未设置,请检查 docker-compose.yaml 中 mogdb 服务的环境变量}" +database="${MOGDB_DATABASE:-ruoyi-vue-pro}" + +if [[ ! "${database}" =~ ^[A-Za-z0-9_-]+$ ]]; then + echo "数据库名 ${database} 不合法,只允许字母、数字、下划线和中划线" >&2 + exit 1 +fi +if [[ ! "${owner}" =~ ^[A-Za-z0-9_]+$ ]]; then + echo "用户名 ${owner} 不合法,只允许字母、数字和下划线" >&2 + exit 1 +fi +if [ "$(id -un)" != "omm" ]; then + echo "请使用 omm 用户执行:docker compose exec -u omm mogdb bash /tmp/create_schema.sh" >&2 + exit 1 +fi + +client=(gsql -p "${port}" -X -q -v ON_ERROR_STOP=1) + +# 1. 等待 MogDB 可连接(首次启动时需要初始化数据目录) +for i in $(seq 1 60); do + if "${client[@]}" -d postgres -c 'SELECT 1' >/dev/null 2>&1; then + break + fi + if [ "${i}" -eq 60 ]; then + echo "等待 MogDB 启动超时,请执行 docker compose logs mogdb 检查启动日志" >&2 + exit 1 + fi + sleep 5 +done + +# 2. 校验数据库所有者 +if [ "$("${client[@]}" -d postgres -At -c "SELECT COUNT(*) FROM pg_catalog.pg_roles WHERE rolname = '${owner}';")" != "1" ]; then + echo "用户 ${owner} 不存在:请通过 MOGDB_OWNER 指定应用连接使用的账号" >&2 + exit 1 +fi + +# 3. 创建全新数据库:不判断是否存在,数据库已存在时直接失败,避免项目 SQL 中的 DROP TABLE 重建已有的同名表 +# 注意:MogDB 默认创建的是 A(Oracle)兼容模式的数据库,导入项目脚本会失败,需要使用 PG 兼容模式 +if ! "${client[@]}" -d postgres -c "CREATE DATABASE \"${database}\" OWNER \"${owner}\" ENCODING 'UTF8' DBCOMPATIBILITY = 'PG';"; then + echo "创建数据库 ${database} 失败:只允许初始化全新的数据库,数据库已存在(包括空库)时拒绝导入;如需重新初始化,请删除 MogDB 数据卷后重建" >&2 + exit 1 +fi + +# 4. 使用数据库所有者,按 quartz、ruoyi-vue-pro 的顺序导入脚本(ON_ERROR_STOP 遇到错误立即退出,失败时保留数据库,便于排查) +owner_client=("${client[@]}" -U "${owner}" -W "${password}" -d "${database}") +"${owner_client[@]}" -f /tmp/quartz.sql +"${owner_client[@]}" -f /tmp/schema.sql + +# 只统计普通表、分区表 +table_count=$("${owner_client[@]}" -At -c "SELECT COUNT(*) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND c.relkind IN ('r', 'p');") +echo "MogDB 初始化完成:数据库 ${database} 共 ${table_count} 张表" diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java index 62a5b08..43d3406 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java @@ -36,7 +36,7 @@ public enum DbTypeEnum { /** * PostgreSQL * - * 华为 openGauss 使用 ProductName 与 PostgreSQL 相同 + * 华为 openGauss(PostgreSQL JDBC 驱动)、MogDB(io.mogdb 的 openGauss JDBC 驱动)使用 ProductName 与 PostgreSQL 相同 */ POSTGRE_SQL(DbType.POSTGRE_SQL, "PostgreSQL", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"), @@ -62,7 +62,22 @@ public enum DbTypeEnum { /** * OceanBase */ - OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET(#{value}, #{column}) <> 0") + OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET(#{value}, #{column}) <> 0"), + + /** + * 华为 GaussDB + * + * 原生驱动(jdbc:gaussdb:)使用的 ProductName 为 GaussDB,需要使用 PG 兼容模式(DBCOMPATIBILITY = 'PG')创建的数据库 + */ + GAUSS_DB(DbType.GAUSS_DB, "GaussDB", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"), + + /** + * openGauss + * + * org.opengauss 的 openGauss JDBC 驱动(jdbc:opengauss:)使用的 ProductName 为 openGauss,连接 MogDB 时也是如此; + * 需要使用 PG 兼容模式(DBCOMPATIBILITY = 'PG')创建的数据库 + */ + OPENGAUSS(DbType.OPENGAUSS, "openGauss", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0") ; diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java index 8f92428..ccfe166 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java @@ -38,14 +38,20 @@ public class JdbcUtils { /** * 获得 URL 对应的 DB 类型 * - * 特殊:瀚高 HighGo 原生驱动(jdbc:highgo:)兼容 PostgreSQL,按 PostgreSQL 处理,复用其序列主键、Quartz 等适配 + * 特殊:以下原生驱动连接的数据库兼容 PostgreSQL,按 PostgreSQL 处理,复用其序列主键、Quartz、代码生成等适配: + * 1. 瀚高 HighGo(jdbc:highgo:) + * 2. openGauss、MogDB(jdbc:opengauss:),需要使用 PG 兼容模式(DBCOMPATIBILITY = 'PG')创建的数据库 + * 3. 华为 GaussDB(jdbc:gaussdb:),需要使用 PG 兼容模式(DBCOMPATIBILITY = 'PG')创建的数据库 + * + * 注意:MyBatis Plus 的分页仍按原生 URL 识别,例如说 GaussDB 使用 GaussDBDialect * * @param url URL * @return DB 类型 */ public static DbType getDbType(String url) { DbType dbType = com.baomidou.mybatisplus.extension.toolkit.JdbcUtils.getDbType(url); - return dbType == DbType.HIGH_GO ? DbType.POSTGRE_SQL : dbType; + return ObjectUtils.equalsAny(dbType, DbType.HIGH_GO, DbType.OPENGAUSS, DbType.GAUSS_DB) + ? DbType.POSTGRE_SQL : dbType; } /** diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessorTest.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessorTest.java index 63c73fa..bd5cc1c 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessorTest.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessorTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.StandardEnvironment; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -51,6 +52,51 @@ public class IdTypeEnvironmentPostProcessorTest { } @Test + public void testPostProcessEnvironment_gaussDBNativeUrl() { + // 准备参数 + StandardEnvironment environment = buildEnvironment("jdbc:gaussdb://127.0.0.1:8000/ruoyi-vue-pro", null); + + // 调用 + processor.postProcessEnvironment(environment, null); + + // 断言:GaussDB(PG 兼容模式)与 PostgreSQL 一致,使用序列主键及 PostgreSQLDelegate + assertEquals("INPUT", environment.getProperty(ID_TYPE_KEY)); + assertEquals(POSTGRESQL_DELEGATE, environment.getProperty(QUARTZ_DELEGATE_KEY)); + assertInstanceOf(PostgreKeyGenerator.class, new YudaoMybatisAutoConfiguration().keyGenerator(environment)); + } + + @Test + public void testPostProcessEnvironment_openGaussNativeUrl() { + // 准备参数:openGauss、MogDB 使用 openGauss JDBC 驱动 + StandardEnvironment environment = buildEnvironment("jdbc:opengauss://127.0.0.1:15432/ruoyi-vue-pro", null); + + // 调用 + processor.postProcessEnvironment(environment, null); + + // 断言:与 PostgreSQL 一致,使用序列主键及 PostgreSQLDelegate + assertEquals("INPUT", environment.getProperty(ID_TYPE_KEY)); + assertEquals(POSTGRESQL_DELEGATE, environment.getProperty(QUARTZ_DELEGATE_KEY)); + assertInstanceOf(PostgreKeyGenerator.class, new YudaoMybatisAutoConfiguration().keyGenerator(environment)); + } + + @Test + public void testPostProcessEnvironment_notNormalizedNativeUrls() { + // 海量 Vastbase 原生驱动、神通、PolarDB(兼容 Oracle)未按 PostgreSQL 处理:保持自增主键,且不设置 Quartz Delegate + for (String url : Arrays.asList("jdbc:vastbase://127.0.0.1:5432/ruoyi-vue-pro", + "jdbc:oscar://127.0.0.1:2003/OSRDB", "jdbc:polardb://127.0.0.1:1521/ruoyi-vue-pro")) { + // 准备参数 + StandardEnvironment environment = buildEnvironment(url, null); + + // 调用 + processor.postProcessEnvironment(environment, null); + + // 断言 + assertEquals("AUTO", environment.getProperty(ID_TYPE_KEY), url); + assertNull(environment.getProperty(QUARTZ_DELEGATE_KEY), url); + } + } + + @Test public void testPostProcessEnvironment_postgresqlUrl() { // 准备参数 StandardEnvironment environment = buildEnvironment("jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro", null); @@ -95,12 +141,19 @@ public class IdTypeEnvironmentPostProcessorTest { } @Test - public void testGetDbType_onlyNormalizeHighGo() { + public void testGetDbType_normalizePostgreSQLCompatible() { + // 按 PostgreSQL 处理:瀚高、openGauss(MogDB)、GaussDB 的原生驱动 assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType("jdbc:highgo://127.0.0.1:5866/ruoyi-vue-pro")); + assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType("jdbc:opengauss://127.0.0.1:15432/ruoyi-vue-pro")); + assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType("jdbc:gaussdb://127.0.0.1:8000/ruoyi-vue-pro")); assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType("jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro")); + // 保持 MyBatis Plus 的识别结果 assertEquals(DbType.MYSQL, JdbcUtils.getDbType("jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro")); assertEquals(DbType.KINGBASE_ES, JdbcUtils.getDbType("jdbc:kingbase8://127.0.0.1:54321/test")); assertEquals(DbType.DM, JdbcUtils.getDbType("jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO")); + assertEquals(DbType.VASTBASE, JdbcUtils.getDbType("jdbc:vastbase://127.0.0.1:5432/ruoyi-vue-pro")); + assertEquals(DbType.OSCAR, JdbcUtils.getDbType("jdbc:oscar://127.0.0.1:2003/OSRDB")); + assertEquals(DbType.OTHER, JdbcUtils.getDbType("jdbc:polardb://127.0.0.1:1521/ruoyi-vue-pro")); } private static StandardEnvironment buildEnvironment(String url, Map extra) { diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnumTest.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnumTest.java new file mode 100644 index 0000000..9f3cc07 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnumTest.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.mybatis.core.enums; + +import com.baomidou.mybatisplus.annotation.DbType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * {@link DbTypeEnum} 的单元测试 + * + * @author 芋道源码 + */ +public class DbTypeEnumTest { + + @Test + public void testFind() { + // 华为 GaussDB 原生驱动(jdbc:gaussdb:)返回的 ProductName + assertEquals(DbType.GAUSS_DB, DbTypeEnum.find("GaussDB")); + // org.opengauss 的 openGauss JDBC 驱动(jdbc:opengauss:)连接 MogDB 时返回的 ProductName + assertEquals(DbType.OPENGAUSS, DbTypeEnum.find("openGauss")); + // io.mogdb 的 openGauss JDBC 驱动连接 MogDB 时,返回的 ProductName 与 PostgreSQL 相同 + assertEquals(DbType.POSTGRE_SQL, DbTypeEnum.find("PostgreSQL")); + assertEquals(DbType.KINGBASE_ES, DbTypeEnum.find("KingbaseES")); + assertNull(DbTypeEnum.find("")); + } + + @Test + public void testGetFindInSetTemplate_postgreSQLCompatible() { + // GaussDB、openGauss(PG 兼容模式)与 PostgreSQL 使用相同的 POSITION 模板 + assertEquals(DbTypeEnum.getFindInSetTemplate(DbType.POSTGRE_SQL), DbTypeEnum.getFindInSetTemplate(DbType.GAUSS_DB)); + assertEquals(DbTypeEnum.getFindInSetTemplate(DbType.POSTGRE_SQL), DbTypeEnum.getFindInSetTemplate(DbType.OPENGAUSS)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java index b74558c..898cb45 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java @@ -113,6 +113,10 @@ public class MyBatisUtilsTest { MyBatisUtils.findInSet(DbType.POSTGRE_SQL, "websites", 1)); assertEquals("CHARINDEX(',' + CAST({2} AS varchar(255)) + ',', ',' + websites + ',') > 0", MyBatisUtils.findInSet(DbType.SQL_SERVER, "websites", 2)); + assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0", + MyBatisUtils.findInSet(DbType.GAUSS_DB, "websites", 0)); + assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0", + MyBatisUtils.findInSet(DbType.OPENGAUSS, "websites", 0)); } @Test diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java index 8297dbc..0559ab9 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java @@ -1,13 +1,17 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.config; import cn.hutool.core.collection.ListUtil; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmActivityBehaviorFactory; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import lombok.extern.slf4j.Slf4j; import org.flowable.common.engine.api.delegate.FlowableFunctionDelegate; import org.flowable.common.engine.api.delegate.event.FlowableEventListener; +import org.flowable.common.engine.impl.AbstractEngineConfiguration; import org.flowable.spring.SpringProcessEngineConfiguration; import org.flowable.spring.boot.EngineConfigurationConfigurer; import org.springframework.beans.factory.ObjectProvider; @@ -18,6 +22,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; /** @@ -26,6 +32,7 @@ import java.util.List; * @author jason */ @Configuration(proxyBeanMethods = false) +@Slf4j public class BpmFlowableConfiguration { /** @@ -53,6 +60,7 @@ public class BpmFlowableConfiguration { * * 1. 设置各种监听器 * 2. 设置自定义的 ActivityBehaviorFactory 实现 + * 3. 设置华为 GaussDB、openGauss(含 MogDB)的数据库类型 */ @Bean public EngineConfigurationConfigurer bpmProcessEngineConfigurationConfigurer( @@ -66,9 +74,36 @@ public class BpmFlowableConfiguration { configuration.setActivityBehaviorFactory(bpmActivityBehaviorFactory); // 设置自定义的函数 configuration.setCustomFlowableFunctionDelegates(ListUtil.toList(customFlowableFunctionDelegates.stream().iterator())); + // 设置华为 GaussDB、openGauss(含 MogDB)的数据库类型 + initPostgreSQLCompatibleDatabaseType(configuration); }; } + /** + * 以下原生驱动返回的 ProductName 不是 PostgreSQL,Flowable 无法识别,启动时会报错: + * 1. 华为 GaussDB 的原生驱动(jdbc:gaussdb:),返回 GaussDB + * 2. org.opengauss 的 openGauss JDBC 驱动(jdbc:opengauss:),返回 openGauss,连接 MogDB 时也是如此 + * + * 因此,在未显式设置 databaseType 时,按 PostgreSQL 处理(需要使用 PG 兼容模式创建的数据库)。 + * 只读取 Flowable 自身数据源的元数据,其它数据库不做处理,仍由 Flowable 自动识别 + * + * @param configuration Flowable 配置 + */ + private static void initPostgreSQLCompatibleDatabaseType(SpringProcessEngineConfiguration configuration) { + if (configuration.getDatabaseType() != null || configuration.getDataSource() == null) { + return; + } + try (Connection connection = configuration.getDataSource().getConnection()) { + if (ObjectUtils.equalsAny(connection.getMetaData().getDatabaseProductName(), + DbTypeEnum.GAUSS_DB.getProductName(), DbTypeEnum.OPENGAUSS.getProductName())) { + configuration.setDatabaseType(AbstractEngineConfiguration.DATABASE_TYPE_POSTGRES); + } + } catch (SQLException ex) { + // 获取失败时,不做处理,由 Flowable 自动识别时报错 + log.warn("[initPostgreSQLCompatibleDatabaseType][获取数据库类型失败]", ex); + } + } + // =========== 审批人相关的 Bean ========== @Bean diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfigurationTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfigurationTest.java new file mode 100644 index 0000000..1140da8 --- /dev/null +++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfigurationTest.java @@ -0,0 +1,137 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.config; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmActivityBehaviorFactory; +import org.flowable.common.engine.api.delegate.FlowableFunctionDelegate; +import org.flowable.common.engine.api.delegate.event.FlowableEventListener; +import org.flowable.common.engine.impl.AbstractEngineConfiguration; +import org.flowable.spring.SpringProcessEngineConfiguration; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.beans.factory.ObjectProvider; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Collections; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link BpmFlowableConfiguration} 的单元测试 + * + * @author 芋道源码 + */ +public class BpmFlowableConfigurationTest extends BaseMockitoUnitTest { + + @Mock + private ObjectProvider listeners; + @Mock + private ObjectProvider customFlowableFunctionDelegates; + + @Mock + private DataSource dataSource; + @Mock + private Connection connection; + @Mock + private DatabaseMetaData metaData; + + @Test + public void testBpmProcessEngineConfigurationConfigurer_gaussDB() throws SQLException { + // mock 方法:GaussDB 原生驱动返回的 ProductName + mockDatabaseProductName("GaussDB"); + // 准备参数 + SpringProcessEngineConfiguration configuration = buildConfiguration(null); + + // 调用 + configure(configuration); + + // 断言:按 PostgreSQL 处理,并关闭连接 + assertEquals(AbstractEngineConfiguration.DATABASE_TYPE_POSTGRES, configuration.getDatabaseType()); + verify(connection).close(); + } + + @Test + public void testBpmProcessEngineConfigurationConfigurer_openGauss() throws SQLException { + // mock 方法:org.opengauss 的 openGauss JDBC 驱动(jdbc:opengauss:)连接 MogDB 时返回的 ProductName + mockDatabaseProductName("openGauss"); + // 准备参数 + SpringProcessEngineConfiguration configuration = buildConfiguration(null); + + // 调用 + configure(configuration); + + // 断言:按 PostgreSQL 处理,并关闭连接 + assertEquals(AbstractEngineConfiguration.DATABASE_TYPE_POSTGRES, configuration.getDatabaseType()); + verify(connection).close(); + } + + @Test + public void testBpmProcessEngineConfigurationConfigurer_postgreSQL() throws SQLException { + // mock 方法:PostgreSQL,以及 io.mogdb 的 openGauss JDBC 驱动连接 MogDB 时返回的 ProductName + mockDatabaseProductName("PostgreSQL"); + // 准备参数 + SpringProcessEngineConfiguration configuration = buildConfiguration(null); + + // 调用 + configure(configuration); + + // 断言:不处理,仍由 Flowable 自动识别 + assertNull(configuration.getDatabaseType()); + verify(connection).close(); + } + + @Test + public void testBpmProcessEngineConfigurationConfigurer_explicitDatabaseType() throws SQLException { + // 准备参数:用户显式设置 databaseType + SpringProcessEngineConfiguration configuration = buildConfiguration(AbstractEngineConfiguration.DATABASE_TYPE_MYSQL); + + // 调用 + configure(configuration); + + // 断言:不覆盖,且不获取连接 + assertEquals(AbstractEngineConfiguration.DATABASE_TYPE_MYSQL, configuration.getDatabaseType()); + verify(dataSource, never()).getConnection(); + } + + @Test + public void testBpmProcessEngineConfigurationConfigurer_connectionError() throws SQLException { + // mock 方法:获取连接失败 + when(dataSource.getConnection()).thenThrow(new SQLException("connection refused")); + // 准备参数 + SpringProcessEngineConfiguration configuration = buildConfiguration(null); + + // 调用 + configure(configuration); + + // 断言:不处理,由 Flowable 自动识别时报错 + assertNull(configuration.getDatabaseType()); + } + + private void mockDatabaseProductName(String productName) throws SQLException { + when(dataSource.getConnection()).thenReturn(connection); + when(connection.getMetaData()).thenReturn(metaData); + when(metaData.getDatabaseProductName()).thenReturn(productName); + } + + private SpringProcessEngineConfiguration buildConfiguration(String databaseType) { + SpringProcessEngineConfiguration configuration = new SpringProcessEngineConfiguration(); + configuration.setDataSource(dataSource); + configuration.setDatabaseType(databaseType); + return configuration; + } + + private void configure(SpringProcessEngineConfiguration configuration) { + when(listeners.iterator()).thenReturn(Collections.emptyIterator()); + when(customFlowableFunctionDelegates.stream()).thenReturn(Stream.empty()); + new BpmFlowableConfiguration().bpmProcessEngineConfigurationConfigurer(listeners, + customFlowableFunctionDelegates, new BpmActivityBehaviorFactory()).configure(configuration); + } + +}