今天學 GORM 的外鍵。
關於 GORM#
GORM
是 Go 語言中我認為好用的 ORM 庫,對開發人員非常友好。擁有以下特性:
- 全功能 ORM
- 關聯 (Has One,Has Many,Belongs To,Many To Many,多態,單表繼承)
- Create,Save,Update,Delete,Find 中鉤子方法
- 支持
Preload
、Joins
的預加載 - 事務,嵌套事務,Save Point,Rollback To Saved Point
- Context、預編譯模式、DryRun 模式
- 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表達式、Context Valuer 進行 CRUD
- SQL 構建器,Upsert,數據庫鎖,Optimizer/Index/Comment Hint,命名參數,子查詢
- 複合主鍵,索引,約束
- Auto Migration
- 自定義 Logger
- 靈活的可擴展插件 API:Database Resolver(多數據庫,讀寫分離)、Prometheus…
- 每個特性都經過了測試的重重考驗
- 開發者友好
外鍵#
在關係型數據庫中,外鍵 (是一個) 或是一組字段,用於建立兩個表數據之間的連接。外鍵通常是指向另一個表的主鍵,它的主要作用是:
- 關聯性(Relationship):將一個表中的記錄與另一個表中的記錄關聯起來。
- 數據完整性(Data Integrity):確保引用的數據是有效的。例如:不能創建一個用戶不存在的訂單,也就是訂單對應的用戶根本不存在於數據庫中。
GORM 遵循 ” 約定優於配置 “ 的原則,能夠自動判斷出對應的外鍵關係。
注意:部分數據庫不會為外鍵自動創建索引。
Belongs To#
Belongs To
會與另一個模型建立一對一關聯,這種模型的每一個實例都 "屬於" 另一個模型的實例,也就是一對一關聯對照。
例如:一個員工只屬於一家公司。
type User struct {
gorm.Model
Name string
CompanyID int
Company Company
}
type Company struct {
ID int
Name string
}
這裡的 User
和 Company
就是建立了 Belongs To
關係。每個 User
只能分配一個 Company
。User
結構體中,CompanyID
就是用來存儲 User
對應的 Company
的 ID
。
GORM 很智能,當我們在結構體中,有一個 Company
類型的字段,並且還有一個 CompanyID
字段,那麼 GORM 會自動推斷,它會認為,開發者想讓 User
與 Company
建立 Belongs To
關係,所以,會自動將 Company
的 ID
指向 User.CompanyID
字段,也就是建立外鍵關係,這是 GORM 的約定。
CompanyID
字段必須在 User
結構體中,GORM 才能識別並推斷,沒有這個字段,GORM 就無法去將記錄鏈接到 Company
表中的記錄,字段名也是一種約定,必須為 ModelName + ID
這樣才可以。
為什麼還會有一個類型為 Company
的字段呢?因為在查詢 User
的數據時,可以使用 GORM 的 預加載 功能同時將對應的 Company
信息加載到 User
結構體中。GORM 會通過 CompanyID
這個字段,去 Company
表中查詢對應的數據。
重寫外鍵#
要定義一個 Belongs to
關係的外鍵,默認情況下,外鍵字段名是:擁有者的類型名稱+表的主鍵的字段名字
,也就是 Company
是類型名,ID
是主鍵字段名。
但是我們可以使用一些 tag
來指定外鍵名,例如:
type User struct {
gorm.Model
Name string
CompanyRefer int
Company Company `gorm:"foreignKey:CompanyRefer"`
// 使用 CompanyRefer 作為外鍵
}
type Company struct {
ID int
Name string
}
重寫引用#
在 Belongs to
中,GORM 通常會使用數據表的主鍵作為外鍵參考,如同上面 User
和 Company
的例子,就是使用了 Company
的主鍵 ID
作為外鍵。
同樣的,也可以使用 tag
來指定,例如:
type User struct {
gorm.Model
Name string
CompanyID string
Company Company `gorm:"references:Code"` // 使用 Code 作為引用
// 這樣 GORM 就會將 Company.Code 自動填充至 CompanyID 作為外鍵
}
type Company struct {
ID int
Code string
Name string
}
但是如果外鍵名恰好在擁有者類型中存在,GORM 通常會錯誤的誤認為是 Has One
的關係,所以需要手動指定,例如:
type User struct {
gorm.Model
Name string
CompanyID int
Company Company `gorm:"references:CompanyID"` // use Company.CompanyID as references
}
type Company struct {
CompanyID int
Code string
Name string
}
外鍵約束#
我們還可以通過 constraint
配置 OnUpdate
和 OnDelete
實現外鍵約束,例如:
type User struct {
gorm.Model
Name string
CompanyID int
Company Company `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
type Company struct {
ID int
Name string
}
Has One#
Has One
與另一個模型建立一對一關係,但是與 Belongs to
不太一樣。Has One
表示一個模型的每個實例都 "擁有" 另一個模型的一個實例。
可以這樣理解:
// Belongs to
// User 屬於 Company
// 這裡就是 User 屬於 Company,也就是用戶屬於這家公司,是這家公司的員工的意思
type User struct {
gorm.Model
Name string
CompanyID int // 外鍵:存儲所屬 Company 的 ID
Company Company // Belongs To 關係聲明
}
type Company struct {
ID int // Company 的主鍵
Name string
}
// Has One
// User 擁有一條 Company 記錄
// 這裡就是,User 擁有一個 Company,也就是這個用戶創辦了一個公司
type User struct {
gorm.Model
Name string
Company Company // Has One 關係聲明
}
type Company struct {
ID int // Company 的主鍵
Name string
UserID uint // 外鍵:存儲所屬 User 的 ID
}
核心規則:外鍵總是放在 “引用” 或 “從屬” 的那一方。
當 User Belongs to Company 時
:User
是從屬方,表達的含義是User
屬於Company
,所以外鍵CompanyID
在User
表中。當 User Has One Company 時
:Company
是被擁有方,也可以理解為Company
屬於User
,所以外鍵UserID
就放在Company
中。
基於我目前的理解,我認為 Has One
與 Belongs to
的區別,也就僅僅是語義上的區別,這二者其實都只是 一對一關係
。但是為了更加清晰的表達不同的關係,方便開發者定義不同的模型,所以在語義上,區分了 "包含" 和 "屬於"。
重寫外鍵#
都是 一對一關係
,那麼 Has One
和 Belongs to
一樣,都必須存在外鍵字段。默認是由 Has one
的模型類型 + 主鍵
生成,對於 Company
這個例子,就是 UserID
了。
但是我們也可以使用 tag
來指定另一個字段來作為外鍵:
type User struct {
gorm.Model
CreditCard CreditCard `gorm:"foreignKey:UserName"` // 使用 UserName 作為外鍵
}
type CreditCard struct {
gorm.Model
Number string
UserName string
}
這裡就是指定 CreditCard
的 UserName
字段作為外鍵。
重寫引用#
我們可以使用 foreignKey
來指定外鍵,也就是對應模型中作為外鍵的字段。當然,也可以指定目前模型中的引用,也就是填充到外鍵模型中的字段:
type User struct {
gorm.Model
Name string `gorm:"index"`
CreditCard CreditCard `gorm:"foreignKey:UserName;references:Name"`
}
type CreditCard struct {
gorm.Model
Number string
UserName string
}
這裡就是,將 CreditCard
的 UserName
作為外鍵,並且是將 User
的 Name
字段填充進 UserName
,也就是將 User.Name
與 CreditCard.UserName
做一個綁定。
自引用 Has One#
我們經常會遇到一些場景,需要將同表的數據關聯至同表的另一個記錄,這就是自引用,當這種關係是一對一時,我們可以稱之為:自引用 Has One。
案例如下:
type User struct {
gorm.Model
Name string
ManagerID *uint
Manager *User
}
在這個案例中,就定義了這樣的關係,表示用戶(員工)和其經理(也是用戶)之間的關係。這樣的模型定義,可以作為 AFF 拉新分成的數據模型,也就是用戶註冊時可以寫邀請碼,邀請碼對應了上級,然後通過 自引用 去綁定上級,最終實現分潤和上下級綁定。
Has Many#
Has Many
會建立一對多關係,不同於 Has One
,Has Many
指的是擁有多個,也就是一個擁有多個。比如:
// User 有多張 CreditCard,UserID 是外鍵
type User struct {
gorm.Model
CreditCards []CreditCard
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
這裡就是一個經典的 Has Many
關係,一個 User
可以有多個 CreditCard
。
重寫外鍵#
還是使用 foreignKey
,這裡不再演示。
重寫引用#
使用 references
,不再演示。
自引用 Has Many#
自引用 Has Many
與 自引用 Has One
類似,也就是在同一個數據表中的不同記錄之間創建關聯。不同的地方是,Has Many
表示一個記錄可以關聯到多個同類型的其他記錄。
例如:
type User struct {
gorm.Model
Name string
ManagerID *uint
Team []User `gorm:"foreignkey:ManagerID"`
}
這個例子就是,一個用戶管理層 (User) 可以有多個下屬 (User)。
與 自引用 Has One 的區別就是,Has One 更加側重於查找用戶的上級,而 Has Many 則更側重於查找用戶的下級。
Many To Many#
Many To Many
關係會在兩張表中創建一張連接表,是處理多對多關係的方案。
例如,我們產品中存在 Language
和 User
,可以說一個 User
包含多種 Language
,也可以說一個 Language
被多個 User
所屬。
// User 擁有並屬於多種 language,`user_languages` 是連接表
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
}
那麼,這裡就會創建一張 user_languages
的連接表,負責處理 Language
和 User
之間的多對多關係。這裡,GORM 會自動創建該連接表。
反向引用#
反向引用 並不是一個專業術語,但是這個詞可以很好地描述出 Many To Many
的 雙向性。當我們定義了一個多對多關係時,比如 User
和 Language
,我們不僅可以從 User
去查詢這個 User
所使用的 Language
,也可以從 Language
查詢有哪些 User
使用該語言。也就是可以 "反過來查",從 "另一端" 加載數據的能力,就被稱為反向引用。
例如:
// User 擁有並屬於多種 language,`user_languages` 是連接表
type User struct {
gorm.Model
Languages []*Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
Users []*User `gorm:"many2many:user_languages;"`
}
這就是一個很經典的反向引用案例,二者指向了同一個連接表 user_languages
,並且都定義了 關聯關係。
我們可以很方便的使用 預加載 去加載關聯數據,例如:
// 檢索 User 列表並預加載 Language(正向)
func GetAllUsers(db *gorm.DB) ([]User, error) {
var users []User
err := db.Model(&User{}).Preload("Languages").Find(&users).Error
return users, err
}
// 檢索 Language 列表並預加載 User(反向)
func GetAllLanguages(db *gorm.DB) ([]Language, error) {
var languages []Language
err := db.Model(&Language{}).Preload("Users").Find(&languages).Error
return languages, err
}
反向引用 這個功能也恰好說明了,GORM 中的 Many To Many
關係,是對稱的。通過在兩個模型之間使用 Many To Many
並指向同一張連接表,GORM 就會具備從任何一方查詢關聯數據的能力。
重寫外鍵#
對於 Many To Many
的重寫外鍵,與一般的重寫外鍵不一樣,因為該關係需要一張連接表,而連接表中至少存在兩個外鍵,分別是兩個模型的外鍵。比如上面的例子,那麼 user_languages
這個連接表中,就會有下面兩個外鍵:
// 連接表:user_languages
// foreign key: user_id, reference: users.id
// foreign key: language_id, reference: languages.id
如果需要重寫,那麼需要將兩個外鍵的 reference
和 foreignkey
全部重寫,當然,也可以選擇只重寫其中任意一個,根據需求而定。
type User struct {
gorm.Model
Profiles []Profile `gorm:"many2many:user_profiles;foreignKey:Refer;joinForeignKey:UserReferID;References:UserRefer;joinReferences:ProfileRefer"`
Refer uint `gorm:"index:,unique"`
}
type Profile struct {
gorm.Model
Name string
UserRefer uint `gorm:"index:,unique"`
}
這裡使用 many2many
指定 ManyToMany
關係和連接表名稱。然後使用 foreignKey
來指定 User
模型中與 user_profiles
表關聯的字段為 Refer
,然後使用 JoinForeignKey
來指定外鍵名稱為 UserReferID
。使用 References
來指定 Profile
模型中與 user_profiles
表關聯的字段為 UserRefer
,然後使用 JoinReferences
來指定外鍵名稱為 ProfilesRefer
。
所以最終的表會是:
// 會創建連接表:user_profiles
// foreign key: user_refer_id, reference: users.refer
// foreign key: profile_refer, reference: profiles.user_refer
這裡涉及到的 tag
比較亂,所以我會詳細解釋一下。
在 GORM 中,提供了一些 tag
來支持重寫外鍵的功能,但是這些 tag
在不同的關係中,表示的含義有細微的不同,我們可以從 直接 和 間接 來理解:
- 直接關係 (Has One, Belongs To, Has Many)
- 外鍵直接存在於 參與關係 的兩個模型中的某一個模型中
- 在這種情況下,
foreignKey
指的就是 持有外鍵的模型中 的 外鍵字段名- Has One:指 被 擁有方模型 的外鍵字段名
比如:Company 的 UserID
- Belongs To:指 擁有方模型 的外鍵字段名
比如:User 的 CompanyRefer
- Has One:指 被 擁有方模型 的外鍵字段名
references
指的則是 被引用方 模型中,foreignKey
所引用的字段。比如:User 的 ID
- 間接關係 (Many To Many)
- 外鍵 存於 獨立的連接表 中,由連接表連接兩個模型的關聯關係
- 這裡的
tag
更多是用來 配置連接表如何與兩個模型關聯 的 - 對於
Many To Many
關係,我認為 擁有方 與 被擁有方 的關係是很模糊的,所以這裡不使用這種詞代指兩個模型,而是採用 A 模型 和 B 模型 的方法。foreignKey
指 A 模型 中被連接表外鍵引用的字段名joinForeignKey
指的是 連接表 中指向 A 模型 的外鍵名references
指的則是 B 模型 中被連接表外鍵引用的字段名joinReferences
指的是 連接表 中指向 B 模型 的外鍵名
帶著這些概念,再去看上面的案例,其實就很好懂了,A 模型 可以代指 User
,B 模型 則代指 Profile
。
自引用 Many2Many#
Many To Many
關係指的是一個模型與自身建立多對多關係。這個功能使用最多的地方是好友功能,一個用戶可以有多個好友,並且可以是很多個人的好友。
type User struct {
gorm.Model
Friends []*User `gorm:"many2many:user_friends"`
}
// 會創建連接表:user_friends
// foreign key: user_id, reference: users.id
// foreign key: friend_id, reference: users.id
自定義連接表#
連接表
可以是個功能齊全的模型,比如支持:軟刪除
、鉤子函數
等功能,並且可以具有更多的字段,我們可以通過 SetupJoinTable
函數設置,例如:
自定義連接表要求外鍵是複合主鍵或複合唯一索引。
type Person struct {
ID int
Name string
Addresses []Address `gorm:"many2many:person_addresses;"`
}
type Address struct {
ID uint
Name string
}
type PersonAddress struct {
PersonID int `gorm:"primaryKey"`
AddressID int `gorm:"primaryKey"`
CreatedAt time.Time
DeletedAt gorm.DeletedAt
}
func (PersonAddress) BeforeCreate(db *gorm.DB) error {
// ...
}
// 修改 Person 的 Addresses 字段的連接表為 PersonAddress
// PersonAddress 必須定義好所需的外鍵,否則會報錯
err := db.SetupJoinTable(&Person{}, "Addresses", &PersonAddress{})
總結#
沒啥總結,其實是想寫點東西,但是技術太強大了太廢物了。
下一期可能會出關聯模式。