为啥 GraphQL 查询返回 null?
Posted
技术标签:
【中文标题】为啥 GraphQL 查询返回 null?【英文标题】:Why does a GraphQL query return null?为什么 GraphQL 查询返回 null? 【发布时间】:2020-04-22 15:26:35 【问题描述】:我有一个graphql
/apollo-server
/graphql-yoga
端点。此端点公开从数据库(或 REST 端点或其他服务)返回的数据。
我知道我的数据源正在返回正确的数据——如果我在解析器中记录对数据源的调用结果,我可以看到正在返回的数据。但是,我的 GraphQL 字段总是解析为 null。
如果我将该字段设为非空,我会在响应中的 errors
数组中看到以下错误:
不能为不可为空的字段返回 null
为什么 GraphQL 不返回数据?
【问题讨论】:
注意:此问题旨在作为参考问题和类似问题的潜在欺骗目标。这就是为什么这个问题很广泛并且省略了任何特定的代码或架构细节。有关更多详细信息,请参阅this meta post。 我认为您应该更改标题,因为这仍然不容易通过“无法为不可空字段返回 null”甚至“[graphql] 无法为不可为空字段返回 null”... . “不能为不可为空的字段返回 null - 为什么它返回 null?” ? 【参考方案1】:您的一个或多个字段解析为 null 的常见原因有两个:1) 在解析器中以错误的形状返回数据;和 2) 没有正确使用 Promises。
注意:如果您看到以下错误:
不能为不可为空的字段返回 null
根本问题是您的字段返回 null。您仍然可以按照下面列出的步骤尝试解决此错误。
以下示例将引用这个简单的架构:
type Query
post(id: ID): Post
posts: [Post]
type Post
id: ID
title: String
body: String
以错误的形状返回数据
我们的架构和请求的查询一起定义了端点返回的响应中data
对象的“形状”。所谓形状,是指对象具有哪些属性,以及这些属性的“值”是标量值、其他对象还是对象数组或标量。
与架构定义总响应的形状相同,单个字段的类型定义该字段值的形状。我们在解析器中返回的数据的形状也必须与这个预期的形状相匹配。如果不是这样,我们的响应中经常会出现意外的空值。
不过,在我们深入研究具体示例之前,了解 GraphQL 如何解析字段非常重要。
了解默认解析器行为
虽然您当然可以为架构中的每个字段编写解析器,但通常没有必要,因为 GraphQL.js 在您不提供解析器时使用默认解析器。
在高层次上,默认解析器所做的很简单:它查看 parent 字段解析为的值,如果该值是 javascript 对象,它会查找该对象的属性与正在解析的字段同名。如果它找到该属性,则解析为该属性的值。否则,它会解析为 null。
假设在post
字段的解析器中,我们返回值 title: 'My First Post', bod: 'Hello World!'
。如果我们不为Post
类型的任何字段编写解析器,我们仍然可以请求post
:
query
post
id
title
body
我们的回应是
"data":
"post"
"id": null,
"title": "My First Post",
"body": null,
title
字段已解析,即使我们没有为其提供解析器,因为默认解析器完成了繁重的工作 - 它看到父字段的 Object 上有一个名为 title
的属性(在此case post
) 解析为,因此它只是解析为该属性的值。 id
字段解析为 null,因为我们在 post
解析器中返回的对象没有 id
属性。由于拼写错误,body
字段也解析为 null —— 我们有一个名为 bod
的属性,而不是 body
!
专业提示:如果
bod
不是错字而是 API 或数据库实际返回的内容,我们总是可以为body
字段编写解析器以匹配我们的架构。例如:(parent) => parent.bod
要记住的重要一点是,在 JavaScript 中,几乎所有东西都是对象。因此,如果post
字段解析为字符串或数字,Post
类型上每个字段的默认解析器仍将尝试在父对象上找到适当命名的属性,不可避免地会失败并返回 null。如果一个字段具有对象类型,但您在其解析器中返回的不是对象(如字符串或数组),您将不会看到任何关于类型不匹配的错误,但该字段的子字段将不可避免地解析为 null。
常见场景 #1:包装响应
如果我们正在为 post
查询编写解析器,我们可能会从其他端点获取我们的代码,如下所示:
function post (root, args)
// axios
return axios.get(`http://SOME_URL/posts/$args.id`)
.then(res => res.data);
// fetch
return fetch(`http://SOME_URL/posts/$args.id`)
.then(res => res.json());
// request-promise-native
return request(
uri: `http://SOME_URL/posts/$args.id`,
json: true
);
post
字段的类型为Post
,因此我们的解析器应该返回一个具有id
、title
和body
等属性的对象。如果这是我们的 API 返回的内容,我们就准备好了。 然而,响应实际上是一个包含附加元数据的对象是很常见的。所以我们实际上从端点返回的对象可能看起来像这样:
"status": 200,
"result":
"id": 1,
"title": "My First Post",
"body": "Hello world!"
,
在这种情况下,我们不能按原样返回响应并期望默认解析器正常工作,因为我们返回的对象没有 id
、 title
和 body
我们需要的属性。我们的解析器不需要执行以下操作:
function post (root, args)
// axios
return axios.get(`http://SOME_URL/posts/$args.id`)
.then(res => res.data.result);
// fetch
return fetch(`http://SOME_URL/posts/$args.id`)
.then(res => res.json())
.then(data => data.result);
// request-promise-native
return request(
uri: `http://SOME_URL/posts/$args.id`,
json: true
)
.then(res => res.result);
注意:上面的例子从另一个端点获取数据;但是,当直接使用数据库驱动程序(而不是使用 ORM)时,这种包装响应也很常见!例如,如果您使用node-postgres,您将获得一个Result
对象,其中包含rows
、fields
、rowCount
和command
等属性。您需要先从该响应中提取适当的数据,然后再将其返回到您的解析器中。
常见场景#2:数组代替对象
如果我们从数据库中获取帖子,我们的解析器可能看起来像这样:
function post(root, args, context)
return context.Post.find( where: id: args.id )
Post
是我们通过上下文注入的一些模型。如果我们使用sequelize
,我们可能会调用findAll
。 mongoose
和 typeorm
有 find
。这些方法的共同点是,虽然它们允许我们指定 WHERE
条件,但它们返回的 Promise仍然解析为数组而不是单个对象。虽然您的数据库中可能只有一个具有特定 ID 的帖子,但当您调用其中一种方法时,它仍然包装在一个数组中。因为 Array 仍然是 Object,GraphQL 不会将 post
字段解析为 null。但它将将所有子字段解析为 null,因为它无法在数组上找到适当命名的属性。
您只需抓住数组中的第一项并将其返回到解析器中即可轻松解决此问题:
function post(root, args, context)
return context.Post.find( where: id: args.id )
.then(posts => posts[0])
如果您从另一个 API 获取数据,这通常是唯一的选择。另一方面,如果您使用的是 ORM,则通常可以使用另一种方法(例如 findOne
),它将显式地从数据库中仅返回一行(如果不存在,则返回 null)。
function post(root, args, context)
return context.Post.findOne( where: id: args.id )
关于INSERT
和UPDATE
调用的特别说明:我们通常期望插入或更新行或模型实例的方法返回插入或更新的行。通常他们会这样做,但有些方法不会。例如,sequelize
的 upsert
方法解析为布尔值,或者被插入的记录和一个布尔值的元组(如果 returning
选项设置为 true)。 mongoose
的 findOneAndUpdate
解析为具有 value
属性的对象,该属性包含修改后的行。在将结果返回到解析器之前,请查阅您的 ORM 文档并适当地解析结果。
常见场景#3:对象而不是数组
在我们的架构中,posts
字段的类型是 List
的 Post
s,这意味着它的解析器需要返回一个对象数组(或解析为一个的 Promise)。我们可能会获取这样的帖子:
function posts (root, args)
return fetch('http://SOME_URL/posts')
.then(res => res.json())
但是,我们 API 的实际响应可能是一个包装帖子数组的对象:
"count": 10,
"next": "http://SOME_URL/posts/?page=2",
"previous": null,
"results": [
"id": 1,
"title": "My First Post",
"body" "Hello World!"
,
...
]
我们无法在解析器中返回此对象,因为 GraphQL 需要一个数组。如果我们这样做,该字段将解析为 null,并且我们会在响应中看到一个错误,例如:
预期为 Iterable,但没有为字段 Query.posts 找到一个。
与上述两种情况不同,在这种情况下,GraphQL 能够显式检查我们在解析器中返回的值的类型,如果它不是像数组那样的 Iterable,则会抛出。
就像我们在第一个场景中讨论的那样,为了修复这个错误,我们必须将响应转换为适当的形状,例如:
function posts (root, args)
return fetch('http://SOME_URL/posts')
.then(res => res.json())
.then(data => data.results)
没有正确使用 Promise
GraphQL.js 在底层使用了 Promise API。因此,解析器可以返回一些值(如 id: 1, title: 'Hello!'
),或者它可以返回一个将 解析 到该值的 Promise。对于具有 List
类型的字段,您还可以返回一个 Promises 数组。如果 Promise 拒绝,该字段将返回 null 并且相应的错误将添加到响应中的 errors
数组中。如果字段具有 Object 类型,则 Promise 解析为的值将作为 父值 传递给任何子字段的解析器。
Promise 是一个“对象表示异步操作的最终完成(或失败)及其结果值。”接下来的几个场景概述了在解析器中处理 Promise 时遇到的一些常见陷阱。但是,如果您不熟悉 Promises 和较新的 async/await 语法,强烈建议您花一些时间阅读基础知识。
注意:接下来的几个示例涉及getPost
函数。这个函数的实现细节并不重要——它只是一个返回 Promise 的函数,它将解析为一个 post 对象。
常见场景 #4:不返回值
post
字段的工作解析器可能如下所示:
function post(root, args)
return getPost(args.id)
getPosts
返回一个 Promise,我们正在返回该 Promise。无论 Promise 解析为什么,都将成为我们的字段解析为的值。看起来不错!
但是如果我们这样做会发生什么:
function post(root, args)
getPost(args.id)
我们仍在创建一个将解析为帖子的 Promise。但是,我们没有返回 Promise,因此 GraphQL 不知道它并且不会等待它解决。在没有显式 return
语句的 JavaScript 函数中,隐式返回 undefined
。所以我们的函数创建了一个 Promise,然后立即返回 undefined
,导致 GraphQL 为该字段返回 null。
如果getPost
返回的 Promise 被拒绝,我们也不会在响应中看到任何错误——因为我们没有返回 Promise,底层代码并不关心它是解析还是拒绝。事实上,如果 Promise 被拒绝,您会在服务器控制台中看到
UnhandledPromiseRejectionWarning
。
解决这个问题很简单——只需添加return
。
常见场景 #5:未正确链接 Promise
您决定将调用结果记录到getPost
,因此您将解析器更改为如下所示:
function post(root, args)
return getPost(args.id)
.then(post =>
console.log(post)
)
当您运行查询时,您会看到结果记录在控制台中,但 GraphQL 将该字段解析为 null。为什么?
当我们在 Promise 上调用 then
时,我们实际上是在获取 Promise 解析为的值并返回一个新的 Promise。你可以把它想象成Array.map
,除了 Promises。 then
可以返回一个值,或者另一个 Promise。在任何一种情况下,then
内部返回的内容都会“链接”到原始 Promise 上。多个 Promise 可以像这样通过使用多个 then
s 链接在一起。链中的每一个 Promise 都是按顺序解析的,最终的值就是被有效解析为原始 Promise 的值。
在上面的示例中,我们在 then
内没有返回任何内容,因此 Promise 解析为 undefined
,GraphQL 将其转换为 null。为了解决这个问题,我们必须返回帖子:
function post(root, args)
return getPost(args.id)
.then(post =>
console.log(post)
return post // <----
)
如果您需要在解析器中解析多个 Promise,则必须使用 then
正确链接它们并返回正确的值。例如,如果我们需要在调用getPost
之前调用另外两个异步函数(getFoo
和getBar
),我们可以这样做:
function post(root, args)
return getFoo()
.then(foo =>
// Do something with foo
return getBar() // return next Promise in the chain
)
.then(bar =>
// Do something with bar
return getPost(args.id) // return next Promise in the chain
)
专业提示:如果您在正确链接 Promise 方面遇到困难,您可能会发现 async/await 语法更简洁且更易于使用。
常见场景#6
在 Promise 之前,处理异步代码的标准方法是使用回调,或在异步工作完成后调用的函数。例如,我们可能会像这样调用mongoose
的findOne
方法:
function post(root, args)
return Post.findOne( where: id: args.id , function (err, post)
return post
)
这里的问题有两个。第一,在回调中返回的值不会用于任何事情(即它不会以任何方式传递给底层代码)。二、当我们使用回调时,Post.findOne
不会返回 Promise;它只是返回未定义。在这个例子中,我们的回调将被调用,如果我们记录post
的值,我们将看到从数据库返回的任何内容。然而,因为我们没有使用 Promise,GraphQL 不会等待这个回调完成——它接受返回值(未定义)并使用它。
最流行的库,包括 mongoose
支持开箱即用的 Promises。那些不经常拥有添加此功能的免费“包装器”库。 使用 GraphQL 解析器时,应避免使用利用回调的方法,而应使用返回 Promises 的方法。
专业提示:同时支持回调和 Promise 的库经常以这样的方式重载其函数,即如果未提供回调,该函数将返回 Promise。有关详细信息,请查看库的文档。
如果你必须使用回调,你也可以将回调包装在 Promise 中:
function post(root, args)
return new Promise((resolve, reject) =>
Post.findOne( where: id: args.id , function (err, post)
if (err)
reject(err)
else
resolve(post)
)
)
【讨论】:
【参考方案2】:我在 Nest.js 上遇到了同样的问题。
如果您想解决问题。您可以将 nullable: true 选项添加到您的 @Query 装饰器。
这是一个例子。
@Resolver(of => Team)
export class TeamResolver
constructor(
private readonly teamService: TeamService,
private readonly memberService: MemberService,
)
@Query(returns => Team, name: 'team', nullable: true )
@UseGuards(GqlAuthGuard)
async get(@Args('id') id: string)
return this.teamService.findOne(id);
然后,您可以返回空对象进行查询。
【讨论】:
我在这里发布了这个答案,因为这个 URL (***.com/questions/58140891/…) 上的一个问题被标记为这个问题的重复。【参考方案3】:如果以上都没有帮助,并且您有一个全局拦截器,例如在“数据”字段中包含所有响应,则您必须为 graphql 禁用此功能,其他明智的 graphql 解析器转换为 null。
这就是我在我的案例中对拦截器所做的:
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>>
if (context['contextType'] === 'graphql') return next.handle();
return next
.handle()
.pipe(map(data =>
return
data: isObject(data) ? this.transformResponse(data) : data
;
));
【讨论】:
【参考方案4】:如果有人使用apollo-server-express
并获得空值。
const typeDefs = require('./schema');
const resolvers = require('./resolver');
const server = new ApolloServer(typeDefs,resolvers);
这将返回您期望的值。
const withDifferentVarNameSchema = require('./schema');
const withDifferentVarNameResolver= require('./resolver');
const server = new ApolloServer(withDifferentVarNameSchema,withDifferentVarNameResolver);
这将返回 null 而不是预期的。
注意:在创建 Apolloserver 实例时,仅传递 typeDefs 和 resolvers var name。
【讨论】:
【参考方案5】:我在查询嵌套对象时遇到此错误,但我的解析器没有展开该对象。它并没有像我预期的那样自动发生。一旦我强制它展开,错误就消失了
【讨论】:
【参考方案6】:来自 Flutter 这里。 我找不到任何与颤振相关的解决方案,所以由于我的搜索总是把我带到这里,所以我把它添加到这里。
确切的错误是:
对 AppSync 执行同步查询失败: [GraphQLResponse.Errormessage='不能为非空返回空 类型:父级中的“AWSTimestamp”
所以,在我的架构中(在 AppSync 控制台上)我有这个:
type TypeName
id: ID!
...
_version: Int!
_deleted: Boolean
_lastChangedAt: AWSTimestamp!
createdAt: AWSDateTime!
updatedAt: AWSDateTime!
我从_lastChangedAt
字段中得到错误,因为AWSTimestamp
不能为空。
我只需要remove the null-check (!) from the field
就解决了。
现在,从长远来看,我不知道这会产生什么影响,但如有必要,我会更新这个答案。
编辑:正如我发现的那样,这意味着我所做的任何事情,amplify.push
改变是相反的。只需返回您的 appsync 控制台并在测试时再次更改它。所以这不是一个可持续的解决方案,但我在网上听到的喋喋不休表明很快就会有改进来放大颤振。
【讨论】:
以上是关于为啥 GraphQL 查询返回 null?的主要内容,如果未能解决你的问题,请参考以下文章