2

动手写ORM框架 - GeeORM第五天 实现钩子(Hooks)

 2 years ago
source link: https://geektutu.com/post/geeorm-day5.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

动手写ORM框架 - GeeORM第五天 实现钩子(Hooks)

源代码/数据集已上传到 Github - 7days-golang

本文是7天用Go从零实现ORM框架GeeORM的第五篇。

  • 通过反射(reflect)获取结构体绑定的钩子(hooks),并调用。
  • 支持增删查改(CRUD)前后调用钩子。代码约50行

1 Hook 机制

Hook,翻译为钩子,其主要思想是提前在可能增加功能的地方埋好(预设)一个钩子,当我们需要重新修改或者增加这个地方的逻辑的时候,把扩展的类或者方法挂载到这个点即可。钩子的应用非常广泛,例如 Github 支持的 travis 持续集成服务,当有 git push 事件发生时,会触发 travis 拉取新的代码进行构建。IDE 中钩子也非常常见,比如,当按下 Ctrl + s 后,自动格式化代码。再比如前端常用的 hot reload 机制,前端代码发生变更时,自动编译打包,通知浏览器自动刷新页面,实现所写即所得。

钩子机制设计的好坏,取决于扩展点选择的是否合适。例如对于持续集成来说,代码如果不发生变更,反复构建是没有意义的,因此钩子应设计在代码可能发生变更的地方,比如 MR、PR 合并前后。

那对于 ORM 框架来说,合适的扩展点在哪里呢?很显然,记录的增删查改前后都是非常合适的。

比如,我们设计一个 Account 类,Account 包含有一个隐私字段 Password,那么每次查询后都需要做脱敏处理,才能继续使用。如果提供了 AfterQuery 的钩子,查询后,自动地将 Password 字段的值脱敏,是不是能省去很多冗余的代码呢?

2 实现钩子

GeeORM 的钩子与结构体绑定,即每个结构体需要实现各自的钩子。hook 相关的代码实现在 session/hooks.go 中。

day5-hooks/session/hooks.go

package session

import (
"geeorm/log"
"reflect"
)

// Hooks constants
const (
BeforeQuery = "BeforeQuery"
AfterQuery = "AfterQuery"
BeforeUpdate = "BeforeUpdate"
AfterUpdate = "AfterUpdate"
BeforeDelete = "BeforeDelete"
AfterDelete = "AfterDelete"
BeforeInsert = "BeforeInsert"
AfterInsert = "AfterInsert"
)

// 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().Modelvalue 即当前会话正在操作的对象,使用 MethodByName 方法反射得到该对象的方法。
  • s *Session 作为入参调用。每一个钩子的入参类型均是 *Session

接下来,将 CallMethod() 方法在 Find、Insert、Update、Delete 方法内部调用即可。例如,Find 方法修改为:

// Find gets all eligible records
func (s *Session) Find(values interface{}) error {
s.CallMethod(BeforeQuery, nil)
// ...
for rows.Next() {
dest := reflect.New(destType).Elem()
// ...
s.CallMethod(AfterQuery, dest.Addr().Interface())
// ...
}
return rows.Close()
}
  • AfterQuery 钩子可以操作每一行记录。

新建 session/hooks.go 文件添加对应的测试用例。

package session

import (
"geeorm/log"
"testing"
)

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

在这个测试用例中,测试了 BeforeInsertAfterQuery 2 个钩子。

  • BeforeInsert 将 account.ID 的值增加 1000
  • AfterQuery 将密码脱敏,显示为 6 个 *

附 推荐阅读


last updated at 2022-04-21


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK