Go实现ORM及构建查询

 
 

奇技指南

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


01

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)
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)
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 子句,或者应该如何对记录进行排序。


02

用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...)
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...)
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实现这一目标?


03

用于查询构建的第一个类函数

 
 

下面是一个查询构建的例子:

posts := make([]*Post, 0)db := sqlx.Open("postgres", "...")q := Select(    Columns("*"),    Table("posts"),)err := db.Select(&posts, q.Build(), q.Args()...)
db := sqlx.Open("postgres", "...")

q := Select(
Columns("*"),
Table("posts"),
)

err := db.Select(&posts, q.Build(), q.Args()...)

我知道一个简单的例子。但是让我们来看看我们如何实现这样的API,以便它可以用于查询构建。首先,我们应该实现一个查询结构来跟踪查询在构建时的状态。

type statement uint8type Query struct {    stmt  statement    table []string    cols  []string    args  []interface{}}const (    _select statement = iota)
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}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    }}
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{}}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    }}
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()...)
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)    }}
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()...)
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

如果你对这篇文章中所说的有任何异议,或者想进一步讨论这个问题,请留言。



界世的你当不

只做你的肩膀

640?wx_fmt=jpeg 640?wx_fmt=jpeg

 360官方技术公众号 

技术干货|一手资讯|精彩活动

空·

640?wx_fmt=gif

更多精彩内容“阅读原文”

右边给我一朵小花花

640?wx_fmt=gif


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值