Agentic AI、具身智能、强化学习框架、端侧大模型……来QCon上海站,感受AI的未来! 了解详情
写点什么

Go 实现 ORM 及构建查询

  • 2019-11-14
  • 本文字数:4323 字

    阅读完需:约 14 分钟

Go实现ORM及构建查询

最近,作者一直在研究各种与数据库轻松交互的解决方案。我对数据库的操作主要是使用的 sqlx,它使得将数据库中的数据解组到 structs 非常容易。你可以编写 SQL 查询,使用 db 标记 struct,然后让 sqlx 处理其余的操作。然而,我遇到的主要问题是惯用查询构建。这让我开始研究这个问题,并在本篇文章中写下我的一些想法。

1 GORM,分层复杂性及 ActiveRecord 模式

很多的 Go 开发者,在涉及到数据库操作时,基本上都会使用 gorm 库来处理。当然它是一个功能相当全面的 ORM,支持迁移、关系、事务等等。对于那些使用过 ActiveRecord 或 Eloquent 的开发者来说,GORM 的用法应该是很熟悉的。


作者之前也简单地使用过 GORM,对于简单的基于 CRUD 的应用程序,这是没有问题的。然而,当涉及更多分层复杂性时,我发现它有些不够用。假设我们正在开发一个博客类应用,并且允许用户通过 URL 中的 search 查询字符串搜索文章。如果出现这种情况,我们希望用 WHERE title LIKE 约束查询,否则就实现不了。


posts := make([]Post, 0)
search := r.URL.Query().Get("search")
db := gorm.Open("postgres", "...")
if search != "" { db = db.Where("title LIKE ?", "%" + search + "%")}
db.Find(&posts)
复制代码


没有什么特殊的地方,我们只是检查是否有一个值,并修改对 GORM 本身的调用。但是,如果我们想要允许在某个日期之后搜索文章呢?我们需要添加更多的检查,首先查看 URL 中是否存在 after 查询字符串,如果存在,则相应地修改查询。


posts := make([]Post, 0)
search := r.URL.Query().Get("search")after := r.URL.Query().Get("after")
db := gorm.Open("postgres", "...")
if search != "" { db = db.Where("title LIKE ?", "%" + search + "%")}
if after != "" { db = db.Where("created_at > ?", after)}
db.Find(&posts)
复制代码


因此,我们添加另一个检查来确定是否应该修改调用。到目前为止,这种方法还不错,但事情可能会开始失控。理想情况下,我们想要的是使用一些自定义回调来扩展 GORM,这些回调可以接受 search 和 after 变量而不管它们的值,并将逻辑延迟到定制回调。GORM 确实支持一个插件系统,用于编写自定义回调,但是这似乎更适合在某些操作时修改表状态。


如上所述,我发现 GORM 最大的缺点是实现分层复杂性非常的繁琐。在编写 SQL 查询时,您通常需要这样做。试图确定是否要根据某些用户输入向查询添加 WHERE 子句,或者应该如何对记录进行排序。

2 用 Go 构建符合习惯的查询

标准库中的 database/sql 包非常适合与数据库交互。sqlx 是处理数据返回的一个很好的扩展。然而,这仍然不能完全解决当前的问题。如何以编程的方式有效地构建复杂的查询,这是一个惯用的方法。假设我们对上面的相同查询使用 sqlx,那会是什么样子?


posts := make([]Post, 0)
search := r.URL.Query().Get("search")after := r.URL.Query().Get("after")
db := sqlx.Open("postgres", "...")
query := "SELECT * FROM posts"args := make([]interface{}, 0)
if search != "" { query += " WHERE title LIKE ?" args = append(args, search)}
if after != "" { if search != "" { query += " AND " } else { query += " WHERE " }
query += "created_at > ?"
args = append(args, after)}
err := db.Select(&posts, sqlx.Rebind(query), args...)
复制代码


并不比我们对 GORM 做的好多少,事实上更丑陋。我们将检查 search 是否存在两次,以便为查询准备正确的 SQL 语法,将参数存储在 []interface{} 切片中,并连接到一个字符串。这也是不可扩展或易于维护的。理想情况下,我们希望能够构建查询,并将其交给 sqlx 来处理其余的查询。那么,Go 中的惯用查询构建器会是什么样子?在我看来,它将采用两种形式之一,第一种是利用选项结构,另一种利用一级函数。


让我们来看看 squirrel。这个库提供了构建查询的能力,并以一种作者认为相当惯用的方式直接执行查询。在这里,我们将只关注查询构建方面。


使用 squirrel,我们可以像这样实现上述逻辑。


