Help us understand the problem. What is going on with this article?

[Scala]Sangriaを使ってGraphQL APIを実装する

More than 1 year has passed since last update.

この記事はなに?

ScalaでGraphQLサーバを実装するためのsangria/sangriaの導入。

公式ドキュメントおよびLarning Sangriaを読むのが一番速い。
さらに公式がサンプルも用意しているので、そちらを参照するのが良い。

sangria-graphql/sangria-akka-http-example

が、簡単に一覧出来るように短めのサンプルを載せておく。

使い方のサンプル

Query/Mutationを投げられるようになるまで

ある独自のオブジェクトに対してQueryおよびMutationを投げられるようにするまで。
ソースコードはGithubに置いた。

まずはGraphQLでやり取りしたいクラスを定義。

  case class MyObject(id: Long, name: String)
  class MyObjectRepository() {  // DBへのアクセスをするやつ
    def findAll: Seq[MyObject] = ???
    def findById(id: Long): Option[MyObject] = ???
    def store(obj: MyObject): MyObject = ???
    def create(name: String): MyObject = ???
  }

このMyObjectに対応するGraphQLスキーマを定義していく。

case classを単純に変換するだけならsangria.macros.derive.deriveObjectTypeを使うと機械的に実装できる。

  // MyObjectをGraphQL的に表現する
  // macroを使って楽に導出する
  val myObj = sangria.macros.derive.deriveObjectType[Unit, MyObject]()

続いて、allMyObjectRepository.findAllfind_by_id(id)MyObjectRepository.findById(id)の結果を返却するようなQueryを実装する。

  // MyObjectに対するQuery
  lazy val myQuery: ObjectType[MyObjectRepository, Unit] = {
    ObjectType.apply(
      "MyQuery",
      fields[MyObjectRepository, Unit](
        Field("all", ListType(myObj), resolve = c => c.ctx.findAll), // allで全部返す
        {
          val idArg = Argument("id", LongType)
          Field("find_by_id", OptionType(myObj), arguments = idArg :: Nil, 
            resolve = c => c.ctx.findById(ctx arg idArg)) // find_by_id(id)でid指定して取得する
        }
      )
    )
  }

Queryを実行するためにはMyObjectRepositoryが必要なので、ObjectTypeCtx型パラメータはMyObjectRepositoryを与える。

Fieldの第二引数で返却するデータの型を表し、arguments引数ではこのQueryを実行するための入力、resolveには実際にMyObjectRepositoryを使ってデータにアクセスする関数を与える。

Queryが実装できたので次はMutation。
MyObjectの情報を貰って保存するstoreとnameだけもらって残りは生成するcreateを実装してみる。

storeのためにMyObjectを入力として受け取れるようにする必要があるため、そのあたりから。

  // MyObjectをinputとして受け取れるようにする
  // ここはもう少しいいやり方があるかも知れない...
  val myObjectInputType: InputObjectType[MyObject] =
  InputObjectType[MyObject]("MyObjectInput",
                            List(
                              InputField("id", LongType),
                              InputField("name", StringType)
                            ))

implicit val myObjectInput: FromInput[MyObject] = new FromInput[MyObject] {
  override val marshaller: ResultMarshaller = CoercedScalaResultMarshaller.default

  override def fromResult(node: marshaller.Node): MyObject = {
    val m = node.asInstanceOf[Map[String, Any]]
    MyObject(m("id").asInstanceOf[Long], m("name").asInstanceOf[String])
  }
}

これでMyObjectを受け取れるようになったので、Mutationを実装する。

Queryと同様にMyObjectRepositoryObjectTypeCtx型パラメータとして渡しておき、Field.resolveで使えるようにしておく。

// mutation
lazy val myMutation: ObjectType[MyObjectRepository, Unit] = {
  ObjectType.apply(
    "MyMutation",
    fields[MyObjectRepository, Unit](
      {
        // my_objectにJSONでMyObjectのデータをもらって追加保存する
        val inputMyObject = Argument("my_object", myObjectInputType)
        Field(
          "store",
          arguments = inputMyObject :: Nil,
          fieldType = myObjectType,
          resolve = c => c.ctx.store(c arg inputMyObject)
        )
      }, {
        // nameだけ貰って新規作成する
        val inputName = Argument("name", StringType)
        Field(
          "create",
          arguments = inputName :: Nil,
          fieldType = myObjectType,
          resolve = c => c.ctx.create(c arg inputName)
        )
      }
    )
  )
}

