Gorm

寒江蓑笠翁大约 61 分钟

Gorm

官方文档:GORM - The fantastic ORM library for Golang, aims to be developer friendly.open in new window

开源仓库:go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly (github.com)open in new window


在go社区中,对于数据库交互这一块,有两派人,一派人更喜欢简洁的sqlx这一类的库,功能并不那么强大但是自己可以时时刻刻把控sql,性能优化到极致。另一派人喜欢为了开发效率而生的ORM,可以省去开发过程中许多不必要的麻烦。而提到ORM,在go语言社区中就绝对绕不开gorm,它是一个非常老牌的ORM,与之类似的还有相对比较年轻的xorment等。这篇文章讲的就是关于gorm的内容,本文只是对它的基础入门内容做一个讲解,权当是抛砖引玉,想要了解更深的细节可以阅读官方文档,它的中文文档已经相当完善了,并且笔者也是gorm文档的翻译人员之一。

特点

  • 全功能 ORM
  • 关联 (拥有一个,拥有多个,属于,多对多,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 Preload、Joins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To to Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • 自动迁移
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

gorm当然也有一些缺点,比如几乎所有的方法参数都是空接口类型,不去看文档恐怕根本就不知道到底该传什么参数,有时候可以传结构体,有时候可以传字符串,有时候可以传map,有时候可以传切片,语义比较模糊,并且很多情况还是需要自己手写SQL。

作为替代的有两个orm可以试一试,第一个是aorm,刚开源不久,它不再需要去自己手写表的字段名,大多情况下都是链式操作,基于反射实现,由于star数目不多,可以再观望下。第二个就是ent,是facebook开源的orm,它同样支持链式操作,并且大多数情况下不需要自己去手写SQL,它的设计理念上是基于图(数据结构里面的那个图),实现上基于代码生成而非反射(比较认同这个),但是文档是全英文的,有一定的上手门槛。

安装

安装gorm库

$ go get -u gorm.io/gorm

连接

gorm目前支持以下几种数据库

  • MySQL :"gorm.io/driver/mysql"
  • PostgreSQL: "gorm.io/driver/postgres"
  • SQLite:"gorm.io/driver/sqlite"
  • SQL Server:"gorm.io/driver/sqlserver"
  • TIDB:"gorm.io/driver/mysql",TIDB兼容mysql协议
  • ClickHouse:"gorm.io/driver/clickhouse"

除此之外,还有一些其它的数据库驱动是由第三方开发者提供的,比如oracle的驱动CengSin/oracleopen in new window。本文接下来将使用MySQL来进行演示,使用的什么数据库,就需要安装什么驱动,这里安装Mysql的gorm驱动。

$ go get -u gorm.io/driver/mysql

然后使用dsn(data source name)连接到数据库,驱动库会自行将dsn解析为对应的配置

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"log/slog"
)

func main() {
	dsn := "root:123456@tcp(192.168.48.138:3306)/hello?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn))
	if err != nil {
		slog.Error("db connect error", err)
	}
	slog.Info("db connect success")
}

或者手动传入配置

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"log/slog"
)

func main() {
	db, err := gorm.Open(mysql.New(mysql.Config{}))
	if err != nil {
		slog.Error("db connect error", err)
	}
	slog.Info("db connect success")
}

两种方法都是等价的,看自己使用习惯。

连接配置

通过传入gorm.Config配置结构体,我们可以控制gorm的一些行为

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

下面是一些简单的解释,使用时可以根据自己的需求来进行配置。

type Config struct {
	// 禁用默认事务,gorm在单个创建和更新时都会开启事务以保持数据一致性
	SkipDefaultTransaction bool
	// 自定义的命名策略
	NamingStrategy schema.Namer
	// 保存完整的关联
	FullSaveAssociations bool
	// 自定义logger
	Logger logger.Interface
	// 自定义nowfunc,用于注入CreatedAt和UpdatedAt字段
	NowFunc func() time.Time
	// 只生成sql不执行
	DryRun bool
	// 使用预编译语句
	PrepareStmt bool
	// 建立连接后,ping一下数据库
	DisableAutomaticPing bool
	// 在迁移数据库时忽略外键
	DisableForeignKeyConstraintWhenMigrating bool
	// 在迁移数据库时忽略关联引用
	IgnoreRelationshipsWhenMigrating bool
	// 禁用嵌套事务
	DisableNestedTransaction bool
	// 运行全局更新,就是不加where的update
	AllowGlobalUpdate bool
	// 对表的所有字段进行查询
	QueryFields bool
	// 批量创建的size
	CreateBatchSize int
	// 启用错误转换
	TranslateError bool

	// ClauseBuilders clause builder
	ClauseBuilders map[string]clause.ClauseBuilder
	// ConnPool db conn pool
	ConnPool ConnPool
	// Dialector database dialector
	Dialector
	// Plugins registered plugins
	Plugins map[string]Plugin

	callbacks  *callbacks
	cacheStore *sync.Map
}

