- 投稿日:2020-05-30T23:44:32+09:00
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 wtzgolint などの静的解析ツールは、このコメントが含まれるファイルのチェックをスキップしますし、 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.go や wtz.go といったオープンソースプロジェクトでも go generate するコードばかりを書いてきたので、ここいらで知見やノウハウ、普段思っていることをまとめておこうと思い、この記事を書き始めました。
まだ書き足りない点がいくつかあるので、この記事は今後も更新していきたいと思います。内容について、意見、感想、質問、誤りの指摘などがありましたらお気軽にお知らせください。よろしくお願いします。
参考文献
- 投稿日:2020-05-30T18:06:03+09:00
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_planstype ActivityPlan struct { Model ActivityPlanName *string `gorm:"" json:"activityPlanName"` Activities []*Activity `gorm:"many2many:activity_plan_activities" json:"activity"` }activitiestype Activity struct { Model ActivityName *string `gorm:"" json:"activityName"` }そして、DBからデータを取得するためのfunctionをActivityPlanモデルに作成し、まずはPreloadなしで実行してみます。
ActivityPlanのfunctionfunc 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[] 12Preloadを使った場合
では、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] 0Auto 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のfunctionfunc 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] 0Nested 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のfunctionfunc 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する先を指定できる場合は、なるべくきちんと指定しましょう。
- 投稿日:2020-05-30T16:50:05+09:00
Go言語「スライスが分からない件」を解決する
Go言語のスライスはとても便利なのですが、いまいちGo言語のスライスが分からないのはなぜでしょうか。いろいろなプログラムを作って検証してみましょう。
基本
例えば、
[]int
のスライス(intの可変配列とも言える)を作成し、適当に初期化して値を一気に表示するプログラムは、以下のようになります。basic.gopackage 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.goa := []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.goa := []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.goa := []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.gopackage 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.gopackage 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.gopackage 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.goa := []int{0, 1, 2} // 空のスライスを作成 b := make([]int, len(a)) // 内容をコピー copy(b, a) fmt.Println(b)まとめ
スライスがよく分かると、Goの使い勝手が向上します。スライスの理解は、Goをより使いこなすうえでの第一歩と言うことができるでしょう!
- 投稿日:2020-05-30T12:05:43+09:00
「Golang」 ×「gorilla/mux router」でシンプルなREST APIサーバーを開発する
Why Golang?
Golang
の特徴として「シンプル」「静的型付けのため高速」「マルチプロセッシングによる並列処理」があります。このような特徴から
Docker
やKubernetes
などの高速処理を要求されるインフラ基盤構築サービスでも使われており、またイーサリアムのGeth
など「ブロックチェーン」の基盤として活用されているケースもあるようです。また、
C言語
をベースに開発された言語のため、「構造体」「ポインタ」「チャネル」など普段Typescript
やPython
などのコードを書いていると若干とっつきにくさはあるのですが、C言語よりは数段理解がしやすく、短い記述で処理を書くことができます。
(私自身、C言語で挫折した経験があったので、若干不安ではありましたがコードがシンプルなため他の言語の経験があれば学習コストは非常に低いと思います。)まとめると「比較的学習コストが低い」かつ「高速」... 最高ですね!
Golang
ときたら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 削除日 コード実装
バージョン情報
- go 1.14
- github.com/gorilla/mux v1.7.4
HTTPルーター
Gin
などのフルスタックフレームワークの導入も考えましたが、REST API
を作成するのに最低限の要素にしたかったのでルーター機能だけを提供するgorilla/mux
を採用しました。
これは好みの問題ですが、個人的にReact
のように「必要なときに必要な分だけ」みたいなパッケージの導入が一番ベストかなと考えています。それでは早速
gorilla/mux
をgo modulesを使って導入していきます。
go module
を使うと、システム内で使われているパッケージを管理できるうえ、ビルド時に依存パッケージを自動インストールできます。(便利!)shellですgo mod init { プロジェクト名 } go get github.com/gorilla/mux
go.mod
・go.sum
ファイルが自動生成されます。go.modmodule { プロジェクト名 } 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.gopackage main // go modulesで管理すると絶対パスでのimportが必須です import ( "merchandise_control_system/controllers" ) func main() { controllers.StartWebServer() }controllers/webserver.gopackage 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
【GET】 /item/1
【POST】 /item
【DELETE】 /item/1
【PUT】 /item/2
まとめ
今回はコントローラ部分のみで、とりあえず動くように実装してみました。
エラーハンドリングは甘々ですので、改善ポイントは多くあると思います。Golang
はエラーハンドリングを行う部分が多く、どこまで徹底するのか判断が難しい気がしますね...C言語をはじめ、静的型付け言語はどうしても難しそうという抵抗感がありますが、「安全」「高速」などメリットも多いかと思います。
Go言語は深く極めるのは難しいのですが、「とりあえず動くものを作る」ハードルは低く、手軽に試せるのかなという印象を受けました。また、今回の記事では実装しませんでしたが、以下内容も開発予定です。
- 「モデル」部分を
Gorm
を利用したMysql
へのデータベース保存(以下URLに、記事を投稿いたしました)- 「ビュー」の部分は切り離して
ReactNative
で別途モバイルアプリ開発開発が終わり次第、こちらも記事にしていきたいと思います。
参考サイト
- 投稿日:2020-05-30T11:48:02+09:00
【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() }
- 投稿日:2020-05-30T03:34:00+09:00
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_planstype ActivityPlan struct { Model ActivityPlanName *string `gorm:"" json:"activityPlanName"` Activities []*Activity `gorm:"many2many:activity_plan_activities" json:"activity"` }activitiestype Activity struct { Model ActivityName *string `gorm:"" json:"activityName"` }上記のstructを元に、DBを作成すると、以下の通り、activity_plansテーブル、activitiesテーブル、及びその中間テーブルであるactivity_plan_activitiesが作成されます。
1.関連に主キーが含まれない場合
前述の順序とは逆になってしまいますが、まずはDBにデータを入れたいので、「関連に主キーが含まれない」形でPOSTし、きちんとCreateが実行されるかを確認してみます。
POST内容{ "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した場合はどうでしょう。
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してみます。
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します。
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)で、以下のようなデータを入れるとどうなるでしょうか。
POST内容{ "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してみます。
POST内容{ "activityPlanName": "プランB", "activity": [ { "activityName": "アクティビティX" }, { "activityName": "アクティビティY" }, { "activityName": "アクティビティZ" } ] }
すると、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" } ] }
すると、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