gormx

package module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 16 Imported by: 0

README

gormx

SQL/GORM 全家桶——连接池增强/框架无关 CRUD Service/仓储

🌐 项目主页:https://oekit.github.io/ · 名称由来:Open Easy Kit(开源 · 易用 · 工具集合) 🤖 AI Agent:本仓规范与硬规则见 AGENTS.md;机器可读索引见 llms.txt(接口签名以源码为准)。

安装

go get github.com/oekit/gormx@v0.9.0

这个仓库做什么

gormx 是 SQL/GORM 全家桶:连接池增强、框架无关的 CRUD Service、GORM 仓储实现。 让业务 handler 不直接碰 gorm,也让 Service 可被 HTTP/RPC 双协议复用。

组成

包 import 路径 做什么
gormx(模块根包) github.com/oekit/gormx Open(函数选项连接池)、OpenMySQL、Paginate(单查询分页)、InTransactionContext、FilterBuilder
crud github.com/oekit/gormx/crud 框架无关 CRUD Service:泛型 NewBaseService[T, ID, CreateReq, UpdateReq](ID=实体主键类型,int64/string 文本主键/UUID 均可);repo 实现 TxRepository 时 Update 自动「事务 + FOR UPDATE 行锁」消除丢更新,afterUpdate 钩子失败整体回滚
gormrepo github.com/oekit/gormx/gormrepo NewGormRepository[T, ID](db, opts...) 仓储实现(可选查询装饰器:行级 scope 自动注入,含按 ID 路径)

快速使用

import (
    "github.com/oekit/gormx"
    "github.com/oekit/gormx/crud"
    "github.com/oekit/gormx/gormrepo"
)

db, _ := gormx.Open(dialector, gormx.WithConnectionPool(10, 100, time.Hour))
repo := gormrepo.NewGormRepository[Project, int64](db)
svc := crud.NewBaseService[Project, int64, CreateReq, UpdateReq](repo)

第二个类型参数 ID 是实体主键类型(comparable):int64 自增、string 文本主键(TEXT PRIMARY KEY)、UUID 等均可,v0.6.0 起不再写死 int64。

查询装饰器(多租户 / 行级隔离)

scope 从「每个调用点手拼」上收为仓储层强制——按 ID 的 Get/Update/Delete 同样自动注入(越权最常出在主键路径):

repo := gormrepo.NewGormRepository[Project, int64](db,
    gormrepo.WithQueryDecorator[Project](func(ctx context.Context) func(*gorm.DB) *gorm.DB {
        uid := identity.UID(ctx)
        return func(db *gorm.DB) *gorm.DB { return db.Where("uid = ?", uid) }
    }),
    gormrepo.WithScopeRequired[Project](), // fail-closed:裸查询显式报错
)
  • decorator 返回 nil = 本次不加 scope(admin 直通);开启 WithScopeRequired 时 nil scope 报错,admin 请返回空 scope (func(db *gorm.DB) *gorm.DB { return db })
  • RunInTx 事务仓储 / WithTx 副本继承同一 decorator(「事务 + 行锁读」 不丢 scope)
  • 默认无 decorator 即 passthrough:单租户服务零负担
查询构造红线(FilterBuilder / crud.Query)
  • FilterBuilder.Add 与 crud.Query.WhereRaw 的片段自动包一层括号再进 AND 链(参数绑定不变):片段内部含 OR 不会把过滤击穿成 (... AND a) OR b ——忘写手括号不再是静默漏数据(fail-open)的方向。
  • OR 分组用 crud.Query.WhereGroup / OrGroup 显式声明(A AND (B OR C)); 不分组的 Or() 仍是全局 OR((A AND B) OR C),既有语义不变。
  • 操作符白名单含 FIND_IN_SET(FIND_IN_SET(?, field) > 0,逗号串列成员测试); 白名单外操作符与非法值形状一律 fail-closed 渲染 1=0,绝不静默丢弃条件。
写语义(RowsAffected / found)与错误映射

写方法回传受影响行数(0 = 记录不存在,>0 = 实际改动),found 标志可按行数 构造;BaseService 提供 found 语义——ErrNotFound 只在「确实无行」时返回 (Delete 不再对不存在的记录静默成功),no-op 更新(写步 0 行)默认视为成功, 可切 WithNoOpUpdateAsNotFound()(rows==0 → 404 契约):

