《Learning Go 第二版》入门实战系列 18:数据库框架—GORM
GORM是Go语言生态中最流行的ORM(对象关系映射)框架之一,它通过简洁优雅的API设计和强大的功能特性,为开发者提供了一种高效处理数据库操作的解决方案。
以下是GORM的主要功能特性:
- 多数据库支持与连接管理
GORM支持多种主流关系型数据库,包括MySQL、PostgreSQL、SQLite和SQL Server等。它提供了统一的接口操作不同数据库,开发者可以根据需要选择合适的数据库,而无需担心ORM的兼容性问题。GORM还支持连接池配置,可以优化数据库连接性能。 - 模型定义与自动迁移
GORM允许开发者通过定义Go结构体来映射数据库表,结构体中的字段对应表中的列。通过在结构体字段上添加标签来自定义主键、列名、类型、约束等属性。它还支持通过简单的约定自动生成或更新数据库表结构,简化了数据库结构的变更和升级。此外,GORM还预定义了结构体gorm.Model,包含ID、CreatedAt、UpdatedAt、DeletedAt等常用字段,可直接将其嵌入到结构体中,以快速构建常见模型。 - 强大的查询构建器
GORM提供了丰富的查询构建器,支持链式调用和灵活的组合。开发者可以通过简单的API实现复杂的数据库查询操作,如条件筛选、排序、分组、连接等。同时,它还支持预加载(Eager Loading)关联模型数据,提高查询效率。此外,GORM还支持执行原生SQL查询,满足特殊场景的需求。 - 关联关系处理
GORM支持定义和处理数据库表之间的复杂关联关系:一对一、一对多、多对多。 - 事务与钩子函数
提供完整的事务管理功能,包括手动和自动事务,确保数据库操作的原子性。同时,GORM支持在模型操作前后执行自定义的钩子函数(Hook),如BeforeCreate、AfterUpdate等,用于实现业务逻辑或数据校验。 - 其他实用特性
GORM还提供了上下文支持、日志记录、插件扩展、逻辑删除、批量插入与更新等其他实用特性。
GORM通过以上功能特性,显著提升了开发效率,降低了维护成本,使开发者能够专注于业务逻辑而非底层数据库交互细节。
18.1 快速开始
可以使用go get工具安装GORM包以及其对应的数据库驱动。例如,要使用GORM连接MySQL数据库,可以执行以下命令:
go get gorm.io/gorm
go get gorm.io/driver/mysql // 根据不同的数据库类型安装对应的驱动18.1.1 模型声明
GORM中的模型是通过Go结构体定义的,结构体将被映射到数据库中的表,结构体中的字段将通过结构体标签映射到数据库表中的列。这些结构体中可以包含原生类型、指针类型、类型别名,甚至可以包含实现了database/sql包中Scanner和Valuer接口的自定义类型。
例如,下面的Go结构体对应一个名为users的表:
type User struct {
ID uint // 标准的主键字段
Name string // 常规的字符串字段
Email *string // 字符串指针,指针类型表示允许为空值的字段
Age uint8 // 无符号8-bit integer
Birthday *time.Time // time.Time指针,指针类型表示允许为空值的字段
MemberNumber sql.NullString // 用sql.NullString处理可以为空的字符串
ActivatedAt sql.NullTime // 用sql.NullTime处理可以为空的time字段
CreatedAt time.Time // 由GORM自动管理的时间戳
UpdatedAt time.Time // 由GORM自动管理的时间戳
ignored string // 忽略此字段,不被GORM映射到数据库表
}在上述User模型中:
- 像
uint、string、uint8等原生类型,可以直接作为结构体字段类型。 - 如果字段类型是指针(如
*string、*time.Time),则表示该字段在数据库中可以存储NULL值。 database/sql包提供的sql.NullString、sql.NullTime等类型,也可用于表示可为空的字段,且比指针类型多一层“是否存在值”的判断。- GORM会自动识别名为
CreatedAt、UpdatedAt的字段,并在记录创建或更新时自动填充当前时间。
结构体中以小写字母开头的非导出字段(如ignored),GORM会忽略它们,不会在数据库中创建对应的列。
除了模型声明的基础功能(如字段类型映射、空值处理、自动时间填充等),GORM还支持通过serializer标签实现自定义序列化逻辑,这一特性极大提升了在数据库中读写数据的灵活性,尤其适合需要特殊处理的字段。
18.1.1.1 模型约定
- 主键(PrimaryKey):GORM默认使用名为
ID的结构体字段作为表的主键。也可以用标签primaryKey显式指定主键字段,将多个字段设置为主键会创建复合主键:
type Product struct {
ID string `gorm:"primaryKey"` // 复合主键的第一个字段
LanguageCode string `gorm:"primaryKey"` // 复合主键的第二个字段
Code string
Name string
}如果主键字段类型为整型(如int、uint等),GORM会默认将其配置为自增字段。可以使用标签autoIncrement:false禁用主键自增:
type Product struct {
CategoryID uint64 `gorm:"primaryKey;autoIncrement:false"`
TypeID uint64 `gorm:"primaryKey;autoIncrement:false"`
}- 表名(Table Names):默认将结构体名转为蛇形小写并复数化作为表名。例如,结构体
User对应表users,结构体GormUserName对应表gorm_user_names。通过实现Tabler接口,可以更改默认表名:
type User struct {
ID string // 默认将ID作为主键字段,表名默认为users。
Name string
}
func (User) TableName() string {
return "users"
}TableName方法不支持动态表名。如果要使用动态表名,则应使用Scopes方法:
// 定义一个基于用户角色动态选择数据表的作用域函数生成器
// 参数user为User类型实例,用于判断用户角色(是否为管理员)
// 返回值是一个作用域函数,该函数接收*gorm.DB并返回*gorm.DB,用于修改数据库操作的目标表
func UserTable(user User) func(tx *gorm.DB) *gorm.DB {
// 返回具体的作用域函数,根据用户是否为管理员动态切换数据表
return func(tx *gorm.DB) *gorm.DB {
if user.Admin { // 判断用户是否为管理员
return tx.Table("admin_users") // 若是管理员,操作"admin_users"表
}
return tx.Table("users") // 若不是管理员,操作默认的"users"表
}
}
// 使用Scopes方法应用UserTable作用域,动态选择数据表后执行创建操作
// 最终会根据user.Admin的值,将user数据插入到admin_users或users表中
db.Scopes(UserTable(user)).Create(&user)- 列名(Column Names):默认将结构体字段名转为蛇形小写作为数据库列名。例如,结构体字段
Birthday对应数据库列birthday、字段CreatedAt对应数据库列created_at。可通过column标签自定义数据库列名:
type User struct {
ID uint // 列名默认为id
Name string // 列名默认为name
Birthday time.Time // 列名默认为birthday
CreatedAt time.Time // 列名默认为created_at
}- 时间戳字段(Timestamp Fields):通过
CreatedAt和UpdatedAt字段自动跟踪记录的创建时间和更新时间。而DeletedAt自动跟踪记录的逻辑删除时间。可以通过标签autoCreateTime: false、autoUpdateTime: false和autoDeleteTime: false禁用自动填充。
18.1.1.2 gorm.Model
GORM预定义了一个名为gorm.Model的结构体,它包含了常用的字段:ID、CreatedAt、UpdatedAt、DeletedAt。通过嵌入此结构体,可以快速为模型添加常用字段:
// gorm.Model定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}如果模型有DeletedAt字段,调用Delete方法删除数据时,会将DeletedAt字段设置为当前时间,而不是直接物理删除记录。
18.1.1.3 字段级权限
使用GORM进行CRUD操作时,导出(exported)字段默认拥有所有权限。GORM允许通过标签更改字段级别的权限,例如,可以将字段设置为只读、只写、只建、只更新或忽略:
type User struct {
Name string `gorm:"<-:create"` // 允许读取、新建
Name string `gorm:"<-:update"` // 允许读取、更新
Name string `gorm:"<-:false"` // 允许读取,禁止写入
Name string `gorm:"->"` // 只读
Name string `gorm:"->;<-:create"` // 允许读取、新建
Name string `gorm:"->:false;<-:create"` // 仅允许新建(禁止读取)
Name string `gorm:"-"` // 用结构体进行写入与读取时,忽略此字段
Name string `gorm:"-:all"` // 用结构体进行写入、读取、迁移时,忽略此字段
Name string `gorm:"-:migration"` // 用结构体进行迁移时,忽略此字段
}18.1.1.4 时间戳字段
GORM默认约定了如下时间戳字段:
- CreatedAt:跟踪记录的创建时间。如果定义了此字段,GORM会在创建记录时自动填充当前时间。可通过标签(
autoCreateTime)禁用自动填充。 - UpdatedAt:跟踪记录的更新时间。如果定义了此字段,GORM会在更新记录时自动填充当前时间。可通过标签(
autoUpdateTime)禁用自动填充。 - DeletedAt:跟踪记录的逻辑删除时间。如果定义了此字段,GORM会在调用
Delete方法时将该字段设置为当前时间,而不是直接物理删除数据行。可通过标签(autoDeleteTime)禁用自动填充。
type User struct {
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt time.Time `gorm:"autoDeleteTime"`
}要使用不同名称的时间戳字段,可以使用标签autoCreateTime、autoUpdateTime、autoDeleteTime显式指定这些字段。如果更倾向于保存UNIX(毫秒/纳秒)时间戳而非time.Time,只需将字段的数据类型改为int即可。
type User struct {
Created int64 `gorm:"autoCreateTime"` // Use uint seconds as creating time
Updated int64 `gorm:"autoUpdateTime:nano"` // Use uint nano seconds as updating time
Deleted int64 `gorm:"autoDeleteTime:milli"` // Use uint milli seconds as deleting time
}18.1.1.5 嵌入结构体
对于匿名字段,GORM会将其字段包含到其父结构体中,例如:
type Author struct {
Name string
Email string
}
type Blog struct {
Author
ID int
Upvotes int32
}
// 等价于
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}对于普通的结构体字段,可以用embedded标签来嵌入结构体:
type Author struct {
Name string
Email string
}
type Blog struct {
ID int
Author Author `gorm:"embedded"`
Upvotes int32
}
// 等价于
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}并且可以使用标签embeddedPrefix为嵌入字段添加前缀,例如:
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}
// 等价于
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}18.1.1.6 字段标签
在声明模型时,标签的使用是可选的,GORM支持以下标签:
表 18.1: GORM 模型声明中的结构体字段标签
| 标签名称 | 描述说明 | 标签示例 |
|---|---|---|
column | 映射到表中的列名 | gorm:"column:pid" |
type | 列的数据类型,推荐用通用类型(如bool、int、uint、float、string、time、bytes)且可与其他标签(如not null、size、autoIncrement等)配合使用。也支持特定数据库类型(如varbinary(8)),但需指定完整类型(如:MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT) | gorm:"type:string" |
serializer | 指定数据序列化/反序列化方式,如:gob(Gob序列化)、json(JSON序列化)、unixtime(时间戳序列化) | gorm:"serializer:json" |
size | 指定列的数据长度,例如:size:256(长度为256) | gorm:"size:256" |
primaryKey | 指定列为主键 | gorm:"primaryKey" |
unique | 指定列为唯一键 | gorm:"unique" |
default | 指定列的默认值 | gorm:"default:18" |
precision | 指定列的精度(用于数值类型) | gorm:"precision:10" |
scale | 指定列的小数位数(用于数值类型) | gorm:"scale:2" |
not null | 指定列为非空(NOT NULL) | gorm:"not null" |
autoIncrement | 指定列为自增 | gorm:"autoIncrement" |
autoIncrementIncrement | 自增步长,控制连续列值之间的间隔 | gorm:"autoIncrementIncrement:2" |
embedded | 嵌入该字段(将结构体字段嵌入到父结构体中) | gorm:"embedded" |
embeddedPrefix | 为嵌入字段的数据库列名添加前缀 | gorm:"embeddedPrefix:author_" |
autoCreateTime | 创建行记录时自动跟踪当前时间。若字段为int类型,默认跟踪Unix秒级时间;可通过nano/milli指定纳秒/毫秒级。 | gorm:"autoCreateTime:nano" |
autoUpdateTime | 创建或更新记录时自动跟踪当前时间。若字段为int类型,默认跟踪Unix秒级时间;可通过nano/milli指定纳秒/毫秒。 | gorm:"autoUpdateTime:milli" |
index | 创建索引(可带选项),多个字段使用相同标签名称可创建复合索引 | gorm:"index" |
uniqueIndex | 与index类似,但创建唯一索引 | gorm:"uniqueIndex" |
check | 创建check约束,如:check:age > 13 | gorm:"check:age > 13" |
<- | 设置字段的写入权限:<-:create(仅创建时可写)、<-:update(仅更新时可写)、<-:false(禁止写入)、<-(创建和更新均可写) | gorm:"<-:create" |
-> | 设置字段的读取权限:->:false(禁止读取) | gorm:"->" |
- | 忽略该字段:-(禁止读写)、-:migration(迁移时忽略)、-:all(禁止读写和迁移) | gorm:"-" |
comment | 在迁移时为字段添加注释 | gorm:"comment:这是一个注释" |
标签不区分大小写,但推荐使用驼峰式(camelCase),多个标签应以分号(;)分隔。对于对解析器有特殊意义的字符,可使用反斜杠(\)进行转义,使其能作为参数值使用。
GORM允许通过关系标签(Association Tags)配置外键、约束、多对多等关联关系,详见官方文档。
18.1.2 连接数据库
GORM官方支持MySQL、PostgreSQL、GaussDB、SQLite、SQL Server和TiDB等数据库。
要连接某个类型的数据库,需要先安装相应的驱动包。例如,要连接MySQL数据库,则需要安装mysql驱动:
go get gorm.io/driver/mysql18.1.2.1 MySQL
package main
import (
"fmt"
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// MySQL数据库连接信息常量
const (
MySQLHost = "localhost" // 数据库主机地址
MySQLPort = "3306" // 数据库端口
MySQLUser = "root" // 数据库用户名
MySQLPassword = "your_password" // 数据库密码
MySQLDBName = "test_db" // 数据库名称
MySQLCharset = "utf8mb4" // 字符集(支持emoji需用utf8mb4)
)
// InitMySQL 初始化MySQL数据库连接
// 返回 *gorm.DB 和可能的错误
func InitMySQL() (*gorm.DB, error) {
// 构建DSN(数据源名称)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
MySQLUser, MySQLPassword, MySQLHost, MySQLPort, MySQLDBName, MySQLCharset)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("连接MySQL失败:%w", err)
}
return db, nil
}
// 主函数:测试数据库连接和操作
func main() {
// 初始化数据库连接
db, err := InitMySQL()
if err != nil {
log.Fatal("数据库初始化失败:", err)
}
fmt.Println("MySQL数据库连接成功")
}使用自定义驱动:
import (
"example.com/my_mysql_driver"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
db, err := gorm.Open(mysql.New(mysql.Config{
DriverName: "my_mysql_driver",
DSN: dsn, // data source name, 详情参考: https://github.com/go-sql-driver/mysql#dsn-data-source-name
}), &gorm.Config{})18.1.2.2 PostgreSQL
package main
import (
"fmt"
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// PostgreSQL数据库连接信息常量
const (
PGHost = "localhost" // 数据库主机地址
PGPort = "5432" // PostgreSQL默认端口
PGUser = "postgres" // 数据库用户名(赋入管理员用户)
PGPassword = "your_password" // 数据库密码
PGDBName = "test_db" // 数据库名称
PGSSLMode = "disable" // SSL模式
PGTimeZone = "Asia/Shanghai" // 时区设置
)
// InitPostgreSQL 初始化PostgreSQL数据库连接
// 返回*gorm.DB和可能的错误
func InitPostgreSQL() (*gorm.DB, error) {
// 构建PostgreSQL的DSN(数据源名称)
// PostgreSQL的DSN采用key=value键值对格式
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
PGHost, PGPort, PGUser, PGPassword, PGDBName, PGSSLMode, PGTimeZone)
// 连接数据库(使用postgres驱动)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("连接PostgreSQL失败:%w", err)
}
return db, nil
}
func main() {
// 初始化数据库连接
db, err := InitPostgreSQL()
if err != nil {
log.Fatal("数据库初始化失败:", err)
}
fmt.Println("PostgreSQL数据库连接成功")
}使用pgx作为postgres的database/sql驱动:
go get -u gorm.io/driver/postgres默认情况下,它会启用prepared statement缓存,可以这样禁用它:
// https://github.com/go-gorm/postgres
db, err := gorm.Open(postgres.New(postgres.Config{
DSN: dsn,
PreferSimpleProtocol: true, // 禁用prepared statement缓存
}), &gorm.Config{})使用自定义驱动:
import (
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres"
"gorm.io/gorm"
)
db, err := gorm.Open(postgres.New(postgres.Config{
DriverName: "cloudsqlpostgres",
DSN: dsn,
}), &gorm.Config{})18.1.2.3 GaussDB
package main
import (
"fmt"
"log"
"gorm.io/driver/gaussdb"
"gorm.io/gorm"
)
// GaussDB数据库连接信息常量
const (
GaussHost = "localhost" // 数据库主机地址
GaussPort = "5432" // GaussDB默认端口(兼容PostgreSQL协议)
GaussUser = "gaussdb" // 数据库用户名
GaussPassword = "your_password" // 数据库密码
GaussDBName = "test_db" // 数据库名称
GaussSSLMode = "disable" // SSL模式(开发环境通常关闭)
GaussTimeZone = "Asia/Shanghai" // 时区设置
)
// InitGaussDB 初始化GaussDB数据库连接
// 返回*gorm.DB和可能的错误
func InitGaussDB() (*gorm.DB, error) {
// 构建GaussDB的DSN(数据源名称)
// GaussDB兼容PostgreSQL协议,DSN格式与PostgreSQL类似
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
GaussHost, GaussPort, GaussUser, GaussPassword, GaussDBName, GaussSSLMode, GaussTimeZone)
// 连接数据库(使用gaussdb驱动)
db, err := gorm.Open(gaussdb.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("连接GaussDB失败:%w", err)
}
return db, nil
}
// 主函数:测试数据库连接和操作
func main() {
// 初始化数据库连接
db, err := InitGaussDB()
if err != nil {
log.Fatal("数据库初始化失败:", err)
}
fmt.Println("GaussDB数据库连接成功")
}使用gaussdb-go作为database/sql驱动:
go get -u gorm.io/driver/gaussdb默认情况下,它会启用prepared statement缓存,可以这样禁用它:
// https://github.com/go-gorm/gaussdb
db, err := gorm.Open(gaussdb.New(gaussdb.Config{
DSN: dsn,
PreferSimpleProtocol: true, // 禁用隐式prepared statement
}), &gorm.Config{})使用自定义驱动:
import (
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/gaussdb"
"gorm.io/gorm"
)
db, err := gorm.Open(gaussdb.New(gaussdb.Config{
DriverName: "cloudsqlgaussdb",
DSN: dsn,
}), &gorm.Config{})18.1.2.4 SQLite
import (
"gorm.io/driver/sqlite" // 基于CGO的SQLite驱动
// "github.com/glebarez/sqlite" // 纯Go实现的SQLite驱动,详情参考:https://github.com/glebarez/sqlite
"gorm.io/gorm"
)
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})可以使用file::memory:?cache=shared替代文件路径。这会告诉SQLite在系统内存中使用一个临时数据库。详见SQLite文档。
18.1.2.5 TiDB
TiDB兼容MySQL协议。因此可以按照“18.1.2.1 MySQL”一节来创建与TiDB的连接。在使用TiDB时,可以在结构体中使用gorm:"primaryKey;default:auto_random()"标签从而启用TiDB的AUTO RANDOM特性:
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
const (
TiDBHost = "localhost" // 数据库主机地址
TiDBPort = "4000" // TiDB默认端口
TiDBUser = "root" // 数据库用户名
TiDBPassword = "your_password" // 数据库密码
TiDBDBName = "test_db" // 数据库名称
TiDBCharset = "utf8mb4" // 字符集(支持emoji需用utf8mb4)
)
type Product struct {
ID uint `gorm:"primaryKey;default:auto_random()"` // 启用AUTO RANDOM特性
Code string
Price uint
}
func InitTiDB() (*gorm.DB, error) {
// 构建DSN(数据源名称)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
TiDBUser, TiDBPassword, TiDBHost, TiDBPort, TiDBDBName, TiDBCharset)
// 连接数据库
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("连接TiDB失败:%w", err)
}
return db, nil
}
func main() {
db, err := InitTiDB()
if err != nil {
panic("failed to connect database")
}
db.AutoMigrate(&Product{})
}18.1.2.6 ClickHouse
import (
"fmt"
"gorm.io/driver/clickhouse"
"gorm.io/gorm"
)
const (
CKHost = "localhost" // 数据库主机地址
CKPort = "9000" // 数据库端口
CKUser = "default" // 数据库用户名
CKPassword = "your_password" // 数据库密码
CKDBName = "test_db" // 数据库名称
)
func main() {
dsn := fmt.Sprintf("clickhouse://%s:%s@%s:%s/%s?dial_timeout=10s&read_timeout=20s",
CKUser, CKPassword, CKHost, CKPort, CKDBName)
db, err := gorm.Open(clickhouse.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 自动迁移(这是GORM自动创建表的一种方式—译者注)
db.AutoMigrate(&User{})
// 设置表选项
db.Set("gorm:table_options", "ENGINE=Distributed(cluster, default, hits)").AutoMigrate(&User{})
// 插入
user := User{Name: "John"}
db.Create(&user)
// 查询
var result User
db.First(&result, "id = ?", 10)
// 批量插入
var users = []User{user1, user2, user3}
db.Create(&users)
}18.1.2.7 连接池
GORM使用database/sql来维护连接池:
sqlDB, err := db.DB()
// SetMaxIdleConns设置空闲连接池中连接的最大数量。
sqlDB.SetMaxIdleConns(10)
// SetMaxOpenConns设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)
// SetConnMaxLifetime设置了可以重新使用连接的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)GORM提供了DB方法,该方法从当前的*gorm.DB返回一个通用数据库接口(即*sql.DB)。
18.2 增删改查操作
18.2.1 插入、更新数据
GORM为创建记录提供了两种接口:泛型API和传统API。
示例 18.1: 泛型 API
// 初始化单行记录
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
// 方式一:创建单行记录,不保存操作结果
ctx := context.Background()
// gorm.G[User](db)是泛型方法,指示要操作User模型对应的表。Create是执行插入操作的方法
err := gorm.G[User](db).Create(ctx, &user)
// 方式二:创建单行记录,保存操作结果
// gorm.Result对象,用于接收GORM操作的结果。
result := gorm.WithResult()
// gorm.G[User](db, result)是一个泛型方法,它将result作为参数传入,以便在后续操作中填充结果。Create方法执行插入操作,并将结果存储到result中。
err := gorm.G[User](db, result).Create(ctx, &user)
user.ID // 返回插入的主键值
result.Error // 返回操作的错误信息
result.RowsAffected // 返回影响的行数gorm.G[User](db)是GORM的泛型工具方法,通过指定模型User,返回一个绑定该模型的数据库操作对象(类似db.Model(&User{})的泛型版本),后续操作会默认关联User模型对应的表。Create(ctx, user)是用于向数据库插入一条记录的方法。第一个参数ctx是上下文(context.Context),用于控制请求超时、传递元数据等。第二个参数必须是数据的指针(&user),这是因为GORM需要通过指针修改原对象(例如自动填充ID、CreatedAt等字段),如果传值(user)会导致修改无法同步到原对象。gorm.WithResult()用于创建一个空的gorm.Result对象,为泛型方法gorm.G[User](db)传入gorm.Result对象,可将操作结果(如错误信息、影响的行数等)填充到result中。
示例 18.2: 传统API
// 初始化单行记录
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
// 方式一,插入单行记录,保存操作结果
result := db.Create(&user) // 为Create方法传入数据的指针,返回操作结果对象
user.ID // 返回插入的主键值
result.Error // 返回操作的错误信息
result.RowsAffected // 返回影响的行数
// 初始化多行记录
users := []*User{ // 创建包含多个User对象的切片
{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
{Name: "Jackson", Age: 19, Birthday: time.Now()},
}
// 方式二,批量插入多行记录,保存操作结果
result := db.Create(users) // 传入切片,插入多行数据
for _, user := range users { // 遍历切片,获取插入后的ID等信息
user.ID // 1,2,3
}
result.Error // 返回操作的错误信息
result.RowsAffected // 返回影响的行数- 直接调用
db.Create方法,传入数据的指针(&user)。该方法会返回操作结果对象(包含错误信息、影响行数等) - 创建包含多个
User对象的切片,并将切片传入db.Create方法,同样返回操作结果对象(包含错误信息、影响行数等)。
不能为Create方法直接传入结构体,而应传入结构体指针(如&user)。因为GORM需要通过指针修改原对象(如填充ID、CreatedAt等字段)。如果直接传值(即db.Create(user)),则会导致无法同步更新字段到原始数据中。
除了从结构体(或结构体切片)创建记录之外,GORM还支持从映射(map[string]interface{})或其切片([]map[string]interface{})创建记录。例如:
// 从`map[string]interface{}`创建单条记录
db.Model(&User{}).Create(map[string]interface{}{
"Name": "jinzhu",
"Age": 18,
})
// 从`[]map[string]interface{}`创建多条记录
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "jinzhu_1", "Age": 18},
{"Name": "jinzhu_2", "Age": 20},
})通过映射创建记录时,钩子(Hooks)方法不会被调用,关联关系不会被保存,主键值也不会回填。
最后,GORM允许使用SQL表达式插入数据,有两种方法可以实现这一目标:从map[string]interface{}创建或从自定义数据类型创建记录,例如:
// 方式一:从映射创建记录
db.Model(&User{}).Create(map[string]interface{}{
"Name": "jinzhu",
"Location": clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu", ST_PointFromText("POINT(100 100)"));
// 方式二:从自定义数据类型创建记录
type Location struct {
X, Y int
}
// 实现sql.Scanner接口的Scan方法
func (loc *Location) Scan(v interface{}) error {
// 从数据库驱动中扫描一个值到结构体中
return nil
}
func (loc Location) GormDataType() string {
return "geometry"
}
func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
return clause.Expr{
SQL: "ST_PointFromText(?)",
Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
}
}
type User struct {
Name string
Location Location
}
db.Create(&User{
Name: "jinzhu",
Location: Location{X: 100, Y: 100},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu", ST_PointFromText("POINT(100 100)"));18.2.1.1 批量插入
为了高效插入大量记录,可将切片传递给Create方法。GORM会生成一条SQL语句来批量插入所有数据并回填主键值,同时也会调用钩子方法。当记录可以分成多个批次时,GORM会开启一个事务。使用CreateInBatches批量创建记录时,还可以指定批次大小。
var users1 = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users1)
for _, user := range users1 {
user.ID // 1,2,3
}
var users2 = []User{}
for i := 0; i < 10000; i++ {
users2 = append(users2, User{Name: fmt.Sprintf("jinzhu_%d", i)})
}
// 指定每批100条进行批量插入
db.CreateInBatches(&users2, 100)在用Upsert创建记录和关联关系创建记录时,也支持批量插入。
在用CreateBatchSize选项初始化GORM后,所有的INSERT在创建记录和创建关联关系时都会遵循此选项。
// 用CreateBatchSize选项初始化GORM连接,设置批量大小为1000
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
CreateBatchSize: 1000, // 默认批量大小为1000
})
// 通过Session方法为特定会话设置批量大小,这将覆盖全局设置。
db := db.Session(&gorm.Session{CreateBatchSize: 1000})
// 定义一个包含5000个User对象的数组,每个User对象包含Name字段和一个Pet切片
users := [5000]User{}
for i := 0; i < 5000; i++ {
users[i] = User{
Name: fmt.Sprintf("jinzhu_%d", i),
Pets: []Pet{{Name: "pet1"}, {Name: "pet2"}, {Name: "pet3"}},
}
}
// 批量创建用户记录
// 自动将5000条记录分成5个批次(5000/1000=5)
// 对于每个用户的宠物,会创建15个批次(5000用户*3宠物/1000=15)
db.Create(&users)
// INSERT INTO users xxx (5 batches)
// INSERT INTO pets xxx (15 batches)18.2.1.2 插入部分字段
GORM允许创建一条记录,只为特定的字段赋值。也可以在创建记录时,忽略某些字段,例如:
// 只插入Name和Age字段,忽略其他字段(例如CreatedAt)
db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")
// 忽略Name、Age和CreatedAt字段,只插入其他字段
db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")18.2.1.3 级联插入
在创建一些带有关联关系的数据时,如果其关联关系的值不是零值,这些关联关系将被插入或更新,并且其Hooks方法也会被调用。例如:
// 定义CreditCard模型,表示用户的信用卡信息
type CreditCard struct {
gorm.Model // 内嵌gorm.Model,包含ID、CreatedAt、UpdatedAt、DeletedAt字段
Number string
UserID uint // 外键,关联User表的ID
}
// 定义User模型,表示用户信息
type User struct {
gorm.Model // 内嵌gorm.Model
Name string // 用户名
CreditCard CreditCard // 用户的信用卡,一对一关系
}
// 创建新用户及其关联的信用卡记录
db.Create(&User{
Name: "jinzhu", // 设置用户名
CreditCard: CreditCard{Number: "41111111111"}, // 初始化关联的信用卡
})可以使用Select、Omit跳过保存关联关系,例如:
// 只保存User记录,不保存CreditCard。
db.Omit("CreditCard").Create(&user)
// 跳过所有关联关系的保存,包括嵌套关联关系。
db.Omit(clause.Associations).Create(&user)18.2.1.4 字段默认值
可以使用default标签为字段指定默认值。这样,当插入数据库时,零值字段将使用默认值填充:
type User struct {
ID int64
Name string `gorm:"default:galeone"` // 默认值为'galeone'
Age int64 `gorm:"default:18"` // 默认值为'18'
}对于定义了默认值的字段,任何零值(如0、""、false)都不会被保存到数据库中。为了插入零值(Zero Value),可以使用指针类型或Scanner/Valuer接口类型,例如:
type User struct {
gorm.Model
Name string
Age *int `gorm:"default:18"`
Active sql.NullBool `gorm:"default:true"`
}如果希望在迁移时跳过默认值的定义,需要为具有默认值或虚拟/生成值的字段设置default标签。例如:
type User struct {
ID string `gorm:"default:uid_generate_v3()"` // db func
FileName string
LastName string
Age string
Name string
FullName string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname, ' ', lastname)); default:(-);"`
}18.2.1.5 Upsert支持
GORM为不同的数据库提供了兼容的Upsert支持:
import "gorm.io/gorm/clause"
// 冲突时不做任何操作(忽略冲突)
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
// 主键冲突时,更新指定列为默认值
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// 使用SQL表达式处理主键冲突
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)
// 主键冲突时,更新指定列为新值
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignColumns([]string{"name", "age"}),
}).Create(&users)
// 主键冲突时更新所有列(除主键和具有SQL函数默认值的列外)
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&users)更多详情,请参阅Advanced Query与Raw SQL and SQL Builder。
18.2.2 删除数据
在删除记录时,需要提供主键,否则将触发批量删除。
示例 18.3: 删除记录:泛型API
ctx := context.Background()
// 根据主键ID=10删除记录
err := gorm.G[Email](db).Where("id = ?", 10).Delete(ctx)
// DELETE from emails where id = 10;
// 根据多个条件删除记录(例如,ID=10且Name='jinzhu')
err := gorm.G[Email](db).Where("id = ? AND name = ?", 10, "jinzhu").Delete(ctx)
// DELETE from emails where id = 10 AND name = "jinzhu";
// 根据条件,批量删除记录(例如,所有包含"jinzhu"的Email)
err := gorm.G[Email](db).Where("email LIKE ?", "%jinzhu%").Delete(ctx)
// DELETE from emails where email LIKE "%jinzhu%";示例 18.4: 删除记录:传统API
// Email's ID is '10'
db.Delete(&email)
// DELETE from emails where id = 10;
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";
db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{})
// DELETE from emails where email LIKE "%jinzhu%";
db.Delete(&Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";为了高效地删除大量记录,请将包含主键的切片传递给Delete方法:
var users = []User{{ID: 1}, {ID: 2}, {ID: 3}}
db.Delete(&users)
// DELETE FROM users WHERE id IN (1,2,3);
db.Delete(&users, "name LIKE ?", "%jinzhu%")
// DELETE FROM users WHERE name LIKE "%jinzhu%" AND id IN (1,2,3);未指定任何条件的批量删除,称为“全局删除”。GORM默认不会执行全局删除,并且会返回ErrMissingWhereClause错误。
必须使用一些条件、使用原始SQL或启用AllowGlobalUpdate模式,例如:
ctx := context.Background()
// 将返回错误
err := gorm.G[User](db).Delete(ctx) // gorm.ErrMissingWhereClause
// 正常执行
err := gorm.G[User](db).Where("1 = 1").Delete(ctx)
// DELETE FROM `users` WHERE 1=1
db.Delete(&User{}).Error // gorm.ErrMissingWhereClause
db.Delete(&[]User{{Name: "jinzhu1"}, {Name: "jinzhu2"}}).Error // gorm.ErrMissingWhereClause
db.Where("1 = 1").Delete(&User{}) // DELETE FROM `users` WHERE 1=1
db.Exec("DELETE FROM users") // DELETE FROM users
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{}) // DELETE FROM users18.2.2.1 从删除的行返回数据
返回删除的数据,仅对支持此特性的数据库有效,例如:
// return all columns
var users []User
db.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING *
// users => []User{ID:1,Name:"jinzhu",Role:"admin",Salary:100}, {ID:2,Name: "jinzhu.2", Role:"admin", Salary: 1000}
// return specified columns
db.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING `name`, salary
// users => []User{ID:0,Name:"jinzhu",Role:"",Salary:100}, {ID:0,Name:"jinzhu.2", Role:"", Salary: 1000}18.2.2.2 逻辑删除
如果定义的模型包含gorm.DeletedAt字段(该字段包含在gorm.Model中),它将自动获得逻辑删除功能!调用Delete时,不会从数据库中物理删除记录,而是将DeletedAt的值设置为当前时间,使用常规的查询方法将无法找到这些数据。
示例 18.5: 逻辑删除
// user's ID is '111'
db.Delete(&user)
// UPDATE users SET deleted_at = '2013-10-29 10:23' WHERE id = 111;
// Batch Delete
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at = '2013-10-29 10:23' WHERE age = 20;
// Soft deleted records will be ignored when querying
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;若不想嵌入gorm.Model结构体,但又要启用逻辑删除特性。可以将逻辑删除字段定义为gorm.DeletedAt类型,如下所示:
type User struct {
ID int
Deleted gorm.DeletedAt
Name string
}用Unscoped方法可以获取所有记录(包含逻辑删除的记录),也可以物理删除记录。
// 获取所有记录(含逻辑删除的记录)
db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
// 物理删除记录
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;gorm.Model结构体中的DeletedAt字段默认使用*time.Time类型(指针类型的时间),用于存储逻辑删除的时间标记。如果需要其他数据格式来表示逻辑删除状态(如Unix时间戳、0/1标志等),可以通过引入gorm.io/plugins/soft_delete插件来实现。
import "gorm.io/plugins/soft_delete"
type User struct {
ID uint
Name string
DeletedAt soft_delete.DeletedAt // 用Unix时间戳作为逻辑删除标志
}
// 查询记录
// SELECT * FROM users WHERE deleted_at = 0;
// 执行逻辑删除
// UPDATE users SET deleted_at = /*current_unix_second*/ WHERE ID = 1;
type Person struct {
ID uint
Name string
DeletedAt soft_delete.DeletedAt `gorm:"softDelete:milli"` // 使用毫秒时间戳作为逻辑删除标志
}
// 查询记录
// SELECT * FROM persons WHERE deleted_at = 0;
// 执行逻辑删除
// UPDATE persons SET deleted_at = /*current_unix_milli_second_or_nano_second*/ WHERE ID = 1;
type Student struct {
ID uint
Name string
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"` // 使用0/1作为逻辑删除标志
}
// 查询记录
// SELECT * FROM students WHERE is_del = 0;
// 执行逻辑删除
// UPDATE students SET is_del = 1 WHERE ID = 1;
// 使用0/1标志(或Unix时间)标记数据删除状态,并保存删除时间
type User struct {
ID uint
Name string
DeletedAt time.Time
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,deletedAtField:DeletedAt"` // use '1'/'0'
// IsDel soft_delete.DeletedAt `gorm:"softDelete:,deletedAtField:DeletedAt"` // use 'unix second'
// IsDel soft_delete.DeletedAt `gorm:"softDelete:nano"`
}18.2.3 更新数据
18.2.3.1 保存所有字段
Save方法在更新数据库时,会保存(更新)所有字段。它是一个upsert(更新或插入)函数,具体逻辑如下:
- 当传入的对象没有主键时,
Save方法会执行新增(Create)操作。 - 当传入的对象有主键时,
Save方法会先尝试更新(Update)所有字段(相当于Select(*)的效果) - 若更新后受影响的行数为0(即未找到对应主键的记录),则自动转为新增(
Create)操作。
简单来说,Save能确保要么更新已有记录,要么新增一条记录。
需要注意:如果想避免“未匹配到记录时意外新增”的情况,应使用Select(*).Updates()替代Save。
在GORM的泛型API中,Save方法被有意移除,目的是避免模糊性和并发问题。建议改用Create或Updates方法。
示例 18.6: 保存所有字段
db.Save(&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`updated_at`) VALUES ("jinzhu", 100, "0000-00-00 00:00:00", "0000-00-00 00:00:00")
db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu", `age`=100, `birthday`="0000-00-00 00:00:00", `updated_at`='0000-00-00 00:00:00" WHERE id=118.2.3.2 更新单个列
当用Update方法更新单个列时,必须指定筛选条件,否则将引发ErrMissingWhereClause错误。这么做,是为了避免全局更新,导致意外修改大量数据。
示例 18.7: 更新单个列:泛型 API
ctx := context.Background()
// Update with conditions
err := gorm.G[User](db).Where("active = ?", true).Update(ctx, "name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
// Update with ID condition
err := gorm.G[User](db).Where("id = ?", 111).Update(ctx, "name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// Update with multiple conditions
err := gorm.G[User](db).Where("id = ? AND active = ?", 111, true).Update(ctx, "name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10 WHERE id=111 AND active=true;示例 18.8: 更新单个列:传统API
// Update with conditions
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true
// 如果user包含主键值,GORM会将该主键作为查询条件的一部分
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// Update with conditions and model value
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;在传统API中使用Model方法时若传入的模型实例包含主键值,GORM会自动将该主键作为查询条件的一部分。
18.2.3.3 更新多个列
Updates方法支持用结构体或映射(map[string]interface{})更新多个列。
示例 18.9: 更新多个列:泛型API
ctx := context.Background()
// Update attributes with `struct`, will only update non-zero fields
err := gorm.G[User](db).Where("id = ?", 111).Updates(ctx, User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at='2013-11-17 21:34:10' WHERE id=111;
// Update attributes with `map`
err := gorm.G[User](db).Where("id = ?", 111).Updates(ctx, map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;示例 18.10: 更新多个列,传统API
// Update attributes with `struct`, will only update non-zero fields
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at='2013-11-17 21:34:10' WHERE id=111;
// Update attributes with `map`
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;当使用结构体作为更新参数时,GORM默认只会更新结构体中非零值字段。例如,若结构体User{Name: "hello", Age: 0}中Age为0,则更新时只会修改Name字段,Age不会被更新。若需更新包括零值在内的所有指定字段,可改用map[string]interface{}作为参数,如map{"name": "hello", "age": 0},此时所有键值对都会被用于更新。或使用Select方法明确指定需要更新的字段,例如Select("Name", "Age"),即使字段为零值,也会强制更新。
18.2.3.4 更新特定字段
GORM中更新数据时对字段进行筛选的操作方式:
Select:用于指定只更新哪些字段。例如,Select("name", "age").Updates(...)表示只更新name和age字段,其他字段即便在更新数据中有值也不会被修改。Omit:用于指定忽略哪些字段(即不更新这些字段)。例如,Omit("role").Updates(...)表示更新时排除role字段,其他字段正常更新。
这两个方法可以灵活控制更新范围,避免误改不需要变动的字段,尤其在使用结构体更新(默认只更新非零值)或批量更新时很有用。
示例 18.11: 更新特定字段:泛型API
// Select all fields but omit Role (select all fields include zero value fields)
err := gorm.G[User](db).Where("id = ?", 111).Select("*").Omit("Role").Updates(ctx, User{Name: "jinzhu", Role: "admin", Age: 0})示例 18.12: 更新特定字段:传统 API
// Select with Map
// User's ID is 111
db.Model(&user).Select("name").Updates(map[string]interface{}{"name":"hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name":"hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
// Select with Struct (select zero value fields)
db.Model(&user).Select("Name","Age").Updates(User{Name:"new_name", Age:0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;
// Select all fields (select all fields include zero value fields)
db.Model(&user).Select("*").Updates(User{Name:"jinzhu", Role:"admin", Age:0})
// Select all fields but omit Role (select all fields include zero value fields)
db.Model(&user).Select("*").Omit("Role").Updates(User{Name:"jinzhu", Role:"admin", Age:0})18.2.3.5 用子查询更新
db.Model(&user).Update("company_name", db.Model(&Company{}).Select("name").Where("companies.id = users.company_id"))
// UPDATE "users" SET "company_name" = (SELECT name FROM companies WHERE companies.id = users.company_id);
db.Table("users as u").Where("name = ?", "jinzhu").Update("company_name", db.Table("companies as c").Select("name").Where("c.id = u.company_id"))
db.Table("users as u").Where("name = ?", "jinzhu").Updates(map[string]interface{}{"company_name": db.Table("companies as c").Select("name").Where("c.id = u.company_id")})18.2.4 查询数据
18.2.4.1 检索单个对象
GORM提供了First、Take、Last方法从数据库中检索单个对象,在查询数据时会添加LIMIT 1条件。如果没有找到记录,将会返回错误ErrRecordNotFound。
示例 18.13: 查询数据:泛型API
ctx := context.Background() // 创建上下文对象
// ①获取单条记录
// 按主键升序,取第一条记录
user, err := gorm.G[User](db).First(ctx)
// SELECT * FROM users ORDER BY id LIMIT 1;
// 按主键降序,取第一条记录
user, err := gorm.G[User](db).Last(ctx)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
// 不指定顺序,取第一条记录
user, err := gorm.G[User](db).Take(ctx)
// SELECT * FROM users LIMIT 1;
// 检查错误ErrRecordNotFound
errors.Is(err, gorm.ErrRecordNotFound)
// ②获取所有记录
users, err := gorm.G[User](db).Find(ctx)
// SELECT * FROM users;示例 18.14: 查询数据:传统API
// ①获取单条记录
// 按主键升序,取第一条记录
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
// 按主键降序,取第一条记录
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
// 不指定顺序,取第一条记录
db.Take(&user)
// SELECT * FROM users LIMIT 1;
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // 返回错误或nil
// 检查错误ErrRecordNotFound
errors.Is(result.Error, gorm.ErrRecordNotFound)
// ②获取所有记录
// 获取所有记录
result := db.Find(&users)
// SELECT * FROM users;
result.RowsAffected // 返回找到的记录数,equals `len(users)`
result.Error // 返回错误或nil如果想避免ErrRecordNotFound错误,可以像db.Limit(1).Find(&user)这样使用Find方法,Find方法同时接受结构体和切片数据。
在对单个对象使用Find时,若不设置限制条件(如db.Find(&user)),将会查询整张表,但仅返回第一个对象。这样,不但具有不确定性,性能也不佳。
First和Last方法将分别按照主键顺序查找第一条和最后一条记录。只有将指向目标结构体的指针作为参数传递给这些方法,或者使用db.Model()指定模型时,它们才会起作用。此外,如果相关模型没有定义主键,则模型将按第一个字段排序。
18.2.4.2 查询部分字段
示例 18.15: 查询部分字段
db.Select("name", "age").Find(&users)
// SELECT name, age FROM users;
db.Select([]string{"name", "age"}).Find(&users)
// SELECT name, age FROM users;
db.Table("users").Select("COALESCE(age,?)", 42).Rows()
// SELECT COALESCE(age, '42') FROM users;18.2.4.3 条件筛选
按主键筛选
示例 18.16: 按主键筛选:泛型API
ctx := context.Background()
// 用数值类型主键筛选(例如,整型主键)
user, err := gorm.G[User](db).Where("id = ?", 10).First(ctx)
// SELECT * FROM users WHERE id = 10;
// 用字符串类型主键筛选
user, err := gorm.G[User](db).Where("id = ?", "10").First(ctx)
// SELECT * FROM users WHERE id = 10;
// 使用多个主键筛选
users, err := gorm.G[User](db).Where("id IN ?", []int{1,2,3}).Find(ctx)
// SELECT * FROM users WHERE id IN (1,2,3);
// 如果主键类型为字符串(例如,像uuid)
user, err := gorm.G[User](db).Where("id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a").First(ctx)
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";示例 18.17: 按主键筛选:传统API
db.First(&user, 10) // SELECT * FROM users WHERE id = 10;
db.First(&user, "10") // SELECT * FROM users WHERE id = 10;
db.Find(&users, []int{1,2,3}) // SELECT * FROM users WHERE id IN (1,2,3);在GORM中,当目标对象包含主键值时,GORM会自动使用该主键作为查询条件:
var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;
var result User // 声明一个空的User变量用于接收结果
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;字符串条件
示例 18.18: 字符串条件
// 获取匹配的第一个记录
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// 获取匹配的所有记录
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';
// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu', 'jinzhu 2');
// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';
// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';
// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';如果目标对象的主键字段已经被赋值(非零值),那么后续的查询条件不会覆盖这个主键条件,而是会以AND的方式组合使用。
var user = User{ID: 10}
db.Where("id = ?", 20).First(&user)
// SELECT * FROM users WHERE id = 10 AND id = 20 ORDER BY id ASC LIMIT 1结构体、映射指定条件
示例 18.19: 结构体、映射指定条件
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20,21,22);使用结构体进行查询时,GORM只会使用非零值字段进行查询,这意味着如果字段值为0、""、false或其他零值,则不会用于构建查询条件,例如:
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";要在查询条件中包含零值,可以使用映射,它会将所有键值对作为查询条件,例如:
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;示例 18.20: NOT、OR条件
db.Not("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1;
// Not In
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
// Struct
db.Not(User{Name: "jinzhu", Age: 18}).First(&user)
// SELECT * FROM users WHERE name <> "jinzhu" AND age <> 18 ORDER BY id LIMIT 1;
// Not In slice of primary keys
db.Not([]int64{1,2,3}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);18.2.4.4 Order、Limit、Offset
示例 18.21: Order
db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;
// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;
db.Clauses(clause.OrderBy{
Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1,2,3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)示例 18.22: Limit、Offset
db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;
// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)
db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;
db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;
// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)18.2.4.5 Group By、Having、Distinct
示例 18.23: Group By、Having
type Result struct {
Date time.Time
Total int
}
db.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result)
// SELECT name, sum(age) as total FROM `users` WHERE name LIKE "group%" GROUP BY `name` LIMIT 1
db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result)
// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
defer rows.Close()
for rows.Next() { ... }
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) = ?", 100).Rows()
defer rows.Close()
for rows.Next() { ... }
type Result struct {
Date time.Time
Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)示例 18.24: Distinct
db.Distinct("name", "age").Order("name, age desc").Find(&results)示例 18.25: Joins
type Result struct {
Name string
Email string
}
db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id
rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {
...
}
db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)
// multiple joins with parameter
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)18.3 执行原生SQL查询
18.3.1 DB.Raw与DB.Exec方法执行原生SQL查询
DB.Raw与DB.Exec方法,均可用于执行原生SQL查询。但二者的使用方式有细微区别,具体如下:
示例 18.26: 执行原生SQL查询:泛型API
// ①用DB.Raw执行原生SQL查询
type Result struct {
ID int
Name string
Age int
}
// 获取单条记录,并保存到结构体中
result, err := gorm.G[Result](db).Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Find(context.Background())
// 获取单个字段值,并保存到原生类型中
age, err := gorm.G[int](db).Raw("SELECT SUM(age) FROM users WHERE title = ?", "admin").Find(context.Background())
// 获取多条记录,并保存到切片中
users, err := gorm.G[User](db).Raw("UPDATE users SET name = ? WHERE age = ? RETURNING id, name", "jinzhu", 20).Find(context.Background())
// ②用DB.Exec执行原生SQL查询
// Execute raw SQL
result := gorm.WithResult()
err := gorm.G[any](db, result).Exec(context.Background(), "DROP TABLE users")
// execute with parameters
err = gorm.G[any](db).Exec(context.Background(), "UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})示例 18.27: 执行原生SQL查询:传统API
// 用DB.Raw执行原生SQL查询
type Result struct {
ID int
Name string
Age int
}
var result Result // 查询单条记录,并保存到结构体中
db.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result)
db.Raw("SELECT id, name, age FROM users WHERE name = ?", "jinzhu").Scan(&result)
var age int // 查询单个字段值,并保存到原生类型中
db.Raw("SELECT SUM(age) FROM users WHERE role = ?", "admin").Scan(&age)
var users []User // 查询多条记录,并保存到切片中
db.Raw("UPDATE users SET name = ? WHERE age = ? RETURNING id, name", "jinzhu", 20).Scan(&users)
// 用DB.Exec执行原生SQL查询
db.Exec("DROP TABLE users")
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})
// Exec with SQL Expression
db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")GORM支持缓存预处理语句(Prepared Statement),以提升查询性能。详见Performance。
18.3.2 命名参数
GORM支持命名参数,可通过三种方式实现:
sql.NamedArg:如sql.Named("name", "jinzhu"),直接指定参数名和参数值,用于SQL语句中通过@name引用。map[string]interface{}:以键值对形式传递参数,例如map[string]interface{}{"name": "jinzhu2"},SQL中用@name对应键名获取值。- 结构体:定义包含对应字段的结构体(如
NamedArgument{Name: "jinzhu", Name2: "jinzhu2"}),SQL中通过@字段名(如@Name)引用结构体字段值。
这些方式让SQL语句中的参数更清晰,尤其在复杂查询中便于维护和理解参数与占位符的对应关系。
示例 18.28: 命名参数:泛型API
users, err := gorm.G[User](db).Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(context.Background())
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"
result3, err := gorm.G[User](db).Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu2"}).First(context.Background())
// SELECT * FROM `users` WHERE name1 = "jinzhu2" OR name2 = "jinzhu2" ORDER BY `users`.id LIMIT 1
// Named Argument with Raw SQL
users, err := gorm.G[User](db).Raw("SELECT * FROM users WHERE name1 = @name OR name2 = @name2 OR name3 = @name",
sql.Named("name", "jinzhu1"), sql.Named("name2", "jinzhu2")).Find(context.Background())
// SELECT * FROM users WHERE name1 = "jinzhu1" OR name2 = "jinzhu2" OR name3 = "jinzhu1"
err := gorm.G[any](db).Exec(context.Background(), "UPDATE users SET name1 = @name, name2 = @name2, name3 = @name",
sql.Named("name", "jinzhunew"), sql.Named("name2", "jinzhunew2"))
// UPDATE users SET name1 = "jinzhunew", name2 = "jinzhunew2", name3 = "jinzhunew"
users, err := gorm.G[User](db).Raw("SELECT * FROM users WHERE (name1 = @name AND name3 = @name) AND name2 = @name2",
map[string]interface{}{"name": "jinzhu", "name2": "jinzhu2"}).Find(context.Background())
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"
type NamedArgument struct {
Name string
Name2 string
}
users, err := gorm.G[User](db).Raw("SELECT * FROM users WHERE (name1 = @Name AND name3 = @Name) AND name2 = @Name2",
NamedArgument{Name: "jinzhu", Name2: "jinzhu2"}).Find(context.Background())
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"示例 18.29: 命名参数:传统API
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu2"}).First(&result3)
// SELECT * FROM `users` WHERE name1 = "jinzhu2" OR name2 = "jinzhu2" ORDER BY `users`.id LIMIT 1
// Named Argument with Raw SQL
db.Raw("SELECT * FROM users WHERE name1 = @name OR name2 = @name OR name3 = @name",
sql.Named("name", "jinzhu1"), sql.Named("name2", "jinzhu2")).Find(&user)
// SELECT * FROM users WHERE name1 = "jinzhu1" OR name2 = "jinzhu2" OR name3 = "jinzhu1"
db.Exec("UPDATE users SET name1 = @name, name2 = @name2, name3 = @name",
sql.Named("name", "jinzhunew"), sql.Named("name2", "jinzhunew2"))
// UPDATE users SET name1 = "jinzhunew", name2 = "jinzhunew2", name3 = "jinzhunew"
db.Raw("SELECT * FROM users WHERE (name1 = @name AND name3 = @name) AND name2 = @name2",
map[string]interface{}{"name": "jinzhu", "name2": "jinzhu2"}).Find(&user)
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"
type NamedArgument struct {
Name string
Name2 string
}
db.Raw("SELECT * FROM users WHERE (name1 = @Name AND name3 = @Name) AND name2 = @Name2",
NamedArgument{Name: "jinzhu", Name2: "jinzhu2"}).Find(&user)
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"18.3.3 Row、Rows
示例 18.30: 保存结果到Row或Rows:泛型API
// 保存结果为 *sql.Row
// Use GORM API build SQL
row := gorm.G[any](db).Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row(context.Background())
row.Scan(&name, &age)
// Use Raw SQL
row := gorm.G[any](db).Raw("select name, age, email from users where name = ?", "jinzhu").Row(context.Background())
row.Scan(&name, &age, &email)
// 保存结果为 *sql.Rows
// Use GORM API build SQL
rows, err := gorm.G[User](db).Where("name = ?", "jinzhu").Select("name", "age, email").Rows(context.Background())
defer rows.Close()
for rows.Next() {
rows.Scan(&name, &age, &email)
// do something
}
// Raw SQL
rows, err := gorm.G[any](db).Raw("select name, age, email from users where name = ?", "jinzhu").Rows(context.Background())
defer rows.Close()
for rows.Next() {
rows.Scan(&name, &age, &email)
// do something
}示例 18.31: 保存结果到Row或Rows:传统API
// 保存结果为 *sql.Row
// Use GORM API build SQL
row := db.Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row()
row.Scan(&name, &age)
// Use Raw SQL
row := db.Raw("select name, age, email from users where name = ?", "jinzhu").Row()
row.Scan(&name, &age, &email)
// 保存结果为 *sql.Rows
// Use GORM API build SQL
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name", "age", "email").Rows()
defer rows.Close()
for rows.Next() {
rows.Scan(&name, &age, &email)
// do something
}
// Raw SQL
rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows()
defer rows.Close()
for rows.Next() {
rows.Scan(&name, &age, &email)
// do something
}18.3.3.1 保存 *sql.Row 到结构体
ScanRows扫描一行数据到结构体,它类似于sql.Row的Scan方法。
*示例 18.32: 保存__sql.Row 到结构体
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name", "age", "email").Rows() // (*sql.Rows, error)
defer rows.Close()
var user User
for rows.Next() {
// ScanRows scan a row into user
db.ScanRows(rows, &user)
// do something
}18.4 GORM钩子函数
Hooks是在创建、查询、更新或删除之前或之后被调用的函数。如果为一个模型定义了特定的Hook函数,在创建、更新、查询或删除时,这些函数会自动被调用。如果任何回调返回错误,GORM将停止后续操作并回滚当前事务。Hook方法的类型为:func(*gorm.DB) error。
18.4.1 创建对象
示例 18.33: 创建对象时,可用的钩子函数
以下是GORM执行创建操作时的完整生命周期及事务流程:
- 开始数据库事务:启动一个数据库事务,确保后续操作的原子性
- 执行模型钩子函数(按顺序执行)
BeforeSave- 在保存操作前执行,可用于数据验证或预处理BeforeCreate- 在创建记录前执行,专门针对创建操作的预处理
- 保存关联数据(如果模型有关联关系):先保存所有设置了
save_before_associations标记的关联数据 - 执行核心数据库操作:将模型数据插入到数据库表中
- 保存剩余的关联数据:保存其他所有关联数据
- 执行后置钩子函数(按顺序执行)
AfterCreate- 记录创建成功后执行,可用于后处理或通知AfterSave- 保存操作完成后执行,无论创建还是更新都会触发
- 根据操作结果提交或回滚事务:如果所有操作成功,则提交事务,否则回滚所有更改
示例 18.34: 创建对象钩子函数示例
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.UUID = uuid.New()
if !u.IsValid() {
err = errors.New("can't save invalid data")
}
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.ID == 1 {
tx.Model(u).Update("role", "admin")
}
return
}18.4.2 更新对象
示例 18.35: 更新对象时,可用的钩子函数
以下是GORM执行更新操作时的完整生命周期及事务流程:
- 开始数据库事务:启动一个数据库事务,确保后续操作的原子性
- 执行模型钩子函数(按顺序执行)
BeforeSaveBeforeUpdate
- 保存关联数据
- 更新数据库
- 保存剩余的关联数据:保存其他所有关联数据
- 执行后置钩子函数(按顺序执行)
AfterUpdateAfterSave
- 根据操作结果提交或回滚事务
示例 18.36: 更新对象钩子函数示例
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
if u.readonly() {
err = errors.New("read only user")
}
return
}18.4.3 删除对象
示例 18.37: 删除对象时,可用的钩子函数
- 开始数据库事务:启动一个数据库事务,确保后续操作的原子性
BeforeDelete- 从数据库中删除数据
AfterDelete- 根据操作结果提交或回滚事务
示例 18.38: 删除对象钩子函数示例
// Updating data in same transaction
func (u *User) AfterDelete(tx *gorm.DB) (err error) {
if u.Confirmed {
tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("invalid", false)
}
return
}18.4.4 查询对象
- 从数据库加载数据
- 预加载(eager loading)
AfterFind
func (u *User) AfterFind(tx *gorm.DB) (err error) {
if u.Membership == "" {
u.Membership = "user"
}
return
}18.5 高级特性
18.5.1 上下文(Context)支持
GORM的上下文支持是一项强大的功能,它增强了Go应用程序中数据库操作的灵活性和可控性。它允许在不同的操作模式、超时设置中进行上下文管理,甚至可以集成到钩子/回调函数和中间件中。
18.5.1.1 单会话模式
单会话模式适用于执行单个操作。它确保特定操作在上下文范围内执行,从而实现更好的控制和监控。使用泛型API时,需将上下文作为第一个参数,直接传递给操作方法:
示例 18.39: 单会话模式示例:泛型API
users, err := gorm.G[User](db).Find(ctx)使用传统API时,通过WithContext方法传递上下文:
示例 18.40: 单会话模式示例:传统API
db.WithContext(ctx).Find(&users)18.5.1.2 连续会话模式
连续会话模式适合执行一系列相关操作。连续会话模式会在这些操作之间保持上下文,这在诸如事务处理等场景中特别有用。
示例 18.41: 连续会话模式示例
tx := db.WithContext(ctx)
tx.First(&user, 1)
tx.Model(&user).Update("role", "admin")18.5.1.3 上下文超时
在上下文中设置超时,可为长时间运行的数据库查询设定时长限制,一旦超过设定时间,操作会被终止。可以避免长时间运行的查询占用资源,防止因数据库响应缓慢导致的应用阻塞。
示例 18.42: 设置上下文超时:泛型API
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
users, err := gorm.G[User](db).Find(ctx)示例 18.43: 设置上下文超时:传统API
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
db.WithContext(ctx).Find(&users)18.5.1.4 回调函数中使用上下文
GORM的钩子/回调函数内部可以访问上下文,这使得在数据库操作的生命周期事件(如创建、更新前/后等)中,能够利用上下文中的信息(如超时设置、请求ID等)。在钩子/回调函数中,可通过tx.Statement.Context字段获取上下文。例如在BeforeCreate钩子中,通过ctx := tx.Statement.Context即可拿到上下文对象,进而使用其中的信息。
示例 18.44: 在回调函数中访问上下文
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
ctx := tx.Statement.Context
// 使用上下文中的信息
return
}18.5.2 自动schema迁移
AutoMigrate可自动同步schema(如表结构、约束、索引等),以保持schema为最新状态。
AutoMigrate会修改数据库结构,如建表、建约束、建索引、增加列等。在生产环境中使用时需谨慎,建议在开发或测试环境使用AutoMigrate,而在生产环境手动执行迁移操作。
如果现有列的大小、精度发生变化,或者从不可为空变为可为空,AutoMigrate将更改现有列的类型。为保护数据,AutoMigrate不会删除未使用的列。
示例 18.45: 自动迁移示例
db.AutoMigrate(&User{})
db.AutoMigrate(&User{}, &Product{}, &Order{})
// 创建表时添加表后缀
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})AutoMigrate会自动创建数据库外键约束,可以在初始化时禁用外键约束的自动迁移:
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})18.5.2.1 Migrator接口
GORM提供了一个Migrator接口,为不同类型数据库提供了统一的迁移接口,可用于构建数据库无关的schema迁移。
示例 18.46: 用migrator接口操作数据库、表
// ①操作数据库
// 返回当前使用的数据库名称
db.Migrator().CurrentDatabase()
// ②操作表
// 创建表
db.Migrator().CreateTable(&User{})
// 为表设置存储引擎ENGINE=InnoDB
db.Set("gorm:table_options", "ENGINE=InnoDB")
// 检查表存在与否
db.Migrator().HasTable(&User{})
db.Migrator().HasTable("users")
// 删除表(将忽略或删除外键约束)
db.Migrator().DropTable(&User{})
db.Migrator().DropTable("users")
// 重命名表
db.Migrator().RenameTable(&User{}, &UserInfo{})
db.Migrator().RenameTable("users", "user_infos")示例 18.47: 用Migrator接口操作表中的列
type User struct {
Name string
}
// 为表添加列
db.Migrator().AddColumn(&User{}, "Name")
// 从表中删除列
db.Migrator().DropColumn(&User{}, "Name")
// 修改表中的列
db.Migrator().AlterColumn(&User{}, "Name")
// 检查列存在与否
db.Migrator().HasColumn(&User{}, "Name")
type User struct {
Name string
NewName string
}
// 修改列名
db.Migrator().RenameColumn(&User{}, "Name", "NewName")
db.Migrator().RenameColumn(&User{}, "name", "new_name")
// 修改列类型
db.Migrator().ColumnTypes(&User{}) ([]gorm.ColumnType, error)
type ColumnType interface {
Name() string
DatabaseTypeName() string // varchar
ColumnType() (columnType string, ok bool) // varchar(
PrimaryKey() (isPrimaryKey bool, ok bool)
AutoIncrement() (isAutoIncrement bool, ok bool)
Length() (length int64, ok bool)
DecimalSize() (precision int64, scale int64, ok bool)
Nullable() (nullable bool, ok bool)
Unique() (unique bool, ok bool)
ScanType() reflect.Type
Comment() (value string, ok bool)
DefaultValue() (value string, ok bool)
}GORM内置了一个ViewOption结构体(如下所示),可用于操作视图。
type ViewOption struct {
Replace bool // 必需,为true,则执行`CREATE OR REPLACE`;为false,则执行`CREATE`
CheckOption string // 可选,将被附加到SQL语句中,例如`WITH LOCAL CHECK OPTION`
Query *DB // 必需,一个子查询
}示例 18.48: 用CreateView与ViewOption操作视图
// 创建子查询,作为视图的基础查询语句
query := db.Model(&User{}).Where("age > ?", 20)
// 创建视图
db.Migrator().CreateView("users_view", gorm.ViewOption{Query: query})
// CREATE VIEW `users_view` AS SELECT * FROM `users` WHERE age > 20
// Create or Replace View
db.Migrator().CreateView("users_view", gorm.ViewOption{Query: query, Replace: true})
// CREATE OR REPLACE VIEW `users_view` AS SELECT * FROM `users` WHERE age > 20
// Create View With Check Option
db.Migrator().CreateView("users_view", gorm.ViewOption{Query: query, CheckOption: "WITH CHECK OPTION"})
// CREATE VIEW `users_view` AS SELECT * FROM `users` WHERE age > 20 WITH CHECK OPTION
// 删除视图
db.Migrator().DropView("users_view")
// DROP VIEW IF EXISTS "users_view"示例 18.49: 用Migrator接口操作约束、外键
// 操作约束
type UserIndex struct {
Name string `gorm:"check:name_checker,name <> 'jinzhu'"`
}
// Create constraint
db.Migrator().CreateConstraint(&User{}, "name_checker")
// Drop constraint
db.Migrator().DropConstraint(&User{}, "name_checker")
// Check constraint exists
db.Migrator().HasConstraint(&User{}, "name_checker")
// 为关联关系创建外键
type User struct {
gorm.Model
CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
// create database foreign key for user & credit cards
db.Migrator().CreateConstraint(&User{}, "CreditCards")
db.Migrator().CreateConstraint(&User{}, "fk_users_credit_cards")
// ALTER TABLE `credit_cards` ADD CONSTRAINT `fk_users_credit_cards` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
// check database foreign key for user & credit cards exists or not
db.Migrator().HasConstraint(&User{}, "CreditCards")
db.Migrator().HasConstraint(&User{}, "fk_users_credit_cards")
// drop database foreign key for user & credit cards
db.Migrator().DropConstraint(&User{}, "CreditCards")
db.Migrator().DropConstraint(&User{}, "fk_users_credit_cards")示例 18.50: 用Migrator接口操作索引
type User struct {
gorm.Model
Name string `gorm:"size:255;index:idx_name,unique"`
}
// Create index for Name field
db.Migrator().CreateIndex(&User{}, "Name")
db.Migrator().CreateIndex(&User{}, "idx_name")
// Drop index for Name field
db.Migrator().DropIndex(&User{}, "Name")
db.Migrator().DropIndex(&User{}, "idx_name")
// Check Index exists
db.Migrator().HasIndex(&User{}, "Name")
db.Migrator().HasIndex(&User{}, "idx_name")
type User struct {
gorm.Model
Name string `gorm:"size:255;index:idx_name,unique"`
Name2 string `gorm:"size:255;index:idx_name_2,unique"`
}
db.Migrator().RenameIndex(&User{}, "Name", "Name2")
db.Migrator().RenameIndex(&User{}, "idx_name", "idx_name_2")18.5.2.2 迁移工具集成
Atlas是一款开源数据库迁移工具,与GORM有官方集成。GORM的AutoMigrate功能在多数情况下适用,但在某些场景下,用户可能需要对数据库schema的变更进行版本管理。一旦采用版本化管理,规划迁移脚本以及确保脚本与GORM运行时预期一致的责任,就转移到了开发者身上。通过官方的GORM Provider,Atlas能为开发者自动规划数据库schema迁移。配置好该Provider后,运行如下命令即可自动生成迁移计划。
atlas migrate diff --env gorm若想了解如何将Atlas与GORM结合使用,可参考Atlas官方文档。
18.5.3 会话模式
GORM提供的Session方法是一种新建会话的方式,它允许通过配置Session结构体来创建新的会话模式。
示例 18.51: Session结构体定义
// Session configuration
type Session struct {
DryRun bool // 为true时,只生成SQL,而不执行
PrepareStmt bool // 为true时,启用SQL预编译(Prepared Statements)
NewDB bool // 为true时,创建一个不带任何条件的新DB连接实例
Initialized bool // 为true时,创建轻量DB实例(无方法链式调用安全性、并发安全性)
SkipHooks bool // 为true时,忽略钩子(Hooks)函数
SkipDefaultTransaction bool
DisableNestedTransaction bool // 为true时,禁用嵌套事务
AllowGlobalUpdate bool // 为true时,允许执行全局(即无where条件)的更新/删除
FullSaveAssociations bool // 为true时,保存关联时,会保存所有有关联的记录(即启用级联更新)
QueryFields bool // 为true时,SQL语句将列出所有字段,而不是SELECT *
Context context.Context // 为后续的SQL操作设置上下文
Logger logger.Interface // 设置自定义的日志记录器
NowFunc func() time.Time // 设置自定义的Now函数,用于获取当前时间
CreateBatchSize int // 设置批量插入的记录数
}18.5.3.1 生成SQL,而不执行
开启DryRun模式后,GORM会生成SQL语句,但不会实际执行到数据库中。主要用于预览或测试生成的SQL是否符合预期,方便调试或验证SQL逻辑。
示例 18.52: 开启DryRun模式
// session mode
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = $1 ORDER BY `id`
stmt.Vars // => [interface{}{1}]
// globally mode with DryRun
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{DryRun: true})
// different databases generate different SQL
stmt := db.Find(&user, 1).Statement
stmt.SQL.String() // => SELECT * FROM `users` WHERE id = $1 // PostgreSQL
stmt.SQL.String() // => SELECT * FROM `users` WHERE id = ? // MySQL
stmt.Vars // => [interface{}{1}]若要生成最终的SQL语句,可以使用以下代码:
// 注意:生成的SQL并不总是安全可执行的,GORM仅将其用于日志记录,它可能会导致SQL注入问题
db.Dialector.Explain(stmt.SQL.String(), stmt.Vars...)
// SELECT * FROM `users` WHERE `id` = 118.5.3.2 PrepareStmt
在执行任何SQL时创建预编译语句(prepared statements),并对这些语句进行缓存。缓存的预编译语句可在后续调用中复用,从而加快执行速度(避免重复解析和编译SQL的开销)。
// 全局开启PrepareStmt,所有数据库操作前会预编译SQL,并缓存它们
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{PrepareStmt: true})
// 会话模式开启PrepareStmt,仅对当前会话有效
tx := db.Session(&gorm.Session{PrepareStmt: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
// returns prepared statements manager
stmtManager, ok := tx.ConnPool.(*PreparedStmtDB)
// 关闭当前会话的预编译语句
stmtManager.Close()
// prepared SQL for *current session*
stmtManager.PreparedSQL // => []string{}
// prepared statements for current database connection pool (all sessions)
stmtManager.Stmts // map[string]*sql.Stmt
for sql, stmt := range stmtManager.Stmts {
sql // prepared SQL
stmt // prepared statement
stmt.Close() // close the prepared statement
}18.5.3.3 NewDB
创建一个不带任何where条件的新数据库会话。
tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{NewDB: true}) // ① 创建不带任何条件的新会话
tx.First(&user) // ② 不受原始会话的 `Where("name = ?", "jinzhu")` 条件影响
// SELECT * FROM users ORDER BY id LIMIT 1
tx.First(&user, "id = ?", 10)
// SELECT * FROM users WHERE id = 10 ORDER BY id
// Without option `NewDB`
tx2 := db.Where("name = ?", "jinzhu").Session(&gorm.Session{}) // ③ 无NewDB选项,并指定where条件
tx2.First(&user) // ④ 查询受原始会话的where条件影响
// SELECT * FROM users WHERE name = "jinzhu" ORDER BY id① 即使原始db实例带有Where("name = ?", "jinzhu")查询条件,通过NewDB: true创建的新会话tx也会忽略该条件。
② 因此,后续执行的tx.First(&user)与tx.First(&user, "id = ?", 10)操作,都不会受到原始db中Where条件的影响,而是基于无预设条件的状态执行。
③ 相比之下,如果不使用NewDB: true,则在原始db会话上设置的Where条件会影响后续的所有查询操作。
④ 因此,执行的tx2.First(&user)操作会包含原始的name = "jinzhu"查询条件。
18.5.3.4 忽略回调函数
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)
db.Session(&gorm.Session{SkipHooks: true}).Create(&users)
db.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(&users, 100)
db.Session(&gorm.Session{SkipHooks: true}).Find(&user)
db.Session(&gorm.Session{SkipHooks: true}).Delete(&user)18.5.3.5 禁用嵌套事务
在数据库事务中使用Transaction方法时,GORM会使用SavePoint(savedPointName)、RollbackTo(savedPointName)来提供嵌套事务支持。可以通过使用DisableNestedTransaction选项来禁用嵌套事务的支持:
示例 18.53: 禁用嵌套事务
db.Session(&gorm.Session{
DisableNestedTransaction: true,
}).CreateInBatches(&users, 100)18.5.3.6 启用全局更新/删除
默认情况下,GORM不允许全局更新/删除操作,会返回ErrMissingWhereClause错误。可以将此选项设置为true来启用它。
示例 18.54: 启用全局更新/删除操作
db.Session(&gorm.Session{
AllowGlobalUpdate: true,
}).Model(&User{}).Update("name", "jinzhu")
// UPDATE users SET `name` = "jinzhu"18.5.3.7 启用级联更新
在创建/更新记录时,GORM会使用Upsert自动保存关联关系及其引用。如果想更新关联数据(即级联更新),应该使用FullSaveAssociations模式:
示例 18.55: 使用FullSaveAssociations模式更新数据
db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&user)
// ...
// INSERT INTO "addresses" ("address1") VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY UPDATE address1=VALUES(address1);
// INSERT INTO "users" ("name","billing_address_id","shipping_address_id") VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" ("user_id", "email") VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY UPDATE email=VALUES(email);18.5.3.8 上下文(Context)
通过Context选项,可以为会话中后续的SQL操作指定上下文:
示例 18.56: 为会话设置上下文(Context)
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
tx := db.Session(&gorm.Session{Context: timeoutCtx})GORM为设置上下文提供了一个快捷方法WithContext,其定义如下:
示例 18.57: WithContext方法的定义
func (db *DB) WithContext(ctx context.Context) *DB {
return db.Session(&gorm.Session{Context: ctx})
}18.5.3.9 自定义日志记录器
GORM允许通过Logger选项初始化自定义日志记录器,用于替换默认的日志输出。例如:
示例 18.58: 指定自定义的日志记录器
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Silent,
Colorful: false,
},
)
db.Session(&gorm.Session{Logger: newLogger})
db.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)})18.5.3.10 NowFunc
通过设置NowFunc选项,可以更改GORM获取当前时间的函数:
示例 18.59: 更改GORM获取当前时间的函数
db.Session(&gorm.Session{
NowFunc: func() time.Time {
return time.Now().Local()
},
})18.5.3.11 返回字段列表(而不是*)
通过QueryFields选项,可以更改GORM在查询记录时使用字段列表(而不是SELECT *):
db.Session(&gorm.Session{QueryFields: true}).Find(&user)
// SELECT `users`.`name`, `users`.`age`, ... FROM `users` // with this option
// SELECT * FROM `users` // without this option18.5.3.12 指定批量操作的批次大小
通过CreateBatchSize选项,可以更改GORM在批量创建记录时使用的批次大小:
示例 18.60: 更改GORM在批量创建记录时使用的批次大小
users := [5000]User{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}
for i := range users {
users[i].Name = fmt.Sprintf("jinzhu_%d", i)
}
db.Session(&gorm.Session{CreateBatchSize: 1000}).Create(&users)
// INSERT INTO users xxx (5 batches)
// INSERT INTO pets xxx (15 batches)参考链接:
