GolangのgoaでAPIをデザインしよう(Model編②)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
Model層をいい感じにしてくれるgorma
前回の記事で、紹介したgormaですが、gormの仕様上、すごーーーーーく多対多参照が分かりにくいです。これについては慣れれば大した問題ではないので、やり方を紹介したいと思います。
なので、この記事はgormaの記事というよりは、gormでの多対多つまりManyToManyの参照方法の解説に近くなりそうです。
今回使うサンプル
github.com
$ go get github.com/tikasan/goa-simple-sample または $ ghq get github.com/tikasan/goa-simple-sample
今回のケースのManyToManyの対応例
普通はManyToManyというと、お互いのテーブルに複数出現するような関係なので、bottles
とcategories
が直接繋がっていることを想像すると思いますが、gormにおいてはbottle_categories
を無視して、ManyToManyで、bottles
とcategories
を繋げることで参照することが出来ます。
bottlesテーブルの例
関係性の見方
関係性 | 対象Model名 |
---|---|
Belongs To | Account |
Has Many | BottleCategory |
Many To Many | Category |
これを理解した上で、gormaでどうすれば良いか?
Model("Bottle", func() { RendersTo(BottleData) Description("celler bottle") Field("id", gorma.Integer, func() { PrimaryKey() }) Field("name", gorma.String) Field("quantity", gorma.Integer) Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) BelongsTo("Account") HasMany("BottleCategories", "BottleCategory") ManyToMany("Category", "bottle_categories") <-----これ })
上記のような修正を行った上で、コード生成をし直すと。
// celler bottle type Bottle struct { ID int `gorm:"primary_key"` AccountID int // Belongs To Account BottleCategories []BottleCategory // has many BottleCategories Categories []Category `gorm:"many2many:bottle_categories"` <---これ Name string Quantity int CreatedAt time.Time // timestamp DeletedAt *time.Time // nullable timestamp (soft delete) UpdatedAt time.Time // timestamp Account Account }
実装
これで、準備が整いました。実際にどのようにすれば参照することが出来るか、コントローラーとモデルのクエリを実装します。
Model
横に長過ぎるので、gistに上げました。
処理の解説
var native []*Bottle var objs []*app.BottleRelation err := m.Db.Scopes(BottleFilterByAccount(accountID, m.Db)). // bottlesテーブルをベースにクエリを実行する Table(m.TableName()). // PreloadでbottleCategoriesテーブルを呼ぶ Preload("BottleCategories"). // Preloadでaccountsテーブルを呼ぶ Preload("Account"). // 複数件取得する Find(&native). Error
ここまでで処理で取得出来るjson
[ { "account": { "email": "example1@gmail.com", "id": 1, "name": "ユーザー1" }, "categories": null, <---ここは取れていない ], "id": 1, "name": "赤ワイン1", "quantity": 20 }, { "account": { "email": "example1@gmail.com", "id": 1, "name": "ユーザー1" }, "categories": null, "id": 2, "name": "赤ワイン2", "quantity": 10 } ]
カテゴリを取得する(少しパフォーマンスが気になる)
Relatedというメソッドも使えるそうですが、InnerJOINの動きに限定されます。
var native []*Bottle var objs []*app.BottleRelation // 取得したbottlesの情報をループさせる for k, v := range native { // bottlesの中のbottle_categoriesの情報をループさせる for k2, v2 := range v.BottleCategories { // bottles_categoriesの情報を使って、個々のカテゴリの詳細情報を取得する native2 := []*BottleCategory{} m.Db.Preload("Category"). Where("bottle_id = ?", v2.BottleID). Find(&native2) // 取得した情報を元のBottlesの構造体に代入する native[k].Categories = append(native[k].Categories, native2[k2].Category) } }
この処理で出来上がるjson
[ { "account": { "email": "example1@gmail.com", "id": 1, "name": "ユーザー1" }, "categories": [ { "id": 1, "name": "赤" }, { "id": 3, "name": "ワイン" } ], "id": 1, "name": "赤ワイン1", "quantity": 20 }, { "account": { "email": "example1@gmail.com", "id": 1, "name": "ユーザー1" }, "categories": [ { "id": 2, "name": "白" }, { "id": 3, "name": "ワイン" } ], "id": 2, "name": "赤ワイン2", "quantity": 10 } ]
Controller
このメソッドを使って、実装するコントローラーのコードは以下の通りです。
// ListRelation runs the listRelation action. func (c *BottlesController) ListRelation(ctx *app.ListRelationBottlesContext) error { // BottlesController_ListRelation: start_implement // Put your logic here bdb := models.NewBottleDB(c.db) b := bdb.ListBottleFullScan(ctx.Context, 0) // BottlesController_ListRelation: end_implement res := app.BottleRelationCollection{} res = b return ctx.OKRelation(res) }
まとめ
gormaを使えばよくあるクエリへの対応などは問題なく出来ます。ですが、どうしても無理な場面もあるので、やっている案件に発生するクエリの複雑さによって採用するかしないか決めるべきだと思います。今回の記事のやり方はあくまで私が考えたやり方なので、もっと良い方法があれば教えて欲しいです!