20200530のGoに関する記事は6件です。

go generate のベストプラクティス

概要

Go 言語におけるコード生成 (go generate) について、自分の中でベストプラクティスと思えるものが増えてきたので、ここでまとめて紹介してみたいと思います。

wtz.go と time について

go generate のベストプラクティスを説明するにあたり、この記事では wtz.go と time の 2 つのライブラリを実例としてとりあげます。

wtz.go は筆者が Go 標準ライブラリの time の Windows ランタイム部分を参考にして作成したもので、 Windows タイムゾーンをどの OS や実行環境でも扱えるようにするためのライブラリです。これを使うと、例えば Tokyo Standard Time のような Windows 独自のタイムゾーン文字列と time.Location とで相互変換ができるようになります。

wtz.go と time のいずれのライブラリも Unicode CLDR Project が管理する windowsZones.xml をダウンロードしてそれを基にタイムゾーン変換表の Go ソースコードを出力するコードジェネレータを含んでおり、これを //go:generate ディレクティブにより呼び出しています。

wtz.go 自体の説明や Windows タイムゾーンの話題については、別の記事に改めて書きたいと思います。

go generate のベストプラクティス

基本的または対応が容易と思えることから、順に紹介していきます。

生成するコードに Code generated コメントを追加する

go generate のマニュアルで説明されていますが、Go の世界では次の正規表現にマッチするコメントを含むファイルは自動生成されたファイルとして扱うという取り決めがあります。

正規表現
^// Code generated .* DO NOT EDIT\.$

なので、コードジェネレータを作るときには、まず出力ファイルの先頭にこのコメントを出力するようにしましょう。例えば wtz.go でも生成したファイル wtz_maps.go の先頭には次のようなコメントを出力しています。

wtz_maps.go
// Code generated by genmaps.go; DO NOT EDIT.
// Based on information from https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml

package wtz

golint などの静的解析ツールは、このコメントが含まれるファイルのチェックをスキップしますし、 Visual Studio Code のような IDE でも警告が出なくなるので、見通しがよくなります。コードジェネレータとしても golint などの基準に従ったコードを出力する必要がなくなるので、開発が楽になります。

生成したコードをきれいにする

コードジェネレータが出力したコードはツールできれいにフォーマットしましょう。 生成したコードを常にフォーマットすると決めておけば、適切なインデントや改行を気にすることなくコード出力してよくなるので、コードジェネレータの開発が楽になります。

Go の開発環境では gofmt が標準コマンドとして利用可能なので、生成したファイルに対してこれを実行するのが最も簡単です。

//go:generate go run generator.go
//go:generate gofmt -w genarted.go

標準コマンドではありませんが gofmt の代わりに goimports を使うと、パッケージの import 文も勝手に追加・削除してくれるので開発がさらに楽になります。

gofmt も goimports もその機能はライブラリパッケージとして公開されており、コードジェネレータに組み込んで呼び出し可能です。 gofmt 相当の処理は format.Source() 関数でできます (time のコードジェネレータで使っています)。 goimports 相当の処理は imports.Process() 関数を使います (wtz.go のコードジェネレータで使っています)。

どちらの関数も Go のソースコードを []byte で受け取って []byte で返すので、コード生成部分では bytes.Buffer のようなメモリバッファに書き出すように作るのがよいでしょう。この構成にすればフォーマットによる文法チェックが通った場合のみ実際のファイルに出力されるようにできるので、安全性が増します。

独立したフォルダに //go:generate とコードジェネレータを書く

これは実践している人をあまり見ないのですが、 //go:generate は実際の出力先フォルダ内には書かず、 gen といった決まった名前の独立したフォルダに分離して配置するのがよいと思っています。ついでにコードジェネレータ本体も同じフォルダに格納すると、まとまりがよくなります。

実例としては wtz.go のフォルダ構成を見てください。

