今日は 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
- コンテキスト、プリコンパイルモード、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 は別のモデルと一対一の関連を確立します。このモデルの各インスタンスは「別のモデルのインスタンスに属する」ということです。
例えば:一人の従業員は一つの会社にしか属しません。
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"` // Company.CompanyID を参照として使用
}
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 が Company に属する場合:Userは従属側であり、意味するところはUserがCompanyに属するため、外部キーCompanyIDはUserテーブルにあります。User が 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{})
まとめ#
特にまとめることはありません、実際には何かを書きたかったのですが、技術があまりにも強力すぎてあまりにも無力でした。
次回はおそらく関連モデルについて出す予定です。