# 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