Go中操作数据库的5种方法


如果您的应用程序是一顿饭,那么数据库模型就像一种乏味的碳水化合物,可以填饱您的肚子,但永远不会满足。

因此,Go 拥有如此多的工具来完成这项工作也就不足为奇了。

有选择固然很好,但选择太多,可能会让人不知所措。

这篇文章介绍了几类工具,并提供了每种工具的示例,以便您可以为您的项目选择正确的方法。

示例代码位于GitHub上。

Vanilla
数据库驱动程序和标准库,因此第一个示例仅使用这些。

使模型本身成为一个普通的数据结构,并编写一个CharacterStore结构来执行 CRUD 操作:

type Character struct {
    ID      int64
    ActorID int64
    Name    string
}

type CharacterStore struct {
    db *sql.DB
}

CharacterStore获取代码:

func (cs *CharacterStore) Get(ctx context.Context, id int64) (*Character, error) {
    row := cs.db.QueryRowContext(ctx, `SELECT id, actor_id, name FROM characters WHERE id = $1`, id)

    var c Character
    err := row.Scan(&c.ID, &c.ActorID, &c.Name)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, nil
    }

    return &c, err
}

这种方法有一个明显的缺点,那就是代码编写繁琐。它还容易出错(例如,将查询中的列与Scan中的顺序相匹配)。但是您可以绝对控制每一个方面。

Struct映射器
有时你可能需要比标准库提供的更多的东西。这时就需要一个可以在SQL和结构体之间进行映射的包。

以sqlx为例。使用反射来自动处理vanilla方法中最繁琐的部分。

我们的字符结构获得了一些db标签:

type Character struct {
    ID      int64  `db:"id"`
    ActorID int64  `db:"actor_id"`
    Name    string `db:"name"`
}

这样我们就可以使用 sqlx 的方法将数据直接加载到结构体中:

    var c Character
    err := cs.dbx.GetContext(ctx, &c, `SELECT id, actor_id, name FROM characters WHERE id = $1`, id)

您也可以在查询中使用字段的标签名称:

es, err := cs.dbx.NamedExecContext(ctx, `UPDATE characters SET actor_id = :actor_id, name = :name WHERE id = :id`, c)


当您想保留大部分控制,但又想避免编写另一个for rows.Scan()循环时,这样做很好。

我发现唯一的缺点是数据结构中的字段经常需要实现Scanner和Valuer。
这也是可以避免的,但这通常意味着将sql.NullString或pq.StringArray暴露给代码库中不需要了解数据库的地方。

SQL 构建器
如果您厌倦了手写 SQL 查询,请尝试 SQL 生成器。

如果您还记得Get原始示例中的方法,它已被简化为:

func (cs *CharacterStore) Get(ctx context.Context, id int64) (*Character, error) {
    var c Character
    err := squirrel.
        Select("id", "actor_id", "name").
        From("characters").
        Where("id = ?", id).
        RunWith(cs.db).
        QueryRowContext(ctx).
        Scan(&c.ID, &c.ActorID, &c.Name)

    if errors.Is(err, sql.ErrNoRows) {
        return nil, nil
    }

    return &c, err
}

也许是一个小小的改进,但真正的优势是对于更复杂的查询,例如方法List:

func (cs *CharacterStore) List(ctx context.Context, filters *CharacterFilters) ([]*Character, error) {
    q := squirrel.
        Select("c.id", "c.actor_id", "c.name").
        From("characters c").
        RunWith(cs.db)

    if filters != nil {
        if filters.ActorID != 0 {
            q = q.Where("actor_id = ?", filters.ActorID)
        } else if filters.ActorName != "" {
            q = q.
                Join("actors a ON a.id = c.actor_id").
                Where("LOWER(a.name) LIKE ?", "%"+strings.ToLower(filters.ActorName)+"%")
        }

        if filters.Name != "" {
            q = q.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(filters.Name)+"%")
        }

        if filters.SceneNumber != 0 {
            q = q.
                Join("scene_characters sc ON sc.character_id = c.id").
                Where("sc.scene_id = ?", filters.SceneNumber)
        }
    }

    rows, err := q.QueryContext(ctx)
    // ... same as before from here ...
}

