SQLBoiler官方文档翻译--最好用的Go ORM框架

2,603 阅读21分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

文章主要是对 sqlboiler 官方文档 的翻译总结,但外键的内容可能会被忽略。

需要快速入门可以看文章SQLBoiler入门指导1-最好用的Go ORM框架

名词解释

  • models/model 模型,一个model可以理解成一张数据库的表
  • database handle 数据库句柄/数据源

SQLBoiler 是什么

SQLBoiler 是一个根据数据库表生成对应的 Go ORM 代码的工具。

它是一个数据库先行的 ORM 框架,也就是说你需要先设计你的数据库,而不是像 gorm 那样先设计 struct。

关于SQLBoiler

特性

  • 完整模型生成(Full model generation)
  • 代码生成速度非常快
  • 通过代码生成和智能缓存实现高性能
  • 使用 boil.Executor,可以适应 sql.DB, sqlx.DB 等
  • 使用 context.Context
  • 强类型查询(通常不需要类型转换和指针绑定)
  • 钩子(Before/After Create/Select/Update/Delete/Upsert)
  • 自动设置 CreatedAt/UpdatedAt/DeletedAt
  • 表和列的白名单、黑名单
  • 自定义 struct tags
  • 事务
  • 原始 SQL
  • 兼容性测试(根据你使用的数据库架构)
  • Debug 日志
  • Eunm 类型
  • 支持许多引擎

支持的数据库

数据库驱动地址
PostgreSQLgithub.com/volatiletec…
MySQLgithub.com/volatiletec…
MSSQLServer 2012+github.com/volatiletec…
SQLite3github.com/volatiletec…
CockroachDBgithub.com/glerchundi/…

尝试一下

import (
  // 使用 . import 这样在 qm.Limit等操作的时候就不用加前缀了
  . "github.com/volatiletech/sqlboiler/v4/queries/qm"
)

// 打开数据库
db, err := sql.Open("postgres", "dbname=fun user=abc")
if err != nil {
  return err
}

// 如果你不想调用方法时都带上 db 参数
// 你可以使用 boil.SetDB() 设置全局数据源
// 然后在生成代码的时候加上 --add-global-variants 参数使能够生成带G(global)后缀的方法
boil.SetDB(db)
users, err := models.Users().AllG(ctx)

// 查询全部用户
users, err := models.Users().All(ctx, db)

// P后缀,直接 panic 如果 err 不为 nil
// 在生成代码的时候加上 --add-panic-variants  参数使能够生成带P(panic)后缀的方法
users := models.Users().AllP(db)

// 复杂查询
users, err := models.Users(Where("age > ?", 30), Limit(5), Offset(6)).All(ctx, db)

// 复杂查询
users, err := models.Users(
  Select("id", "name"),
  InnerJoin("credit_cards c on c.user_id = users.id"),
  Where("age > ?", 30),
  AndIn("c.kind in ?", "visa", "mastercard"),
  Or("email like ?", `%aol.com%`),
  GroupBy("id", "name"),
  Having("count(c.id) > ?", 2),
  Limit(5),
  Offset(6),
).All(ctx, db)

// 所有查询都可以使用任意的 boil.Executor 实现类(*sql.DB, *sql.Tx等)
tx, err := db.BeginTx(ctx, nil)
if err != nil {
  return err
}
users, err := models.Users().All(ctx, tx)

要求和提示

要求

  • Go 1.13以上。

  • 表名和列名必须使用蛇形命名 snake_case。

  • MySQL 最低 5.6.30;因为 ssl-mode 在之前版本不支持。

  • 对于 MySQL 如果使用 github.com/go-sql-driver/mysql druver,请在连接上加上parseTime=True 参数,以激活 time.Time 解析。在 models 中,SQLBoiler 使用 time.Timenull.Time 代表时间,如果不启用任何 DATE/DATETIME 列的 models 将不可用。

    • 也就是db, err := sql.Open("mysql","root:root@tcp(127.0.0.1:3306)/dbname?parseTime=True")

    • 具体原因可以看timetime-support。也就是默认MySQL 的 MySQL DATE 和 DATETIME 的值是 []byte,可以使用 一个 []bytestring 或 sql.RawBytes 变量去扫描,加上这个参数后可以把内部输出类型从 []byte 改为 time.Time

      • 再进一步的原因是Go1.1版本的时候使 time.Time 成为唯一的可以扫描 DATE 和 DATETIME 的变量,这违背了 sql.RawBytes support

