多对多关系
多对多关系
多对多关系,需要用第三张表存储两张表的关系
表结构搭建
type Tag struct {
ID uint
Name string
Articles []Article `gorm:"many2many:article_tag;"` // 用于反向引用
}
func (t Tag) TableName() string {
return "tag"
}
type Article struct {
ID uint
Title string
Tags []Tag `gorm:"many2many:article_tag;"` // 需要使用标签说明是多对多的关系,并指定第三张表的表名,会自动生成相应表名的表(不用管)
}
func (a Article) TableName() string {
return "article"
}
/*
TableName方法指定表名
*/
多对多添加
添加文章,并创建标签
DB.Create(&Article{
Title: "Python基础",
Tags: []Tag{
{
Name: "python",
},
{
Name: "后端",
},
},
})
添加文章,选择已有标签
var tag []Tag
// 查找出已有的标签
DB.Take(&tag, "name in ?", []string{"后端"})
// 添加
DB.Create(&Article{
Title: "Golang",
Tags: tag,
})
多对多查询
查询文章,显示文章的标签列表
article := Article{}
DB.Preload("Tags").Take(&article, 2)
fmt.Println(article)
查询标签,显示文章列表
tag := Tag{}
DB.Preload("Articles").Take(&tag, 2)
fmt.Println(tag)
多对多更新(先删除原有的,再添加新的)
移除文章所有的标签
article := Article{}
DB.Preload("Tags").Take(&article, 1) // 必须使用预加载 ,不然会删除失败
DB.Model(&article).Association("Tags").Delete(article.Tags)
添加文章标签
tag := Tag{}
DB.Take(&tag, 2)
DB.Model(&article).Association("Tags").Append(&tag)
更新文章的标签
// 先获取要更新的文章
article := Article{}
DB.Preload("Tags").Take(&article, 1)
// 获取要更新的标签
tag := Tag{}
DB.Take(&tag, 1)
// 更新 (会删除原来的所有,然后再添加)
DB.Model(&article).Association("Tags").Replace(&tag)
自定义连接表
默认的连接表,只有双方的主键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
}
生成表结构
// 设置Article的Tags表为ArticleTag
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{})
// 如果tag要反向应用Article,那么也得加上
// DB.SetupJoinTable(&Tag{}, "Articles", &ArticleTag{})
err := DB.AutoMigrate(&Article{}, &Tag{}, &ArticleTag{})
fmt.Println(err)
操作案例
举一些简单的例子
-
添加文章并添加标签,并自动关联
-
添加文章,关联已有标签
-
给已有文章关联标签
-
替换已有文章的标签
-
添加文章并添加标签,并自动关联
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!
SetupJoinTable
添加和更新的时候得用这个
这样才能走自定义的连接表,以及走它的钩子函数
查询则不需要这个
自定义连接表主键
这个功能还是很有用的,例如你的文章表 可能叫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!
添加,更新,查询操作和上面的都是一样
操作连接表
如果通过一张表去操作连接表,这样会比较麻烦
比如查询某篇文章关联了哪些标签
或者是举个更通用的例子,用户和文章,某个用户在什么时候收藏了哪篇文章
无论是通过用户关联文章,还是文章关联用户都不太好查
最简单的就是直接查连接表
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)
}