模型

在gorm中,模型与数据库表相对应,它通常由结构体的方式展现,例如下面的结构体。

type Person struct {
	Id      uint
	Name    string
	Address string
	Mom     string
	Dad     string
}

结构体的内部可以由基本数据类型与实现了sql.Scannersql.Valuer接口的类型组成。在默认情况下,Person结构体所映射的表名为perons,其为蛇形复数风格,以下划线分隔。列名同样是以蛇形风格,比如Id对应列名id,gorm同样也提供了一些方式来对其进行配置。

指定列名

通过结构体标签,我们可以对结构体字段指定列名,这样在实体映射的时候,gorm就会使用指定的列名。

type Person struct {
	Id      uint   `gorm:"column:ID;"`
	Name    string `gorm:"column:Name;"`
	Address string
	Mom     string
	Dad     string
}

指定表名

通过实现Table接口,就可以指定表明,它只有一个方法,就是返回表名。

type Tabler interface {
	TableName() string
}

在实现的方法中,它返回了字符串person,在数据库迁移的时候,gorm会创建名为person的表。

type Person struct {
	Id      uint   `gorm:"column:ID;"`
	Name    string `gorm:"column:Name;"`
	Address string
	Mom     string
	Dad     string
}

func (p Person) TableName() string {
	return "person"
}

对于命名策略,也可以在创建连接时传入自己的策略实现来达到自定义的效果。

时间追踪

type Person struct {
	Id      uint
	Name    string
	Address string
	Mom     string
	Dad     string

	CreatedAt sql.NullTime
	UpdatedAt sql.NullTime
}

func (p Person) TableName() string {
	return "person"
}

当包含CreatedAtUpdatedAt字段时,在创建或更新记录时,如果其为零值,那么gorm会自动使用time.Now()来设置时间。

db.Create(&Person{
		Name:    "jack",
		Address: "usa",
		Mom:     "lili",
		Dad:     "tom",
	})

// INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom','2023-10-25 14:43:57.16','2023-10-25 14:43:57.16')

gorm也支持时间戳追踪

type Person struct {
	Id      uint   `gorm:"primaryKey;"`
	Name    string `gorm:"primaryKey;"`
	Address string
	Mom     string
	Dad     string

	// nanoseconds
	CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
	// milliseconds
	UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

那么在Create执行时,等价于下面的SQL

INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom',1698216540519000000,1698216540)

在实际情况中,如果有时间追踪的需要,我更推荐后端存储时间戳,在跨时区的情况下,处理更为简单。

Model

gorm提供了一个预设的Model结构体,它包含ID主键,以及两个时间追踪字段,和一个软删除记录字段。

type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}

在使用时只需要将其嵌入到你的实体模型中即可。

type Order struct {
	gorm.Model
	Name string
}

这样它就会自动具备gorm.Model所有的特性。

主键

在默认情况下,名为Id的字段就是主键,使用结构体标签可以指定主键字段

type Person struct {
	Id      uint `gorm:"primaryKey;"`
	Name    string
	Address string
	Mom     string
	Dad     string

	CreatedAt sql.NullTime
	UpdatedAt sql.NullTime
}

多个字段形成联合主键

type Person struct {
	Id      uint `gorm:"primaryKey;"`
	Name    string `gorm:"primaryKey;"`
	Address string
	Mom     string
	Dad     string

	CreatedAt sql.NullTime
	UpdatedAt sql.NullTime
}

索引

通过index结构体标签可以指定列索引

