今天学 GORM 的关联模式,其实这是打算昨天学的,但是昨天被 beyond Many To Many
耽误太久,所以挪到今天了。
Polymorphism (多态关联)#
怎么还有个这玩意没学,在 GORM 中,多态关联这个特性很强大,它允许一个模型可以属于多种不同的父模型,而不需要为每种父模型都创建单独的外键字段。
比如,Toy
和 Cat
, Dog
的例子,这种能力的实现,是根据 Toy
中的两个字段实现的:
- 父模型的主键 ID
- 父模型的类型
GORM 支持 Has One
和 Has 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
,提供了一种更全面的数据同步方式,会在我们使用 Save
或 Updates
时,其关联模型 (不管是一对一、一对多、还是多对多) 在 Go 代码中的修改也能被持久化到数据库中。
跳过自动创建、更新#
GORM 会自动在创建时,自动引用并创建或更新所关联对象的数据(如果有关联关系的话),但 GORM 也提供了灵活性,使用 Select
或 Omit
方法可以指定哪些字段可以被删除或被包含。
使用 Select 指定字段范围#
Select
方法可以选择模型中的哪些字段应该被保存,也就是说只有被选中的字段才会包含在 SQL 中。
var user User
user = User{
Name: "gopher",
Age: 21
}
db.Select("Name").Create(&user)
这里在数据库层面,就只会为 user
填充 name
字段,而我们所定义的 age
字段则会被抛弃忽略。
使用 Omit 来排除字段或关联#
Omit
允许我们在执行创建或更新操作时,指定跳过哪些字段或关联关系,这提供了更加精细的控制,决定哪些数据会被持久化。
一共有以下几种用法:
- 排除指定字段
// 创建用户时跳过字段“BillingAddress”
db.Omit("BillingAddress").Create(&user)
- 排除全部关联关系
// 创建用户时跳过全部关联关系
db.Omit(clause.Associations).Create(&user)
- 排除多对多关系
// 跳过更新"Languages"关联
db.Omit("Languages.*").Create(&user)
这个用法,仍然会保持 user
和 language
的多对多关联,但是会跳过对 language
的更新。也就是。如果 language
不存在,那么还是会创建,并关联;但是如果 language
已经存在,那么则不会进行更新。
- 排除创建关联及引用
// 跳过创建 'Languages' 关联及其引用
db.Omit("Languages").Create(&user)
这样会完全忽略 Language
,在创建 user
时,既不会关联创建,也不会在连接表中处理关联关系。
Select/Omit 关联字段#
在 GORM 创建或更新记录时,可以使用 Select
和 Omit
来指定是否包含某个关联的字段。例如:
- Select
// 创建用户和他的账单地址,邮寄地址,只包括账单地址指定的字段
db.Select("BillingAddress.Address1", "BillingAddress.Address2").Create(&user)
// SQL: 只使用地址1和地址2来创建用户和账单地址
这行代码会正常处理 user
的创建,但是在创建 BillingAddress
时,只包含 Address1
和 Address2
这两个字段,其他字段不会被包含在对 address
表的 SQL 中。
- Omit
// 创建用户和账单地址,邮寄地址,但不包括账单地址的指定字段
db.Omit("BillingAddress.Address2", "BillingAddress.CreatedAt").Create(&user)
// SQL: 创建用户和账单地址,省略'地址2'和创建时间字段
同样正常处理 user
的创建,但是在处理 BillingAddress
时,除了 Address2
和 CreatedAt
这两个字段,其他全部包含。
也就是,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 = 1
的 Language
,该如何处理?代码如下:
// 这里我认为也可以直接创建一个 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
方法。
- 软删除关联记录(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)
- 物理删除关联记录
如果我们想彻底从数据库中物理删除关联记录,你需要结合使用两个 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)