提示

  • SQLBoiler 会生成类型安全的表名(/models/boil_table_names.go/TableNames)、列名(/models/[表名].go/UserColumns)和where语句(/models/[表名].go/UserWhere)。你应该使用这些而不是直接使用字符串,这样在数据库改变时容易产生错误。
  • 建议在需要执行多条语句的时候使用事务提高性能和保证数据的完整性。
  • 如果你不考虑使用 hooks 功能可以使用 --no-hooks 参数避免产生相应的代码,减少文件大小。

开始

下载安装

你必须先安装代码生成器。

# Go 1.16 或更新
go install github.com/volatiletech/sqlboiler/v4@latest
go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-mysql@latest

# Go 1.15 或之前
# 安装 sqlboiler v4 和 mysql driver (psql, mysql, mssql, sqlite3 也可以)
# 注意:不要在其他 go module 安装,否则会污染你的 go.mod 文件
GO111MODULE=on go get -u -t github.com/volatiletech/sqlboiler/v4
GO111MODULE=on go get github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-mysql

在你的项目中使用下面命令安装 sqlboiler 的依赖,会自动添加到你的 go.mod 文件里。

go get github.com/volatiletech/sqlboiler/v4
go get github.com/volatiletech/null/v8

配置

只支持 TOML 格式文件和环境变量。

配置文件应该命名为sqlboiler.toml 并根据下面的顺序搜索:

  • ./
  • $XDG_CONFIG_HOME/sqlboiler/
  • $HOME/.config/sqlboiler/

数据库驱动配置项

对应驱动的配置必须以[驱动名]开头。你必须使用配置文件或环境变量去配置一个数据库驱动。

MySQL 的配置文件应该像下面那样:

[mysql]
dbname = "your_database_name"

当你使用环境变量时配置必须以驱动名作为前缀:

MYSQL_DBNAME="your_database_name"

数据库对应的参数:

配置名是否必须Postgres 默认值MySQL 默认值MSSQL 默认值
schemano"public"none"dbo"
dbnameyesnonenonenone
hostyesnonenonenone
portno543233061433
useryesnonenonenone
passnononenonenone
sslmodeno"require""true""true"
whitelistno[][][]
blacklistno[][][]

黑白名单例子:

[mysql]
# migrations 表和 addresses 表的 name 字段将不会被生成。
# 对应的外键也不会被删除,因为可能导致某些错误的产生。
blacklist = ["migrations", "addresses.name"]

一般配置项

如果你不想通过环境变量或命令行参数指定这些变量,你可以把下面的配置项添加到配置文件里。

配置项默认值解释
pkgname"models"生成代码的包名,默认models
output"models"生成代码的文件夹名,默认 models
tag[]除 json,yaml,toml外的tag结构
debugfalsedebug模式,在error时会打印堆栈信息
add-global-variantsfalse启动生成全局变量(方法带G后缀)
add-panic-variantsfalse启动生成panic变量(方法带P后缀)
no-contextfalse禁用生成代码中对 context.Context 的使用,比如用于方法参数
no-hooksfalse禁用hook特性
no-testsfalse禁用生成测试文件
no-auto-timestampsfalse禁用自动设置 created_at/updated_at
no-rows-affectedfalse禁用生成代码的影响行数返回值
no-driver-templatesfalse禁用解析由数据库驱动定义的模板
tag-ignore[]哪些列名的tag设置为'-',也就是忽略解析

配置例子

output   = "my_models"
wipe     = true
no-tests = true

[psql]
  dbname = "dbname"
  host   = "localhost"
  port   = 5432
  user   = "dbusername"
  pass   = "dbpassword"
  schema = "myschema"
  blacklist = ["migrations", "other"]

[mysql]
  dbname  = "dbname"
  host    = "localhost"
  port    = 3306
  user    = "dbusername"
  pass    = "dbpassword"
  sslmode = "false"

[mssql]
  dbname  = "dbname"
  host    = "localhost"
  port    = 1433
  user    = "dbusername"
  pass    = "dbpassword"
  sslmode = "disable"
  schema  = "notdbo"

TOML格式的介绍可以看此文章

初次代码生成

编写完配置文件后,我们可以调用 sqlboiler 命令工具去生成代码。

格式:
  sqlboiler [flags] <driver>

例子:
sqlboiler mysql

