今天學 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)