Files
go-omni/database/README.md

10 KiB
Raw Blame History

database

结构化 MySQL 配置、独立 TLS/mTLS、驱动日志和生成项目接入见 官方 MySQL 适配器。已有 Open/OpenConnector 接口继续支持。

使用 GORM 的项目可显式接入 gormx 可选适配器,复用本包连接池,并将 SQL 日志接入 logx;普通 database 包不导入 GORM。

database 包提供基于标准库 database/sql 的连接池生命周期,不绑定 ORM 或驱动:

  • Config:驱动、DSN 与连接池参数;嵌入项目配置,通过文件显式引用变量。
  • Open / OpenConnector:通过 DSN 或标准 Connector 创建连接池、设置连接上限并在返回前执行 PingContext。
  • Health:用于就绪检查,等待连接和 Ping 均受 PingTimeout 限制。
  • InTx:统一事务提交和回滚;工作函数返回错误或 panic 时尝试回滚。返回 TxError 区分阶段与结果,提交错误不代表已回滚。
  • Close:由嵌入的 *sql.DB 提供,应用退出时显式调用。

框架不导入 MySQL、PostgreSQL 等具体驱动。应用负责注册驱动:

import (
	"context"
    "git.yuntuo.hk/k2safe/go-omni/config"
	_ "github.com/go-sql-driver/mysql"
	"git.yuntuo.hk/k2safe/go-omni/database"
)

cfg, err := config.LoadProject("etc/database.yaml", database.DefaultConfig())
if err != nil {
	return err
}
db, err := database.Open(context.Background(), cfg)
if err != nil {
	return err
}
defer db.Close()

对应文件(框架不规定变量名):

driver: mysql
dsn: "${DB_DSN}"
maxOpenConns: "${DB_MAX_OPEN:-10}"

Open 使用的 Driver、DSN 必填。建库、迁移、表结构和业务 Repository 由项目负责。v0.22 删除 ConfigFromEnv/ApplyEnv;不再隐式扫描数据库变量。

MaxIdleConns=0 会禁用空闲连接。初始化 Ping 失败会关闭已创建的连接池。

go-omni new --database mysql 会生成项目自己的 MySQL Repository、ServiceContext、 数据库就绪探针和 schema.sql。核心包不需要知道业务表结构。

真实数据库验收(需要本地 Docker):

python3 scripts/verifyDatabase.py --version v0.2.0

脚本仅操作本次创建的独立容器与数据库,验证 HTTP CRUD、服务重启持久化、事务提交与 错误/panic/取消回滚、连接池关闭、缺少表结构时启动失败,以及停库后的 readiness 503 和恢复后的 200。 结束时清理容器、匿名数据卷和临时项目。常规 go test ./... 不依赖 Docker。

项目配置

配置结构作为项目 Config 的字段,Default() 初始化连接池,文件按需覆盖。DSN 可使用 dsn: "${DB_DSN}";database.Config 的 JSON 序列化不输出 DSN 原值。不要打印原始配置对象。当前 API 脚手架使用结构化 MySQL 适配器,由 svc 统一初始化与关闭。

事务阶段与结果

v0.25 起,db.InTx(ctx, options, work) 签名不变,所有普通错误返回都是 *database.TxError。返回 nil 表示 Commit 返回成功;框架不重试回调、提交或整个事务。业务通过标准库 errors.As/Is 判断,不解析错误文本:

err := db.InTx(ctx, nil, func(tx *sql.Tx) error {
    _, err := tx.ExecContext(ctx, "INSERT INTO objects (id, storage_key) VALUES (?, ?)", id, key)
    return err
})
if err != nil {
    var txErr *database.TxError
    if errors.As(err, &txErr) {
        switch txErr.Outcome {
        case database.TxOutcomeNotStarted, database.TxOutcomeRolledBack:
            // 当前受管理事务未执行回调或已确认回滚。
            // 业务结合文件归属、并发操作和其他副作用,决定是否清理。
        default:
            // 包括 TxOutcomeUnknown:保留文件,交给业务后续核查。
            // 不因为收到提交错误就删除文件或直接重试整个操作。
        }
        // txErr.Stage 是 begin / work / commit。
        // txErr.Err 是主要原因;txErr.RollbackErr 是回滚调用返回的原因。
    }
    if errors.Is(err, context.DeadlineExceeded) {
        // 说明错误链包含超时,不意味着数据库没有提交。
    }
    return err
}

errors 为标准库包,业务自己的数据库驱动和 SQL 参数规则不变。完整可运行示例见 examples/transactions。

情况 Stage Outcome RollbackErr
未初始化数据库、nil 回调、BeginTx 报错 TxStageBegin TxOutcomeNotStarted nil;框架没有拿到事务,不执行回调
回调报错,显式 Rollback 成功 TxStageWork TxOutcomeRolledBack nil
回调报错,Rollback 失败或返回 ErrTxDone TxStageWork TxOutcomeUnknown 原始回滚错误
Commit 报错,包括取消/断网/驱动错误 TxStageCommit TxOutcomeUnknown 后续清理的 Rollback 返回值,可能为 ErrTxDone 或 nil

Stage 描述主要失败发生的位置;Outcome 只描述框架掌握的结果,二者独立。提交调用返回错误后一律保守标为 unknown,即使某个驱动的某种错误意味着没有提交,通用层也不根据错误文本猜测。标准库在 Commit 后通常已经结束本地事务对象,后续 Rollback 返回 sql.ErrTxDone;它不能证明数据库已回滚,框架保留该原因。

