今日は 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{})
まとめ#
特にまとめることはありません、実際には何かを書きたかったのですが、技術があまりにも強力すぎてあまりにも無力でした。
次回はおそらく関連モデルについて出す予定です。