FanXing Blog

FanXing Blog

一个热爱编程的高中生,正在努力成为一位优秀的后端工程师~

Day4. Go语言精进之路:GORM 关联模式

今天学 GORM 的关联模式,其实这是打算昨天学的,但是昨天被 beyond Many To Many 耽误太久,所以挪到今天了。


Polymorphism (多态关联)#

怎么还有个这玩意没学,在 GORM 中,多态关联这个特性很强大,它允许一个模型可以属于多种不同的父模型,而不需要为每种父模型都创建单独的外键字段。

比如,ToyCat, Dog 的例子,这种能力的实现,是根据 Toy 中的两个字段实现的:

  1. 父模型的主键 ID
  2. 父模型的类型

GORM 支持 Has OneHas Many 关系的多态关联。

默认约定#

在我们使用 多态关联 时,会有一些默认的约定:

  • 类型字段 (Type Field):GORM 会在子模型表中自动创建一个字段来存储父模型的类型。默认情况下,字段的名字是 polymorphic 指定的值加上 Type
  • ID 字段 (ID Field):GORM 会在子模型表中自动创建一个字段来存储父模型的主键 ID。默认情况下,字段的名字是 polymorphic 指定的值加上 ID
  • 类型值 (Type Value):存储在类型字段中的值,默认是父模型对应的数据表的复数。
// 定义 Dog 模型
type Dog struct {
  ID   int
  Name string
  // Dog 有多个 Toy,使用多态关联
  // gorm:"polymorphic:Owner;" 指定了多态关联的前缀为 "Owner"
  Toys []Toy `gorm:"polymorphic:Owner;"`
}

// 定义 Toy 模型
type Toy struct {
  ID        int
  Name      string
  // OwnerID 存储父模型 (Dog) 的 ID
  OwnerID   int
  // OwnerType 存储父模型 (Dog) 的类型,默认为 "dogs"
  OwnerType string
}

// 创建记录
db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})

在这个案例中,OwnerType 将被设置为 dogs

自定义标签#

当然,我们也可以使用一些 tag 来自定义我们的多态关联:

  • polymorphicType:指定存储类型的字段名
  • polymorphicId:指定存储 ID 的字段名
  • polymorphicValue:指定存储在类型字段中的具体值
type Dog struct {
  ID   int
  Name string
  // 自定义多态关联:
  // polymorphicType:Kind -> 类型字段名为 Kind
  // polymorphicId:OwnerID -> ID 字段名为 OwnerID
  // polymorphicValue:master -> 类型值为 "master"
  Toys []Toy `gorm:"polymorphicType:Kind;polymorphicId:OwnerID;polymorphicValue:master"`
}

// 定义 Toy 模型
type Toy struct {
  ID        int
  Name      string
  // OwnerID 存储父模型 (Dog) 的 ID (由 polymorphicId 指定)
  OwnerID   int
  // Kind 存储父模型 (Dog) 的类型 (由 polymorphicType 指定),值为 "master" (由 polymorphicValue 指定)
  Kind      string
}

// 创建记录
db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})

关联模式#

GORM 提供了强大的功能来处理数据库模型之间的关联,这让开发变得高效。

自动创建、更新#

GORM 会在创建时,自动引用或更新其关联数据。例如:

type User struct {
  gorm.Model
  Languages []Language
}

type Language struct {
  gorm.Model
  Name string
  UserID uint
}

user := User{
  Languages:       []Language{
    {Name: "ZH"},
    {Name: "EN"},
  },
}

// 创建用户以及其关联的语言
db.Create(&user)

db.Save(&user)

这样的操作,GORM 会自动创建 Language 并且自动与 User 绑定 Has Many 的外键关系。

FullSaveAssociations#

FullSaveAssociations 是 GORM 为 Session 提供的一个参数,可以强制 GORM 完整的保存或更新所有关联的记录,而不只是处理关系

在我们不使用 FullSaveAssociations 参数时,或者说 FullSaveAssociations:false 时,默认的 GORM 是不会去处理所关联模型的变化的,而只是会检查其关联关系(也就是检查外键 ID 是否链接正确)。例如:

我们现在有以下模型:

type User struct {
  gorm.Model
  Name           string
  Age int
  BillingAddress Address // Has One 关系
  BillingAddressID uint
}

