我有一个graphql / apollo-server / graphql-yoga端点。该终结点公开了从数据库(或REST终结点或其他服务)返回的数据。

我知道我的数据源正在返回正确的数据-如果我记录对数据的调用结果解析器中的源,我可以看到正在返回的数据。但是,我的GraphQL字段始终解析为null。

如果将字段设置为非null,则在响应中在errors数组内看到以下错误:


无法为不可为空的字段返回null


为什么GraphQL不返回数据?

评论

注意:该问题旨在作为参考问题,也是类似问题的潜在重复目标。这就是为什么问题很广泛,而忽略了任何特定的代码或架构细节的原因。有关其他详细信息,请参见此meta帖子。

我认为您应该更改标题,因为它仍然很难通过“无法为不可为空的字段返回null”甚至“ [graphql]无法为不可为空的字段返回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在不提供默认解析器时会使用默认解析器。 >
从高层看,默认解析器的作用很简单:它查看解析为父字段的值,如果该值是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字段已解析,尽管我们没有为其提供解析器,因为默认解析器完成了繁重的工作-它看到对象上有一个名为title的属性父字段(在本例中为post)解析为,因此它只是解析为该属性的值。由于我们在id解析器中返回的对象没有post属性,因此id字段解析为null。由于输入错误,body字段也解析为空-我们有一个称为bod的属性,而不是body!实际返回时,我们总是可以为bod字段编写一个解析器以匹配我们的模式。例如:body


要记住的重要一件事是,在JavaScript中,几乎所有东西都是对象。因此,如果(parent) => parent.bod字段解析为字符串或数字,则post类型上每个字段的默认解析器仍将尝试在父对象上找到适当命名的属性,不可避免地失败并返回null。如果某个字段具有对象类型,但是在其解析程序中返回的对象不是对象(例如String或Array),则不会看到有关类型不匹配的任何错误,但该字段的子字段将不可避免地解析为null。 br />
常见方案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,因此我们的解析器应返回一个对象,其属性如Postidtitle。如果这是我们的API返回的结果,那么我们都准备就绪。但是,通常将响应实际上是包含其他元数据的对象。因此,我们实际上从端点返回的对象可能看起来像这样:

{
  "status": 200,
  "result": {
    "id": 1,
    "title": "My First Post",
    "body": "Hello world!"
  },
}


在这种情况下,我们不能只按原样返回响应并期望默认解析器可以正常工作,因为我们返回的对象没有我们需要的bodyidtitle属性。我们的解析器不需要执行以下操作:

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,则将得到一个body对象,该对象包含诸如ResultrowsfieldsrowCount之类的属性。您需要先从此响应中提取适当的数据,然后再将其返回到解析器中。

常见场景2:数组而不是对象

如果我们获取帖子怎么办从数据库中,我们的解析器可能看起来像这样:

function post(root, args, context) {
  return context.Post.find({ where: { id: args.id } })
}


其中command是我们通过上下文注入的某种模型。如果我们使用Post,我们可以调用sequelizefindAllmongoose具有typeorm。这些方法的共同之处在于,尽管它们允许我们指定find条件,但它们返回的Promise仍然解析为数组而不是单个对象。虽然数据库中可能只有一个帖子具有特定ID,但是当您调用这些方法之一时,它仍包裹在数组中。因为数组仍然是对象,所以GraphQL不会将WHERE字段解析为null。但这会将所有子字段都解析为null,因为它将无法在数组上找到适当命名的属性。

您只需在表格中抓取第一项即可轻松解决此情况。数组并在解析器中返回该数组:

function post(root, args, context) {
  return context.Post.find({ where: { id: args.id } })
    .then(posts => posts[0])
}


如果要从另一个API提取数据,通常这是唯一的选择。另一方面,如果您使用的是ORM,则通常可以使用其他方法(例如post),该方法将显式地仅从数据库返回一行(如果不存在,则返回null)。关于findOneINSERT调用的特殊说明:我们通常希望插入或更新行或模型实例的方法返回插入或更新的行。他们经常这样做,但是有些方法却没有。例如,UPDATEsequelize方法解析为布尔值,或者是上载记录和布尔值的元组(如果upsert选项设置为true)。 returningmongoose解析为具有findOneAndUpdate属性的对象,其中包含修改后的行。在将结果返回到解析器内部之前,请查阅您的ORM文档并对其进行适当的解析。

常见情况#3:对象而不是数组

在我们的架构中,value字段的类型是postsList,这意味着其解析器需要返回一个对象数组(或解析为一个的Promise)。我们可能会这样获取帖子:

function post(root, args, context) {
  return context.Post.findOne({ where: { id: args.id } })
}


但是,我们API的实际响应可能是包装了帖子数组的对象:

function posts (root, args) {
  return fetch('http://SOME_URL/posts')
    .then(res => res.json())
}


因为GraphQL需要一个数组,所以我们无法在解析器中返回此对象。如果这样做,该字段将解析为null,并且我们将在响应中看到一个错误,例如:


Expected Iterable,但在Query.posts字段中找不到一个。


