我当然赶不上时髦的火车,但是今天我们要谈论GraphQL ,这是一种非常有趣的构建REST(ful) Web服务和API的方法。 在我看来,重申REST(ful)体系结构是建立在相当合理的原则和约束之上的,这是很公平的(尽管对此的争论在业界从未间断)。
那么……什么是GraphQL ? 总的来说,它是另一种查询语言。 但是有趣的是,它旨在使客户(前端)可以根据期望的数据表达其需求(后端)。 坦白地说, GraphQL远远超出了它,但这是其最引人注目的功能之一。
GraphQL只是一个规范,对编程语言或技术堆栈没有任何特殊要求,但是毫不奇怪,它在现代Web开发中得到了广泛的接受,无论是在客户端( Apollo , Relay )还是服务器端(这些天,API通常称为BFF )。 在今天的帖子中,我们将带GraphQL一下,讨论它在哪里可能是一个合适的选择,以及为什么您可以考虑采用它。 虽然桌子上有相当多的选择, 桑格利亚 ,了不起斯卡拉基础的实施GraphQL规范,将是我们要建立顶上的基础。
本质上,我们的目标是开发一个简单的用户管理Web API。 数据模型远非完整,但足以满足其目的。 这是User类。
case class User(id: Long, email: String, created: Option[LocalDateTime],
firstName: Option[String], lastName: Option[String], roles: Option[Seq[Role.Value]],
active: Option[Boolean], address: Option[Address])
该角色是一个硬编码的枚举:
object Role extends Enumeration {
val USER, ADMIN = Value
}
地址是数据模型中的另一个小类:
case class Address(street: String, city: String, state: String,
zip: String, country: String)
GraphQL的核心是强类型化。 这意味着,应以某种方式在GraphQL中表示特定于应用程序的模型。 要自然地说,我们需要定义schema 。 在Sangria中 ,模式定义非常简单,由GraphQL规范中借鉴的三个主要类别组成: 对象类型 , 查询类型和变异类型 。 所有这些我们都将涉及,但是对象类型定义听起来像是一个逻辑起点。
val UserType = ObjectType(
"User",
"User Type",
interfaces[Unit, User](),
fields[Unit, User](
Field("id", LongType, Some("User id"), tags = ProjectionName("id") :: Nil,
resolve = _.value.id),
Field("email", StringType, Some("User email address"),
resolve = _.value.email),
Field("created", OptionType(LocalDateTimeType), Some("User creation date"),
resolve = _.value.created),
Field("address", OptionType(AddressType), Some("User address"),
resolve = _.value.address),
Field("active", OptionType(BooleanType), Some("User is active or not"),
resolve = _.value.active),
Field("firstName", OptionType(StringType), Some("User first name"),
resolve = _.value.firstName),
Field("lastName", OptionType(StringType), Some("User last name"),
resolve = _.value.lastName),
Field("roles", OptionType(ListType(RoleEnumType)), Some("User roles"),
resolve = _.value.roles)
))
在许多方面,它只是从User到UserType的直接映射。 Sangria通过提供宏支持可以使您轻松完成此工作,从而可以在编译时生成架构。 AddressType定义非常相似,让我们跳过它,看看如何处理枚举,例如Roles 。
val RoleEnumType = EnumType(
"Role",
Some("List of roles"),
List(
EnumValue("USER", value = Role.USER, description = Some("User")),
EnumValue("ADMIN", value = Role.ADMIN, description = Some("Administrator")
))
简单,简单和紧凑…在传统的REST(ful) Web服务中,关于资源的元数据通常不是开箱即用的。 但是,一些互补的规范(例如JSON Schema )可以通过一些工作来填补这一空白。
很好,这里有类型 ,但是这些查询和变异是什么? 查询是GraphQL规范中的一种特殊类型,它基本上描述了如何获取数据及其形状。 例如,经常需要通过其标识符来获取用户详细信息,这可以通过以下GraphQL查询来表示:
query {
user(id: 1) {
email
firstName
lastName
address {
country
}
}
}
你可以从字面上把它读作-是:用标识1查找用户 ,并只与国家只返回他的电子邮件 , 姓氏和名字和地址 。 令人敬畏的是,不仅GraphQL查询功能异常强大,而且它们还控制了返回相关方的内容。 如果您必须支持各种不同的客户端,而又不增加API端点的数量,那么这是无价的功能。 在桑格利亚汽酒中定义查询类型也很容易,例如:
val Query = ObjectType(
"Query", fields[Repository, Unit](
Field("user", OptionType(UserType), arguments = ID :: Nil,
resolve = (ctx) => ctx.ctx.findById(ctx.arg(ID))),
Field("users", ListType(UserType),
resolve = Projector((ctx, f) => ctx.ctx.findAll(f.map(_.name))))
))
上面的代码片段实际上描述了两个查询。 我们之前见过的一个,通过标识符获取用户,另一个,获取所有用户。 这是后者的一个简单示例:
query {
users {
id
email
}
}
希望您同意不需要任何解释,意图很明确。 查询可以说是支持采用GraphQL的最强论据,其价值主张确实是巨大的。 使用Sangria,您可以访问客户想要返回的字段,因此可以使用投影,选择或类似概念告知数据存储区仅返回这些子集。 为了更贴近现实,我们的示例应用程序将数据存储在MongoDB中,因此我们可以要求它仅返回客户端感兴趣的字段。
def findAll(fields: Seq[String]): Future[Seq[User]] = collection.flatMap(
_.find(document())
.projection(
fields
.foldLeft(document())((doc, field) => doc.merge(field -> BSONInteger(1)))
)
.cursor[User]()
.collect(Int.MaxValue, Cursor.FailOnError[Seq[User]]())
)
如果我们回到典型的REST(ful) Web API,那么当今最广泛用于概述所需响应形状的方法是传递查询字符串参数,例如/ api / users?fields = email,firstName,姓,… 。 但是,从实现的角度来看,没有多少框架本身支持这些功能,因此每个人都必须提出自己的方式。 关于查询功能,如果您碰巧是出色的Apache CXF框架的用户,那么您可能会从其功能强大的搜索扩展中受益,该扩展我们已经在前段时间进行了讨论。
如果查询通常只是获取数据,那么变异就是达到数据修改的目的。 从句法上讲,它们与查询非常相似,但它们的解释不同。 例如,这是我们可以向应用程序添加新用户的方法之一。
mutation {
add(email: "a@b.com", firstName: "John", lastName: "Smith", roles: [ADMIN]) {
id
active
email
firstName
lastName
address {
street
country
}
}
}
在此突变中,将向新用户John Smith发送电子邮件a@b.com,并为其分配ADMIN角色。 与查询一样 ,当变种完成时,客户端始终可以控制服务器需要从服务器获取哪些数据形状。 可以将突变视为对操作的呼吁,类似于很多方法的调用,例如,可以通过以下方式完成用户的激活:
mutation {
activate(id: 1) {
active
}
}
在桑格利亚汽酒中 , 突变的描述与查询完全相同,例如,我们之前研究的突变具有以下类型定义:
val Mutation = ObjectType(
"Mutation", fields[Repository, Unit](
Field("activate", OptionType(UserType),
arguments = ID :: Nil,
resolve = (ctx) => ctx.ctx.activateById(ctx.arg(ID))),
Field("add", OptionType(UserType),
arguments = EmailArg :: FirstNameArg :: LastNameArg :: RolesArg :: Nil,
resolve = (ctx) => ctx.ctx.addUser(ctx.arg(EmailArg), ctx.arg(FirstNameArg),
ctx.arg(LastNameArg), ctx.arg(RolesArg)))
))
这样,我们的GraphQL模式就完成了:
val UserSchema = Schema(Query, Some(Mutation))
很好,但是……我们能做些什么? 如有疑问,欢迎使用GraphQL服务器。 我们记得,没有特定技术或堆栈的附件,但是在Web API的整个领域中,您可以将GraphQL服务器视为绑定到POST HTTP动词的单个端点。 而且,一旦我们开始谈论HTTP和Scala ,他们比出色的Akka HTTP可以做的更好,幸运的是Sangria与它无缝集成。
val route: Route = path("users") {
post {
entity(as[String]) { document =>
QueryParser.parse(document) match {
case Success(queryAst) =>
complete(Executor.execute(SchemaDefinition.UserSchema, queryAst, repository)
.map(OK -> _)
.recover {
case error: QueryAnalysisError => BadRequest -> error.resolveError
case error: ErrorWithResolver => InternalServerError -> error.resolveError
})
case Failure(error) => complete(BadRequest -> Error(error.getMessage))
}
}
} ~ get {
complete(SchemaRenderer.renderSchema(SchemaDefinition.UserSchema))
}
}
您可能会注意到,我们还将模式也暴露在GET端点下,这是为了什么? 好吧,如果您对Swagger熟悉,我们在这里已经讨论了很多,那么这是一个非常相似的概念。 该架构包含所有必要的部分,足以使外部工具自动发现各自的GraphQL查询和变异以及它们所引用的类型。 GraphiQL是用于探究GraphQL的浏览器内置 IDE,它就是其中之一(想想REST(完整)服务世界中的Swagger UI )。
我们基本上就在那儿,我们的GraphQL服务器已经准备好了,让我们运行它并发送几个查询和变异,以获得它的感觉:
[info] Running com.example.graphql.Boot
INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
INFO reactivemongo.api.MongoDriver - No mongo-async-driver configuration found
INFO reactivemongo.api.MongoDriver - [Supervisor-1] Creating connection: Connection-2
INFO r.core.actors.MongoDBSystem - [Supervisor-1/Connection-2] Starting the MongoDBSystem akka://reactivemongo/user/Connection-2
我们的数据存储区(我们正在使用以Docker容器运行的MongoDB )很可能目前没有用户,因此,立即添加一个存储区听起来是个好主意:
$ curl -vi -X POST http://localhost:48080/users -H "Content-Type: application/json" -d " \
mutation { \
add(email: \"a@b.com\", firstName: \"John\", lastName: \"Smith\", roles: [ADMIN]) { \
id \
active \
email \
firstName \
lastName \
address { \
street \
country \
} \
} \
}"
HTTP/1.1 200 OK
Server: akka-http/10.0.5
Date: Tue, 25 Apr 2017 01:01:25 GMT
Content-Type: application/json
Content-Length: 123
{
"data":{
"add":{
"email":"a@b.com",
"lastName":"Smith",
"firstName":"John",
"id":1493082085000,
"address":null,
"active":false
}
}
}
它似乎工作得很好。 无论您正在运行哪种查询或变异 ,响应细节都将始终包裹在数据信封中,例如:
$ curl -vi -X POST http://localhost:48080/users -H "Content-Type: application/json" -d " \
query { \
users { \
id \
email \
} \
}"
HTTP/1.1 200 OK
Server: akka-http/10.0.5
Date: Tue, 25 Apr 2017 01:09:21 GMT
Content-Type: application/json
Content-Length: 98
{
"data":{
"users":[
{
"id":1493082085000,
"email":"a@b.com"
}
]
}
}
完全按照我们的命令…老实说,使用GraphQL感觉很自然,特别是在涉及数据查询时。 而且我们甚至没有谈论片段 , 变量 , 指令和许多其他内容。
现在是一个问题:我们是否应该放弃所有实践,即JAX-RS , Spring MVC …并改用GraphQL ? 坦率地说,事实并非如此, GraphQL非常适合解决某些类型的问题,但是总的来说,与Swagger或任何其他已建立的API规范框架结合使用的传统REST(ful) Web服务大都是可以保留的。
并请注意, GraphQL及其优点是有代价的。 例如, HTTP缓存和缓存控制将不再适用, HATEOAS也没有多大意义,无论您叫什么,都具有统一的响应,可靠性是所有事物都在单个外观的基础上……鉴于此, GraphQL确实很棒工具,请明智地使用它!
完整的项目源可在Github上获得 。