ぺい

渋谷系アドテクエンジニアの落書き

GolangのgoaでAPIをデザインしよう(Model編②)

goaはいいぞ!

f:id:tikasan0804:20170505212036p:plain

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の対応例

f:id:tikasan0804:20170515171457p:plain

普通はManyToManyというと、お互いのテーブルに複数出現するような関係なので、bottlescategoriesが直接繋がっていることを想像すると思いますが、gormにおいてはbottle_categoriesを無視して、ManyToManyで、bottlescategoriesを繋げることで参照することが出来ます。

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を使えばよくあるクエリへの対応などは問題なく出来ます。ですが、どうしても無理な場面もあるので、やっている案件に発生するクエリの複雑さによって採用するかしないか決めるべきだと思います。今回の記事のやり方はあくまで私が考えたやり方なので、もっと良い方法があれば教えて欲しいです!