Files
hotime/.cursor/plans/支持达梦数据库_a1d7f581.plan.md
T
hoteas 7f7b585ffb feat(db): 添加对达梦数据库的支持
- 在应用程序中新增对达梦数据库(DM)的配置和连接支持
- 实现 SetDmDB 函数以配置达梦数据库连接
- 更新数据库操作逻辑,支持达梦特有的 SQL 语法和功能
- 在相关文件中添加达梦数据库的处理逻辑,包括表创建、数据插入和查询
- 更新 go.mod 和 go.sum 文件以引入达梦数据库驱动
- 增强文档,详细说明达梦数据库的配置和使用方法
2026-03-20 10:46:51 +08:00

13 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
支持达梦数据库 在 HoTime 框架的现有 Dialect 方言体系下,新增达梦(DM8)数据库方言实现,涉及驱动引入、方言适配、连接配置、缓存层/代码生成/备份模块的 DM 分支补充。
id content status
dm-driver 引入达梦 Go 驱动依赖(go.mod + import),确定驱动引入方式 pending
id content status
dm-dialect 在 db/dialect.go 中实现 DMDialectQuote/Placeholder/UpsertSQL 等全部方法) pending
id content status
dm-init 在 db/db.go 的 initDialect() 中注册 dm 类型 pending
id content status
dm-connect 在 application.go 中添加 SetDmDB() 连接函数和 SetDB() 中的 dm 分支 pending
id content status
dm-config 在 var.go 中添加 dm 数据库配置说明 pending
id content status
dm-cache 在 cache/cache_db.go 中为 tableExists/createMainTable/createHistoryTable/migrateFromCached/set 添加 dm 分支 pending
id content status
dm-makecode 在 code/makecode.go 中添加 dm 的表结构查询逻辑 pending
id content status
dm-insert-returning 修改 db/crud.go 的 Insert() 函数,当 SupportsLastInsertId()=false 时使用 RETURNING + QueryRow 获取插入 ID pending
id content status
dm-upsert 在 db/crud.go 中新增 buildDMUpsert() 方法(MERGE INTO 语法),并在 Upsert() 中添加 dm 分支 pending
id content status
dm-backup 在 db/backup.go 中适配达梦的 DDL 导出和标识符引号 pending
id content status
dm-query-types 在 db/query.go 的 classifyDBType() 中补充达梦特有数据类型(NUMBER 等) pending
id content status
refactor-example 重构 example/main.go,最小化启动配置,拆分到 example/app/ 目录 pending
id content status
example-test-ctr 创建 example/app/test.go(通用 ORM 测试 Ctr+ test_test.go(测试定义),含数据库无关的 CRUD/条件/JOIN/聚合/分页/批量/Upsert/事务 pending
id content status
example-mysql-ctr 创建 example/app/mysql.goMySQL 特有 Ctr+ mysql_test.go,含 DDL/SHOW TABLES/DESCRIBE/原生SQL pending
id content status
example-dmdb-ctr 创建 example/app/dmdb.go(达梦特有 Ctr+ dmdb_test.go,含达梦 DDL/USER_TABLES/元数据/原生SQL pending
id content status
example-app-test 创建 example/app/init.go(路由注册)+ app_test.goTestMain+TestApi),集成测试框架 pending
id content status
example-config 修改 example/config/config.jsonmysql 改为 mysql-(假注释禁用),新增 dm 配置 pending
false

支持达梦(DM8)数据库

现状分析

项目已有完善的 Dialect 接口体系(db/dialect.go),以及 initDialect() 工厂方法(db/db.go),当前支持 MySQL、PostgreSQL、SQLite 三种方言。需要新增达梦方言并在各处补充 case "dm" 分支。

flowchart TB
    subgraph currentState [当前架构]
        DialectIf["Dialect 接口<br/>(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 流程引入:

go get gitee.com/chunanyong/dm
go mod vendor
  • go.mod - 自动添加 gitee.com/chunanyong/dm 依赖
  • 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 末尾新增 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 - 新增 SetDmDB() 函数,连接串格式: dm://user:password@host:port?schema=dbName
  • application.goSetDB() 中添加 dbDm := db.GetMap("dm") 分支
  • var.go - 在 Config"db"ConfigNote"db" 下新增 "dm" 配置项

var.goConfigNote 新增的达梦配置说明如下:

"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.goInsert() 直接调用 res.LastInsertId() 获取插入 ID但达梦不支持 LAST_INSERT_ID()。Dialect 接口已预留了 SupportsLastInsertId()ReturningClause() 两个方法但 Insert() 未使用。

修改 Insert() 函数逻辑:

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 中需在以下函数增加 case "dm" 分支:

  • tableExists() - 使用 SELECT TABLE_NAME FROM USER_TABLES WHERE TABLE_NAME='...'
  • createMainTable() - 达梦建表 DDLIDENTITY(1,1) 代替 AUTO_INCREMENT
  • createHistoryTable() - 同上
  • migrateFromCached() - 使用 MERGE INTO 代替 INSERT IGNORE,用 DATEADD 代替 FROM_UNIXTIME
  • set() - 使用 MERGE INTO 实现 upsert

6. 代码生成适配

code/makecode.go 第 160-165 行,增加 case "dm" 分支:

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 中:

  • backupDdl() - SHOW CREATE TABLE 替换为通过系统视图(USER_CONS_COLUMNS + USER_TAB_COLUMNS)拼接 DDL
  • backupSave() / backupCol() - 将反引号替换为通过 Dialect 的 Quote() 方法动态生成

8. initDialect 注册

db/db.goinitDialect() 添加:

case "dm", "dameng":
    that.Dialect = &DMDialect{}

驱动引入与 vendor 更新流程

采用标准 go module 方式,驱动包为 gitee.com/chunanyong/dm(达梦官方维护的 module 化版本):

# 1. 添加依赖到 go.mod
go get gitee.com/chunanyong/dm

# 2. 同步到 vendor 目录
go mod vendor

go mod vendor 会自动将 gitee.com/chunanyong/dm 及其传递依赖(github.com/golang/snappygolang.org/x/text)下载并复制到 vendor/ 目录。后续只需提交 go.modgo.sumvendor/ 的变更即可。

9. 数据类型映射补充

db/query.goclassifyDBType() 函数(第 388-417 行)需要补充达梦特有类型:

  • NUMBER -> dbTypeDecimal(达梦/Oracle 的通用数值类型)
  • VARCHAR2 -> dbTypeUnknown(字符串类型,保持默认即可)
case "DECIMAL", "NUMERIC", "NEWDECIMAL", "NUMBER":
    return dbTypeDecimal

10. 重构 example 目录 + 集成测试框架

当前问题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 建表)、crudconditionchainjoinaggregatepaginationbatchupserttransaction
  • MysqlCtrMySQL 特有):tablesSHOW TABLES)、describeDESCRIBE)、rawsql(反引号原生 SQL
  • DmdbCtr(达梦特有):tablesUSER_TABLES)、describeUSER_TAB_COLUMNS)、rawsql(双引号原生 SQL
  • CacheCtr(缓存测试,共用):allcompatbatch

main.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):

{
    "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 UPDATEINSERT IGNORE 的标准方式
  • 达梦默认端口为 5236,默认管理员账户为 SYSDBA
  • 达梦的 NOW() 函数在 DM8 中可用,SYSDATECURRENT_TIMESTAMP 也可用
  • 达梦支持 LIMIT 语法,分页查询无需额外适配
  • 达梦 Go 驱动使用 ? 作为参数占位符,与 MySQL/SQLite 一致