从 SQL 生成 CRUD Golang 代码 | 比较 db/sql、gorm、sqlx、sqlc

2025-06-07

从 SQL 生成 CRUD Golang 代码 | 比较 db/sql、gorm、sqlx、sqlc

欢迎回到后端大师班!

在上一讲中,我们学习了如何编写迁移脚本来为我们的简单银行项目创建数据库模式。

今天我们来学习如何编写golang代码对数据库进行CRUD操作。

以下是:

什么是 CRUD?

粗俗

嗯,它们是 4 个基本操作:

  • C代表Create,或向数据库插入新记录。
  • R代表Read从数据库中检索记录。
  • UUpdate,改变数据库中记录的内容
  • 并且DDelete,从数据库中删除记录。

使用哪个库?

golang中有多种方法可以实现CRUD操作。

图书馆

标准database/sql套餐

第一个是使用低级标准库数据库/sql包。

数据库/SQL

正如您在本例中看到的,我们只是使用QueryRowContext()函数,传入原始 SQL 查询和一些参数。然后将结果扫描到目标变量中。

这种方法的主要优点是运行速度非常快,而且编写代码非常简单。

然而,它的缺点是我们必须手动将 SQL 字段映射到变量,这非常繁琐且容易出错。如果变量的顺序不一致,或者我们忘记将某些参数传递给函数调用,错误只会在运行时显示。

戈姆

另一种方法是使用Gorm,它是 Golang 的高级对象关系映射库。

使用起来非常方便,因为所有 CRUD 操作都已实现。因此我们的生产代码会非常简短,因为我们只需要声明模型并调用 Gorm 提供的函数。

戈姆-克里德

正如您在这些示例代码中看到的,我们有用于创建记录NewRecord()和函数。以及几个用于检索数据Create()函数,例如First(),,,,Take()Last()Find()

看起来很酷,但问题是:我们必须学习如何使用 Gorm 提供的函数来编写查询。如果我们不知道该用哪些函数,那就太烦人了。

特别是当我们有一些需要连接表的复杂查询时,我们必须学习如何声明关联标签以使 gorm 理解表之间的关系,以便它可以生成正确的 SQL 查询。

gorm协会

对我来说,我更喜欢自己编写 SQL 查询。它更灵活,而且我可以完全控制数据库的操作。

使用 Gorm 的一个主要问题是,当流量很大时,它的运行速度非常慢。网上有一些基准测试,显示 Gorm 的运行速度比标准库慢 3-5 倍。

sqlx

正因为如此,许多人都转向了中间方法,即使用sqlx库。

它的运行速度几乎与标准库一样快,而且非常易于使用。字段映射可以通过查询文本或结构体标签完成。

sqlx

它提供了一些类似Select()或 的函数StructScan(),这些函数会自动将结果扫描到结构体字段中,这样我们就不必像在database/sql包中那样手动进行映射。这有助于缩短代码,并减少潜在的错误。

但是,我们需要编写的代码仍然很长。而且查询中的任何错误都只能在运行时捕获。

那么有没有更好的方法呢?

sqlc

答案是sqlc

它运行速度非常快,就像 一样database/sql。使用起来非常简单。最令人兴奋的是,我们只需要编写 SQL 查询,Golang 的 CRUD 代码就会自动生成。

sqlc

正如您在本例中看到的,我们只需将数据库模式和 SQL 查询传递给sqlc。每个查询顶部都有 1 个注释,以告诉 sqlc 生成正确的函数签名。

然后sqlc将生成使用标准database/sql库的惯用 Golang 代码。

sqlc 生成的

而且由于它sqlc能够解析 SQL 查询,理解其功能,从而为我们生成代码,所以任何错误都会被捕获并立即报告。听起来很神奇,对吧?

我发现的唯一问题sqlc是,目前它只完全支持 Postgres。MySQL 仍处于实验阶段。所以,如果你在项目中使用 Postgres,我认为sqlc它是正确的选择。否则,我建议你继续使用sqlx

安装 Sqlc

好的,现在我将向您展示如何安装和使用sqlc它来为我们的简单银行项目生成 CRUD 代码。

首先我们打开它的github页面,然后搜索“安装”。

我使用的是 Mac,所以会使用 Homebrew。我们复制下面这段 brew install 命令,并在终端中运行它:



brew install kyleconroy/sqlc/sqlc


Enter fullscreen mode Exit fullscreen mode

好的,sqlc现在已安装!

我们可以运行sqlc version看看它运行的是什么版本。我这里是 1.4.0 版本。

让我们来sqlc help学习如何使用它。

sqlc 帮助

  • 首先我们用编译命令来检查SQL语法和类型错误。
  • 然后最重要的命令是generate。它将为我们进行错误检查并从SQL查询生成golang代码。
  • 我们还有 init 命令来创建一个空的 slqc.yaml 设置文件。

编写设置文件

现在我要进入我们在之前的课程中讨论过的简单银行项目文件夹。运行:



sqlc init


Enter fullscreen mode Exit fullscreen mode

然后用 Visual Studio Code 打开它。我们可以看到sqlc.yaml文件。目前,它还是空的。所以,让我们回到 sqlc 的 GitHub 页面,选择带有标签 的分支v1.4.0,然后搜索“settings”。

sqlc 设置

让我们复制设置列表并将其粘贴到sqlc.yaml文件中。

我们可以让 sqlc 生成多个 Go 包。不过为了简单起见,我现在只使用一个包。



version: "1"
packages:
  - name: "db"
    path: "./db/sqlc"
    queries: "./db/query/"
    schema: "./db/migration/"
    engine: "postgresql"
    emit_json_tags: true
    emit_prepared_queries: false
    emit_interface: false
    emit_exact_table_names: false


Enter fullscreen mode Exit fullscreen mode
  • 这里的选项name是告诉 sqlc 将要生成的 Go 包的名称。我认为db这是一个不错的包名。
  • 接下来,我们必须指定用于path存储生成的 golang 代码文件的文件夹。我将sqlc在该db文件夹中创建一个新文件夹,并将此path字符串更改为./db/sqlc
  • 然后,我们可以queries选择告诉 sqlc 在哪里查找 SQL 查询文件。让我们query在该db文件夹内创建一个新文件夹。然后将此值更改为./db/query
  • 同样,此 schema 选项应指向包含数据库架构或迁移文件的文件夹。在我们的例子中,它是./db/migration
  • 下一个选项是engine告诉 sqlc 我们要使用哪个数据库引擎。我们Postgresql在这个简单的银行项目中使用了它。如果您想尝试使用 MySQL,可以将此值更改为mysql
  • 这里我们将其设置emit_json_tagstrue,因为我们希望 sqlc 将 JSON 标签添加到生成的结构中。
  • 告诉emit_prepared_queriessqlc 生成适用于预处理语句的代码。目前,我们不需要优化性能,因此我们将其设置为false以简化操作。
  • 然后是emit_interface告诉 sqlcQuerier为生成的包生成接口的选项。如果我们以后想模拟数据库来测试更高级的函数,这个选项可能会有用。现在我们先把它设置为false
  • 最后一个选项是emit_exact_table_names。默认情况下,此值为false。Sqlc 将尝试将表名单数化以用作模型结构体名称。例如,accountstable 将变为Accountstruct。如果将此选项设置为 true,则结构体名称将Accounts改为 。我认为单数名称更好,因为一个Accounts复数类型的对象可能会被混淆为多个对象。

运行 sqlc 生成命令

好的,现在让我们打开终端并运行



sqlc generate


Enter fullscreen mode Exit fullscreen mode

我们遇到错误,因为查询文件夹中还没有查询。

SQL 生成错误

我们稍后会编写查询。现在,让我们sqlc向 中添加一个新命令Makefile。它将帮助我们的团队成员在一个地方轻松找到所有可用于开发的命令。



...

