HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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

评论

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

AI大模型加速RPAxAI时代到来,谁会是RPA领域的杀手级应用?

王吉伟频道

RPA AI大模型 ChatGPT RPAxAI 企业级RPA

什么叫函数的注册,用c++举个例子

linux大本营

c++ 函数

SpringBoot之Tomcat与Undertow容器性能对比 | 超级详细,建议收藏

bug菌

tomcat 三周年连更 Undertow

C++grpc 服务器接收到请求后如何处理

linux大本营

gRPC 序列化 protobuf C++

Spring中事务嵌套这么用一定得注意了!!

JAVA旭阳

Java spring

算法题每日一练:组合总和 Ⅳ

知心宝贝

数据结构 算法 前端 后端 三周年连更

Qz学算法-数据结构篇(链表、栈)

浅辄

数据结构 链表 三周年连更

【Python实战】XPath采集数据

BROKEN

三周年连更

火山引擎云原生数据仓库ByteHouse技术白皮书V1.0 (Ⅴ)

字节跳动数据平台

数据仓库 云原生 白皮书 企业号 4 月 PK 榜

Unity 之 音频类型和编码格式介绍

陈言必行

三周年连更

设计模式之原型模式和建造者模式

共饮一杯无

设计模式 建造者模式 三周年连更

leaflet加载聚合

linux大本营

delphi中TServerSocker使用stThreadBlocking模式的例子

linux大本营

Discourse 服务器上手动升级

HoneyMoose

跨平台应用开发进阶(五十三):uni-app 通过webview方式嵌套H5实现图片点击下载

No Silver Bullet

uni-app 跨平台应用开发 三周年连更 web-view

ChatGPT安全受质疑 网信办发布生成式人工智能服务管理办法意见稿

郑州埃文科技

ES开发指南|如何快速上手ElasticSearch

浅羽技术

全文检索 搜索 Lucence Elastic Search 三周年连更

@PathVariable 和 @RequestParam 的区别

linux大本营

Django笔记十三之select_for_update等选择和更新等相关操作

Hunter熊

Python django select_for_update bulk_create update_or_create

强大的音频分析编辑工具:Amadeus Pro 汉化激活版

真大的脸盆

Mac Mac 软件 音频编辑 音频处理工具 编辑音频

创新引领・数创未来 | 数据流通与治理专题论坛交流会顺利召开

郑州埃文科技

领先企业的数智化进入2.0阶段,需要升级数智底座

用友BIP

缓解过拟合方法

linux大本营

winDbg 提示 Unable to verify checksum for IOCommModel.exe

linux大本营

网络安全 数字证书

Gradle工程适配为Hvigor工程

坚果

OpenHarmony 三周年连更

Go 语言中的 Slice 陷阱:如何避免常见错误

陈明勇

Go golang 切片 三周年连更 切片陷阱

基于Java+Dubbo设计的智能公交查询系统

DS小龙哥

三周年连更

【Linux】iptables之防火墙概述及规则匹配+实例(1)

A-刘晨阳

Linux iptables 防火墙规则 三周年连更

基于Ubuntu安装Kubernetes集群指南

王玉川

Kubernetes 云原生 k8s 安装 集群

面试必考: 手撕代码系列(一)

Immerse

JavaScript 手写代码 前端面试 手撕代码 超全前端面试题

uniapp配置基本的tabbar和动态修改内容

格斗家不爱在外太空沉思

uni-app 三周年连更

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