petitviolet blog

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

    2018-01-26

    QiitaScalaGraphQLsangria

    この記事はなに?

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

    from: https://qiita.com/petitviolet/items/e3e87c3f3e740b3c57ba