sqlc:
    sqlc generate

.PHONY: postgres createdb dropdb migrateup migratedown sqlc


Enter fullscreen mode Exit fullscreen mode

CREATE 操作

现在让我们向CREATE帐户写入第一个 SQL 查询。我将在文件夹account.sql中创建一个新文件db/query

然后返回 sqlc github 页面并搜索getting started

入门

编写 SQL 查询来创建帐户

这里我们看了一些 SQL 查询的示例。让我们复制CreateAuthor命令并将其粘贴到我们的account.sql文件中。

这只是一个基本INSERT查询。唯一特别的是它上面的注释。这条注释会指示 sqlc 如何为该查询生成 Golang 函数签名。

在我们的例子中,函数的名称将是CreateAccount。它应该返回 1 个单个Account对象,所以我们:one在这里有标签。



-- name: CreateAccount :one
INSERT INTO accounts (
  owner,
  balance,
  currency
) VALUES (
  $1, $2, $3
) RETURNING *;


Enter fullscreen mode Exit fullscreen mode

我们不需要提供,id因为它是一个自动递增列。每次插入新记录时,数据库都会自动增加帐户 ID 序列号,并将其用作该列的值id

created_at列还将自动填充默认值,即记录的创建时间。

因此,我们只需要为ownerbalance和提供值currency。共有 3 列,所以我们需要将 3 个参数传递给VALUES子句。

最后,该RETURNING *子句用于告诉 Postgres 在将记录插入到 accounts 表后返回所有列的值(包括idcreated_at)。这非常重要,因为在创建帐户后,我们总是希望将其 ID 返回给客户端。

生成 Go 代码来创建帐户

好的,现在让我们打开终端并运行make sqlc

然后回到 Visual Studio Code。在db/sqlc文件夹中,我们可以看到 3 个新生成的文件。

第一个是models.go。该文件包含3个模型的结构定义:AccountEntryTransfer



// Code generated by sqlc. DO NOT EDIT.

package db

import (
  "time"
)

type Account struct {
  ID        int64     `json:"id"`
  Owner     string    `json:"owner"`
  Balance   int64     `json:"balance"`
  Currency  string    `json:"currency"`
  CreatedAt time.Time `json:"created_at"`
}

type Entry struct {
  ID        int64 `json:"id"`
  AccountID int64 `json:"account_id"`
  // can be negative or positive
  Amount    int64     `json:"amount"`
  CreatedAt time.Time `json:"created_at"`
}

type Transfer struct {
  ID            int64 `json:"id"`
  FromAccountID int64 `json:"from_account_id"`
  ToAccountID   int64 `json:"to_account_id"`
  // must be positive
  Amount    int64     `json:"amount"`
  CreatedAt time.Time `json:"created_at"`
}


Enter fullscreen mode Exit fullscreen mode

它们都有 JSON 标签,因为我们在 中设置emit_json_tags的字段结构体顶部也有一个注释,因为我们在上一节课的数据库模式定义中添加了它们。truesqlc.yamlAmountEntryTransfer

第二个文件是db.go。该文件包含接口。它定义了和对象DBTX都具有的 4 个通用方法。这允许我们自由地使用数据库或事务来执行查询。sql.DBsql.Tx



// Code generated by sqlc. DO NOT EDIT.

package db

import (
  "context"
  "database/sql"
)

type DBTX interface {
  ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
  PrepareContext(context.Context, string) (*sql.Stmt, error)
  QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
  QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}

func New(db DBTX) *Queries {
  return &Queries{db: db}
}

type Queries struct {
  db DBTX
}

func (q *Queries) WithTx(tx *sql.Tx) *Queries {
  return &Queries{
    db: tx,
  }
}


Enter fullscreen mode Exit fullscreen mode

如你所见,该New()函数接受一个 aDBTX作为输入并返回一个Queries对象。因此,我们可以传入一个 asql.DB或一个sql.Tx对象,具体取决于我们是想在事务中执行单个查询,还是执行一组多个查询。

