[Gorm]Gorm快速入门

1.什么是Gorm

gorm是ORM(Object-Relationl Mapping)的一种,适用于golang的开发,它的作用是在关系型数据库和对象之间作一个映射,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了 。

换言之,gorm可以让程序员用代码来对数据库进行操作,他对sql语句进行封装,让代码定义的数据结构(结构体的结构)转换成数据库的表的结构,让代码定义的定位转换为对数据库的操作。

gorm是go语言开发数据库的一个库,具备很多优秀特性,官方文档上列出了这样一些特性。

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

2.Gorm的安装和基本配置

2.1 环境配置

我们首先新建一个数据库,名字是gorm_test,字符集为utf8mb4。

我们采用mod的方式对gorm进行使用,首先新建一个项目,配置我们的项目的goproxy

添加https://goproxy.cn作为本项目的GOPROXY

新建一个文件,作为连接数据库的文件。

我们为其导入两个库,分别是gorm的本体和对mysql的驱动

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

2.2 最简方式连接数据库

只需要在main函数中,设置dsn和open函数即可。

func main() {
	dsn := "root:123456@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	fmt.Println(db,err)
}

2.3 Config方式连接数据库

实际在工程中使用更多的是这种方式,通过这种方式可以设置的配置更多

func main() {
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN: "root:123456@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local", // DSN data source name
		DefaultStringSize: 171, // string 类型字段的默认长度
		DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
		DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
		DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
		SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
	}), &gorm.Config{})
	fmt.Println(db,err)
}
mysql.Config还有很多可选配置项,如
 
而后面的gorm.Config也有很多可选配置,并且非常常用,给出官方文档的API供参考。https://gorm.io/zh_CN/docs/gorm_config.html
我们选几个常用的配置项说明,
&gorm.Config{
		SkipDefaultTransaction: false, //跳过默认事务
		NamingStrategy: schema.NamingStrategy{  //更改默认的命名约定
			TablePrefix: "t_",   // 设定一个新建的表名前缀,比如新建一个`User`表名字会自动改为为`t_users`
			SingularTable: true, // 使用单数表名,启用该选项后,`User` 表将是`user`
			NameReplacer: strings.NewReplacer("CID", "Cid"), // 在转为数据库名称之前,使用NameReplacer更改结构/字段名称。
		},
		DisableForeignKeyConstraintWhenMigrating: false, //逻辑外键 代码里自动建立外键关系
	}

2.4 连接池

什么是连接池?考虑现有的数据库连接方式,用户每一次对数据库的请求都需要open一次数据库,创建一个数据库连接单例db,这样会给服务器造成极大的负载。数据库连接是一种关键的有限的昂贵的资源,对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标。数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。

数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中, 这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。

连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。
在gorm中,我们对连接池的配置方式如下:

sqlDB, err := db.DB()

// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)

// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)

// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)

3.Gorm的基本使用

3.1 迁移

Gorm中的AutoMigrate方法用于自动迁移数据库中的组织和结构(schema)

比如我们声明一个struct,命名其为User

type User struct {
  Name string
}

 使用Automigrate的方法将User这个结构体的迁移到数据库中,作为一个表。

err := db.AutoMigrate(&User{})
if err!=nil{
  fmt.Println(err)
}

 可以看到,生成了一个t_user的表,表中有一个字段name。

我们也可以使用 Migrator 接口对数据库构建独立迁移,以下是一些方法。

// 为 `User` 创建表
db.Migrator().CreateTable(&User{})

// 将 "ENGINE=InnoDB" 添加到创建 `User` 的 SQL 里去
db.Set("gorm:table_options", "ENGINE=InnoDB").Migrator().CreateTable(&User{})

// 检查 `User` 对应的表是否存在
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")

还可以对列、约束、索引等结构进行操作,具体实现可以参考官方文档https://gorm.io/zh_CN/docs/migration.html

3.2 模型声明

什么是模型?模型就是一个go的结构体,携带了gorm规定的标签或实现了gorm的一些接口。模型是对数据库操作的基础,一个好的模型定义会帮助提高数据库的使用效率。

我们首先声明一个全局的结构体,以便将其他的文件定义的结构传入。(在实际使用中一般不直接这么做)

var GLOBAL_DB *gorm.DB

