stardb/README.MD

561 lines
15 KiB
Markdown
Raw Permalink 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.

# 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