27
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

この記事はなに?

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を実行するには基本的には必要になるもの。

27
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?