接下来我们新建一个文件,专门用于定义各种struct。定义一个TestUser的结构体,这里使用官方文档的例子。

type TestUser struct {
  ID           uint
  Name         string
  Email        *string
  Age          uint8
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
  CreatedAt    time.Time
  UpdatedAt    time.Time
}

 我们也可以使用嵌套结构体,gorm有一个默认的gorm.Model,其包括字段 IDCreatedAtUpdatedAtDeletedAt,已经定义好了数据库常用的字段,可以用来嵌入各类结构体中。

type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}
 在文件中写一个函数用来迁移结构体,方便在主函数中直接被调用。
 
func TestUserCreate(){
	GLOBAL_DB.AutoMigrate(&TestUser{})
}

我们在主函数中将数据库单例复制给全局的数据库对象,调用这个迁移函数。

GLOBAL_DB = db
TestUserCreate()

会发现数据库中自动生成了我们定义好的数据库对象。并且他的命名符合蛇形规范,会将大写开头的字母(除首个外)转为小写前加一个下划线_

我们会发现id字段自动被设定成了主键,这是怎么回事呢,因为使用了标签tag,gorm支持标签tag的方法,tag 是可选的,tag 名大小写不敏感,但建议使用 camelCase 风格,下面列举了一些常用的tag:

type Model struct {
  UUID uint  `gorm:"primaryKey"`
  Time time.Time `gorm:"column:my_time"`
}
type TestUser struct {
  Model        Model `gorm:"embedded;embeddedPrefix:embed_"`
  Name         string `gorm:"default:xxx"`
  Email        *string `gorm:"not null"`
  Age          uint8 `gorm:"comment:年龄"`
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
}

primaryKey标签设定某一项为主项,column设定行名,emdedded可以实现非匿名的嵌入,输入not null可以设定为键为非空,comment可以设定注释。下面是官方文档给出的标签的使用说明。

4.CRUD

4.1 创建

gorm的创建操作通过以下方式实现,

db_res:= GLOBAL_DB.Create(&[]TestUser{
   {Name:"xxx",Age: 18},
   {Name:"yyy",Age: 18},
   {Name:"zzz",Age: 18},
})

通过为声明的结构体赋值,再将其取地址传递给数据库单例的Create方法即可,这里可以传入一个结构体的实例,也可以传入一个存有结构体实例的切片,会返回一个对象*DB,

包括错误信息,创建影响的行数,状态等,我们接收这个对象,可以用来判断创建是否成功。

if db_res.Error!=nil{
   fmt.Println("创建失败!")
}else {
   fmt.Println("创建成功")
}

创建也可以在创建之前通过一些方法对特定字段操作,比如Omit()方法,跳过某个字段,Select创建指定字段等等。

db_res:= GLOBAL_DB.Omit("Name").Create(&TestUser{Name:"该名字将不会被传入",Age: 18})

创建操作官方文档:https://gorm.io/zh_CN/docs/create.html

4.2 查询

查询需要一个容器来接收查询返回的结果,通常有两者方式来实现。

第一种方法可以使用一个map[string]interface{}类型的数据结构来实现它,First()方法表示将首条记录写入传入的参数中。

func TestFind()  {
	var result map[string]interface{}
	GLOBAL_DB.Model(&TestUser{}).First(&result)
	fmt.Println(result)
}

 结果为

第二种我们可以使用一个和声明的结构体同样的结构体来接收它。Last()方法表示将最后一条记录写入传入的参数中。

func TestFind()  {
	var User TestUser
	GLOBAL_DB.Model(&TestUser{}).Last(&User)
	fmt.Println(User)
}

 结果为

4.2.1 主键查询

通过主键(如果没有指定主键,默认为第一个键)来查询。我们将数据结构设置为如下情况,并将数据库修改。

type TestUser struct {
	Id			 int `gorm:"primaryKey"`
	Name         string `gorm:"default:xxx"`
	Age          uint8 `gorm:"comment:年龄"`
}

 

通过主键查询的写法

func TestFind()  {
	var User TestUser
	GLOBAL_DB.First(&User,2)
	fmt.Println(User)
}

我们查找主键为2的那条数据。

4.2.2 通过string查询

第一种方式使用Where函数,类似sql语句的方式进行查询。 查找name = xxx 且age=12的首个数据项。