type Person struct {
	Id      uint   `gorm:"primaryKey;"`
	Name    string `gorm:"primaryKey;"`
    Address string `gorm:"index:idx_addr,unique,sort:desc;"`
	Mom     string
	Dad     string

	// nanoseconds
	CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
	// milliseconds
	UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

在上面的结构体中,对Address字段建立了唯一索引。两个字段使用同一个名字的索引就会创建复合索引

type Person struct {
    Id      uint   `gorm:"primaryKey;"`
    Name    string `gorm:"primaryKey;"`
    Address string `gorm:"index:idx_addr,unique;"`
    School  string `gorm:"index:idx_addr,unique;"`
    Mom     string
    Dad     string

    // nanoseconds
    CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
    // milliseconds
    UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

外键

在结构体中定义外键关系,是通过嵌入结构体的方式来进行的,比如

type Person struct {
	Id   uint `gorm:"primaryKey;"`
	Name string

	MomId uint
	Mom   Mom `gorm:"foreignKey:MomId;"`

	DadId uint
	Dad   Dad `gorm:"foreignKey:DadId;"`
}

type Mom struct {
	Id   uint
	Name string

	Persons []Person `gorm:"foreignKey:MomId;"`
}

type Dad struct {
	Id   uint
	Name string

	Persons []Person `gorm:"foreignKey:DadId;"`
}

例子中,Person结构体有两个外键,分别引用了DadMom两个结构体的主键,默认引用也就是主键。Person对于DadMom是一对一的关系,一个人只能有一个爸爸和妈妈。DadMom对于Person是一对多的关系,因为爸爸和妈妈可以有多个孩子。

Mom   Mom `gorm:"foreignKey:MomId;"`

嵌入结构体的作用是为了方便指定外键和引用,在默认情况下,外键字段名格式是被引用类型名+Id,比如MomId。默认情况下是引用的主键,通过结构体标签可以指定引用某一个字段

type Person struct {
	Id   uint `gorm:"primaryKey;"`
	Name string

	MomId uint
	Mom   Mom `gorm:"foreignKey:MomId;references:Sid;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`

	DadId uint
	Dad   Dad `gorm:"foreignKey:DadId;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type Mom struct {
	Id   uint
	Sid  uint `gorm:"uniqueIndex;"`
	Name string

	Persons []Person `gorm:"foreignKey:MomId;"`
}

其中constraint:OnUpdate:CASCADE,OnDelete:SET NULL;便是定义的外键约束。

钩子

一个实体模型可以自定义钩子

  • 创建
  • 更新
  • 删除
  • 查询

对应的接口分别如下

// 创建前触发
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}

// 创建后触发
type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}

// 更新前触发
type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}

// 更新后触发
type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}

// 保存前触发
type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}

// 保存后触发
type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}

// 删除前触发
type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}

// 删除后触发
type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}

// 查询后触发
type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}

结构体通过实现这些接口,可以自定义一些行为。

标签

下面是gorm支持的一些标签

标签名说明
column指定 db 列名
type列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsize, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime
size定义列数据类型的大小或长度,例如 size: 256
primaryKey将列定义为主键
unique将列定义为唯一键
default定义列的默认值
precision指定列的精度
scale指定列大小
not null指定列为 NOT NULL
autoIncrement指定列为自动增长
autoIncrementIncrement自动步长,控制连续记录之间的间隔
embedded嵌套字段
embeddedPrefix嵌入字段的列名前缀
autoCreateTime创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引open in new windowopen in new window 获取详情
uniqueIndexindex 相同,但创建的是唯一索引
check创建检查约束,例如 check:age > 13,查看 约束open in new windowopen in new window 获取详情
<-设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
->设置字段读的权限,->:false 无读权限
-忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限
comment迁移时为字段添加注释
foreignKey指定当前模型的列作为连接表的外键
references指定引用表的列名,其将被映射为连接表外键
polymorphic指定多态类型,比如模型名
polymorphicValue指定多态值、默认表名
many2many指定连接表表名
joinForeignKey指定连接表的外键列名,其将被映射到当前表
joinReferences指定连接表的外键列名,其将被映射到引用表
constraint关系约束,例如:OnUpdateOnDelete

迁移

AutoMigrate方法会帮助我们进行自动迁移,它会创建表,约束,索引,外键等等。

func (db *DB) AutoMigrate(dst ...interface{}) error

例如

type Person struct {
	Id      uint   `gorm:"primaryKey;"`
	Name    string `gorm:"type:varchar(100);uniqueIndex;"`
	Address string
}

type Order struct {
	Id   uint
	Name string
}

db.AutoMigrate(Person{}, Order{})
// CREATE TABLE `person` (`id` bigint unsigned AUTO_INCREMENT,`name` varchar(100),`address` longtext,PRIMARY KEY (`id`),UNIQUE INDEX `idx_person_name` (`name`))
// CREATE TABLE `orders` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,PRIMARY KEY (`id`))

或者也可以我们手动来操作,通过Migrator方法访问Migrator接口

func (db *DB) Migrator() Migrator 

它支持以下接口方法

type Migrator interface {
	// AutoMigrate
	AutoMigrate(dst ...interface{}) error

	// Database
	CurrentDatabase() string
	FullDataTypeOf(*schema.Field) clause.Expr
	GetTypeAliases(databaseTypeName string) []string

	// Tables
	CreateTable(dst ...interface{}) error
	DropTable(dst ...interface{}) error
	HasTable(dst interface{}) bool
	RenameTable(oldName, newName interface{}) error
	GetTables() (tableList []string, err error)
	TableType(dst interface{}) (TableType, error)

	// Columns
	AddColumn(dst interface{}, field string) error
	DropColumn(dst interface{}, field string) error
	AlterColumn(dst interface{}, field string) error
	MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
	HasColumn(dst interface{}, field string) bool
	RenameColumn(dst interface{}, oldName, field string) error
	ColumnTypes(dst interface{}) ([]ColumnType, error)

	// Views
	CreateView(name string, option ViewOption) error
	DropView(name string) error

	// Constraints
	CreateConstraint(dst interface{}, name string) error
	DropConstraint(dst interface{}, name string) error
	HasConstraint(dst interface{}, name string) bool

	// Indexes
	CreateIndex(dst interface{}, name string) error
	DropIndex(dst interface{}, name string) error
	HasIndex(dst interface{}, name string) bool
	RenameIndex(dst interface{}, oldName, newName string) error
	GetIndexes(dst interface{}) ([]Index, error)
}

方法列表中涉及到了数据库,表,列,视图,索引,约束多个维度,对需要自定义的用户来说可以更加精细化的操作。

指定表注释

在迁移时,如果想要添加表注释,可以按照如下方法来设置

db.Set("gorm:table_options", " comment 'person table'").Migrator().CreateTable(Person{})

需要注意的是如果使用的是AutoMigrate()方法来进行迁移,且结构体之间具引用关系,gorm会进行递归先创建引用表,这就会导致被引用表和引用表的注释都是重复的,所以推荐使用CreateTable方法来创建。

提示

在创建表时CreateTable方法需要保证被引用表比引用表先创建,否则会报错,而AutoMigrate方法则不需要,因为它会顺着关系引用关系递归创建。

创建

Create

在创建新的记录时,大多数情况都会用到Create方法

func (db *DB) Create(value interface{}) (tx *DB)

现有如下的结构体

type Person struct {
	Id   uint `gorm:"primaryKey;"`
	Name string
}

创建一条记录

user := Person{
    Name: "jack",
}

// 必须传入引用
db = db.Create(&user)

// 执行过程中发生的错误
err = db.Error
// 创建的数目
affected := db.RowsAffected

创建完成后,gorm会将主键写入user结构体中,所以这也是为什么必须得传入指针。如果传入的是一个切片,就会批量创建

user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Create(&user)

同样的,gorm也会将主键写入切片中。当数据量过大时,也可以使用CreateInBatches方法分批次创建,因为生成的INSERT INTO table VALUES (),()这样的SQL语句会变的很长,每个数据库对SQL长度是有限制的,所以必要的时候可以选择分批次创建。

db = db.CreateInBatches(&user, 50)

除此之外,Save方法也可以创建记录,它的作用是当主键匹配时就更新记录,否则就插入。

func (db *DB) Save(value interface{}) (tx *DB)
user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Save(&user)

Upsert

Save方法只能是匹配主键,我们可以通过构建Clause来完成更加自定义的upsert。比如下面这行代码

db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: false,
    DoUpdates: clause.AssignmentColumns([]string{"address"}),
    UpdateAll: false,
}).Create(&p)

它的作用是当字段name冲突后,更新字段address的值,不冲突的话就会创建一个新的记录。也可以在冲突的时候什么都不做

db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: true,
}).Create(&p)

或者直接更新所有字段

db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    UpdateAll: true,
}).Create(&p)

在使用upsert之前,记得给冲突字段添加索引。

查询

First

gorm对于查询而言,提供了相当多的方法可用,第一个就是First方法

func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)

它的作用是按照主键升序查找第一条记录,例如

var person Person
result := db.First(&person)
err := result.Error
affected := result.RowsAffected

传入dest指针方便让gorm将查询到的数据映射到结构体中。

或者使用TableModel方法可以指定查询表,前者接收字符串表名,后者接收实体模型。

db.Table("person").Find(&p)
db.Model(Person{}).Find(&p)

提示

如果传入的指针元素包含实体模型比如说结构体指针,或者是结构体切片的指针,那么就不需要手动使用指定查哪个表,这个规则适用于所有的增删改查操作。

Take

Take方法与First类似,区别就是不会根据主键排序。

func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB) 
var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffected

Pluck

Pluck方法用于批量查询一个表的单列,查询的结果可以收集到一个指定类型的切片中,不一定非得是实体类型的切片。

func (db *DB) Pluck(column string, dest interface{}) (tx *DB)

比如将所有人的地址搜集到一个字符串切片中

var adds []string

// SELECT `address` FROM `person` WHERE name IN ('jack','lili')
db.Model(Person{}).Where("name IN ?", []string{"jack", "lili"}).Pluck("address", &adds)

其实就等同于

db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)

Count

Count方法用于统计实体记录的数量

func (db *DB) Count(count *int64) (tx *DB)

看一个使用示例

var count int64

// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)

Find

批量查询最常用的是Find方法

func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)

它会根据给定的条件查找出所有符合的记录

// SELECT * FROM `person`
var ps []Person
db.Find(&ps)

Select

gorm在默认情况下是查询所有字段,我们可以通过Select方法来指定字段

func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)

比如

// SELECT `address`,`name` FROM `person` ORDER BY `person`.`id` LIMIT 1
db.Select("address", "name").First(&p)

等同于

db.Select([]string{"address", "name"}).First(&p)

同时,还可以使用Omit方法来忽略字段

func (db *DB) Omit(columns ...string) (tx *DB)

比如

// SELECT `person`.`id`,`person`.`name` FROM `person` WHERE id IN (1,2,3,4)
db.Omit("address").Where("id IN ?", []int{1, 2, 3, 4}).Find(&ps)``

SelectOmit选择或忽略的字段,在创建更新查询的时候都会起作用。

Where

条件查询会用到Where方法

func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)

下面是一个简单的示例

var p Person

db.Where("id = ?", 1).First(&p)

在链式操作中使用多个Where会构建多个AND语句,比如

// SELECT * FROM `person` WHERE id = 1 AND name = 'jack' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).Where("name = ?", "jack").First(&p)

或者使用Or方法来构建OR语句

func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB)
// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
    Or("name = ?", "jack").
    Where("address = ?", "usa").
    First(&p)

还有Not方法,都是类似的

func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB)
// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND NOT name = 'mike' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
    Or("name = ?", "jack").
    Not("name = ?", "mike").
    Where("address = ?", "usa").
    First(&p)

对于IN条件,可以直接在Where方法里面传入切片。

db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)

或者多列IN条件,需要用[][]any类型来承载参数

// SELECT * FROM `person` WHERE (id, name, address) IN ((1,'jack','uk'),(2,'mike','usa'))
db.Where("(id, name, address) IN ?", [][]any{{1, "jack", "uk"}, {2, "mike", "usa"}}).Find(&ps)

gorm支持where分组使用,就是将上述几个语句结合起来

db.Where(
		db.Where("name IN ?", []string{"cn", "uk"}).Where("id IN ?", []uint{1, 2}),
	).Or(
		db.Where("name IN ?", []string{"usa", "jp"}).Where("id IN ?", []uint{3, 4}),
	).Find(&ps)
// SELECT * FROM `person` WHERE (name IN ('cn','uk') AND id IN (1,2)) OR (name IN ('usa','jp') AND id IN (3,4))

Order

排序会用到Order方法

func (db *DB) Order(value interface{}) (tx *DB)

来看个使用的例子

var ps []Person

// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)

也可以多次调用

// SELECT * FROM `person` ORDER BY name ASC, id DESC,address
db.Order("name ASC, id DESC").Order("address").Find(&ps)

Limit

LimitOffset方法常常用于分页查询

func (db *DB) Limit(limit int) (tx *DB)

func (db *DB) Offset(offset int) (tx *DB)

下面是一个简单的分页示例

var (
    ps   []Person
    page = 2
    size = 10
)

// SELECT * FROM `person` LIMIT 10 OFFSET 10
db.Offset((page - 1) * size).Limit(size).Find(&ps)

Group

GroupHaving方法多用于分组操作

func (db *DB) Group(name string) (tx *DB)

func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)

下面看个例子

var (
    ps []Person
)

// SELECT `address` FROM `person` GROUP BY `address` HAVING address IN ('cn','us')
db.Select("address").Group("address").Having("address IN ?", []string{"cn", "us"}).Find(&ps)

Distinct

Distinct方法多用于去重

func (db *DB) Distinct(args ...interface{}) (tx *DB)

看一个示例

// SELECT DISTINCT `name` FROM `person` WHERE address IN ('cn','us')
db.Where("address IN ?", []string{"cn", "us"}).Distinct("name").Find(&ps)

子查询

子查询就是嵌套查询,例如想要查询出所有id值大于平均值的人

// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)

from子查询

// SELECT * FROM (SELECT * FROM `person` WHERE address IN ('cn','uk')) as p
db.Table("(?) as p", db.Model(Person{}).Where("address IN ?", []string{"cn", "uk"})).Find(&ps)

gorm使用clause.Locking子句来提供锁的支持

// SELECT * FROM `person` FOR UPDATE
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&ps)

// SELECT * FROM `person` FOR SHARE NOWAIT
db.Clauses(clause.Locking{Strength: "SHARE", Options: "NOWAIT"}).Find(&ps)

迭代

通过Rows方法可以获取一个迭代器

func (db *DB) Rows() (*sql.Rows, error) 

通过遍历迭代器,使用ScanRows方法可以将每一行的结果扫描到结构体中。

rows, err := db.Model(Person{}).Rows()
if err != nil {
    return
}
defer rows.Close()

for rows.Next() {
    var p Person
    err := db.ScanRows(rows, &p)
    if err != nil {
        return
    }
}

修改

save

在创建的时候提到过Save方法,它也可以用来更新记录,并且它会更新所有字段,即便有些结构体的字段是零值,不过如果主键匹配不到的话就会进行插入操作了。

var p Person

db.First(&p)

p.Address = "poland"
// UPDATE `person` SET `name`='json',`address`='poland' WHERE `id` = 2
db.Save(&p)

可以看到它把除了主键以外的字段全都添到了SET语句中。

update

所以大多数情况下,建议使用Update方法

func (db *DB) Update(column string, value interface{}) (tx *DB)

它主要是用来更新单列字段

var p Person

db.First(&p)

// UPDATE `person` SET `address`='poland' WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Update("address", "poland")

updates

Updates方法用于更新多列,接收结构体和map作为参数,并且当结构体字段为零值时,会忽略该字段,但在map中不会。

func (db *DB) Updates(values interface{}) (tx *DB) 

下面是一个例子

var p Person

db.First(&p)

// UPDATE `person` SET `name`='jojo',`address`='poland' WHERE `id` = 2
db.Model(p).Updates(Person{Name: "jojo", Address: "poland"})

// UPDATE `person` SET `address`='poland',`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "address": "poland"})

SQL表达式

有些时候,常常会会需要对字段进行一些自增或者自减等与自身进行运算的操作,一般是先查再计算然后更新,或者是使用SQL表达式。

func Expr(expr string, args ...interface{}) clause.Expr

看下面的一个例子

// UPDATE `person` SET `age`=age + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age + age")})

// UPDATE `person` SET `age`=age * 2 + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age * 2 + age")})

删除

在gorm中,删除记录会用到Delete方法,它可以直接传实体结构,也可以传条件。

func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)