wtz.go
|-- gen
|   |-- genmaps.go ← (package main) (//go:generate) (コードジェネレータ本体)
|-- go.mod         ← (コードジェネレータの依存関係も記録される)
|-- go.sum         ← (同上)
|-- wtz.go         ← (package wtz)
|-- wtz_maps.go    ← (package wtz) (コードジェネレータ出力先)

コードジェネレータ genmaps.go には次のような //go:generate が書かれています。このコマンドは gen フォルダで実行されるので、 go run . でコードジェネレータをビルド・実行できますし、また相対パスを使って正しい出力先を指定できます。

genmaps.go
//go:generate go run . -output ../wtz_maps.go

wtz.go の元となった time パッケージは独立したフォルダを作らない、標準的なコード生成の例として参考になります。

time
|-- genzabbrs.go              ← (package main) (// +build ignore) (コードジェネレータ本体)
|-- zoneinfo.go               ← (package time) (//go:generate)
|-- zoneinfo_abbrs_windows.go ← (package time) (コードジェネレータ出力先)

zoneinfo.go に //go:generate が書かれています。コードジェネレータ genzabbrs.go は main パッケージのプログラムですが // +build ignore のビルドタグを追加することでライブラリのビルド時には無視されるようにしています。

zoneinfo.go
//go:generate env ZONEINFO=$GOROOT/lib/time/zoneinfo.zip go run genzabbrs.go -output zoneinfo_abbrs_windows.go

独立したフォルダに配置することについては //go:generate とコードジェネレータのそれぞれについて個別の利点があります。どちらか片方だけを採用したり、全く別のフォルダに置いたりすることもありだと思います。

  • //go:generate を独立フォルダに置く理由
    • まずいコードを生成してしまったときの問題を回避できる
    • 大規模プロジェクトに含まれる多数の go generate を効率的に実行できる
  • コードジェネレータを独立フォルダに置く理由
    • // +build ignore のようなビルドタグが不要になる
    • コードジェネレータが依存するモジュールを go.mod や go.sum に記録できる
    • コードジェネレータを複数のファイルで実装でき、テストも書ける

上記の //go:generate に関する理由 2 点について補足説明します。

ひとつめの理由は、まずいコードを生成してしまったときに発生する問題を回避するためです。例えばコードジェネレータがなにかの不具合で空ファイルを作って終わってしまうと、 package 宣言がないというエラーでそれ以上 go generate できなくなり、元に戻すには手動でそのファイルを消す必要があります。これはコードジェネレータの開発中には特に頻繁に発生する煩わしい問題です。

コマンドライン
$ go generate .
can't load package: package .: 
generated.go:1:1: expected 'package', found 'EOF'

ただし、この挙動については cmd/go: generate shouldn't require a complete valid package という issue で改善が進められており、次の Go のリリース (1.15?) では気にする必要がなくなるかもしれません。

もうひとつの理由は、大規模なプロジェクトにおいて、フォルダ名を揃えておくことで go generate の実行効率が向上できることです。 //go:generateを gen という名前のフォルダに入れるルールにすれば、次の 1 行のコマンドでソースツリー内の必要なフォルダだけを効率よく処理することができます。

コマンドライン
$ go generate ./gen ./.../gen

サードパーティのコードジェネレータも go run で実行する

Go Modules が導入されてから、公開されている Go 製のツールは go run によるダウンロード・ビルド・実行が一度にできるようになりました。 //go:generate でもなるべくこの機能を利用するようにしましょう。

例えば次のように書くと、開発者には go generate の実行前に stringer コマンドのインストールを要求することになりますが、

//go:generate stringer -type=Pill

代わりに次のように書けば、開発者は事前の準備なしに go generate を実行するだけですべてが終わるようにできます。

//go:generate go run golang.org/x/tools/cmd/stringer -type=Pill

さらにこの方法を使う場合、使用したコードジェネレータのバージョンを記録できるという大きな利点があります。次のような内容の tools.go ファイルを作ることで go.mod と go.sum にモジュールのバージョンが記録でき、go generate ではそのバージョンを使用することが保証できます。

tools.go
// +build tools

package main

import _ "golang.org/x/tools/cmd/stringer"

このテクニックは Go modules wiki の How can I track tool dependencies for a module? で紹介されています。

Go だけでコードを生成する (ように努力する)

ビルド・リリースのプロセスに make を使ったり cp や tar などのコマンドや Bash のシェルスクリプトを使っている Go のプロジェクトをよく見かけます。しかしながら、これをやると Windows のような Unix 以外のプラットフォームでの開発環境のセットアップが面倒になるのでよろしくないと個人的には思っています。

go generate についてもこれは同様で、 //go:generate のコマンドラインで使用するのは、 Go にバンドルされている標準コマンドか go run による Go ソースファイルまたはパッケージの実行に限るのが望ましいと言えます。

ただしそうはいってもシェルのコマンドひとつで済むことを Go で書くのもしんどいので、実際にはそのコストとメリットを考慮して総合的に判断するべきことでしょう。

やや脱線しますが、ビルド・リリースのプロセスもなるべくクロスプラットフォームにするため、筆者は GoReleaser をよく使っています。この他にも Go で書かれたクロスプラットフォームなシェルや make となるライブラリやツール (Ruby の FileUtils とか rake のようなもの) で何かいいものがないかと思っています。

まとめ

go generate のベストプラクティスを実例とともに何点か紹介しました。

どういうわけか、これまで筆者は所属組織の Go プロジェクトでも msgraph.gowtz.go といったオープンソースプロジェクトでも go generate するコードばかりを書いてきたので、ここいらで知見やノウハウ、普段思っていることをまとめておこうと思い、この記事を書き始めました。

まだ書き足りない点がいくつかあるので、この記事は今後も更新していきたいと思います。内容について、意見、感想、質問、誤りの指摘などがありましたらお気軽にお知らせください。よろしくお願いします。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

目次

はじめに
はじめに

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

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

Preloading (Eager loading)

Preloadingは事前読み込みのことです。多くのormで実装されている機能ですが、gormでもPreload(struct's)を指定することで、リソースを先読みしてキャッシュできるようなります。

Preload

Preloadを使わない場合

まずは、以下のような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"`
}

そして、DBからデータを取得するためのfunctionをActivityPlanモデルに作成し、まずはPreloadなしで実行してみます。

ActivityPlanのfunction
func GetAllActivityPlans() (ml []*ActivityPlan, err error) {
    tx := db.Begin()
    err = tx.Find(&ml).Commit().Error

    return ml, err
}
取得したデータ
[
  {
    "id": 1,
    "createdAt": "2020-05-30T14:55:53+09:00",
    "updatedAt": "2020-05-30T14:55:53+09:00",
    "deletedAt": null,
    "activityPlanName": "プランA",
    "tourScheduleId": 1,
    "activity": null
  },
  {
    "id": 2,
    "createdAt": "2020-05-30T14:55:53+09:00",
    "updatedAt": "2020-05-30T14:55:53+09:00",
    "deletedAt": null,
    "activityPlanName": "プランB",
    "tourScheduleId": 1,
    "activity": null
  },

 中略 

  {
    "id": 11,
    "createdAt": "2020-05-30T14:55:53+09:00",
    "updatedAt": "2020-05-30T14:55:53+09:00",
    "deletedAt": null,
    "activityPlanName": "プランK",
    "tourScheduleId": 3,
    "activity": null
  },
  {
    "id": 12,
    "createdAt": "2020-05-30T14:55:53+09:00",
    "updatedAt": "2020-05-30T14:55:53+09:00",
    "deletedAt": null,
    "activityPlanName": "プランL",
    "tourScheduleId": 3,
    "activity": null
  }
]

取得したデータを見てみると、activity_plansテーブルの関連先であるactivitiesテーブルの項目("activity")がnullになっており、自テーブルのデータしか取得できていないのがわかります。
以下のログを見ても、activity_plansテーブルのデータのみ全件取得していますね。

ログ
SELECT * FROM `activity_plans`  WHERE `activity_plans`.`deleted_at` IS NULL[] 12

Preloadを使った場合

では、Preloadを使用した場合はどうでしょう。

func GetAllActivityPlans() (ml []*ActivityPlan, err error) {
    tx := db.Preload("Activities").Begin()

    err = tx.Find(&ml).Commit().Error

    return ml, err
}
取得したデータ
[
  {
    "id": 1,
    "createdAt": "2020-05-30T14:55:53+09:00",
    "updatedAt": "2020-05-30T14:55:53+09:00",
    "deletedAt": null,
    "activityPlanName": "プランA",
    "tourScheduleId": 1,
    "activity": [
      {
        "id": 1,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティA"
      },
      {
        "id": 2,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティB"
      },
      {
        "id": 3,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティC"
      }
    ]
  },
  {
    "id": 2,
    "createdAt": "2020-05-30T14:55:53+09:00",
    "updatedAt": "2020-05-30T14:55:53+09:00",
    "deletedAt": null,
    "activityPlanName": "プランB",
    "tourScheduleId": 1,
    "activity": [
      {
        "id": 2,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティB"
      },
      {
        "id": 3,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティC"
      },
      {
        "id": 4,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティD"
      }
    ]
  },

 中略 

  {
    "id": 11,
    "createdAt": "2020-05-30T14:55:53+09:00",
    "updatedAt": "2020-05-30T14:55:53+09:00",
    "deletedAt": null,
    "activityPlanName": "プランK",
    "tourScheduleId": 3,
    "activity": [
      {
        "id": 2,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティB"
      },
      {
        "id": 4,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティD"
      },
      {
        "id": 5,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティE"
      }
    ]
  },
  {
    "id": 12,
    "createdAt": "2020-05-30T14:55:53+09:00",
    "updatedAt": "2020-05-30T14:55:53+09:00",
    "deletedAt": null,
    "activityPlanName": "プランL",
    "tourScheduleId": 3,
    "activity": [
      {
        "id": 1,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティA"
      },
      {
        "id": 2,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティB"
      },
      {
        "id": 6,
        "createdAt": "2020-05-30T14:55:52+09:00",
        "updatedAt": "2020-05-30T14:55:52+09:00",
        "deletedAt": null,
        "activityName": "アクティビティF"
      }
    ]
  }
]

先ほどはnullだったactivitiesテーブルの項目("activity")も、データを取得できていることがわかります。

ちなみに、以下のログを確認すると、前回発行されていたactivity_plansテーブルのデータを取得するためのSQLに加えて、activitiesテーブルのデータを取得するためのSQLも発行されているようです。
中身を確認すると、中間テーブルであるactivity_plan_activitiesテーブルをactivitiesテーブルに内部結合して、activity_plan_activitiesテーブルのactivity_plan_idに指定したIDが含まれるかどうかを探しにいっているみたいですね。

ログ
SELECT * FROM `activity_plans`  WHERE `activity_plans`.`deleted_at` IS NULL[] 12
SELECT * FROM `activities` INNER JOIN `activity_plan_activities` ON `activity_plan_activities`.`activity_id` = `activities`.`id` WHERE `activities`.`deleted_at` IS NULL AND ((`activity_plan_activities`.`activity_plan_id` IN (?,?,?,?,?,?,?,?,?,?,?,?)))[4 5 6 7 8 9 1 2 12 11 3 10] 0

Auto Preloading

常に association を preload します

先ほどはアソシエーションを組んでいるモデルが一つだけでしたが、以下のように複数のモデルとアソシエーションを組んでいる場合はどうでしょう。

type User struct {
    Model
    UserName  *string     `gorm:"" json:"userName"`  // ユーザー名
    CompanyID *int        `gorm:"" json:"companyId"` // 所属企業ID
    Company   *Company    `gorm:"" json:"company"`
    Languages []*Language `gorm:"many2many:user_languages;association_autoupdate:false" json:"language"` // 使用可能言語
}
type Language struct {
    Model
    LanguageName *string `gorm:"" json:"languageName"`
}
type Company struct {
    Model
    ConmpanyName *string `gorm:"" json:"companyName"`
}

もちろん、先ほど使用したPreload(struct's)を複数つなげて、全てのstructを指定していくことも可能ですが、関連モデル全てに対してPreloadingしたい場合には、Set("gorm:auto_preload", true)が有効です。

Set("gorm:auto_preload", true)は以下のようにfunction側に設定を追加します。

Userのfunction
func GetAllUsers() (ml []*User, err error) {
    tx := db.Set("gorm:auto_preload", true).Begin()

    err = tx.Find(&ml).Commit().Error

    return ml, err
}

すると、以下のようにアソシエーションを組んでいるモデルの情報をすべてPreloadingすることができます。

取得したデータ
[
  {
    "id": 1,
    "createdAt": "2020-05-30T16:47:31+09:00",
    "updatedAt": "2020-05-30T16:47:31+09:00",
    "deletedAt": null,
    "userName": "user1",
    "companyId": 1,
    "company": {
      "id": 1,
      "createdAt": "2020-05-30T16:47:29+09:00",
      "updatedAt": "2020-05-30T16:47:29+09:00",
      "deletedAt": null,
      "companyName": "A株式会社"
    },
    "language": [
      {
        "id": 1,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "日本語"
      }
    ]
  },
  {
    "id": 2,
    "createdAt": "2020-05-30T16:47:32+09:00",
    "updatedAt": "2020-05-30T16:47:32+09:00",
    "deletedAt": null,
    "userName": "user2",
    "companyId": 1,
    "company": {
      "id": 1,
      "createdAt": "2020-05-30T16:47:29+09:00",
      "updatedAt": "2020-05-30T16:47:29+09:00",
      "deletedAt": null,
      "companyName": "A株式会社"
    },
    "language": [
      {
        "id": 2,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "英語"
      },
      {
        "id": 4,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "イタリア語"
      }
    ]
  },
  {
    "id": 3,
    "createdAt": "2020-05-30T16:47:32+09:00",
    "updatedAt": "2020-05-30T16:47:32+09:00",
    "deletedAt": null,
    "userName": "user3",
    "companyId": 2,
    "company": {
      "id": 2,
      "createdAt": "2020-05-30T16:47:29+09:00",
      "updatedAt": "2020-05-30T16:47:29+09:00",
      "deletedAt": null,
      "companyName": "C株式会社"
    },
    "language": [
      {
        "id": 2,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "英語"
      }
    ]
  },
  {
    "id": 4,
    "createdAt": "2020-05-30T16:47:32+09:00",
    "updatedAt": "2020-05-30T16:47:32+09:00",
    "deletedAt": null,
    "userName": "user4",
    "companyId": 3,
    "company": {
      "id": 3,
      "createdAt": "2020-05-30T16:47:29+09:00",
      "updatedAt": "2020-05-30T16:47:29+09:00",
      "deletedAt": null,
      "companyName": "B株式会社"
    },
    "language": [
      {
        "id": 1,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "日本語"
      },
      {
        "id": 2,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "英語"
      },
      {
        "id": 3,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "フランス語"
      }
    ]
  },
  {
    "id": 5,
    "createdAt": "2020-05-30T16:47:32+09:00",
    "updatedAt": "2020-05-30T16:47:32+09:00",
    "deletedAt": null,
    "userName": "user5",
    "companyId": 4,
    "company": {
      "id": 4,
      "createdAt": "2020-05-30T16:47:29+09:00",
      "updatedAt": "2020-05-30T16:47:29+09:00",
      "deletedAt": null,
      "companyName": "D株式会社"
    },
    "language": [
      {
        "id": 4,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "イタリア語"
      }
    ]
  }
]
ログ
SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL[] 5
SELECT * FROM `companies`  WHERE `companies`.`deleted_at` IS NULL AND ((`id` IN (?,?,?,?,?)))[0xc000306d10 0xc000306d88 0xc000306e00 0xc000306e78 0xc000306ef0] 4
SELECT * FROM `languages` INNER JOIN `user_languages` ON `user_languages`.`language_id` = `languages`.`id` WHERE `languages`.`deleted_at` IS NULL AND ((`user_languages`.`user_id` IN (?,?,?,?,?)))[1 2 3 4 5] 0

では、こんな場合はどうでしょう。

【お題】
たしかに、基本は全ての関連モデルのデータを取得したいものの、一部の関連モデルのデータは取得しないようにしたい。

そんなときに使用するのが、gorm:"PRELOAD:false"です。

試しに、先ほど使用したUserモデルのCompanyを以下のように変更してみます。

type User struct {
    Model
    UserName  *string     `gorm:"" json:"userName"`  // ユーザー名
    CompanyID *int        `gorm:"" json:"companyId"` // 所属企業ID
    Company   *Company    `gorm:"PRELOAD:false" json:"company"`
    Languages []*Language `gorm:"many2many:user_languages;association_autoupdate:false" json:"language"` // 使用可能言語
}

すると、以下の通り、Set("gorm:auto_preload", true)をつけているにも関わらず、companyの項目はnullになり、Preloadingの対象から除外されていることがわかります。

取得したデータ
[
  {
    "id": 1,
    "createdAt": "2020-05-30T16:47:31+09:00",
    "updatedAt": "2020-05-30T16:47:31+09:00",
    "deletedAt": null,
    "userName": "user1",
    "companyId": 1,
    "company": null,
    "language": [
      {
        "id": 1,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "日本語"
      }
    ]
  },
  {
    "id": 2,
    "createdAt": "2020-05-30T16:47:32+09:00",
    "updatedAt": "2020-05-30T16:47:32+09:00",
    "deletedAt": null,
    "userName": "user2",
    "companyId": 1,
    "company": null,
    "language": [
      {
        "id": 2,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "英語"
      },
      {
        "id": 4,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "イタリア語"
      }
    ]
  },
  {
    "id": 3,
    "createdAt": "2020-05-30T16:47:32+09:00",
    "updatedAt": "2020-05-30T16:47:32+09:00",
    "deletedAt": null,
    "userName": "user3",
    "companyId": 2,
    "company": null,
    "language": [
      {
        "id": 2,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "英語"
      }
    ]
  },
  {
    "id": 4,
    "createdAt": "2020-05-30T16:47:32+09:00",
    "updatedAt": "2020-05-30T16:47:32+09:00",
    "deletedAt": null,
    "userName": "user4",
    "companyId": 3,
    "company": null,
    "language": [
      {
        "id": 1,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "日本語"
      },
      {
        "id": 2,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "英語"
      },
      {
        "id": 3,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "フランス語"
      }
    ]
  },
  {
    "id": 5,
    "createdAt": "2020-05-30T16:47:32+09:00",
    "updatedAt": "2020-05-30T16:47:32+09:00",
    "deletedAt": null,
    "userName": "user5",
    "companyId": 4,
    "company": null,
    "language": [
      {
        "id": 4,
        "createdAt": "2020-05-30T16:47:29+09:00",
        "updatedAt": "2020-05-30T16:47:29+09:00",
        "deletedAt": null,
        "languageName": "イタリア語"
      }
    ]
  }
]
ログ
SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL[] 5
SELECT * FROM `languages` INNER JOIN `user_languages` ON `user_languages`.`language_id` = `languages`.`id` WHERE `languages`.`deleted_at` IS NULL AND ((`user_languages`.`user_id` IN (?,?,?,?,?)))[4 5 1 2 3] 0

Nested Preloading

先ほどの3つのモデルに加えて、Departmentモデルを作成し、Company(企業)とDepartment(部署)が多対多(Many2Many)の関係になるよう以下のようなアソシエーションを組みます。

type User struct {
    Model
    UserName  *string     `gorm:"" json:"userName"`  // ユーザー名
    CompanyID *int        `gorm:"" json:"companyId"` // 所属企業ID
    Company   *Company    `gorm:"" json:"company"`
    Languages []*Language `gorm:"many2many:user_languages;association_autoupdate:false" json:"language"` // 使用可能言語
}
type Language struct {
    Model
    LanguageName *string `gorm:"" json:"languageName"`
}
type Company struct {
    Model
    ConmpanyName *string       `gorm:"" json:"companyName"`
    Departments  []*Department `gorm:"many2many:company_departments; association_autoupdate:false" json:"department"`
}
type Department struct {
    Model
    DepartmentName *string `gorm:"" json:"departmentName"`
}

このとき、UserモデルからDepartmentモデルのデータをPreloadingするにはどのようにすればよいでしょうか。

それぞれのモデルの関係性を考えると、Companyは直接Userモデルとのアソシエーションを結んでいますが、DepartmentはCompanyによってネストされており、Userとの直接のアソシエーションはありません。

そこで、以下のように「.(ドット)」でPreload()の中のstructをつなげることで、ネストされたモデルのデータをPreloadingすることができます。

Userのfunction
func GetAllUsers(limit int64, offset int64) (ml []*User, err error) {
    tx := db.Preload("Company.Departments").Begin()

    err = tx.Find(&ml).Commit().Error

    return ml, err
}
取得したデータ
[
  {
    "id": 1,
    "createdAt": "2020-05-30T17:28:40+09:00",
    "updatedAt": "2020-05-30T17:28:40+09:00",
    "deletedAt": null,
    "userName": "user1",
    "companyId": 1,
    "company": {
      "id": 1,
      "createdAt": "2020-05-30T17:28:38+09:00",
      "updatedAt": "2020-05-30T17:28:38+09:00",
      "deletedAt": null,
      "companyName": "A株式会社",
      "department": [
        {
          "id": 1,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "営業部"
        },
        {
          "id": 2,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "マーケティング部"
        },
        {
          "id": 3,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "情報システム部"
        },
        {
          "id": 4,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "経理部"
        }
      ]
    },
    "language": null
  },
  {
    "id": 2,
    "createdAt": "2020-05-30T17:28:40+09:00",
    "updatedAt": "2020-05-30T17:28:40+09:00",
    "deletedAt": null,
    "userName": "user2",
    "companyId": 1,
    "company": {
      "id": 1,
      "createdAt": "2020-05-30T17:28:38+09:00",
      "updatedAt": "2020-05-30T17:28:38+09:00",
      "deletedAt": null,
      "companyName": "A株式会社",
      "department": [
        {
          "id": 1,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "営業部"
        },
        {
          "id": 2,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "マーケティング部"
        },
        {
          "id": 3,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "情報システム部"
        },
        {
          "id": 4,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "経理部"
        }
      ]
    },
    "language": null
  },
  {
    "id": 3,
    "createdAt": "2020-05-30T17:28:40+09:00",
    "updatedAt": "2020-05-30T17:28:40+09:00",
    "deletedAt": null,
    "userName": "user3",
    "companyId": 2,
    "company": {
      "id": 2,
      "createdAt": "2020-05-30T17:28:38+09:00",
      "updatedAt": "2020-05-30T17:28:38+09:00",
      "deletedAt": null,
      "companyName": "C株式会社",
      "department": [
        {
          "id": 1,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "営業部"
        },
        {
          "id": 2,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "マーケティング部"
        },
        {
          "id": 4,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "経理部"
        }
      ]
    },
    "language": null
  },
  {
    "id": 4,
    "createdAt": "2020-05-30T17:28:40+09:00",
    "updatedAt": "2020-05-30T17:28:40+09:00",
    "deletedAt": null,
    "userName": "user4",
    "companyId": 3,
    "company": {
      "id": 3,
      "createdAt": "2020-05-30T17:28:38+09:00",
      "updatedAt": "2020-05-30T17:28:38+09:00",
      "deletedAt": null,
      "companyName": "B株式会社",
      "department": [
        {
          "id": 1,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "営業部"
        },
        {
          "id": 3,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "情報システム部"
        },
        {
          "id": 4,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "経理部"
        }
      ]
    },
    "language": null
  },
  {
    "id": 5,
    "createdAt": "2020-05-30T17:28:40+09:00",
    "updatedAt": "2020-05-30T17:28:40+09:00",
    "deletedAt": null,
    "userName": "user5",
    "companyId": 4,
    "company": {
      "id": 4,
      "createdAt": "2020-05-30T17:28:38+09:00",
      "updatedAt": "2020-05-30T17:28:38+09:00",
      "deletedAt": null,
      "companyName": "D株式会社",
      "department": [
        {
          "id": 1,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "営業部"
        },
        {
          "id": 2,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "マーケティング部"
        },
        {
          "id": 3,
          "createdAt": "2020-05-30T17:28:38+09:00",
          "updatedAt": "2020-05-30T17:28:38+09:00",
          "deletedAt": null,
          "departmentName": "情報システム部"
        }
      ]
    },
    "language": null
  }
]
ログ
SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL[] 5
SELECT * FROM `companies`  WHERE `companies`.`deleted_at` IS NULL AND ((`id` IN (?,?,?,?,?)))[0xc00030f330 0xc00030f3a8 0xc00030f420 0xc00030f498 0xc00030f510] 4
SELECT * FROM `departments` INNER JOIN `company_departments` ON `company_departments`.`department_id` = `departments`.`id` WHERE `departments`.`deleted_at` IS NULL AND ((`company_departments`.`company_id` IN (?,?,?,?)))[1 2 3 4] 0

なお、前述のSet("gorm:auto_preload", true)を設定している場合は、ネストされたモデルのデータも自動で取得することができます。
ただし、Set("gorm:auto_preload", true)を濫用してしまうと、本来は不要なデータも毎回Preloadingしてしまいパフォーマンスが低下してしまう可能性がありますし、アソシエーションが複雑になってくると予期しないエラー(無限ループなど)が発生してしまう可能性がありますので、Preload()でPreloadingする先を指定できる場合は、なるべくきちんと指定しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語「スライスが分からない件」を解決する

Go言語のスライスはとても便利なのですが、いまいちGo言語のスライスが分からないのはなぜでしょうか。いろいろなプログラムを作って検証してみましょう。

基本

例えば、[]intのスライス(intの可変配列とも言える)を作成し、適当に初期化して値を一気に表示するプログラムは、以下のようになります。

basic.go
package main
import "fmt"
func main() {
    a := []int{0, 1, 2, 3, 4, 5}
    fmt.Printf("%#v\n", a)
}

実行結果:

$ go run a.go
[]int{0, 1, 2, 3, 4, 5}

部分的に取り出す

データを取り出すには、a[要素番号]。部分スライスを取り出すには、a[i1:i2]のように書きます。なお、i2には終わりのインデックス+1の値を指定します。

substring.go
...
func main() {
    a := []int{0, 1, 2, 3, 4, 5}
    b := a[0:3]
    fmt.Println(b)
}

実行結果は[0 1 2]です。

なお、部分スライスを得る場合、0や末尾の値を省略できます。

  • a[:3] ... a[0:3] と同じ
  • a[3:] ... a[0:末尾+1] と同じ

スライスへの追加

ここが難しいところですが、配列の追加や削除はあまり直感的ではありません。

末尾への追加は、appendを使います。ポイントなのは、appendの第二引数以降に複数の値を指定してできます。もし、別途スライスを追加したいときは、値...のように記述します。

a := []int{0, 1, 2, 3, 4, 5}
// 一つ追加
a = append(a, 6)

// 複数の追加
a = append(a, 7, 8, 9)
a = append(a, []int{10, 11, 12}...) // --- (*b)
fmt.Println(a)

実行結果: [0 1 2 3 4 5 6 7 8, 9, 10, 11, 12]

上記(*b)が分かりにくいですね。もう少し分かりやすくしてみます。

a := []int{0, 1, 2, 3, 4, 5}
b := []int{6, 7, 8}
// a + b追加
a = append(a, b...)
fmt.Println(a)

実行結果: [0 1 2 3 4 5 6 7 8]

ちなみに、append(a, b)とできませんか?
もちろん、型エラーになるのでできませんが、もしできたとするなら、[0,1,2,3,4,5,[6,7,8]]となってしまいます。b...と書くことでスライスの内容を引数に展開できるのです。

スライスの一部分を削除

任意の要素を削除するには、以下のようにします。例えば、(0から数えて)3番目の要素を削除してみます。

remove.go
a := []int{0, 1, 2, 3, 4, 5}
// 3を削除
a = append(a[:3], a[4:]...)
fmt.Println(a)

実行結果: [0 1 2 4 5]

もう少し詳しくスライスを学ぶ

ここまで、基本的な部分を紹介しましたが、スライスをもっと詳しく調べてみましょう。

疑問-スライスを変更するとアドレスは変わる?

appendでスライスの内容を変更できますが、スライスのアドレスは変わってしまうのでしょうか。

ShowAddress.go
// 初期化
a := []int{0, 1, 2, 3, 4, 5}
// 値を追加
b := append(a, 6)
// 値とアドレスを表示
fmt.Printf("--- a = %#v\n", a)
println(&a)
fmt.Printf("--- b = %#v\n", b)
println(&b)

実行結果:

--- a = []int{0, 1, 2, 3, 4, 5}
0xc00009af60
--- b = []int{0, 1, 2, 3, 4, 5, 6}
0xc00009af48

異なる内容のスライスが二つできるのですから、当然、スライスのアドレスは変わっています。

内容を変更してもアドレスが変わらない場合

しかし、内容を変更しても、スライスのアドレスが変わらない場合もあります。

ShowAddress2.go
a := []int{0, 1, 2, 3, 4, 5}
fmt.Printf("--- a = %#v\n", a)
println(&a)

a = append(a, 6)
fmt.Printf("--- a = %#v\n", a)
println(&a)

同じ変数に代入した場合はスライスのアドレスは変わりません。以下、実行結果です。

--- a = []int{0, 1, 2, 3, 4, 5}
0xc00009af60
--- a = []int{0, 1, 2, 3, 4, 5, 6}
0xc00009af60

これは、先頭の要素を削除したとしても変わりません。

ShowAddress3.go
    a := []int{0, 1, 2, 3, 4, 5}
    fmt.Printf("--- a = %#v\n", a)
    println(&a)

    // 先頭の要素を削除した場合
    a = a[1:]
    fmt.Printf("--- a = %#v\n", a)
    println(&a)

結果:

--- a = []int{0, 1, 2, 3, 4, 5}
0xc00005ef60
--- a = []int{1, 2, 3, 4, 5}
0xc00005ef60

スライスを型として定義する

スライスは、typeを利用して異なる型として定義できます。

type1.go
package main

import "fmt"

type IntSlice []int

func main() {
    a := IntSlice{0, 1, 2, 3, 4, 5}
    fmt.Printf("--- a = %#v\n", a)
}

実行結果:

--- a = main.IntSlice{0, 1, 2, 3, 4, 5}

スライスを型にしてメソッドを定義

さらに、メソッドを定義することもできます。

type2.go
package main
import "fmt"

type IntSlice []int

func (p *IntSlice) Sum() int {
    c := 0
    for _, v := range *p {
        c += v
    }
    return c
}

func main() {
    a := IntSlice{0, 1, 2, 3}
    println(a.Sum())
}

実行結果: 6

スライス自身を変更するメソッド

そして、スライス自身を変更するメソッドも追加できます。以下は、スライスに値を追加するAppendメソッドを定義してみます。

type3.go
package main

import "fmt"

type IntSlice []int

func (p *IntSlice) Append(v int) {
    *p = append(*p, v)
}

func main() {
    a := IntSlice{0, 1, 2, 3}
    println(&a)

    a.Append(4)
    fmt.Printf("%#v\n", a)
    println(&a)
}

実行結果:

0xc00005ef60
main.IntSlice{0, 1, 2, 3, 4}
0xc00005ef60

スライスの複製について

スライスaを作成した後、b:=aのように書いた場合、スライスが複製されるのではなく、bはスライスaのエイリアスのような状態になります。そのため、bの要素を変更した場合、aの要素も変更されてしまいます。

詳しくは、以下が役にたつので参照すると良いでしょう。

簡単なスライスの複製例:

slicecopy.go
a := []int{0, 1, 2}

// 空のスライスを作成
b := make([]int, len(a))
// 内容をコピー
copy(b, a)

fmt.Println(b)

まとめ

スライスがよく分かると、Goの使い勝手が向上します。スライスの理解は、Goをより使いこなすうえでの第一歩と言うことができるでしょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「Golang」 ×「gorilla/mux router」でシンプルなREST APIサーバーを開発する

Why Golang?

Golangの特徴として「シンプル」「静的型付けのため高速」「マルチプロセッシングによる並列処理」があります。

このような特徴からDockerKubernetesなどの高速処理を要求されるインフラ基盤構築サービスでも使われており、またイーサリアムのGethなど「ブロックチェーン」の基盤として活用されているケースもあるようです。

また、C言語をベースに開発された言語のため、「構造体」「ポインタ」「チャネル」など普段TypescriptPythonなどのコードを書いていると若干とっつきにくさはあるのですが、C言語よりは数段理解がしやすく、短い記述で処理を書くことができます。
(私自身、C言語で挫折した経験があったので、若干不安ではありましたがコードがシンプルなため他の言語の経験があれば学習コストは非常に低いと思います。)

まとめると「比較的学習コストが低い」かつ「高速」... 最高ですね!

GoogleGolangときたらgRPCだろと突っ込まれるかもしれませんが、今回は基礎をおさえる意味で単純なREST APIで実装を行います。

何を作るか?

今回、ベタですが「商品管理システム」を開発すると仮定して、商品マスタより商品の閲覧・登録・削除・更新ができるような仕様を想定しています。

とりあえずはREST APIの機能に注力したいので、データベースは使わずシステム内で一時的に保持するような仕組みで作成します。

本来であれば、「生産地」や「工場情報」などを入れるべきですが、今回は構成をシンプルにするために最低限の要素に絞っています。また、カテゴリIDなどのリレーションシップも今回は省き、1テーブルのみで機能を実装します。

Properties    Types     Summaries    
id integer 商品ID
jan_code string JANコード
item_name string 商品名
price integer 価格
category_id integer カテゴリID
series_id integer シリーズID
stock integer 在庫数
discontinued boolean 廃番
release_date datetime 発売日
created_at datetime 作成日
updated_at datetime 更新日
deleted_at datetime 削除日

コード実装

バージョン情報

HTTPルーター

Ginなどのフルスタックフレームワークの導入も考えましたが、REST APIを作成するのに最低限の要素にしたかったのでルーター機能だけを提供するgorilla/muxを採用しました。
これは好みの問題ですが、個人的にReactのように「必要なときに必要な分だけ」みたいなパッケージの導入が一番ベストかなと考えています。

それでは早速gorilla/muxgo modulesを使って導入していきます。
go moduleを使うと、システム内で使われているパッケージを管理できるうえ、ビルド時に依存パッケージを自動インストールできます。(便利!)

shellです
go mod init { プロジェクト名 }
go get github.com/gorilla/mux

go.modgo.sumファイルが自動生成されます。

go.mod
module { プロジェクト名 }

go 1.14

require (
    github.com/gorilla/mux v1.7.4
)

フォルダ構成

MVCモデルです。ルートに配置したmain.goファイルからコントローラを呼び出し、サーバを起動します。

フォルダ構成
merchandise_control_system
├── controllers
│   └── webserver.go
├── go.mod
├── go.sum
└── main.go

コード全体

main.go
package main

// go modulesで管理すると絶対パスでのimportが必須です
import (
    "merchandise_control_system/controllers"
)

func main() {
    controllers.StartWebServer()
}
controllers/webserver.go
package controllers

import (
    "encoding/json"
    "fmt"
    "github.com/gorilla/mux"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

// json: ~ をパラメータに付与すると、jsonエンコード時にパラメータ名を指定することができます。
// また、omitemptyを付与するとパラメータが空のときに、jsonのパラメータから消すことができます。
// これはクライアントアプリと仕様を統一する必要があります。
type ItemParams struct {
    Id           string    `json:"id"`
    JanCode      string    `json:"jan_code,omitempty"`
    ItemName     string    `json:"item_name,omitempty"`
    Price        int       `json:"price,omitempty"`
    CategoryId   int       `json:"category_id,omitempty"`
    SeriesId     int       `json:"series_id,omitempty"`
    Stock        int       `json:"stock,omitempty"`
    Discontinued bool      `json:"discontinued"`
    ReleaseDate  time.Time `json:"release_date,omitempty"`
    CreatedAt    time.Time `json:"created_at"`
    UpdatedAt    time.Time `json:"updated_at"`
    DeletedAt    time.Time `json:"deleted_at"`
}

// ポインタ型でitemsを定義します。今回はこのグローバル変数【配列】がデータベースの役割をします
var items []*ItemParams

func rootPage(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the Go Api Server")
    fmt.Println("Root endpoint is hooked!")
}

func fetchAllItems(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items)
}

func fetchSingleItem(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    vars := mux.Vars(r)
    key := vars["id"]

    for _, item := range items {
        if item.Id == key {
            json.NewEncoder(w).Encode(item)
        }
    }
}

func createItem(w http.ResponseWriter, r *http.Request) {
    reqBody, _ := ioutil.ReadAll(r.Body)
    var item ItemParams
    if err := json.Unmarshal(reqBody, &item); err != nil {
        log.Fatal(err)
    }

    items = append(items, &item)
    json.NewEncoder(w).Encode(item)
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]

    for index, item := range items {
        if item.Id == id {
            items = append(items[:index], items[index+1:]...)
        }
    }
}

func updateItem(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]

    reqBody, _ := ioutil.ReadAll(r.Body)
    var updateItem ItemParams
    if err := json.Unmarshal(reqBody, &updateItem); err != nil {
        log.Fatal(err)
    }

    for index, item := range items {
        if item.Id == id {
            items[index] = &ItemParams{
                Id:           item.Id,
                JanCode:      updateItem.JanCode,
                ItemName:     updateItem.ItemName,
                Price:        updateItem.Price,
                CategoryId:   updateItem.CategoryId,
                SeriesId:     updateItem.SeriesId,
                Stock:        updateItem.Stock,
                Discontinued: updateItem.Discontinued,
                ReleaseDate:  updateItem.ReleaseDate,
                CreatedAt:    item.CreatedAt,
                UpdatedAt:    updateItem.UpdatedAt,
                DeletedAt:    item.DeletedAt,
            }
        }
    }
}

// 先頭を「大文字」にすると外部ファイルから読み込めるようになります。(export)
func StartWebServer() error {
    fmt.Println("Rest API with Mux Routers")
    router := mux.NewRouter().StrictSlash(true)

    // router.HandleFunc({ エンドポイント }, { レスポンス関数 }).Methods({ リクエストメソッド(複数可能) })
    router.HandleFunc("/", rootPage)
    router.HandleFunc("/items", fetchAllItems).Methods("GET")
    router.HandleFunc("/item/{id}", fetchSingleItem).Methods("GET")

    router.HandleFunc("/item", createItem).Methods("POST")
    router.HandleFunc("/item/{id}", deleteItem).Methods("DELETE")
    router.HandleFunc("/item/{id}", updateItem).Methods("PUT")

    return http.ListenAndServe(fmt.Sprintf(":%d", 8080), router)
}

// モックデータを初期値として読み込みます
func init() {
    items = []*ItemParams{
        &ItemParams{
            Id:           "1",
            JanCode:      "327390283080",
            ItemName:     "item_1",
            Price:        2500,
            CategoryId:   1,
            SeriesId:     1,
            Stock:        100,
            Discontinued: false,
            ReleaseDate:  time.Now(),
            CreatedAt:    time.Now(),
            UpdatedAt:    time.Now(),
            DeletedAt:    time.Now(),
        },
        &ItemParams{
            Id:           "2",
            JanCode:      "3273902878656",
            ItemName:     "item_2",
            Price:        1200,
            CategoryId:   2,
            SeriesId:     2,
            Stock:        200,
            Discontinued: false,
            ReleaseDate:  time.Now(),
            CreatedAt:    time.Now(),
            UpdatedAt:    time.Now(),
            DeletedAt:    time.Now(),
        },
    }
}

デモ

APIサーバを起動します。

go run main.go

Postmanからリクエストを送ってみます。(curlコマンドでもOKです)

【GET】 /items

f899b6d09ac1bc86c96424ab33fd5756.png

【GET】 /item/1

116bc4a421560f04cfb02a9b38327e75.png

【POST】 /item

1fc1dd1fc422be364916f0181e16b3bb.png

【DELETE】 /item/1

5a6ef4e714f695d6e6ab5c6cb0bb9ebc.png

33e7b45ab8ab23ec55c3931235dbad53.png

【PUT】 /item/2

700480fb85e54d8aabd5565e51b775c8.png
8b854b697612094b36a382f0ec5435cd.png

まとめ

今回はコントローラ部分のみで、とりあえず動くように実装してみました。
エラーハンドリングは甘々ですので、改善ポイントは多くあると思います。Golangはエラーハンドリングを行う部分が多く、どこまで徹底するのか判断が難しい気がしますね...

C言語をはじめ、静的型付け言語はどうしても難しそうという抵抗感がありますが、「安全」「高速」などメリットも多いかと思います。
Go言語は深く極めるのは難しいのですが、「とりあえず動くものを作る」ハードルは低く、手軽に試せるのかなという印象を受けました。

また、今回の記事では実装しませんでしたが、以下内容も開発予定です。

開発が終わり次第、こちらも記事にしていきたいと思います。

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Golang】ゴルーチン(並行処理)

【Golang】ゴルーチン(並行処理)

Golangの基礎学習〜Webアプリケーション作成までの学習を終えたので、復習を兼ねてまとめていく。 基礎〜応用まで。

package main
//goroutineとsync.WaitGroup
//goroutinの基礎

//軽量のスレッド 並列処理のこと
//暗黙的にGoはやってくれるため、他の言語ほど深い知識がなくても
//勝手にやってくれる

import (
    "fmt"
    "sync"
    "time"
    "runtime"
)


//並列処理
//3
//引数でポインタを宣言
func goroutin(wg *sync.WaitGroup){
    //3
    //wgを終了する
    defer wg.Done()
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(i)
    }
}
/*
wg.doneが無かったり、wg.Add(1)が複数あるとエラーになる。
*/



//1
//通常の処理
func normal(){
    for i := 1; i < 5; i++ {
        //1秒待つ
        time.Sleep(1000 * time.Millisecond)
        fmt.Println(i)
    }
}

func main() {
    /*
    //1
    //通常の処理
    normal()
    //2
    //関数の前に go を書くだけで並列化できる。
    //関数内のtime.sleepを消すと、並列処理のスレッド生成中にループが終了してしまうため、
    //normalが終わったら、終了してしまう為、goroutinは実行されない。
    //time.sleep(2000 * time.Millisecond)で時間を開けると、実行を確認できる。
    go goroutin()
    go goroutin()
    */

    //3
    //上記を防ぐ
    //sync.WaitGroupを宣言
    var wg sync.WaitGroup

    //並列処理が1つあることを設定する。
    wg.Add(2)

    //引数でwgのアドレスを渡す
    go goroutin(&wg)
    go goroutin(&wg)

    //NumCPU Goランタイムが動作する環境のCPUを表す。使用できるCPUのコア数。
    fmt.Printf("CPU: %d\n", runtime.NumCPU())
    //NumGoroutin Goのランタイム上で動作しているゴルーチンの数を表す
    fmt.Printf("NumGoroutin: %d\n", runtime.NumGoroutine())
    //Version Goのバージョン
    fmt.Printf("Version: %s\n", runtime.Version())

    //通常
    normal()

    //wg,Doneが行われるまで、待ってくれる
    wg.Wait()

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

目次

はじめに
はじめに

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む