func TestFind()  {
	var User TestUser
	GLOBAL_DB.Where("name = ? AND age = 12","xxx").First(&User)
	fmt.Println(User)
}

 第二种方式使用结构体或Map查询

func TestFind()  {
	var User TestUser
	var User1 TestUser
	GLOBAL_DB.Where(TestUser{Name:"xxx"}).First(&User)
	GLOBAL_DB.Where(map[string]interface{}{
		"name":"xxx",
	}).First(&User1)
	fmt.Println(User)
	fmt.Println(User1)
}

 gorm也提供了一些筛选查询方式,比如

db.Not("name = ?", "jinzhu").First(&user) //排除这个条件查询
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) //符合or的条件也可以被检索到
db.Select("name", "age").Find(&users) //Select 允许指定从数据库中检索哪些字段, 默认情况下,GORM 会检索所有字段。
db.Order("age desc, name").Find(&users)//指定从数据库检索记录时的排序方式

db.Offset(3).Find(&users)//Limit 指定获取记录的最大数量 Offset 指定在开始返回记录之前要跳过的记录数量
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()//分组,聚合

也支持模糊查询

func TestFind()  {
	var User TestUser
	GLOBAL_DB.Where("name LIKE ? ","%x%").Find(&User)
	fmt.Println(User)
}

4.2.3 内联条件

我们可以使用内联条件来代替Where函数,Where函数给出的参数均可以放在First的参数中作为内联条件使用,比如

func TestFind()  {
	var User TestUser
	GLOBAL_DB.First(&User,TestUser{Name:"xxx"})
	fmt.Println(User)
}

 又或者

func TestFind()  {
	var User TestUser
	GLOBAL_DB.Not("age = 12 ").Find(&User,map[string]interface{}{
		"Name":"xxx",
	})
	fmt.Println(User)
}

4.2.4 为查询指定对象

当我们想把查询的对象保存在另一个结构体中时,我们会发现我们没有为查询指定查询的对象,可以使用Model为查询指定查询的对象,再返回。

type UserInfo struct{
	Name string
	Age uint8
}

func TestFind()  {
  	var u []UserInfo
	GLOBAL_DB.Model(&TestUser{}).Where("name LIKE ? ","%x%").Find(&u)
	fmt.Println(u)
}

4.3 更新

更新和上面的查询一样,先查询到目标函数后更新,只是把Find()函数或者First()函数换成Update()、Updatas()和Save(),第一种只更新选择的字段,第二种更新所有字段,可以使用Map结构或结构体形式传入参数,结构体零值不参与更新,第三种无论如何都更新所有的内容,包括零值。

新建文件,我们进行简单的测试。

func TestUpdate()  {
	GLOBAL_DB.Model(&TestUser{}).Where("name LIKE ? ","%x%").Update("name","帅")
}

 先使用第一种Update(),模糊查找,将所有以x开头结尾的name都变成帅,结果如下。

使用Save()函数,Save函数会根据主键更新,如果没有主键,可以先Find()到需要更新的数据项,对需要更新的项进行更新,再使用Save进行保存。

func TestUpdate()  {
	var users []TestUser
	dbres :=GLOBAL_DB.Model(&TestUser{}).Where("name LIKE ? ","帅").Find(&users)
	for k:= range users{
		users[k].Name = "不帅"
	}
	dbres.Save(&users)
}

使用Updates()函数,可以使用两种形式进行传参。

func TestUpdate()  {
	var user TestUser
	GLOBAL_DB.First(&user).Updates(TestUser{Name:"第一条",Age: 0}) // 结构体的0值不会被写入更新
	GLOBAL_DB.First(&user).Updates(map[string]interface{}{"Name":"第一条","Age": 0}) // map类型的0值会被写入更新
}

批量更新,使用Find函数先查询再更新。注意这里的结构体要写成切片,以便返回所有的结果。所有的数据项都会被修改。

func TestUpdate()  {
	var user []TestUser
	GLOBAL_DB.Find(&user).Updates(map[string]interface{}{"Name":"第一条","Age": 0})
}

4.4 删除

删除的用法和更新一样,也是使用find或first先查找再删除,但是删除涉及到一个软删除的概念,数据仍然保留在数据库内,但是标识为已删除的数据。

我们先来看基础用法,单条删除,删除首个元素。

