stardb/README.MD

561 lines
15 KiB
Plaintext
Raw Permalink Normal View History

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