posts := make([]Post, 0)
search := r.URL.Query().Get("search")after := r.URL.Query().Get("after")
eqs := make([]sq.Eq, 0)
if search != "" { eqs = append(eqs, sq.Like{"title", "%" + search + "%"})}
if after != "" { eqs = append(eqs, sq.Gt{"created_at", after})}
q := sq.Select("*").From("posts")
for _, eq := range eqs { q = q.Where(eq)}
query, args, err := q.ToSql()
if err != nil { return}
err := db.Select(&posts, query, args...)
复制代码


这比 GORM 稍微好一点,比我们之前做的字符串连接好一些。然而,它给人的印象仍然有点冗长。squirrel 对 SQL 查询中的一些子句使用选项结构。可选结构是 Go for api 中常见的模式,其目标是高度可配置。


一个用于在 Go 中构建查询的 API 应该满足这两个需求:


如何用 Go 实现这一目标?


  • 符合语言习惯

  • 可扩展

3 用于查询构建的第一个类函数

下面是一个查询构建的例子:


posts := make([]*Post, 0)
db := sqlx.Open("postgres", "...")
q := Select( Columns("*"), Table("posts"),)
err := db.Select(&posts, q.Build(), q.Args()...)
复制代码


我知道一个简单的例子。但是让我们来看看我们如何实现这样的 API,以便它可以用于查询构建。首先,我们应该实现一个查询结构来跟踪查询在构建时的状态。


type statement uint8
type Query struct { stmt statement table []string cols []string args []interface{}}
const ( _select statement = iota)
复制代码


上面的 struct 将跟踪我们正在构建的语句,无论是 SELECT、UPDATE、INSERT 还是 DELETE,正在操作的表,我们正在使用的列,以及将传递给最终查询的参数。为了简单起见,让我们专注于为查询构建器实现 SELECT 语句。


接下来,我们需要定义一个类型,用于修改正在构建的查询。这种类型将作为第一个类函数被多次传递。每次调用此函数时,如果适用,它应该返回新修改的查询。


type Option func(q Query) Query


现在,我们可以实现构建器的第一部分 Select 函数。这将开始为我们想要构建的 SELECT 语句构建一个查询。


func Select(opts ...Option) Query {    q := Query{        stmt: select_,    }
for _, opt := range opts { q = opt(q) }
return q}
复制代码


现在,应该能够看到所有内容是如何慢慢地结合在一起的,以及 UPDATE、INSERT 和 DELETE 语句是如何实现的。如果没有实际实现一些要传递给 Select 的选项,上面的函数是相当无用的,所以让我们这样做。


func Columns(cols ...string) Option {    return func(q Query) Query {        q.cols = cols
return q }}
func Table(table string) Option { return func(q Query) Query { q.table = table
return q }}
复制代码


如你所见,我们以某种方式实现这些第一类函数,以便它们返回将被调用的基础选项函数。通常期望选项函数修改传递给它的查询,并返回一个副本。


为了使其对构建复杂查询的用例有用,我们应该实现向查询添加 WHERE 子句的功能。这还需要跟踪查询中的各种 WHERE 子句。


type where struct {    col string    op  string    val interface{}}
type Query struct { stmt statement table []string cols []string wheres []where args []interface{}}
复制代码


我们为 WHERE 子句定义了一个自定义类型,并向原始查询结构添加了一个 WHERE 属性。让我们根据需要实现两种类型的 WHERE 子句,第一种是 WHERE LIKE,另一种是 WHERE >。


func WhereLike(col string, val interface{}) Option {    return func(q Query) Query {        w := where{            col: col,            op:  "LIKE",            val: fmt.Sprintf("$%d", len(q.args) + 1),        }
q.wheres = append(q.wheres, w) q.args = append(q.args, val)
return q }}
func WhereGt(col string, val interface{}) Option { return func(q Query) Query { w := where{ col: col, op: ">", val: fmt.Sprintf("$%d", len(q.args) + 1), }
q.wheres = append(q.wheres, w) q.args = append(q.args, val)
return q }}
复制代码


在处理向查询添加 WHERE 子句时,我们为底层 SQL 驱动程序(本例中为 Postgres)适当地处理绑定变量语法,并将实际值本身存储在查询的 args 切片中。


因此,由于我们实现的很少,我们应该能够以惯用的方式实现我们想要的。


posts := make([]Post, 0)
search := r.URL.Query().Get("search")after := r.URL.Query().Get("after")
db := sqlx.Open("postgres", "...")
opts := []Option{ Columns("*"), Table("posts"),}
if search != "" { opts = append(opts, WhereLike("title", "%" + search + "%"))}
if after != "" { opts = append(opts, WhereGt("created_at", after))}
q := Select(opts...)
err := db.Select(&posts, q.Build(), q.Args()...)
复制代码


稍微好一点,但仍然不是很好。然而,我们可以扩展功能来得到我们想要的。因此,让我们实现一些函数,这些函数将返回特定需求的选项。


