🐈

[GraphQL] sangria-playgroundを解析してみた [Play]

2021/01/11に公開

Scala製のGraphQLサーバを構築するためのライブラリSangriaPlay Frameworkで利用するにはどのようにするのか調べたところ、公式で用意しているSangriaの実装sangria-graphql/sangria-playgroundがPlay Framework製だったので、どのような実装になっているか分析してみたため記事として残すことにしました。

ライブラリ依存(build.sbt)

build.sbtの依存を見てみると以下のようでした。

"org.sangria-graphql" %% "sangria" % "2.0.1",
"org.sangria-graphql" %% "sangria-slowlog" % "2.0.1",
"org.sangria-graphql" %% "sangria-play-json" % "2.0.1"

sangriaはライブラリ本体になります。

sangria-slowlogはGraphQLのクエリの実行を計測するプロファイリング実装のようです。このようなパフォーマンス測定、メトリクスの収集、セキュリティの強化などはMiddlewareという仕組みで自由に組み込み拡張が行えることがSangriaのウリの一つのようです。

sangria-play-jsonはplay-jsonのためのSangriaの実行時のserialize(ResultMarshaller)deserialize(InputUnmarshaller)の実装になります。Sangriaはこれらをライブラリでハードコードするのではなく分離して管理することでいろんなフレームワークやデータ変換ライブラリに対応できるような設計にしているようです。

ルーティング(routes)

routesファイルに書いてあるのは、以下のようなものです。

GET        /graphql              controllers.Application.graphql(query: String, variables: Option[String], operation: Option[String])
POST       /graphql              controllers.Application.graphqlBody
GET        /render-schema        controllers.Application.renderSchema
GET        /playground           controllers.Application.playground

/graphqlは、その名の通りGrapQLのクエリを実行するためのパスです。GET, POSTはどちらの実装の違いはないようでした。

/render-schameは、SangriaのSchema型の値をpretty printした結果(Schema Definition Language)を返すためのパスでした。

/playgroundは、GraphQLのPlaygroundです。graphql-playground-reactというライブラリを利用しているようでした。機能はGraphiQLと差がなさそうでしたがテーマがかっこよかったです。

コントローラ(Application)

コントローラの実装ではPlay特有の実装は部分的なため、その部分のみを取り上げて解説をします。

POST graphqlの実装では、Request Bodyの中身からクエリとオペレーション名、変数をそれぞれ取り出してから、executeQueryメソッドに渡しています。

def graphqlBody = Action.async(parse.json) { request =>
  val query = (request.body \ "query").as[String]
  val operation = (request.body \ "operationName").asOpt[String]

  val variables = (request.body \ "variables").toOption.flatMap {
    case JsString(vars) => Some(parseVariables(vars))
    case obj: JsObject => Some(obj)
    case _ => None
  }

  executeQuery(query, variables, operation, isTracingEnabled(request))
}

クエリの計測の実行有無を決めるのは、リクエストヘッダのX-Apollo-Tracingの値を見ているようです。

def isTracingEnabled(request: Request[_]) = request.headers.get("X-Apollo-Tracing").isDefined

クエリに渡す変数オブジェクトなどは、play-jsonのJsObject型が扱えるように、ライブラリ依存で説明をしたsangria-play-jsonの暗黙の型変換を利用しています。

import sangria.marshalling.playJson._
...
 private def executeQuery(query: String, variables: Option[JsObject], operation: Option[String], tracing: Boolean) =
    QueryParser.parse(query) match {

      // query parsed successfully, time to execute it!
      case Success(queryAst) =>
        Executor.execute(SchemaDefinition.StarWarsSchema, queryAst, new CharacterRepo,
            operationName = operation,
            variables = variables getOrElse Json.obj(),
            deferredResolver = DeferredResolver.fetchers(SchemaDefinition.characters),
            exceptionHandler = exceptionHandler,
            queryReducers = List(
              QueryReducer.rejectMaxDepth[CharacterRepo](15),
              QueryReducer.rejectComplexQueries[CharacterRepo](4000, (_, _) => TooComplexQueryError)),
            middleware = if (tracing) SlowLog.apolloTracing :: Nil else Nil)
          .map(Ok(_))
...

エラー処理はクエリエラーかそれ以外かでエラーの種類を分けて、それぞれBadRequestかInternalServerErrorを出しています。

.recover {
  case error: QueryAnalysisError => BadRequest(error.resolveError)
  case error: ErrorWithResolver => InternalServerError(error.resolveError)
}

まとめ

Sangria自身がGraphQLを扱うためだけのライブラリと思っていたためフルスタックフレームワークであるPlay Frameworkに導入するにはひと癖あるのかな?と思っていましたが、公式で丁寧かつ薄いサンプルが用意されているため非常に助かりました。また、Play特有であるplay-jsonの扱いも上手くMarshallingの仕組みが吸収しているようなので結果としてSangriaの使い方だけを正しく学べば簡単に導入することが出来るように感じました。

Discussion