Bootstrap

【Gorm】多表关系

       在学习 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,
    })
}

;