使用Scala的Sangria库来实现GraphQL API
这篇文章是关于什么的?
引入了sangria/sangria来实现在Scala中创建GraphQL服务器。
阅读官方文档和学习Sangria是最快的方法。此外,由于官方还提供了示例,所以参考示例会更好。
不过,为了方便简单查阅,我会附上简短的样本清单。
用法示例
直到能够发出查询/变异。
直到能够向某个独特的对象发送查询和变更请求为止。
源代码已放置在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中的所有对象,并使用find_by_id(id)方法查询MyObjectRepository中指定id的对象。
// 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指定して取得する
}
)
)
}
为了执行查询,需要MyObjectRepository,因此ObjectType的Ctx类型参数需要提供MyObjectRepository。
Field的第二个参数表示返回的数据类型,在arguments参数中,传入用于执行此查询的输入,通过resolve参数提供使用实际的MyObjectRepository来访问数据的函数。
既经实现了查询功能,接下来是突变。尝试实现一个存储获取MyObject信息并保存的函数store,并只接收名称name,其余属性由函数create生成。
因为需要能够接收MyObject作为输入来为store做准备,所以可以从这一点开始。
// 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执行查询和变更操作了。
使用Akka-HTTP公开GraphQL API。
使用Akka-HTTP和spray-json执行GraphQL的示例将如下所示。
将graphiql.html放置在src/main/resources下。
// 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 MyQuery {
all {
...MyObj
}
find_by_id(id: 2) {
...MyObj
}
}
fragment MyObj on MyObject {
id
name
}
这样的片段已经准备好了,GraphQL很好。结果以JSON的形式返回,作为示例就是这样的样子。
{
"data": {
"all": [
{
"id": 1,
"name": "alice"
},
{
"id": 2,
"name": "bob"
},
],
"find_by_id": {
"id": 2,
"name": "bob"
}
}
}
接下来是变异。
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查询所需的上下文,具体而言,它提供了可以访问数据库的service或repository等对象。
如果可以表示类型而无需访问数据库,则Ctx可以为Unit。
基本上,执行Query或Mutation时都会需要它。