详解go如何使用xorm在执行前改写 SQL

 更新时间:2023年06月08日 10:08:53   作者:LinkinStar  
这篇文章主要为大家介绍了详解go如何使用xorm在执行前改写SQL的实现过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

前言

有时候你需要再 SQL 执行之前对于 SQL 语句进行改写,有可能是修改表名字段名,有可能只是添加注释,这些看起来奇怪的操作其实有时候是为了帮助在数据库之前的 proxy 来实现某些功能,比如最常见的分库分表,读写分离,多租户等等。

使用Hint语法

举个具体的例子:

有些数据库中间件支持在 SQL 语句之前添加注释来实现读写分离

如何在读写模式为可读可写(自动读写分离)的集群地址中使用Hint语法。

使用限制

仅读写模式为可读可写(自动读写分离)的集群地址支持Hint语法,只读模式下的集群地址和主地址均不支持Hint语法。关于集群地址的读写模式信息,请参见集群地址的读写模式

注意事项

Hint的路由优化级别最高,不受一致性级别和事务拆分的约束,使用前请进行评估。

使用方法

支持在SQL语句前加上/*FORCE_MASTER*//*FORCE_SLAVE*/强制指定这条SQL的路由方向。

例如select * from test默认会路由到只读节点,改为/*FORCE_MASTER*/ select * from test就会路由到主节点。

支持在SQL语句前加上/*force_node='<节点ID>'*/强制指定在某节点执行某查询命令。

例如/*force_node='pi-bpxxxxxxxx'*/ show processlist,该show processlist命令只在pi-bpxxxxxxxx节点执行。如果该节点发生故障,则返回报错force hint server node is not found, please check.

支持在SQL语句前加上/*force_proxy_internal*/set force_node = '<节点ID>'强制指定在某节点执行所有查询命令。

例如/*force_proxy_internal*/set force_node = 'pi-bpxxxxxxxx',执行该命令后,后续所有查询命令只发往pi-bpxxxxxxxx节点,如果该节点发生故障,则返回报错set force node 'rr-bpxxxxx' is not found, please check.

说明

  • 若您需要通过MySQL官方命令行执行上述Hint语句,请在命令行中加上-c参数,否则该Hint会被MySQL官方命令行过滤导致Hint失效,具体请参见MySQL官方命令行
  • 通常不建议使用/*force_proxy_internal*/语法,会导致后续所有查询请求都发往该节点,读写分离失效。
  • Hint语句里不要有改变环境变量的语句,例如/*FORCE_SLAVE*/ set names utf8; 等,这类语句可能导致后续的业务出错。

支持在SQL语句前加上/*FORCE_MASTER*//*FORCE_SLAVE*/强制指定这条SQL的路由方向

所以当我们使用 orm 库的时候,就需要有一个类似钩子的东西,能在执行之前想办法将 sql 改写为所需要的样子,这就是今天的需求。

尝试过程

如果你只想知道如何使用,可跳过本段,直接去看最后的实现部分

一开始我做了各种尝试,由于 xorm 本身其实并没有相关文档说明,寻找并尝试了半天,虽然最后实现了,但是路径比较曲折。

尝试 1 ContextHook

最开始我想到的就是肯定是 Hook,不错,如我所料,确实有 Hook,并且里面有执行的 SQL,我非常高兴,然后直接开干。

// 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

// 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,用于分库分表或实现多租户等。

type MyDialect struct {
    dialects.Dialect
}
func (d *MyDialect) Filters() []dialects.Filter {
    return []dialects.Filter{&amp;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 服务来使用。

以上就是详解go如何使用xorm在执行前改写 SQL的详细内容,更多关于go xorm改写SQL的资料请关注脚本之家其它相关文章!

相关文章

  • go语言中的二维切片赋值

    go语言中的二维切片赋值

    这篇文章主要介绍了go语言中的二维切片赋值操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Golang模拟令牌桶进行对访问的限流方式

    Golang模拟令牌桶进行对访问的限流方式

    这篇文章主要介绍了Golang模拟令牌桶进行对访问的限流方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • 一文详解Golang 定时任务库 gron 设计和原理

    一文详解Golang 定时任务库 gron 设计和原理

    这篇文章主要介绍了一文详解Golang 定时任务库 gron 设计和原理,gron是一个比较小巧、灵活的定时任务库,可以执行定时的、周期性的任务。gron提供简洁的、并发安全的接口
    2022-08-08
  • Go语言字符串操作指南:简单易懂的实战技巧

    Go语言字符串操作指南:简单易懂的实战技巧

    本文将介绍Go语言中字符串的实战操作,通过本文的学习,读者将掌握Go语言中字符串的常用操作,为实际开发提供帮助,需要的朋友可以参考下
    2023-10-10
  • 基于Go实现TCP长连接上的请求数控制

    基于Go实现TCP长连接上的请求数控制

    在服务端开启长连接的情况下,四层负载均衡转发请求时,会出现服务端收到的请求qps不均匀的情况或是服务器无法接受到请求,因此需要服务端定期主动断开一些长连接,所以本文给大家介绍了基于Go实现TCP长连接上的请求数控制,需要的朋友可以参考下
    2024-05-05
  • go语言使用RocketMq的示例代码

    go语言使用RocketMq的示例代码

    这篇文章主要介绍了go语言使用RocketMq的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2026-01-01
  • gin使用自定义结构绑定表单数据的示例代码

    gin使用自定义结构绑定表单数据的示例代码

    这篇文章主要介绍了gin使用自定义结构绑定表单数据的示例代码,代码简单易懂,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • Golang中设置全局变量并在其他文件中使用

    Golang中设置全局变量并在其他文件中使用

    全局变量是被整个程序都可见的变量,通常用于存储程序中需要共享的数据,本文就来介绍一下Golang中设置全局变量并在其他文件中使用的方法,感兴趣的可以了解一下
    2024-01-01
  • golang如何利用原始套接字构造UDP包详解

    golang如何利用原始套接字构造UDP包详解

    这篇文章主要给大家介绍了关于golang如何利用原始套接字构造UDP包的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用golang具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2017-10-10
  • 使用Go+GoQuery库实现头条新闻采集

    使用Go+GoQuery库实现头条新闻采集

    在本文中,我们将介绍如何使用Go语言和GoQuery库实现一个简单的爬虫程序,用于抓取头条新闻的网页内容,我们还将使用爬虫代理服务,提高爬虫程序的性能和安全性,我们将使用多线程技术,提高采集效率,最后,我们将展示爬虫程序的运行结果和代码,需要的朋友可以参考下
    2023-10-10

最新评论