func TestDelete(){
	var user TestUser
	GLOBAL_DB.First(&user).Delete(&user)
}

 为了演示软删除,我们把数据类型嵌入一个gorm.Model,初始化创建数据库为

使用上面的删除操作后,可以看到为首个数据项添加了一个delete_at数据,这就是软删除。

我们可以使用unscoped()函数来实现直接删除记录。

func TestDelete(){
	var user TestUser
	GLOBAL_DB.Unscoped().First(&user).Delete(&user)
}

4.5 原生SQL

我们可以使用Raw()函数实现原生sql语句的直接操作。

func TestRaw(){
	var user TestUser
	GLOBAL_DB.Raw("SELECT * FROM t_test_user WHERE NAME = ?","yyy").Scan(&user)
	fmt.Println(user)
}

查询name =yyy的数据项

5.一对一关系

5.1 Belong To

belongs to 会与另一个模型建立了一对一的连接。 这种模型的每一个实例都“属于”另一个模型的一个实例。

我们建立两个结构体分别是Dog和GirlGod,其中Dog是属于GirlGod的,每个dog只能属于一个girlgod。

// `Dog` 属于 `GirlGod`,`GirlGodID` 是外键
type Dog struct{
	gorm.Model
	Name string
	GirlGodID uint
  	GirlGod GirlGod
}

type GirlGod struct{
	gorm.Model
	Name string
}

注意,这里的Dog中有一个属性为GirlGodID,默认的标志着这个Dog属于哪一个GirlGod,这个约定默认了他是Dog的外键,GirlGod的主键为ID。可以通过tag重写外键。

我们只需要创建Dog这个表,gorm会自动为我们创建GirlGod这个表,因为Dog是属于GirlGod的,不能单独存在。

func CreateB2(){
	GLOBAL_DB.AutoMigrate(&Dog{})
}

 

我们初始化一个女神和一个舔狗,对舔狗进行创建。

g:=GirlGod{
		Model:gorm.Model{
			ID: 1,
		},
		Name:"女神一号",

	}
d:=Dog{
		Model:gorm.Model{
			ID: 1,
		},
		Name: "舔狗一号",
		GirlGod: g,

}
GLOBAL_DB.Create(&d)
GLOBAL_DB.AutoMigrate(&Dog{})

 可以发现女神被自动创建了,同时舔狗拥有一个女神的id。

我们还可以通过预加载,查找Belong to的那个结构。

我们声明一个Dog结构体的实例,用于接收返回的结果,对id为1的舔狗进行查询。

var dog Dog
GLOBAL_DB.First(&dog,1)
fmt.Println(dog)

如果我们想找到该Dog属于的女神的信息,可以使用预加载,在查找函数前加Preload函数,参数是要查找的那个关联属性。

var dog Dog
GLOBAL_DB.Preload("GirlGod").First(&dog,1)
fmt.Println(dog)

 可以看到女神一号被找到了。

5.2 Has One

has one 与另一个模型建立一对一的关联,但它和一对一关系有些许不同。 这种关联表明一个模型的每个实例都包含或拥有另一个模型的一个实例。

 我们建立一个Dog和GirlGod结构,每个GirlGod只能拥有一个Dog,

// GirlGod 有一个舔狗Dog,GirlGodID 是外键
type GirlGod struct{
	gorm.Model
	Name string
	Dog Dog  //has one 女神拥有一个舔狗
}

type Dog struct{
	gorm.Model
	Name string
	GirlGodID uint  // 被拥有 舔狗指向女神的外键
}

注意,每个GirlGod都有一个Dog嵌入结构体,每个Dog都有一个GirlGodID 作为外键连接GirlGod。可以通过tag重写外键。

我们实例化一个女神和一个舔狗结构体,只创建女神,但是这时不会自动生成舔狗,因为在has one情况下女神不强制拥有dog

d:=Dog{
		Model:gorm.Model{
			ID: 1,
		},
		Name: "舔狗一号",
	}

g:=GirlGod{
		Model:gorm.Model{
			ID: 1,
		},
		Name:"女神一号",
		Dog: d,
	}
GLOBAL_DB.Create(&g)
GLOBAL_DB.AutoMigrate(&GirlGod{})

 

我们还可以通过预加载,查找Has one的那个结构。