TxError.Err 与 RollbackErr 分开保存;Unwrap() []error 按主要原因、回滚原因顺序暴露,所以 errors.Is/As 可查两条链。需要区分来源时对 txErr.Err 或 txErr.RollbackErr 单独判断。回调返回另一个事务的 TxError 时外层不把它压平:外层结果只属于外层事务,内部事务的结果仍在 Err 中。

结果保证的范围: 回调使用给定 *sql.Tx 完成事务操作,不自行 Commit/Rollback,不执行会隐式提交的 SQL,不将事务交给后台 goroutine。非事务表、其他连接、嵌套的独立事务、文件和第三方调用的副作用不在这份保证内。框架没有删除文件、入队补偿或分布式事务规则。

取消与异常边界: context 取消时 database/sql 可能自行异步回滚,且不公开那次驱动 Rollback 的原始错误。框架若只观察到 ErrTxDone,就返回 unknown,不能补造成功证据或不存在的底层原因。正常返回路径上,框架实际观察到的主要/回滚原因全部保留。panic/runtime.Goexit 保持原有传播行为并尝试回滚,不转换为普通 error;这类展开没有错误返回值,因此不能把回滚原因作为返回值交给调用方。nil context 使用 Background,nil 回调在开启事务前报错。

兼容说明: 已有 errors.Is/As 继续生效;以前直接用 err == sentinel、err.(*SomeError) 或匹配文本的调用要改成 errors.Is/As。业务 APIError、Go 错误目录和参数提示仍可穿过包装解析,驱动错误不会自动进入公开提示。TxError.Error 包含内部原因,仅供内部诊断。GORM 自己的 Transaction 不经过本方法,也不会自动变成 TxError;可选 gormx 的连接池接入不变。

Connector 接入(v0.9.1-preview)

项目已有 database/sql/driver.Connector 时使用 OpenConnector。这是标准驱动接口,核心包不增加具体驱动依赖。项目可以在同一进程创建多个使用不同证书、日志、拨号策略的连接池。

下面是 MySQL 的完整组装函数;连接地址、凭据、TLS 和驱动 Logger 都从调用方传入,不在框架中约定:

package connection

import (
    "context"
    "crypto/tls"
    "github.com/go-sql-driver/mysql"
    "git.yuntuo.hk/k2safe/go-omni/database"
)

func Open(ctx context.Context, address, user, password, name string,
    tlsConfig *tls.Config, logger mysql.Logger,
    pool database.PoolConfig) (*database.DB, error) {
    cfg := mysql.NewConfig()
    cfg.Net, cfg.Addr = "tcp", address
    cfg.User, cfg.Passwd, cfg.DBName = user, password, name
    cfg.TLS = tlsConfig // 项目设置 RootCAs、ServerName、MinVersion 等
    if logger != nil {
        cfg.Logger = logger // 项目现有的 Print(...any) 日志适配器
    }
    connector, err := mysql.NewConnector(cfg)
    if err != nil {
        return nil, err
    }
    return database.OpenConnector(ctx, connector, pool)
}

调用时以 database.DefaultPoolConfig() 为起点,按项目需要调整。已有 database.Config 可调用 .PoolConfig() 提取池设置;Connector 不需要填写 DSN。PoolConfig 支持通过 config.DecodeFile 加载项目的 JSON/YAML 嵌套配置,环境引用由项目在文件里显式声明。

PoolConfig 字段 默认值 / 含义
MaxOpenConns 10;0 表示不限制
MaxIdleConns 5;0 禁止保留空闲连接
ConnMaxLifetime 30m;0 不因连接年龄回收
ConnMaxIdleTime 5m;0 不因空闲时长回收
PingTimeout 5s;必须为正,约束启动 Ping 和 Health 等待

返回的 *database.DB 嵌入 *sql.DB,可使用 QueryContext、ExecContext、Stats,以及框架的 Health、InTx。需要标准数据库指针的 ORM/迁移器可取 db.DB;不要重复建立池。项目负责驱动支持的 context 取消与读写超时设置,框架不能强制中断不遵守 context 的自定义驱动。

生命周期: nil/带类型的 nil Connector、非法池参数、启动前已取消的 context 会在创建池前返回,Connector 仍归调用方。进入 sql.OpenDB 后由池持有;启动 Ping 失败自动关闭,成功后由调用方负责 db.Close,通常注册 app.OnShutdown(db.Close)。若 Connector 实现 io.Closer,遵循标准 database/sql 规则随池关闭;有资源的 Connector 不应跨多个池共享。资源初始化过程中、注册退出回调之前若业务校验失败,项目也须关闭池。

db.PoolConfig() 返回池配置快照。Connector 入口的 db.Config() 中 Driver / DSN 为空,不能用它重建 Connector。TLS、Logger、驱动版本检查、字符集、时区以及迁移规则保持由项目负责;不写入框架配置快照。

验收脚本使用独立本地 MySQL 8.4 容器(1 CPU / 768 MiB),自动生成测试 CA/证书与随机凭据,检查 TLS 信任/主机名、两池配置隔离、驱动日志、事务和 HTTP 超时,结束后清理本次资源:

python3 scripts/verifyConnector.py --local --report performance/connectorLocal.json
# 发布后通过全新 module cache 验证远程依赖,无本地 replace:
python3 scripts/verifyConnector.py --version v0.9.1-preview --report performance/connectorRemote.json