例如直接传结构体

var p Person

db.First(&p)

// // DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)

或者

var p Person

db.First(&p)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)

或者指定条件

// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)

也可以简写成

var p Person

db.First(&p)

// DELETE FROM `person` WHERE id = 2
db.Delete(&Person{}, "id = ?", 2)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&Person{}, 2)

批量删除的话就是传入切片

// DELETE FROM `person` WHERE id IN (1,2,3)
db.Delete(&Person{}, "id IN ?", []uint{1, 2, 3})
// DELETE FROM `person` WHERE `person`.`id` IN (1,2,3)
db.Delete(&Person{}, []uint{1, 2, 3})

软删除

假如你的实体模型使用了软删除,那么在删除时,默认进行更新操作,若要永久删除的话可以使用Unscope方法

db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})

关联定义

gorm提供了表关联的交互能力,通过嵌入结构体和字段的形式来定义结构体与结构体之间的关联。

一对一

一对一关系是最简单的,正常情况下一个人只能有一个母亲,看下面的结构体

type Person struct {
	Id      uint
	Name    string
	Address string
	Age     uint

	MomId sql.NullInt64
	Mom   Mom `gorm:"foreignKey:MomId;"`
}

type Mom struct {
	Id   uint
	Name string
}

Person结构体通过嵌入Mom结构体,实现了对Mom类型的引用,其中Person.MomId就是引用字段,主键Mom.Id就是被引用字段,这样就完成了一对一关系的关联。如何自定义外键以及引用和约束还有默认的外键规则这些已经在外键定义中已经讲到过,就不再赘述

