[GraphQL] sangria-playgroundを解析してみた [Play]
Scala製のGraphQLサーバを構築するためのライブラリSangriaをPlay 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