六、一对多关系
我们先从一对多开始多表关系的学习
因为一对多的关系生活中到处都是
例如
老板与员工
女神和舔狗
老师和学生
班级与学生
用户与文章
...CopyErrorOK!
1. 表结构建立
在gorm中,官方文档是把一对多关系分为了两类,
Belongs To 属于谁
Has Many 我拥有的
他们本来是一起的,本教程把它们合在一起讲
我们以用户和文章为例
一个用户可以发布多篇文章,一篇文章属于一个用户
type User struct {
ID uint `gorm:"size:4"`
Name string `gorm:"size:8"`
Articles []Article // 用户拥有的文章列表
}
type Article struct {
ID uint `gorm:"size:4"`
Title string `gorm:"size:16"`
UserID uint // 属于 这里的类型要和引用的外键类型一致,包括大小
User User // 属于
}CopyErrorOK!
关于外键命名,外键名称就是关联表名+ID,类型是uint
2. 重写外键关联
type User struct {
ID uint `gorm:"size:4"`
Name string `gorm:"size:8"`
Articles []Article `gorm:"foreignKey:UID"` // 用户拥有的文章列表
}
type Article struct {
ID uint `gorm:"size:4"`
Title string `gorm:"size:16"`
UID uint // 属于
User User `gorm:"foreignKey:UID"` // 属于
}CopyErrorOK!
这里有个地方要注意
我改了Article 的外键,将UID作为了外键,那么User这个外键关系就要指向UID
与此同时,User所拥有的Articles也得更改外键,改为UID
3. 重写外键引用
type User struct {
ID uint `gorm:"size:4"`
Name string `gorm:"size:8"`
Articles []Article `gorm:"foreignKey:UserName;references:Name"` // 用户拥有的文章列表
}
type Article struct {
ID uint `gorm:"size:4"`
Title string `gorm:"size:16"`
UserName string
User User `gorm:"references:Name"` // 属于
}CopyErrorOK!
这一块的逻辑比较复杂
比如有1个用户
id | name |
1 | 枫枫 |
之前的外键关系是这样表示文章的
id | title | user_id |
1 | python | 1 |
2 | javascript | 1 |
3 | golang | 1 |
如果改成直接关联Name,那就变成了这样
id | title | user_name |
1 | python | 枫枫 |
2 | javascript | 枫枫 |
3 | golang | 枫枫 |
虽然这样很方便,但是非常不适合在实际项目中这样用
我们还是用第一版的表结构做一对多关系的增删改查
4. 一对多的添加]
创建用户,并且创建文章
a1 := Article{Title: "python"}
a2 := Article{Title: "golang"}
user := User{Name: "枫枫", Articles: []Article{a1, a2}}
DB.Create(&user)CopyErrorOK!
gorm自动创建了两篇文章,以及创建了一个用户,还将他们的关系给关联上了
创建文章,关联已有用户
a1 := Article{Title: "golang零基础入门", UserID: 1}
DB.Create(&a1)CopyErrorOK!
var user User
DB.Take(&user, 1)
DB.Create(&Article{Title: "python零基础入门", User: user})CopyErrorOK!
5. 外键添加
给现有用户绑定文章
var user User
DB.Take(&user, 2)
var article Article
DB.Take(&article, 5)
user.Articles = []Article{article}
DB.Save(&user)CopyErrorOK!
也可以用Append方法
var user User
DB.Take(&user, 2)
var article Article
DB.Take(&article, 5)
//user.Articles = []Article{article}
//DB.Save(&user)
DB.Model(&user).Association("Articles").Append(&article)CopyErrorOK!
给现有文章关联用户
var article Article
DB.Take(&article, 5)
article.UserID = 2
DB.Save(&article)CopyErrorOK!
也可用Append方法
var user User
DB.Take(&user, 2)
var article Article
DB.Take(&article, 5)
DB.Model(&article).Association("User").Append(&user)CopyErrorOK!
6. 查询
查询用户,显示用户的文章列表
var user User
DB.Take(&user, 1)
fmt.Println(user)CopyErrorOK!
直接这样,是显示不出文章列表
6.1. 预加载
我们必须要使用预加载来加载文章列表
var user User
DB.Preload("Articles").Take(&user, 1)
fmt.Println(user)CopyErrorOK!
预加载的名字就是外键关联的属性名
查询文章,显示文章用户的信息
同样的,使用预加载
var article Article
DB.Preload("User").Take(&article, 1)
fmt.Println(article)CopyErrorOK!
6.2. 嵌套预加载
查询文章,显示用户,并且显示用户关联的所有文章,这就得用到嵌套预加载了
var article Article
DB.Preload("User.Articles").Take(&article, 1)
fmt.Println(article)CopyErrorOK!
6.3. 带条件的预加载
查询用户下的所有文章列表,过滤某些文章
var user User
DB.Preload("Articles", "id = ?", 1).Take(&user, 1)
fmt.Println(user)CopyErrorOK!
这样,就只有id为1的文章被预加载出来了
6.4. 自定义预加载
var user User
DB.Preload("Articles", func(db *gorm.DB) *gorm.DB {
return db.Where("id in ?", []int{1, 2})
}).Take(&user, 1)
fmt.Println(user)CopyErrorOK!
7. 删除
7.1. 级联删除
删除用户,与用户关联的文章也会删除
var user User
DB.Take(&user, 1)
DB.Select("Articles").Delete(&user)CopyErrorOK!
7.2. 清除外键关系
删除用户,与将与用户关联的文章,外键设置为null
var user User
DB.Preload("Articles").Take(&user, 2)
DB.Model(&user).Association("Articles").Delete(&user.Articles)
七、一对一关系
一对一关系比较少,一般用于表的扩展
例如一张用户表,有很多字段
那么就可以把它拆分为两张表,常用的字段放主表,不常用的字段放详情表
1. 表结构搭建
type User struct {
ID uint
Name string
Age int
Gender bool
UserInfo UserInfo // 通过UserInfo可以拿到用户详情信息
}
type UserInfo struct {
UserID uint // 外键
ID uint
Addr string
Like string
}CopyErrorOK!
2. 添加记录
添加用户,自动添加用户详情
DB.Create(&User{
Name: "枫枫",
Age: 21,
Gender: true,
UserInfo: UserInfo{
Addr: "湖南省",
Like: "写代码",
},
})CopyErrorOK!
添加用户详情,关联已有用户
这个场景特别适合网站的注册,以及后续信息完善
刚开始注册的时候,只需要填写很基本的信息,这就是添加主表的一条记录
注册进去之后,去个人中心,添加头像,修改地址…
这就是添加附表
DB.Create(&UserInfo{
UserID: 2,
Addr: "南京市",
Like: "吃饭",
})CopyErrorOK!
当然,也可以直接把用户对象传递进来
我们需要改一下表结构
type User struct {
ID uint
Name string
Age int
Gender bool
UserInfo UserInfo // 通过UserInfo可以拿到用户详情信息
}
type UserInfo struct {
User *User // 要改成指针,不然就嵌套引用了
UserID uint // 外键
ID uint
Addr string
Like string
}CopyErrorOK!
不限于重新迁移,直接添加即可
var user User
DB.Take(&user, 2)
DB.Create(&UserInfo{
User: &user,
Addr: "南京市",
Like: "吃饭",
})CopyErrorOK!
3. 查询
一般是通过主表查副表
var user User
DB.Preload("UserInfo").Take(&user)
fmt.Println(user)
八、多对多关系
多对多关系,需要用第三张表存储两张表的关系
1. 表结构搭建
type Tag struct {
ID uint
Name string
Articles []Article `gorm:"many2many:article_tags;"` // 用于反向引用
}
type Article struct {
ID uint
Title string
Tags []Tag `gorm:"many2many:article_tags;"`
}CopyErrorOK!
2. 多对多添加
添加文章,并创建标签
DB.Create(&Article{
Title: "python基础课程",
Tags: []Tag{
{Name: "python"},
{Name: "基础课程"},
},
})CopyErrorOK!
添加文章,选择标签
var tags []Tag
DB.Find(&tags, "name = ?", "基础课程")
DB.Create(&Article{
Title: "golang基础",
Tags: tags,
})CopyErrorOK!
3. 多对多查询
查询文章,显示文章的标签列表
var article Article
DB.Preload("Tags").Take(&article, 1)
fmt.Println(article)CopyErrorOK!
查询标签,显示文章列表
var tag Tag
DB.Preload("Articles").Take(&tag, 2)
fmt.Println(tag)CopyErrorOK!
4. 多对多更新
移除文章的标签
var article Article
DB.Preload("Tags").Take(&article, 1)
DB.Model(&article).Association("Tags").Delete(article.Tags)
fmt.Println(article)CopyErrorOK!
更新文章的标签
var article Article
var tags []Tag
DB.Find(&tags, []int{2, 6, 7})
DB.Preload("Tags").Take(&article, 2)
DB.Model(&article).Association("Tags").Replace(tags)
fmt.Println(article)CopyErrorOK!
5. 自定义连接表
默认的连接表,只有双方的主键id,展示不了更多信息了
这是官方的例子,我修改了一下
type Article struct {
ID uint
Title string
Tags []Tag `gorm:"many2many:article_tags"`
}
type Tag struct {
ID uint
Name string
}
type ArticleTag struct {
ArticleID uint `gorm:"primaryKey"`
TagID uint `gorm:"primaryKey"`
CreatedAt time.Time
}
CopyErrorOK!
5.1. 生成表结构
// 设置Article的Tags表为ArticleTag
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{})
// 如果tag要反向应用Article,那么也得加上
// DB.SetupJoinTable(&Tag{}, "Articles", &ArticleTag{})
err := DB.AutoMigrate(&Article{}, &Tag{}, &ArticleTag{})
fmt.Println(err)CopyErrorOK!
5.2. 操作案例
举一些简单的例子
-
添加文章并添加标签,并自动关联
-
添加文章,关联已有标签
-
给已有文章关联标签
-
替换已有文章的标签
-
添加文章并添加标签,并自动关联
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{}) // 要设置这个,才能走到我们自定义的连接表
DB.Create(&Article{
Title: "flask零基础入门",
Tags: []Tag{
{Name: "python"},
{Name: "后端"},
{Name: "web"},
},
})
// CreatedAt time.Time 由于我们设置的是CreatedAt,gorm会自动填充当前时间,
// 如果是其他的字段,需要使用到ArticleTag 的添加钩子 BeforeCreateCopyErrorOK!
- 添加文章,关联已有标签
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{})
var tags []Tag
DB.Find(&tags, "name in ?", []string{"python", "web"})
DB.Create(&Article{
Title: "flask请求对象",
Tags: tags,
})CopyErrorOK!
- 给已有文章关联标签
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{})
article := Article{
Title: "django基础",
}
DB.Create(&article)
var at Article
var tags []Tag
DB.Find(&tags, "name in ?", []string{"python", "web"})
DB.Take(&at, article.ID).Association("Tags").Append(tags)CopyErrorOK!
- 替换已有文章的标签
var article Article
var tags []Tag
DB.Find(&tags, "name in ?", []string{"后端"})
DB.Take(&article, "title = ?", "django基础")
DB.Model(&article).Association("Tags").Replace(tags)CopyErrorOK!
- 查询文章列表,显示标签
var articles []Article
DB.Preload("Tags").Find(&articles)
fmt.Println(articles)CopyErrorOK!
5.3. SetupJoinTable
添加和更新的时候得用这个
这样才能走自定义的连接表,以及走它的钩子函数
查询则不需要这个
6. 自定义连接表主键
这个功能还是很有用的,例如你的文章表 可能叫ArticleModel,你的标签表可能叫TagModel
那么按照gorm默认的主键名,那就分别是ArticleModelID,TagModelID,太长了,根本就不实用
这个地方,官网给的例子看着也比较迷,不过我已经跑通了
主要是要修改这两项
joinForeignKey 连接的主键id
JoinReferences 关联的主键id
type ArticleModel struct {
ID uint
Title string
Tags []TagModel `gorm:"many2many:article_tags;joinForeignKey:ArticleID;JoinReferences:TagID"`
}
type TagModel struct {
ID uint
Name string
Articles []ArticleModel `gorm:"many2many:article_tags;joinForeignKey:TagID;JoinReferences:ArticleID"`
}
type ArticleTagModel struct {
ArticleID uint `gorm:"primaryKey"` // article_id
TagID uint `gorm:"primaryKey"` // tag_id
CreatedAt time.Time
}CopyErrorOK!
生成表结构
DB.SetupJoinTable(&ArticleModel{}, "Tags", &ArticleTagModel{})
DB.SetupJoinTable(&TagModel{}, "Articles", &ArticleTagModel{})
err := DB.AutoMigrate(&ArticleModel{}, &TagModel{}, &ArticleTagModel{})
fmt.Println(err)CopyErrorOK!
添加,更新,查询操作和上面的都是一样
7. 操作连接表
如果通过一张表去操作连接表,这样会比较麻烦
比如查询某篇文章关联了哪些标签
或者是举个更通用的例子,用户和文章,某个用户在什么时候收藏了哪篇文章
无论是通过用户关联文章,还是文章关联用户都不太好查
最简单的就是直接查连接表
type UserModel struct {
ID uint
Name string
Collects []ArticleModel `gorm:"many2many:user_collect_models;joinForeignKey:UserID;JoinReferences:ArticleID"`
}
type ArticleModel struct {
ID uint
Title string
// 这里也可以反向引用,根据文章查哪些用户收藏了
}
// UserCollectModel 用户收藏文章表
type UserCollectModel struct {
UserID uint `gorm:"primaryKey"` // article_id
ArticleID uint `gorm:"primaryKey"` // tag_id
CreatedAt time.Time
}
func main() {
DB.SetupJoinTable(&UserModel{}, "Collects", &UserCollectModel{})
err := DB.AutoMigrate(&UserModel{}, &ArticleModel{}, &UserCollectModel{})
fmt.Println(err)
}CopyErrorOK!
常用的操作就是根据用户查收藏的文章列表
var user UserModel
DB.Preload("Collects").Take(&user, "name = ?", "枫枫")
fmt.Println(user)CopyErrorOK!
但是这样不太好做分页,并且也拿不到收藏文章的时间
var collects []UserCollectModel
DB.Find(&collects, "user_id = ?", 2)
fmt.Println(collects)CopyErrorOK!
这样虽然可以查到用户id,文章id,收藏的时间,但是搜索只能根据用户id搜,返回也拿不到用户名,文章标题等
我们需要改一下表结构,不需要重新迁移,加一些字段
type UserModel struct {
ID uint
Name string
Collects []ArticleModel `gorm:"many2many:user_collect_models;joinForeignKey:UserID;JoinReferences:ArticleID"`
}
type ArticleModel struct {
ID uint
Title string
}
// UserCollectModel 用户收藏文章表
type UserCollectModel struct {
UserID uint `gorm:"primaryKey"` // article_id
UserModel UserModel `gorm:"foreignKey:UserID"`
ArticleID uint `gorm:"primaryKey"` // tag_id
ArticleModel ArticleModel `gorm:"foreignKey:ArticleID"`
CreatedAt time.Time
}CopyErrorOK!
查询
var collects []UserCollectModel
var user UserModel
DB.Take(&user, "name = ?", "枫枫")
// 这里用map的原因是如果没查到,那就会查0值,如果是struct,则会忽略零值,全部查询
DB.Debug().Preload("UserModel").Preload("ArticleModel").Where(map[string]any{"user_id": user.ID}).Find(&collects)
for _, collect := range collects {
fmt.Println(collect)
}
九、事务
事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。
很形象的一个例子,张三给李四转账100元,在程序里面,张三的余额就要-100,李四的余额就要+100 整个事件是一个整体,哪一步错了,整个事件都是失败的
gorm事务默认是开启的。为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。
如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。
一般不推荐禁用
// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
CopyErrorOK!
本节课表结构
type User struct {
ID uint `json:"id"`
Name string `json:"name"`
Money int `json:"money"`
}
// InnoDB引擎才支持事务,MyISAM不支持事务
// DB.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})