func Search(col, val string) Option {    return func(q Query) Query {        if val == "" {            return q        }
return WhereLike(col, "%" + val + "%")(q) }}
func After(val string) Option { return func(q Query) Query { if val == "" { return q }
return WhereGt("created_at", val)(q) }}
复制代码


实现了上述两个函数之后,我们现在可以为我们的用例构建一个稍微复杂的查询。如果传递给它们的值被认为是正确的,这两个函数只会修改查询。


posts := make([]Post, 0)
search := r.URL.Query().Get("search")after := r.URL.Query().Get("after")
db := sqlx.Open("postgres", "...")
q := Select( Columns("*"), Table("posts"), Search("title", search), After(after),)
err := db.Select(&posts, q.Build(), q.Args()...)
复制代码

总结

我发现这是在 Go 中构建复杂查询的一种相当惯用的方法。现在,当然你已经在本文中做了这么多,并且一定在想,“这很好,但是你没有实现 Build() 或 Args() 方法”。这确实是。出于不想把这篇文章延长到不必要的时间,就没有继续实现。所以,如果你对这里展示的一些想法感兴趣,看看 GitHub 上的代码


如果你对这篇文章中所说的有任何异议,或者想进一步讨论这个问题,请留言。


本文转载自公众号 360 云计算(ID:hulktalk)。


原文链接:


https://mp.weixin.qq.com/s/XbtSamp7I6HwvRO_OweqJg


2019-11-14 18:441621

评论

发布
暂无评论
发现更多内容

企业级开发平台的演进

世开 Coding

软件开发 低代码 快速开发平台 少代码

一文详解MySQL事务底层原理,全是干货,推荐收藏

一灯架构

Java 10月月更

ES6 Promise、Generator与async简单介绍与应用

木偶

前端 ES6 异步 10月月更

2022年上海前端培训开发技术就业前景如何

小谷哥

MySQL十种锁,一篇文章带你全解析

一灯架构

Java 10月月更

突破单点瓶颈、挑战海量离线任务,Apache Dolphinscheduler在生鲜电商领域的落地实践

白鲸开源

海豚调度 Apache DolphinScheduler 大数据调度 数据调度 企业号十月 PK 榜

“程”风破浪的开发者|OpenHarmony设备开发-开发板介绍

坚果

OpenHarmony 10月月更 “程”风破浪的开发者

细说JavaScript闭包

hellocoder2029

JavaScript

JS词法环境和执行上下文

hellocoder2029

JavaScript

Plan Stitch:一种使用缝合物理计划解决查询计划性能退化问题的方法

KaiwuDB

如何通过C#/VB.NET重命名Excel表格并设置选项卡颜色

在下毛毛雨

C# .net Excel 重命名工作表

js对象和原型、原型链的关系

hellocoder2029

JavaScript

大数据开发学习周期是多久

小谷哥

前端培训学习后的就业方向哪个比较好

小谷哥

“程”风破浪的开发者|Lombok的使用

楠羽

学习方法 lombok “程”风破浪的开发者

软件测试 | 测试开发 | 使用 Zabbix + Grafana 搭建服务器监控系统

测吧(北京)科技有限公司

测试

北京培训java开发的机构比较好

小谷哥

【云舟说直播间】数字可视化专场来袭

云计算

阿里巴巴 Noslate 正式开源 - 面向云原生的 JavaScript 容器方案

阿里巴巴终端技术

JavaScript 开源 云原生 前端 企业号十月PK榜

java线下培训机构哪里比较好

小谷哥

诺亚财富 X Hologres : 统一OLAP分析引擎,全面打造金融数字化分析平台

阿里云大数据AI技术

大数据 数据分析 企业号十月 PK 榜

JS知识点梳理之作用域、作用域链、柯里化、闭包

hellocoder2029

JavaScript

新来个技术总监,仅花2小时,撸出一个多线程永动任务,看完直接跪了,真牛逼!

程序知音

Java 架构 多线程 多线程与高并发 后端技术

UIE Slim满足工业应用场景,解决推理部署耗时问题,提升效能

汀丶人工智能

基于网络安全的Docker逃逸

网络安全学海

黑客 网络安全 信息安全 渗透测试 WEB安全

股票价格跨度

掘金安东尼

算法 10月月更

解决 error: failed to push some refs to 'https://github.com/

timerring

git bug 10月月更

低代码开发平台盘点,让你的开发告别低效

飞算JavaAI开发助手

软件测试 | 测试开发 | 测试人生 | 疫情之下涨薪70%从手工转到测试开发,是种什么样的体验?

测吧(北京)科技有限公司

测试

Spring Boot 应用使用 application.yml 和 application.properties 的区别

汪子熙

Java mvc spring 后端开发 10月月更

如何让研发成员在会议上知无不言?

LigaAI

项目管理 敏捷开发 会议管理 迭代管理 企业号十月PK榜

Go实现ORM及构建查询_文化 & 方法_360云计算_InfoQ精选文章