Documentation
¶
Overview ¶
Package gormx 提供 GORM 数据库客户端封装。
统一封装:自定义 Logger、连接池配置、会话级咨询锁(RunWithAdvisoryLock:拿锁→执行→解锁钉在同一条连接上,#79)。 零可观测性副作用:构造不注入任何 tracing 插件。
快速接入 ¶
基本用法:
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
log.Fatal(err)
}
自定义连接池:
db, err := gorm.OpenWithPool(mysql.Open(dsn),
10, // maxIdleConns
100, // maxOpenConns
30*time.Minute, // connMaxLifetime
)
OTel 追踪(装饰器显式接入) ¶
本包不做任何 tracing 注入。需要 SQL Span 时 用 github.com/oekit/tracing/gormotel 显式挂载(无 SDK 时 noop):
db, err := gormx.Open(mysql.Open(dsn))
if err := db.Use(gormotel.NewPlugin(gormotel.WithDBName("mydb"))); err != nil {
return err
}
自定义 Logger ¶
默认使用内置 Logger,支持:
- 慢查询阈值:默认 200ms
- 日志级别:默认 Info
Index ¶
- Variables
- func InTransactionContext(ctx context.Context, db *gorm.DB, fn TXFunc) error
- func IsBusyErr(err error) bool
- func IsNotFound(err error) bool
- func IsUniqueViolation(err error) bool
- func MapQueryError(op, notFoundMsg string, err error) error
- func Open(dialector gorm.Dialector, opts ...OpenOption) (*gorm.DB, error)
- func OpenMySQL(cfg MySQLConfig) (*gorm.DB, error)
- func PaginateScope(req *pagination.PageRequest) func(db *gorm.DB) *gorm.DB
- func RunWithAdvisoryLock(ctx context.Context, db *gorm.DB, key int64, fn func(tx *gorm.DB) error) error
- type AuditModel
- type AuditPlugin
- type BaseModel
- type FilterBuilder
- type IntSoftDelete
- func (d IntSoftDelete) DeleteClauses(f *schema.Field) []gclause.Interface
- func (d IntSoftDelete) QueryClauses(f *schema.Field) []gclause.Interface
- func (d *IntSoftDelete) Scan(value any) error
- func (d IntSoftDelete) UpdateClauses(f *schema.Field) []gclause.Interface
- func (d IntSoftDelete) Value() (any, error)
- type Logger
- func (l *Logger) Error(ctx context.Context, msg string, data ...any)
- func (l *Logger) Info(ctx context.Context, msg string, data ...any)
- func (l *Logger) LogMode(level gormLogger.LogLevel) gormLogger.Interface
- func (l *Logger) Trace(ctx context.Context, begin time.Time, ...)
- func (l *Logger) Warn(ctx context.Context, msg string, data ...any)
- type Model
- type MySQLConfig
- type OpenOption
- type PageResponse
- type SingletonRepo
- type SoftDeleteModel
- type TXFunc
Constants ¶
This section is empty.
Variables ¶
var ErrAdvisoryLockUnsupported = errors.New("gormx: advisory lock not supported by dialect")
ErrAdvisoryLockUnsupported 是当前方言不支持会话级咨询锁的哨兵错误 (errors.Is 可辨)。咨询锁的方言面:postgres=pg_advisory_lock、 mysql=GET_LOCK;sqlite(单写者,无跨连接互斥需求)等不支持方言显式报错, 不静默退化成「无锁执行」——调用方要的是互斥,静默无锁比报错危险得多。
Functions ¶
func InTransactionContext ¶
InTransactionContext 在带有 context 的数据库事务中执行指定函数。
func IsBusyErr ¶ added in v0.9.0
IsBusyErr 判定 err 是否 SQLite busy 类错误(库/表被锁,可退避重试)。 沿 errors 链遍历(gorm 与驱动的包装层透明)。err 为 nil 返回 false。
典型用法(配合 retry 退避,busy 是瞬态锁竞态、重试通常即刻成功):
err := retry.Do(ctx, retry.Config{Attempts: 5, Delay: 50 * time.Millisecond,
Exponential: true, MaxDelay: time.Second}, func(int) error {
return tx.Create(&row).Error
})
// 或门控:if gormx.IsBusyErr(err) { ... 退避后重试 ... }
func IsNotFound ¶ added in v0.6.5
IsNotFound 报告 err 是否为 GORM「记录未找到」。
func IsUniqueViolation ¶
IsUniqueViolation 判断给定 error 是否为唯一键冲突。
覆盖三种驱动的唯一冲突文案(**窄判据**:只匹配各家确切的唯一冲突措辞, 不加裸 "constraint failed" 之类的宽子串,避免把普通约束错误误判成唯一冲突):
- sqlite(glebarez 纯 Go 驱动):"UNIQUE constraint failed: <表>.<列>"
- mysql:"Error 1062: Duplicate entry …"
- pg:"… duplicate key value violates unique constraint …"(含 23505)
注:sqlite 文案既不映射 gorm.ErrDuplicatedKey(未开 TranslateError 时), 也不含 mysql/pg 的措辞 ⇒ 缺这条分支时 sqlite 下的唯一冲突**判不出来** (消费方实证:pigeonbox/core 默认库就是 sqlite,见 oekit/gormx#30)。
sqlite 分支由**纯 Go 驱动**(github.com/glebarez/sqlite)直接验证:2026-10-28 起本模块测试改用该驱动(此前用 CGO 驱动 ⇒ 验证面 ≠ 承诺面,且免 CGO 环境 跑不了本模块测试,见 oekit/gormx#36)。纯 Go 驱动的实际文案是 "constraint failed: UNIQUE constraint failed: <表>.<列> (2067)"——含上述子串。
func MapQueryError ¶ added in v0.6.5
MapQueryError 把仓储层单条查询(Get/GetByKeyword 等)的错误映射为业务错误: 记录未找到 → 404 NewNotFoundError(notFoundMsg);其余 → 500 WrapBizError(op)。 统一「查不到」语义,避免被笼统包成 500(历史上 group/detail、 template/set/detail 等接口把 404 报成 500)。
func Open ¶
Open 使用指定的 dialector 打开数据库连接,自动注入默认日志。
默认行为:
- 注入默认 Logger(Info 级别)
- 零 tracing 注入:需要 SQL Span 经 github.com/oekit/tracing/gormotel 显式挂载
⚠️ 连接池配置是 **opt-in**(#75):不传 WithConnectionPool ⇒ MaxOpenConns=0 (database/sql 缺省,**无上限**)——多副本部署下必然打满 PG max_connections (too many clients)。Open 对「MaxOpenConns 未设上限」的形态打一条 WARN; 生产部署必须显式配置:
db, err := gormx.Open(
mysql.Open(dsn),
gormx.WithConnectionPool(100, 10, time.Hour), // maxOpen 在前(#76)
)
需要 OTel 追踪时(装饰器显式接入,无 SDK 时 noop):
db, err := gormx.Open(mysql.Open(dsn))
if err := db.Use(gormotel.NewPlugin()); err != nil { ... }
func OpenMySQL ¶
func OpenMySQL(cfg MySQLConfig) (*gorm.DB, error)
OpenMySQL 从配置创建 GORM DB 实例(含连接池配置;零 tracing 注入, 需要 SQL Span 经 github.com/oekit/tracing/gormotel 显式挂载)。
用法:
db, err := gormx.OpenMySQL(gormx.MySQLConfig{
Host: cfg.Database.Host, Port: cfg.Database.Port,
User: cfg.Database.User, Password: cfg.Database.Password,
DBName: cfg.Database.DBName,
MaxIdleConns: 10, MaxOpenConns: 100,
})
func PaginateScope ¶
func PaginateScope(req *pagination.PageRequest) func(db *gorm.DB) *gorm.DB
PaginateScope 返回一个 GORM Scope 函数,自动应用分页的 Offset 和 Limit。
func RunWithAdvisoryLock ¶ added in v0.10.0
func RunWithAdvisoryLock(ctx context.Context, db *gorm.DB, key int64, fn func(tx *gorm.DB) error) error
RunWithAdvisoryLock 把 fn 钉在**同一条连接**上执行,前后配对拿/释放会话级 咨询锁([#79](https://github.com/oekit/gormx/issues/79))。
## 为什么必须是库级原语
会话级咨询锁(pg_advisory_lock / MySQL GET_LOCK)的坑在连接池语义:
db.Exec("SELECT pg_advisory_lock(?)") // 从池取连接 X 拿锁
defer db.Exec("SELECT pg_advisory_unlock(?)") // 从池取连接 Y——可能是另一条!
解锁落在另一条连接时是**空操作(不报错)**:锁随连接 X 归还池而泄漏(会话 级锁不随归还释放),第二个副本的阻塞式拿锁要等到持锁连接关闭(池 lifetime 分钟级)= 并发首启挂死。且 database/sql 池的 LIFO 空闲复用让单线程下解锁 **大概率**落在同一条连接——缺陷单进程内近乎不可见、无任何报错,只在多副本 / 并发 churn 下显形,极难排查。正解 = gorm 的 Connection()(底层 *sql.Conn) 把 拿锁→执行→解锁 钉在同一条连接上——每个消费方都会独立踩一遍的坑,库级 单源一处钉死。
## 用法(多副本迁移串行化的标准形态)
if err := gormx.RunWithAdvisoryLock(ctx, db, migrateLockKey, func(tx *gorm.DB) error {
// 互斥区:版本化迁移/幂等初始化——锁内第二副本阻塞等待
return autoMigrate(tx)
}); err != nil {
return err
}
## 语义契约
- 锁是**会话级**:fn 结束(含 panic)必然解锁(defer 落在持锁连接上); fn 拿到的 tx 与拿锁/解锁同一条连接,锁内语句都在该会话执行;
- 拿锁**阻塞**直到获得或 ctx 取消(取消中断阻塞并返回 ctx 错误——未拿到 锁的路径不做解锁);
- 解锁走 context.Background()(收尾路径:调用方 ctx 已取消也要尽力解锁, 否则锁随连接归还而泄漏到连接关闭);解锁失败打 ERROR——此时锁泄漏到 持锁连接关闭(池 lifetime 兜底),调用方告警面应覆盖该条日志;
- 方言:postgres → pg_advisory_lock/unlock(int64 键);mysql → GET_LOCK/RELEASE_LOCK(同键同坑,一并收编,键按十进制串语义隐式转换); 其余方言(含 sqlite)返回 ErrAdvisoryLockUnsupported;
- 键为 int64:PG 咨询锁原生两态键(int64 单键形态),调用方用同一常量 跨副本对齐即可。
Types ¶
type AuditModel ¶
type AuditModel struct {
ID int64 `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
CreatedBy int64 `json:"created_by"`
UpdatedBy int64 `json:"updated_by"`
}
AuditModel 是带审计字段的基础模型。 除了软删除外,还包含 created_by 和 updated_by 字段。
type AuditPlugin ¶
type AuditPlugin struct {
// UserIDFunc 从 context 提取当前用户 ID。
UserIDFunc func(ctx context.Context) int64
}
AuditPlugin 是 GORM 审计插件,自动填充 created_by/updated_by 字段。 实体必须包含 CreatedBy 和 UpdatedBy 字段。
func NewAuditPlugin ¶
func NewAuditPlugin(userIDFunc func(ctx context.Context) int64) *AuditPlugin
NewAuditPlugin 创建审计插件。 userIDFunc 从 context 提取当前用户 ID 的函数。
func (*AuditPlugin) Initialize ¶
func (p *AuditPlugin) Initialize(db *gorm.DB) error
Initialize 初始化插件,注册 GORM 回调。
type BaseModel ¶
type BaseModel struct {
ID int64 `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
BaseModel 是通用基础模型,包含 ID 和时间戳。 嵌入此结构体后,实体自动获得主键和时间戳字段。
type FilterBuilder ¶
type FilterBuilder struct {
// contains filtered or unexported fields
}
FilterBuilder 构建动态 SQL WHERE 条件。 适用于查询参数可选的场景,避免大量 if-else 拼接。
用法:
fb := gormx.NewFilterBuilder() fb.Add(opts.Provider != "", "provider = ?", opts.Provider) fb.Add(opts.Repo != "", "repo ILIKE ?", "%"+opts.Repo+"%") fb.Add(!opts.From.IsZero(), "created_at >= ?", opts.From) db = fb.Apply(db)
func (*FilterBuilder) Add ¶
func (fb *FilterBuilder) Add(ok bool, sql string, args ...any) *FilterBuilder
Add 条件为 true 时追加 WHERE 子句。
片段进入 AND 链时自动包一层括号(参数绑定不变):片段内部含 OR 时不会因 SQL 优先级把整条 AND 链击穿成 (... AND a) OR b —— 未加括号的 OR 右侧会 绕过之前所有 AND 过滤,静默返回不该返回的行(fail-open 漏数据)。 自动括号后忘写手括号不再是错误方向的安全缺口。
func (*FilterBuilder) Apply ¶
func (fb *FilterBuilder) Apply(db *gorm.DB) *gorm.DB
Apply 将构建的条件应用到 GORM 查询链。 每个条件之间用 AND 连接。
func (*FilterBuilder) Clauses ¶
func (fb *FilterBuilder) Clauses() (string, []any)
Clauses 返回合并后的子句字符串和参数(用于 raw SQL 场景)。 每个片段自带括号(见 Add),合并结果形如 "(a) AND (b OR c)"。
type IntSoftDelete ¶ added in v0.10.1
type IntSoftDelete int
IntSoftDelete 是整型逻辑删除标记(deleted TINYINT:0=未删 1=已删)。
与 gorm.DeletedAt(时间戳列)的区别:列类型是整型而非时间戳, 零值 0 表示未删 ⇒ 零值实体不会被误判为已删。
来源:kuboard-go/pkg/db/softdelete.go 的已验证实现上移(matrix 决策 U1)。
const ( // IntSoftDeleteAlive 未删除。 IntSoftDeleteAlive IntSoftDelete = 0 // IntSoftDeleteRemoved 已删除。 IntSoftDeleteRemoved IntSoftDelete = 1 )
func (IntSoftDelete) DeleteClauses ¶ added in v0.10.1
func (d IntSoftDelete) DeleteClauses(f *schema.Field) []gclause.Interface
DeleteClauses 把 DELETE 改写成 UPDATE deleted = 1。
func (IntSoftDelete) QueryClauses ¶ added in v0.10.1
func (d IntSoftDelete) QueryClauses(f *schema.Field) []gclause.Interface
QueryClauses 让所有查询自动带上 `deleted = 0`。
func (*IntSoftDelete) Scan ¶ added in v0.10.1
func (d *IntSoftDelete) Scan(value any) error
Scan 实现 sql.Scanner。
func (IntSoftDelete) UpdateClauses ¶ added in v0.10.1
func (d IntSoftDelete) UpdateClauses(f *schema.Field) []gclause.Interface
UpdateClauses 让更新操作自动带上 `deleted = 0` 条件。
func (IntSoftDelete) Value ¶ added in v0.10.1
func (d IntSoftDelete) Value() (any, error)
Value 实现 driver.Valuer。
type Logger ¶
type Logger struct {
gormLogger.Config
// contains filtered or unexported fields
}
Logger 是基于 logx 的 GORM 日志适配器,实现 gormLogger.Interface 接口。 通过 logx 门面输出,支持 slog 结构化日志和 trace_id 传播。
func NewDefaultLogger ¶
func NewDefaultLogger() *Logger
NewDefaultLogger 创建带有合理默认配置(慢查询阈值 200ms、Info 级别)的 Logger。
func NewLogger ¶
func NewLogger(config gormLogger.Config) *Logger
NewLogger 根据给定的配置创建 Logger 实例。 内部持有 Delegating logger 懒解析默认后端:构造(gormx.Open)先于业务 logx.SetDefault 时,SQL 日志仍能跟随运行期切换的后端,不会固化到构造 时刻的 Default。
func (*Logger) LogMode ¶
func (l *Logger) LogMode(level gormLogger.LogLevel) gormLogger.Interface
LogMode 返回一个设置了新日志级别的 Logger 副本。
type MySQLConfig ¶
type MySQLConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
DBName string `yaml:"dbname"`
MaxIdleConns int `yaml:"max_idle_conns"`
MaxOpenConns int `yaml:"max_open_conns"`
ConnMaxLifeMin int `yaml:"conn_max_life_min"`
}
MySQLConfig 是 MySQL 连接配置,可直接从 YAML 反序列化。
type OpenOption ¶
type OpenOption func(*openConfig)
OpenOption 配置 Open 的行为(函数选项模式)。
func WithConnMaxIdleTime ¶
func WithConnMaxIdleTime(d time.Duration) OpenOption
WithConnMaxIdleTime 配置连接最大空闲时间。 空闲超过此时间的连接会被关闭,防止 stale 连接积累。
func WithConnectionPool ¶
func WithConnectionPool(maxOpenConns, maxIdleConns int, connMaxLifetime time.Duration) OpenOption
WithConnectionPool 配置连接池参数。
⚠️ 参数顺序与 database/sql 习惯对齐(#76):**maxOpen 在前**,同 SetMaxOpenConns / SetMaxIdleConns 的声明次序(db 主资源在先、池缓存次之)。 v0.8.0 及以前是 idle 在前——传反不报错、只会静默把 MaxOpenConns 设成 idle 值 (idle > open 的组合 database/sql 不报错,行为是 idle 永远达不成)。
func WithGormOptions ¶
func WithGormOptions(opts ...gorm.Option) OpenOption
WithGormOptions 追加底层 gorm.Option。
type PageResponse ¶
type PageResponse[T any] = pagination.PageResponse[T]
PageResponse 分页查询的响应结果(pkg/pagination 统一信封的别名)。 字段为 Items(JSON "items"),与 crud、hertz/crudh 的契约统一为 pkg/pagination.PageResponse。
func Paginate ¶
func Paginate[T any](db *gorm.DB, req *pagination.PageRequest, queryFn func(db *gorm.DB) *gorm.DB) (*PageResponse[T], error)
Paginate 执行分页查询,返回指定类型的分页结果。 queryFn 只调用一次(GORM 的 *gorm.DB 是不可变的,每次方法调用返回新实例)。 queryFn 可传 nil(等价于恒等 scope)。
若调用方既未在 db 上 Model() 也未在 queryFn 里 Table()/Model(), 自动用泛型参数 T 补 Model(new(T))——否则 Count 步会报 "Table not set",这是最常见的用法遗漏(13+ 处调用踩过)。 显式 Table() 的自定义行扫描(join/Select 到非表结构体)不受影响。
type SingletonRepo ¶
type SingletonRepo[T Model] struct { // contains filtered or unexported fields }
SingletonRepo 提供单例记录的 get-or-default + upsert 模式。 适用于全局配置、系统设置等只有一条记录的场景。
主键列名通过 GORM schema 解析(不硬编码 "id"),主键列名非 id 的模型 同样可用;解析失败时 Get/Save/GetOrCreate 返回错误。
用法:
type MyConfig struct { gormx.BaseModel; Value string }
// MyConfig 通过嵌入 BaseModel 自动获得 GetID/SetID,满足 Model 接口
repo := gormx.NewSingletonRepo(db, func() *MyConfig { return &MyConfig{Value: "default"} })
cfg, err := repo.Get(ctx)
cfg.Value = "updated"
err = repo.Save(ctx, cfg)
func NewSingletonRepo ¶
func NewSingletonRepo[T Model](db *gorm.DB, defaultVal func() T) *SingletonRepo[T]
NewSingletonRepo 创建单例仓库。 defaultVal 返回当记录不存在时的默认值。
func (*SingletonRepo[T]) Get ¶
func (r *SingletonRepo[T]) Get(ctx context.Context) (T, error)
Get 获取唯一的记录。不存在时返回 defaultVal() 的结果,不返回 error。
func (*SingletonRepo[T]) GetOrCreate ¶
func (r *SingletonRepo[T]) GetOrCreate(ctx context.Context) (T, error)
GetOrCreate 获取记录,不存在时创建默认值并返回。 在事务中使用 SELECT ... FOR UPDATE 锁定行。与 Save 相同:FOR UPDATE 锁不住空表并发插入的幻影,Create 撞唯一约束时重读收敛(最多重试一次); 单例表建议加唯一约束作为最终防线。
type SoftDeleteModel ¶
type SoftDeleteModel struct {
ID int64 `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
}
SoftDeleteModel 是带软删除的基础模型。 Delete 操作会设置 deleted_at 而非物理删除。
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package crud 提供通用 CRUD 的泛型数据层基础设施。
|
Package crud 提供通用 CRUD 的泛型数据层基础设施。 |
|
Package gormrepo 提供 github.com/oekit/gormx/crud 仓储接口的 GORM 实现。
|
Package gormrepo 提供 github.com/oekit/gormx/crud 仓储接口的 GORM 实现。 |
|
Package sqlitedrv 是 gormx 的 **opt-in 纯 Go sqlite 入口**。
|
Package sqlitedrv 是 gormx 的 **opt-in 纯 Go sqlite 入口**。 |