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
  • コンテキスト、プリコンパイルモード、DryRun モード
  • バルク挿入、FindInBatches、Find/Create with Map、SQL 表現、Context Valuer を使用した CRUD
  • SQL ビルダー、Upsert、データベースロック、Optimizer/Index/Comment Hint、命名パラメータ、サブクエリ
  • 複合主キー、インデックス、制約
  • 自動マイグレーション
  • カスタムロガー
  • 柔軟な拡張可能なプラグイン 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
}

ここでの UserCompanyBelongs To 関係を確立しています。各 User は一つの Company にしか割り当てられません。User 構造体の中で、CompanyIDUser に対応する CompanyID を格納するために使用されます。

image

GORM は非常に賢く、構造体に Company 型のフィールドがあり、さらに CompanyID フィールドがある場合、GORM は自動的に推論します。開発者が UserCompany の間に Belongs To 関係を確立したいと考えているとみなし、CompanyIDUser.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"` // Company.CompanyID を参照として使用
}

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 が Company に属する場合User は従属側であり、意味するところは UserCompany に属するため、外部キー CompanyIDUser テーブルにあります。
  • User が Company を持つ場合Company は所有側であり、CompanyUser に属することを意味するため、外部キー UserIDCompany に置かれます。

私の現在の理解に基づくと、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 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 関係は二つのテーブルに接続テーブルを作成し、多対多の関係を処理するためのソリューションです。

例えば、私たちの製品には 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 モデル を指す外部キー名を指します。
      • referencesB モデル の中で接続テーブルの外部キーが参照するフィールド名を指します。
      • 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{})

まとめ#

特にまとめることはありません、実際には何かを書きたかったのですが、技術があまりにも強力すぎてあまりにも無力でした。

次回はおそらく関連モデルについて出す予定です。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。