SangriaでGraphQL APIを実装するのに知っておきたいこと
2018-01-30
QiitaScalaGraphQLsangriaこの記事はなに
Sangriaで GraphQL な API を実装するにあたって、公式のサンプルだけだと少し足りないためそれの補足というか tips 的な記事。
Sangria の導入にはこちらをどうぞ。
[Scala]Sangria を使って GraphQL API を実装する - Qiita
複数のモデルにアクセスしたい
Ctx と Val についての補足にも少し書いたが、sangria ではCtx
という型パラメータが頻出していてドキュメントでは
A typical example of such a context object is a service or repository object that is able to access a database
と書いてある。
つまり Repository や Service のような DB へのアクセス可能なオブジェクトが渡されることを期待している。
https://qiita.com/petitviolet/items/e3e87c3f3e740b3c57ba#ctx%E3%81%A8val%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E3%81%AE%E8%A3%9C%E8%B6%B3
しかし、サンプルのように Repository が 1 種類(CharacterRepo
)しかないこともないはず。
val Query = ObjectType[CharacterRepo, Unit](
"Query", fields[CharacterRepo, Unit](
Field("hero", Character,
arguments = EpisodeArg :: Nil,
resolve = ctx ⇒ ctx.ctx.getHero(ctx.argOpt(EpisodeArg))),
...
))
val StarWarsSchema = Schema(Query)
StarWarsSchema
ではCharacterRepo
をCtx
として要求するので、実行するときには以下のようになる。
Executor.execute(
SchemaDefinition.StarWarsSchema,
queryAst,
new CharacterRepo,
...
)
GraphQL として Query(Mutation)を実行する際にSchema
とnew CharacterRepo
を渡していて、他の Repository を使えないようになってしまっている。
複数のモデルにアクセスするような API を実装したいときにはこれでは困る。
どうやって複数の Repository や DAO を渡すか
手っ取り早い方法が、Ctx
として使いたい Repository や DAO を保持するオブジェクトを用意すればよい。
trait container {
def userRepo: UserRepo
def characterRepo: CharacterRepo
}
このcontainer
をCtx
として使用すればUserRepo
とcharacterRepo
のどちらにもアクセス出来るようになる。
使うときにはcontainer
として使えばいいだけなので特に難しくない。
val Query = ObjectType[container, Unit](
"Query", fields[container, Unit](
Field("hero", Character,
arguments = EpisodeArg :: Nil,
resolve = ctx ⇒ ctx.ctx.characterRepo.getHero(ctx.argOpt(EpisodeArg))), // characterRepoを使用
Field("user", User,
arguments = UserArg :: Nil,
resolve = ctx ⇒ ctx.ctx.userRepo.getUser(ctx.argOpt(UserArg))), // userRepoを使用
...
))
val StarWarsSchema = Schema(Query)
このようにして定義したSchema
に対して GraphQL を実行するには以下のように、container
の実装を注入すればよい。
object containerImpl extends container {
override val userRepo = new UserRepoImpl()
override val characterRepo = new CharacterRepoImpl()
}
Executor.execute(
SchemaDefinition.StarWarsSchema,
queryAst,
containerImpl,
...
)
Ctx ごとにExecutor
を作成して合成できるとよいのだが...。
なお以降のサンプルコードではcontainer
を使用している。
Fetcher を使った遅延取得
たとえばUser
とTodo
という 2 つのオブジェクトがある。
case class User(id: Id,
name: String,
email: String,
createdAt: ZonedDateTime,
updatedAt: ZonedDateTime)
case class Todo(id: Id,
title: String,
description: String,
userId: Id,
createdAt: ZonedDateTime,
updatedAt: ZonedDateTime)
Todo
にはuserId
があり、これを使ってUser
を取得したい。
普通に実装するとこのようになる。
lazy val TodoType: ObjectType[container, Todo] =
derive.deriveObjectType(
derive.AddFields(
Field("user", OptionType(UserType), // UserTypeはObjectType[Unit, User]のインスタンス
resolve = { ctx: Context[container, Todo] =>
// ここでTodo#userIdを使ってDBからUserを取得する
ctx.ctx.userDao.findById(ctx.value.userId)
})
)
)
このような実装ではいわゆる N+1 問題が生じてしまう。
N+1 問題への対策はDeferred Value Reslutionで紹介されている。
Deferred
を使えばいいらしい。
まずはSeq[Id]
からSeq[User]
を取得するためのFetcher
を実装する。
Fetcher.caching
に(Ctx, Seq[Id]) => Future[Seq[User]]
な関数を与えるだけでよい。
val userFetcher: Fetcher[container, User, User, Id] = Fetcher.caching {
(ctx: container, ids: Seq[Id]) =>
Future.apply {
ctx.userDao.findAllByIds(ids)
}
これを使って先程のTodoType
を改めて実装するとこうなる。
lazy val TodoType: ObjectType[container, Todo] =
derive.deriveObjectType(
derive.AddFields(
Field("user", OptionType(UserType),
resolve = { ctx: Context[container, Todo] =>
// userFetcherを使ってuserIdからUserを取得する
userFetcher.defer(ctx.value.userId)
})
)
)
このTodoType
を使って Query や Mutation を実装すればよいが、これだけでExecutor.execute
を実行すると、
sangria.execution.deferred.UnsupportedDeferError: Deferred resolver is not defined for deferred value ...
というエラーが出てしまう。Deferred resolver
が定義されていないというエラー。
sangria/Executor.scalaを見るとDeferredResolver[Ctx]
が必要らしいので用意する。
非常に簡単で、DeferredResolver.fetchersに実装したFetcher
を渡すだけでよい。
val resolver: DeferredResolver[container] = DeferredResolver.fetchers(userFetcher)
このresolver
をExecutor.execute
に渡す。
Executor.execute(
SchemaDefinition.StarWarsSchema,
queryAst,
containerImpl,
deferredResolver = resolver,
...
)
先程まではUserDao.findById
でつど DB アクセスしていたのが、Fetcher
を使うことによって遅延評価(?)されてまとめてUserDao.findAllByIds
でのアクセスとなり、N+1 問題が回避出来る。
from: https://qiita.com/petitviolet/items/5b1cbc2116c537953eab