goaでControllerでDBアクセスする話

最近goaでAPIサーバを書いてるんですけど、DBアクセスしたいときにControllerからDBクライアントをどうやって呼び出すかを色々考えてた話。

goroutineごとにsql.Open()する

メリットはすごく単純だしわかりやすいこと。デメリットはパフォーマンス最悪なこと。

絶対やめようね。

sql.Open()のGoDocにも以下のように書かれています。

The returned DB is safe for concurrent use by multiple goroutines and maintains its own pool of idle connections. Thus, the Open function should be called just once. It is rarely necessary to close a DB.

sql - GoDoc

せっかくThread-safeに設計されてるのにgoroutineごとに生成するのは思想に反してるしコネクションプールでメモリを食うだけなのでやめましょう。

ただ、それでもきちんと動くのは動くのでハッカソン中であれこれ調べている時間がなくて今すぐ書きたい!とかならこれでもいいです。

 

パッケージ変数にしてしまう

これも単純でわかりやすいけど、例えばあるDBクライアントAとBがあって、あるControllerのhogeがAをfugaがBを使うという感じで、複数のControllerdeでDBクライアントを使い分けたいときに、本来hogeではBは使わない(し、使ってはいけない)のに、Bにアクセスできてしまう(fugaも然り)のでよくないです。

でも複数のDBを使い分けるときに危険っていうのは、それほど普遍的なデメリットでもないので、めんどくさかったら実は意外とこれでもいいのかもしれない。

 

MiddleWareでcontextにDBクライアントを差し込む

context.WithValueでDBクライアントをcontextに持たせるようなMiddleWareを差し込めば、ControllerからDBにアクセスすることができます。

一見良さそうなんですけど、こういうcontextの使い方はアンチパターンとして紹介されています。

peter.bourgon.org

Peter BourgonさんはGo kitを作った人です。

詳しくは記事に書いてある通りですけど、contextはある特定のリクエストスコープ内で限定的な値を渡すときに有効に使うことができます。

よく認証情報がcontextに乗っていたりするのはそれですね。

DBクライアントはリクエストによって変わるものではないので、contextに含めるには不適です。

 

Controller構造体に入れて渡す

ここからが本題で、最終的に僕はこの方法に落ち着きました。

公式でこんなサンプルを出しているのを見つけたので、多分この方法がベストプラクティスなんじゃないかと思います。

github.com

今日はこれの解説です。

 

desgin DSL

まずはgoa design DSLですが、これは特に変わったことをする必要はないので、ここではとりあえずGetting Started With goaで使用されているdesignを使います。

goa.design

 

Implement

goagen bootstrapしたらまずは、bottle.goのBottleController構造体にメンバを追加します。

// BottleController implements the bottle resource.
type BottleController struct {
*goa.Controller
DB *sql.DB // <- Controller構造体にメンバを追加する
}

// NewBottleController creates a bottle controller.
func NewBottleController(service *goa.Service, db *sql.DB) *BottleController {
return &BottleController{
Controller: service.NewController("BottleController"),
DB: db, // <- DBクライアントを差し込む
}
}

NewBottleControllerで引数に*sql.DBを追加して、受け取ったDBクライアントを構造体に差し込みます。

Controllerからは、この構造体を通してアクセスします。

次にmain.goでのNewBottleControllerの呼び出しを書き換えます。 

   db, _ := sql.Open("mysql", DSN)

// Mount "bottle" controller
c := NewBottleController(service, db)
app.MountBottleController(service, c)

まずはsql.OpenでDBクライアントdbを用意します。ドライバは例としてmysqlにしていますがなんでもいいです。DSNは適当によしなにしてください。errを握り潰してるのは省スペースのためなので目をつぶってください。

次に生成したDBクライアントをNewBottleControllerの引数に渡します。

実際のアプリケーションではもっとControllerが増えると思いますが、同様にして各Controllerの構造体にDBクライアントを持たせます。

 

ここまでで、ControllerからDBにアクセスできるようになりました。

ControllerからはレシーバからDBクライアントを引き出して使うことになります。

// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
// BottleController_Show: start_implement

db := c.DB

// dbを使ってDBを介する処理

res := &app.GoaExampleBottle{}
return ctx.OK(res)
// BottleController_Show: end_implement
}

これで無事ControllerからDBアクセスするAPIが作れます。

 

まとめ

goaで作ったAPIでDBアクセスする処理が書きたいときは、Controller構造体にDBクライアントを置いて、ControllerメソッドではレシーバからDBクライアントを引き出す形で実装しましょう。