--- name: 支持达梦数据库 overview: 在 HoTime 框架的现有 Dialect 方言体系下,新增达梦(DM8)数据库方言实现,涉及驱动引入、方言适配、连接配置、缓存层/代码生成/备份模块的 DM 分支补充。 todos: - id: dm-driver content: 引入达梦 Go 驱动依赖(go.mod + import),确定驱动引入方式 status: pending - id: dm-dialect content: 在 db/dialect.go 中实现 DMDialect(Quote/Placeholder/UpsertSQL 等全部方法) status: pending - id: dm-init content: 在 db/db.go 的 initDialect() 中注册 dm 类型 status: pending - id: dm-connect content: 在 application.go 中添加 SetDmDB() 连接函数和 SetDB() 中的 dm 分支 status: pending - id: dm-config content: 在 var.go 中添加 dm 数据库配置说明 status: pending - id: dm-cache content: 在 cache/cache_db.go 中为 tableExists/createMainTable/createHistoryTable/migrateFromCached/set 添加 dm 分支 status: pending - id: dm-makecode content: 在 code/makecode.go 中添加 dm 的表结构查询逻辑 status: pending - id: dm-insert-returning content: 修改 db/crud.go 的 Insert() 函数,当 SupportsLastInsertId()=false 时使用 RETURNING + QueryRow 获取插入 ID status: pending - id: dm-upsert content: 在 db/crud.go 中新增 buildDMUpsert() 方法(MERGE INTO 语法),并在 Upsert() 中添加 dm 分支 status: pending - id: dm-backup content: 在 db/backup.go 中适配达梦的 DDL 导出和标识符引号 status: pending - id: dm-query-types content: 在 db/query.go 的 classifyDBType() 中补充达梦特有数据类型(NUMBER 等) status: pending - id: refactor-example content: 重构 example/main.go,最小化启动配置,拆分到 example/app/ 目录 status: pending - id: example-test-ctr content: 创建 example/app/test.go(通用 ORM 测试 Ctr)+ test_test.go(测试定义),含数据库无关的 CRUD/条件/JOIN/聚合/分页/批量/Upsert/事务 status: pending - id: example-mysql-ctr content: 创建 example/app/mysql.go(MySQL 特有 Ctr)+ mysql_test.go,含 DDL/SHOW TABLES/DESCRIBE/原生SQL status: pending - id: example-dmdb-ctr content: 创建 example/app/dmdb.go(达梦特有 Ctr)+ dmdb_test.go,含达梦 DDL/USER_TABLES/元数据/原生SQL status: pending - id: example-app-test content: 创建 example/app/init.go(路由注册)+ app_test.go(TestMain+TestApi),集成测试框架 status: pending - id: example-config content: 修改 example/config/config.json,mysql 改为 mysql-(假注释禁用),新增 dm 配置 status: pending isProject: false --- # 支持达梦(DM8)数据库 ## 现状分析 项目已有完善的 Dialect 接口体系([db/dialect.go](db/dialect.go)),以及 `initDialect()` 工厂方法([db/db.go](db/db.go)),当前支持 MySQL、PostgreSQL、SQLite 三种方言。需要新增达梦方言并在各处补充 `case "dm"` 分支。 ```mermaid flowchart TB subgraph currentState [当前架构] DialectIf["Dialect 接口
(db/dialect.go)"] MySQL["MySQLDialect"] PG["PostgreSQLDialect"] SQLite["SQLiteDialect"] DialectIf --> MySQL DialectIf --> PG DialectIf --> SQLite end subgraph newState [新增] DM["DMDialect"] DialectIf --> DM end ``` ## 达梦 vs MySQL 关键 SQL 差异 - **标识符引号**: 达梦使用双引号 `"name"`(同 PostgreSQL),MySQL 用反引号 - **自增列**: 达梦用 `IDENTITY(1,1)`,MySQL 用 `AUTO_INCREMENT` - **占位符**: 达梦使用 `?`(同 MySQL) - **LastInsertId**: 达梦不支持 `LAST_INSERT_ID()`,需用 `RETURNING "id"` 子句获取插入 ID(同 PostgreSQL) - **Upsert**: 达梦使用 `MERGE INTO ... USING ... WHEN MATCHED ... WHEN NOT MATCHED ...`(Oracle 风格) - **表存在检查**: `SELECT TABLE_NAME FROM USER_TABLES WHERE TABLE_NAME='xxx'` - **列信息查询**: `SELECT COLUMN_NAME, DATA_TYPE, COMMENTS FROM USER_COL_COMMENTS ...` - **时间戳转换**: 无 `FROM_UNIXTIME`,需用 `DATEADD(SECOND, ts, TIMESTAMP '1970-01-01 00:00:00')` - **INSERT IGNORE**: 不支持,需用 `MERGE INTO` 替代 - **DDL 导出**: 无 `SHOW CREATE TABLE`,使用系统视图组合 ## 涉及修改的文件 ### 1. 新增达梦 Go 驱动(go module 方式) 使用达梦官方 module 化驱动 `gitee.com/chunanyong/dm`,通过标准 go module 流程引入: ```bash go get gitee.com/chunanyong/dm go mod vendor ``` - [go.mod](go.mod) - 自动添加 `gitee.com/chunanyong/dm` 依赖 - [db/db.go](db/db.go) - 添加 `import _ "gitee.com/chunanyong/dm"` 并在 `initDialect()` 添加 `case "dm"` - `vendor/` 目录通过 `go mod vendor` 自动同步 驱动注册名为 `"dm"`,连接串格式:`dm://user:password@host:port?schema=dbName` ### 2. 新增 DMDialect 方言 在 [db/dialect.go](db/dialect.go) 末尾新增 `DMDialect` 结构体,实现 `Dialect` 接口全部方法: - `Quote` / `QuoteIdentifier` / `QuoteChar` - 使用双引号 - `Placeholder` - 返回 `?` - `Placeholders` - 返回逗号分隔的 `?` - `SupportsLastInsertId` - 返回 `false`(达梦不支持 MySQL 的 `LAST_INSERT_ID()`) - `ReturningClause` - 返回 `RETURNING "id"`(用于 Insert 后获取自增 ID) - `UpsertSQL` - 生成 `MERGE INTO ... USING (SELECT ? AS col1, ...) src ON (...) WHEN MATCHED THEN UPDATE SET ... WHEN NOT MATCHED THEN INSERT ...` ### 3. 连接配置 - [application.go](application.go) - 新增 `SetDmDB()` 函数,连接串格式: `dm://user:password@host:port?schema=dbName` - [application.go](application.go) 的 `SetDB()` 中添加 `dbDm := db.GetMap("dm")` 分支 - [var.go](var.go) - 在 `Config` 的 `"db"` 和 `ConfigNote` 的 `"db"` 下新增 `"dm"` 配置项 `var.go` 中 `ConfigNote` 新增的达梦配置说明如下: ```go "dm": Map{ "注释": "达梦数据库配置,除prefix及主从数据库slave项,其他全部必须", "host": "默认127.0.0.1,必须,数据库ip地址", "name": "默认SYSDBA,必须,数据库schema名称", "user": "默认SYSDBA,必须,数据库用户名", "password": "默认SYSDBA,必须,数据库密码", "port": "默认5236,必须,数据库端口", "prefix": "默认空,非必须,数据表前缀", "slave": Map{ "注释": "从数据库配置,dm里配置slave项即启用主从读写,减少数据库压力", "host": "默认127.0.0.1,必须,数据库ip地址", "name": "默认SYSDBA,必须,数据库schema名称", "user": "默认SYSDBA,必须,数据库用户名", "password": "默认SYSDBA,必须,数据库密码", "port": "默认5236,必须,数据库端口", }, }, ``` ### 4. Insert 返回 ID 适配(关键兼容点) 当前 [db/crud.go](db/crud.go) 的 `Insert()` 直接调用 `res.LastInsertId()` 获取插入 ID,**但达梦不支持 `LAST_INSERT_ID()`**。Dialect 接口已预留了 `SupportsLastInsertId()` 和 `ReturningClause()` 两个方法但 Insert() 未使用。 修改 `Insert()` 函数逻辑: ```go if that.Dialect != nil && !that.Dialect.SupportsLastInsertId() { // 达梦/PostgreSQL: 用 RETURNING 子句 + QueryRow 获取 ID returningClause := that.Dialect.ReturningClause("id") query = strings.TrimSuffix(query, ";") + returningClause + ";" var insertedId int64 err := that.QueryRow(query, values...).Scan(&insertedId) // ... id = insertedId } else { // MySQL/SQLite: 继续使用 Exec + LastInsertId res, err := that.Exec(query, values...) id1, _ := res.LastInsertId() id = id1 } ``` 同理,`Upsert()` 函数也需在 `switch dbType` 中增加 `case "dm"` 分支,调用新增的 `buildDMUpsert()` 方法生成 `MERGE INTO` 语法。 **RowsAffected 无需额外处理**:`Update()` 和 `Delete()` 使用的 `res.RowsAffected()` 在达梦中正常工作。 ### 5. 缓存层适配 [cache/cache_db.go](cache/cache_db.go) 中需在以下函数增加 `case "dm"` 分支: - `tableExists()` - 使用 `SELECT TABLE_NAME FROM USER_TABLES WHERE TABLE_NAME='...'` - `createMainTable()` - 达梦建表 DDL(`IDENTITY(1,1)` 代替 `AUTO_INCREMENT`) - `createHistoryTable()` - 同上 - `migrateFromCached()` - 使用 `MERGE INTO` 代替 `INSERT IGNORE`,用 `DATEADD` 代替 `FROM_UNIXTIME` - `set()` - 使用 `MERGE INTO` 实现 upsert ### 6. 代码生成适配 [code/makecode.go](code/makecode.go) 第 160-165 行,增加 `case "dm"` 分支: ```go if db.Type == "dm" { tableInfo = db.Select("USER_TAB_COLUMNS", "COLUMN_NAME AS name, DATA_TYPE AS type, COMMENTS AS label, NULLABLE AS must, DATA_DEFAULT AS dflt_value", Map{"TABLE_NAME": v.GetString("name")}) } ``` (注:需要 JOIN `USER_COL_COMMENTS` 获取列注释) ### 7. 备份模块适配 [db/backup.go](db/backup.go) 中: - `backupDdl()` - `SHOW CREATE TABLE` 替换为通过系统视图(`USER_CONS_COLUMNS` + `USER_TAB_COLUMNS`)拼接 DDL - `backupSave()` / `backupCol()` - 将反引号替换为通过 Dialect 的 `Quote()` 方法动态生成 ### 8. initDialect 注册 [db/db.go](db/db.go) 的 `initDialect()` 添加: ```go case "dm", "dameng": that.Dialect = &DMDialect{} ``` ## 驱动引入与 vendor 更新流程 采用标准 go module 方式,驱动包为 `gitee.com/chunanyong/dm`(达梦官方维护的 module 化版本): ```bash # 1. 添加依赖到 go.mod go get gitee.com/chunanyong/dm # 2. 同步到 vendor 目录 go mod vendor ``` `go mod vendor` 会自动将 `gitee.com/chunanyong/dm` 及其传递依赖(`github.com/golang/snappy`、`golang.org/x/text`)下载并复制到 `vendor/` 目录。后续只需提交 `go.mod`、`go.sum` 和 `vendor/` 的变更即可。 ### 9. 数据类型映射补充 [db/query.go](db/query.go) 的 `classifyDBType()` 函数(第 388-417 行)需要补充达梦特有类型: - `NUMBER` -> `dbTypeDecimal`(达梦/Oracle 的通用数值类型) - `VARCHAR2` -> `dbTypeUnknown`(字符串类型,保持默认即可) ```go case "DECIMAL", "NUMERIC", "NEWDECIMAL", "NUMBER": return dbTypeDecimal ``` ### 10. 重构 example 目录 + 集成测试框架 **当前问题**:[example/main.go](example/main.go) 有 1427 行,所有测试逻辑堆在一个文件。 **重构后目录结构**: ``` example/ main.go -- 最小化:只有 Init + Run + 路由引用 config/ config.json -- mysql- (假注释禁用) + dm (启用) app/ init.go -- Project 路由注册 test.go -- TestCtr: 通用 ORM 测试(数据库无关) test_test.go -- TestTest: 测试定义 mysql.go -- MysqlCtr: MySQL 特有测试 mysql_test.go -- MysqlTest: MySQL 测试定义 dmdb.go -- DmdbCtr: 达梦特有测试 dmdb_test.go -- DmdbTest: 达梦测试定义 cache.go -- CacheCtr: 缓存测试(共用) cache_test.go -- CacheTest: 缓存测试定义 app_test.go -- TestMain + TestApi + ProjectTest ``` **拆分原则**: - **TestCtr**(通用 ORM 测试,数据库无关):`init`(根据 `that.Db.Type` 建表)、`crud`、`condition`、`chain`、`join`、`aggregate`、`pagination`、`batch`、`upsert`、`transaction` - **MysqlCtr**(MySQL 特有):`tables`(SHOW TABLES)、`describe`(DESCRIBE)、`rawsql`(反引号原生 SQL) - **DmdbCtr**(达梦特有):`tables`(USER_TABLES)、`describe`(USER_TAB_COLUMNS)、`rawsql`(双引号原生 SQL) - **CacheCtr**(缓存测试,共用):`all`、`compat`、`batch` **main.go 最小化**: ```go package main import ( . "code.hoteas.com/golang/hotime" "code.hoteas.com/golang/hotime/example/app" ) func main() { appIns := Init("config/config.json") appIns.Run(Router{"app": app.Project}) } ``` **config.json 修改**(`mysql` 改为 `mysql-` 假注释禁用,新增 `dm`): ```json { "db": { "mysql-": { "host": "192.168.6.253", "name": "dgs-cms2601301", "password": "dasda8454456", "port": "3306", "user": "root" }, "dm": { "host": "172.31.31.5", "port": "5236", "user": "SYSDBA", "password": "SC_sysdba2026", "name": "SYSDBA" } }, "mode": 2, "port": "8081" } ``` **测试框架集成**(`app_test.go`),运行 `go test ./example/app/... -v` ## 注意事项 - 达梦默认对标识符大小写敏感(默认转大写),建表和查询时需注意统一使用双引号包裹 - 达梦的 `MERGE INTO` 语法与 Oracle 基本一致,这是替代 MySQL `ON DUPLICATE KEY UPDATE` 和 `INSERT IGNORE` 的标准方式 - 达梦默认端口为 5236,默认管理员账户为 SYSDBA - 达梦的 `NOW()` 函数在 DM8 中可用,`SYSDATE` 和 `CURRENT_TIMESTAMP` 也可用 - 达梦支持 `LIMIT` 语法,分页查询无需额外适配 - 达梦 Go 驱动使用 `?` 作为参数占位符,与 MySQL/SQLite 一致