与上述两种情况不同,在这种情况下,GraphQL能够显式检查我们在解析器中返回的值的类型,如果它不是像数组那样的Iterable,则会抛出该错误。 />
就像我们在第一种情况中讨论的那样,为了解决此错误,我们必须将响应转换为适当的形状,例如:

{
  "count": 10,
  "next": "http://SOME_URL/posts/?page=2",
  "previous": null,
  "results": [
    {
      "id": 1,
      "title": "My First Post",
      "body" "Hello World!"
    },
    ...
  ]
}


没有正确使用Promise

GraphQL.js在后台使用Promise API。这样,解析程序可以返回某个值(例如Post),也可以返回将解析为该值的Promise。对于具有{ id: 1, title: 'Hello!' }类型的字段,您还可以返回一个Promises数组。如果Promise拒绝,则该字段将返回null,并且相应的错误将添加到响应中的List数组。如果字段具有对象类型,则Promise解析为的值将作为父值向下传递给任何子字段的解析器。

承诺是“对象表示异步操作的最终完成(或失败)及其结果值”。接下来的几种情况概述了在解析器内部处理Promises时遇到的一些常见陷阱。但是,如果您不熟悉Promises和较新的async / await语法,则强烈建议您花一些时间阅读基础知识。

注意:下面的几个示例涉及errors函数。此函数的实现细节并不重要-只是返回Promise的函数,该Promise将解析为post对象。

常见情况#4:不返回值

getPost字段的有效解析器可能看起来像这样:无论Promise决定解决什么,它将成为我们领域解决的价值。看起来不错!

但是如果执行此操作会发生什么情况:

function posts (root, args) {
  return fetch('http://SOME_URL/posts')
    .then(res => res.json())
    .then(data => data.results)
}


我们仍在创建一个Promise,它将解决一个帖子。但是,我们没有返回Promise,因此GraphQL并不知道它,也不会等待它解决。在没有显式post语句的JavaScript函数中,隐式返回getPosts。因此,我们的函数创建了一个Promise,然后立即返回return,从而导致GraphQL为该字段返回null。

如果undefined返回的Promise拒绝,我们也不会在响应中看到任何错误- -因为我们没有返回Promise,所以底层代码不关心它是解决还是拒绝。实际上,如果Promise拒绝了,您将在服务器控制台中看到一个undefined

解决此问题很简单-只需添加getPost即可。

常见方案# 5:未正确链接Promises

您决定将调用结果记录到 UnhandledPromiseRejectionWarning,因此将解析器更改为如下所示:

function post(root, args) {
  return getPost(args.id)
}


运行查询时,您会在控制台中看到记录的结果,但是GraphQL将该字段解析为null。为什么?

当我们在Promise上调用return时,我们实际上是在兑现Promise所解决的价值并返回一个新的Promise。您可以想到它类似于getPost,但Promises除外。 then可以返回一个值或另一个Promise。无论哪种情况,Array.map内部返回的内容都“链接”到原始Promise上。通过使用多个then,可以像这样将多个Promise链接在一起。链中的每个Promise都是按顺序解析的,最终值是作为原始Promise的值有效解析的结果。

在上面的示例中,我们在then内部未返回任何内容,因此Promise解析为then,该GraphQL转换为null。要解决此问题,我们必须退回这些帖子:

function post(root, args) {
  getPost(args.id)
}


如果您有多个承诺需要在解析器中解析,则必须使用then正确链接它们并返回正确的值。例如,如果需要在调用undefined之前调用另外两个异步函数(thengetFoo),则可以执行以下操作:

function post(root, args) {
  return getPost(args.id)
    .then(post => {
      console.log(post)
    })
}



专业提示:如果您在正确地链接Promises方面遇到困难,则可能会发现async / await语法更简洁,更易于使用。


常见情况#6

在承诺之前,处理异步代码的标准方法是使用回调或异步工作完成后将被调用的函数。例如,我们可以这样调用getBargetPost方法:

function post(root, args) {
  return getPost(args.id)
    .then(post => {
      console.log(post)
      return post // <----
    })
}


这里的问题有两个。一种是,在回调内部返回的值不会用于任何事情(即不会以任何方式传递给基础代码)。第二,当我们使用回调时,mongoose不会返回Promise;它只是返回未定义。在此示例中,我们的回调将被调用,如果我们记录findOne的值,我们将看到从数据库返回的所有内容。但是,因为我们没有使用Promise,所以GraphQL不会等待此回调完成-它使用返回值(未定义)并使用该返回值。

大多数最受欢迎的库,包括Post.findOne支持开箱即用。那些不经常具有添加此功能的免费“包装”库的库。使用GraphQL解析器时,应避免使用利用回调的方法,而应使用返回Promises的方法。


专业提示:同时支持回调和Promises的库经常重载其功能。这样,如果不提供回调,该函数将返回Promise。请查看库的文档以获取详细信息。


如果绝对必须使用回调,也可以将回调包装在Promise中:

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
    })


#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(stackoverflow.com/questions/58140891/…)上的一个问题被标记为该问题的重复。

– shima岛敦宏
2月1日8:18

#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
        };
      }));
  }