ちょっとしたAPIを作ることになりそうなんで、goでAPI+DB設計するとしたらどんなもんがあるのだろう・・・と調べていたら、 gorma ってのがそれなりに使えそうな気がしてきました。
ただ、こういうのって、
- 自動化はたしかに楽
- ただしその縛りを受けて意外と辛い
みたいのがあるあるだったりするので、自分の中で気になっていた点をいくつか確認してみました。
参考になったのは こちら 。(とても参考にさせて頂きました。ありがとうございます)
ただ、今のgoにあわせて dep を使わないように修正しました。
dep外し
以下が修正箇所です。
- こちら の0. goa/goagenをインストールする` の節で、自分のリポジトリは適当な場所に置けばいい(GOPATH意識しない)
- その中に、 こちら の
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
を修正するだけ。
こちら の3. APIを実装する
を参考に進めます。
ここから疑問
自分の中で、この後のフェーズで気になったのは2点です。
もしAPIのエンドポイントが修正になった場合等、再度生成するとロジックが全部上書きされてしまうのでは?
当然APIのエンドポイントが追加になることはありますよね。
例えば、list というエンドポイントで一覧を取得していた場合に、 list/detail というのを足したくなったとします。
その場合、 resources/task.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)
// 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
}
ちゃんとここだけ足されており、既存のロジック書いた部分は影響を受けていませんでした!!
これは非常に良かったです。まず問題1解決。
次。
エンドポイントのパスが変わったらどうなんの?
例えば、 /tasks/:taskID
とかのパスを、ある勢力からの圧力で どうしても /tasks/show/:taskID
にしてほしいんだよねっ!!!とか言われたらどうなるのか。
その場合 resources/task.go
を修正することになりますね。
変更前の resources/task.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
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
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
ロジックは維持されたままです。
// 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
こちらはちゃんと変わってますね。
// 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")
ということで、とりあえず上記の2点のようなケースには対応できてそうですね。
番外編
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が出来、ロジックは初期化されてました。
とはいえ、それなりにちゃんと使えそうな気がするんですけどね。もう少しいろいろ検証してみます。