b43f968b6c
- 在应用程序中新增对达梦数据库(DM)的配置和连接支持 - 实现 SetDmDB 函数以配置达梦数据库连接 - 更新数据库操作逻辑,支持达梦特有的 SQL 语法和功能 - 在相关文件中添加达梦数据库的处理逻辑,包括表创建、数据插入和查询 - 更新 go.mod 和 go.sum 文件以引入达梦数据库驱动 - 增强文档,详细说明达梦数据库的配置和使用方法
329 lines
13 KiB
Markdown
329 lines
13 KiB
Markdown
---
|
||
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 接口<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 流程引入:
|
||
|
||
```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 一致
|
||
|