前言
有时候你需要再 SQL 执行之前对于 SQL 语句进行改写,有可能是修改表名字段名,有可能只是添加注释,这些看起来奇怪的操作其实有时候是为了帮助在数据库之前的 proxy 来实现某些功能,比如最常见的分库分表,读写分离,多租户等等。
举个具体的例子:有些数据库中间件支持在 SQL 语句之前添加注释来实现读写分离 支持在SQL语句前加上
/*FORCE_MASTER*/
或/*FORCE_SLAVE*/
强制指定这条SQL的路由方向
所以当我们使用 orm 库的时候,就需要有一个类似钩子的东西,能在执行之前想办法将 sql 改写为所需要的样子,这就是今天的需求。
尝试过程
如果你只想知道如何使用,可跳过本段,直接去看最后的实现部分
一开始我做了各种尝试,由于 xorm 本身其实并没有相关文档说明,寻找并尝试了半天,虽然最后实现了,但是路径比较曲折。
尝试 1 ContextHook
最开始我想到的就是肯定是 Hook,不错,如我所料,确实有 Hook,并且里面有执行的 SQL,我非常高兴,然后直接开干。
代码语言:javascript复制// Hook represents a hook behaviour
type Hook interface {
BeforeProcess(c *ContextHook) (context.Context, error)
AfterProcess(c *ContextHook) error
}
// ContextHook represents a hook context
type ContextHook struct {
start time.Time
Ctx context.Context
SQL string // log content or SQL
Args []interface{} // if it's a SQL, it's the arguments
Result sql.Result
ExecuteTime time.Duration
Err error // SQL executed error
}
于是我直接实现了一个自定义的 Hook 然后使用 BeforeProcess
方法,在执行 SQL
前,替换了 ContextHook
其中的 SQL
代码非常简单,我就不展示了,然后调试了半天,发现打印的 SQL 已经被改写了,但实际执行却还是原来的 SQL。
为什么?于是我去翻了源码,发现,见鬼,这个 ContextHook
里面的 SQL 仅仅是为了日志打印用的。也就是说,这个 Hook 其实目的很明确,就是为了打印日志和计算 SQL 执行时间用的。
尝试 2 Events
在尝试 Event 之前我其实找了很多曲线救国的方式,但确实实现不了。然后我在文档里面找到了 Events。
比如:BeforeUpdate()
BeforeDelete()
等等。问题是,Event 无法获取到需要执行的 SQL,事件仅能拿到需要执行的条件,而还没有解析成 SQL,所以这个方案也不行
尝试 3 Filter
于是我翻遍了源码,看看源码之前到底有什么操作能帮助我来完成这件事,然后发现了 Filter
代码语言:javascript复制// Filter is an interface to filter SQL
type Filter interface {
Do(sql string) string
}
Filter 原本的作用是帮助 dialect 去过滤一些特殊数据库的特殊 SQL 来帮助 xorm 来适配各种类型的数据库。我发现在 SQL 执行之前,只有它能获取到 SQL 并改写,并且改写后的 SQL 能被执行。但,你从上面的接口也看到了,Filter 除了 SQL,其他什么也没有。于是我其实返回去尝试了很多其他的解法,发现仍然无解,最后去官方仓库提交了 PR,将 context 信息传递了进去,至此,就有了后面的实现。
实现
首先需要自定义 Dialect 和 Filter,因为 go 没有继承,所以使用组合的方式来实现多态,将原来的 dialects.Dialect 定义包装,并重写 Filters 方法用于获取到我们自定义的 Filter。 注意,mysql 默认是没有 Filter 的,其他数据库可能存在 Filter,可能需要将原来的拿过来并在末尾 append 一个自定义的 Filter。 替换 SQL 就很简单了,你只需要按照你的需求,改写 SQL 并返回就可以了。如果你和我一样需要额外的信息,可以从 context 中获取,比如传递用户信息,或者 id,用于分库分表或实现多租户等。
代码语言:javascript复制type MyDialect struct {
dialects.Dialect
}
func (d *MyDialect) Filters() []dialects.Filter {
return []dialects.Filter{&MyFilter{}}
}
type MyFilter struct {
}
func (m *MyFilter) Do(ctx context.Context, sql string) string {
return "/** 获取信息,改写sql **/" sql
}
然后 xorm 只有 NewEngineWithDialectAndDB
方法执行自定义 Dialect
,所以用这个方法创建 Engine。并且使用 OpenDialect 方法将默认原先 xorm 的 mysql 对应的 Dialect 拿出来封装成自己的。
driver := "mysql"
connection := "root:root@tcp(127.0.0.1:3306)/test?charset=utf8mb4"
dialect, err := dialects.OpenDialect(driver, connection)
if err != nil {
panic(err)
}
s := &MyDialect{Dialect: dialect}
dbs, err := core.Open(driver, connection)
if err != nil {
panic(err)
}
engine, err := xorm.NewEngineWithDialectAndDB(driver, connection, s, dbs)
if err != nil {
panic(err)
}
总结
其实总的实现并不难,但过程还是异常艰辛,不过好在后面的路都很顺畅了,有了 SQL 你就可以解析它,比如解析需要操作的表名和操作语句,查询走 A,插入走 B 等等。最后我码住一些 Golang 的 MySQL proxy,或许你也需要。PS:目前我没有使用以下的库,仅仅是将抽离了下面的几个库里面的协议部分,伪造了 MySQL 服务来使用。
- https://github.com/vitessio/vitess
- https://github.com/XiaoMi/Gaea
- https://github.com/flike/kingshard