de17ecbfd5
- 在 .gitignore 中添加调试日志文件的忽略规则,避免不必要的调试信息被提交 - 修改 application.go 中的 stdout 重定向逻辑,使用 log.CaptureStream 以支持更灵活的日志捕获 - 更新 README 文档,增加对日志重定向功能的说明
1046 lines
27 KiB
Markdown
1046 lines
27 KiB
Markdown
# HoTimeDB ORM 使用说明书
|
||
|
||
> 完整教程见本文;方法签名与条件运算符速查见 [HoTimeDB_API参考.md](HoTimeDB_API参考.md)
|
||
|
||
## 概述
|
||
|
||
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 速查手册
|