查询生成器非常适合大多数普通的database/sql代码库。

我发现了一些缺点,主要是因为很容易退回到手写 SQL。

sqlc
你见过Go生成SQL,但是你见过SQL生成Go吗?如果没有,请查看sqlc。您编写一个带有一些最少注释的 SQL 文件:

-- name: GetCharacter :one
-- GetCharacter loads a character from the database by ID.
SELECT * FROM characters WHERE id = ?;

sqlc生成Go代码:

// models.go
type Character struct {
    ID      int64
    Name    string
    ActorID int64
}

// characters.sql.go
const getCharacter = `-- name: GetCharacter :one
SELECT id, name, actor_id FROM characters WHERE id = ?
`

// GetCharacter loads a character from the database by ID.
func (q *Queries) GetCharacter(ctx context.Context, id int64) (Character, error) {
    row := q.db.QueryRowContext(ctx, getCharacter, id)
    var i Character
    err := row.Scan(&i.ID, &i.Name, &i.ActorID)
    return i, err
}

完整的例子

sqlc删除了大量繁琐的代码,却不会影响你对SQL的控制。而且,与本帖中的其他选项不同,使用它不会有运行时惩罚。它需要一些配置才能启动(例如,它需要知道你的模式),但在此之后,你可以经常重新生成模型。

sqlc的主要缺点是你失去了对生成代码界面的直接控制。它生成的代码并不糟糕,但有时感觉有点机械。当然,如果你真的不喜欢它,你可以用sqlc生成私有方法,然后按你的需要封装它们。我在StoreCharacter方法中就是这么做的:

func (q *Queries) StoreCharacter(ctx context.Context, c *Character) error {
    if c.ID == 0 {
        id, err := q.insertCharacter(ctx, insertCharacterParams{
            ActorID: c.ActorID,
            Name:    c.Name,
        })
        if err != nil {
            return err
        }

        c.ID = id
        return nil
    }

    return q.updateCharacter(ctx, updateCharacterParams{
        ID:      c.ID,
        ActorID: c.ActorID,
        Name:    c.Name,
    })
}

insertCharacter和updateCharacter来自sqlc。

另一个问题是,您在SQL中表达的内容受到限制。如果您想基于一些可选条件建立一个查询,就像我的列表示例一样,您是做不到的。我不得不这样做:

func (q *Queries) ListCharacters(ctx context.Context, filters *CharacterFilters) ([]Character, error) {
    switch {
    case filters.ActorID != 0:
        return q.listCharactersByActor(ctx, filters.ActorID)
    case filters.ActorName != "":
        return q.listCharactersByActorName(ctx, filters.ActorName)
    case filters.Name != "":
        return q.listCharactersByName(ctx, filters.Name)
    case filters.SceneNumber != 0:
        return q.listCharactersByScene(ctx, filters.SceneNumber)
    default:
        return q.listAllCharacters(ctx)
    }
}

与该方法的其他实现不同,该版本不能通过一个以上的过滤参数进行过滤。可以通过为每种排列写一个SQL查询来实现。我经常扩充sqlc生成的内容,所以你可以自由地使用它,当它有帮助的时候使用,当它没有帮助的时候忽略它。

ORM
Go有几种ORM可供选择,但GORM是相当流行的,我在本例中也使用了它。与任何全功能的ORM一样,你只需要定义对象。下面是Character结构的定义:

type Character struct {
    ID      int64  `gorm:"id,primaryKey"`
    ActorID int64  `gorm:"actor_id"`
    Name    string `gorm:"name"`

    Actor Actor
}

这足以让GORM处理所有的CRUD操作,例如创建一个新的字符:

    err := db.Create(&Character{
        Name:    "Sir Not-Appearing-in-this-Film",
        ActorID: 1,
    }).Error

List方法(现在是ListCharacters函数)看起来像squirrel 版本,只是因为GORM处理了Scan,所以更简单一些。

GORM为我们提供了大量的代码。但是,作为交换,我们不得不放弃对接口和SQL查询的控制。这是魔鬼的交易。我避免使用ORM,但如果快速开发是你的首要考虑,GORM可能是正确的选择。