stardb/README.MD

15 KiB
Raw Permalink Blame History

StarDB

StarDB 是对 Go database/sql 的轻量封装,目标是把常见的数据库操作做得更直白:

  • 少量 API 覆盖日常 CRUD、事务、批量写入、结构体映射。
  • 兼容原生 database/sql 心智,不引入重量级依赖。
  • 在可读性、可调试性和性能之间做实用平衡。

适合:

  • 想保留 SQL 控制权,但不想反复写样板代码。
  • 需要轻量 ORM 映射(不是全功能 ORM
  • 需要在生产里追踪 SQL可选 Hook + 慢 SQL 阈值)。

不适合:

  • 需要完整领域模型关系管理、自动迁移、复杂查询 DSL 的项目。

安装

go get b612.me/stardb

要求:

  • Go >= 1.16
  • 自行选择并导入数据库驱动(本库只封装 database/sql

常见 DSN 示例

下面示例都可以直接用于 db.Open(driver, dsn),替换为实际账号、密码、库名即可。

MySQLgithub.com/go-sql-driver/mysql

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)。

PostgreSQLgithub.com/lib/pq

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 形式:

urlDSN := "postgres://postgres:secret@127.0.0.1:5432/demo?sslmode=disable"
if err := db.Open("postgres", urlDSN); err != nil {
    log.Fatal(err)
}

SQLitemodernc.org/sqlite

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、指纹、分片元信息

快速开始

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 判定错误类别。

一个常用初始化示例:

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) 连接与连接池

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,
})

也可以直接拿到底层连接:

raw := db.DB()
raw.SetMaxOpenConns(50)

2) 三种查询模式

A. 内存模式(默认)

Query 会把结果解析为 StarRows,适合中小结果集:

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 处理:

rawRows, err := db.QueryRaw("SELECT id, name FROM users")
if err != nil { /* ... */ }
defer rawRows.Close()

C. 回调流式模式(常用)

ScanEach 逐行回调,避免全量缓存:

关闭内存预读时,使用 QueryRaw / ScanEach / ScanEachORM,不使用 Query

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 提前终止:

count := 0
_ = db.ScanEach("SELECT * FROM users", func(row *stardb.StarResult) error {
    count++
    if count >= 1000 {
        return stardb.ErrScanStopped
    }
    return nil
})

3) 流式 ORM逐行映射

ScanEachORM 将每行映射到结构体,再回调。

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
name, err := row.GetString("name")
age, err := row.GetNullInt64("age")
if age.Valid {
    // use age.Int64
}

5) ORM 映射

type User struct {
    ID   int64  `db:"id"`
    Name string `db:"name"`
}

var u User
_ = rows.Orm(&u)

var list []User
_ = rows.Orm(&list)

严格列检查(字段/SQL 变更敏感场景可开启):

db.SetStrictORM(true)

若结构体 tag 大范围调整,可清理反射缓存:

stardb.ClearReflectCache()

6) 命名参数绑定

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

_, _ = db.Insert(&user, "users", "id")      // id 作为自增字段跳过
_, _ = db.Update(&user, "users", "id")      // id 作为主键

BatchInsert

columns := []string{"name", "email", "age"}
values := [][]interface{}{
    {"Alice", "alice@example.com", 25},
    {"Bob", "bob@example.com", 30},
}
_, _ = db.BatchInsert("users", columns, values)

如需避免单条 SQL 过大(参数过多),可打开分片:

db.SetBatchInsertMaxRows(500) // 0 或负数表示关闭分片(默认)
db.SetBatchInsertMaxParams(60000) // 0 表示自动识别常见驱动参数上限

分片模式下会在一个事务里按块执行,避免部分写入成功。 自动识别当前覆盖SQLite 999、PostgreSQL 65535、MySQL 65535、SQL Server 2100

分片行为细节:

  • 分片阈值按更严格条件生效:min(maxRows, maxParams/列数)(忽略未设置项)。
  • 分片关闭条件:maxRows <= 0maxParams <= 0 且未命中驱动自动阈值。
  • 分片执行失败会回滚整个批次。
  • 分片结果语义:
    • RowsAffected() 返回所有分片累计值。
    • LastInsertId() 返回最后一个分片的 insert id。

BatchInsertStructs

users := []User{{Name: "Alice"}, {Name: "Bob"}}
_, _ = db.BatchInsertStructs("users", users, "id")

8) 事务

手动事务

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()

托管事务(常用)

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 阈值

默认关闭;仅在显式设置时生效。

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 <= 0After 每次都触发
  • 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) 占位符方言

db.SetPlaceholderStyle(stardb.PlaceholderQuestion) // 默认
// 或
db.SetPlaceholderStyle(stardb.PlaceholderDollar)   // ? -> $1,$2...

11) QueryBuilder

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 做分支处理:

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
  1. 大结果集优先用流式 API。
  • Query 适合中小结果集
  • ScanEach / ScanEachORM 更稳
  1. 日志 Hook 按需打开。
  • 生产环境最好配合慢 SQL 阈值,减少噪音
  1. ScanEachORM 回调里的 target 会复用。
  • 需要持久化时请拷贝结构体值

测试、竞态与基准

# 根模块
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