前言
首先,本文是为那些已经熟悉GraphQL并更多地了解使用它的复杂性和细微差别的读者设计的。 但是,我希望它对初学者有用。
GraphQL是一个很棒的工具。 我认为许多人已经知道并了解其优势。 但是,在构建基于GraphQL的API时,需要注意一些细微差别。
例如,GraphQL允许您返回使用者(用户或程序),仅请求该使用者感兴趣的那部分数据。 但是,在构建服务器时,很容易出错,这导致一个事实,即在服务器内部(可以是分布式的),数据将以完整的“捆绑包”运行。 这主要是由于以下事实:即装即用的GraphQL本身不提供用于解析传入查询的便捷工具,并且其中放置的那些接口没有得到很好的文档说明。
问题根源
让我们看一个非最佳实现的典型示例(如果阅读不佳,请在单独的窗口中打开图片):

假设我们的消费者是“电话簿”的某个应用程序或组件,它仅从我们的API询问我们存储的用户的标识符,名称和电话号码。 同时,我们的API更加广泛,它将允许访问其他数据,例如住所的实际地址和用户的电子邮件地址。
在使用者与API之间进行数据交换时,GraphQL可以完美完成我们所需的所有工作-仅响应于请求发送请求的数据。 在这种情况下,问题在于从数据库中采样数据-即 在服务器的内部实现中,事实是,尽管我们不需要其中的一些数据,但对于每个传入请求,我们都会从数据库中选择所有用户数据。 这会在数据库上产生过多的负载,并导致系统内过多流量的流通。 通过大量查询,可以通过更改数据采样方法并仅选择那些请求的字段来获得重大优化。 同时,我们的数据源是什么都无关紧要-关系数据库,NoSQL技术或其他服务(内部或外部)。 任何非最佳行为都可能受到任何实现的影响。 在这种情况下,仅以MySQL为例。
解决方案
如果我们分析resolve()
函数的参数,则可以优化此服务器行为:
async resolve(source, args, context, info) {
在这种情况下,这是我们特别感兴趣的最后一个参数info
。 我们转向文档,并详细分析resolve()
函数和我们感兴趣的参数包括什么:
type GraphQLFieldResolveFn = ( source?: any, args?: {[argName: string]: any}, context?: any, info?: GraphQLResolveInfo ) => any type GraphQLResolveInfo = { fieldName: string, fieldNodes: Array<Field>, returnType: GraphQLOutputType, parentType: GraphQLCompositeType, schema: GraphQLSchema, fragments: { [fragmentName: string]: FragmentDefinition }, rootValue: any, operation: OperationDefinition, variableValues: { [variableName: string]: any }, }
因此,传递给解析器的前三个参数是source
-从架构的GraphQL树中的父节点传递的数据,args-请求参数(来自查询)和context
-开发人员定义的执行上下文对象,通常被称为传输某些全局数据在“解析器”中。 最后,第四个参数是有关请求的元信息。
我们可以从GraphQLResolveInfo
提取什么来解决我们的问题?
它最有趣的部分是:
fieldName
是其GraphQL模式的当前字段名称。 即 它对应于此解析器的架构中指定的字段名称。 如果像上面的示例一样,我们在users
字段上捕获了info
对象,则将“ users”作为fieldName
的值包含在其中fieldNodes
查询中请求的节点的集合(数组)。 正是所需要的!fragments
-请求fragments
的集合(如果请求被片段化)。 也是检索最终数据字段的重要信息。
因此,作为一种解决方案,我们需要解析info
工具并选择查询中出现的字段列表,然后将其传递给SQL查询。 不幸的是,Facebook的GraphQL软件包“开箱即用”并没有给我们提供任何简化此任务的方法。 总体而言,如实践所示,鉴于请求可以分散的事实,该任务并非那么琐碎。 此外,这种分析具有通用的解决方案,随后可以将其简单地从一个项目复制到另一个项目。
因此,我决定根据ISC许可将其编写为开放源代码库。 在它的帮助下,解析传入查询字段的解决方案非常简单地解决了,例如,在我们的示例中,如下所示:
const { fieldsList } = require('graphql-fields-list');
在这种情况下, fieldsList(info)
为我们完成了所有工作,并为此解析器返回了一个“扁平”子字段数组,即 我们最终的SQL查询将如下所示:
SELECT id, name, phone FROM users;
如果我们将传入请求更改为:
query UserListQuery { users { id name phone email } }
那么SQL查询将变成:
SELECT id, name, phone, email FROM users;
但是,并非总是有可能应对这种简单的挑战。 通常,实际应用程序的结构要复杂得多。 在某些实现中,我们可能需要相对于最终GraphQL模式中的数据在更高级别上描述解析器。 例如,如果我们决定使用Relay库,我们想使用一种现成的机制将数据对象的集合分解为页面,这导致了GraphQL模式将根据某些规则构建的事实。 例如,我们以这种方式(TypeScript)重做架构:
import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; import { connectionDefinitions, connectionArgs, nodeDefinitions, fromGlobalId, globalIdField, connectionFromArray, GraphQLResolveInfo, } from 'graphql-relay'; import { fieldsList } from 'graphql-fields-list'; export const { nodeInterface, nodeField } = nodeDefinitions(async (globalId: string) => { const { type, id } = fromGlobalId(globalId); let node: any = null; if (type === 'User') { node = await database.select(`SELECT id FROM user WHERE id="${id}"`); } return node; }); const User = new GraphQLObjectType({ name: 'User', interfaces: [nodeInterface], fields: { id: globalIdField('User', (user: any) => user.id), name: { type: GraphQLString }, email: { type: GraphQLString }, phone: { type: GraphQLString }, address: { type: GraphQLString }, } }); export const { connectionType: userConnection } = connectionDefinitions({ nodeType: User }); const Query = new GraphQLObjectType({ name: 'Query', fields: { node: nodeField, users: { type: userConnection, args: { ...connectionArgs }, async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) {
在这种情况下,来自Relay的connectionDefinition
会将edges
, node
, pageInfo
和cursor
节点添加到方案中,即 现在,我们将需要以不同的方式重建查询(我们现在不再关注分页):
query UserListQuery { users { edges { node { id name phone email } } } }
因此, resolve()
users
节点上实现resolve()
功能现在将必须确定请求的字段不是针对自身,而是针对其嵌套的子节点node
,正如我们所看到的,该子节点相对于edges.node
路径上的users
edges.node
。
fieldsList
graphql-fields-list
库中的graphql-fields-list
将有助于解决此问题,为此,您应将相应的path
选项传递给它。 例如,这是我们的示例实现:
async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node' }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); }
同样在现实世界中,可能是在GraphQL模式中我们仅定义了一个字段名称,而在数据库模式中我们定义了其他字段名称。 例如,假设数据库中的用户表定义不同:
CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, fullName VARCHAR(255), email VARCHAR(255), phoneNumber VARCHAR(15), address VARCHAR(255) );
在这种情况下,应将GraphQL查询中的字段重命名,然后将其嵌入到SQL查询中。 如果您在相应的transform
选项中为其传递名称转换图,则fieldsList
将对此有所帮助:
async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node', transform: { phone: 'phoneNumber', name: 'fullName' }, }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); }
但是,有时仅转换为平面字段是不够的(例如,如果数据源返回带有嵌套的复杂结构)。 在这种情况下,来自graphql-fields-list
库的fieldsMap
函数将进行救援,该函数将请求的字段的整个树作为对象返回:
const { fieldsMap } = require(`graphql-fields-list`);
如果我们假设用户是由一个复杂的结构描述的,那么我们将获得全部信息。 此方法还可以采用可选的path
参数,该参数允许您从整个树中获取仅必要分支的映射,例如:
const { fieldsMap } = require(`graphql-fields-list`);
卡上名称的转换目前尚不支持,并且仍由开发人员负责。
请求碎片
GraphQL支持查询分段,例如,我们可以期望使用者向我们发送这样的请求(这里我们指的是原始模式,有点牵强,但在语法上是正确的):
query UsersFragmentedQuery { users { id ...NamesFramgment ...ContactsFragment } } fragment NamesFragment on User { name } fragment AddressFragment on User { address } fragment ContactsFragment on User { phone email ...AddressFragment }
在这种情况下,您不必担心,在这种情况下, fieldsList(info)
和fieldsMap(info)
将返回预期的结果,因为它们考虑了将请求分段的可能性。 因此, fieldsList(info)
将分别返回['id', 'name', 'phone', 'email', 'address']
和fieldsMap(info)
分别返回:
{ id: false, name: false, phone: false, email: false, address: false }
聚苯乙烯
我希望本文有助于阐明在服务器上使用GraphQL的一些细微差别,并且graphql-fields-list库可以在将来帮助您创建最佳解决方案。
UPD 1
该库的版本1.1.0已发布-已添加对请求中@skip
和@include
支持。 默认情况下,该选项处于启用状态,如有必要,请按以下方式禁用它:
fieldsList(info, { withDirectives: false }) fieldsMap(info, null, false);