561 lines
15 KiB
Markdown
561 lines
15 KiB
Markdown
# StarDB
|
||
|
||
StarDB 是对 Go `database/sql` 的轻量封装,目标是把常见的数据库操作做得更直白:
|
||
- 少量 API 覆盖日常 CRUD、事务、批量写入、结构体映射。
|
||
- 兼容原生 `database/sql` 心智,不引入重量级依赖。
|
||
- 在可读性、可调试性和性能之间做实用平衡。
|
||
|
||
适合:
|
||
- 想保留 SQL 控制权,但不想反复写样板代码。
|
||
- 需要轻量 ORM 映射(不是全功能 ORM)。
|
||
- 需要在生产里追踪 SQL(可选 Hook + 慢 SQL 阈值)。
|
||
|
||
不适合:
|
||
- 需要完整领域模型关系管理、自动迁移、复杂查询 DSL 的项目。
|
||
|
||
## 安装
|
||
|
||
```bash
|
||
go get b612.me/stardb
|
||
```
|
||
|
||
要求:
|
||
- Go `>= 1.16`
|
||
- 自行选择并导入数据库驱动(本库只封装 `database/sql`)
|
||
|
||
## 常见 DSN 示例
|
||
|
||
下面示例都可以直接用于 `db.Open(driver, dsn)`,替换为实际账号、密码、库名即可。
|
||
|
||
### MySQL(`github.com/go-sql-driver/mysql`)
|
||
|
||
```go
|
||
import _ "github.com/go-sql-driver/mysql"
|
||
|
||
dsn := "app:secret@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local"
|
||
if err := db.Open("mysql", dsn); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
```
|
||
|
||
常用参数说明:
|
||
- `charset=utf8mb4`:避免字符集问题。
|
||
- `parseTime=true`:把 `DATETIME/TIMESTAMP` 解析为 `time.Time`。
|
||
- `loc=Local`:指定时间解析时区(也可改成 `Asia/Shanghai`)。
|
||
|
||
### PostgreSQL(`github.com/lib/pq`)
|
||
|
||
```go
|
||
import _ "github.com/lib/pq"
|
||
|
||
dsn := "host=127.0.0.1 port=5432 user=postgres password=secret dbname=demo sslmode=disable"
|
||
if err := db.Open("postgres", dsn); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
```
|
||
|
||
也可以用 URL 形式:
|
||
|
||
```go
|
||
urlDSN := "postgres://postgres:secret@127.0.0.1:5432/demo?sslmode=disable"
|
||
if err := db.Open("postgres", urlDSN); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
```
|
||
|
||
### SQLite(`modernc.org/sqlite`)
|
||
|
||
```go
|
||
import _ "modernc.org/sqlite"
|
||
|
||
// 文件数据库
|
||
if err := db.Open("sqlite", "file:demo.db"); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
// 内存数据库(适合测试)
|
||
if err := db.Open("sqlite", "file::memory:?cache=shared"); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
```
|
||
|
||
Windows 路径建议使用 `file:C:/data/demo.db` 这种写法,跨平台更稳。
|
||
|
||
## 能力概览
|
||
|
||
| 能力 | 主要 API | 说明 |
|
||
|---|---|---|
|
||
| 连接与连接池 | `Open` `Close` `Ping` `SetPoolConfig` | 保留原生 `sql.DB` 用法 |
|
||
| 常规查询 | `Query` `QueryContext` | 自动解析为 `StarRows` |
|
||
| 流式查询 | `QueryRaw` `ScanEach` | 大结果集不必全量进内存 |
|
||
| 流式 ORM | `ScanEachORM` | 逐行映射结构体 |
|
||
| 安全取值 | `Get*` `GetNull*` | 明确错误与 NULL 语义 |
|
||
| 结构体 ORM | `rows.Orm` | 支持单个、切片、数组映射 |
|
||
| 命名参数 | `QueryX` `ExecX` | `:field` 绑定结构体字段 |
|
||
| 结构体写入 | `Insert` `Update` | 通过 `db` tag 生成 SQL |
|
||
| 批量写入 | `BatchInsert` `BatchInsertStructs` `SetBatchInsertMaxRows` `SetBatchInsertMaxParams` | 多行插入,支持按行数/参数阈值分片 |
|
||
| 事务 | `Begin/Commit/Rollback` `WithTx` | 手动或托管事务 |
|
||
| 可观测性 | `SetSQLHooks` `SetSQLSlowThreshold` `SetSQLFingerprintEnabled` `SetSQLFingerprintMode` `SetSQLFingerprintKeepComments` `SetSQLFingerprintCounterEnabled` `SQLFingerprintCounters` `ResetSQLFingerprintCounters` `SQLHookMetaFromContext` `BatchExecMetaFromContext` | Before/After Hook,默认关闭,支持指纹策略、命中计数与批量分片元信息 |
|
||
| 方言占位符 | `SetPlaceholderStyle` | `?` / `$1,$2...` |
|
||
| 查询构建 | `QueryBuilder` | 支持 `JOIN/GROUP BY/HAVING` |
|
||
|
||
## 场景选型
|
||
|
||
| 场景 | 首选 API | 说明 |
|
||
|---|---|---|
|
||
| 中小结果集查询 | `Query` + `rows.Orm` | 读取方便,开发效率高 |
|
||
| 大结果集查询 | `ScanEach` / `ScanEachORM` | 逐行处理,避免全量缓存 |
|
||
| 需要底层 `Scan` 控制 | `QueryRaw` | 直接返回 `*sql.Rows` |
|
||
| 批量写入 | `BatchInsert` + 分片阈值 | 控制单条 SQL 大小与参数数量 |
|
||
| SQL 可观测 | `SetSQLHooks` + `SetSQLSlowThreshold` + 指纹配置 | 支持慢 SQL、指纹、分片元信息 |
|
||
|
||
## 快速开始
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"log"
|
||
|
||
"b612.me/stardb"
|
||
_ "modernc.org/sqlite"
|
||
)
|
||
|
||
type User struct {
|
||
ID int64 `db:"id"`
|
||
Name string `db:"name"`
|
||
Age int `db:"age"`
|
||
}
|
||
|
||
func main() {
|
||
db := stardb.NewStarDB()
|
||
if err := db.Open("sqlite", "test.db"); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
defer db.Close()
|
||
|
||
rows, err := db.Query("SELECT id, name, age FROM users WHERE age >= ?", 18)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
var users []User
|
||
if err := rows.Orm(&users); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
log.Printf("users: %d", len(users))
|
||
}
|
||
```
|
||
|
||
## 接入流程
|
||
|
||
按下面顺序接入,可在开发阶段先固定运行边界:
|
||
|
||
1. 建立连接并设置连接池。
|
||
2. 在查询路径中区分内存模式与流式模式。
|
||
3. 在批量写入路径设置分片阈值(按行数和参数数)。
|
||
4. 启用 SQL Hook、慢 SQL 阈值、指纹策略。
|
||
5. 在调用侧统一使用 `errors.Is` 判定错误类别。
|
||
|
||
一个常用初始化示例:
|
||
|
||
```go
|
||
db := stardb.NewStarDB()
|
||
if err := db.Open("mysql", dsn); err != nil {
|
||
return err
|
||
}
|
||
|
||
db.SetPoolConfig(&stardb.PoolConfig{
|
||
MaxOpenConns: 25,
|
||
MaxIdleConns: 5,
|
||
ConnMaxLifetime: time.Hour,
|
||
ConnMaxIdleTime: 10 * time.Minute,
|
||
})
|
||
|
||
db.SetBatchInsertMaxRows(500)
|
||
db.SetBatchInsertMaxParams(60000)
|
||
|
||
db.SetSQLSlowThreshold(200 * time.Millisecond)
|
||
db.SetSQLFingerprintEnabled(true)
|
||
db.SetSQLFingerprintMode(stardb.SQLFingerprintMaskLiterals)
|
||
db.SetSQLFingerprintKeepComments(false)
|
||
```
|
||
|
||
## API 指南
|
||
|
||
### 1) 连接与连接池
|
||
|
||
```go
|
||
db := stardb.NewStarDB()
|
||
_ = db.Open("mysql", "user:pass@tcp(localhost:3306)/app")
|
||
|
||
db.SetPoolConfig(&stardb.PoolConfig{
|
||
MaxOpenConns: 25,
|
||
MaxIdleConns: 5,
|
||
ConnMaxLifetime: time.Hour,
|
||
ConnMaxIdleTime: 10 * time.Minute,
|
||
})
|
||
```
|
||
|
||
也可以直接拿到底层连接:
|
||
|
||
```go
|
||
raw := db.DB()
|
||
raw.SetMaxOpenConns(50)
|
||
```
|
||
|
||
### 2) 三种查询模式
|
||
|
||
#### A. 内存模式(默认)
|
||
|
||
`Query` 会把结果解析为 `StarRows`,适合中小结果集:
|
||
|
||
```go
|
||
rows, err := db.Query("SELECT * FROM users WHERE active = ?", true)
|
||
if err != nil { /* ... */ }
|
||
defer rows.Close()
|
||
|
||
for i := 0; i < rows.Length(); i++ {
|
||
row := rows.Row(i)
|
||
_ = row.MustString("name")
|
||
}
|
||
```
|
||
|
||
#### B. 原生流式模式
|
||
|
||
`QueryRaw` 返回 `*sql.Rows`,完全按原生 `Scan` 处理:
|
||
|
||
```go
|
||
rawRows, err := db.QueryRaw("SELECT id, name FROM users")
|
||
if err != nil { /* ... */ }
|
||
defer rawRows.Close()
|
||
```
|
||
|
||
#### C. 回调流式模式(常用)
|
||
|
||
`ScanEach` 逐行回调,避免全量缓存:
|
||
|
||
关闭内存预读时,使用 `QueryRaw` / `ScanEach` / `ScanEachORM`,不使用 `Query`。
|
||
|
||
```go
|
||
err := db.ScanEach("SELECT id, name FROM users", func(row *stardb.StarResult) error {
|
||
id := row.MustInt64("id")
|
||
name := row.MustString("name")
|
||
_ = id
|
||
_ = name
|
||
return nil
|
||
})
|
||
```
|
||
|
||
可通过 `stardb.ErrScanStopped` 提前终止:
|
||
|
||
```go
|
||
count := 0
|
||
_ = db.ScanEach("SELECT * FROM users", func(row *stardb.StarResult) error {
|
||
count++
|
||
if count >= 1000 {
|
||
return stardb.ErrScanStopped
|
||
}
|
||
return nil
|
||
})
|
||
```
|
||
|
||
### 3) 流式 ORM(逐行映射)
|
||
|
||
`ScanEachORM` 将每行映射到结构体,再回调。
|
||
|
||
```go
|
||
var model User
|
||
var users []User
|
||
|
||
err := db.ScanEachORM("SELECT id, name, age FROM users", &model, func(target interface{}) error {
|
||
u := *(target.(*User)) // 注意拷贝一份,target 会被复用
|
||
users = append(users, u)
|
||
return nil
|
||
})
|
||
```
|
||
|
||
同样支持 `Tx` / `Stmt`:
|
||
- `tx.ScanEachORM(...)`
|
||
- `stmt.ScanEachORM(...)`
|
||
|
||
### 4) 结果读取与 NULL 语义
|
||
|
||
#### Must 系列(无错误,失败给零值)
|
||
- `MustString` `MustInt64` `MustFloat64` `MustBool` ...
|
||
|
||
#### 安全系列(带错误)
|
||
- `GetString` `GetInt64` `GetFloat64`
|
||
- `GetNullString` `GetNullInt64` `GetNullFloat64` `GetNullBool` `GetNullTime`
|
||
|
||
```go
|
||
name, err := row.GetString("name")
|
||
age, err := row.GetNullInt64("age")
|
||
if age.Valid {
|
||
// use age.Int64
|
||
}
|
||
```
|
||
|
||
### 5) ORM 映射
|
||
|
||
```go
|
||
type User struct {
|
||
ID int64 `db:"id"`
|
||
Name string `db:"name"`
|
||
}
|
||
|
||
var u User
|
||
_ = rows.Orm(&u)
|
||
|
||
var list []User
|
||
_ = rows.Orm(&list)
|
||
```
|
||
|
||
严格列检查(字段/SQL 变更敏感场景可开启):
|
||
|
||
```go
|
||
db.SetStrictORM(true)
|
||
```
|
||
|
||
若结构体 tag 大范围调整,可清理反射缓存:
|
||
|
||
```go
|
||
stardb.ClearReflectCache()
|
||
```
|
||
|
||
### 6) 命名参数绑定
|
||
|
||
```go
|
||
type Filter struct {
|
||
Name string `db:"name"`
|
||
MinAge int `db:"min_age"`
|
||
}
|
||
|
||
f := Filter{Name: "Alice", MinAge: 18}
|
||
rows, err := db.QueryX(&f,
|
||
"SELECT * FROM users WHERE name = ? AND age >= ?",
|
||
":name", ":min_age")
|
||
```
|
||
|
||
### 7) 写入能力
|
||
|
||
#### Insert / Update
|
||
|
||
```go
|
||
_, _ = db.Insert(&user, "users", "id") // id 作为自增字段跳过
|
||
_, _ = db.Update(&user, "users", "id") // id 作为主键
|
||
```
|
||
|
||
#### BatchInsert
|
||
|
||
```go
|
||
columns := []string{"name", "email", "age"}
|
||
values := [][]interface{}{
|
||
{"Alice", "alice@example.com", 25},
|
||
{"Bob", "bob@example.com", 30},
|
||
}
|
||
_, _ = db.BatchInsert("users", columns, values)
|
||
```
|
||
|
||
如需避免单条 SQL 过大(参数过多),可打开分片:
|
||
|
||
```go
|
||
db.SetBatchInsertMaxRows(500) // 0 或负数表示关闭分片(默认)
|
||
db.SetBatchInsertMaxParams(60000) // 0 表示自动识别常见驱动参数上限
|
||
```
|
||
|
||
分片模式下会在一个事务里按块执行,避免部分写入成功。
|
||
自动识别当前覆盖:SQLite `999`、PostgreSQL `65535`、MySQL `65535`、SQL Server `2100`。
|
||
|
||
分片行为细节:
|
||
- 分片阈值按更严格条件生效:`min(maxRows, maxParams/列数)`(忽略未设置项)。
|
||
- 分片关闭条件:`maxRows <= 0` 且 `maxParams <= 0` 且未命中驱动自动阈值。
|
||
- 分片执行失败会回滚整个批次。
|
||
- 分片结果语义:
|
||
- `RowsAffected()` 返回所有分片累计值。
|
||
- `LastInsertId()` 返回最后一个分片的 insert id。
|
||
|
||
#### BatchInsertStructs
|
||
|
||
```go
|
||
users := []User{{Name: "Alice"}, {Name: "Bob"}}
|
||
_, _ = db.BatchInsertStructs("users", users, "id")
|
||
```
|
||
|
||
### 8) 事务
|
||
|
||
#### 手动事务
|
||
|
||
```go
|
||
tx, err := db.Begin()
|
||
if err != nil { /* ... */ }
|
||
defer tx.Rollback()
|
||
|
||
if _, err := tx.Exec("UPDATE users SET age = age + 1 WHERE id = ?", 1); err != nil {
|
||
return err
|
||
}
|
||
return tx.Commit()
|
||
```
|
||
|
||
#### 托管事务(常用)
|
||
|
||
```go
|
||
err := db.WithTx(func(tx *stardb.StarTx) error {
|
||
if _, err := tx.Exec("UPDATE users SET age = ? WHERE id = ?", 26, 1); err != nil {
|
||
return err
|
||
}
|
||
if _, err := tx.Exec("INSERT INTO logs (msg) VALUES (?)", "age updated"); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
})
|
||
```
|
||
|
||
`WithTx` 规则:
|
||
- `fn` 返回 `nil` -> `Commit`
|
||
- `fn` 返回错误 -> `Rollback`
|
||
- `fn` panic -> `Rollback` 后继续抛出 panic
|
||
|
||
### 9) SQL Hook 与慢 SQL 阈值
|
||
|
||
默认关闭;仅在显式设置时生效。
|
||
|
||
```go
|
||
db.SetSQLSlowThreshold(200 * time.Millisecond)
|
||
db.SetSQLFingerprintEnabled(true) // 可选:在 Hook context 附带 SQL 指纹
|
||
db.SetSQLFingerprintMode(stardb.SQLFingerprintMaskLiterals) // 可选:指纹里脱敏数字/字符串字面量
|
||
db.SetSQLFingerprintKeepComments(false) // 默认 false:指纹不保留 SQL 注释
|
||
db.SetSQLFingerprintCounterEnabled(true) // 可选:记录指纹命中次数(内存级)
|
||
|
||
db.SetSQLHooks(
|
||
func(ctx context.Context, query string, args []interface{}) {
|
||
// before
|
||
},
|
||
func(ctx context.Context, query string, args []interface{}, d time.Duration, err error) {
|
||
// after
|
||
if hookMeta, ok := stardb.SQLHookMetaFromContext(ctx); ok {
|
||
_ = hookMeta.Fingerprint
|
||
}
|
||
if meta, ok := stardb.BatchExecMetaFromContext(ctx); ok {
|
||
// chunked batch insert metadata
|
||
// meta.ChunkIndex / meta.ChunkCount / meta.ChunkRows ...
|
||
}
|
||
},
|
||
)
|
||
```
|
||
|
||
阈值行为:
|
||
- `threshold <= 0`:`After` 每次都触发
|
||
- `threshold > 0`:仅在“慢于阈值”或“执行出错”时触发
|
||
- 打开 `SetSQLFingerprintEnabled(true)` 后,可从 `SQLHookMetaFromContext` 获取 SQL 指纹
|
||
- 指纹模式:`SQLFingerprintBasic`(默认,仅归一化)/ `SQLFingerprintMaskLiterals`(归一化 + 字面量脱敏)
|
||
- `SetSQLFingerprintKeepComments(true)` 可保留注释文本(默认关闭,利于聚合)
|
||
- `SetSQLFingerprintCounterEnabled(true)` 后,可通过 `SQLFingerprintCounters()` 查看命中次数,`ResetSQLFingerprintCounters()` 清空
|
||
- 若是分片批量写入,Hook 可通过 `BatchExecMetaFromContext` 读取分片元信息
|
||
|
||
Hook 上下文字段说明:
|
||
- `SQLHookMetaFromContext(ctx)`:
|
||
- `Fingerprint`:按配置生成的 SQL 指纹。
|
||
- `BatchExecMetaFromContext(ctx)`(仅分片批量写入):
|
||
- `ChunkIndex`:当前分片序号(从 1 开始)
|
||
- `ChunkCount`:总分片数
|
||
- `ChunkRows`:当前分片行数
|
||
- `TotalRows`:本次批量总行数
|
||
- `ColumnCount`:本次写入列数
|
||
|
||
### 10) 占位符方言
|
||
|
||
```go
|
||
db.SetPlaceholderStyle(stardb.PlaceholderQuestion) // 默认
|
||
// 或
|
||
db.SetPlaceholderStyle(stardb.PlaceholderDollar) // ? -> $1,$2...
|
||
```
|
||
|
||
### 11) QueryBuilder
|
||
|
||
```go
|
||
query, args := stardb.NewQueryBuilder("users u").
|
||
Select("u.id", "u.name", "COUNT(o.id) AS order_count").
|
||
Join("LEFT JOIN orders o ON o.user_id = u.id").
|
||
Where("u.active = ?", true).
|
||
GroupBy("u.id", "u.name").
|
||
Having("COUNT(o.id) > ?", 2).
|
||
OrderBy("order_count DESC").
|
||
Limit(20).
|
||
Offset(0).
|
||
Build()
|
||
|
||
_ = query
|
||
_ = args
|
||
```
|
||
|
||
## 错误处理
|
||
|
||
库内置可判定错误,调用侧使用 `errors.Is` 做分支处理:
|
||
|
||
```go
|
||
if errors.Is(err, stardb.ErrDBNotInitialized) {
|
||
// 未初始化
|
||
}
|
||
if errors.Is(err, stardb.ErrColumnNotFound) {
|
||
// 字段/列不匹配
|
||
}
|
||
if errors.Is(err, stardb.ErrNoInsertValues) {
|
||
// 批量插入空数据
|
||
}
|
||
```
|
||
|
||
常见错误类别:
|
||
- 生命周期:`ErrDBNotInitialized` `ErrTxNotInitialized` `ErrStmtNotInitialized`
|
||
- 参数校验:`ErrQueryEmpty` `ErrTargetNil` `ErrTargetNotPointer` ...
|
||
- 映射问题:`ErrColumnNotFound` `ErrFieldNotFound`
|
||
- 批量写入:`ErrNoInsertColumns` `ErrNoInsertValues` `ErrBatchRowValueCountMismatch`
|
||
- 流式回调:`ErrScanFuncNil` `ErrScanORMFuncNil`
|
||
|
||
## 使用边界
|
||
|
||
1. 这是轻量封装,不是全功能 ORM。
|
||
- 不做模型关系管理(has-many/association)
|
||
- 不做自动迁移
|
||
- 不做复杂查询 DSL
|
||
|
||
2. 大结果集优先用流式 API。
|
||
- `Query` 适合中小结果集
|
||
- `ScanEach` / `ScanEachORM` 更稳
|
||
|
||
3. 日志 Hook 按需打开。
|
||
- 生产环境最好配合慢 SQL 阈值,减少噪音
|
||
|
||
4. `ScanEachORM` 回调里的 target 会复用。
|
||
- 需要持久化时请拷贝结构体值
|
||
|
||
## 测试、竞态与基准
|
||
|
||
```bash
|
||
# 根模块
|
||
go test ./...
|
||
|
||
go test -race ./...
|
||
|
||
go test -run ^$ -bench BenchmarkQueryBuilder_ -benchmem ./...
|
||
|
||
# testing 子模块(集成测试/基准)
|
||
cd testing
|
||
go test ./...
|
||
go test -race ./...
|
||
go test -run ^$ -bench "Benchmark(QueryX|Orm|ScanEach|BatchInsert)" -benchmem
|
||
```
|
||
|
||
## 支持数据库驱动
|
||
|
||
本库兼容所有实现 `database/sql` 的驱动。常见示例:
|
||
- SQLite: `_ "modernc.org/sqlite"`
|
||
- MySQL: `_ "github.com/go-sql-driver/mysql"`
|
||
- PostgreSQL: `_ "github.com/lib/pq"`
|
||
|
||
## License
|
||
|
||
Apache License 2.0
|