7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Goでよく使われるgormを理解する:Associations編

Last updated at Posted at 2020-05-29

##目次
はじめに
はじめに

CRUD Interface
Goでよく使われるgormを理解する:Query編

Associations
Goでよく使われるgormを理解する:Associations編
Goでよく使われるgormを理解する:Preloading編

#Associations
アソシエーションとは、みなさんご存知の通り、「テーブル同士の関連付け(リレーションシップ)をモデル上の関係として操作出来るようにする仕組みのこと」ですね。

<参照>【Rails】アソシエーションを図解形式で徹底的に理解しよう!

##Auto Create/Update

GORM はレコードの作成・更新時に関連および関連先を自動的に保存します。もし関連に主キーが含まれる場合、GORM は関連先の Update を保存時にコールし、そうでなければ作成します。

なるほど、他のテーブルとアソシエーション(関連)を組んでいる場合、
・関連に主キーが含まれれば:Update
・関連に主キーが含まれなければ:Create
がコールされるらしい。

、、、。「関連に主キーが含まれる場合(または、含まれない場合)」って、どういうこと?

ということで、以下のようなstruct(テーブル)があった場合、どのような挙動になるのか実際に確認してみましょう!

activity_plans

type ActivityPlan struct {
	Model
	ActivityPlanName *string `gorm:"" json:"activityPlanName"`
	Activities []*Activity `gorm:"many2many:activity_plan_activities" json:"activity"`
}
activities

type Activity struct {
	Model
	ActivityName *string `gorm:"" json:"activityName"`
}

上記のstructを元に、DBを作成すると、以下の通り、activity_plansテーブル、activitiesテーブル、及びその中間テーブルであるactivity_plan_activitiesが作成されます。
スクリーンショット 2020-05-29 23.18.23.png

####1.関連に主キーが含まれない場合
前述の順序とは逆になってしまいますが、まずはDBにデータを入れたいので、「関連に主キーが含まれない」形でPOSTし、きちんとCreateが実行されるかを確認してみます。

POST内容
{
  "activityPlanName": "プランA",
  "activity": [
    {
      "activityName": "アクティビティA"
    },
    {
      "activityName": "アクティビティB"
    },
    {
      "activityName": "アクティビティC"
    }
  ]
}
スクリーンショット 2020-05-29 23.24.12.png スクリーンショット 2020-05-29 23.25.07.png スクリーンショット 2020-05-29 23.24.46.png

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した場合はどうでしょう。

POST内容
{
  "activityPlanName": "プランB",
  "activity": [
    {
      "id": 1,
      "activityName": "アクティビティX"
    },
    {
      "id": 2,
      "activityName": "アクティビティY"
    },
    {
      "id": 3,
      "activityName": "アクティビティZ"
    }
  ]
}
スクリーンショット 2020-05-30 2.12.03.png スクリーンショット 2020-05-30 2.12.21.png スクリーンショット 2020-05-30 2.12.38.png

見た感じ、たしかに、先ほど作成した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してみます。

POST内容

{
  "activityPlanName": "プランB",
  "activity": [
    {
      "id": 1
    },
    {
      "id": 2
    },
    {
      "id": 3
    }
  ]
}
スクリーンショット 2020-05-29 23.50.20.png スクリーンショット 2020-05-29 23.50.44.png スクリーンショット 2020-05-29 23.31.10.png

はい。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します。

POST内容

{
  "activityPlanName": "プランB",
  "activity": [
    {
      "id": 1
    },
    {
      "id": 2
    },
    {
      "id": 3
    }
  ]
}
スクリーンショット 2020-05-29 23.50.20.png スクリーンショット 2020-05-29 23.50.44.png スクリーンショット 2020-05-29 23.51.05.png

すると、今度は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)で、以下のようなデータを入れるとどうなるでしょうか。

POST内容
{
  "activityPlanName": "プランC",
  "activity": [
    {
      "activityName": "アクティビティD"
    },
    {
      "activityName": "アクティビティE"
    },
    {
      "activityName": "アクティビティF"
    }
  ]
}
スクリーンショット 2020-05-30 0.02.46.png スクリーンショット 2020-05-30 0.03.15.png スクリーンショット 2020-05-30 0.03.40.png
ログ

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してみます。

POST内容
{
  "activityPlanName": "プランB",
  "activity": [
    {
      "activityName": "アクティビティX"
    },
    {
      "activityName": "アクティビティY"
    },
    {
      "activityName": "アクティビティZ"
    }
  ]
}
スクリーンショット 2020-05-30 13.32.03.png スクリーンショット 2020-05-30 13.32.27.png スクリーンショット 2020-05-30 13.32.46.png すると、activity_plansテーブルには新規のレコードが作成されましたが、先ほどと違って関連テーブルであるactivitiesテーブルには、レコードが作成されていないことがわかります。

では、この状態で、以下のようにIDを指定して、先ほどのデータをPOSTするとどうなるでしょう。

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してみます。

POST内容
{
  "activityPlanName": "プランA",
  "activity": [
    {
      "activityName": "アクティビティA"
    },
    {
      "activityName": "アクティビティB"
    },
    {
      "activityName": "アクティビティC"
    }
  ]
}
スクリーンショット 2020-05-30 14.12.30.png スクリーンショット 2020-05-30 14.12.48.png スクリーンショット 2020-05-30 14.13.07.png すると、activity_plansテーブルとactivitiesテーブルにはデータが追加されたものの、中間テーブルであるactivity_plans_activitiesテーブルにはIDが保存されなくなりました。 ログを見ても、activity_plansテーブルとactivitiesテーブルのレコードだけが作成されていますね。
ログ
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
7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?