お久しぶりでございます。Scalaでバックエンドを開発しているaxtstar(@axtstart)です。
今回は、WebAPI開発時にSangria(GraphQL)を使ってみて便利だなと感じたので紹介します。
GraphQLとは?
GraphQL(グラフQL)は、APIのために作られた、データクエリとデータ操作のための言語と、保存されたデータに対してクエリを実行するランタイムである
~~
GraphQLは、2012年にFacebookの内部で開発され、2015年に公開された
(抜粋: Wikipedia)
GitHubやNetflix、SpotifyでGraphQLを利用しているというニュースも記憶に新しいかと思います。
REST vs GraphQL
上記、Wikipediaにもあるように、RESTと比較して、 ウェブAPIの開発に、その他のWebサービスと比較して、効率的、堅牢、フレキシブルなアプローチを提供することを目的としているようです。
また、機能としては、読み込み、書き込み(ミューテーション)、データのサブスクリプション(リアルタイム更新機能)を持っています。
RESTとGraphQLの比較表
-- | REST | GraphQL |
---|---|---|
主導 | クライアントドリブン | サーバドリブン |
エンドポイント | インターフェース数分 | 基本的には一つ |
通信 | JSON | JSON |
ただ、以下の点では注意が必要とのこと
- 比較的新しい技術のため、サーバライブラリは、言語によって差がある
- クライアントでどんなクエリでもかけるのでパフォーマンスに注意
- キャッシュの機構はRESTより複雑になる
- ファイルアップロードのような機能は無い
Sangriaとは?
Sangriaとは、Scala版GraphQLの実装です。
play-frameworkやakka-http上で動作するようです。
今回はakka-http上で動作する公式サンプルに基づいて説明していきます。
依存関係
akka-http、JSONの変換にcirceを用いたバージョンのbuild.sbtは以下な感じです。
libraryDependencies ++= Seq( "org.sangria-graphql" %% "sangria" % "1.4.2", "org.sangria-graphql" %% "sangria-slowlog" % "0.1.8", "org.sangria-graphql" %% "sangria-circe" % "1.2.1", "com.typesafe.akka" %% "akka-http" % "10.1.10", "de.heikoseeberger" %% "akka-http-circe" % "1.29.1", "io.circe" %% "circe-core" % "0.12.1", "io.circe" %% "circe-parser" % "0.12.1", "io.circe" %% "circe-optics" % "0.9.3" )
公式のakka-httpサンプルとはほんの少し変えています。
起動
sbt run で起動すると、playgroundが下記でアクセスできます。
こんな画面
こちらはある種のIDE*1になっていて、補完やサジェッスションを提供してくれるため、意外なほど簡単に目的のクエリを記述することができます。
クエリ例
query{ humans{ id name homePlanet current_time } }
補完の例
また右側のDOCS、SCHEMAというところに存在するクエリの仕様が表示されます。
スキーマ表示
スキーマ定義
Scalaで記載するスキーマ定義はsangria.schema._ で提供される、DSL*2で記述します。
EnumType、InterfaceType、ObjectType、Argument、ListType、OptionTypeなどを定義でき、最終的にObjectTypeとしてクエリのスキーマとして公開します。 StringやIntなども組み込みのTypeが存在するようですが、LocalDateTimeのような、文字列化の法則が決まっていないようなTypeは自分でParserを記述する必要があるようです。
スキーマ定義の例
Enum
val EpisodeEnum = EnumType( "Episode", Some("One of the films in the Star Wars Trilogy"), List( EnumValue("NEWHOPE", value = Episode.NEWHOPE, description = Some("Released in 1977.")), EnumValue("EMPIRE", value = Episode.EMPIRE, description = Some("Released in 1980.")), EnumValue("JEDI", value = Episode.JEDI, description = Some("Released in 1983."))))
ObjectTypeの例
val Human = ObjectType( "Human", "A humanoid creature in the Star Wars universe.", interfaces[CharacterRepo, Human](Character), fields[CharacterRepo, Human]( Field("id", StringType, Some("The id of the human."), resolve = _.value.id), Field("name", OptionType(StringType), Some("The name of the human."), resolve = _.value.name), Field("friends", ListType(Character), Some("The friends of the human, or an empty list if they have none."), resolve = ctx ⇒ characters.deferSeqOpt(ctx.value.friends)), Field("appearsIn", OptionType(ListType(OptionType(EpisodeEnum))), Some("Which movies they appear in."), resolve = _.value.appearsIn map (e ⇒ Some(e))), Field("homePlanet", OptionType(StringType), Some("The home planet of the human, or null if unknown."), resolve = _.value.homePlanet), /* 試しに追加した型 */ Field("current_time", Types.LocalDateTimeType, Some("current time."), resolve = _.value.current_time), ))
引数定義
val LimitArg = Argument("limit", OptionInputType(IntType), defaultValue = 20)
LimitArg(最大取得件数)をクエリ定義の部分で利用する
Field("humans", ListType(Human), arguments = LimitArg :: OffsetArg :: Nil, resolve = ctx ⇒ ctx.ctx.getHumans(ctx arg LimitArg, ctx arg OffsetArg))
上記、Types.LocalDateTimeType(LocalDateTime)のパーサー例*3
object Types { case object DateCoercionViolation extends ValueCoercionViolation("Date value expected") def parseDate(s: String) = Try(LocalDateTime.parse(s)) match { case Success(date) => Right(date) case Failure(_) => Left(DateCoercionViolation) } implicit val LocalDateTimeType = ScalarType[LocalDateTime]( "DateTime", coerceOutput = (dt, _) => dt.toString, coerceUserInput = { case s: String => parseDate(s) case _ => Left(DateCoercionViolation) }, coerceInput = { case StringValue(s, _, _, _, _) => parseDate(s) case _ => Left(DateCoercionViolation) }) }
パフォーマンス計測
GraphQLは複雑なクエリやネストの深いクエリをクライアントから自由に書けるようになっているためか、パフォーマンス測定を有効にできるようにサンプルではなっていました。
HTTP Headersに以下の設定を行うと有効になるよう*4です。
{ "X-Apollo-Tracing":true }
クエリ(サーバサイド)
サーバサイド側でDSLライクにクエリを書くこともできます。 RESTのグルー(のり)として利用する*5のもありかなと思いました。
path("query"){ get { import sangria.ast._ import sangria.macros._ val queryAst: Document = graphql""" { humans { name } } """ executeGraphQL(queryAst, None, Json.obj(), false) } }
結果
$ curl http://localhost:8080/query % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 139 100 139 0 0 158 0 --:--:-- --:--:-- --:--:-- 158 {"data":{"humans":[{"name":"Luke Skywalker"},{"name":"Darth Vader"},{"name":"Han Solo"},{"name":"Leia Organa"},{"name":"Wilhuff Tarkin"}]}}
クエリ
ここからはクライアント側でどのように呼び出すかを決めるクエリの記述法に関して見ていきます。
複数のスキーマを同時に呼び出す
query{ humans{ name } droids{ name } }
結果
引数を取る場合
query{ human(id:"1000"){ name } droid(id:"2000"){ id name } }
結果
また、記述時に型エラーなどを教えてくれます(Validation)
以下は文字列型に数値を適用しようとしてエラー
クライアント側でクエリを自由に記述できるため、ある意味奇妙なクエリもかけるようです。
友達の友達の友達を検索
query { humans{ name friends{ name friends{ name friends{ name } } } } }
結果:
まとめ
RESTでAPIを作ろうとした場合、丁寧につくれば作るほど、Endpointが細分化していき、またそれらを繋げるためにエッジ-サーバ間のラウンドトリップが増加する、もしくは統合的な項目を持った、メンテナンス性の低いEndpointが増加する問題があると感じていますが、GraphQLの場合はクエリに複数のスキーマ呼び出しを行ったりといったことが柔軟なので、エッジ-サーバ間の通信量をおさえつつもtidyな開発ができるのでは無いかと期待しています。
最後になりましたが、アスタミューゼでは現在、エンジニア・デザイナーを絶賛大大大募集中です! 興味のある方はぜひ下記バナーからご応募ください!!