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)
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。