##目次
はじめに
・はじめに
CRUD Interface
・Goでよく使われるgormを理解する:Query編
Associations
・Goでよく使われるgormを理解する:Associations編
・Goでよく使われるgormを理解する:Preloading編
#Associations
アソシエーションとは、みなさんご存知の通り、「テーブル同士の関連付け(リレーションシップ)をモデル上の関係として操作出来るようにする仕組みのこと」ですね。
<参照>【Rails】アソシエーションを図解形式で徹底的に理解しよう!
##Auto Create/Update
GORM はレコードの作成・更新時に関連および関連先を自動的に保存します。もし関連に主キーが含まれる場合、GORM は関連先の Update を保存時にコールし、そうでなければ作成します。
なるほど、他のテーブルとアソシエーション(関連)を組んでいる場合、
・関連に主キーが含まれれば:Update
・関連に主キーが含まれなければ:Create
がコールされるらしい。
、、、。「関連に主キーが含まれる場合(または、含まれない場合)」って、どういうこと?
ということで、以下のようなstruct(テーブル)があった場合、どのような挙動になるのか実際に確認してみましょう!
type ActivityPlan struct {
Model
ActivityPlanName *string `gorm:"" json:"activityPlanName"`
Activities []*Activity `gorm:"many2many:activity_plan_activities" json:"activity"`
}
type Activity struct {
Model
ActivityName *string `gorm:"" json:"activityName"`
}
上記のstructを元に、DBを作成すると、以下の通り、activity_plansテーブル、activitiesテーブル、及びその中間テーブルであるactivity_plan_activitiesが作成されます。
####1.関連に主キーが含まれない場合
前述の順序とは逆になってしまいますが、まずはDBにデータを入れたいので、「関連に主キーが含まれない」形でPOSTし、きちんとCreateが実行されるかを確認してみます。
{
"activityPlanName": "プランA",
"activity": [
{
"activityName": "アクティビティA"
},
{
"activityName": "アクティビティB"
},
{
"activityName": "アクティビティC"
}
]
}
DBをみると、想定通りのデータが作成されていますね。
念のため、ログも確認してみます。
INSERT INTO `activity_plans` (`created_at`,`updated_at`,`deleted_at`,`activity_plan_name`) VALUES (?,?,?,?)[2020-05-29 23:19:37.1237118 +0900 JST m=+79.777882601 2020-05-29 23:19:37.1237118 +0900 JST m=+79.777882601 <nil> 0xc000396360] 1
INSERT INTO `activities` (`created_at`,`updated_at`,`deleted_at`,`activity_name`) VALUES (?,?,?,?)[2020-05-29 23:19:37.1268686 +0900 JST m=+79.781038401 2020-05-29 23:19:37.1268686 +0900 JST m=+79.781038401 <nil> 0xc000396370] 1
INSERT INTO `activity_plan_activities` (`activity_id`,`activity_plan_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_id` = ? AND `activity_plan_id` = ?)[1 1 1 1] 1
INSERT INTO `activities` (`created_at`,`updated_at`,`deleted_at`,`activity_name`) VALUES (?,?,?,?)[2020-05-29 23:19:37.1486119 +0900 JST m=+79.805353501 2020-05-29 23:19:37.1486119 +0900 JST m=+79.805353501 <nil> 0xc000396380] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[1 2 1 2] 1
INSERT INTO `activities` (`created_at`,`updated_at`,`deleted_at`,`activity_name`) VALUES (?,?,?,?)[2020-05-29 23:19:37.2171606 +0900 JST m=+79.871331701 2020-05-29 23:19:37.2171606 +0900 JST m=+79.871331701 <nil> 0xc000396390] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[1 3 1 3] 1
たしかにactivity_plansテーブルの関連テーブルであるactivitiesテーブル、及びその中間テーブルであるactivity_plan_activitiesテーブルにINSERTが実行されており、「関連に主キーが含まれない場合」はレコードのCreateが実行されているようです。
####2.関連に主キーが含まれる場合
では、先ほど作成したactivitiesテーブルのIDを指定して、以下のようなデータをPOSTした場合はどうでしょう。
{
"activityPlanName": "プランB",
"activity": [
{
"id": 1,
"activityName": "アクティビティX"
},
{
"id": 2,
"activityName": "アクティビティY"
},
{
"id": 3,
"activityName": "アクティビティZ"
}
]
}
見た感じ、たしかに、先ほど作成したactivitiesテーブルのデータが上書き(Update)されていますね。
ということで、こちらも念のため発行されたログを確認して見ましょう。
INSERT INTO `activity_plans` (`created_at`,`updated_at`,`deleted_at`,`activity_plan_name`) VALUES (?,?,?,?)[2020-05-30 02:07:25.577717 +0900 JST m=+31.113118001 2020-05-30 02:07:25.577717 +0900 JST m=+31.113118001 <nil> 0xc0003aff70] 1
UPDATE `activities` SET `updated_at` = ?, `deleted_at` = ?, `activity_name` = ? WHERE `activities`.`deleted_at` IS NULL AND `activities`.`id` = ?[2020-05-30 02:07:25.6000009 +0900 JST m=+31.135398801 <nil> 0xc0003aff80 1] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[2 1 2 1] 1
UPDATE `activities` SET `updated_at` = ?, `deleted_at` = ?, `activity_name` = ? WHERE `activities`.`deleted_at` IS NULL AND `activities`.`id` = ?[2020-05-30 02:07:25.6806456 +0900 JST m=+31.216049401 <nil> 0xc0003aff90 2] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[2 2 2 2] 1
UPDATE `activities` SET `updated_at` = ?, `deleted_at` = ?, `activity_name` = ? WHERE `activities`.`deleted_at` IS NULL AND `activities`.`id` = ?[2020-05-30 02:07:25.7135422 +0900 JST m=+31.248939701 <nil> 0xc0003affa0 3] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[2 3 2 3] 1
ログをみると、activitiesテーブルの指定したIDに対応するレコードをUpdateする処理が走っていることがわかります。
ということで、こちらも「関連に主キーが含まれる場合」は指定した主キーに応じたデータのUpdateが実行されると言えそうですね。
##Skip AutoUpdate
関連がすでにデータベースに存在する場合、更新したくないでしょう。
そのような場合は gorm:association_autoupdate を false に設定することができます。
先ほど確認した通り、デフォルトのgormの挙動では、関連に主キーを含めてPOSTすると、その主キーに対応したデータのUpdate処理が実行されますが、アソシエーション(関連)を組んでいるデータベースの情報を書き換えたくない(Updateしたくない)ときの設定のようです。
例えば、以下のような状況を考えてみましょう。
【お題】
activitiesテーブルに登録されているデータをリストで選択(IDを指定)し、activities_plansテーブルとの紐付けだけ中間テーブルに保存したい。
※IDを指定するだけで、activitiesテーブルのレコードのUpdateはしたくない。
まずは、現状のstructのままで、以下のようなデータをPOSTしてみます。
{
"activityPlanName": "プランB",
"activity": [
{
"id": 1
},
{
"id": 2
},
{
"id": 3
}
]
}
はい。activitiesテーブルのActivityNameの値(value)が消えてますね(笑)
INSERT INTO `activity_plans` (`created_at`,`updated_at`,`deleted_at`,`activity_plan_name`) VALUES (?,?,?,?)[2020-05-29 23:30:24.9132724 +0900 JST m=+26.877551601 2020-05-29 23:30:24.9132724 +0900 JST m=+26.877551601 <nil> 0xc00028bf80] 1
UPDATE `activities` SET `updated_at` = ?, `deleted_at` = ?, `activity_name` = ? WHERE `activities`.`deleted_at` IS NULL AND `activities`.`id` = ?[2020-05-29 23:30:24.9213947 +0900 JST m=+26.885673601 <nil> <nil> 1] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[2 1 2 1] 1
UPDATE `activities` SET `updated_at` = ?, `deleted_at` = ?, `activity_name` = ? WHERE `activities`.`deleted_at` IS NULL AND `activities`.`id` = ?[2020-05-29 23:30:24.9364236 +0900 JST m=+26.900700601 <nil> <nil> 2] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[2 2 2 2] 1
UPDATE `activities` SET `updated_at` = ?, `deleted_at` = ?, `activity_name` = ? WHERE `activities`.`deleted_at` IS NULL AND `activities`.`id` = ?[2020-05-29 23:30:24.9571674 +0900 JST m=+26.921450401 <nil> <nil> 3] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[2 3 2 3] 1
ログをみてみると、レコードのIDだけ指定して、ActivityNameは空の状態になっているので、指定したIDに紐づくActivityNameがnilでUpdateされているようです。
ということで、こういうときに役に立つのが、gorm:"association_autoupdate:false"
を使用したgormのSkip AutoUpdate機能です。
例えば、ActivityPlanのstructを以下のように変更してみます。
type ActivityPlan struct {
Model
ActivityPlanName *string `gorm:"" json:"activityPlanName"`
Activities []*Activity `gorm:"many2many:activity_plan_activities; association_autoupdate:false" json:"activity"`
}
そして、再度、先ほどと同様の内容をPOSTします。
{
"activityPlanName": "プランB",
"activity": [
{
"id": 1
},
{
"id": 2
},
{
"id": 3
}
]
}
すると、今度はactivity_plansテーブル、及びactivity_plan_activitiesはINSERTでレコードが追加されているものの、activityテーブルは元のままになっていますね。
ログを確認すると、たしかにactivityテーブルのUpdate処理は実行されていないようです。
INSERT INTO `activity_plans` (`created_at`,`updated_at`,`deleted_at`,`activity_plan_name`) VALUES (?,?,?,?)[2020-05-29 23:49:49.9542801 +0900 JST m=+14.995407401 2020-05-29 23:49:49.9542801 +0900 JST m=+14.995407401 <nil> 0xc0003ac810] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[2 1 2 1] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[2 2 2 2] 1
sINSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[2 3 2 3] 1
では、現在の状態(association_autoupdate:false)で、以下のようなデータを入れるとどうなるでしょうか。
{
"activityPlanName": "プランC",
"activity": [
{
"activityName": "アクティビティD"
},
{
"activityName": "アクティビティE"
},
{
"activityName": "アクティビティF"
}
]
}
INSERT INTO `activity_plans` (`created_at`,`updated_at`,`deleted_at`,`activity_plan_name`) VALUES (?,?,?,?)[2020-05-30 00:01:49.0121961 +0900 JST m=+109.399398001 2020-05-30 00:01:49.0121961 +0900 JST m=+109.399398001 <nil> 0xc000020260] 1
INSERT INTO `activities` (`created_at`,`updated_at`,`deleted_at`,`activity_name`) VALUES (?,?,?,?)[2020-05-30 00:01:49.0310455 +0900 JST m=+109.418447301 2020-05-30 00:01:49.0310455 +0900 JST m=+109.418447301 <nil> 0xc000020270] 1
INSERT INTO `activity_plan_activities` (`activity_id`,`activity_plan_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_id` = ? AND `activity_plan_id` = ?)[4 3 4 3] 1
INSERT INTO `activities` (`created_at`,`updated_at`,`deleted_at`,`activity_name`) VALUES (?,?,?,?)[2020-05-30 00:01:49.1122595 +0900 JST m=+109.499631201 2020-05-30 00:01:49.1122595 +0900 JST m=+109.499631201 <nil> 0xc000020280] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[3 5 3 5] 1
INSERT INTO `activities` (`created_at`,`updated_at`,`deleted_at`,`activity_name`) VALUES (?,?,?,?)[2020-05-30 00:01:49.2325077 +0900 JST m=+109.619708901 2020-05-30 00:01:49.2325077 +0900 JST m=+109.619708901 <nil> 0xc000020290] 1
INSERT INTO `activity_plan_activities` (`activity_plan_id`,`activity_id`) SELECT ?,? FROM DUAL WHERE NOT EXISTS (SELECT * FROM `activity_plan_activities` WHERE `activity_plan_id` = ? AND `activity_id` = ?)[3 6 3 6] 1
ということで、association_autoupdate:false
はあくまでデフォルトで設定されている関連テーブルの自動アップデートをfalse(実行されないよう)にしているだけなので、既存レコードの主キーを指定せずにPOSTした場合は関連テーブルに新規のレコードが作成されます。
##Skip AutoCreate
では、関連テーブルの新規レコードが作成されないようにするにはどうすればよいでしょうか。
そのときに使用するのが、gorm:"association_autocreate:false"
です。
試しに、ActivityPlanのstructを以下のように変更し、
type ActivityPlan struct {
Model
ActivityPlanName *string `gorm:"" json:"activityPlanName"`
Activities []*Activity `gorm:"many2many:activity_plan_activities; association_autocreate:false" json:"activity"`
}
以下のようなデータをPOSTしてみます。
{
"activityPlanName": "プランB",
"activity": [
{
"activityName": "アクティビティX"
},
{
"activityName": "アクティビティY"
},
{
"activityName": "アクティビティZ"
}
]
}
では、この状態で、以下のようにIDを指定して、先ほどのデータをPOSTするとどうなるでしょう。
{
"activityPlanName": "プランB",
"activity": [
{
"id": 1,
"activityName": "アクティビティX"
},
{
"id": 2,
"activityName": "アクティビティY"
},
{
"id": 3,
"activityName": "アクティビティZ"
}
]
}
これはDBを見るまでもないと思いますが、activitiesテーブルとの関係はCreate時のみ制限をかけているため、IDを指定するとactivitiesテーブルにあるデータが上書き(Update)されてしまいます。
##Skip AutoCreate/Update
これまで見てきたgorm:"association_autoupdate:false"
とgorm:"association_autocreate:false"
の両方を適用したい場合は、gorm:"save_associations:false"
を使用します。
特に、関連先からのデータの投稿や変更は予定しておらず(むしろされると困る)、関連先からはデータの参照(IDを指定してのデータの呼び出しなど)だけできればよいということであれば、gorm:"save_associations:false"
が有効です。
##Skip Save Reference
個人的には、あまり使い所がよくわかりませんが、アソシエーションに基づく参照IDを保存したくない場合には、gorm:"association_save_reference:false"
を使用します。
※使い所がわかる方がいればコメントください!(切)
例えば、ActivityPlanのstructを以下のように変更します。
type ActivityPlan struct {
Model
ActivityPlanName *string `gorm:"" json:"activityPlanName"`
Activities []*Activity `gorm:"many2many:activity_plan_activities; association_save_reference:false" json:"activity"`
}
この状態で、以下のデータをPOSTしてみます。
{
"activityPlanName": "プランA",
"activity": [
{
"activityName": "アクティビティA"
},
{
"activityName": "アクティビティB"
},
{
"activityName": "アクティビティC"
}
]
}
INSERT INTO `activity_plans` (`created_at`,`updated_at`,`deleted_at`,`activity_plan_name`) VALUES (?,?,?,?)[2020-05-30 14:12:24.7420672 +0900 JST m=+359.487860301 2020-05-30 14:12:24.7420672 +0900 JST m=+359.487860301 <nil> 0xc0002da5f0] 1
INSERT INTO `activities` (`created_at`,`updated_at`,`deleted_at`,`activity_name`) VALUES (?,?,?,?)[2020-05-30 14:12:24.7481309 +0900 JST m=+359.493925801 2020-05-30 14:12:24.7481309 +0900 JST m=+359.493925801 <nil> 0xc0002da600] 1
INSERT INTO `activities` (`created_at`,`updated_at`,`deleted_at`,`activity_name`) VALUES (?,?,?,?)[2020-05-30 14:12:24.7515339 +0900 JST m=+359.497328401 2020-05-30 14:12:24.7515339 +0900 JST m=+359.497328401 <nil> 0xc0002da610] 1
INSERT INTO `activities` (`created_at`,`updated_at`,`deleted_at`,`activity_name`) VALUES (?,?,?,?)[2020-05-30 14:12:24.7893762 +0900 JST m=+359.535169301 2020-05-30 14:12:24.7893762 +0900 JST m=+359.535169301 <nil> 0xc0002da620] 1