FanXing Blog

FanXing Blog

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

Day3. Go語言精進之路:GORM 外鍵

今天學 GORM 的外鍵。


關於 GORM#

GORM 是 Go 語言中我認為好用的 ORM 庫,對開發人員非常友好。擁有以下特性:

  • 全功能 ORM
  • 關聯 (Has One,Has Many,Belongs To,Many To Many,多態,單表繼承)
  • Create,Save,Update,Delete,Find 中鉤子方法
  • 支持 PreloadJoins 的預加載
  • 事務,嵌套事務,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 會與另一個模型建立一對一關聯,這種模型的每一個實例都 "屬於" 另一個模型的實例,也就是一對一關聯對照。

image

例如:一個員工只屬於一家公司。

type User struct {
	gorm.Model
	Name      string
	CompanyID int
	Company   Company
}

type Company struct {
	ID   int
	Name string
}

這裡的 UserCompany 就是建立了 Belongs To 關係。每個 User 只能分配一個 CompanyUser 結構體中,CompanyID 就是用來存儲 User 對應的 CompanyID

image

GORM 很智能,當我們在結構體中,有一個 Company 類型的字段,並且還有一個 CompanyID 字段,那麼 GORM 會自動推斷,它會認為,開發者想讓 UserCompany 建立 Belongs To 關係,所以,會自動將 CompanyID 指向 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 通常會使用數據表的主鍵作為外鍵參考,如同上面 UserCompany 的例子,就是使用了 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 配置 OnUpdateOnDelete 實現外鍵約束,例如:

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,所以外鍵 CompanyIDUser 表中。
  • 當 User Has One Company 時Company 是被擁有方,也可以理解為 Company 屬於 User,所以外鍵 UserID 就放在 Company 中。

基於我目前的理解,我認為 Has OneBelongs to 的區別,也就僅僅是語義上的區別,這二者其實都只是 一對一關係。但是為了更加清晰的表達不同的關係,方便開發者定義不同的模型,所以在語義上,區分了 "包含" 和 "屬於"。

重寫外鍵#

都是 一對一關係,那麼 Has OneBelongs 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
}

這裡就是指定 CreditCardUserName 字段作為外鍵。

重寫引用#

我們可以使用 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
}

這裡就是,將 CreditCardUserName 作為外鍵,並且是將 UserName 字段填充進 UserName,也就是將 User.NameCreditCard.UserName 做一個綁定。

自引用 Has One#

我們經常會遇到一些場景,需要將同表的數據關聯至同表的另一個記錄,這就是自引用,當這種關係是一對一時,我們可以稱之為:自引用 Has One

案例如下:

type User struct {
  gorm.Model
  Name      string
  ManagerID *uint
  Manager   *User
}

在這個案例中,就定義了這樣的關係,表示用戶(員工)和其經理(也是用戶)之間的關係。這樣的模型定義,可以作為 AFF 拉新分成的數據模型,也就是用戶註冊時可以寫邀請碼,邀請碼對應了上級,然後通過 自引用 去綁定上級,最終實現分潤和上下級綁定。

Has Many#

Has Many 會建立一對多關係,不同於 Has OneHas 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 關係會在兩張表中創建一張連接表,是處理多對多關係的方案。

例如,我們產品中存在 LanguageUser,可以說一個 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 的連接表,負責處理 LanguageUser 之間的多對多關係。這裡,GORM 會自動創建該連接表。

反向引用#

反向引用 並不是一個專業術語,但是這個詞可以很好地描述出 Many To Many雙向性。當我們定義了一個多對多關係時,比如 UserLanguage ,我們不僅可以從 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

如果需要重寫,那麼需要將兩個外鍵的 referenceforeignkey 全部重寫,當然,也可以選擇只重寫其中任意一個,根據需求而定。

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 在不同的關係中,表示的含義有細微的不同,我們可以從 直接間接 來理解:

  1. 直接關係 (Has One, Belongs To, Has Many)
    • 外鍵直接存在於 參與關係 的兩個模型中的某一個模型中
    • 在這種情況下,foreignKey 指的就是 持有外鍵的模型中外鍵字段名
      • Has One:指 被 擁有方模型 的外鍵字段名
        • 比如:Company 的 UserID
      • Belongs To:指 擁有方模型 的外鍵字段名
        • 比如:User 的 CompanyRefer
    • references 指的則是 被引用方 模型中,foreignKey 所引用的字段。
      • 比如:User 的 ID
  2. 間接關係 (Many To Many)
    • 外鍵 存於 獨立的連接表 中,由連接表連接兩個模型的關聯關係
    • 這裡的 tag 更多是用來 配置連接表如何與兩個模型關聯
    • 對於 Many To Many 關係,我認為 擁有方被擁有方 的關係是很模糊的,所以這裡不使用這種詞代指兩個模型,而是採用 A 模型B 模型 的方法。
      • foreignKeyA 模型 中被連接表外鍵引用的字段名
      • joinForeignKey 指的是 連接表 中指向 A 模型 的外鍵名
      • references 指的則是 B 模型 中被連接表外鍵引用的字段名
      • joinReferences 指的是 連接表 中指向 B 模型 的外鍵名

帶著這些概念,再去看上面的案例,其實就很好懂了,A 模型 可以代指 UserB 模型 則代指 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{})

總結#

沒啥總結,其實是想寫點東西,但是技術太強大了太廢物了。

下一期可能會出關聯模式。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。