Go 实现 ORM 及构建查询

阅读数:60 2019 年 11 月 14 日 18:44

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

评论

发布