写点什么

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:441447

评论

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

亚信安慧AntDB-T:使用Brin索引提升OLAP查询性能以及节省磁盘空间

亚信AntDB数据库

AntDB

14点自动化经验

FunTester

数据分析与决策支持:京东商品详情API的商业价值

技术冰糖葫芦

API Explorer api 货币化 API 接口 API 测试

远程访问内网设备:对比IPsec VPN,SD-WAN异地组网更具优势

贝锐

运维 SD-WAN 远程运维 组网

IoTDB 单机/双活/集群部署的区别和适用场景

Apache IoTDB

阿里巴巴拍立淘API返回值:商品关联推荐与交叉销售

技术冰糖葫芦

API Explorer api 货币化 API 接口 API 测试

【活动预告】研讨会+开源集市,IoTDB “登录” GOTC 2024!

Apache IoTDB

JNPF快速开发平台赋能数字办公方式转变

不在线第一只蜗牛

低代码 数字化转型 数字化办公

全文彩印!人民邮电出版的“24小时学会黑客攻防”,讲的太好了!

我再BUG界嘎嘎乱杀

黑客 网络安全 安全 信息安全 网安

某个国外的真实XSS漏洞利用探寻

我再BUG界嘎嘎乱杀

黑客 网络安全 信息安全 XSS 漏洞

智源未来选择 TDengine Cloud,解锁高效能源管理

TDengine

观测云突变告警,精准预测云原生的系统异常

观测云

云原生 监控告警

畅捷通基于Flink的实时数仓落地实践

Apache Flink

大数据 flink 实时数仓

邀请函 I 松下信息和望繁信科技邀您参加「数智时代下大数据应用的“道”与“术”」闭门会议

望繁信科技

大数据 数字化转型 解决方案 流程挖掘 流程智能

Qwen2-Math 开源 AI 模型发布;阿里云推出首个域名 AI 大模型应用丨 RTE 开发者日报

声网

相聚中国香港,共赢智能未来!华为云邀您共赴 KubeCon China 2024

华为云原生团队

云计算 云原生 KubeCON AI 人工智能

就一次!带你彻底搞懂CSRF攻击与防御

我再BUG界嘎嘎乱杀

黑客 网络安全 信息安全 CSRF 网安

实用指南|在多云环境中部署向量数据库

Zilliz

大数据 向量数据库 LLM 大语言模型 AICG

XIAOJUSURVEY重磅升级,推出图形化逻辑编排能力

XIAOJUSURVEY

开源 规则引擎 可视化编排 图形化编排 问卷逻辑

京东面试:说说CMS工作原理?

王磊

Pinterest:从 Druid 到 StarRocks,实现 6 倍成本效益比提升

StarRocks

Druid Pinterest

有限元CAE分析厂商 热力学仿真CFD分析咨询

极客天地

Kubernetes 监控:观测云与 Prometheus CRD 的集成

可观测技术

Kubernetes

易点天下KreadoAI爆款视频生成功能上新 解锁出海营销新路径

新消费日报

API可观察性对于现代应用程序的最大好处

幂简集成

API API 接口

vue前端自适应布局,一步到位所有自适应

不在线第一只蜗牛

Vue 前端

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