Files
hotime/docs/HoTimeDB_使用说明.md
T
hoteas 5ad4e2e11b feat(docs): 完善达梦数据库支持文档
- 更新 .gitignore 文件,添加对 .sql 文件的忽略
- 移除 cache_db.go 中的调试日志相关代码,简化缓存操作逻辑
- 在多个文档中添加达梦 DM8 数据库的支持信息,包括配置、使用注意事项及示例
- 更新 QUICKSTART.md,明确支持的数据库类型及配置示例
- 在 ROADMAP.md 中记录达梦数据库完整支持的进展
2026-03-20 11:25:09 +08:00

1044 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HoTimeDB ORM 使用说明书
## 概述
HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计,提供简洁的数据库操作接口。支持MySQL、SQLite、PostgreSQL、达梦(DM8)等数据库,并集成了缓存、事务、链式查询等功能。
## 目录
- [快速开始](#快速开始)
- [数据库配置](#数据库配置)
- [基本操作](#基本操作)
- [查询(Select)](#查询select)
- [获取单条记录(Get)](#获取单条记录get)
- [插入(Insert)](#插入insert)
- [批量插入(Inserts)](#批量插入Inserts)
- [更新(Update)](#更新update)
- [Upsert操作](#upsert操作)
- [删除(Delete)](#删除delete)
- [链式查询构建器](#链式查询构建器)
- [条件查询语法](#条件查询语法)
- [JOIN操作](#join操作)
- [分页查询](#分页查询)
- [聚合函数](#聚合函数)
- [事务处理](#事务处理)
- [缓存机制](#缓存机制)
- [PostgreSQL支持](#postgresql支持)
- [达梦数据库支持](#达梦数据库支持)
- [高级特性](#高级特性)
## 快速开始
### 初始化数据库连接
```go
import (
"code.hoteas.com/golang/hotime/db"
"code.hoteas.com/golang/hotime/common"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
// 创建连接函数
func createConnection() (master, slave *sql.DB) {
master, _ = sql.Open("mysql", "user:password@tcp(localhost:3306)/database")
// slave是可选的,用于读写分离
slave = master // 或者连接到从数据库
return
}
// 初始化HoTimeDB
database := &db.HoTimeDB{
Type: "mysql", // 可选:mysql, sqlite3, postgres, dm
}
database.SetConnect(createConnection)
```
## 数据库配置
### 基本配置
```go
type HoTimeDB struct {
*sql.DB
ContextBase
DBName string
*cache.HoTimeCache
Log *logrus.Logger
Type string // 数据库类型:mysql, sqlite3, postgres, dm
Prefix string // 表前缀
LastQuery string // 最后执行的SQL
LastData []interface{} // 最后的参数
ConnectFunc func(err ...*Error) (*sql.DB, *sql.DB)
LastErr *Error
limit Slice
*sql.Tx // 事务对象
SlaveDB *sql.DB // 从数据库
Mode int // 0生产模式,1测试模式,2开发模式
Dialect Dialect // 数据库方言适配器
}
```
### 设置表前缀
```go
database.Prefix = "app_"
```
### 设置运行模式
```go
database.Mode = 2 // 开发模式,会输出SQL日志
```
## 基本操作
### 查询(Select)
#### 基本查询
```go
// 查询所有字段
users := database.Select("user")
// 查询指定字段
users := database.Select("user", "id,name,email")
// 查询指定字段(数组形式)
users := database.Select("user", []string{"id", "name", "email"})
// 单条件查询
users := database.Select("user", "*", common.Map{
"status": 1,
})
// 多条件查询(自动用 AND 连接)
users := database.Select("user", "*", common.Map{
"status": 1,
"age[>]": 18,
})
// 生成: WHERE `status`=? AND `age`>?
```
#### 复杂条件查询
```go
// 简化语法:多条件自动用 AND 连接
users := database.Select("user", "*", common.Map{
"status": 1,
"age[>]": 18,
"name[~]": "张",
})
// 生成: WHERE `status`=? AND `age`>? AND `name` LIKE ?
// 显式 AND 条件(与上面等效)
users := database.Select("user", "*", common.Map{
"AND": common.Map{
"status": 1,
"age[>]": 18,
"name[~]": "张",
},
})
// OR 条件
users := database.Select("user", "*", common.Map{
"OR": common.Map{
"status": 1,
"type": 2,
},
})
// 混合条件(嵌套 AND/OR
users := database.Select("user", "*", common.Map{
"AND": common.Map{
"status": 1,
"OR": common.Map{
"age[<]": 30,
"level[>]": 5,
},
},
})
// 带 ORDER BY、LIMIT 等特殊条件(关键字支持大小写)
users := database.Select("user", "*", common.Map{
"status": 1,
"age[>]": 18,
"ORDER": "id DESC", // 或 "order": "id DESC"
"LIMIT": 10, // 或 "limit": 10
})
// 带多个特殊条件
users := database.Select("user", "*", common.Map{
"OR": common.Map{
"level": "vip",
"balance[>]": 1000,
},
"ORDER": []string{"created_time DESC", "id ASC"},
"GROUP": "department",
"LIMIT": []int{0, 20}, // offset 0, limit 20
})
// 使用 HAVING 过滤分组结果
users := database.Select("user", "dept_id, COUNT(*) as cnt", common.Map{
"GROUP": "dept_id",
"HAVING": common.Map{
"cnt[>]": 5,
},
})
// 使用独立 OFFSET
users := database.Select("user", "*", common.Map{
"status": 1,
"LIMIT": 10,
"OFFSET": 20,
})
```
### 获取单条记录(Get)
```go
// 获取单个用户
user := database.Get("user", "*", common.Map{
"id": 1,
})
// 获取指定字段
user := database.Get("user", "id,name,email", common.Map{
"status": 1,
})
```
### 插入(Insert)
```go
// 基本插入
id := database.Insert("user", common.Map{
"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
// 批量插入多条记录(使用 []Map 格式,更直观)
affected := database.Inserts("user", []common.Map{
{"name": "张三", "email": "zhang@example.com", "age": 25},
{"name": "李四", "email": "li@example.com", "age": 30},
{"name": "王五", "email": "wang@example.com", "age": 28},
})
// 生成: INSERT INTO `user` (`age`, `email`, `name`) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
fmt.Printf("批量插入 %d 条记录\n", affected)
// 推荐:使用服务器时间(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},
})
```
### 更新(Update)
```go
// 基本更新
affected := database.Update("user", common.Map{
"name": "李四",
"email": "lisi@example.com",
"modify_time": Time2Str(time.Now()), // 推荐:服务器时间
}, common.Map{
"id": 1,
})
// 条件更新(多条件自动 AND 连接)
affected := database.Update("user", common.Map{
"status": 0,
}, common.Map{
"age[<]": 18,
"status": 1,
})
fmt.Println("更新的记录数:", affected)
```
### Upsert操作
Upsert(插入或更新):如果记录存在则更新,不存在则插入。
```go
// Upsert 操作(使用 Slice 格式)
affected := database.Upsert("user",
common.Map{
"id": 1,
"name": "张三",
"email": "zhang@example.com",
"login_count": 1,
},
common.Slice{"id"}, // 唯一键(用于冲突检测)
common.Slice{"name", "email", "login_count"}, // 冲突时更新的字段
)
// MySQL 生成:
// INSERT INTO user (id,name,email,login_count) VALUES (?,?,?,?)
// ON DUPLICATE KEY UPDATE name=VALUES(name), email=VALUES(email), login_count=VALUES(login_count)
// PostgreSQL 生成:
// 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{
"id": 1,
"name": "张三",
"login_count[#]": "login_count + 1", // 直接 SQL 表达式
},
common.Slice{"id"},
common.Slice{"name", "login_count"},
)
// 也支持可变参数形式
affected := database.Upsert("user",
common.Map{"id": 1, "name": "张三"},
common.Slice{"id"},
"name", "email", // 可变参数
)
```
### 删除(Delete)
```go
// 根据ID删除
affected := database.Delete("user", common.Map{
"id": 1,
})
// 条件删除(多条件自动 AND 连接)
affected := database.Delete("user", common.Map{
"status": 0,
"created_time[<]": "2023-01-01",
})
fmt.Println("删除的记录数:", affected)
```
## 链式查询构建器
HoTimeDB提供了链式查询构建器,让查询更加直观:
```go
// 基本链式查询
users := database.Table("user").
Where("status", 1).
And("age[>]", 18).
Order("created_time DESC").
Limit(10, 20). // offset, limit
Select()
// 链式获取单条记录
user := database.Table("user").
Where("id", 1).
Get()
// 链式更新
affected := database.Table("user").
Where("id", 1).
Update(common.Map{
"name": "新名称",
"modify_time": Time2Str(time.Now()),
})
// 链式删除
affected := database.Table("user").
Where("status", 0).
Delete()
// 链式统计
count := database.Table("user").
Where("status", 1).
Count()
```
### 链式条件组合
```go
// 复杂条件组合
users := database.Table("user").
Where("status", 1).
And("age[>=]", 18).
Or(common.Map{
"level[>]": 5,
"vip": 1,
}).
Order("created_time DESC", "id ASC").
Group("department").
Having(common.Map{"COUNT(*).[>]": 5}). // 新增 HAVING 支持
Limit(0, 20).
Offset(10). // 新增独立 OFFSET 支持
Select("id,name,email,age")
```
## 条件查询语法
HoTimeDB支持丰富的条件查询语法,类似于Medoo:
### 基本比较
```go
// 等于
"id": 1
// 不等于
"id[!]": 1
// 大于
"age[>]": 18
// 大于等于
"age[>=]": 18
// 小于
"age[<]": 60
// 小于等于
"age[<=]": 60
```
### 模糊查询
```go
// LIKE %keyword%
"name[~]": "张"
// LIKE keyword% (右边任意)
"name[~!]": "张"
// LIKE %keyword (左边任意)
"name[!~]": "san"
// 手动LIKE(需要手动添加%
"name[~~]": "%张%"
```
### 区间查询
```go
// BETWEEN
"age[<>]": []int{18, 60}
// NOT BETWEEN
"age[><]": []int{18, 25}
```
### IN查询
```go
// IN
"id": []int{1, 2, 3, 4, 5}
// NOT IN
"id[!]": []int{1, 2, 3}
```
### NULL查询
```go
// IS NULL
"deleted_at": nil
// IS NOT NULL
"deleted_at[!]": nil
```
### 直接SQL
```go
// 直接SQL片段(通常用于复杂条件,时间推荐用 Go 计算)
"[##]": "user.status = 1 AND user.level > 0"
// 时间范围条件:推荐在 Go 侧计算,避免跨库 NOW() 差异
"created_time[>]": Time2Str(time.Now().AddDate(0, 0, -1)) // 一天前
// 如必须用数据库函数(不跨库时可用,但不推荐)
"update_time[#]": "NOW()"
```
## JOIN操作
### 链式JOIN
```go
// LEFT JOIN
users := database.Table("user").
LeftJoin("profile", "user.id = profile.user_id").
LeftJoin("department", "user.dept_id = department.id").
Where("user.status", 1).
Select("user.*, profile.avatar, department.name AS dept_name")
// RIGHT JOIN
users := database.Table("user").
RightJoin("order", "user.id = order.user_id").
Select()
// INNER JOIN
users := database.Table("user").
InnerJoin("profile", "user.id = profile.user_id").
Select()
// FULL JOIN
users := database.Table("user").
FullJoin("profile", "user.id = profile.user_id").
Select()
```
### 传统JOIN语法
```go
users := database.Select("user",
common.Slice{
common.Map{"[>]profile": "user.id = profile.user_id"},
common.Map{"[>]department": "user.dept_id = department.id"},
},
"user.*, profile.avatar, department.name AS dept_name",
common.Map{
"user.status": 1,
},
)
```
### JOIN类型说明
- `[>]`: LEFT JOIN
- `[<]`: RIGHT JOIN
- `[><]`: INNER JOIN
- `[<>]`: FULL JOIN
## 分页查询
### 基本分页
```go
// 设置分页:页码3,每页20条
users := database.Page(3, 20).PageSelect("user", "*", common.Map{
"status": 1,
})
// 链式分页
users := database.Table("user").
Where("status", 1).
Page(2, 15). // 第2页,每页15条
Select()
```
### 分页信息获取
```go
// 获取总数
total := database.Count("user", common.Map{
"status": 1,
})
// 计算分页信息
page := 2
pageSize := 20
offset := (page - 1) * pageSize
totalPages := (total + pageSize - 1) / pageSize
fmt.Printf("总记录数: %d, 总页数: %d, 当前页: %d\n", total, totalPages, page)
```
## 聚合函数
### 计数
```go
// 总数统计
total := database.Count("user")
// 条件统计
activeUsers := database.Count("user", common.Map{
"status": 1,
})
// JOIN统计
count := database.Count("user",
common.Slice{
common.Map{"[>]profile": "user.id = profile.user_id"},
},
common.Map{
"user.status": 1,
"profile.verified": 1,
},
)
```
### 求和
```go
// 基本求和
totalAmount := database.Sum("order", "amount")
// 条件求和
paidAmount := database.Sum("order", "amount", common.Map{
"status": "paid",
"created_time[>]": "2023-01-01",
})
```
### 平均值
```go
// 基本平均值
avgAge := database.Avg("user", "age")
// 条件平均值
avgAge := database.Avg("user", "age", common.Map{
"status": 1,
})
```
### 最大值/最小值
```go
// 最大值
maxAge := database.Max("user", "age")
// 最小值
minAge := database.Min("user", "age")
// 条件最大值
maxBalance := database.Max("user", "balance", common.Map{
"status": 1,
})
```
## 事务处理
```go
// 事务操作
success := database.Action(func(tx db.HoTimeDB) bool {
// 在事务中执行多个操作
// 扣减用户余额
affected1 := tx.Update("user", common.Map{
"balance[#]": "balance - 100",
}, common.Map{
"id": 1,
})
if affected1 == 0 {
return false // 回滚
}
// 创建订单
orderId := tx.Insert("order", common.Map{
"user_id": 1,
"amount": 100,
"status": "paid",
"created_time": Time2Str(time.Now()),
})
if orderId == 0 {
return false // 回滚
}
return true // 提交
})
if success {
fmt.Println("事务执行成功")
} else {
fmt.Println("事务回滚")
fmt.Println("错误:", database.LastErr.GetError())
}
```
## 缓存机制
HoTimeDB集成了缓存功能,可以自动缓存查询结果:
### 缓存配置
```go
import "code.hoteas.com/golang/hotime/cache"
// 设置缓存
database.HoTimeCache = &cache.HoTimeCache{
// 缓存配置
}
```
### 缓存行为
- 查询操作会自动检查缓存
- 增删改操作会自动清除相关缓存
- 缓存键格式:`表名:查询MD5`
- `cached`表不会被缓存
### 缓存清理
```go
// 手动清除表缓存
database.HoTimeCache.Db("user*", nil) // 清除user表所有缓存
```
## PostgreSQL支持
HoTimeDB 支持 PostgreSQL 数据库,自动处理语法差异。
### PostgreSQL 配置
```go
import (
"code.hoteas.com/golang/hotime/db"
_ "github.com/lib/pq" // PostgreSQL 驱动
)
database := &db.HoTimeDB{
Type: "postgres", // 设置数据库类型为 postgres
Prefix: "app_",
}
database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) {
dsn := "host=localhost port=5432 user=postgres password=secret dbname=mydb sslmode=disable"
master, _ = sql.Open("postgres", dsn)
return master, master
})
```
### 主要差异
| 特性 | MySQL | PostgreSQL |
|------|-------|------------|
| 标识符引号 | \`name\` | "name" |
| 占位符 | ? | $1, $2, $3... |
| Upsert | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE |
所有这些差异由框架自动处理,无需手动调整代码。
## 达梦数据库支持
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 (?, ?, ?)
```
## 高级特性
### 调试模式
```go
// 设置调试模式
database.Mode = 2
// 查看最后执行的SQL
fmt.Println("最后的SQL:", database.LastQuery)
fmt.Println("参数:", database.LastData)
fmt.Println("错误:", database.LastErr.GetError())
```
### 主从分离
```go
func createConnection() (master, slave *sql.DB) {
// 主库连接
master, _ = sql.Open("mysql", "user:password@tcp(master:3306)/database")
// 从库连接
slave, _ = sql.Open("mysql", "user:password@tcp(slave:3306)/database")
return master, slave
}
database.SetConnect(createConnection)
// 查询会自动使用从库,增删改使用主库
```
### 原生SQL执行
```go
// 执行查询SQL
results := database.Query("SELECT * FROM user WHERE age > ? AND status = ?", 18, 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)
}
```
## 特殊语法详解
### 条件标记符说明
| 标记符 | 功能 | 示例 | 生成SQL |
|--------|------|------|---------|
| `[>]` | 大于 | `"age[>]": 18` | `age > 18` |
| `[<]` | 小于 | `"age[<]": 60` | `age < 60` |
| `[>=]` | 大于等于 | `"age[>=]": 18` | `age >= 18` |
| `[<=]` | 小于等于 | `"age[<=]": 60` | `age <= 60` |
| `[!]` | 不等于/NOT IN | `"id[!]": 1` | `id != 1` |
| `[~]` | LIKE模糊查询 | `"name[~]": "张"` | `name LIKE '%张%'` |
| `[!~]` | 左模糊 | `"name[!~]": "张"` | `name LIKE '%张'` |
| `[~!]` | 右模糊 | `"name[~!]": "张"` | `name LIKE '张%'` |
| `[~~]` | 手动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表达式 | `"balance[#]": "balance + 1"` | `balance = balance + 1` |
| `[##]` | SQL片段 | `"[##]": "a > b"` | `a > b` |
| `[#!]` | 不等于直接SQL | `"status[#!]": "1"` | `status != 1` |
| `[!#]` | 不等于直接SQL | `"status[!#]": "1"` | `status != 1` |
### 特殊关键字(支持大小写)
| 关键字 | 功能 | 示例 |
|--------|------|------|
| `ORDER` / `order` | 排序 | `"ORDER": "id DESC"` |
| `GROUP` / `group` | 分组 | `"GROUP": "dept_id"` |
| `LIMIT` / `limit` | 限制 | `"LIMIT": 10` |
| `OFFSET` / `offset` | 偏移 | `"OFFSET": 20` |
| `HAVING` / `having` | 分组过滤 | `"HAVING": Map{"cnt[>]": 5}` |
| `DISTINCT` / `distinct` | 去重 | 在 SELECT 中使用 |
## 常见问题
### Q1: 多条件查询需要用 AND 包装吗?
A1: 不再需要!现在多条件会自动用 AND 连接。当然,使用 `AND` 包装仍然有效(向后兼容)。
### Q2: 如何处理事务中的错误?
A2: 在 `Action` 函数中返回 `false` 即可触发回滚,所有操作都会被撤销。
### Q3: 缓存何时会被清除?
A3: 执行 `Insert``Update``Delete``Upsert``Inserts` 操作时会自动清除对应表的缓存。
### Q4: 如何执行复杂的原生SQL?
A4: 使用 `Query` 方法执行查询,使用 `Exec` 方法执行更新操作。
### Q5: 主从分离如何工作?
A5: 查询操作自动使用从库(如果配置了),增删改操作使用主库。
### Q6: 如何处理NULL值?
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.1*
*最后更新: 2026年3月*
> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,根据Golang语言特性进行了适配和优化,并新增了对达梦(DM8)国产数据库的完整支持。
**更多参考:**
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册