我们创建一个女神和一个舔狗,演示预加载操作。

	var girl GirlGod
	GLOBAL_DB.Preload("Dog").First(&girl,2)
	fmt.Println(girl)
	GLOBAL_DB.AutoMigrate(&GirlGod{})

5.3 使用Gorm建立关系

建立关系

我们清空舔狗和女神的关系,将外键都清空,使用belong to的形式,为舔狗添加女神外键。

通过grom的Association函数进行建立关系。

d := Dog{
	Model: gorm.Model{
		ID: 1,
	},
}
g := GirlGod{
	Model: gorm.Model{
		ID: 1,
	},
}
GLOBAL_DB.Model(&d).Association("GirlGod").Append(&g)

注意要先创建d的模型,再连接要建立外键的属性,最后指定建立外键的对象。

删除关系

同上,GLOBAL_DB.Model(&d).Association("GirlGod").Delete(&g)

或清除所有关系GLOBAL_DB.Model(&d).Association("GirlGod").Clear()

更新关系

同上,GLOBAL_DB.Model(&d).Association("GirlGod").Replace(&g,&g2),从指定g到指定g2。

同样的我们对于Has one也可以使用上述的代码,注意模型的建立和外键与对象的更改。

6.一对多关系

为了说明一对多关系的效果,我们在之前结构的基础上,给Dog增加一个钱包信息的内嵌结构体,用来描述Dog拥有的钱,Dog拥有这个结构体。

type Info struct {  
	gorm.Model
	Money int
	DogID uint
}

type Dog struct {  //拥有Info
	gorm.Model
	Name      string
	GirlGodID uint //舔狗指向女神的外键
	Info Info  
}

type GirlGod struct { //拥有dog
	gorm.Model
	Name string
	Dogs []Dog
}

使用AutoMigrate()函数为数据库引入这个表,手动给Info赋值如下。

可以看到,id为1的dog拥有了100大小的money,id为2的dog拥有了20000大小的money。

创建2个dog表和1个女神表

	d1 :=Dog{
		Model:gorm.Model{
			ID: 1,
		},
		Name: "汪汪一号",
	}
	d2 :=Dog{
		Model:gorm.Model{
			ID: 2,
		},
		Name: "汪汪二号",
	}
	g:=GirlGod{
		Model:gorm.Model{
			ID: 1,
		},
		Name: "女神一号",
		Dogs: []Dog{d1,d2},
	}

id为1的女神拥有id为1和2的dog。

我们对其进行查询操作。

使用preload查询女神的dog

var girl GirlGod
GLOBAL_DB.Preload("Dogs").First(&girl)
fmt.Println(girl)
汪汪一号 1 {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 0 0}} {{2 2021-11-21 16:32:21.965 +0800 CST 2021-11-21 16:32:21.965 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 汪汪二号 1 {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 0 0}}]}

其他的查询方法

GLOBAL_DB.Preload("Dogs","name = ?","汪汪一号").First(&girl)
GLOBAL_DB.Preload("Dogs",func(db *gorm.DB)*gorm.DB{
	return db.Where("name = ? ","汪汪一号")
}).First(&girl)

多级查询

我们如果想在查询女神的时候同时查询到dog的info信息应该如何操作呢,也可以使用preload()函数。

GLOBAL_DB.Preload("Dogs.Info").First(&girl)

 假如我们想对查询的结果进行一些限制应该如何操作呢,比如我想限制只输出money大于300的dog。

可以通过两级的preload函数实现

GLOBAL_DB.Preload("Dogs.Info","Where money > ?","300").Preload("Dogs").First(&girl)

注意这里的查询条件语句只能限制当前层的信息,如果在Info层添加Dog的name信息的话,将不会生效。

Join方式预加载查询

GLOBAL_DB.Preload("Dogs", func(db *gorm.DB)(*gorm.DB) {
	return db.Joins("Info").Where("money > 200")
}).First(&girl) //查询包括所有Dog的信息

通过这种方式可以多级查询到只有money项大于200的dog的信息。

7.多对多关系

 我们创建这样的一个结构体来演示多对多关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Info struct {
   gorm.Model
   Money int
   DogID uint
}
 
type Dog struct { //拥有Info
   gorm.Model
   Name     string
   Info Info
   GirlGods []GirlGod `gorm:"many2many:dog_girl_god"`
}
 
type GirlGod struct { //拥有dog
   gorm.Model
   Name string
   Dogs []Dog`gorm:"many2many:dog_girl_god"`
}