Flags:
      --add-global-variants        启动生成全局变量(方法带G后缀)
      --add-panic-variants         启动生成panic变量(方法带P后缀)
      --add-soft-deletes           启动通过更新 deleted_at timestamp 的软删除
  -c, --config 文件名               配置文件的文件名
  -d, --debug                      debug模式,在error时会打印堆栈信息
  -h, --help                       帮助
      --no-auto-timestamps         禁用自动设置 created_at/updated_at
      --no-back-referencing        Disable back referencing in the loaded relationship structs
      --no-context                 禁用生成代码中对 context.Context 的使用,比如用于方法参数
      --no-driver-templates        禁用解析由数据库驱动定义的模板
      --no-hooks                   禁用hook特性
      --no-rows-affected           禁用生成代码的影响行数返回值
      --no-tests                   禁用生成测试文件
  -o, --output string              生成代码的文件夹名,默认 models
  -p, --pkgname string             生成代码的包名,默认models
      --struct-tag-casing string   决定Go代码tag标签上的名字格式,可以选择 camel 驼峰,title,alias 或 snake 蛇形 (默认 "snake")
  -t, --tag strings                除 json,yaml,toml外的tag结构
      --tag-ignore strings         哪些列名的tag设置为'-',也就是忽略解析
      --templates strings          模板目录,覆盖 sqlboiler 的bindata模板文件夹
      --version                    打印版本
      --wipe                       在生成代码前先使用rm -rf删除输出文件夹

下面是一个基本的model生成。生成代码后你可以运行兼容性测试去运行全部生成的代码,通过测试可以保证你的数据库兼容SQLBoiler,如果你测试失败,请看下面的问题诊断章节。

# 产生models
# 当发现 mysql 参数时会自动调用 sqlboiler-psql 在你的 CWD 和 PATH
sqlboiler mysql

# 运行测试
go test ./models

你也可以使用 go generate 去生成代码如果你想让你的应用容易在命令行运行。

//go:generate sqlboiler mysql

不要修改任何生成文件,否则会导致重新生成代码出错。

重新生成代码

建议重新生成代码时添加 --wipe flag 以完整删除之前生成的目录,因为 Sqlboiler 不会比较前后代码的不同,之后简单的覆盖之前的代码。

代码生成控制

模板会以指定的方式执行,命令行或配置文件可以控制功能的开关。

别名

sqlboiler会自动生成名字,但如果你的数据库名字起的不好且无法修改,或者 sqlboiler 无法推断你的名字,那么你可以使用 别名 去修改生成代码中的表名、列名名。

注:不需要提供所有的名字,其他名字将会像往常一样被推断。

# 为原始表名为 user 的表设置别名
[aliases.tables.user]
# 大写复数时的名字
up_plural     = "UserInfos"
# 大写单数时的名字
up_singular   = "UserInfo"
# 小写复数时的名字
down_plural   = "userInfos"
# 小写单数时的名字
down_singular = "userInfo"

    # 为表user的password列设置别名 psw
    [aliases.tables.user.columns]
    password = "psw"

另外一种写法是,使用toml的数组而不是map,当然现在需要使用name指定名字,用alias指定别名。

# 为原始表名为 user 的表设置别名
[[aliases.tables]]
# 原始表名
name = "user"
# 大写复数时的名字
up_plural     = "UserInfos"
# 大写单数时的名字
up_singular   = "UserInfo"
# 小写复数时的名字
down_plural   = "userInfos"
# 小写单数时的名字
down_singular = "userInfo"

    # 为表user的password字段设置别名 psw
    [[aliases.tables.columns]]
    # 原始列名
    name = "password"
    # 别名
    alias = "psw"

类型

可以通过配置文件覆盖sqlboiler驱动对类型的推断。

# 多个类型替换会从上到下进行匹配,匹配多会依次进行替换
# 更具体的匹配规则应该放在下方,因为一旦匹配到则会立刻执行
[[types]]
  # 匹配需要替换的类型,会对所有列进行匹配
  # 可以使用 types.match: tables = ['users', 'videos'] 添加匹配的白名单,白名单中的表才会被匹配
  [types.match]
    type = "null.String"
    nullable = true
    # tables = ['user']

  # 替换的类型
  [types.replace]
    type = "mynull.String"

  # 导入替换类型的包
  [types.imports]
    third_party = ['"github.com/me/mynull"']

导入 Imports

替换import的内容,正常情况下应该避免。

[imports.all]
  standard = ['"context"']
  third_party = ['"github.com/my/package"']

# 改变 boil_queries 文件的import
[imports.singleton."boil_queries"]
  standard = ['"context"']
  third_party = ['"github.com/my/package"']

# 和 all 一样
[imports.test]

