go で gorma を使ってAPI開発してみる。まずはgoa編

ちょっとしたAPIを作ることになりそうなんで、goでAPI+DB設計するとしたらどんなもんがあるのだろう・・・と調べていたら、 [gorma](https://github.com/goadesign/gorma) ってのがそれなりに使えそうな気がしてきました。


ただ、こういうのって、

- 自動化はたしかに楽
- ただしその縛りを受けて意外と辛い

みたいのがあるあるだったりするので、自分の中で気になっていた点をいくつか確認してみました。

参考になったのは [こちら](https://kuroeveryday.blogspot.com/2018/05/goa-design-first-api-generation.html) 。（とても参考にさせて頂きました。ありがとうございます）

ただ、今のgoにあわせて dep を使わないように修正しました。

## dep外し

以下が修正箇所です。

- [こちら](https://kuroeveryday.blogspot.com/2018/05/goa-design-first-api-generation.html) の0. goa/goagenをインストールする` の節で、自分のリポジトリは適当な場所に置けばいい（GOPATH意識しない）
- その中に、 [こちら](https://kuroeveryday.blogspot.com/2018/05/goa-design-first-api-generation.html) の `1. DSLでAPIデザインを書く` と同じ構造を作る

```
./design
├── api.go        // API全体の定義（APIで定義）
├── mediatypes    // レスポンスのスキーマ（MediaTypeで定義）
│   └── task.go
├── usertypes     // リクエストのpayloadなどのスキーマ（Typeで定義）
│   └── task.go
└── resources     // エンドポイントの定義（Resourceで定義）
    └── task.go
```

- その後は、 `go mod init` する
- `go build`とかしてgo.modの内容を修正だけしておく。buildはコケても後でなんとかなるので気にしない
  - この時点ではmain.goがないのでコケる。あとで自動で作られるのでそこで問題回避
- `2. goagen(コードジェネレーター)で自動生成する` までの流れは同じ
- `goagen` は普通に `go install github.com/goadesign/goa/goagen` する
- (これは自分だけですが) `"github.com/BcRikko/learning-goa/design/resources"` な部分は自分のリポジトリに合わせて修正
- ちなみに個人的にimport時の名前変更は理由がない限りやらない主義なので、以下のコードでは `design -> goa` というエイリアスは `design` のままにしてます。

この状態で、

`goagen bootstrap -d github.com/<your github user>/<your respository>/design`

するとファイルが生成されます。この時点でmain.goも出来るのでgo buildも通ります。

あとは、TOPにある `task.go` を修正するだけ。

[こちら](https://kuroeveryday.blogspot.com/2018/05/goa-design-first-api-generation.html)  の`3. APIを実装する` を参考に進めます。



## ここから疑問

自分の中で、この後のフェーズで気になったのは2点です。

#### もしAPIのエンドポイントが修正になった場合等、再度生成するとロジックが全部上書きされてしまうのでは？

当然APIのエンドポイントが追加になることはありますよね。

例えば、**list** というエンドポイントで一覧を取得していた場合に、 **list/detail** というのを足したくなったとします。

その場合、 `resources/task.go` に以下エンドポイントを足します。

```go
var _ = dsl.Resource("Tasks", func() {
	dsl.DefaultMedia(mediatypes.Task)
	dsl.BasePath("/tasks")

	dsl.Action("list", func() {
		dsl.Routing(dsl.GET(""))
		dsl.Description("Retrieve list of tasks")
		dsl.Response(design.OK, dsl.CollectionOf(mediatypes.Task))
	})

	dsl.Action("list_detailed", func() { <--足した部分
		dsl.Routing(dsl.GET("/detail"))
		dsl.Description("Retrieve detail list of tasks")
		dsl.Response(design.OK, dsl.CollectionOf(mediatypes.Task))
	})
```

この状態で `goagen` するとどうなるのか・・・。具体的にはtopの `tasks.go` はどうなるのか。

・・・あれ？変わらないぞ？

ということでヘルプを見てみる。

```
% goagen bootstrap --help                                                                                                                                                (git)-[develop]
Equivalent to running the "app", "main", "client" and "swagger" commands.

Usage:
  goagen bootstrap [flags]

Flags:
      --force            overwrite existing files
  -h, --help             help for bootstrap
      --notest           Prevent generation of test helpers
      --notool           Prevent generation of cli tool
      --pkg string       Name of generated Go package containing controllers supporting code (contexts, media types, user types etc.) (default "app")
      --regen            regenerate scaffolding, maintaining controller implementations
      --tool string      Name of generated tool (default "[API-name]-cli")
      --tooldir string   Name of generated tool directory (default "tool")

Global Flags:
      --debug           enable debug mode, does not cleanup temporary files.
  -d, --design string   design package import path
  -o, --out string      output directory (default ".")
```

`--regen`をすればいいのですね。でやってみました。すると、、、

（以下、修正後のtasks.go）

```go
// ListDetailed runs the list_detailed action.
func (c *TasksController) ListDetailed(ctx *app.ListDetailedTasksContext) error {
	// TasksController_ListDetailed: start_implement

	// Put your logic here

	res := app.XLearningGoaCollection{}
	return ctx.OK(res)

	// TasksController_ListDetailed: end_implement
}
```

**ちゃんとここだけ足されており、既存のロジック書いた部分は影響を受けていませんでした!!**

これは非常に良かったです。まず問題１解決。

次。

### エンドポイントのパスが変わったらどうなんの？

例えば、 `/tasks/:taskID`とかのパスを、ある勢力からの圧力で `どうしても /tasks/show/:taskID`にしてほしいんだよねっ！！！とか言われたらどうなるのか。

その場合 `resources/task.go` を修正することになりますね。

#### 変更前の resources/task.go

```go
	dsl.Action("show", func() {
		dsl.Routing(dsl.GET("/:taskID"))
		dsl.Description("Retrieve detail of task by specified ID")
		dsl.Params(func() {
			dsl.Param("taskID", design.Integer, "ID of task")
		})
		dsl.Response(design.OK)
		dsl.Response(design.NotFound)
		dsl.Response(design.BadRequest, design.ErrorMedia)
	})
```

#### 変更後の resources/task.go

```go
	dsl.Action("show", func() {
		dsl.Routing(dsl.GET("/show/:taskID")) <-- ここを修正した
		dsl.Description("Retrieve detail of task by specified ID")
		dsl.Params(func() {
			dsl.Param("taskID", design.Integer, "ID of task")
		})
		dsl.Response(design.OK)
		dsl.Response(design.NotFound)
		dsl.Response(design.BadRequest, design.ErrorMedia)
	})
```

さて、ここで `goagen` すればいいのだと思うのですが、まずその直前のtopのtasks.goで、当該APIがどうなっているかというと・・・

#### goagen再実行前の topのtasks.go

```
// Show runs the show action.
func (c *TasksController) Show(ctx *app.ShowTasksContext) error {
	// TasksController_Show: start_implement

	// Put your logic here

	if ctx.TaskID == 0 {
		return ctx.NotFound()
	}

	res := &app.XLearningGoa{
		ID:        ctx.TaskID,
		Title:     "example task title",
		Done:      false,
		CreatedAt: time.Now(),
		UpdatedAt: time.Now(),
	}
	return ctx.OK(res)

	// TasksController_Show: end_implement
}
```

#### gogen 再実行前の app/controllers.go

```go
	h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
		// Check if there was an error loading the request
		if err := goa.ContextError(ctx); err != nil {
			return err
		}
		// Build the context
		rctx, err := NewShowTasksContext(ctx, req, service)
		if err != nil {
			return err
		}
		return ctrl.Show(rctx)
	}
	service.Mux.Handle("GET", "/api/tasks/:taskID", ctrl.MuxHandler("show", h, nil))
	service.LogInfo("mount", "ctrl", "Tasks", "action", "Show", "route", "GET /api/tasks/:taskID") <-- ここは関係しそう
```

さて、 `topのtasks.go` が変更されず、 `app/controllers.go` の当該箇所が正しく変更されればOKですよね。

やってみます。



### topのtasks.go

ロジックは維持されたままです。

```go
// Show runs the show action.
func (c *TasksController) Show(ctx *app.ShowTasksContext) error {
	// TasksController_Show: start_implement

	// Put your logic here

	if ctx.TaskID == 0 {
		return ctx.NotFound()
	}

	res := &app.XLearningGoa{
		ID:        ctx.TaskID,
		Title:     "example task title",
		Done:      false,
		CreatedAt: time.Now(),
		UpdatedAt: time.Now(),
	}
	return ctx.OK(res)

	// TasksController_Show: end_implement
}
```



### app/controllers.go

こちらはちゃんと変わってますね。

```go
		// Check if there was an error loading the request
		if err := goa.ContextError(ctx); err != nil {
			return err
		}
		// Build the context
		rctx, err := NewShowTasksContext(ctx, req, service)
		if err != nil {
			return err
		}
		return ctrl.Show(rctx)
	}
	service.Mux.Handle("GET", "/api/tasks/show/:taskID", ctrl.MuxHandler("show", h, nil))
	service.LogInfo("mount", "ctrl", "Tasks", "action", "Show", "route", "GET /api/tasks/show/:taskID")
```

ということで、とりあえず上記の２点のようなケースには対応できてそうですね。

## 番外編

`resources/task.go` で、もし `dsl.Action` の第一引数を変えるとします。つまり以下のようなこと。

```
	dsl.Action("show", func() { <-- ここのshowを変えたい！
		dsl.Routing(dsl.GET("/show/:taskID"))
		dsl.Description("Retrieve detail of task by specified ID")
		dsl.Params(func() {
			dsl.Param("taskID", design.Integer, "ID of task")
		})
		dsl.Response(design.OK)
		dsl.Response(design.NotFound)
		dsl.Response(design.BadRequest, design.ErrorMedia)
	})
```

このようなケースは流石に無理でしたね。Showが消えて、Show2が出来、ロジックは初期化されてました。



とはいえ、それなりにちゃんと使えそうな気がするんですけどね。もう少しいろいろ検証してみます。