一个dog拥有多个女神,一个女神拥有多个dog,是多对多的关系。通过一个中间表连接两个主键dog_girl_god,专门用于存储两个关系的变更。

初始化这样的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
i:=Info{
   Money: 200,
}
g1:=GirlGod{
   Model:gorm.Model{
      ID:1,
   },
   Name: "女神一号",
}
g2:=GirlGod{
   Model:gorm.Model{
      ID:2,
   },
   Name: "女神二号",
}
d:=Dog{
   Name: "汪汪二号",
   GirlGods: []GirlGod{g1,g2},
   Info: i,
}

 通过预加载获取包含的信息。

1
2
3
4
5
6
7
8
d:=Dog{
   Model:gorm.Model{
      ID: 1,
   },
}
 
//预加载获取女神
GLOBAL_DB.Preload("GirlGods").Find(&d)

 

 

 如果想通过dog查找其拥有的女神,而不想显示dog的信息,可以通过association实现。

1
2
3
var girls []GirlGod
GLOBAL_DB.Model(&d).Association("GirlGods").Find(&girls)
fmt.Println(girls)

 

 想同时显示dog和其拥有女神的信息。

1
2
3
var girls []GirlGod
GLOBAL_DB.Model(&d).Preload("Dogs").Association("GirlGods").Find(&girls)
fmt.Println(girls)

使用joins实现条件查询,查询money少于10000的dog。

1
2
3
4
5
var girls []GirlGod
GLOBAL_DB.Model(&d).Preload("Dogs", func(db *gorm.DB)*gorm.DB {
   return db.Joins("Info").Where("money < ?","10000")
}).Association("GirlGods").Find(&girls)
fmt.Println(girls)

 

 多对多关系的维护

首先要创建一个dog3号。

创建一个结构体用于建立关联模型Model。

1
2
3
4
5
d:=Dog{
   Model:gorm.Model{
      ID: 3,
   },
}

 可以通过如下操作实现关系维护,会修改dog_girl_god表的信息,表示关系的变更。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
g1:=GirlGod{
   Model:gorm.Model{
      ID:1,
   },
 
}
g2:=GirlGod{
   Model:gorm.Model{
      ID:2,
   },
}
GLOBAL_DB.Model(&d).Association("GirlGods").Append(&g1,&g2)//添加id=3的dog对id为1,2的女神的关系
GLOBAL_DB.Model(&d).Association("GirlGods").Delete(&g1)//删除id=3的dog对id=1的女神的关系
GLOBAL_DB.Model(&d).Association("GirlGods").Replace(&g2) //会删除当前的所有关系再添加指定的参数
GLOBAL_DB.Model(&d).Association("GirlGods").Clear()//清空id=3的dog的所有关系
type Jiazi struct {
   ID uint
   Name string
   Gift Gift "polymorphic:Owner"
}
type Yujie struct {
   ID uint
   Name string
   Gift Gift "polymorphic:Owner"
}
 
type Gift struct {
   ID uint
   Name string
   OwnerType string
   OwnerID   uint
}
func Polymorphic(){
   //GLOBAL_DB.AutoMigrate(&Jiazi{},&Yujie{},&Gift{})
   GLOBAL_DB.Create(&Jiazi{Name: "夹子一号",Gift:Gift{
      Name: "小风车",
   }})
   GLOBAL_DB.Create(&Yujie{Name: "御姐一号",Gift: Gift{
      Name: "大风车",
   }})
}
type Jiazi struct {
   ID uint
   Name string
   Gift []Gift `gorm:"polymorphic:Owner;polymorphicValue:huhu"` //默认类型为类型名,设置value后type可以为指定值
}
type Yujie struct {
   ID uint
   Name string
   Gift Gift `gorm:"polymorphic:Owner;polymorphicValue:dudu"`
}
func Polymorphic(){
   //GLOBAL_DB.AutoMigrate(&Jiazi{},&Yujie{},&Gift{})
   GLOBAL_DB.Create(&Jiazi{Name: "夹子一号",Gift:[]Gift{
      {Name: "小风车1",},
      {Name: "小风车2",},
   }})
   GLOBAL_DB.Create(&Yujie{Name: "御姐一号",Gift: Gift{
      Name: "大风车",
   }})
}
type Jiazi struct {
    ID uint
    Name string
    Gift []Gift  `gorm:"foreignKey:JiaziName;references:name"`//外键指向外部 引用指向自己
}
type Gift struct {
    ID uint
    Name string
    JiaziName string
}