# 和 singleton 一样
[imports.test_singleton]

# 当 model 包含 string类型时改变 import
[imports.based_on_type.string]
  standard = ['"context"']
  third_party = ['"github.com/my/package"']

也可以这样写,通过name字段指定。

[[imports.singleton]]
  name = "boil_queries"
  third_party = ['"github.com/my/package"']

[[imports.based_on_type]]
  name = "null.Int64"
  third_party = ['"github.com/my/int64"']

模板 Templates

可以生成额外的其他语言的文件,若需要该功能可以看文档 Templates

扩展生成的 models

可以给models扩展一些辅助函数。最好把扩展的函数放在models外包的其他地方,避免在重新生成代码的时候被删除。下面介绍3扩展models的方式:

方式1:简单函数

package modext

// UserFirstTimeSetup 是 user model 的扩展方法
func UserFirstTimeSetup(ctx context.Context, db *sql.DB, u *models.User) error { ... }

调用方式:

user, err := Users().One(ctx, db)
// 错误检查

err = modext.UserFirstTimeSetup(ctx, db, user)
// 错误检查

方式2:空结构方法

这种方式可以更好的把方法进行划分。

package modext

type users struct {}

var Users = users{}

// FirstTimeSetup 是 user model 的扩展方法
func (users) FirstTimeSetup(ctx context.Context, db *sql.DB, u *models.User) error { ... }

调用方式:

user, err := Users().One(ctx, db)
// 错误检查

err = modext.Users.FirstTimeSetup(ctx, db, user)
// 错误检查

方式3:嵌套

user, err := Users().One(ctx, db)
// 错误检查

enhUser := modext.User{user}
err = ehnUser.FirstTimeSetup(ctx, db)
// 错误检查

常见问题

  • 在生成代码时忘记排除你不想要的表,比如迁移表。
  • 表忘记设置主键,所有表都需要主键。
  • 兼容性测试需要权限去创建一个数据库进行测试,请确保在 sqlboiler.toml 中配置足够的权限。
  • nil或者被关闭的数据源(database handle)。确保你传入的 boil.Executor 不为 nil,也就是 boil.SetDB(db)db
    • 如果你使用 G (全部数据源),请确保已经初始化全局数据源并使用boil.SetDB()
  • 命名冲突 Naming collisions,如果存在命名冲突而编译失败,请查看别名特性。
  • 字段没有被插入(通常是默认值为true的布尔类型),boil.Infer把Go的所有零值都当成你不想插入。当你想插入bool类型的false时,请使用 whitelist/greylist 让sqlboiler知道你想插入什么字段。
  • decimal 库错误,比如 pq: encode: unknown type types.NullDecimal 是因为 github.com/ericlargergren/decimal 库太新或者损坏导致,请在go.mod中使用下面版本 github.com/ericlagergren/decimal v0.0.0-20181231230500-73749d4874d5

其他的错误原因可以通过查看生成的代码,或者设置 boil.DebugModetrue 可以查看 sql和参数。也可以设置 boil.DebugWriter 重定向 debug 输出流,默认是 os.Stdout

如果你仍然没有解决或者你发现了bug,可以 issue

特性和例子

下面的大部分例子使用这个表结构:

CREATE TABLE pilots (
  id integer NOT NULL,
  name text NOT NULL
);
ALTER TABLE pilots ADD CONSTRAINT pilot_pkey PRIMARY KEY (id);

CREATE TABLE jets (
  id integer NOT NULL,
  pilot_id integer NOT NULL,
  age integer NOT NULL,
  name text NOT NULL,
  color text NOT NULL
);
ALTER TABLE jets ADD CONSTRAINT jet_pkey PRIMARY KEY (id);

CREATE TABLE languages (
  id integer NOT NULL,
  language text NOT NULL
);
ALTER TABLE languages ADD CONSTRAINT language_pkey PRIMARY KEY (id);

CREATE TABLE pilot_languages (
  pilot_id integer NOT NULL,
  language_id integer NOT NULL
);
ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pkey PRIMARY KEY (pilot_id, language_id);

自动 CreatedAt/UpdatedAt

如果你生成的 SQLBoiler models 包含 created_atupdated_at 列将会被自动设置为 time.Now()。可以使用 --no-auto-timestamps 去禁用这个功能。

注意:你可以通过 boil.SetLocation()设置时区。

跳过自动设置时间

如果你不想在 insert 或 update 时自动设置时间戳,可以使用 boil.SkipTimestamps 避免。