还有一种方法WithTx(),允许将 Queries 实例与事务关联。我们将在另一节关于事务的课程中详细学习。

第三个文件是account.sql.go文件。



// Code generated by sqlc. DO NOT EDIT.
// source: account.sql

package db

import (
  "context"
)

const createAccount = `-- name: CreateAccount :one
INSERT INTO accounts (
  owner,
  balance,
  currency
) VALUES (
  $1, $2, $3
) RETURNING id, owner, balance, currency, created_at
`

type CreateAccountParams struct {
  Owner    string `json:"owner"`
  Balance  int64  `json:"balance"`
  Currency string `json:"currency"`
}

func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) {
  row := q.db.QueryRowContext(ctx, createAccount, arg.Owner, arg.Balance, arg.Currency)
  var i Account
  err := row.Scan(
    &i.ID,
    &i.Owner,
    &i.Balance,
    &i.Currency,
    &i.CreatedAt,
  )
  return i, err
}


Enter fullscreen mode Exit fullscreen mode

包名称db与我们在sqlc.yaml文件中定义的一样。

account.sql在顶部,我们可以看到创建帐户的 SQL 查询。除了子句之外,它看起来与我们在文件中编写的查询几乎相同RETURN。SQLC 已RETURN *明确替换所有列的名称。这使得查询更清晰,并避免以错误的顺序扫描值。

然后我们有了CreateAccountParams结构体,它包含了我们在创建新账户时想要设置的所有列owner,,balancecurrency

CreateAccount()函数被定义为Queries对象的一个​​方法。它之所以有这个名字,是因为我们在 SQL 查询中使用了注释来指示 sqlc。该函数接受一个上下文和一个CreateAccountParams对象作为输入,并返回一个Account模型对象或一个error

红线

Visual Studio 代码在这里显示一些红线,因为我们尚未为我们的项目初始化模块。

让我们打开终端并运行:



go mod init github.com/techschool/simplebank


Enter fullscreen mode Exit fullscreen mode

我们的模块名称是github.com/techschool/simplebank。现在go.mod文件已生成。让我们运行以下命令来安装所有依赖项。



go mod tidy


Enter fullscreen mode Exit fullscreen mode

好了,现在回到account.sql.go文件。所有红线都消失了。

在函数中CreateAccount(),我们调用QueryRowContext()该函数来执行 create-account SQL 查询。此函数属于DBTX我们之前见过的接口。我们传入上下文、查询以及 3 个参数:ownerbalancecurrency

该函数返回一个行对象,我们可以使用它来将每列的值扫描到正确的变量中。如果我们使用标准database/sql库,这通常是需要手动编写的基本代码。但是,它能自动生成,真是太酷了!是不是很棒?

sqlc 还有一个很棒的功能:它会在生成代码之前检查 SQL 查询语法。所以,如果我尝试删除查询中的第三个参数,



-- name: CreateAccount :one
INSERT INTO accounts (
  owner,
  balance,
  currency
) VALUES (
  $1, $2, $3
) RETURNING *;


Enter fullscreen mode Exit fullscreen mode

并运行 make sqlc again,报错:INSERT has more target columns than expressions

插入错误

因此,如果 sqlc 成功生成代码,我们就可以确信我们的 SQL 查询中没有愚蠢的错误。

使用 sqlc 时,有一点很重要:我们不应该修改生成文件的内容,因为每次运行 make sqlc 时,所有这些文件都会重新生成,我们的更改将会丢失。因此,如果要向 db 包添加更多代码,请务必创建新文件。

好了,现在我们知道如何在数据库中创建记录。

读取操作(GET/LIST)

让我们进入下一个操作:READ

读操作

在此示例中,有 2 个基本数据检索查询:GetList。让我们将它们复制到我们的account.sql文件中。

编写 SQL 查询以获取/列出帐户

get 查询用于通过 ID 获取 1 条账户记录。因此,我将把它的名称改为GetAccount。查询语句如下:



-- name: GetAccount :one
SELECT * FROM accounts
WHERE id = $1 LIMIT 1;


Enter fullscreen mode Exit fullscreen mode

我们在这里使用LIMIT 1是因为我们只想选择 1 条记录。

下一个操作是ListAccounts。它将返回多个帐户记录,因此我们:many在这里使用标签。



-- name: ListAccounts :many
SELECT * FROM accounts
ORDER BY id
LIMIT $1
OFFSET $2;


Enter fullscreen mode Exit fullscreen mode

与查询类似GetAccount,我们从帐户表中进行选择,然后按其 ID 对记录进行排序。

由于数据库中可能包含大量账户,我们不应该一次性选择所有账户。相反,我们会进行分页。因此,我们使用LIMIT来设置要获取的行数,并使用OFFSET来告诉 Postgres 在开始返回结果之前跳过这么多行。

就是这样!

生成 Go 代码以获取/列出帐户

现在让我们运行make sqlc重新生成代码,并打开account.sql.go文件。

现在,GetAccount()ListAccounts()函数已生成。和以前一样,sqlc 已SELECT *为我们替换为显式列名。



const getAccount = `-- name: GetAccount :one
SELECT id, owner, balance, currency, created_at FROM accounts
WHERE id = $1 LIMIT 1
`

func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) {
  row := q.db.QueryRowContext(ctx, getAccount, id)
  var i Account
  err := row.Scan(
    &i.ID,
    &i.Owner,
    &i.Balance,
    &i.Currency,
    &i.CreatedAt,
    )
  return i, err
}


Enter fullscreen mode Exit fullscreen mode

GetAccount()函数仅接受上下文和帐户 ID 作为输入。在函数内部,它仅QueryRowContext()使用原始 SQL 查询和帐户 ID 进行调用。它会将行扫描成帐户对象并将其返回给调用者。非常简单!

ListAccounts 函数稍微复杂一些。它接受一个上下文、一个limit参数offset作为输入,并返回一个对象列表Account



const listAccounts = `-- name: ListAccounts :many
SELECT id, owner, balance, currency, created_at FROM accounts
ORDER BY id
LIMIT $1
OFFSET $2
`

type ListAccountsParams struct {
  Limit  int32 `json:"limit"`
  Offset int32 `json:"offset"`
}

func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) {
  rows, err := q.db.QueryContext(ctx, listAccounts, arg.Limit, arg.Offset)
  if err != nil {
    return nil, err
  }
  defer rows.Close()
  var items []Account
  for rows.Next() {
    var i Account
    if err := rows.Scan(
      &i.ID,
      &i.Owner,
      &i.Balance,
      &i.Currency,
      &i.CreatedAt,
    ); err != nil {
      return nil, err
    }
    items = append(items, i)
  }
  if err := rows.Close(); err != nil {
    return nil, err
  }
  if err := rows.Err(); err != nil {
    return nil, err
  }
  return items, nil
}


Enter fullscreen mode Exit fullscreen mode

在内部,它调用QueryContext(),并将列表帐户查询与limit和一起传入offset

此函数返回一个 rows 对象。它类似于迭代器,允许我们逐条遍历记录,并将每条记录扫描到 account 对象中,并将其附加到项目列表中。

最后,它会关闭这些行以避免数据库连接泄漏。在将数据返回给调用者之前,它还会检查是否存在任何错误。

代码看起来很长,但很容易理解。最重要的是:它运行速度非常快!而且我们不必担心代码中出现愚蠢的错误,因为 sqlc 已经保证生成的代码能够完美运行。

UPDATE 操作

好的,让我们进行下一个操作:UPDATE

编写 SQL 查询来更新帐户

更新

我们将此代码复制到我们的account.sql文件中,并将函数名称更改为UpdateAccount()



-- name: UpdateAccount :exec
UPDATE accounts
SET balance = $2
WHERE id = $1;


Enter fullscreen mode Exit fullscreen mode

