在学习 MySQL 多表关系的时候,我们都知道有一对一关系,一对多关系和多对多关系,我们现在从实际场景去理解。
一、一对一关系
用户与用户详情是很明显的一对一关系,那么我们需要怎么表示这种一对一关系呢??
在MySQL里面,需要在另一种表里面增加一个字段,比如:user表和user_detail表。我们需要在user_detail表中增加一个user_id 字段,或者在user表中新增user_detail_id 字段。这个字段我们将其称之为外键字段,在一对一关系里面,外键字段随便在哪一张表里面都可以。
我们来看一看gorm的写法:
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
type UserModel struct {
ID int64
Name string `gorm:"size:32"`
Age int
}
type UserDetailModel struct {
ID int64
UserID int64
UserModel UserModel `gorm:"foreignKey:UserID"`
Email string `gorm:"size:64"`
}
func init() {
db, err := gorm.Open(mysql.Open("root:root@tcp(127.0.0.1:3306)/gorm_new_db?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{})
if err != nil {
fmt.Println(err)
return
}
DB = db
}
func migrate() {
err := DB.AutoMigrate(&UserModel{}, &userDetailModel{})
if err !- nil {
fmt.Println(err)
return
}
fmt.Println("表结构生成成功")
}
func main() {
migrate()
}
DB.AutoMigrate(&UserModel{}, &userDetailModel{})
我们来解释一下这句话:
这句话是Go语言中使用 GORM ORM 框架进行数据库迁移的代码,具体解释如下:
- DB:表示数据库连接的实例
- AutoMigrate:这是GORM提供的一种方法,用于自动迁移数据库模式。他会根据传入的模型结构体更新数据库表,确保表的结构与模型定义一致
- &UserModel{}, &UserDetailModel{}:这两个是传入的模型结构体,代表要迁移的数据库表,使用 & 符号表示传递指针,这样GORM能够访问结构体的元信息
- err:这是用于捕获方法返回的错误,如果迁移过程中发生错误,err将包含相关信息
这种方式会生成实体外键,如果是个人使用的话,有外键会好一些,他可以做一些数据约束。但是在分布式系统中,有实体外键其实是一件不太好的事情,原因如下:
在分布式系统中,实体外键约束的使用可能带来一些危害和挑战,主要体现在以下几个方面:
性能问题:
- 跨网络延迟:在分布式数据库中,外键约束通常要求在不同节点间进行检查,这可能导致显著的网络延迟,影响整体性能
- 事务开销:外键约束可能使得事务变得复杂,导致锁竞争和资源争用,增加了执行时间
- 数据一致性挑战:
- 弱一致性模型:许多分布式系统采用最终一致性模型,
- 分区问题:在分区情况下,外键关系可能跨域多个分区,增加了维护和管理的复杂性
可扩展性限制:
- 阻碍扩展:外键约束可能限制系统的扩展性,增加了在分布式环境中处理数据的复杂性,难以灵活的添加或者移动节点
- 管理复杂性:维护外键关系需要额外的管理和协调,可能导致更高的运维成本
开发复杂性:
- 增加开发难度:开发人员需要更仔细地设计数据模型,以处理外键约束带来的复杂性,增加了学习曲线和开发时间
- 错误处理:在分布式系统中,外键约束的错误处理和恢复机制变得复杂,可能导致更高的出错率
如果不需要生成实体外键,则可以进行如下配置:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true, // 不生成实体外键
})
在一对一关系中,在数据库中,不可能有一个ID对应多个邮箱,为了禁止这些行为,我们可以在结构体字段中添加一个约束:`gorm:"unique"`
type UserModel struct {
ID int64
Name string `gorm:"size:32"`
Age int
}
type UserDetailModel struct {
ID int64
UserID int64 `gorm:"unique"` // 在一对一的情况下,需要添加唯一约束
UserModel UserModel `gorm:"foreignKey:UserID"`
Email string `gorm:"size:64"`
}
正反向引用:
在 GORM 中,正向引用和反向引用主要是用于处理模型之间的关系,特别是在一对多和多对多的关系中,以下是详细介绍:
正向引用
正向引用指的是从子模型访问父模型的关系,例如:在一对多的关系中,可以通过子模型直接引用父模型。下面来看一个代码例子:
type User struct { gorm.Model Name string Posts []Post // 正向引用 } type Post struct { gorm.Model Title string UserID uint }
在这个例子中,User结构体通过Posts字段正向引用Post结构体。
反向引用:
反向引用则是从父模型访问子模型的关系。在 GORM 中,可以通过关联查询来实现。实现代码如下:
var user User db.Preload("Posts").First(&user, userID) // 反向引用
这里使用 Preload 方法来加载用户的所有帖子,实现了从 User 访问其关联的 Post。
总结:
正向引用和反向引用使得 GORM 能够有限地处理模型件的关系,方便开发者进行数据操作盒查询。通过理解这两种引用方式,开发者可以更加灵活的设计数据库结构和实现业务逻辑。
在了解完正向引用和反向引用之后,我们来看一看以下的表结构:
type UserModel struct {
ID int64
Name string `gorm:"size:32"`
Age int
UserDetailModel *UserDetailModel `gorm:"foreignKey:UserID"`
}
type UserDetailModel struct {
ID int64
UserID int64
UserModel UserModel `gorm:"foreignKey:UserID"`
Email string `gorm:"size:64"`
}
针对于这个表结构,UserModel 中的 UserDetailModel 属于反向引用,UserDetailModel 中的 UserModel 属于正向引用。
1.1 下面,我们来看一看增删改查怎么操作??
1.1.1 插入数据
场景一:先插入 user ,在插入 user_detail
DB.Create(&UserModel{
Name: "加油旭杏",
UserDetailModel: &UserDetailModel{ // 在结构体中,是用指针来表示外键的
Email: "[email protected]",
}
})
场景二:给已有的 user 插入 user_detail
DB.Create(&UserDetailModel{
UserID: 2,
Email: "[email protected]",
})
场景三:知道 user,插入 user_detail
var user UserModel
DB.Take(&user, 2)
DB.Create(&UserDetailModel{
UserModel: user,
Email: "[email protected]",
})
1.1.2 查询数据
我们先来了解一下 Preload 的用法:
在 GORM 中,Preload 是用于进行关联加载的一个非常中阿更要的功能。他允许开发者在查询主模型的同时,预先加载相关联的子模型,以减少后续查询次数,提高查询效率。
基本用法:
假设我们有 User 和 Post 这两种模型,User 和 Post 之间是 一对多 的关系,代码如下:
type User struct { gorm.Model Name string Post []Post // 正向引用 } type Post struct { gorm.Model Title string UserID uint }
我们可以通过使用 Preload 方法来预加载 User 的 Posts:
var users []User db.Preload("Posts").Find(&users)
在这个例子中,Preload("Posts") 会在查询 User 的事后,自动将每一个用户的帖子也加载进来,这样就避免了 N + 1 查询问题。
预加载嵌套关系:
Preload 还支持多级嵌套的预加载,例如,如果 Post 还有一个评论模型 Comment ,还可以这样使用:
type Comment struct { gorm.Model Content string PostID uint } db.Preload("Post.Comments").Find(&users)
这样会在查询 User 的同时,也预加载每一个 User 的 Posts 及其 Comments。
条件预加载
可以对预加载进行条件限制,例如只加载特定条件下的关联:
db.Preload("Posts", "is_active = ?", true).Find(&users)
这样只会加载 is_active 为 true 的帖子。
使用多个 Preload
可以在一次查询中使用多个 Preload 来加载不同的关联:
db.Preload("Posts").Preload("Posts.Comments").Find(&users)
总结:
Preload 是 GORM 中非常强大的功能,他可以显著提高数据库查询的性能,简化数据处理的复杂性。通过合理使用 Preload,可以优化应用的性能,减少不必要的数据库请求。
场景一:根据用户查询用户详情
var user UserModel
DB.Preload("UserDetailModel").Take(&user, 2)
fmt.Println(user,Name, user.UserDetailModel.Email)
场景二:根据用户详情查询用户
var userDetail UserDetailModel
DB.Preload("UserModel").Take(&userDetail, "user_id = ?", 2)
fmt.Println(userDetail.Email, userDetail.UserModel.Name)
1.1.3 删除数据
有两种删除方法:级联删除和设置NULL。
针对多表关系下的删除,情况就有所不同了,一般会涉及到关联删除,例如,用户和用户详情,如果用户被删除了,那么用户详情就没有必要留着了,需要进行级联删除。在级联删除中,可以先删除再清空,也可以先清空再删除。代码如下:
var user UserModel
DB.Take(&user, 1)
DB.Delete(&user)
DB.Model(&user).Association("UserDetailModel").Clear()
这两种情况都是很常见的关联删除,可以有更加简单的方式实现,不过前提是必须要生成实体外键才行,而且修改关系之后要重新删除实体外键再生成。代码如下:
type UserModel struct {
ID int64
Name string `gorm:"size:32"`
Age int
UserDetailModel *UserDetailModel `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
// CASCADE 或者 SET NULL
}
二、一对多关系
在现实生活中,一对多的关系就比较多了,用户与文章,班级与学生,舔狗与女生。下面我们来看一看这样的代码:
type Girl struct {
ID int64
Name string `gorm:"size:32"`
BoyList []Boy `gorm:"foreignkey:GirlID"`
}
type Boy struct {
ID int64
GirlID int64
Name string `gorm:"size:32"`
Girl Girl `gorm:"foreignkey:GirlID"`
}
我们来看一看上述代码:那么对于 Girl 来说,Boy 就是他拥有的,叫做 Has Many,对于 boy 来说,Girl 就是他的归属,叫做:belong to。
2.1 插入数据
创建一个女生,连带两个舔狗,代码如下:
DB.Create(&Firl{
Name: "南岸",
BoyList: []Boy{
{Name: "张三"},
{Name: "王茹"},
},
})
创建出 boy,自带女生,代码如下:
DB.Create(&Boy{
Name: "加油",
Girl: Girl{
Name: "露露",
},
})
创建出 Boy,然后关联出已有女神
var girl Girl
DB.Create(&girl, "name=?", "露")
DB.Create(&Boy{
Name: "莉莉丝",
Girl: girl,
})
2.2 关联查询
我们需要进行查女神, 连带将其舔狗也查出来,代码如下:
var girl Girl
DB.Proload("Boyist").Take(&girl, "name=?", "露露")
fmt.Println(girl)
现在,我们需要在预处理的时候带上条件,代码如下:
girl = Girl{}
DB.Proload("BoyList", "name = ?", "凤凤").Take(&girl, "name = ?", "露露")
fmt.Println(girl)
在看下一句代码之前,我们先来看一看 Take 和 Find 的区别:
在 GORM 中,Take 和 Find 的主要区别在于他们的使用场景和返回的结果:
Take:
Take 方法用于从数据库中随机获取一条记录,他会返回一条记录,如果没有找到,则会返回一个错误
实例代码:var user User result := db.Take(&user)
Find:
Find 方法用于根据条件查找多条记录,如果查询到记录,他会将结构填充到传入的切片中;如果没有记录,他不会返回错误,但是返回的切片是空的
实例代码:
var users []User result := db.Find(&users)
总结来说,Take 用于获取单条记录,而Find用于获取多条记录。
在 GORM 中,Association 函数用于处理模型之间的关联关系。GORM 是一个流行的 Go 语言 ORM(对象关系映射)库,允许开发者方便地与数据库进行交互。
Association 函数的主要功能:
添加关联:可以使用 Association 方法将新的关联记录添加到当前模型。例如:向一个 User 模型汇总添加一个新的 Post 记录:
db.Model(&user).Association("Posts").Apppend(&post)
删除关联:可以删除已经存在的关联记录:
db.Model(&user).Association("Posts").Delete(&post)
查找关联:可以查询与当前模型相关的记录:
var posts []Post db.Model(&user).Association("Posts").Find(&posts)
清除关联:可以清除当前模型与关联模型之间的所有关系:
db.Model(&usr).Association("Posts").Clear()
替换关联:可以使用新的关联替换当前的关联记录:
db.Model(&user).Association("Posts").Replace(&newPosts)
现在,我们需要进行查询女神的舔狗列表,然后进行打印出来,代码如下:
girl = Girl{}
DB.Take(&girl, "name = ?", "Lulu")
var boylist []Boy
DB.Model(girl).Association("BoyList").Find(&boyList)
fmt.Println(boyList)
如果我们想要查询女神的舔狗总数,代码如下:
count := DB.Model(girl).Association("BoyList").Count()
fmt.Println(count)
2.3 关联操作
1号和2号不舔了,换成了3号,代码如下:
var b3 = Boy {
ID: 3,
}
girl := Girl{}
DB.Take(&girl, "name = ?", "lulu")
DB.Model(&girl).Association("BoyList").Replace([]Boy{b3})
如果说舔狗都不舔了,则需要进行清空数组列表,代码如下:
girl := Girl{}
DB.Take(&girl, "name = ?", "lulu")
DB.Model(&girl).Association("BoyList").Clear()
当1号和3号又开始重新舔的话,我们需要将其加入到舔狗列表中,代码如下:
girl := Girl{}
DB.Take(&girl, "name = ?", "lulu")
DB.Model(&girl).Association("BoyList").Append([]Boy{{ID:1}, {ID:3}})
只有3退出了,我们需要将其从舔狗列表中删除,代码如下:
girl := Girl{}
DB.Take(&girl, "name = ?", "lulu")
DB.Model(&girl).Assocaiton("BoyList").Delete([]Boy{{ID"3}})
三、多对多关系
生活中的多对多关系就更多了,文章和标签,书和作者,舔狗和女神,多对多关系就必须借助第三张表来进行关联,例如文章和标签,如图所示:
很容易看出,文章1有两个标签,分别是1和2,文章2有一个标签是1,所以在Gorm中,如何进行实现的呢??重新创建出一个新的表,将两个表关联起来:
type Airticle struct {
ID int64
Title string `gorm:"size:32"`
TagList []Tag `gorm:"many2many:article_tags;"`
}
type Tag struct {
ID int64
Title int64
ArticleList []Article `gorm:"many2many:article_tags;"`
}
3.1 插入数据
当我们创建出文章,并创建标签,代码如下:
article := Article{Title: "文章1", TagList: []Tag{
{Title: "go"},
{Title: "python"},
}}
DB.Create(&article)
创建出文章,选择已有标签,代码如下:
tagIDList := []int64{1, 2}
var tagList []Tag
DB.Find(&tagList, "id in ?", tagIDList)
DB.Create(&Article{
Title: "文章2",
TagList: tagList,
})
3.2 关联查询
查询文章列表,并把标签带出来,代码如下:
var articleList []Article
DB.Preload("TagList").Find(&articleLsit)
fmt.Println(articleList)
3.3 关联操作
更新一篇文章的标签列表,代码如下:
var article Article
DB.Take(&article, "title = ?", "文章1")
DB.Model(&article).Association("TagList").Replace([]Tag{
{ID:1},
{Title: "后端"},
})
3.4 自定义第三张表
以用户收藏文章为例,就是用户与文章的多对多关系,如果使用默认的第三张表,用户想知道什么时候收藏的某一篇文章,就没有办法了。
下面,我们来看一看表的结构,代码如下:
type UserModel struct {
ID int64
Name string
CollArticleList []ArticleModel `gorm:"many2many:user2_article_models;joinForeignKey:UserID;JoinReferences:ArticleID"`
}
type ArticleModel struct {
ID int64
Title string `gorm:"size:32"`
}
// 自定义的第三张表
type User2ArticleModel struct {
UserID int64 `gorm:"primaryKey"`
UserModel UserModel `gorm:"foreignKey:UserID"`
ArticleID int64 `gorm:"primaryKey"`
ArticleModel ArticleModel `gorm:"foreignKey:ArticleID"`
CreateAt time.Time `json:"createdAt"`
}
- 重点是many2many 生成的表的名字要和第三张表的名字要对上。
- 然后joinForeignKey 对应的是本表的ID,例如用户表的joinForeignKey 就是 UserID
- JoinReference 对应就是对方表的ID,用户表的 JoinReference 就是 ArticleID
- 然后就是要添加 SetupJoinTable ,不然是不会走第三张表的创建钩子的。
// 必须要加上这个才会走第三张表的创建钩子
DB.SetupJoinTable(&UserModel{}, "CollArticleList", &user2ArticleModel{})
在看下一句代码之前,我们先来看一看 select 函数的用法:
在 GORM 中,Select 函数用于指定查询时要选择的字段。这使得你可以只获取特定的列,而不是默认获取整个记录。使用 select 可以提供性能, 减少数据传输量,尤其是在处理大表的时候:
选择特定的字段:
var users []string { Name string Email string } db.Table("users").Select("name", "email").Find(&users)
与其他方法结合使用:
Select 可以与 Where、Order 等其他方法结合使用:
var users []User db,Select("name", "email").Where("active = ?", true).Find(&users)
选择计算的字段:
Select 也可以用来选择计算字段
var result []strust { TitalUsers int ActiveUsers int } db.Table("users"). Select("count(*) as total_users, sum(case when active then 1 else 0end) as active_users). Scan(&result)
总结:
Select函数使得你可以灵活地控制查询中返回的数据字段,适用于需要优化性能或者只对部分字段感兴趣的场景。
然后创建、删除就和之前是一样的,代码如下:
DB.Create(&UserModel{
Name: "张三",
CollArticleLsit: []ArticleModel{
{Title: "文章1"},
{Title: "文章2"},
},
})
var user UserModel
DB.Take(&user)
DB.Select("CollArticleList").Delete(&user)
3.5 查询操作
查询操作不能直接使用 Proload 方法,因为 Proload 方法查询出来的是对方表,是没有收藏时间的。所以常规操作是直接查询第三张表。
type UserCollArtcleResponse struct {
Name string
UserID int64
ArticleTitle string
ArticleID int64
Date time.Time
}
var userID = 4
var userArticleList []models.User2ArticleModel
var collList []userCollArticleResponse
global.DB.Preload("UserModel").Preload("ArticleModel").Find(&userArticleList, "user_id = ?", userID)
for _, model := range userArticleList {
collList = append(collList, UserCollArticleResponse{
Name: model.UserModel.Name
UserID: model.UserID,
ArticleTitle: model.ArticleModel.Title,
ArticleID: model.ArticleID,
Date: model.CreateAt,
})
}