覆盖自动设置时间

  • Insert
    • created_atupdated_at 是零值时会被自动设置。
    • 如果要设置为 null,设置 Validfalse 并设置 Time 为非零值(注意这里是说null.Time类型)。
  • Update
  • updated_at列总会被设置为 time.Now()。如果你想覆盖这个行为,你可以使用 queries.Raw()、使用hook覆盖 updated_at或创建一个 wrapper
  • Upsert
    • 如果created_at是零值将会自动设置。如果要设置为 null,设置 Validfalse 并设置 Time 为非零值。
    • updated_at列总会被设置为 time.Now()

自动DeletedAt (软删除)

SQLBoiler 使用deleted_at 提供软删除功能。 deleted_at 需要是一个nullable的时间字段。

注意:软删除是通过 --add-soft-deletes 启动,并且可能会在后续版本中修改。

注意:查询可以通过 qm.WithDeleted 绕过软删除。但目前还没有方法对 Exists/Find

执行此操作。

注意:当前的 Delete 不会设置 updated_at

查询构造器

SQLBoiler为你生成了一些 启动方法Starter method),这些方法被命名为 model 的复数形式,比如 models.Users()。启动方法被于构造查询模块系统Query Mod System)。启动方法使用一组 查询模块Query Mod) 作为参数,并在最后调用一个结束方法Finisher Method)。

一些例子:

// SELECT COUNT(*) FROM user;
count, err := models.Users().Count(ctx, db)

// SELECT * FROM user LIMIT 5;
users, err := models.Users(qm.Limit(5)).All(ctx, db)

// DELETE FROM user WHERE id = 1;
err := models.Users(qm.Where("id = ?", 1)).DeleteAll(ctx, db)
// 下面是类型安全的方式
err := models.Users(models.UserWhere.ID.EQ(1)).DeleteAll(ctx, db)

如果你需要直接指定数据表,也可以使用models.NewQuery()

rows, err := models.NewQuery(qm.From("user")).Query(db)

如你所见,查询模块允许你修改你的查询,结束方法允许你执行最后的操作。

