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 一致
|
|||
|
|
|