提示

对于外键字段,推荐使用sql包提供的类型,因为外键默认可以为NULL,在使用Create创建记录时,如果使用普通类型,零值0也会被创建,不存在的外键被创建显然是不被允许的。

一对多

下面加一个学校结构体,学校与学生是一对多的关系,一个学校有多个学校,但是一个学生只能上一个学校。

type Person struct {
    Id      uint
    Name    string
    Address string
    Age     uint

    MomId sql.NullInt64
    Mom   Mom `gorm:"foreignKey:MomId;"`

    SchoolId sql.NullInt64
    School   School gorm:"foreignKey:SchoolId;"`
}

type Mom struct {
    Id   uint
    Name string
}


type School struct {
    Id   uint
    Name string

    Persons []Person `gorm:"foreignKey:SchoolId;"`
}

school.Persons[]person类型,表示着可以拥有多个学生,而Person则必须要有包含引用School的外键,也就是Person.SchoolId

多对多

一个人可以拥有很多房子,一个房子也可以住很多人,这就是一个多对多的关系。

type Person struct {
	Id      uint
	Name    string
	Address string
	Age     uint

	MomId sql.NullInt64
	Mom   Mom `gorm:"foreignKey:MomId;"`

	SchoolId sql.NullInt64
	School   School `gorm:"foreignKey:SchoolId;"`

	Houses []House `gorm:"many2many:person_house;"`
}

type Mom struct {
	Id   uint
	Name string
}

type School struct {
	Id   uint
	Name string

	Persons []Person
}

type House struct {
	Id   uint
	Name string

	Persons []Person `gorm:"many2many:person_house;"`
}

type PersonHouse struct {
	PersonId sql.NullInt64
	Person   Person `gorm:"foreignKey:PersonId;"`
	HouseId  sql.NullInt64
	House    House `gorm:"foreignKey:HouseId;"`
}

PersonHouse互相持有对方的切片类型表示多对多的关系,多对多关系一般需要创建连接表,通过many2many来指定连接表,连接表的外键必须要指定正确。

创建完结构体后让gorm自动迁移到数据库中

tables := []any{
    School{},
    Mom{},
    Person{},
    House{},
    PersonHouse{},
}
for _, table := range tables {
    db.Migrator().CreateTable(&table)
}

注意引用表与被引用表的先后创建顺序。

关联操作

在创建完上述三种关联关系后,接下来就是如何使用关联来进行增删改查。这主要会用到Association方法

func (db *DB) Association(column string) *Association

它接收一个关联参数,它的值应该是嵌入引用结构体中的被引用类型的字段名。

db.Model(&person).Association("Mom").Find(&mom)

比如关联查找一个人的母亲,Association的参数就是Mom,也就是Person.Mom字段名。

创建关联

// 定义好数据
jenny := Mom{
    Name: "jenny",
}

mit := School{
    Name:    "MIT",
    Persons: nil,
}

h1 := House{
    Id:      0,
    Name:    "h1",
    Persons: nil,
}

h2 := House{
    Name:    "h2",
    Persons: nil,
}

jack := Person{
    Name:    "jack",
    Address: "usa",
    Age:     18,
}

mike := Person{
    Name:    "mike",
    Address: "uk",
    Age:     20,
}

// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('jack','usa',18,NULL,NULL)
db.Create(&jack)
// INSERT INTO `schools` (`name`) VALUES ('MIT')
db.Create(&mit)

// 添加Person与Mom的关联,一对一关联
// INSERT INTO `moms` (`name`) VALUES ('jenny') ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=1 WHERE `id` = 1
db.Model(&jack).Association("Mom").Append(&jenny) 

// 添加school与Person的关联,一对多关联
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('jack','usa',18,1,1,1),('mike','uk',20,NULL,1,DEFAULT) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
db.Model(&mit).Association("Persons").Append([]Person{jack, mike})

// 添加Person与Houses的关联,多对多关联
// INSERT INTO `houses` (`name`) VALUES ('h1'),('h2') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,1),(1,2) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
db.Model(&jack).Association("Houses").Append([]House{h1, h2})

假如所有的记录都不存在,在进行关联创建时,也会先创建记录再创建关联。

查找关联

下面演示如何进行查找关联。

// 一对一关联查找
var person Person
var mom Mom

// SELECT * FROM `people` ORDER BY `people`.`id` LIMIT 1
db.First(&person)
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
db.Model(person).Association("Mom").Find(&mom)

// 一对多关联查找
var school School
var persons []Person

// SELECT * FROM `schools` ORDER BY `schools`.`id` LIMIT 1
db.First(&school)
// SELECT * FROM `people` WHERE `people`.`school_id` = 1
db.Model(&school).Association("Persons").Find(&persons)

// 多对多关联查找
var houses []House

// SELECT `houses`.`id`,`houses`.`name` FROM `houses` JOIN `person_house` ON `person_house`.`house_id` = `houses`.`id` AND `person_house`.`person_id` IN (1,2)
db.Model(&persons).Association("Houses").Find(&houses)

关联查找会根据已有的数据,去引用表中查找符合条件的记录,对于多对多关系而言,gorm会自动完成表连接这一过程。

更新关联

下面演示如何进行更新关联

// 一对一关联更新
var jack Person

lili := Mom{
    Name: "lili",
}

// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)

// INSERT INTO `moms` (`name`) VALUES ('lili')
db.Create(&lili)

// INSERT INTO `moms` (`name`,`id`) VALUES ('lili',2) ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=2 WHERE `id` = 1
db.Model(&jack).Association("Mom").Replace(&lili)

// 一对多关联更新

var mit School
newPerson := []Person{{Name: "bob"}, {Name: "jojo"}}
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('bob','',0,NULL,NULL),('jojo','',0,NULL,NULL)
db.Create(&newPerson)

//  SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)

// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('bob','',0,NULL,1,4),('jojo','',0,NULL,1,5) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
//  UPDATE `people` SET `school_id`=NULL WHERE `people`.`id` NOT IN (4,5) AND `people`.`school_id` = 1
db.Model(&mit).Association("Persons").Replace(newPerson)

// 多对多关联更新

// INSERT INTO `houses` (`name`) VALUES ('h3'),('h4'),('h5') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,3),(1,4),(1,5) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` NOT IN (3,4,5)
db.Model(&jack).Association("Houses").Replace([]House{{Name: "h3"}, {Name: "h4"}, {Name: "h5"}})

在关联更新时,如果被引用数据和引用数据都不存在,gorm会尝试创建它们。

删除关联

下面演示如何删除关联

// 一对一关联删除
var (
    jack Person
    lili Mom
)

// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)

//  SELECT * FROM `moms` WHERE name = 'lili' ORDER BY `moms`.`id` LIMIT 1
db.Where("name = ?", "lili").First(&lili)

// UPDATE `people` SET `mom_id`=NULL WHERE `people`.`id` = 1 AND `people`.`mom_id` = 2
db.Model(&jack).Association("Mom").Delete(&lili)

// 一对多关联删除

var (
    mit     School
    persons []Person
)

// SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)
// SELECT * FROM `people` WHERE name IN ('jack','mike')
db.Where("name IN ?", []string{"jack", "mike"}).Find(&persons)

// UPDATE `people` SET `school_id`=NULL WHERE `people`.`school_id` = 1 AND `people`.`id` IN (1,2)
db.Model(&mit).Association("Persons").Delete(&persons)

// 多对多关联删除
var houses []House

// SELECT * FROM `houses` WHERE name IN ('h3','h4')
db.Where("name IN ?", []string{"h3", "h4"}).Find(&houses)

// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` IN (3,4)
db.Model(&jack).Association("Houses").Delete(&houses)

关联删除时只会删除它们之间的引用关系,并不会删除实体记录。我们还可以使用Clear方法来直接清空关联

db.Model(&jack).Association("Houses").Clear()

如果想要删除对应的实体记录,可以在Association操作后面加上Unscoped操作(不会影响many2many)

db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)

