本文属使用Prisma构建GraphQL服务系列。
每个GraphQL API的核心:GraphQL schema
schema中的类型定义了API操作
每个GraphQL API的核心是GraphQL schema,它清楚地定义了所有可用的API操作和数据类型。schema使用称为模式定义语言(Schema Define Language,SDL)的专用语法编写。 SDL简洁易用。
下面是一个演示如何定义简单的User
类型的例子,该User
有两个字段,即id
和name
。
type User {
id: ID!
name: String!
}
每个GraphQL schema都有三种特殊的根类型,称为查询(Query
),突变(Mutation
)和订阅(Subscription
)。这些根类型的字段定义了API接受的操作。
作为示例,请考虑以下Query和Mutation类型:
type Query {
users: [User!]!
}
type Mutation {
createUser(name: String!): User!
}
此schema定义的GraphQL API将允许执行以下两项操作:
# 查询用户列表
query {
users {
id
name
}
}
# 创建新用户
mutation {
createUser(name: "Sarah") {
id
}
}
查询(Query
) / 突变(Mutation
)内的所有字段及其参数的集合称为操作的选择集(selection set)。
根字段定义API的入口点(entry-points)
根类型上的字段也称为根字段(root fields),并为API提供入口点(entry-points)。这意味着发送到API的查询(Query
) / 突变(Mutation
)总是需要以其中一个根字段开始。
根字段的类型(type
)确定哪些字段可以进一步包含在查询的选择集中。在上面的例子中,类型是User
和[User!]!
在这两种情况下都允许包含User
类型的任何字段。
如果根字段具有标量(scalar)类型,则不可能在选择集中包含任何其他字段。作为示例,请考虑以下GraphQL schema:
type Query {
hello: String!
}
此查询定义的GraphQL API只接受一个hello
操作:
query {
hello
}
解析器函数(Resolver functions)实现schema
GraphQL服务中的结构与行为
GraphQL具有明确分离的结构(structure)和行为(behaviour)。虽然SDL schema定义仅描述了API的抽象结构(abstract structure),但具体实现是通过所谓的解析器功能(Resolver functions)实现的。schema定义和解析器(resolver)实现的组合通常被称为可执行schema(executable schema)。
GraphQL schema中的每个字段都由一个解析器函数支持,这意味着解析器函数与GraphQL schema中的字段一样多(包括除根类型之外的其他类型的字段)。
他为某个领域的解析器功能负责为该领域准确提取数据。例如,上面的用户root字段的解析器知道如何获取用户列表。
字段的解析器函数负责为该字段准确提取数据。例如,上面的user
根字段的解析器知道如何获取用户列表。
因此,GraphQL查询解析的过程其实是调用查询中包含的字段的解析器函数的操作,因为每个解析器都会为其字段返回数据。
解析器函数(Resolver functions)剖析
解析器函数总是需要四个参数(按以下顺序):
-
parent
(有时也称为root) :查询由GraphQL引擎解析,该引擎调用查询中包含的字段的解析器。因为查询可以包含嵌套字段,所以可能会有多级解析器执行。parent
参数始终表示前一个解析器调用的返回值。请参阅此处了解更多信息。 -
args
:为该字段提供的潜在参数(例如,上面的createUser突变示例中的name
)。 -
context
:上下文,每个解析器都可以写入和读取的解析器链(基本上是解析器交流和共享信息的方法)的对象。 -
info
:查询或变异的AST表示。您可以在这篇文章中了解更多信息:Demystifying the info Argument in GraphQL Resolvers.
以下是我们如何实现上述模式定义的解析器的一种可能方式(实现假定有一些全局对象数据库提供了与数据库的接口):
const Query = {
users: (parent, args, context, info) => {
return db.users()
}
}
const Mutation = {
createUser: (parent, args, context, info) => {
return db.createUser(args.name)
}
}
const User = {
id: (parent, args, context, info) => parent.id,
name: (parent, args, context, info) => parent.name,
}
上面的示例schema定义恰好具有四个字段。此解析器实现提供了四个相应的解析器功能。请注意,User
类型的解析器实际上可以省略,因为它们的实现是微不足道的,并且由GraphQL执行引擎推断。
Prisma如何帮助开发GraphQL服务
构建GraphQL服务的难点在于实现解析器(resolvers)
如上所见,实现GraphQL服务器的主要开发任务围绕着定义schema并实现相应的解析器(resolver)功能。这也被称为schema驱动开发(schema-driven development,SDD)。
当实现解析器功能时,您需要选择某种数据源,为查询响应部分时获取的数据。这个数据源可以是任何东西 - 它可能是一个数据库(SQL或NoSQL),一个REST API,一些第三方服务或任何类型的遗留系统。
上面的例子很简单,并假设全局数据库对象的可用性为一些数据源提供了一个简单的接口。实际上,您可能会遇到更复杂的场景。特别是因为GraphQL查询可以深度嵌套,将它们转换为SQL(或其他数据库API)非常麻烦并且容易出错。
Prisma让解析器变得简单明了
使用Prisma时,一般认为解析器只是将传入查询的执行委托给底层的Prisma引擎,而不是直接访问数据库。因此,大多数解析器实现将是简单的单线程。将传入查询解释为数据库API的工作由Prisma完成。
使用GraphQL bindings搞定这个事情,允许通过调用JavaScript中的专用函数(或任何其他用于后端开发的编程语言)与Prisma GraphQL API交互。如此,解析器实现变得与上面的模拟db
示例一样简单。
GraphQL bindings - 更好的 ORM
绑定允许通过调用编程语言中的函数来发送查询和突变
GraphQL bindings,在某种程度上与传统的ORM有点像。他们都允许通过调用编程语言中的函数来与GraphQL API通讯,而不是构建直接发送给API的原始查询字符串。
像上面的createUser
突变。无论何时您想将其发送到GraphQL API,您都需要像这样拼出整个突变:
mutation {
createUser(name: "Sarah") {
id
}
}
然后你将这个字符串放入HTTP POST请求的body
并将它发送到API,这样的缺点是查询被表示为字符串。这抹杀了GraphQL的核心优势之一:强大的类型系统!基于字符串的方法完全没有发挥强类型API的优势。
GraphQL bindings通过允许您调用专用函数来向API发送查询和突变,而不是通过手动构建字符串并通过HTTP将其发送到服务端。这些函数以您的GraphQL schema的根字段命名。
通过绑定,上面的createUser
突变可以通过调用相同名称的函数发送到服务端:
binding.mutation.createUser({ name: "Sarah" }, '{ id }')
同样,你可以将上面的users
查询转换为函数调用:
binding.query.users({}, '{ id name }')
如你所见,这些函数调用中的第一个参数是一个携带查询(query)/突变(mutation)参数(args)的对象,第二个参数是决定哪些数据应该包含在响应中的选择集(selection set)。
调用这些方法时,引擎中的binding
实例负责将操作转换为GraphQL查询,并将查询发送到服务端,并将响应作为编程语言中的对象返回。
静态与动态绑定(Static vs Dynamic Bindings)
绑定可以使用两种风格:静态(Static)和动态(Dynamic)。
静态绑定(Static bindings )用于与来自静态和强类型编程语言(如TypeScript或Scala)的GraphQL API进行交互时,在编译时需要知道所有表达式的类型。在这种情况下,绑定函数在构建时生成(使用代码生成)。因此,这些绑定函数的所有调用都可以通过编译器进行验证,并且拼写错误以及结构错误(如错误类型的传递参数)在编译时被捕获。
另一个优势是您的编辑器现在可以帮助您创建API请求,例如自动完成可用的操作和查询参数!这样就改变了后端开发游戏的玩法,并将开发者体验提升到一个新的水平。没有SQL字符串或其他脆弱的数据库API - 幸亏有了Prisma绑定使你可以用强类型层与数据库进行交互!
动态绑定(Dynamic bindings )通常用于动态编程语言(如JavaScript)。它们不需要额外的构建步骤(静态绑定就是如此)。绑定实例上的方法调用仅在运行时转换为GraphQL查询。这仍然提供了使用简洁和简单的绑定语法的主要好处。使用适当的构建工具仍然可以实现构建时错误检查和自动完成等优势。
架构入门:两个GraphQL API层
在使用Prisma构建GraphQL服务器时,您需要处理两个GraphQL API:
- database layer:由Prisma负责的数据库层
- application layer:应用层负责与写入或读取数据库数据无关的任何功能(如业务逻辑,身份验证和权限,第三方集成等)。
数据库层完全通过prisma.yml
进行配置,并使用Prisma CLI进行管理。应用层是您正在用自己喜欢的编程语言实现的GraphQL服务。