如果您的应用程序是一顿饭,那么数据库模型就像一种乏味的碳水化合物,可以填饱您的肚子,但永远不会满足。
因此,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可能是正确的选择。