type Address struct {
  gorm.Model
  Address1 string
  Address2 string
  PostCode string
}

我们执行以下操作:

// 查询用户及其关联地址
var user User
db.Preload("BillingAddress").First(&user, 1)
// user.ID = 1
// user.Name = "jinzhu"
// user.BillingAddressID = 10
// user.BillingAddress.ID = 10
// user.BillingAddress.Address1 = "Old Billing St"
// user.BillingAddress.Address2 = "Unit 1"
// user.BillingAddress.PostCode = "10000"

// 修改用户和地址的信息
user.Name = "jinzhu_updated"
user.BillingAddress.Address1 = "New Billing Avenue" // 修改了关联对象的字段
user.BillingAddress.PostCode = "20000"         // 修改了关联对象的字段

db.Save(&user)
// 或者 db.Updates(&user)

在不使用 FullSaveAssociations:true 时,BillingAddress 的信息不会被更新,也就是,在 user 模型上对 BillingAddress 模型的修改是无效的。在使用 FullSaveAssociations:true 后,再执行 db.Save(&user)BillingAddress 的更新会被关联更新。

也就是说,FullSaveAssociations 这个 tag,提供了一种更全面的数据同步方式,会在我们使用 SaveUpdates 时,其关联模型 (不管是一对一、一对多、还是多对多) 在 Go 代码中的修改也能被持久化到数据库中。

跳过自动创建、更新#

GORM 会自动在创建时,自动引用并创建或更新所关联对象的数据(如果有关联关系的话),但 GORM 也提供了灵活性,使用 SelectOmit 方法可以指定哪些字段可以被删除或被包含。

使用 Select 指定字段范围#

Select 方法可以选择模型中的哪些字段应该被保存,也就是说只有被选中的字段才会包含在 SQL 中。

var user User

user = User{
    Name: "gopher",
    Age: 21
}

db.Select("Name").Create(&user)

这里在数据库层面,就只会为 user 填充 name 字段,而我们所定义的 age 字段则会被抛弃忽略。

使用 Omit 来排除字段或关联#

Omit 允许我们在执行创建或更新操作时,指定跳过哪些字段或关联关系,这提供了更加精细的控制,决定哪些数据会被持久化。

一共有以下几种用法:

  1. 排除指定字段
// 创建用户时跳过字段“BillingAddress”
db.Omit("BillingAddress").Create(&user)
  1. 排除全部关联关系
// 创建用户时跳过全部关联关系
db.Omit(clause.Associations).Create(&user)
  1. 排除多对多关系
// 跳过更新"Languages"关联
db.Omit("Languages.*").Create(&user)

这个用法,仍然会保持 userlanguage 的多对多关联,但是会跳过对 language 的更新。也就是。如果 language 不存在,那么还是会创建,并关联;但是如果 language 已经存在,那么则不会进行更新。

  1. 排除创建关联及引用
// 跳过创建 'Languages' 关联及其引用
db.Omit("Languages").Create(&user)

这样会完全忽略 Language,在创建 user 时,既不会关联创建,也不会在连接表中处理关联关系。

Select/Omit 关联字段#

在 GORM 创建或更新记录时,可以使用 SelectOmit 来指定是否包含某个关联的字段。例如:

  1. Select
// 创建用户和他的账单地址,邮寄地址,只包括账单地址指定的字段
db.Select("BillingAddress.Address1", "BillingAddress.Address2").Create(&user)
// SQL: 只使用地址1和地址2来创建用户和账单地址

这行代码会正常处理 user 的创建,但是在创建 BillingAddress 时,只包含 Address1Address2 这两个字段,其他字段不会被包含在对 address 表的 SQL 中。

  1. Omit
// 创建用户和账单地址,邮寄地址,但不包括账单地址的指定字段
db.Omit("BillingAddress.Address2", "BillingAddress.CreatedAt").Create(&user)
// SQL: 创建用户和账单地址,省略'地址2'和创建时间字段

同样正常处理 user 的创建,但是在处理 BillingAddress 时,除了 Address2CreatedAt 这两个字段,其他全部包含。

也就是,Select 是指定只处理哪些,而 Omit 是指定排除哪些。

删除关联#

