4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

ちょっとした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が出来、ロジックは初期化されてました。

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

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?