这里我们使用一个新标签:exec,因为这个命令不返回任何数据,它只是更新数据库中的 1 行。

假设我们只允许更新帐户balance。帐户ownercurrency不应更改。

我们使用该WHERE子句来指定id要更新的帐户。就这样!

生成 Go 代码来更新帐户

现在make sqlc在终端中运行以重新生成代码。瞧,我们得到了这个UpdateAccount()函数。



const updateAccount = `-- name: UpdateAccount :exec
UPDATE accounts
SET balance = $2
WHERE id = $1
`

type UpdateAccountParams struct {
  ID      int64 `json:"id"`
  Balance int64 `json:"balance"`
}

func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) error {
  _, err := q.db.ExecContext(ctx, updateAccount, arg.ID, arg.Balance)
  return err
}


Enter fullscreen mode Exit fullscreen mode

它以上下文、账户idbalance参数作为输入。它所做的就是ExecContext()使用查询和输入参数进行调用,然后返回error给调用者。

返回更新后的行

有时,返回更新后的帐户对象也很有用。在这种情况下,我们可以将:exec标签更改为,并在更新查询的末尾:one添加:RETURNING *



-- name: UpdateAccount :one
UPDATE accounts
SET balance = $2
WHERE id = $1
RETURNING *;


Enter fullscreen mode Exit fullscreen mode

然后重新生成代码。



const updateAccount = `-- name: UpdateAccount :one
UPDATE accounts
SET balance = $2
WHERE id = $1
RETURNING id, owner, balance, currency, created_at
`

type UpdateAccountParams struct {
  ID      int64 `json:"id"`
  Balance int64 `json:"balance"`
}

func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) {
  row := q.db.QueryRowContext(ctx, updateAccount, arg.ID, arg.Balance)
  var i Account
  err := row.Scan(
    &i.ID,
    &i.Owner,
    &i.Balance,
    &i.Currency,
    &i.CreatedAt,
  )
  return i, err
}


Enter fullscreen mode Exit fullscreen mode

现在 SQL 查询已更改,并且UpdateAccount()函数将返回更新后的内容Account以及error。太棒了!

DELETE 操作

最后一个操作是DELETE。它比更新还要简单。

编写 SQL 查询来删除帐户

删除

我们复制这个示例查询,并将函数名称更改为DeleteAccount。我不希望 Postgres 返回已删除的记录,所以我们使用:exec标签。



-- name: DeleteAccount :exec
DELETE FROM accounts
WHERE id = $1;


Enter fullscreen mode Exit fullscreen mode

生成Go代码来删除账户

让我们运行一下make sqlc重新生成代码。现在我们DeleteAccount()在代码中有了函数。



const deleteAccount = `-- name: DeleteAccount :exec
DELETE FROM accounts
WHERE id = $1
`

func (q *Queries) DeleteAccount(ctx context.Context, id int64) error {
  _, err := q.db.ExecContext(ctx, deleteAccount, id)
  return err
}


Enter fullscreen mode Exit fullscreen mode

至此,我们基本上已经学会了如何为我们的accounts表生成完整的 CRUD 操作。您可以尝试对剩下的两个表执行相同的操作:entries和 ,transfers作为练习。

我会将代码推送到 github 的这个存储库,以便您在需要查看时可以参考。

今天的讲座就到这里。非常感谢大家的阅读,我们下期再见!


如果您喜欢这篇文章,请订阅我们的 Youtube 频道在 Twitter 上关注我们,以便将来获取更多教程。


如果你想加入我目前在 Voodoo 的优秀团队,请查看我们的职位空缺。你可以远程办公,也可以在巴黎/阿姆斯特丹/伦敦/柏林/巴塞罗那现场办公,但需获得签证担保。

文章来源:https://dev.to/techschoolguru/generate-crud-golang-code-from-sql-and-compare-db-sql-gorm-sqlx-sqlc-560j
PREV
如何创建和签署 SSL/TLS 证书
NEXT
数据库事务锁以及如何处理死锁