feat(docs): 完善达梦数据库支持文档

- 更新 .gitignore 文件,添加对 .sql 文件的忽略
- 移除 cache_db.go 中的调试日志相关代码,简化缓存操作逻辑
- 在多个文档中添加达梦 DM8 数据库的支持信息,包括配置、使用注意事项及示例
- 更新 QUICKSTART.md,明确支持的数据库类型及配置示例
- 在 ROADMAP.md 中记录达梦数据库完整支持的进展
This commit is contained in:
2026-03-20 11:25:09 +08:00
parent 7f7b585ffb
commit 6fda351585
11 changed files with 823 additions and 133 deletions
+18 -1
View File
@@ -17,7 +17,7 @@
代码生成器可以:
1. **自动读取数据库表结构** - 支持 MySQLSQLite
1. **自动读取数据库表结构** - 支持 MySQLSQLite 和达梦 DM8
2. **生成 CRUD 接口** - 增删改查、搜索、分页
3. **生成配置文件** - 表字段配置、菜单配置、权限配置
4. **智能字段识别** - 根据字段名自动识别类型和权限
@@ -459,6 +459,23 @@ content -- 内容(自动识别为 textArea
2. 使用 `mode=1` 生成代码后手动修改
3. 在生成的配置文件中直接调整字段属性
### 4. 达梦 DM8 使用注意
代码生成器已原生支持达梦 DM8,会自动读取 DM 数据库的表结构和字段备注。使用时注意:
- 表注释(COMMENT)在 DM 中通过 `COMMENT ON TABLE` 语句单独设置,建表后需额外执行
- 字段备注同样通过 `COMMENT ON COLUMN` 设置
- 由于 DM 的 `USER_TAB_COLUMNS` 视图字段名称与 MySQL `information_schema` 不同,代码生成器已做兼容处理
```sql
-- DM8 设置表备注(替代 MySQL 的 COMMENT='...' 语法)
COMMENT ON TABLE "user" IS '用户管理';
-- DM8 设置字段备注
COMMENT ON COLUMN "user"."state" IS '状态:0-正常,1-异常,2-隐藏';
COMMENT ON COLUMN "user"."name" IS '用户名';
```
---
## 相关文档
@@ -191,6 +191,7 @@ SELECT * FROM org WHERE parent_ids LIKE '%,3,%' -- 查询id=3的所有子级
| MySQL | datetime | `2024-01-15 10:30:00` |
| SQLite | TEXT | `2024-01-15 10:30:00` |
| PostgreSQL | timestamp | `2024-01-15 10:30:00` |
| 达梦 DM8 | TIMESTAMP | `2024-01-15 10:30:00` |
---
@@ -324,6 +325,14 @@ SQLite 不支持字段备注(COMMENT),代码生成器会使用字段名作
"modify_time" TIMESTAMP DEFAULT NULL, -- 变更时间
```
### 达梦 DM8
```sql
"state" INT DEFAULT 0, -- 状态:0-正常,1-异常,2-隐藏
"create_time" TIMESTAMP DEFAULT NULL, -- 创建日期
"modify_time" TIMESTAMP DEFAULT NULL, -- 变更时间
```
---
## 完整建表示例
@@ -392,6 +401,56 @@ CREATE TABLE "user" (
);
```
### 达梦 DM8 示例
达梦建表有以下关键差异:
- 自增主键使用 `IDENTITY(1,1)`(对应 MySQL 的 `AUTO_INCREMENT`
- 长文本使用 `CLOB`(对应 MySQL 的 `LONGTEXT`
- 标识符建议用**双引号包裹小写名称**,与框架保持一致
- `admin``user``order` 等是 DM 保留字,必须加双引号
```sql
-- 用户表
CREATE TABLE "user" (
"id" INT IDENTITY(1,1) PRIMARY KEY,
"name" VARCHAR(50) DEFAULT NULL,
"phone" VARCHAR(20) DEFAULT NULL,
"password" VARCHAR(64) DEFAULT NULL,
"org_id" INT DEFAULT NULL,
"role_id" INT DEFAULT NULL,
"avatar" VARCHAR(255) DEFAULT NULL,
"sex" INT DEFAULT 0,
"state" INT DEFAULT 0,
"create_time" TIMESTAMP DEFAULT NULL,
"modify_time" TIMESTAMP DEFAULT NULL
);
-- 组织表(树形结构)
CREATE TABLE "org" (
"id" INT IDENTITY(1,1) PRIMARY KEY,
"name" VARCHAR(100) DEFAULT NULL,
"parent_id" INT DEFAULT NULL,
"parent_ids" VARCHAR(255) DEFAULT NULL,
"level" INT DEFAULT 0,
"sort" INT DEFAULT 0,
"state" INT DEFAULT 0,
"create_time" TIMESTAMP DEFAULT NULL,
"modify_time" TIMESTAMP DEFAULT NULL
);
-- 用户组织关联表
CREATE TABLE "user_org" (
"id" INT IDENTITY(1,1) PRIMARY KEY,
"user_id" INT DEFAULT NULL,
"org_id" INT DEFAULT NULL,
"state" INT DEFAULT 0,
"create_time" TIMESTAMP DEFAULT NULL,
"modify_time" TIMESTAMP DEFAULT NULL
);
```
> **注意**:达梦中 DDL 的表名/列名大小写敏感(双引号包裹时),建议统一使用小写,与框架自动生成的 SQL 保持一致。
---
## 规范检查清单
@@ -406,3 +465,11 @@ CREATE TABLE "user" (
- [ ] 树形表包含 parent_id、parent_ids、level
- [ ] 所有表包含 state、create_time、modify_time
- [ ] 选择字段注释格式正确(`标签:值-名称`
### 达梦 DM8 额外检查项
- [ ] 建表时标识符使用**双引号包裹小写名称**(如 `"user"``"id"`
- [ ] 保留字(`admin``user``order``key` 等)必须加双引号
- [ ] 自增主键使用 `IDENTITY(1,1)` 而非 `AUTO_INCREMENT`
- [ ] 长文本字段使用 `CLOB` 而非 `LONGTEXT`
- [ ] 检测表是否存在时用 `COUNT(*)` 查询,不用 `USER_TABLES`(存在 schema 错配问题)
+72 -6
View File
@@ -107,10 +107,11 @@ affected := database.Inserts("table", []Map{
})
// 返回受影响的行数
// 支持 [#] 标记直接 SQL
// 推荐:使用服务器时间(Time2Str),避免数据库时区差异
now := Time2Str(time.Now())
affected := database.Inserts("log", []Map{
{"user_id": 1, "created_time[#]": "NOW()"},
{"user_id": 2, "created_time[#]": "NOW()"},
{"user_id": 1, "created_time": now},
{"user_id": 2, "created_time": now},
})
```
@@ -223,10 +224,20 @@ data := database.Page(page, pageSize).PageSelect("table", "fields", whereMap)
### 直接SQL
| 写法 | SQL | 说明 |
|------|-----|------|
| `"field[#]": "NOW()"` | `field = NOW()` | 直接SQL函数 |
| `"field[#]": "balance + 1"` | `field = balance + 1` | 直接SQL表达式(数值运算等) |
| `"[##]": "a > b"` | `a > b` | 直接SQL片段 |
| `"field[#!]": "1"` | `field != 1` | 不等于(不参数化) |
> **时间字段推荐写法**:不要用 `"[#]": "NOW()"`,改用 Go 侧传值:
> ```go
> // 赋值当前时间
> "create_time": Time2Str(time.Now())
>
> // 时间范围查询(一天前)
> "create_time[>]": Time2Str(time.Now().AddDate(0, 0, -1))
> ```
> 原因:`NOW()` 使用数据库时区;`Time2Str(time.Now())` 使用应用服务器时区,行为稳定且跨数据库兼容。
## 逻辑连接符
### AND 条件
@@ -391,6 +402,61 @@ database := &db.HoTimeDB{
// - Upsert: ON DUPLICATE KEY -> ON CONFLICT
```
## 达梦数据库(DM8)支持
### 连接配置
```go
import (
_ "gitee.com/chunanyong/dm" // vendor 已内置,无需额外安装
)
database := &db.HoTimeDB{
Type: "dm", // 或 "dameng"
}
database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) {
// schema= 指定当前会话默认搜索 schema
dsn := "dm://SYSDBA:password@127.0.0.1:5236?schema=TEST"
master, _ = sql.Open("dm", dsn)
return master, master
})
```
### 各数据库差异对比
| 特性 | MySQL | PostgreSQL | 达梦 DM8 |
|------|-------|------------|---------|
| 标识符引号 | \`name\` | "name" | "name" |
| 占位符 | ? | $1, $2... | ? |
| Upsert | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE | MERGE INTO...USING |
| 自增列 | AUTO_INCREMENT | SERIAL | IDENTITY(1,1) |
| 分页 | LIMIT m, n | LIMIT n OFFSET m | LIMIT m, n 或 LIMIT n OFFSET m |
框架自动处理所有差异,业务代码无需修改。
### 达梦注意事项速查
| 场景 | 说明 |
|------|------|
| 标识符大小写 | 双引号内大小写敏感,框架统一使用小写 |
| 保留字 | `admin`/`user`/`order` 等框架自动加双引号,原生SQL需手动处理 |
| 表存在检测 | 用 `COUNT(*)` 代替 `USER_TABLES`schema 错配问题) |
| `Insert` 返回ID | 原生驱动已支持 `LastInsertId()`,与 MySQL 一致 |
| 长文本类型 | DM 用 `CLOB`(对应 MySQL 的 `LONGTEXT` |
| 时间赋值 | **推荐统一用** `Time2Str(time.Now())` 传服务器时间,避免 `NOW()` 时区差异及跨库函数不兼容 |
### 原生 SQL 中的保留字
```go
// 达梦中 admin、user 等是保留字,原生 SQL 必须加双引号
results := database.Query(
`SELECT "id", "name" FROM "admin" WHERE "state" = ?`, 1)
// ORM 方法无需处理,框架自动加引号
results := database.Select("admin", "*", common.Map{"state": 1})
```
## 错误处理
```go
// 检查最后的错误
@@ -509,8 +575,8 @@ result := database.Table("order").
---
*快速参考版本: 2.0*
*更新日期: 2026年1*
*快速参考版本: 2.1*
*更新日期: 2026年3*
**详细说明:**
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整教程
+243 -33
View File
@@ -2,7 +2,7 @@
## 概述
HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计,提供简洁的数据库操作接口。支持MySQL、SQLite、PostgreSQL等数据库,并集成了缓存、事务、链式查询等功能。
HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计,提供简洁的数据库操作接口。支持MySQL、SQLite、PostgreSQL、达梦(DM8等数据库,并集成了缓存、事务、链式查询等功能。
## 目录
@@ -24,6 +24,7 @@ HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计
- [事务处理](#事务处理)
- [缓存机制](#缓存机制)
- [PostgreSQL支持](#postgresql支持)
- [达梦数据库支持](#达梦数据库支持)
- [高级特性](#高级特性)
## 快速开始
@@ -48,7 +49,7 @@ func createConnection() (master, slave *sql.DB) {
// 初始化HoTimeDB
database := &db.HoTimeDB{
Type: "mysql", // 可选:mysql, sqlite3, postgres
Type: "mysql", // 可选:mysql, sqlite3, postgres, dm
}
database.SetConnect(createConnection)
```
@@ -64,7 +65,7 @@ type HoTimeDB struct {
DBName string
*cache.HoTimeCache
Log *logrus.Logger
Type string // 数据库类型:mysql, sqlite3, postgres
Type string // 数据库类型:mysql, sqlite3, postgres, dm
Prefix string // 表前缀
LastQuery string // 最后执行的SQL
LastData []interface{} // 最后的参数
@@ -212,17 +213,27 @@ user := database.Get("user", "id,name,email", common.Map{
```go
// 基本插入
id := database.Insert("user", common.Map{
"name": "张三",
"email": "zhangsan@example.com",
"age": 25,
"status": 1,
"created_time[#]": "NOW()", // [#]表示直接插入SQL函数
"name": "张三",
"email": "zhangsan@example.com",
"age": 25,
"status": 1,
"created_time": Time2Str(time.Now()), // 推荐:使用服务器时间,避免数据库时区问题
})
// 返回插入的ID
fmt.Println("插入的用户ID:", id)
```
> **时间字段最佳实践**:建议使用 `Time2Str(time.Now())` 传入服务器时间,而非 `"[#]": "NOW()"` 让数据库执行 `NOW()`。
> 原因:`NOW()` 使用数据库服务器时间,当应用服务器与数据库服务器时区不同时会产生偏差;而 `Time2Str(time.Now())` 始终使用应用服务器时间,行为一致,且跨数据库(MySQL / 达梦 / PostgreSQL)完全兼容。
>
> ```go
> import (
> "time"
> . "code.hoteas.com/golang/hotime/common" // Time2Str 在 common 包中
> )
> ```
### 批量插入(Inserts)
```go
@@ -236,10 +247,11 @@ affected := database.Inserts("user", []common.Map{
fmt.Printf("批量插入 %d 条记录\n", affected)
// 支持 [#] 标记直接插入 SQL 表达式
// 推荐:使用服务器时间(Time2Str)而非数据库函数 NOW()
now := Time2Str(time.Now())
affected := database.Inserts("log", []common.Map{
{"user_id": 1, "action": "login", "created_time[#]": "NOW()"},
{"user_id": 2, "action": "logout", "created_time[#]": "NOW()"},
{"user_id": 1, "action": "login", "created_time": now},
{"user_id": 2, "action": "logout", "created_time": now},
})
```
@@ -248,9 +260,9 @@ affected := database.Inserts("log", []common.Map{
```go
// 基本更新
affected := database.Update("user", common.Map{
"name": "李四",
"email": "lisi@example.com",
"updated_time[#]": "NOW()",
"name": "李四",
"email": "lisi@example.com",
"modify_time": Time2Str(time.Now()), // 推荐:服务器时间
}, common.Map{
"id": 1,
})
@@ -291,6 +303,12 @@ affected := database.Upsert("user",
// INSERT INTO "user" (id,name,email,login_count) VALUES ($1,$2,$3,$4)
// ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, email=EXCLUDED.email, login_count=EXCLUDED.login_count
// 达梦 DM8 生成:
// MERGE INTO "user" USING (SELECT ? AS "id", ? AS "name", ...) src
// ON ("user"."id" = src."id")
// WHEN MATCHED THEN UPDATE SET "name"=src."name", "email"=src."email", ...
// WHEN NOT MATCHED THEN INSERT ("id","name","email","login_count") VALUES (src."id", ...)
// 使用 [#] 标记直接 SQL 更新
affected := database.Upsert("user",
common.Map{
@@ -349,8 +367,8 @@ user := database.Table("user").
affected := database.Table("user").
Where("id", 1).
Update(common.Map{
"name": "新名称",
"updated_time[#]": "NOW()",
"name": "新名称",
"modify_time": Time2Str(time.Now()),
})
// 链式删除
@@ -458,14 +476,14 @@ HoTimeDB支持丰富的条件查询语法,类似于Medoo:
### 直接SQL
```go
// 直接插入SQL表达式(注意防注入
"created_time[#]": "> DATE_SUB(NOW(), INTERVAL 1 DAY)"
// 字段直接赋值(不使用参数化查询)
"update_time[#]": "NOW()"
// 直接SQL片段
// 直接SQL片段(通常用于复杂条件,时间推荐用 Go 计算
"[##]": "user.status = 1 AND user.level > 0"
// 时间范围条件:推荐在 Go 侧计算,避免跨库 NOW() 差异
"created_time[>]": Time2Str(time.Now().AddDate(0, 0, -1)) // 一天前
// 如必须用数据库函数(不跨库时可用,但不推荐)
"update_time[#]": "NOW()"
```
## JOIN操作
@@ -637,10 +655,10 @@ success := database.Action(func(tx db.HoTimeDB) bool {
// 创建订单
orderId := tx.Insert("order", common.Map{
"user_id": 1,
"amount": 100,
"status": "paid",
"created_time[#]": "NOW()",
"user_id": 1,
"amount": 100,
"status": "paid",
"created_time": Time2Str(time.Now()),
})
if orderId == 0 {
@@ -721,6 +739,182 @@ database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) {
所有这些差异由框架自动处理,无需手动调整代码。
## 达梦数据库支持
HoTimeDB 原生支持**达梦数据库 DM8**,驱动已内置于 `vendor/gitee.com/chunanyong/dm`,无需额外安装。
### 达梦配置
`config.json` 中配置:
```json
{
"db": {
"dm": {
"host": "127.0.0.1",
"port": "5236",
"user": "SYSDBA",
"password": "your_password",
"name": "TEST",
"prefix": ""
}
}
}
```
代码配置方式:
```go
import (
"code.hoteas.com/golang/hotime/db"
_ "gitee.com/chunanyong/dm" // 达梦驱动(已在 vendor 中)
)
database := &db.HoTimeDB{
Type: "dm", // 或 "dameng"
Prefix: "", // 表前缀(可选)
}
database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) {
// name 字段即为 schema(默认搜索路径)
dsn := "dm://SYSDBA:your_password@127.0.0.1:5236?schema=TEST"
master, _ = sql.Open("dm", dsn)
return master, master
})
```
> **schema 说明**`?schema=TEST` 将当前会话的默认搜索 schema 设置为 `TEST`。
> 所有不带 schema 前缀的表名均在该 schema 下查找和创建。
> 建议为应用创建独立的专用 schema,避免直接使用 `SYSDBA` 默认 schema。
### 与 MySQL/PostgreSQL 的差异对比
| 特性 | MySQL | PostgreSQL | 达梦 DM8 |
|------|-------|------------|---------|
| 标识符引号 | \`name\` | "name" | "name" |
| 占位符 | ? | $1, $2... | ? |
| Upsert | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE | MERGE INTO ... USING |
| 自增列 | AUTO_INCREMENT | SERIAL | IDENTITY(1,1) |
| 文本类型 | TEXT | TEXT | TEXT / CLOB |
| 时间赋值(推荐) | `Time2Str(time.Now())` | `Time2Str(time.Now())` | `Time2Str(time.Now())` |
| 时间函数(不推荐) | `NOW()` | `NOW()` | `NOW()` |
| 分页语法 | LIMIT m, n | LIMIT n OFFSET m | LIMIT m, n 或 LIMIT n OFFSET m |
所有这些差异由框架自动处理,业务代码无需修改。
### Upsert 在达梦中的行为
达梦不支持 MySQL 的 `ON DUPLICATE KEY UPDATE` 和 PostgreSQL 的 `ON CONFLICT`,框架会自动生成 `MERGE INTO` 语句:
```go
// 调用方式与其他数据库完全一致
affected := database.Upsert("admin",
common.Map{
"name": "张三",
"phone": "13800000001",
"password": "abc123",
"role_id": 1,
},
common.Slice{"phone"}, // 唯一键(冲突检测)
common.Slice{"name", "password", "role_id"}, // 冲突时更新的字段
)
// 达梦自动生成:
// MERGE INTO "admin" USING (SELECT ? AS "name", ? AS "phone", ...) src
// ON ("admin"."phone" = src."phone")
// WHEN MATCHED THEN UPDATE SET "name" = src."name", ...
// WHEN NOT MATCHED THEN INSERT (...) VALUES (src....)
```
### 达梦注意事项
#### 1. 标识符大小写敏感
达梦对双引号包裹的标识符大小写敏感。框架统一使用**小写**创建和访问表/列,确保一致性:
```sql
-- ✅ 正确:框架自动生成,小写标识符
SELECT * FROM "admin" WHERE "id" = ?
-- ❌ 错误:大小写不一致会导致 "无效的表或视图名" 错误
SELECT * FROM "ADMIN" WHERE "ID" = ?
```
> 使用 DM 管理工具手动创建表时,建议使用双引号包裹小写名称,与框架保持一致。
#### 2. 保留字冲突
达梦有大量保留字(如 `admin``user``order``key``value` 等),直接使用这些名称作为表名或列名会报语法错误。
**框架已自动为所有标识符加双引号**,一般情况下无需手动处理。
如果在原生 SQL`Query`/`Exec`)中使用保留字,需要手动加引号:
```go
// 原生 SQL 中需要手动加双引号
results := database.Query(`SELECT "id", "name" FROM "admin" WHERE "state" = ?`, 1)
```
#### 3. schema 与 USER_TABLES 的关系
连接达梦时使用 `?schema=TEST`DM 的 `USER_TABLES` 视图只显示当前**登录用户**(如 SYSDBA)所属 schema 的表,而非当前会话 schema 的表。
框架内部已改用 `COUNT(*)` 方式直接探查表可访问性,避免 schema 错配问题。
如果在自定义代码中检测表是否存在,**不要用 USER_TABLES**,改用:
```go
// ✅ 推荐:直接探查表可访问性
res := database.Query(`SELECT COUNT(*) as cnt FROM "your_table"`)
exists := len(res) > 0
// ❌ 不推荐:USER_TABLES 可能显示当前会话 schema 之外的表
res := database.Query(`SELECT TABLE_NAME FROM USER_TABLES WHERE TABLE_NAME='your_table'`)
```
#### 4. DDL 建表建议
手动建表时,列定义与 MySQL 的区别:
```sql
-- 达梦建表示例
CREATE TABLE "admin" (
"id" INT IDENTITY(1,1) PRIMARY KEY, -- 自增(MySQL: AUTO_INCREMENT
"name" VARCHAR(100),
"phone" VARCHAR(20),
"content" CLOB, -- 长文本(MySQL: TEXT/LONGTEXT
"create_time" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"modify_time" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "uk_admin_phone" UNIQUE ("phone")
);
-- 索引
CREATE INDEX "idx_admin_state" ON "admin" ("state");
```
#### 5. 日期时间函数差异
部分日期函数在达梦中语法不同,使用 `[##]` 直接SQL时需注意:
| 功能 | 推荐(Go侧) | MySQL `[##]`/原生SQL | 达梦 DM8 `[##]`/原生SQL |
|------|------------|---------------------|----------------------|
| 当前时间赋值 | `Time2Str(time.Now())` ✅ | `NOW()` | `NOW()` |
| 一天前(时间范围查询) | `Time2Str(time.Now().AddDate(0,0,-1))` ✅ | `DATE_SUB(NOW(), INTERVAL 1 DAY)` | `DATEADD(DAY, -1, NOW())` |
| 时间戳转字符串 | `Time2Str(time.Unix(ts, 0))` ✅ | `FROM_UNIXTIME(ts)` | 不支持,需 Go 侧处理 |
> **强烈推荐**:时间字段赋值和时间范围计算都在 Go 侧完成(使用 `Time2Str`、`time.Now().Add()` 等),传入参数化的字符串值。这样完全规避数据库时区设置差异,也无需关心各数据库函数的语法不同。
#### 6. 使用 IN 查询时的注意事项
IN 查询在达梦中与 MySQL 行为一致,框架已统一处理:
```go
// 正常使用,框架自动展开
results := database.Select("article", "*", common.Map{
"id": []int{1, 2, 3},
})
// 生成: WHERE "id" IN (?, ?, ?)
```
## 高级特性
### 调试模式
@@ -758,8 +952,8 @@ database.SetConnect(createConnection)
// 执行查询SQL
results := database.Query("SELECT * FROM user WHERE age > ? AND status = ?", 18, 1)
// 执行更新SQL
result, err := database.Exec("UPDATE user SET last_login = NOW() WHERE id = ?", 1)
// 执行更新SQL(原生 SQL 中也建议使用服务器时间)
result, err := database.Exec("UPDATE user SET last_login = ? WHERE id = ?", Time2Str(time.Now()), 1)
if err.GetError() == nil {
affected, _ := result.RowsAffected()
fmt.Println("影响行数:", affected)
@@ -783,7 +977,7 @@ if err.GetError() == nil {
| `[~~]` | 手动LIKE | `"name[~~]": "%张%"` | `name LIKE '%张%'` |
| `[<>]` | BETWEEN | `"age[<>]": [18,60]` | `age BETWEEN 18 AND 60` |
| `[><]` | NOT BETWEEN | `"age[><]": [18,25]` | `age NOT BETWEEN 18 AND 25` |
| `[#]` | 直接SQL | `"time[#]": "NOW()"` | `time = NOW()` |
| `[#]` | 直接SQL表达式 | `"balance[#]": "balance + 1"` | `balance = balance + 1` |
| `[##]` | SQL片段 | `"[##]": "a > b"` | `a > b` |
| `[#!]` | 不等于直接SQL | `"status[#!]": "1"` | `status != 1` |
| `[!#]` | 不等于直接SQL | `"status[!#]": "1"` | `status != 1` |
@@ -822,12 +1016,28 @@ A6: 使用 `nil` 作为值,查询时使用 `"field": nil` 表示 `IS NULL`。
### Q7: PostgreSQL 和 MySQL 语法有区别吗?
A7: 框架会自动处理差异(占位符、引号等),代码无需修改。
### Q8: 达梦数据库和 MySQL 的代码使用方式一样吗?
A8: 基本一样。切换数据库类型只需修改 `Type` 字段和连接字符串,业务查询代码完全不用改。唯一需要注意的是:
- 原生 SQL`Query`/`Exec`)中的标识符需要手动加双引号
- `MERGE INTO` 等 DM 特有 DDL 需要按 DM 语法书写
- 检测表是否存在时,不要用 `USER_TABLES`,改用直接 `COUNT(*)` 查询
### Q9: 表名或列名用了达梦保留字怎么办?
A9: 框架所有自动生成的 SQL 都会对标识符加双引号,无需手动处理。但在原生 SQL 中使用保留字(如 `admin``user``order`)时,必须手动加双引号,否则会报语法错误。
### Q10: 达梦数据库中 INSERT 后如何获取自增 ID?
A10: 框架的 `Insert` 方法已支持从达梦原生驱动获取 `LastInsertId`,与 MySQL 使用方式完全一致:
```go
id := database.Insert("user", common.Map{"name": "张三"})
fmt.Println("新插入的ID:", id)
```
---
*文档版本: 2.0*
*最后更新: 2026年1*
*文档版本: 2.1*
*最后更新: 2026年3*
> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,根据Golang语言特性进行了适配和优化。
> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,根据Golang语言特性进行了适配和优化,并新增了对达梦(DM8)国产数据库的完整支持
**更多参考:**
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册
+12 -1
View File
@@ -86,6 +86,8 @@ func main() {
### 数据库配置
支持的数据库类型:`mysql``sqlite``postgres`PostgreSQL)、`dm`(达梦 DM8)。
```json
{
"db": {
@@ -107,12 +109,21 @@ func main() {
"sqlite": {
"path": "config/data.db",
"prefix": ""
},
"dm": {
"host": "127.0.0.1",
"port": "5236",
"name": "TEST",
"user": "SYSDBA",
"password": "your_password",
"prefix": ""
}
}
}
```
> MySQL 配置 `slave` 项即启用主从读写分离
> MySQL 配置 `slave` 项即启用主从读写分离
> 达梦 DM8 的 `name` 字段对应 schema(搜索路径),驱动已内置于 `vendor/gitee.com/chunanyong/dm`,无需额外安装
### 缓存配置
+1
View File
@@ -276,3 +276,4 @@ a.JSON(Map{"phone": "138..."}).Post("正常注册", 0, Map{"id": notEmpty}) //
|------|------|
| 2026-01-24 | 初始版本,记录分析结果和待改进项 |
| 2026-03-15 | 新增测试框架响应字段断言改进规划(七) |
| 2026-03-20 | 完成达梦(DM8)数据库完整支持:Dialect 适配(MERGE INTO Upsert、双引号标识符)、LastInsertId、保留字自动引号、schema 正确处理、代码生成器 DM 兼容、缓存表自动建表、测试框架 DM 适配 |
+382
View File
@@ -0,0 +1,382 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MySQL 8.0 dump 转达梦 DM8 SQL 转换脚本
兼容 HoTime 框架达梦适配规范
"""
import re
import sys
import os
from typing import Optional
INPUT_FILE = os.path.join(os.path.dirname(__file__), "zwbg_2026-03-20_11-00-59_mysql_data_zhqCt.sql")
OUTPUT_FILE = os.path.join(os.path.dirname(__file__), "zwbg_dm8.sql")
# ─────────────────────────────────────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────────────────────────────────────
def backtick_to_doublequote(s: str) -> str:
"""把反引号标识符改为双引号,并强制小写"""
def replacer(m):
return '"' + m.group(1).lower() + '"'
return re.sub(r'`([^`]+)`', replacer, s)
def convert_col_type(coldef: str) -> str:
"""转换列的数据类型(不含 COMMENT)"""
# tinyint -> INT(先处理,否则会被 int 规则误匹配)
coldef = re.sub(r'\btinyint(?:\s*\(\s*\d+\s*\))?\b', 'INT', coldef, flags=re.I)
# bigint -> BIGINT
coldef = re.sub(r'\bbigint(?:\s*\(\s*\d+\s*\))?\b', 'BIGINT', coldef, flags=re.I)
# int unsigned / int(N) -> INT
coldef = re.sub(r'\bint\s+unsigned(?:\s*\(\s*\d+\s*\))?\b', 'INT', coldef, flags=re.I)
coldef = re.sub(r'\bint(?:\s*\(\s*\d+\s*\))?\b', 'INT', coldef, flags=re.I)
# datetime -> TIMESTAMP
coldef = re.sub(r'\bdatetime\b', 'TIMESTAMP', coldef, flags=re.I)
# text / longtext / mediumtext -> CLOB
coldef = re.sub(r'\b(?:long|medium)?text\b', 'CLOB', coldef, flags=re.I)
# 去掉 CHARACTER SET ... / COLLATE ... 修饰
coldef = re.sub(r'\bCHARACTER\s+SET\s+\S+', '', coldef, flags=re.I)
coldef = re.sub(r'\bCOLLATE\s+\S+', '', coldef, flags=re.I)
# 去掉列级 AUTO_INCREMENT(由外层统一处理自增列声明)
coldef = re.sub(r'\bAUTO_INCREMENT\b', '', coldef, flags=re.I)
# 压缩多余空格
coldef = re.sub(r' +', ' ', coldef).strip()
return coldef
def extract_comment(line: str) -> tuple[str, Optional[str]]:
"""提取行尾 COMMENT '...' 并返回 (去掉comment的行, comment文本 or None)"""
m = re.search(r"\bCOMMENT\s+'((?:[^'\\]|\\.)*)'\s*,?\s*$", line, re.I)
if m:
comment_text = m.group(1)
# 转义单引号
comment_text = comment_text.replace("''", "'")
line_clean = line[:m.start()].rstrip().rstrip(',')
return line_clean, comment_text
return line, None
# ─────────────────────────────────────────────────────────────────────────────
# 主解析器状态机
# ─────────────────────────────────────────────────────────────────────────────
class Table:
def __init__(self, name: str, table_comment: Optional[str]):
self.name = name # 原始小写名
self.table_comment = table_comment
self.col_defs: list[str] = [] # 已转换的列定义(不含索引)
self.col_comments: dict[str, str] = {} # col_name -> comment
self.pk_cols: list[str] = [] # PRIMARY KEY 列
self.unique_keys: list[tuple[str, list[str]]] = [] # (key_name, cols)
self.normal_keys: list[tuple[str, list[str]]] = [] # (key_name, cols)
self.identity_col: Optional[str] = None # 自增列名
self.auto_increment_start: int = 1 # 表级 AUTO_INCREMENT 值
self.inserts: list[str] = [] # INSERT 语句列表
def qname(self) -> str:
"""双引号包裹小写表名"""
return f'"{self.name}"'
def parse_key_cols(col_str: str) -> list[str]:
"""解析索引列列表,如 `col1`,`col2` -> ['col1','col2']"""
cols = []
for part in col_str.split(','):
part = part.strip()
# 去掉长度修饰 `col`(10)
part = re.sub(r'\(\d+\)$', '', part.strip())
m = re.match(r'[`"\']?([^`"\'()\s]+)[`"\']?', part)
if m:
cols.append(m.group(1).lower())
return cols
def parse_mysql_dump(sql_text: str) -> list[Table]:
"""解析 MySQL dump,提取所有表的结构和数据"""
tables: list[Table] = []
current_table: Optional[Table] = None
in_create = False
lines = sql_text.splitlines()
i = 0
while i < len(lines):
raw = lines[i]
stripped = raw.strip()
# 跳过 MySQL 条件注释行和空行
if re.match(r'^/\*!', stripped) or stripped == '' or stripped.startswith('-- '):
i += 1
continue
# DROP TABLE IF EXISTS
m = re.match(r'DROP TABLE IF EXISTS `([^`]+)`', stripped, re.I)
if m:
i += 1
continue
# CREATE TABLE 开始
m = re.match(r'CREATE TABLE `([^`]+)`\s*\(', stripped, re.I)
if m:
tname = m.group(1).lower()
current_table = Table(tname, None)
in_create = True
i += 1
continue
# CREATE TABLE 结束行:) ENGINE=... COMMENT='...'
if in_create and stripped.startswith(')'):
in_create = False
# 提取表注释
tc = re.search(r"COMMENT\s*=\s*'((?:[^'\\]|\\.)*)'", stripped, re.I)
if tc:
current_table.table_comment = tc.group(1)
# 提取 AUTO_INCREMENT=N
ai = re.search(r'AUTO_INCREMENT=(\d+)', stripped, re.I)
if ai:
current_table.auto_increment_start = int(ai.group(1))
tables.append(current_table)
current_table = None
i += 1
continue
# CREATE TABLE 内部行
if in_create and current_table is not None:
# 去掉行尾逗号暂时,用于判断
col_line = stripped
# PRIMARY KEY
m = re.match(r'PRIMARY KEY\s*\((.+)\)', col_line, re.I)
if m:
current_table.pk_cols = parse_key_cols(m.group(1))
i += 1
continue
# UNIQUE KEY
m = re.match(r'UNIQUE KEY\s*`([^`]+)`\s*\((.+?)\)', col_line, re.I)
if m:
kname = m.group(1).lower()
kcols = parse_key_cols(m.group(2))
current_table.unique_keys.append((kname, kcols))
i += 1
continue
# KEY (普通索引)
m = re.match(r'KEY\s*`([^`]+)`\s*\((.+?)\)', col_line, re.I)
if m:
kname = m.group(1).lower()
kcols = parse_key_cols(m.group(2))
current_table.normal_keys.append((kname, kcols))
i += 1
continue
# CONSTRAINT ... PRIMARY KEY (复合pk in some versions)
m = re.match(r'CONSTRAINT\s*`[^`]+`\s*PRIMARY KEY\s*\((.+)\)', col_line, re.I)
if m:
current_table.pk_cols = parse_key_cols(m.group(1))
i += 1
continue
# CONSTRAINT ... UNIQUE KEY
m = re.match(r'CONSTRAINT\s*`([^`]+)`\s*UNIQUE\s*(?:KEY)?\s*\((.+?)\)', col_line, re.I)
if m:
kname = m.group(1).lower()
kcols = parse_key_cols(m.group(2))
current_table.unique_keys.append((kname, kcols))
i += 1
continue
# 列定义
m = re.match(r'`([^`]+)`\s+(.+)', col_line)
if m:
col_name = m.group(1).lower()
col_rest = m.group(2)
# 提取 COMMENT
comment_m = re.search(r"\bCOMMENT\s+'((?:[^'\\]|\\.)*)'\s*,?\s*$", col_rest, re.I)
comment_text = None
if comment_m:
comment_text = comment_m.group(1)
col_rest = col_rest[:comment_m.start()].rstrip().rstrip(',')
# 检测是否是自增列
is_identity = bool(re.search(r'\bAUTO_INCREMENT\b', col_rest, re.I))
# 类型转换
col_rest = convert_col_type(col_rest)
# 去掉行尾逗号
col_rest = col_rest.rstrip(',').strip()
if is_identity:
# 替换成 IDENTITY(1,1),保留 NOT NULL
# 先提取基础类型
type_m = re.match(r'((?:BIG)?INT)\s+(.*)', col_rest, re.I)
if type_m:
base_type = type_m.group(1).upper()
rest_attrs = type_m.group(2).strip()
# 去掉 NOT NULLIDENTITY 列隐含 NOT NULL
rest_attrs = re.sub(r'\bNOT\s+NULL\b', '', rest_attrs, flags=re.I).strip()
col_rest = f"{base_type} IDENTITY(1,1) {rest_attrs}".strip()
else:
col_rest = re.sub(r'\bNOT\s+NULL\b', '', col_rest, flags=re.I).strip()
col_rest = f"INT IDENTITY(1,1) {col_rest}".strip()
current_table.identity_col = col_name
col_def = f' "{col_name}" {col_rest}'
current_table.col_defs.append(col_def)
if comment_text is not None:
current_table.col_comments[col_name] = comment_text
i += 1
continue
i += 1
continue
# INSERT INTO `tablename` VALUES (...)
m = re.match(r'INSERT INTO `([^`]+)`\s+VALUES\s+', stripped, re.I)
if m:
tname = m.group(1).lower()
# 找到对应的 table 对象
tbl = next((t for t in tables if t.name == tname), None)
if tbl is not None:
# 转换反引号 -> 双引号
insert_line = re.sub(r'`([^`]+)`', lambda mm: f'"{mm.group(1).lower()}"', stripped)
# 去掉行尾分号后的 /*!...*/
insert_line = re.sub(r'\s*/\*!.*?\*/\s*', ' ', insert_line)
insert_line = insert_line.strip().rstrip(';') + ';'
tbl.inserts.append(insert_line)
i += 1
continue
i += 1
return tables
# ─────────────────────────────────────────────────────────────────────────────
# 输出生成
# ─────────────────────────────────────────────────────────────────────────────
def gen_drop(tbl: Table) -> str:
return f'DROP TABLE IF EXISTS {tbl.qname()};\n'
def gen_create(tbl: Table) -> str:
lines = []
col_parts = list(tbl.col_defs)
# PRIMARY KEY 约束
if tbl.pk_cols:
pk_str = ', '.join(f'"{c}"' for c in tbl.pk_cols)
col_parts.append(f' PRIMARY KEY ({pk_str})')
create = f'CREATE TABLE {tbl.qname()} (\n'
create += ',\n'.join(col_parts)
create += '\n);\n'
return create
def gen_comments(tbl: Table) -> str:
out = ''
if tbl.table_comment:
tc = tbl.table_comment.replace("'", "''")
out += f"COMMENT ON TABLE {tbl.qname()} IS '{tc}';\n"
for col, comment in tbl.col_comments.items():
cc = comment.replace("'", "''")
out += f"COMMENT ON COLUMN {tbl.qname()}.\"{ col}\" IS '{cc}';\n"
return out
def gen_indexes(tbl: Table) -> str:
out = ''
for kname, kcols in tbl.unique_keys:
cols_str = ', '.join(f'"{c}"' for c in kcols)
out += f'CREATE UNIQUE INDEX "{kname}" ON {tbl.qname()} ({cols_str});\n'
for kname, kcols in tbl.normal_keys:
cols_str = ', '.join(f'"{c}"' for c in kcols)
out += f'CREATE INDEX "{kname}" ON {tbl.qname()} ({cols_str});\n'
return out
def gen_inserts(tbl: Table) -> str:
if not tbl.inserts:
return ''
out = ''
if tbl.identity_col:
out += f'SET IDENTITY_INSERT {tbl.qname()} ON;\n'
for ins in tbl.inserts:
out += ins + '\n'
if tbl.identity_col:
out += f'SET IDENTITY_INSERT {tbl.qname()} OFF;\n'
# 重置自增起始值,取 auto_increment_startMySQL 导出时已包含下一个可用值)
next_val = tbl.auto_increment_start
if next_val > 1:
out += (
f'ALTER TABLE {tbl.qname()} ALTER COLUMN "{tbl.identity_col}" '
f'SET GENERATED BY DEFAULT AS IDENTITY (START WITH {next_val});\n'
)
return out
def generate_dm8_sql(tables: list[Table]) -> str:
header = """\
-- =============================================================================
-- 达梦 DM8 数据库初始化脚本
-- 源文件: zwbg_2026-03-20_11-00-59_mysql_data_zhqCt.sql (MySQL 8.0)
-- 目标数据库: 达梦 DM8
-- 兼容框架: HoTime (标识符统一小写双引号, IDENTITY 自增, TIMESTAMP 时间)
-- 生成时间: 2026-03-20
-- =============================================================================
"""
footer = "\n-- 脚本执行完毕\n"
parts = [header]
for tbl in tables:
parts.append(f'-- ---- 表: {tbl.name} ----\n')
parts.append(gen_drop(tbl))
parts.append(gen_create(tbl))
comments = gen_comments(tbl)
if comments:
parts.append(comments)
indexes = gen_indexes(tbl)
if indexes:
parts.append(indexes)
inserts = gen_inserts(tbl)
if inserts:
parts.append(inserts)
parts.append('\n')
parts.append(footer)
return ''.join(parts)
# ─────────────────────────────────────────────────────────────────────────────
# 入口
# ─────────────────────────────────────────────────────────────────────────────
def main():
print(f"读取源文件: {INPUT_FILE}")
with open(INPUT_FILE, 'r', encoding='utf-8') as f:
sql_text = f.read()
print("解析 MySQL dump...")
tables = parse_mysql_dump(sql_text)
print(f" 共解析到 {len(tables)} 张表: {', '.join(t.name for t in tables)}")
print("生成达梦 DM8 SQL...")
dm8_sql = generate_dm8_sql(tables)
print(f"写出目标文件: {OUTPUT_FILE}")
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
f.write(dm8_sql)
size_kb = os.path.getsize(OUTPUT_FILE) / 1024
print(f"完成! 文件大小: {size_kb:.1f} KB")
if __name__ == '__main__':
main()