GORM 提供了一个机制,可以让我们在删除主模型记录时,通过 Select 去删除其关联的记录(一对一、一对多、多对多),这对于维护数据完整性很有用,可以将无用数据一并清理。

使用 Select 指定要删除的关联:

  • 删除单个关联
// 删除用户时,同时删除该用户的 Account 记录
db.Select("Account").Delete(&user)
  • 删除多个关联
// 删除用户时,同时删除该用户的 Orders 和 CreditCards 记录
db.Select("Orders", "CreditCards").Delete(&user)
  • 删除所有关联
// 删除用户时,删除该用户所有 has one, has many, many2many 类型的关联记录
db.Select(clause.Associations).Delete(&user)
  • 批量删除关联
// 删除 users 切片中每个用户时,也删除他们各自的 Account 记录
db.Select("Account").Delete(&users) // users 是一个 []*User 或 []User 切片

这样删除十分快捷,但是有一个重要前提:主键必须非零,GORM 需要利用主键 ID 作为条件来查找并删除相关记录。

关联模式#

GORM 提供了一系列便捷的方法来处理模型之间的关系,能让开发者高效的管理数据。

如果要开始使用关联模式,我们需要启用关联模式,也就是指定源模型和关联关系的字段名:

var user User // 假设 User 是你的模型,并且有一个名为 Languages 的关联字段
// 启动关联模式,操作 user 对象的 Languages 关联
db.Model(&user).Association("Languages")

// 你也可以检查在启动关联模式时是否发生错误
err := db.Model(&user).Association("Languages").Error
if err != nil {
    // 处理错误
}

Association 里填写的不是所关联的模型名称,而是主模型中定义的包含关联关系的字段名

查询关联#

使用 Find 可以检索关联的记录,也可以选择性的添加查询条件。

var languages []Language // 假设 Language 是关联的模型

// 简单查询:获取 user关联的所有 Language 记录
db.Model(&user).Association("Languages").Find(&languages)

// 带条件查询:获取 user 关联的 Language 记录,但只包括指定的 code
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages)

// 注意:使用 Where 条件时,它会应用在关联模型的查询上

这里的 where 条件,是落实在关联模型上的,如果我们需要 user 模型的条件呢?比如,我想要查找 user.id = 1Language,该如何处理?代码如下:

// 这里我认为也可以直接创建一个 user 类型的 model,将 id 改为 1
// 也就是:
// user := User{ID: 1}
var user User
result := db.First(&user, 1) // 查找主键为 1 的 User
if result.Error != nil {
    // 处理错误,比如用户未找到
    panic("failed to find user with id 1")
}

var languages []Language
// 现在 user 变量就代表 id 为 1 的用户
// GORM 会自动使用 user.ID 来过滤 languages
db.Model(&user).Association("Languages").Find(&languages)

// 如果你还想加上 code 的过滤条件,可以像之前那样链式调用:
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages)

追加关联#

Append 方法用于添加新的关联。在不同的关联关系中,Append 的效果不一:

  • 对于 多对多关系一对多关系Append 会添加新的关联记录
  • 对于 一对一关系 (Has One)属于关系 (Belongs to)Append 会替换掉当前关联
// 追加单个或多个已存在的 Language 对象
db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN})

// 追加一个新的 Language 对象(如果 Language 不存在,GORM 可能会尝试创建它,取决于你的设置)
db.Model(&user).Association("Languages").Append(&Language{Name: "DE"})

// 追加/替换 CreditCard (假设是 has one 或 belongs to 关系)
db.Model(&user).Association("CreditCard").Append(&CreditCard{Number: "411111111111"})

上面 Languages 不存在时,取决于设置来决定是否创建,其实就是 FullSaveAssociations 这个 tag,默认为 false,也就是不会创建。

替换关联#

替换关联会将当前主模型所有的关联关系替换为新的关联记录,也就是抛弃原关联关系,替换为新的。

// 将 user 的 Languages 关联替换为 languageZH 和 languageEN
db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN})

// 也可以传入多个独立的对象
db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN)

删除关联#

这里的 Delete 方法,用于移除主模型和指定参数模型之间的关联关系,这只会删除他们的引用,不会去删除关联对象本身。

