红帽白皮书新鲜出炉!点击获取,让你的云战略更胜一筹! 了解详情
写点什么

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

评论

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

「产品经理训练营」第八章作业

Sòrγy_じò ぴé

你有没有领导力?

石云升

领导力 28天写作 职场经验 管理经验 3月日更

产品 0 期 - 第八周作业

曾烧麦

产品训练营

用户路径地图

王一凡

阿里混沌工程平台实践

心远

阿里巴巴 混沌工程

继承

y

继承

携手百度智能云推动实现工业AR空间智能化

百度大脑

AR 百度智能云

设计模式--享元模式

学Java的猪猪侠

一文搞懂 Flink 处理水印全过程

shengjk1

一文搞懂 Flink Stream Join原理

shengjk1

flink源码 flink join

this指向

y

this指针

数据分析

王一凡

关于全球央行数字货币实验的若干认识与思考

CECBC

银行

API 工具链研发的理论基础 - 导读

李宇飞

工具链 API sdk

Vue中如何在线预览pdf文件

y

2021金三银四总结面试必备清单:字节/蚂蚁金服/腾讯/百度

比伯

Java 程序员 架构 互联网 技术宅

Android 系统开发做什么?

吴小龙同学

systemedctl使用指南

happlyfox

3月日更

浅论指针(二)

Integer

c 指针

maven项目下的重复依赖校验

程序员架构进阶

maven 持续集成 28天写作 3月日更 Jar包扫描

操作系统--虚拟存储器概述

学Java的猪猪侠

户口?大厂?高薪?生活?聊聊应届程序员的职业选择

流沙

职业发展

行业首创,百度自主研发下一代区块链操作系统

CECBC

原子操作

结合 Flink 学习装饰者模式

shengjk1

flink源码 flink源码分析

Flink SQL 自定义 Source format

shengjk1

flink sql

云原生AI成过去式,AI原生的云才是通向未来的“藏宝图”

脑极体

基于docker部署jenkins(一)

李日盛

docker jenkins

产品经理第 0 期训练营第九周作业提交

Krystal

作业 - 第八章 数据分析

hao hao

产品经理训练营 Week11 作业

Mai

第八章作业-用户路劲图

z

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