对于一对多和多对多而言,可以使用Select操作来删除记录

var (
    mit     School
)
db.Where("name = ?", "mit").First(&mit)

db.Select("Persons").Delete(&mit)

预加载

预加载用于查询关联数据,对于具有关联关系的实体而言,它会先预先加载被关联引用的实体。之前提到的关联查询是对关联关系进行查询,预加载是直接对实体记录进行查询,包括所有的关联关系。从语法上来说,关联查询需要先查询指定的[]Person,然后再根据[]Person 去查询关联的[]Mom,预加载从语法上直接查询[]Person,并且也会将所有的关联关系顺带都加载了,不过实际上它们执行的SQL都是差不多的。下面看一个例子

var users []Person

// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)

这是一个一对一关联查询的例子,它的输出

[{Id:1 Name:jack Address:usa Age:18 MomId:{Int64:1 Valid:true} Mom:{Id:1 Name:jenny} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]} {Id:2 Name:mike Address:uk Age:20 MomId:{Int64:0 Valid:false} Mom:{Id:0 Name:} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]}]

可以看到将关联的Mom一并查询出来了,但是没有预加载学校关系,所有School结构体都是零值。还可以使用clause.Associations表示预加载全部的关系,除了嵌套的关系。

db.Preload(clause.Associations).Find(&users)

