今日は GORM の関連モデルを学びます。実はこれは昨日学ぶ予定でした、しかし昨日は beyond Many To Many
に時間を取られてしまったので、今日に延期しました。
多態性(Polymorphism)#
どうしてこれをまだ学んでいないのか、GORM では、多態性の関連性は非常に強力な機能であり、1 つのモデルが異なる親モデルに属することを可能にし、各親モデルのために個別の外部キー フィールドを作成する必要がありません。
例えば、Toy
と Cat
, Dog
の例では、この能力は Toy
の 2 つのフィールドに基づいて実現されます:
- 親モデルの主キー ID
- 親モデルのタイプ
GORM は Has One
および Has Many
関係の多態性をサポートしています。
デフォルトの規約#
多態性の関連性 を使用する際には、いくつかのデフォルトの規約があります:
- タイプフィールド(Type Field):GORM は子モデルテーブルに親モデルのタイプを格納するためのフィールドを自動的に作成します。デフォルトでは、フィールド名は
polymorphic
で指定された値にType
を追加したものです。 - ID フィールド(ID Field):GORM は子モデルテーブルに親モデルの主キー ID を格納するためのフィールドを自動的に作成します。デフォルトでは、フィールド名は
polymorphic
で指定された値にID
を追加したものです。 - タイプ値(Type Value):タイプフィールドに格納される値は、デフォルトで親モデルに対応するデータテーブルの複数形です。
// Dog モデルの定義
type Dog struct {
ID int
Name string
// Dog は複数の Toy を持ち、多態性の関連を使用
// gorm:"polymorphic:Owner;" は多態性の関連のプレフィックスを "Owner" に指定
Toys []Toy `gorm:"polymorphic:Owner;"`
}
// Toy モデルの定義
type Toy struct {
ID int
Name string
// OwnerID は親モデル (Dog) の ID を格納
OwnerID int
// OwnerType は親モデル (Dog) のタイプを格納し、デフォルトは "dogs"
OwnerType string
}
// レコードの作成
db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})
このケースでは、OwnerType
は dogs
に設定されます。
カスタムタグ#
もちろん、私たちはいくつかの tag
を使用して多態性の関連をカスタマイズすることもできます:
polymorphicType
:タイプを格納するフィールド名を指定polymorphicId
:ID を格納するフィールド名を指定polymorphicValue
:タイプフィールドに格納される具体的な値を指定
type Dog struct {
ID int
Name string
// カスタム多態性の関連:
// polymorphicType:Kind -> タイプフィールド名は Kind
// polymorphicId:OwnerID -> ID フィールド名は OwnerID
// polymorphicValue:master -> タイプ値は "master"
Toys []Toy `gorm:"polymorphicType:Kind;polymorphicId:OwnerID;polymorphicValue:master"`
}
// Toy モデルの定義
type Toy struct {
ID int
Name string
// OwnerID は親モデル (Dog) の ID (polymorphicId で指定)
OwnerID int
// Kind は親モデル (Dog) のタイプ (polymorphicType で指定)、値は "master" (polymorphicValue で指定)
Kind string
}
// レコードの作成
db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})
関連モデル#
GORM はデータベースモデル間の関連を処理するための強力な機能を提供し、開発を効率的にします。
自動作成、更新#
GORM は作成時に自動的に関連データを参照または更新します。例えば:
type User struct {
gorm.Model
Languages []Language
}
type Language struct {
gorm.Model
Name string
UserID uint
}
user := User{
Languages: []Language{
{Name: "ZH"},
{Name: "EN"},
},
}
// ユーザーとその関連する言語を作成
db.Create(&user)
db.Save(&user)
この操作では、GORM は自動的に Language
を作成し、User
と Has Many
の外部キー関係を自動的にバインドします。
FullSaveAssociations#
FullSaveAssociations
は GORM がセッションに提供するパラメータで、GORM にすべての関連レコードを完全に保存または更新させることができます。単に関係を処理するのではなく。
FullSaveAssociations
パラメータを使用しない場合、または FullSaveAssociations:false
の場合、デフォルトの GORM は関連モデルの変更を処理せず、関連関係(つまり外部キー ID が正しくリンクされているかどうかを確認する)を確認するだけです。例えば:
以下のモデルがあります:
type User struct {
gorm.Model
Name string
Age int
BillingAddress Address // Has One 関係
BillingAddressID uint
}
type Address struct {
gorm.Model
Address1 string
Address2 string
PostCode string
}
次の操作を実行します:
// ユーザーとその関連住所をクエリ
var user User
db.Preload("BillingAddress").First(&user, 1)
// user.ID = 1
// user.Name = "jinzhu"
// user.BillingAddressID = 10
// user.BillingAddress.ID = 10
// user.BillingAddress.Address1 = "Old Billing St"
// user.BillingAddress.Address2 = "Unit 1"
// user.BillingAddress.PostCode = "10000"
// ユーザーと住所の情報を変更
user.Name = "jinzhu_updated"
user.BillingAddress.Address1 = "New Billing Avenue" // 関連オブジェクトのフィールドを変更
user.BillingAddress.PostCode = "20000" // 関連オブジェクトのフィールドを変更
db.Save(&user)
// または db.Updates(&user)
FullSaveAssociations:true
を使用しない場合、BillingAddress
の情報は更新されず、つまり user
モデル上での BillingAddress
モデルの変更は無効です。FullSaveAssociations:true
を使用した後に db.Save(&user)
を実行すると、BillingAddress
の更新が関連して更新されます。
つまり、FullSaveAssociations
という tag
は、より包括的なデータ同期方法を提供し、Save
または Updates
を使用する際に、関連モデル(1 対 1、1 対多、または多対多に関わらず)の Go コード内での変更も永続化されることを保証します。
自動作成、更新のスキップ#
GORM は作成時に自動的に関連オブジェクトのデータを参照し、作成または更新します(関連関係がある場合)。しかし、GORM は柔軟性も提供しており、Select
または Omit
メソッドを使用して、削除または含めるフィールドを指定できます。
Select を使用してフィールド範囲を指定#
Select
メソッドは、モデル内のどのフィールドを保存するべきかを選択できます。つまり、選択されたフィールドのみが SQL に含まれます。
var user User
user = User{
Name: "gopher",
Age: 21
}
db.Select("Name").Create(&user)
ここでは、データベースレベルで user
の name
フィールドのみが埋め込まれ、定義した age
フィールドは無視されます。
Omit を使用してフィールドまたは関連を除外#
Omit
は、作成または更新操作を実行する際に、どのフィールドまたは関連関係をスキップするかを指定でき、より細かい制御を提供し、どのデータが永続化されるかを決定します。
以下のような使い方があります:
- 指定フィールドを除外
// ユーザーを作成する際に "BillingAddress" フィールドをスキップ
db.Omit("BillingAddress").Create(&user)
- すべての関連関係を除外
// ユーザーを作成する際にすべての関連関係をスキップ
db.Omit(clause.Associations).Create(&user)
- 多対多関係を除外
// "Languages" 関連の更新をスキップ
db.Omit("Languages.*").Create(&user)
この使い方では、user
と language
の多対多関連は維持されますが、language
の更新はスキップされます。つまり、language
が存在しない場合は作成され、関連付けられますが、既に存在する場合は更新されません。
- 関連の作成および参照を除外
// 'Languages' 関連およびその参照の作成をスキップ
db.Omit("Languages").Create(&user)
これにより、Language
は完全に無視され、user
を作成する際に関連作成も行われず、接続テーブルでの関連関係も処理されません。
Select/Omit 関連フィールド#
GORM はレコードを作成または更新する際に、Select
と Omit
を使用して特定の関連フィールドを含めるかどうかを指定できます。例えば:
- Select
// ユーザーとその請求住所、郵送住所を作成し、請求住所の指定フィールドのみを含める
db.Select("BillingAddress.Address1", "BillingAddress.Address2").Create(&user)
// SQL: 住所1と住所2のみを使用してユーザーと請求住所を作成
このコードは user
の作成を正常に処理しますが、BillingAddress
の作成時には Address1
と Address2
の 2 つのフィールドのみが含まれ、他のフィールドは address
テーブルの SQL に含まれません。
- Omit
// ユーザーと請求住所、郵送住所を作成し、請求住所の指定フィールドを含めない
db.Omit("BillingAddress.Address2", "BillingAddress.CreatedAt").Create(&user)
// SQL: ユーザーと請求住所を作成し、'住所2' と作成日時フィールドを省略
同様に、user
の作成を正常に処理しますが、BillingAddress
を処理する際には、Address2
と CreatedAt
の 2 つのフィールドを除いて、他のすべてが含まれます。
つまり、Select はどれを処理するかを指定し、Omit はどれを除外するかを指定します。
関連の削除#
GORM は、主モデルのレコードを削除する際に、Select
を使用してその関連レコード(1 対 1、1 対多、多対多)を削除できるメカニズムを提供します。これはデータの整合性を維持するのに役立ち、不要なデータを一緒にクリーンアップできます。
削除する関連を指定するために Select
を使用:
- 単一の関連を削除
// ユーザーを削除する際に、そのユーザーのアカウントレコードも削除
db.Select("Account").Delete(&user)
- 複数の関連を削除
// ユーザーを削除する際に、そのユーザーの注文とクレジットカードレコードも削除
db.Select("Orders", "CreditCards").Delete(&user)
- すべての関連を削除
// ユーザーを削除する際に、そのユーザーのすべての has one, has many, many2many タイプの関連レコードを削除
db.Select(clause.Associations).Delete(&user)
- 関連のバッチ削除
// users スライス内の各ユーザーを削除する際に、それぞれのアカウントレコードも削除
db.Select("Account").Delete(&users) // users は []*User または []User スライス
このように削除は非常に迅速ですが、重要な前提条件があります:主キーはゼロであってはならない、GORM は主キー ID を条件として関連レコードを検索し削除する必要があります。
関連モデル#
GORM はモデル間の関係を処理するための一連の便利なメソッドを提供し、開発者がデータを効率的に管理できるようにします。
関連モデルを使用するには、関連モデルを有効にする必要があります。つまり、ソースモデルと関連関係のフィールド名を指定します:
var user User // User があなたのモデルであり、Languages という関連フィールドがあると仮定
// 関連モデルを有効にし、user オブジェクトの Languages 関連を操作
db.Model(&user).Association("Languages")
// 関連モデルを有効にする際にエラーが発生したかどうかを確認することもできます
err := db.Model(&user).Association("Languages").Error
if err != nil {
// エラーを処理
}
Association に記入するのは、関連モデルの名前ではなく、主モデル内で定義された関連関係を含むフィールド名です。
関連のクエリ#
Find
を使用して関連レコードを取得でき、条件を追加することもできます。
var languages []Language // Language が関連モデルだと仮定
// 簡単なクエリ:user に関連するすべての Language レコードを取得
db.Model(&user).Association("Languages").Find(&languages)
// 条件付きクエリ:user に関連する Language レコードを取得するが、指定されたコードのみを含む
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages)
// 注意:Where 条件は関連モデルのクエリに適用されます
ここでの where
条件は、関連モデルに適用されます。もし user
モデルの条件が必要な場合は、例えば user.id = 1
の Language
を探したい場合、次のように処理します:
// ここでは、user 型のモデルを直接作成し、id を 1 に変更することもできると思います
// つまり:
// user := User{ID: 1}
var user User
result := db.First(&user, 1) // 主キーが 1 の User を探す
if result.Error != nil {
// エラーを処理、例えばユーザーが見つからない場合
panic("ユーザー ID 1 が見つかりませんでした")
}
var languages []Language
// 現在、user 変数は ID が 1 のユーザーを表します
// GORM は自動的に user.ID を使用して languages をフィルタリングします
db.Model(&user).Association("Languages").Find(&languages)
// もしコードのフィルタリング条件を追加したい場合は、以前のようにチェーン呼び出しできます:
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages)
関連の追加#
Append
メソッドは新しい関連を追加するために使用されます。異なる関連関係において、Append
の効果は異なります:
- 多対多関係 および 一対多関係 に対して、
Append
は新しい関連レコードを追加します。 - 一対一関係(Has One) および 属する関係(Belongs to) に対して、
Append
は現在の関連を置き換えます。
// 既存の Language オブジェクトを 1 つまたは複数追加
db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN})
// 新しい Language オブジェクトを追加(Language が存在しない場合、GORM は設定に応じて作成を試みる場合があります)
db.Model(&user).Association("Languages").Append(&Language{Name: "DE"})
// CreditCard を追加/置き換え(Has One または Belongs to 関係だと仮定)
db.Model(&user).Association("CreditCard").Append(&CreditCard{Number: "411111111111"})
上記の Languages
が存在しない場合、設定に応じて作成されるかどうかが決まります。実際には FullSaveAssociations
という tag
に依存し、デフォルトは false
であり、つまり作成されません。
関連の置き換え#
関連の置き換えは、現在の主モデルのすべての関連関係を新しい関連レコードに置き換え、元の関連関係を破棄します。
// user の Languages 関連を languageZH と languageEN に置き換え
db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN})
// 複数の独立したオブジェクトを渡すこともできます
db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN)
関連の削除#
ここでの Delete
メソッドは、主モデルと指定されたパラメータモデル間の関連関係を削除するために使用され、彼らの参照のみを削除し、関連オブジェクト自体を削除することはありません。
// user と languageZH および languageEN の関連を削除
db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN})
// 複数の独立したオブジェクトを渡すこともできます
db.Model(&user).Association("Languages").Delete(languageZH, languageEN)
関連のクリア#
関連のクリアは、主モデルとそのモデル間のすべての関連関係を削除します。
// user のすべての Languages 関連をクリア
db.Model(&user).Association("Languages").Clear()
関連のカウント#
関連のカウントは、現在の関連レコードの数を取得するために使用でき、条件付きカウントも可能です:
// user に関連するすべての Languages の数を取得
count := db.Model(&user).Association("Languages").Count()
// 条件付きカウント:user に関連する Language の中で、コードが指定リスト内の数を取得
codes := []string{"zh-CN", "en-US", "ja-JP"}
countWithCondition := db.Model(&user).Where("code IN ?", codes).Association("Languages").Count()
バッチデータ処理#
関連モデルは、クエリ、追加、置き換え、削除、関連データのカウントを含む複数のレコードに対してバッチ操作をサポートします。
バッチクエリ関連#
var users []User
var roles []Role
// users が User スライスであると仮定し、これらの users に関連するすべての Role をクエリ
db.Model(&users).Association("Role").Find(&roles)
バッチ削除関連#
var users []User
// users が User スライスであると仮定し、これらの users と userA の Team 関連を削除
db.Model(&users).Association("Team").Delete(&userA)
バッチカウント関連#
var users []User
// users が User スライスであると仮定し、これらの users に関連する Team の合計数を計算(または特定の関係に基づいてカウント)
count := db.Model(&users).Association("Team").Count()
バッチ追加 / 置き換え関連#
var users = []User{user1, user2, user3} // 3 人のユーザーがいると仮定
// バッチ追加:
// - user1 の Team に userA を追加
// - user2 の Team に userB を追加
// - user3 の Team に userA, userB, userC を追加
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})
// バッチ置き換え:
// - user1 の Team を userA に置き換え
// - user2 の Team を userB に置き換え
// - user3 の Team を userA, userB, userC に置き換え
db.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC})
関連内容の削除#
Replace
, Delete
, Clear
メソッドを使用する際に、デフォルトでどのように関連レコードが処理されるかを理解することは非常に重要で、これはデータの整合性に関わります。
デフォルトの動作:参照のみを更新#
デフォルトでは、これらのメソッドは主に外部キー参照または接続テーブルレコードに影響を与え、レコード自体には影響を与えません。
- 参照を更新:これらのメソッドは通常、関連する外部キーを
NULL
に設定します(belongs to
,has one
,has many
に対して)または接続テーブル内の対応する行を削除し、主モデルと関連モデル間のリンクを切断します。 - レコードを物理的に削除しない:関連データテーブル内の実際のレコードは削除されず、これらのレコードはデータベース内に残りますが、主モデルとは関連付けられません。
Unscoped
を使用して削除動作を変更#
関連を切断する際に、実際に関連レコードを削除したい場合は、Unscoped
メソッドを使用する必要があります。
- 関連レコードのソフト削除(Soft Delete)
関連モデルがソフト削除機能を設定している場合、Unscoped()
を使用して関連レコードのソフト削除をトリガーできます。
- 動作:これにより、関連レコード内の
deleted_at
フィールドが現在の時間に設定され、削除されたことがマークされますが、レコードはデータベース内に残ります。
// Language モデルがソフト削除をサポートしていると仮定(gorm.DeletedAt フィールドがある)
// 関連をクリアし、user に関連するすべての Language レコードをソフト削除
db.Model(&user).Association("Languages").Unscoped().Clear()
// Delete および Replace にも同様に適用されます
// db.Model(&user).Association("Languages").Unscoped().Delete(&languageZH)
- 関連レコードを物理削除
データベースから関連レコードを物理的に削除したい場合は、2 つの Unscoped()
を組み合わせて使用する必要があります:
- 最初の
Unscoped()
はdb
コンテキストに適用され、GORM に後続の操作でソフト削除ロジックを無視し、削除ロジックを実行するよう指示します。 - 2 番目の
Unscoped()
はAssociation
チェーンに適用され、関連レコードに対しても物理削除を実行することを保証します。
// 物理削除:
// 1. db.Unscoped() は GORM にグローバルなソフト削除設定を無視し、物理削除操作を行うよう指示します。
// 2. Association("Languages").Unscoped() は関連レコードに対して物理削除を実行することを保証します。
// 関連をクリアし、データベースから user に関連するすべての Language レコードを物理削除
db.Unscoped().Model(&user).Association("Languages").Unscoped().Clear()
// Delete にも同様に適用されます
// db.Unscoped().Model(&user).Association("Languages").Unscoped().Delete(&languageZH)