ここまでで実装したQueryとMutationをGraphQLとして受け付けられるようにするにはSchemaを使う。

  // myQueryとmyMutationをGraphQLのSchemaとする
  lazy val schema: Schema[MyObjectRepository, Unit] = Schema(myQuery, Some(myMutation))
}

これでMyObjectに対してQueryとMutationを実行する準備が出来た。

Akka-HTTPでGraphQL APIを公開する

Akka-HTTPとspray-jsonを使ってGraphQLを実行するサンプルがこんな感じになる。

src/main/resources配下にgraphiql.htmlを配置してある。

// POST: /graphqlで受け付ける
val route: Route = (post & path("graphql")) {
  entity(as[JsValue]) { jsObject =>
    complete(this.execute(jsObject)(executionContext))
  } ~
  get {
    getFromResource("graphiql.html")
  }
}
val repository = new SchemaSample.MyObjectRepository

// Query or Mutationを受け取ってSchemaSample.schemaで実行する
def execute(jsValue: spray.json.JsValue)(implicit ec: ExecutionContext): Future[(StatusCode, JsValue)] = {
  val JsObject(fields) = jsValue
  val operation = fields.get("operationName") collect {
    case JsString(op) => op
  }

  val vars = fields.get("variables") match {
    case Some(obj: JsObject) => obj
    case _                   => JsObject.empty
  }

  // queryかmutationのどちらか
  val Some(JsString(document)) = fields.get("query") orElse fields.get("mutation")

  Future.fromTry(QueryParser.parse(document)) flatMap { queryDocument =>
    import StatusCodes._
    // 実装したSchemaとDBアクセスのためのRepositoryを渡して実行する
    Executor
      .execute(
        SchemaSample.schema, queryDocument, repository
        operationName = operation, variables = vars
      )
      .map { jsValue => OK -> jsValue }
      .recover {
        case error: QueryAnalysisError => BadRequest -> error.resolveError // リクエストが不正な場合
        case error: ErrorWithResolver  => InternalServerError -> error.resolveError // データを取得できなかった場合
      }
  }
}

これをよしなに起動して実行してQuery/Mutationを投げてみる。

実行結果サンプル

Queryを投げてみるには以下のようなリクエストを送る。

query MyQuery {
  all {
    ...MyObj
  }
  find_by_id(id: 2) {
    ...MyObj
  }
}

fragment MyObj on MyObject {
  id
  name
}

こういうfragmentのようなものが用意されていてGraphQLはよい。
結果はJSONで返ってきて、サンプルとしてはこんな感じになる。

{
  "data": {
    "all": [
      {
        "id": 1,
        "name": "alice"
      },
      {
        "id": 2,
        "name": "bob"
      },
    ],
    "find_by_id": {
      "id": 2,
      "name": "bob"
    }
  }
}

続いてMutation。

mutation MyMutation {
  store(my_object: {id: 3, name: "charlie"}) {
    id
  } 
  create(name: "dave") {
    id
  }
}

結果はこんな感じ。

{
  "data": {
    "store": {
      "id": 3
    },
    "create": {
      "id": 4
    }
  }
}

それぞれの操作に対する結果が返却されているのがわかる。

Query/Mutationをとりあえず動かすならこれくらいでおおよそ大丈夫なはず。

CtxとValについての補足

ドキュメント

ObjectTypeFieldについてくるCtxValという型パラメータ。

実装はObjectTypeFieldのあたり

ValはGraphQLで型として表現したいもの、たとえばUserとかTodoみたいな。
Field#resolveで返却するオブジェクトの型をValとして扱う。
QueryやMutationを宣言する際にはUnitでよい。

CtxはGraphQLのクエリを実行するために必要なcontextで、具体的にはデータベースへのアクセスが可能なserviceやrepositoryのようなオブジェクトが与えられる。
DBへのアクセスなしで型を表現できる場合にはCtxUnitでよい。
QueryやMutationを実行するには基本的には必要になるもの。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away