下面来看一个嵌套预加载的例子,它的作用是查询出所有学校关联的所有学生以及每一个学生所关联的母亲和每一个学生所拥有的房子,而且还要查询出每一个房子的主人集合,学校->学生->房子->学生。

var schools []School

db.Preload("Persons").
    Preload("Persons.Mom").
    Preload("Persons.Houses").
    Preload("Persons.Houses.Persons").Find(&schools)

// 输出代码,逻辑可以忽略
for _, school := range schools {
    fmt.Println("school", school.Name)
    for _, person := range school.Persons {
        fmt.Println("person", person.Name)
        fmt.Println("mom", person.Mom.Name)
        for _, house := range person.Houses {
            var persons []string
            for _, p := range house.Persons {
                persons = append(persons, p.Name)
            }
            fmt.Println("house", house.Name, "owner", persons)
        }
        fmt.Println()
    }
}

输出为

school MIT
person jack
mom jenny
house h1 owner [jack]
house h2 owner [jack]

person mike
mom 

可以看到输出了每一个学校的每一个学生的母亲以及它们的房子,还有房子的所有主人。

事务

gorm默认开启事务,任何插入和更新操作失败后都会回滚,可以在连接配置中关闭,性能大概会提升30%左右。gorm中事务的使用有多种方法,下面简单介绍下。

