astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

Sangriaで始めるGraphQL入門

f:id:astamuse:20191120111334p:plain
sangria
お久しぶりでございます。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の実装です。

github.com

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が下記でアクセスできます。

http://localhost:8080/graphql

こんな画面

f:id:astamuse:20191119231758p:plain
playground

こちらはある種のIDE*1になっていて、補完やサジェッスションを提供してくれるため、意外なほど簡単に目的のクエリを記述することができます。

クエリ例

query{
  humans{
    id
    name
    homePlanet
    current_time
  }
}

補完の例

f:id:astamuse:20191120095436p:plain
completion

また右側のDOCS、SCHEMAというところに存在するクエリの仕様が表示されます。

スキーマ表示

f:id:astamuse:20191120095549p:plain
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
}

f:id:astamuse:20191119232453p:plain
tracing

クエリ(サーバサイド)

サーバサイド側で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
  }
}

結果

f:id:astamuse:20191120101351p:plain
double schema

引数を取る場合

query{
  human(id:"1000"){
    name
  }
  droid(id:"2000"){
    id
    name
  }
}

結果

f:id:astamuse:20191120101835p:plain
argument

また、記述時に型エラーなどを教えてくれます(Validation)

以下は文字列型に数値を適用しようとしてエラー

f:id:astamuse:20191120102032p:plain
validation

クライアント側でクエリを自由に記述できるため、ある意味奇妙なクエリもかけるようです。

友達の友達の友達を検索

query  {
  humans{
    name
    friends{
      name
      friends{
        name
        friends{
          name
        }
      }
    }
  }
}

結果:

f:id:astamuse:20191120000357p:plain

まとめ

RESTでAPIを作ろうとした場合、丁寧につくれば作るほど、Endpointが細分化していき、またそれらを繋げるためにエッジ-サーバ間のラウンドトリップが増加する、もしくは統合的な項目を持った、メンテナンス性の低いEndpointが増加する問題があると感じていますが、GraphQLの場合はクエリに複数のスキーマ呼び出しを行ったりといったことが柔軟なので、エッジ-サーバ間の通信量をおさえつつもtidyな開発ができるのでは無いかと期待しています。

最後になりましたが、アスタミューゼでは現在、エンジニア・デザイナーを絶賛大大大募集中です! 興味のある方はぜひ下記バナーからご応募ください!!

*1:graphql-playground-reactというものを利用している

*2:ここに記載

*3:https://github.com/axtstar/sangria-akka-http-example/blob/forBlog/src/main/scala/Types.scala

*4:ここで指定しているので変更は可能

*5:ここに

Copyright © astamuse company, ltd. all rights reserved.