查询模块系统(Query Mod System

查询模块系统允许你通过启动方法构造你的查询。

**注意:**SQLBoiler为表、列生成了类型安全的标识符。使用这些标识符将更加安全,因为如果数据库进行修改之后会产生编译错误而不是运行时错误。

// 点 import 让我们可以直接使用里面的函数而不需要加 "qm." 前缀
import . "github.com/volatiletech/sqlboiler/v4/queries/qm"

// 对生成的 model 使用 raw query(原始查询)
// 如果这个查询模块存在,其他查询模块将被覆盖
SQL("select * from pilots where id=?", 10)
models.Pilots(SQL("select * from pilots where id=?", 10)).All()

Select("id", "name") // 指定查询列
Select(models.PilotColumns.ID, models.PilotColumns.Name)
From("pilots as p") // 指定from表
From(models.TableNames.Pilots + " as p")

// WHERE 语句构建
Where("name=?", "John")
models.PilotWhere.Name.EQ("John")
And("age=?", 24)
// 目前还没有等价的类型安全查询
Or("height=?", 183)
// 目前还没有等价的类型安全查询

Where("(name=? and age=?) or (age=?)", "John", 5, 6)
// Expr 允许手动把语句弄成一组
Where(
  Expr(
    models.PilotWhere.Name.EQ("John"),
    Or2(models.PilotWhere.Age.EQ(5)),
  ),
  Or2(models.PilotAge),
)

// WHERE IN 语句构建
WhereIn("name, age in ?", "John", 24, "Tim", 33) // 生成: WHERE ("name","age") IN (($1,$2),($3,$4))
WhereIn(fmt.Sprintf("%s, %s in ?", models.PilotColumns.Name, models.PilotColumns.Age, "John", 24, "Tim", 33))
AndIn("weight in ?", 84)
AndIn(models.PilotColumns.Weight + " in ?", 84)
OrIn("height in ?", 183, 177, 204)
OrIn(models.PilotColumns.Height + " in ?", 183, 177, 204)

InnerJoin("pilots p on jets.pilot_id=?", 10)
InnerJoin(models.TableNames.Pilots + " p on " + models.TableNames.Jets + "." + models.JetColumns.PilotID + "=?", 10)

GroupBy("name")
GroupBy("name like ? DESC, name", "John")
GroupBy(models.PilotColumns.Name)
OrderBy("age, height")
OrderBy(models.PilotColumns.Age, models.PilotColumns.Height)

Having("count(jets) > 2")
Having(fmt.Sprintf("count(%s) > 2", models.TableNames.Jets)

Limit(15)
Offset(5)

// 显式锁定
For("update")

// With 语句
With("cte_0 AS (SELECT * FROM table_0 WHERE thing=? AND stuff=?)")

函数变体

函数可以生成它的变体通过 --add-global-variants--add-panic-variants

// 设置全局数据库把柄(数据源)G方法变体
boil.SetDB(db)

pilot, _ := models.FindPilot(ctx, db, 1)

err := pilot.Delete(ctx, db) // 普通模式需要db参数
pilot.DeleteP(ctx, db)       // P变体,需要db参数并在error时panic
err := pilot.DeleteG(ctx)    // G变体,不需要db参数
pilot.DeleteGP(ctx)          // GP变体,不需要db参数并在error时panic

db.Begin()                   // 正常的方式创建一个事务
boil.BeginTx(ctx, nil)       // 使用全局数据源创建事务

结束方法

所有的结束方法都有P和G变体。

// 调用方式和下面类似:
models.Pilots().All(ctx, db)

One() // 查询一行 (与LIMIT(1)相同)
All() // 查询全部 (与SELECT * FROM相同)
Count() // 查询行数 (与COUNT(*)相同)
UpdateAll(models.M{"name": "John", "age": 23}) // 更新所有匹配行
DeleteAll() // 删除所有匹配行
Exists() // 返回一个bool表示所查询的行是否存在
Bind(&myObj) // 绑定查询结构到你自己的struct
Exec() // 执行不需要任何返回的SQL查询
QueryRow() // 执行需要一行返回的SQL查询
Query() // 执行需要多行返回的SQL查询

原始查询

我们提供了 queries.Raw() 去执行原始查询. 通常你需要搭配使用 Bind()

err := queries.Raw("select * from pilots where id=?", 5).Bind(ctx, db, &obj)

你可以使用你自己的struct或生成的struct去``Bind()Bind()`支持绑定到单独一个对象或者一个slice。

queries.Raw() 也有一个不需要执行对象绑定的方法。

你也可以使用 models.NewQuery() 去应用 查询模块系统 结合你自定义的 struct。

绑定

Bind()结束函数允许你通过原始查询或者查询构造方式去绑定你自定义的struct或生成的struct。

// 自定义 struct 使用两个生成的 structs
type PilotAndJet struct {
  models.Pilot `boil:",bind"`
  models.Jet   `boil:",bind"`
}

var paj PilotAndJet
// 使用原始查询
err := queries.Raw(db, `
  select pilots.id as "pilots.id", pilots.name as "pilots.name",
  jets.id as "jets.id", jets.pilot_id as "jets.pilot_id",
  jets.age as "jets.age", jets.name as "jets.name", jets.color as "jets.color"
  from pilots inner join jets on jets.pilot_id=?`, 23,
).Bind(&paj)

// 使用查询构造
err := models.NewQuery(
  Select("pilots.id", "pilots.name", "jets.id", "jets.pilot_id", "jets.age", "jets.name", "jets.color"),
  From("pilots"),
  InnerJoin("jets on jets.pilot_id = pilots.id"),
).Bind(ctx, db, &paj)
// 自定义 struct 去查询一个子集
type JetInfo struct {
  AgeSum int `boil:"age_sum"`
  Count int `boil:"juicy_count"`
}

var info JetInfo

// 使用查询构造
err := models.NewQuery(Select("sum(age) as age_sum", "count(*) as juicy_count", From("jets"))).Bind(ctx, db, &info)

// 使用原始查询
err := queries.Raw(`select sum(age) as "age_sum", count(*) as "juicy_count" from jets`).Bind(ctx, db, &info)

Bind()的控制可以使用下面的struct tag 模式:

type CoolObject struct {
  // 如果没有指定名字,会使用列名的 TitleCase 进行匹配
  Frog int

  // 如果指定名字,会使用列名的 titlecased 进行匹配
  Specify an alternative name for the column, it will
  // be titlecased for matching, can be whatever you like.
  Cat int  `boil:"kitten"`

  // 忽略这个域,不绑定
  Pig int  `boil:"-"`

  // 不是用正常的方式进行绑定,像 sql-able strict 如 time.Time
  // 递归的搜索Dog struct 中的查询结果名
  Dog      `boil:",bind"`

  // 同上,除了指定一个不同的表名
  Mouse    `boil:"rodent,bind"`

  // 忽略这个域,不绑定
  Bird     `boil:"-"`
}

钩子

你可以通过钩子进行各种操作。如果你不想生成代码中包含钩子,可以使用 --no-hooks

你可以使用下面类型的钩子:

const (
  BeforeInsertHook HookPoint = iota + 1
  BeforeUpdateHook
  BeforeDeleteHook
  BeforeUpsertHook
  AfterInsertHook
  AfterSelectHook
  AfterUpdateHook
  AfterDeleteHook
  AfterUpsertHook
)

注册钩子需要你定义一个钩子函数并使用AddModelHook方法去挂载。

// 定义钩子函数
func myHook(ctx context.Context, exec boil.ContextExecutor, p *Pilot) error {
  // 自定义逻辑
  return nil
}

// 注册 BeforeInsert 钩子
models.AddPilotHook(boil.BeforeInsertHook, myHook)

跳过钩子

你可以使用 boil.SkipHooks 跳过当前上下文的钩子。

事务

事务的操作很简单:

tx, err := db.BeginTx(ctx, nil)
if err != nil {
  return err
}

users, _ := models.Pilots().All(ctx, tx)
users.DeleteAll(ctx, tx)

// Rollback 或 commit
tx.Commit()
tx.Rollback()

你也可以使用 boil.BeginTx() 通过全局数据源开启一个事务,它利用 boil.SetDB() 设置的数据源。

Debug 日志

Debug 日志会打印 SQL语句和使用的参数。可以通过全局变量开启Debug日志:

boil.DebugMode = true

// 可以选择设置输出源. 默认是 os.Stdout
fh, _ := os.Open("debug.txt")
boil.DebugWriter = fh

Select

Select 通过查询绑定和Find进行。

// 查询一个 pilot
pilot, err := models.Pilots(qm.Where("name=?", "Tim")).One(ctx, db)
// 类型安全方式
pilot, err := models.Pilots(models.PilotWhere.Name.EQ("Tim")).One(ctx, db)

// 查询指定的列
jets, err := models.Jets(qm.Select("age", "name")).All(ctx, db)
// 类型安全方式
jets, err := models.Jets(qm.Select(models.JetColumns.Age, models.JetColumns.Name)).All(ctx, db)

Find

用于通过主键查询一行。

// 查询所有列
pilot, err := models.FindPilot(ctx, db, 1)

// 查询选择的列
jet, err := models.FindJet(ctx, db, 1, "name", "color")

Insert

Insert 操作需要注意到的一点是怎么选择插入的列。你可以使用下面其中一个进行指定:boil.Inferboil.Whitelistboil.Blacklist,或 boil.Greylist

这些操作将会控制什么列会被插入数据库,和什么值将会从数据库返回到你的struct(默认值,自增主键,触发器)。在insert完成后你的 struct 将会被设置这些值。

如果你使用 Infer 进行推断,那么如果域是Go的零值并且域在数据库有一个默认值那么这个域不会被选中。请注意 sqlboiler 无法得知你数据库设置的默认值,因此Go零值是非常重要的(这经常导致 bool 类型的默认是 true 的列的问题)。如果要插入Go零值请使用 whitelistgreylist

方法行为
Infer智能推断插入的列
Whitelist只插入指定的列
Blacklist智能推断插入的列, 但指定的列不会被插入
Greylist智能推断插入的列, 但指定的列会被插入

**注意:**CreatedAt/UpdatedAt 不自动包含在 Whitelist中。

var p1 models.Pilot
p1.Name = "Larry"
err := p1.Insert(ctx, db, boil.Infer()) // 指定插入 name 列
// p1 的 ID 域被设置为1

var p2 models.Pilot
p2.Name = "Boris"
err := p2.Insert(ctx, db, boil.Infer()) //  指定插入 name 列
// p2 的 ID 域被设置为2

var p3 models.Pilot
p3.ID = 25
p3.Name = "Rupert"
err := p3.Insert(ctx, db, boil.Infer()) // 指定插入 id, name 列

var p4 models.Pilot
p4.ID = 0
p4.Name = "Nigel"
err := p4.Insert(ctx, db, boil.Whitelist("id", "name")) // 指定插入 id, name 列
// 注意:这里我们使用 whitelist, 否则 SQLBoiler 将会以为我们想使用自增主键,
// 因为ID = 0是Go的零值

Update

Update操作可以在一个struct,或者一个 slice或用结束函数。

更新操作在一个struct时可以使用 whitelist去指定要被更新的列。

方法行为
Infer智能推断更新的列
Whitelist只更新指定的列
Blacklist智能推断更新的列, 但指定的列不会被更新
Greylist智能推断更新的列, 但指定的列会被更新

**注意:**CreatedAt/UpdatedAt 不自动包含在 Whitelist中。

// 查询一个 pilot 并更新它的 name
pilot, _ := models.FindPilot(ctx, db, 1)
pilot.Name = "Neo"
rowsAff, err := pilot.Update(ctx, db, boil.Infer())

// 更新一个 slice pilots 的 name 为 "Smith"
pilots, _ := models.Pilots().All(ctx, db)
rowsAff, err := pilots.UpdateAll(ctx, db, models.M{"name": "Smith"})

// 更新所有 pilots 的 name 为 "Smith"
rowsAff, err := models.Pilots().UpdateAll(ctx, db, models.M{"name": "Smith"})

Delete

Delete操作可以在一个struct,或者一个 slice或用结束函数。

pilot, _ := models.FindPilot(db, 1)
// 删除 pilot
rowsAff, err := pilot.Delete(ctx, db)

// 删除所有 pilots
rowsAff, err := models.Pilots().DeleteAll(ctx, db)

// 删除一个 slice 的 pilots
pilots, _ := models.Pilots().All(ctx, db)
rowsAff, err := pilots.DeleteAll(ctx, db)

Upsert

Upsert允许你执行一个插入操作和一个可选的在记录存在时的更新操作。

var p1 models.Pilot
p1.ID = 5
p1.Name = "Gaben"

// INSERT INTO pilots ("id", "name") VALUES ($1, $2)
// ON DUPLICATE KEY UPDATE SET "name" = EXCLUDED."name"
err := p1.Upsert(ctx, db, boil.Whitelist("name"), boil.Infer())
  • MySQL 和 MSSQL
    • 通过设置 updateColumnsboil.None() 去指定在冲突时不进行update。

注意:upsert不是sql标准的一部分,sqlboiler不能保证一定会支持它。

Reload

如果你的对象因为某种原因和数据库不同步,你可以使用 ReloadReloadAll去通过附加到对象里的主键重新加载。

pilot, _ := models.FindPilot(ctx, db, 1)

// 对象因为某些原因变得不同步

// 从数据库刷新对象
err := pilot.Reload(ctx, db)

// 从数据库刷新所有对象
pilots, _ := models.Pilots().All(ctx, db)
err := pilots.ReloadAll(ctx, db)

Exists

jet, err := models.FindJet(ctx, db, 1)

// 检查 id为5的记录是否存在
exists, err := models.Pilots(Where("id=?", 5)).Exists(ctx, db)

Enums

如果你的数据库表使用了枚举类型sqlboiler会生成相应的产量去表示对应的值,你可以在你的查询中使用:

CREATE TABLE event_one (
  id     serial PRIMARY KEY NOT NULL,
  name   VARCHAR(255),
  day    ENUM('monday', 'tuesday', 'wednesday', 'thursday', 'friday') NOT NULL
);

会生成下面的代码:

const (
  WorkdayMonday    = "monday"
  WorkdayTuesday   = "tuesday"
  WorkdayWednesday = "wednesday"
  WorkdayThursday  = "thursday"
  WorkdayFriday    = "friday"
)

对于MySQL 生成的代码使用 表名 + 列名 + TitleCased的枚举值

Constants

models 包也包含一些 struct 表示所有的表、列名,也会生成类型安全的where 查询模块。

这些类型安全的标识符存放在:

  • models.TableNames.TableName
  • models.ModelColumns.ColumnName
  • models.ModelWhere.ColumnName.Operator

例如表名在 models.TableNames 下面:

// 生成的代码
var TableNames = struct {
  Messages  string
  Purchases string
}{
  Messages:  "messages",
  Purchases: "purchases",
}

// 用例
fmt.Println(models.TableNames.Messages)

列名在 models.{Model}Columns 下面:

// 生成的代码
var MessageColumns = struct {
  ID         string
  PurchaseID string
}{
  ID:         "id",
  PurchaseID: "purchase_id",
}

// 用例
fmt.Println(models.MessageColumns.ID)

where 语句在 models.{Model}Where.{Column}.{Operator}下面:

var MessageWhere = struct {
  ID       whereHelperint
  Text     whereHelperstring
}{
  ID:         whereHelperint{field: `id`},
  PurchaseID: whereHelperstring{field: `purchase_id`},
}

// 用例
models.Messages(models.MessageWhere.PurchaseID.EQ("hello"))