14
9

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.

SangriaでGraphQL APIを実装するのに知っておきたいこと

Posted at

この記事はなに

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へのアクセス可能なオブジェクトが渡されることを期待している。

しかし、サンプルのように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ではCharacterRepoCtxとして要求するので、実行するときには以下のようになる。

Executor.execute(
  SchemaDefinition.StarWarsSchema, 
  queryAst,
  new CharacterRepo,
  ...
)

GraphQLとしてQuery(Mutation)を実行する際にSchemanew CharacterRepoを渡していて、他のRepositoryを使えないようになってしまっている。
複数のモデルにアクセスするようなAPIを実装したいときにはこれでは困る。

どうやって複数のRepositoryやDAOを渡すか

手っ取り早い方法が、Ctxとして使いたいRepositoryやDAOを保持するオブジェクトを用意すればよい。

trait container {
  def userRepo: UserRepo
  def characterRepo: CharacterRepo
}

このcontainerCtxとして使用すればUserRepocharacterRepoのどちらにもアクセス出来るようになる。
使うときには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を使った遅延取得

たとえばUserTodoという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)

このresolverExecutor.executeに渡す。

Executor.execute(
  SchemaDefinition.StarWarsSchema, 
  queryAst,
  containerImpl,
  deferredResolver = resolver,
  ...
)

先程まではUserDao.findByIdでつどDBアクセスしていたのが、Fetcherを使うことによって遅延評価(?)されてまとめてUserDao.findAllByIdsでのアクセスとなり、N+1問題が回避出来る。

14
9
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
14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?