// immutable 列保护:只写白名单列,type/email 等列不被全字段覆写写穿
repo.Update(ctx, entity, crud.UpdateOption{Columns: []string{"name", "remark", "status"}})
svc := crud.NewBaseService[Project, int64, CreateReq, UpdateReq](repo,
    crud.WithNoOpUpdateAsNotFound(),                       // no-op → not-found
    crud.WithNotFoundErr(bizerrors.NewBizErrorWithStatus(40401, 404, "not found")), // 消费方码表
    // 或 crud.WithErrMapper(fn) 自定义 not-found / duplicate 映射(含哨兵形态)
).SetUpdateColumns("name", "remark", "status") // Service 层列白名单

内部判定路径不变(errors.Is(err, crud.ErrNotFound) 一直成立),映射只改对外形态。

SQLite 驱动与 cgo

  • 核心包零 sqlite 链接(v0.8.0 起,#72):纯 Go sqlite 入口只住 gormx/sqlitedrv 子包——根包、crud/、gormrepo/ 的生产代码不 import 任何 sqlite 驱动(deps_sqlite_link_test.go 以 AST 判 import 钉住)。 只用 MySQL/CRUD/事务的消费方不再被迫带上 glebarez 及其驱动注册副作用, 也不再付它的构建成本(此前实测冷构建 +5s,modernc.org/libc 体量大)。
  • 想要纯 Go SQLite:显式 import github.com/oekit/gormx/sqlitedrv, 用 sqlitedrv.Open(dsn, opts...)(迁移别名 sqlitedrv.OpenPureGoSQLite 同签名); 已有 *sql.DB 时用 sqlite.Dialector{Conn: db}。 ⚠️ breaking:gormx.OpenPureGoSQLite(根包入口)v0.8.0 起不复存在。
  • 只用 gormx 不会引入 cgo:生产代码不依赖任何 CGO 驱动—— MySQL 走 gorm.io/driver/mysql(纯 Go),SQLite 走 glebarez/sqlite(modernc 系,纯 Go)。 ⇒ 本仓与消费方都能 CGO_ENABLED=0 构建/测试;gorm.io/driver/sqlite (CGO 实现,mattn/go-sqlite3)已不再被本仓任何代码引用。
  • ⚠️ 驱动名冲突,运行时才炸(选用 sqlitedrv 的消费方仍须知): modernc.org/sqlite 与 glebarez/go-sqlite (glebarez dialector 的底层)都在 init() 里注册名为 "sqlite" 的驱动, 同时引入两者启动即 panic(sql: Register called twice for driver sqlite)。 v0.8.0 起本仓不再替消费方做这个注册——只有显式 import sqlitedrv 的二进制 才带上 glebarez 的注册;此时注册者必须唯一,不要再直接 import modernc。 mattn 系(gorm.io/driver/sqlite)注册的是 "sqlite3",与两者不冲突。

边界

矩阵总览(24 活跃模块,自动生成)

模块 内容 矩阵内依赖
pkg 微工具集 24 合 1(errors/bloomx/contextx/container/stringx/slicex/enumx/convert/encoding/jsonrepair/httpjson/mathx/pathx/pagination/ptrx/shellx/timex/cache/bufiox/strmap/singleflighttest/urlguard/cli/pack;v0.10.0 起 uidgen/validator 拆至 kitx);errors(v0.12.0 起)New/As/Is/Unwrap 与 stdlib 同型透传——下游哨兵/As 判定单 import 即可,类型同一性红线可完整执行;cache(v0.4.0 起)含缓存接口 CacheAside + BatchGetter 批量能力(MGet)+ LocalCache/SingleFlightCache + cachetest 契约(Run 真实时间 + RunWithClock 假时钟确定性 TTL 边界)+ Checker;LocalCache 时钟可注入(LocalConfig.Now——TTL/NullTTL 全路径确定性测试,v0.13.0 起);v0.16.0 起 LocalCache 可选形状——NoBackgroundSweep 关闭后台清扫 goroutine(嵌入式/短命进程,公开 Sweep 手动清扫口)、CloneOnRead 读命中返回副本(调用方可改写不污染缓存)、EvictPolicy 可选 LRU(默认 FIFO 不变)、GetEx 区分未命中与值为 nil(新增能力以可选接口 Sweeper/HitGetter 外置,不破坏其它后端);v0.23.0 起补 Peek 非删除式读(可选接口 PeekGetter 外置——读到过期条目只算不命中、不删除它,stale-if-error 兜底需要那份过期真值留在表内;SingleFlightCache 包装 LocalCache 时曾因前置检查走 Get 惰性删除而静默废掉 StaleIfError,#72);container(v0.9.0 起,收编原 collectionx)泛型容器 Set/Ring/Queue/SafeMap + Map(sync.Map 背书)/Slice(并发安全);mathx/jsonrepair(v0.11.0)Unstable 单边向上抖动 Upward / 宽松解析 UnmarshalLenient / 抽对象与抽数组对称原语 ExtractObject·ExtractArray(v0.18.0 起——多段数组与散文含 '{' 的数组文本不再整段作废,取首个口径与对象面一致);timex/stringx(v0.7.0 起)时间段边界 BeginningOf/EndOf 七对(分/时/日/周/月/季/年,周起始显式参数)+ trie 多模式替换器 Replacer(最长匹配)/ NewMask 敏感词遮罩;stringx Masker(v0.16.0 起)正则脱敏——按形态分类打码(形态标签进掩码供审计读)+ 保留首尾各 ≤4 字符定位线索且不可复原(保留位封顶不随长度变化),规则表由调用方完全提供;singleflighttest(v0.8.0 起)singleflight 共享契约测试套件(internal 与 concurrency 双侧引用);urlguard(v0.15.0 起)出站 SSRF 防护原语——CheckURL 字面量校验(私网/loopback/链路本地含云元数据 169.254.169.254/CGNAT/ULA/组播保留、.internal·.local·localhost 域名、scheme 白名单、IPv4-mapped IPv6 归一)+ DialFunc 拨号层预解析校验与实际 RemoteAddr 端点核对闭合 TOCTOU(Happy-Eyeballs 覆盖),构造态豁免 WithAllowPrivate/WithAllowCIDRs/WithAllowDomains,与 httpx WithDialFunc 一行组合(httpx#11);cli(v0.16.0 起)框架无关可单测的子命令分发原语——App/Command + 退出码 0/1/2 写死在包里、usage 按 Summary 自动生成、不读 os.Args 不调 os.Exit(输出流全由调用方给);pack(v0.16.0 起)迁移包容器原语——zip + manifest(格式版本/条数/每文件 SHA-256)+ JSONL 流式读写 + Inspect 体检一次列全问题并区分包损坏与版本不认识 —
concurrency 并发与流控原语(async/gopool/group/singleflight/progress/syncx/ratelimit/retry)+ window 滑动窗口 / breaker 自适应熔断(规范哨兵 ErrCircuitOpen)/ shedding CPU 自适应降载 / batch 攒批执行器(满条数/满字节/窗口去重)/ timewheel 单级时间轮;v0.5.0 起 retry 指数退避+抖动(MaxDelay 封顶/溢出饱和)、loop 周期原语(忙时丢 tick/单轮 panic 收编/FirstDelay 可打断)、async labeled RunSafe(panic 按任务标签上报)、syncx.FanInErr 逐槽错误(panic 槽显式标记)、KeyedLimiter 增强(容量护栏/RetryAfter/速率热更)并接入 pkg/singleflighttest 契约测试;v0.8.0 起 singleflight DoShared 折叠可观测(Result.Waiters)、async panic 统一收编 typed *PanicError(errors.As 可辨、携带栈)+ 实例级 Runner panic 路由、retry DelayHint(服务端 Retry-After 建议优先)+ JitterRatio 比例抖动 + retry/httpx Retryable 可重试性判定;v0.9.0 起 breaker 显式三态状态机 closed/open/half-open——冷却(WithProbeTimeout 缺省 1m)期满首个放行即半开探测、探测成功关闸清窗/失败重开、失联超时自动回收 + AbandonGen 显式放弃,GenBreaker 代绑定(AllowGen/SuccessGen/FailureGen/AbandonGen:代仅在探测放行自增、陈旧代回报整体作废、开放期失败不后移冷却);v0.10.0 起 WithCooldown 独立业务冷却期(缺省回落 probeTimeout)+ WithConsecutiveFailures 连续失败计数触发(与速率型并联,补低 QPS 信号,对齐 Envoy consecutive_5xx);v0.11.0 起 ratelimit Lockout 防爆破锁(fail2ban 形态:滑窗失败计数→阈值锁定拒绝 LockDuration→成功 Reset 清零/到期自解,CheckLocked 返回剩余时长);v0.12.0 起 retry/httpx RetryableWithHeaders(403 + Retry-After/X-RateLimit-Remaining 头感知可重试判定,HeaderSource 零依赖抽象)、syncx FanInLimit 受限并发扇出(固定 worker 防下游过载)+ FanInLimitCtx(v0.14.0,ctx 感知:ctx 已取消未启动项 fn 一次不调、剩余槽快速排空 Canceled、在飞项照常落槽)、ratelimit WithTokenClock 时钟注入;v0.13.0 起 breaker 熔断期收敛到探测级(forcePass 强制探活 + 冷却期满半开探测,其余拒绝——SRE 公式退役为 closed→open 触发器,WithConsecutiveFailures 触发后流量真停)+ State()/State 枚举可观测出口;v0.16.0 起 ratelimit NewKeyedLimiterByFunc[K]——factory func(K) Limiter 按 key 定配额(一个管理器按接口维度分档位,NewKeyedLimiterOf 零破坏随迁;未登记 key 拿 factory 兜底档的坑已在 doc 点名,#31);v0.17.0 起 KeyedLimiter.Create 护栏内原子登记(表满先回收空闲与 Allow 同路、仍满拒绝计 OverCapHits、已存在不覆盖——热更走 Replace;解决 ByFunc factory 库锁内调用致消费方锁序倒置,#33);v0.18.0 起 retry.DoValue 泛型伴生——带返回值的重试(DoValue[T](ctx, cfg, fn func(attempt int) (T, error));Do 改为委托给 DoValue——退避序列/抖动/MaxDelay 封顶/DelayHint 优先/Retryable 门控/ctx 取消只有一份实现,杜绝「同一个仓里两份实现」;失败返回零值与最后一次错误) pkg
logx 统一日志门面(多后端可替换 + caller 归属;Sink 已迁嵌套模块 logx/sink,根包纯 stdlib);WithFilter 脱敏/丢弃包装(FilterKey 命中键替换 ***/FilterFunc 丢整条,子 logger 保持规则)、Slogger/SlogHandler 正向桥(三方 slog 直接消费 logx 生态)、TraceHandler 任意 slog.Handler 自动注入 trace_id、NewAuditLogger 审计留痕(级别锁 Info,不受 LOG_LEVEL 降级)、LevelEnabler 热路径提前判级、ParseLogLevel 返回 (Level, error) + ParseLogLevelOr 配错退默认(v0.7.0);优雅关停正身在 core/lifecycle.Manager(logx/shutdown 仅余 Deprecated 别名);桥接子包 hlog/hertzlog/kitexlog 为嵌套模块按需引入 core
core 应用基建件——governance 治理 schema、config 配置加载(LoadBootstrap/LoadDotEnv/Get* 系、default/env/range/options tag 校验、OverlaySource 多源叠加热更、ApplyEnvOverlay env 优先 12-factor 形态)、transport 协议无关传输上下文(Transporter/Header/Server·Client ctx 存取,仅标准库)、chain 中间件链(Handler/Middleware/Chain/Recovery/Timeout,与 nns-endpoint.Endpoint 同形)、lifecycle 契约 + Manager 优雅关停(自 logx/shutdown 迁入,日志 *slog.Logger——core 不再依赖 logx)、network IP 工具、version;v0.9.0 起 config FieldSet 显式提供集合(yaml 显式 port:0 不再被 default/env 覆盖——LoadBootstrap 默认接线,免全量指针化)+ require tag(必须 yaml/env 提供,缺失列全并附 env 提示)+ v0.9.1 修 yaml:,inline 字段在 FieldSet 下漏标记(inline 叶子显式零值曾被 default 覆盖);v0.9.2 env 卫生改纯静态(仅格式/重名,移除依赖进程环境的拼写冒烟检查——误报 PATH/HOME、漏无人设置的真拼错,core#8),命名一致性走 LintEnvNaming 诊断 / WithEnvNamingLint 可选 fail-closed / ignorederived="true" 豁免;v0.10.0 起 config 严格模式——LoadBootstrap[T](envFlag string, opts ...LoadOption) 传 config.WithStrict() 开启:未知键拒载(不再静默忽略),错误带嵌套路径 + 该层合法键清单 + 近似拼写建议(UnknownKeyError);默认行为不变,严格模式显式开启;v0.11.0 起 config 加载入口按来源拆开——LoadFile(任意路径:CLI -config 路径、容器内固定挂载位)+ Unmarshal(字节源:API 下发 / 模板渲染 / 测试夹具),与 LoadBootstrap(etc/ 约定)共享同一条解码线,因此严格模式诊断面(嵌套路径 / 合法键清单 / 拼写建议)与来源无关;LoadBootstrap 行为与错误文案逐字不变 —
kitx 标识与校验——uidgen(Snowflake 全局单例+Extract 元数据、手写 RFC 4122 v4 UUID 零第三方依赖、RandomHex/RandomString 拒绝采样随机串)+ validator(go-playground/validator v10 接口化门面:struct tag 校验/自定义规则注册/ValidationError Unwrap 为 BizError 400 对接 middleware.Fail)+ hash(v0.2.0 起非密码学哈希助手 Md5Hex/Md5Bytes——缓存键/幂等键/短唯一 code,格式稳定承诺,pkg 2026-09-15 治理移除弱哈希后 kitx 侧收编 pkg#10),自 pkg v0.10.0 拆出 pkg
metrics Prometheus 门面 + 弹性指标族 core
tracing OpenTelemetry 追踪 + trace_id 真相源(v0.3.0 起 OTLP 双协议导出:gRPC 缺省 / HTTP,Config.Protocol + Headers,切换协议不丢传播器/指标/旧 provider 关闭) logx
registry 注册发现核心(provider 无关 SPI:Registrar/Discoverer/Subscriber/ConfigStore + 实例模型 + netutil)+ sdbridge + registrymocks——根仓零第三方依赖;Nacos 实现与 Nacos 配置源已拆成嵌套模块(v0.5.0 起:registry/nacosimpl、registry/nacos——nacos-sdk-go 重依赖隔离在此,实测单独引入即拖 63 个第三方模块,不隔离时只要 SPI 接口的消费方也会被连带拖入) core
data 数据访问门面——复刻 ekit data/ 能力面:cache/rediscache/crud/distlock/gormx/redisx/timertask/cachetest 八子包 re-export(类型同一,实现住 pkg 与各子仓)+ cached 缓存读写编排(写后失效/事务提交后失效/查询缓存版本号失效/GetByIDs 批量点读 MGet 快路径/bloom 防穿透)+ twolevel 两级缓存与跨实例失效广播(v0.7.0 起本仓唯二非 re-export 实现);v0.15.0 起壳跟随源仓演进——cache 壳补 LocalCache 可选形状(EvictPolicy/Sweeper/HitGetter)、cachetest 壳补 5 个契约套件入口(套件 8 → 13 场景)、v0.18.0 起 cache 壳补 PeekGetter 别名 + cachetest 壳补 2 个 stale-if-error 契约入口(套件 13 → 15 场景)、timertask 壳的 Redis 形态随源仓 v0.2.0 迁至嵌套模块 timertask/redis(壳不再转发 RedisBackend/ShardManager,Redis 形态直接依赖该嵌套模块);all/ 聚合模块已废弃 retract concurrency、gormx、logx、pkg、redisx、timertask
gormx SQL/GORM 全家——连接池增强、框架无关 CRUD Service、仓储(原 oekit/gorm,v0.3.0 更名) logx、pkg
redisx Redis 全家——客户端封装(根包)、分布式缓存 cache/、分布式锁 distlock(Locker 同形可互换;Renew 手动续期(false=锁丢失,不进 Locker 接口保 etcdx 互换)+ WithAutoRenew 覆盖 TryLock 自动续期(v0.8.0 feat!:此前组合不续期);Semaphore ZSET 槽位信号量(v0.8.0:Acquire 同 identifier 幂等刷新 + Lua 内原子清过期槽崩溃自愈、Renew 仅未过期槽可续、Cleanup 返条数、无 Redis 降级 fail-fast))、分布式限流 ratelimit(Lua 三态 + 故障本地兜底);v0.9.0 起 distlock Lockout 分布式防爆破锁(Redis 侧 fail2ban:滑窗失败计数→阈值锁定 Penalty→Reset 清零,与 concurrency/ratelimit.Lockout 进程内孪生——多实例共享计数是存在理由;惩罚只按首次触发计算、nil client fail-fast 不降级,#6) concurrency、logx、pkg
etcdx etcd 全家——客户端封装(根包:生产默认值/Health 探活/Close(ctx) 幂等)、registry 注册发现实现(SPI 对接 registry/nacos 链路:Lease KeepAlive 注册 + 丢约指数退避自动重注册、prefix Watch 订阅推送、ConfigStore KV 配置监听)、distlock 分布式锁(Session+concurrency.Mutex 公平排队,接口与 redisx/distlock.Locker 同形可互换,可选无 etcd 降级放行) concurrency、logx、registry
httpx HTTP 客户端 + 负载均衡——服务发现/熔断(规范哨兵 ErrCircuitOpen)/重试/指标/OTel 透传;loadbalance Selector 形态(RoundRobin/Random/Weighted/一致性哈希权重感知虚拟副本/p2c 自适应——EWMA 经 DoneFunc 回写建立、Err 计 1s 惩罚;WithFilter 按元数据过滤;v0.4.0 起 Balancer 全家族删除无兼容路径);codec 注册制编解码器(json 内置 + form 表单,DoCodec 请求按 codec 序列化、响应按 Content-Type 反查回退请求 codec);v0.6.0 起 net/http 桥接层 NewRoundTripper/StdClient——存量标准库外呼一行 Transport 接入熔断/重试/指标/链路(重试缺省关显式开、非 2xx 非错误、hop-by-hop 隔离,#8);v0.8.0 起 SafeDialFunc/WithSafeDial 出站 SSRF 防护胶水(判定单源 pkg/urlguard v0.15.0:解析 IP 全量预检 + 实际 RemoteAddr 端点核对闭合重解析 TOCTOU、Happy-Eyeballs 覆盖,构造态豁免;v0.7.0 内置的独立 httpx/urlguard 实现收敛删除,#11) concurrency、logx、metrics、pkg、registry、tracing
network 轻量网络客户端——WebSocket/NATS/MQ 抽象、连接与流工具 concurrency、logx、tracing
timertask 分片定时任务(FNV-1a、心跳续约);v0.1.5 起可选可靠投递 DeliveryAtLeastOnce(租约 + Ack + 可见性超时;过期租约回扫重投/死信、停机 best-effort 收租,v0.1.7 接入生命周期真正生效);v0.2.0 起 Redis 后端迁至嵌套模块 timertask/redis(根包不再引 go-redis/logx/concurrency,require 仅 pkg——import timertask 与 import timertask/local 的模块依赖面均无 redis,纯 stdlib 嵌入式场景可用)+ 新增 local 子包 cron 调度器 Scheduler(表达式复用 pkg/timex/cron 不另写一份;并发度/CatchUp 补跑/退出语义明确,FakeClock 假时钟驱动、触发点序列可确定性断言) pkg
middleware Hertz 标准中间件栈(模块根 mw,双层目录已收平)+ contrib 桥接 + hmw + SheddingMiddleware 自适应降载 + chainmw 子包(transport 感知框架无关中间件,基于 core/chain+transport:Recovery 带 panic 隔离上报钩子 WithRecoveryReporter、AccessLog skip+redact+自定义字段、MetadataPropagate——kitex 经 kitex/chainmw.ToKitex 复用同一实现,根包 Recovery/AccessLog 已薄壳化复用 chainmw 内核、下游 handler 可经 transport.FromServerContext 读协议无关传输信息);v0.6.0 起 accesslog 凭证路径脱敏(WithAccessLogRedactPathPrefixes)、ratelimit 默认 KeyFunc 去端口(连接桶→地址桶);v0.11.0 起 WithAccessLogSink 落库 sink(AccessRecord{Method,Path,Status,Latency,ClientIP,UserAgent,TraceID} 与日志同源、c 同传可读业务键、error/panic 只记 Warn 不影响主链路、SkipPaths 命中不触发)+ chainmw.WithSink 内核同构(kitex 随 ToKitex 获得)+ StackConfig.DisableResponseWrap 响应自管一键形态(TraceID→Recovery→AccessLog)与 AccessLogOptions 透传 + doc/README「适用边界」段(响应契约统一 vs handler 自管的推荐组合,#18);v0.12.0 起 AccessLog panic 恢复点产出记录(AccessRecord.Panic 标记 + Err 摘要,Hertz status 按 Recovery 缺省 500)后按原值重抛——panic 请求不再零访问记录,sink/kitex 桥接同步留痕,#20;v0.13.0 起 accesslog 内建日志可关闭/改道(WithAccessLogSilent 只采不写、WithAccessLogLogger 注入宿主 logger 避免双写;静默下 skip/redact/sink/panic 记录照常),内核 chainmw.WithSilent 同构(kitex 随 ToKitex 获得),#38 concurrency、core、logx、metrics、pkg、registry、tracing
k8s Kubernetes 客户端封装——RESTConfig/Clientset/DynamicClient 构建(显式 kubeconfig → in-cluster 自动 → KUBECONFIG/~/.kube/config 逐级回退,QPS/Burst 限流默认 50/100)+ leaderelection 子包 Lease 选主简化封装(OnStartedLeading/OnStoppedLeading/OnNewLeader 回调,主备单活,仅要求 Lease CRUD 权限) logx
nns NNS 服务治理客户端——provider 无关发现/配置/实例缓存 concurrency、core、logx、registry
nns-endpoint NNS 端点工厂——per-service 熔断、换实例重试、gRPC 池 httpx、logx、metrics、nns、registry
bootstrap 应用生命周期引导——New(WithServiceName/WithShutdownTimeout/WithBeforeServe) + Run(fn) + healthz;App.Ready/ReadyChan/IsReady 就绪门(幂等并发安全)——嵌套 bootstrap/nacos 自 v0.3.0 起 NNS 注册延迟到 Ready 之后(server 就绪 → userFn 触发 Ready → 才进注册中心)、关停序 nns 反注册先于 server 关闭;就绪门漏接时可观测——nacos 看门狗超时打 ERROR 点明「服务不会注册到注册中心」(WithReadyTimeout 可调)+ NNSComponent.RegistrationState() 暴露注册阶段(pending/registered/failed/skipped/shutdown);Metrics/Tracing 在嵌套模块 bootstrap/obs concurrency、core、logx
hertz Hertz 运行时集成(模块根扁平化)——日志桥接、生命周期、SSE/health/metrics、适配器、OTel、crudh/validatorh(基于 kitx/validator)/versionh(原 handler 微库收编);RunServer 变参就绪门 WithOnReady(全部 HostPort TCP 探活可建连后回调,探活超时按启动失败优雅关机;缺省超时 5s 经 WithReadyProbeTimeout 调整——探活目标是 server 自动取到的全部 HostPort,无探活地址覆盖选项(kitex 侧才有 WithReadyAddr)),配合 bootstrap Ready 实现 NNS 延迟注册;transportx(hertz→core/transport 同构适配器)+ chainmw 桥接 bootstrap、concurrency、core、gormx、kitx、logx、metrics、middleware、nns、pkg、registry、tracing
kitex Kitex 生态——日志桥接、治理热更、生命周期、BizError 桥接、kitexotel、istio(xDS 动态熔断,原 istio 微库收编);RunServer 变参就绪门(WithOnReady/WithReadyAddr/WithReadyProbeTimeout)+ chainmw.ToKitex(core/chain.Middleware → kitex 中间件,与 hertz 共享 middleware/chainmw 实现)+ transportx bootstrap、concurrency、core、logx、metrics、nns、pkg、registry
workflow 组合式流程编排——串行/并行/异步/条件分支 + Hook AOP concurrency、logx
kitex-examples Kitex echo 示例——client/server + IDL + 治理配置 bootstrap、core、kitex、logx、tracing

依赖严格自底向上;微库已合并为 pkg,旧微库仓已删除。

本仓在矩阵中的位置与依赖见上表「矩阵内依赖」列。历史沿革:源自 ekit 单体库 (git.enjoye.top/enjoydream/ekit v0.34.0,冻结维护),2026-10 拆分为独立 module。

License

MIT — 见 LICENSE

Documentation

Overview

Package gormx 提供 GORM 数据库客户端封装。

统一封装:自定义 Logger、连接池配置。 零可观测性副作用:构造不注入任何 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

Constants

This section is empty.

Variables

This section is empty.

Functions

func InTransactionContext

func InTransactionContext(ctx context.Context, db *gorm.DB, fn TXFunc) error

InTransactionContext 在带有 context 的数据库事务中执行指定函数。

func IsBusyErr added in v0.9.0

func IsBusyErr(err error) bool

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

func IsNotFound(err error) bool

IsNotFound 报告 err 是否为 GORM「记录未找到」。

func IsUniqueViolation

func IsUniqueViolation(err error) bool

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

func MapQueryError(op, notFoundMsg string, err error) error

MapQueryError 把仓储层单条查询(Get/GetByKeyword 等)的错误映射为业务错误: 记录未找到 → 404 NewNotFoundError(notFoundMsg);其余 → 500 WrapBizError(op)。 统一「查不到」语义,避免被笼统包成 500(历史上 group/detail、 template/set/detail 等接口把 404 报成 500)。

func Open

func Open(dialector gorm.Dialector, opts ...OpenOption) (*gorm.DB, error)

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。

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 字段。

func (*AuditModel) GetID

func (m *AuditModel) GetID() int64

GetID 返回主键 ID。

func (*AuditModel) SetID

func (m *AuditModel) SetID(id int64)

SetID 设置主键 ID。

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 回调。

func (*AuditPlugin) Name

func (p *AuditPlugin) Name() string

Name 返回插件名称。

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 和时间戳。 嵌入此结构体后,实体自动获得主键和时间戳字段。

func (*BaseModel) GetID

func (m *BaseModel) GetID() int64

GetID 返回主键 ID。

func (*BaseModel) SetID

func (m *BaseModel) SetID(id int64)

SetID 设置主键 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 NewFilterBuilder

func NewFilterBuilder() *FilterBuilder

NewFilterBuilder 创建过滤构建器。

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

func (*FilterBuilder) IsEmpty

func (fb *FilterBuilder) IsEmpty() bool

IsEmpty 是否没有任何条件。

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

func (l *Logger) Error(ctx context.Context, msg string, data ...any)

Error 以 Error 级别记录日志。

func (*Logger) Info

func (l *Logger) Info(ctx context.Context, msg string, data ...any)

Info 以 Info 级别记录日志。

func (*Logger) LogMode

func (l *Logger) LogMode(level gormLogger.LogLevel) gormLogger.Interface

LogMode 返回一个设置了新日志级别的 Logger 副本。

func (*Logger) Trace

func (l *Logger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)

Trace 根据耗时和错误情况,以相应级别记录 SQL 追踪日志。 通过 logx 结构化日志输出,包含调用位置、耗时、行数、SQL 等字段。

func (*Logger) Warn

func (l *Logger) Warn(ctx context.Context, msg string, data ...any)

Warn 以 Warn 级别记录日志。

type Model

type Model interface {
	GetID() int64
	SetID(id int64)
}

Model 是 GORM 模型的最小接口,只需暴露主键。 与 BaseModel/SoftDeleteModel/AuditModel 的主键类型(int64)保持一致。

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 撞唯一约束时重读收敛(最多重试一次); 单例表建议加唯一约束作为最终防线。

func (*SingletonRepo[T]) Save

func (r *SingletonRepo[T]) Save(ctx context.Context, model T) error

Save 保存单例记录。已存在则更新(通过 ID),不存在则创建。 在事务中使用 SELECT ... FOR UPDATE 锁定行。注意:FOR UPDATE 锁不住 "空表并发插入"的幻影(READ COMMITTED 下两个事务都读到 not found 后 各自 Create)——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 而非物理删除。

func (*SoftDeleteModel) GetID

func (m *SoftDeleteModel) GetID() int64

GetID 返回主键 ID。

func (*SoftDeleteModel) SetID

func (m *SoftDeleteModel) SetID(id int64)

SetID 设置主键 ID。

type TXFunc

type TXFunc func(tx *gorm.DB) error

TXFunc 是在事务中执行的函数类型,接收事务内的 *gorm.DB 并返回 error。

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 入口**。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL