尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GeeORM 第五天:为 ORM 框架实现 Hook 钩子机制(BeforeInsert / AfterQuery / BeforeUpdate / AfterDelete 等 8 个扩展点)

GeeORM 第五天:为 ORM 框架实现 Hook 钩子机制(BeforeInsert / AfterQuery / BeforeUpdate / AfterDelete 等 8 个扩展点) GeeORM 第五天为 ORM 框架实现 Hook 钩子机制BeforeInsert / AfterQuery / BeforeUpdate / AfterDelete 等 8 个扩展点【免费下载链接】7days-golang7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7天用Go动手写/从零实现系列项目地址: https://gitcode.com/gh_mirrors/7d/7days-golang本文是 7 天用 Go 从零实现 ORM 框架 GeeORM 系列的第五篇讲解如何在记录增删查改CRUD前后埋设扩展点通过 Go 反射reflect动态获取并调用结构体绑定的钩子方法并以不到 50 行代码为框架注入「查询后密码脱敏」「插入前主键自增」等横切能力。读完本文你将掌握 Hook 机制的通用设计思想、GeeORM 中 8 个钩子常量的语义与调用约定以及如何基于 day5-hooks 目录的源码与测试用例验证钩子行为。1 Hook 机制在正确的位置埋下扩展点Hook翻译为钩子核心思想是提前在可能增加功能的地方埋好一个扩展点当需要修改或增强该处逻辑时把扩展的类或方法挂载到这个点上即可而无需改动原有代码。钩子的应用在工程实践中非常普遍持续集成GitHub 与 Travis CI 集成后一旦发生git push事件就会触发 Travis 拉取最新代码执行构建IDE 插件按下Ctrl S保存时自动触发代码格式化前端热更新代码文件变更后自动编译打包并通知浏览器刷新页面实现所写即所得。一个 Hook 机制设计得好不好取决于扩展点选得是否合适。例如对于持续集成而言代码没有变更时反复构建毫无意义因此钩子应设计在代码可能发生变更的位置如 MR/PR 合并前后。同理对于 ORM 框架最合适的扩展点显然是记录的增删查改前后操作前置钩子后置钩子典型用途查询FindBeforeQueryAfterQuery查询后对敏感字段脱敏、组装业务字段插入InsertBeforeInsertAfterInsert插入前生成主键、填充创建时间更新UpdateBeforeUpdateAfterUpdate更新前校验字段、自动刷新UpdatedAt删除DeleteBeforeDeleteAfterDelete删除前软删除标记、级联清理以密码脱敏为例假设Account结构体包含隐私字段Password那么每次查询后都需要对结果做脱敏处理才能继续使用。如果提供了AfterQuery钩子查询后自动将Password置为******就能省去调用方大量重复代码——这正是本文实现的动机。2 GeeORM 钩子 API8 个常量与统一调用约定GeeORM 的钩子与结构体绑定即每个结构体需要自行实现各自的钩子方法。钩子相关代码全部集中在 hooks.go 中首先定义 8 个钩子常量// Hooks constants const ( BeforeQuery BeforeQuery AfterQuery AfterQuery BeforeUpdate BeforeUpdate AfterUpdate AfterUpdate BeforeDelete BeforeDelete AfterDelete AfterDelete BeforeInsert BeforeInsert AfterInsert AfterInsert )这 8 个常量同时充当两层身份一是钩子名称的字符串标识二是约定结构体上需要实现的方法名。即用户若想为Account注册查询后钩子只需实现方法func (account *Account) AfterQuery(s *Session) errorGeeORM 会在查询流程中自动调用。钩子签名约定每个钩子的入参均为*Session当前会话这样钩子内部可以继续访问数据库执行额外操作如级联更新返回值为error非 nil 时 GeeORM 会通过日志输出错误见下文CallMethod中对返回值的第一项进行类型断言。3 核心实现基于反射的 CallMethod钩子机制与 GeeORM 整体的对象 ↔ 表结构映射一样都是靠反射实现的。核心调用入口是 hooks.go 中的CallMethod// CallMethod calls the registered hooks func (s *Session) CallMethod(method string, value interface{}) { fm : reflect.ValueOf(s.RefTable().Model).MethodByName(method) if value ! nil { fm reflect.ValueOf(value).MethodByName(method) } param : []reflect.Value{reflect.ValueOf(s)} if fm.IsValid() { if v : fm.Call(param); len(v) 0 { if err, ok : v[0].Interface().(error); ok { log.Error(err) } } } return }逐行拆解其工作原理获取当前操作对象s.RefTable()返回当前会话的表结构描述对象*schema.Schema见 schema.gos.RefTable().Model即此前通过Model(Account{})绑定的模型指针反射定位方法reflect.ValueOf(...).MethodByName(method)通过方法名字符串如AfterQuery在对象上查找对应方法。若方法不存在返回的Value是无效值IsValid()为 falseCall不会执行因此未实现钩子的结构体完全不受影响——这是钩子机制优雅退化的关键对象优先级当value ! nil时如Find中逐行扫描出的记录指针改用value反射其方法从而让AfterQuery能操作每一行具体的记录构造入参并调用param : []reflect.Value{reflect.ValueOf(s)}将*Session包装为反射参数随后fm.Call(param)完成动态调用错误处理若方法返回了值则断言第一项是否为error接口类型是则通过 log 输出错误日志log.Error(err)。注意这里只记录日志、不中断流程属于设计取舍。从源码结构可以推断钩子的触发条件非常宽松——结构体实现了对应方法就调用没实现就静默跳过调用方无需关心目标类型是否注册过钩子。4 接入 CRUD在 Insert / Find / Update / Delete 内部埋点CallMethod本身只是扳机真正让钩子生效的是把它嵌入到四个 CRUD 方法中。这部分改动全部发生在 record.go。4.1 Insert插入前改值插入后收尾func (s *Session) Insert(values ...interface{}) (int64, error) { recordValues : make([]interface{}, 0) for _, value : range values { s.CallMethod(BeforeInsert, value) table : s.Model(value).RefTable() s.clause.Set(clause.INSERT, table.Name, table.FieldNames) recordValues append(recordValues, table.RecordValues(value)) } // ... 构建 INSERT 语句并执行 s.CallMethod(AfterInsert, nil) return result.RowsAffected() }注意Insert支持一次插入多条记录可变参数values ...interface{}因此BeforeInsert在每条记录上都会触发一次且传入的是该条记录的指针钩子可以修改它的字段值例如测试用例中的account.ID 1000而AfterInsert在整个插入完成后触发一次传入nil。4.2 Find查询前后分别埋点AfterQuery 逐行生效func (s *Session) Find(values interface{}) error { s.CallMethod(BeforeQuery, nil) // ... 构建 SELECT 语句并执行 for rows.Next() { dest : reflect.New(destType).Elem() // ... 将每行数据 Scan 进 dest s.CallMethod(AfterQuery, dest.Addr().Interface()) destSlice.Set(reflect.Append(destSlice, dest)) } return rows.Close() }与BeforeQuery不同AfterQuery在rows.Next()循环体内、对每一行扫描完成的记录指针调用一次对应源码 record.go因此它天然支持逐行脱敏、逐行填充衍生字段等场景。First方法内部正是复用Finds.Limit(1).Find(...)来获取首行记录所以AfterQuery钩子对First同样生效。4.3 Update 与 Delete前置校验、后置清理func (s *Session) Update(kv ...interface{}) (int64, error) { s.CallMethod(BeforeUpdate, nil) // ... 构建 UPDATE 语句并执行 s.CallMethod(AfterUpdate, nil) return result.RowsAffected() } func (s *Session) Delete() (int64, error) { s.CallMethod(BeforeDelete, nil) // ... 构建 DELETE 语句并执行 s.CallMethod(AfterDelete, nil) return result.RowsAffected() }Update与Delete均以前置钩子 — 执行 SQL — 后置钩子的对称结构埋点。需要说明的是record.go 中这两个方法的前置钩子均传入nil即作用于当前Session绑定的模型对象s.RefTable().Model而非具体记录——这意味着更新/删除钩子更适合做基于*Session的全局性处理如记录操作日志若要针对单条记录做精细控制可从CallMethod的参数扩展入手。至此全部 8 个钩子都已在框架内接入整个钩子特性仅新增约 50 行代码含常量定义与CallMethod实现延续了 GeeORM 每天少量代码 完整单测的搭积木式演进风格。5 测试验证BeforeInsert 自增主键 AfterQuery 密码脱敏GeeORM 每个特性都配有可独立运行的单元测试。钩子的测试用例位于 hooks_test.go核心代码如下type Account struct { ID int geeorm:PRIMARY KEY Password string } func (account *Account) BeforeInsert(s *Session) error { log.Info(before inert, account) account.ID 1000 return nil } func (account *Account) AfterQuery(s *Session) error { log.Info(after query, account) account.Password ****** return nil } func TestSession_CallMethod(t *testing.T) { s : NewSession().Model(Account{}) _ s.DropTable() _ s.CreateTable() _, _ s.Insert(Account{1, 123456}, Account{2, qwerty}) u : Account{} err : s.First(u) if err ! nil || u.ID ! 1001 || u.Password ! ****** { t.Fatal(Failed to call hooks after query, got, u) } }该用例覆盖了两个钩子的完整闭环BeforeInsert修改入库数据插入两条记录{1, 123456}与{2, qwerty}钩子将每条记录的ID加 1000实际入库主键变为 1001 和 1002AfterQuery实现脱敏通过First查询出第一条记录AfterQuery钩子把Password置为 6 个*断言校验u.ID 1001 u.Password ******同时验证了插入前改值生效与查询后脱敏生效两条链路。测试执行前置条件测试使用NewSession()见 raw.go与 SQLite 内存数据库驱动DropTable/CreateTable见 table.go保证测试环境干净可重复。运行测试在仓库根目录下执行cd gee-orm/day5-hooks go test ./... -run TestSession_CallMethod -v该模块的go.mod依赖github.com/mattn/go-sqlite3需保证 Go 环境与 CGO 可用。运行日志中会依次输出before inert与after query两条钩子日志直观展示触发时序。6 小结钩子为 GeeORM 注入的可扩展性通过第五天的实现GeeORM 获得了与 gorm、xorm 等主流框架对齐的钩子能力零侵入扩展结构体按约定实现BeforeXxx/AfterXxx方法即自动生效不实现则静默跳过统一反射调用CallMethod一处实现四处复用Insert / Find / Update / Delete新增钩子类型只需扩展常量表逐行与全局两级粒度AfterQuery/BeforeInsert可精确到单条记录传入记录指针其余钩子作用于整个会话。这也是系列后续功能的地基之一第六天的事务Transaction会在会话上引入Begin/Commit/Rollback第七天的数据库迁移Migrate则会借助反射自动比对并变更表结构——钩子机制所依赖的反射与 Session 抽象将在其中继续发挥核心作用。【免费下载链接】7days-golang7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7天用Go动手写/从零实现系列项目地址: https://gitcode.com/gh_mirrors/7d/7days-golang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表