8.多态关联和引用

8.1 多态关联

为了演示多态关联的使用,我们创建如下结构。

 

 

Jiazi和Yujie都有Gift,gift是被二者复用的,由tag"polymorphic:Owner"所指定,在gift定义一个OwnerType表示被什么类型的结构体使用,Ownerid表示被使用者的id。将结构体迁移到数据库,建立三个表,分别是

我们为其创建实例:

 

 

 

 

可以看到 在的gift中标识了属于的对象类型和对象id。

我们还可以通过增加标签tag polymorphicValue:xxx 修改ownertype的默认值。

同时,多态关联也支持一对多。我们修改两个结构如下:

 

将jiazi拥有的礼物变成切片,同时更改他们多态类型的值分别为huhu和dudu。

 

效果

8.2 更改关联标签:外键和引用

在一对多的情况下,我们不使用id作为默认的外键与外部链接,而是使用Name,但是如果单纯的更改外键,拥有者仍会使用id作为外部引用的标志,我们可以通过更改引用让拥有者使用name作为外部引用的标志。

 

注意:在多对多的情况下,需要将其改为中间表的外键和引用。

9.事务操作

什么是事务?事务就是一个不可分割的一系列操作集合。为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,可以在初始化时禁用它,这将获得大约 30%+ 性能提升。

9.1 禁用自带的事务

可以通过如下方式禁用自带的事务:

1
2
3
4
5
6
7
8
9
10
// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})
 
// 持续会话模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)

9.2 事务特性

为了演示事务的特性,我们作如下测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type TMG struct {
   ID uint
   Name string
}
 
func TestTransaction()  {
   flag := false
   GLOBAL_DB.AutoMigrate(&TMG{})
   GLOBAL_DB.Transaction(func(tx *gorm.DB) error {
      tx.Create(&TMG{Name: "汉字"})
      tx.Create(&TMG{Name: "英语"})
      tx.Create(&TMG{Name: "法语"})
      if flag{
         return nil
      }else {
         return errors.New("出错")
      }
      return nil
   })
}

执行上述事务,我们会发现定义的三个数据项并不会被创建,因为在事务return之前发生了error,事务具有原子性,会将创建的数据回滚。

作为对比,我们做一个嵌套。

1
2
3
4
5
6
7
8
9
10
11
12
func TestTransaction()  {
   GLOBAL_DB.AutoMigrate(&TMG{})
   GLOBAL_DB.Transaction(func(tx *gorm.DB) error {
      tx.Create(&TMG{Name: "汉字"})
      tx.Create(&TMG{Name: "英语"})
      tx.Transaction(func(tx *gorm.DB) error {
         tx.Create(&TMG{Name: "法语"})
         return errors.New("出错")
      })
      return nil
   })
}

 这时候嵌套的事务出错,但是外层汉字和英语两个项会被创建。

9.3 手动自建事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestTransaction()  {
   GLOBAL_DB.AutoMigrate(&TMG{})
   tx:=GLOBAL_DB.Begin()
   tx.Create(&TMG{
      Name: "汉字",
   })
   tx.Create(&TMG{
      Name: "英语",
   })
   tx.Create(&TMG{
      Name: "法语",
   })
   tx.Commit()
}

通过Begin()开启一个事务,create等方法进行操作,commit()提交事务。

还可以使用回滚等方法。回滚到存储的某个状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func TestTransaction()  {
   GLOBAL_DB.AutoMigrate(&TMG{})
   tx:=GLOBAL_DB.Begin()
   tx.Create(&TMG{
      Name: "汉字",
   })
   tx.Create(&TMG{
      Name: "英语",
   })
   tx.SavePoint("存档点")
   tx.Create(&TMG{
      Name: "法语",
   })
   tx.RollbackTo("存档点")
   tx.Commit()
}

 

10.自定义数据类型

自定义的数据类型必须实现ScannerValuer接口,以便让 GORM 知道如何将该类型接收、保存到数据库。官方文档:https://gorm.io/zh_CN/docs/data_types.html

10.1 存入数据库