// 删除 user 与 languageZH 和 languageEN 的关联
db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN})

// 也可以传入多个独立的对象
db.Model(&user).Association("Languages").Delete(languageZH, languageEN)

清空关联#

清空关联会移除主模型与该模型之间的所有关联关系。

// 清空 user 的所有 Languages 关联
db.Model(&user).Association("Languages").Clear()

关联计数#

关联计数可以用来获取当前关联记录的数量,也可以带条件计数:

// 获取 user 关联的所有 Languages 的数量
count := db.Model(&user).Association("Languages").Count()

// 带条件计数:获取 user 关联的 Language 中,code 在指定列表内的数量
codes := []string{"zh-CN", "en-US", "ja-JP"}
countWithCondition := db.Model(&user).Where("code IN ?", codes).Association("Languages").Count()

批量数据处理#

关联模式也支持对多个记录进行批量操作,包括查询、追加、替换、删除和计数关联数据。

批量查询关联#

var users []User
var roles []Role
// 假设 users 是一个 User 切片,查询这些 users 关联的所有 Role
db.Model(&users).Association("Role").Find(&roles)

批量删除关联#

var users []User
// 假设 users 是一个 User 切片,删除这些 users 与 userA 的 Team 关联
db.Model(&users).Association("Team").Delete(&userA)

批量计数关联#

var users []User
// 假设 users 是一个 User 切片,计算这些 users 关联的 Team 总数(或根据具体关系计数)
count := db.Model(&users).Association("Team").Count()

批量追加 / 替换关联#

var users = []User{user1, user2, user3} // 假设有三个用户

// 批量追加:
// - 给 user1 的 Team 追加 userA
// - 给 user2 的 Team 追加 userB
// - 给 user3 的 Team 追加 userA, userB, userC
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})

// 批量替换:
// - 将 user1 的 Team 替换为 userA
// - 将 user2 的 Team 替换为 userB
// - 将 user3 的 Team 替换为 userA, userB, userC
db.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC})

删除关联内容#

我们在使用 Replace, Delete, Clear 方法时,理解它们默认如何处理关联记录非常重要,这关联到数据完整性。

默认行为:仅更新引用#

默认情况下,这些方法主要影响的是外键引用连接表记录,而不是记录本身。

  • 更新引用:这些方法通常会将关联的外键设置为 NULL(对于 belongs to, has one, has many) 或者删除连接表中的对应行,通过这样来断开主模型和关联模型之间的链接。
  • 不物理删除记录:关联数据表中的实际记录不会被删除,这些记录仍然存在于数据库中,只是不在和主模型有关联关系。

使用 Unscoped 改变删除行为#

如果希望断开关联关系的同时,实际删除关联的记录,就需要使用 Unscoped 方法。

  1. 软删除关联记录(Soft Delete)

如果你的关联模型配置了软删除功能,那么可以使用 Unscoped() 来触发对关联记录的软删除。

  • 行为:这会将关联记录中的 deleted_at 字段设置为当前时间,将它们标记为已删除,但记录仍然在数据库中。
// 假设 Language 模型支持软删除 (有 gorm.DeletedAt 字段)
// 清除关联,并软删除 user 关联的所有 Language 记录
db.Model(&user).Association("Languages").Unscoped().Clear()

// 同样适用于 Delete 和 Replace
// db.Model(&user).Association("Languages").Unscoped().Delete(&languageZH)
  1. 物理删除关联记录

如果我们想彻底从数据库中物理删除关联记录,你需要结合使用两个 Unscoped()

  • 第一个 Unscoped() 应用在 db 上下文,指示 GORM 在后续操作中忽略软删除逻辑,执行删除逻辑。
  • 第二个 Unscoped() 应用在 Association 链中,确保对关联记录也使用物理删除。
// 物理删除:
// 1. db.Unscoped() 指示 GORM 忽略全局的软删除设定,进行物理删除操作。
// 2. Association("Languages").Unscoped() 确保对关联记录执行物理删除。

// 清除关联,并从数据库中物理删除 user 关联的所有 Language 记录
db.Unscoped().Model(&user).Association("Languages").Unscoped().Clear()

// 同样适用于 Delete
// db.Unscoped().Model(&user).Association("Languages").Unscoped().Delete(&languageZH)
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。