Go实现ORM及构建查询

mob604756f04b77 · · 573 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

 平台开发 360云计算 

女主宣言

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

PS:丰富的一线技术、多元化的表现形式,尽在“360云计算”,点关注哦!

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 上的代码。

  • https://github.com/andrewpillar/query



有疑问加站长微信联系(非本文作者)

本文来自:51CTO博客

感谢作者:mob604756f04b77

查看原文:Go实现ORM及构建查询

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

573 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传