自动

闭包事务,通过Transaction方法,传入一个闭包函数,如果函数返回值不为nil,那么就会自动回滚。

func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) 

下面看一个例子,闭包中的操作应该使用参数tx,而非外部的db

var ps []Person

db.Transaction(func(tx *gorm.DB) error {
    err := tx.Create(&ps).Error
    if err != nil {
        return err
    }

    err = tx.Create(&ps).Error
    if err != nil {
        return err
    }

    err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
    if err != nil {
        return err
    }

    return nil
})

手动

比较推荐使用手动事务,由我们自己来控制何时回滚,何时提交。手动事务会用到下面三个方法

// Begin方法用于开启事务
func (db *DB) Begin(opts ...*sql.TxOptions) *DB 

// Rollback方法用于回滚事务
func (db *DB) Rollback() *DB 

// Commit方法用于提交事务
func (db *DB) Commit() *DB

下面看一个例子,开启事务后,就应该使用tx来操作orm。

var ps []Person

tx := db.Begin()

err := tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
    tx.Rollback()
    return
}

tx.Commit()

可以指定回滚点

var ps []Person

tx := db.Begin()

err := tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

tx.SavePoint("createBatch")

err = tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
    tx.RollbackTo("createBatch")
    return
}

tx.Commit()

总结

如果你阅读完了上面的所有内容,并动手敲了代码,那么你就可以使用gorm进行对数据库进行增删改查了,gorm除了这些操作以外,还有其它许多功能,更多细节可以前往官方文档了解。