如果我们想把一个json格式的数据存入数据库,应该怎么办呢?

我们可以创建如下格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type CInfo struct {
   Name string
   Age int
}
 
func (c CInfo)Value()(driver.Value,error){  //传入要自定义数据类型的那个结构
   str,err:=json.Marshal(c)  //将数据编码为json字符串
   if err!=nil{
      return nil,err
   }
   return string(str),nil  //返回string类型的json字符串
}
func (c CInfo)Scan(value interface{})(error){
   return nil
}
type CUser struct {
   ID uint
   Info CInfo
}
 
func Customize()  {
   GLOBAL_DB.AutoMigrate(&CUser{})
   GLOBAL_DB.Create(&CUser{Info: CInfo{
      "小明",
      18,
   }})
}

 通过Value函数将目标格式的数据转为json格式的string放入数据库,其中返回的driver.Value为最终存入数据库的数据。

 如果我们此时使用First来查找id=1的数据,会发生什么呢?

1
2
3
4
var u CUser
   GLOBAL_DB.First(&u)
   fmt.Println(u)
}

 可以看到,查找到的数据项是0。

10.2 在数据库中查找自定义数据类型

我们如何在数据库中找到Name等于小明的相关信息呢?

这就需要用到下面的Scan()函数了,我们可以通过实现Scan函数,来完成反解析json。

1
2
3
4
5
6
7
8
func (c *CInfo)Scan(value interface{})(error){
   str,ok := value.([]byte)
   if !ok{
      return errors.New("不匹配的数据类型")
   }
   json.Unmarshal(str,c)
   return nil
}

这里用到了go的类型断言,这里的value是一个interface{}类型的变量,为查询从数据库获得的数据,这里我们用的json格式的字符串。这句的字面含义是“我认为value这个interface{}类型变量的underlying type是[]byte,如果是,请将其值赋给变量str,并且ok =true,如果不是ok = false。

这样我们就获取到了json的字符串其形式是[]byte,存放在str中,我们对其进行反序列化,将str的数据反序列化成原始格式,传到c中,c是最终渲染解析到目标数据结构类型的数据,这里的c是一个指针,会对其进行直接修改。也就是我们在下面给出的var u CUser中的CInfo。

1
2
3
var u CUser
GLOBAL_DB.First(&u)
fmt.Println(u)

 

 再来写一个案例,我们增加一个字符串切片类型,定义一个字符串切片为Args。

1
2
3
4
5
6
7
8
9
10
11
type Args []string
type CInfo struct {
   Name string
   Age  int
}
 
type CUser struct {
   ID   uint
   Info CInfo
   Args Args
}

 写一个Value用来处理Args数据类型。

1
2
3
4
5
6
7
8
9
10
11
func (a Args) Value() (driver.Value, error) {
   if len(a) > 0 {
      var str string = a[0]
      for _, v := range a[1:] {
         str += "," + v
      }
      return str, nil
   }else{
      return "",nil
   }
}

 这里我们接受这个Args类型,将输入的字符切片,间隔插入一个逗号拼接成一个字符串。

1
2
3
4
GLOBAL_DB.Create(&CUser{Info: CInfo{
   Name: "小王",
   Age:  18,
}, Args: Args{"1", "2", "3"}})

 

 对于查询需求,我们再为Args写一个Scan函数。

1
2
3
4
5
6
7
8
func (a *Args) Scan(value interface{}) error {
   str,ok:=value.([]byte)
   if !ok{
      return errors.New("数据类型无法解析")
   }
   *a = strings.Split(string(str),","//按照,为分割将string转换为一个数组切片
   return nil
}

 先断言判断查询到的数据的类型是否为字符切片,我们将其按照逗号为分割使用split函数将其转换为一个数组切片,存放到查询建立的数据载体a中。

1
2
3
var u CUser
GLOBAL_DB.First(&u)
fmt.Println(u)

 

10.3 通过type定义数据库中的字段名

我们自定义的数据类型放在数据库中一般会默认为一个类型,这里我们是

 我们可以通过tag来指定类型名,

1
2
3
4
5
type CUser struct {
   ID   uint
   Info CInfo `gorm:"type:text"`
   Args Args
}

 我们把Info指定为text类型,重新Automigrate()后查看:

posted @   梦想是能睡八小时的猪  阅读(3355)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示