この記事はなに?
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]()
続いて、all
でMyObjectRepository.findAll
、find_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
が必要なので、ObjectType
のCtx
型パラメータは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と同様にMyObjectRepository
をObjectType
のCtx
型パラメータとして渡しておき、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についての補足
ObjectType
やField
についてくるCtx
とVal
という型パラメータ。
実装はObjectTypeとFieldのあたり
Val
はGraphQLで型として表現したいもの、たとえばUser
とかTodo
みたいな。
Field#resolve
で返却するオブジェクトの型をVal
として扱う。
QueryやMutationを宣言する際にはUnit
でよい。
Ctx
はGraphQLのクエリを実行するために必要なcontextで、具体的にはデータベースへのアクセスが可能なserviceやrepositoryのようなオブジェクトが与えられる。
DBへのアクセスなしで型を表現できる場合にはCtx
はUnit
でよい。
QueryやMutationを実行するには基本的には必要になるもの。