写点什么

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

评论

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

android自定义View——仿九宫格解锁,kotlin缺点

android 程序员 移动开发

Android自定义view之模仿登录界面文本输入框(华为云APP)

android 程序员 移动开发

Android自定义View之游戏摇杆键盘实现(一)(1),全网独家首发

android 程序员 移动开发

Android第三方库收藏汇总,移动应用开发框架

android 程序员 移动开发

Android网络请求心路历程(1),2021Android开发现状分析

android 程序员 移动开发

Android网络请求心路历程,面试安卓工程师会问到那些问题

android 程序员 移动开发

【LeetCode】删除链表中的节点Java题解

Albert

算法 LeetCode 11月日更

Android网络优化攻略,简单了解一下?,图文详解

android 程序员 移动开发

Android组件化开发的意义何在?,androidui开发框架

android 程序员 移动开发

Android自动化页面测速在美团的实践,百度、阿里、滴滴、新浪的面试心经总结

android 程序员 移动开发

Android自定义View播放Gif动画,ffmpeg音视频开发实战6下载

android 程序员 移动开发

Android研发大厂面试记:阿里,字节,腾讯android面试题目

android 程序员 移动开发

Android系统架构与系统源码目录,灵魂一问-如何彻底防止APK反编译

android 程序员 移动开发

Android系统启动流程(一)解析init进程启动过程,安卓移动开发基础案例教程

android 程序员 移动开发

Android模拟面试,解锁大厂—,这些面试题你会吗

android 程序员 移动开发

Android知识笔记:记录 2 个 “容易误解(1),【干货】

android 程序员 移动开发

Android程序员的Java后台学习建议,2021最新Android中级面试题目汇总解答

android 程序员 移动开发

重磅!四大行正在大规模内测数字货币App 可凭手机号完成转账

CECBC

Android老司机被打脸!Dialog 对应的 Context 必须是 Activity吗?

android 程序员 移动开发

Android自定义View之游戏摇杆键盘实现(一),android开发计算器界面

android 程序员 移动开发

【Flutter 专题】21 易忽略的【小而巧】的技术点汇总 (二)

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 11月日更

Android自定义控件 _ 高可扩展单选按钮(再也不和产品经理吵架了)

android 程序员 移动开发

Android篇:2019初中级Android开发社招面试解答(中,跨平台app开发框架排名

android 程序员 移动开发

微信朋友圈的高性能复杂度分析

stars

架构训练营

Android热修复基础篇(一),flutter图片压缩

android 程序员 移动开发

Android知识笔记:记录 2 个 “容易误解,网易的朋友给我这份339页的Android面经

android 程序员 移动开发

Android源码-一文带你搞懂OkHttp,kotlin高阶函数

android 程序员 移动开发

Android源码解析——Handler,看完直接跪服

android 程序员 移动开发

Android篇:2019初中级Android开发社招面试解答(上,作为Android开发者

android 程序员 移动开发

Android热修复基础篇(二),android设计模式面试题

android 程序员 移动开发

Android程序员现状:没有架构师的命,却得了架构师的病

android 程序员 移动开发

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