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

## この記事はなに

[Sangria](http://sangria-graphql.org)でGraphQLなAPIを実装するにあたって、公式のサンプルだけだと少し足りないためそれの補足というかtips的な記事。

Sangriaの導入にはこちらをどうぞ。  
[[Scala]Sangriaを使ってGraphQL APIを実装する - Qiita](https://qiita.com/petitviolet/items/e3e87c3f3e740b3c57ba)

## 複数のモデルにアクセスしたい

[CtxとValについての補足](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)にも少し書いたが、sangriaでは`Ctx`という型パラメータが頻出していてドキュメントでは

> A typical example of such a context object is a service or repository object that is able to access a database

と[書いてある](http://sangria-graphql.org/learn/#input-and-context-objects)。  
つまり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`)しかないこともないはず。

```scala
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`として要求するので、実行するときには以下のようになる。

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

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

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

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

この`container`を`Ctx`として使用すれば`UserRepo`と`characterRepo`のどちらにもアクセス出来るようになる。
使うときには`container`として使えばいいだけなので特に難しくない。

```scala
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`の実装を注入すればよい。
  
```scala
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つのオブジェクトがある。

```scala
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`を取得したい。  
普通に実装するとこのようになる。

```scala
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](http://sangria-graphql.org/learn/#deferred-value-resolution)で紹介されている。
`Deferred`を使えばいいらしい。

まずは`Seq[Id]`から`Seq[User]`を取得するための`Fetcher`を実装する。  
`Fetcher.caching`に`(Ctx, Seq[Id]) => Future[Seq[User]]`な関数を与えるだけでよい。

```scala
val userFetcher: Fetcher[container, User, User, Id] = Fetcher.caching {
  (ctx: container, ids: Seq[Id]) =>
    Future.apply {
      ctx.userDao.findAllByIds(ids)
    }
```

これを使って先程の`TodoType`を改めて実装するとこうなる。

```scala
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](https://github.com/sangria-graphql/sangria/blob/4f96ea218b5db65c57bb296c18e7ded4f5d1a797/src/main/scala/sangria/execution/Executor.scala#L17)を見ると`DeferredResolver[Ctx]`が必要らしいので用意する。
非常に簡単で、[DeferredResolver.fetchers](https://github.com/sangria-graphql/sangria/blob/4f96ea218b5db65c57bb296c18e7ded4f5d1a797/src/main/scala/sangria/execution/deferred/DeferredResolver.scala#L26)に実装した`Fetcher`を渡すだけでよい。

```scala
val resolver: DeferredResolver[container] = DeferredResolver.fetchers(userFetcher)
```

この`resolver`を`Executor.execute`に渡す。

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

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