为什么GraphQL查询返回null? [英] Why does a GraphQL query return null?

查看:271
本文介绍了为什么GraphQL查询返回null?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

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

为什么GraphQL不返回数据?

解决方案

您的一个或多个字段解析为null有两个常见原因:1)返回解析器中错误形状的数据;和2)没有正确使用Promises.

注意:如果您遇到以下错误:

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

潜在的问题是您的字段返回null.您仍然可以按照以下概述的步骤尝试解决此错误.

以下示例将引用此简单模式:

type Query {
  post(id: ID): Post
  posts: [Post]
}

type Post {
  id: ID
  title: String
  body: String
}

返回错误形状的数据

我们的架构与请求的查询一起定义了端点返回的响应中data对象的形状".形状是指对象具有的属性,以及这些属性的值是标量值,其他对象还是对象或标量的数组.

以相同的方式,架构定义了总响应的形状,单个字段的 type 定义了该字段值的形状.同样,我们在解析器中返回的数据的形状也必须与此预期的形状匹配.如果不是这样,我们经常会在响应中得到意外的空值.

在进入具体示例之前,了解GraphQL如何解析字段很重要.

了解默认的解析程序行为

虽然您肯定可以为架构中的每个字段编写解析器,但通常没有必要,因为GraphQL.js在不提供默认解析器时会使用默认解析器.

从高层次上讲,默认解析器的作用很简单:它查看解析为 parent 字段的值,如果该值是JavaScript对象,它将在该Object上查找属性. 相同的名称作为要解析的字段.如果找到该属性,它将解析为该属性的值.否则,它将解析为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.如果字段具有对象类型,但是在其解析程序中返回的对象不是对象(例如String或Array),则不会看到有关类型不匹配的任何错误,但该字段的子字段将不可避免地解析为null./p>

常见场景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,因此我们的解析器应返回具有idtitlebody之类的属性的对象.如果这是我们的API返回的结果,那么我们都准备就绪. 但是,通常将响应实际上是包含其他元数据的对象.因此,我们实际上从端点返回的对象可能看起来像这样:

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

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

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对象,该对象包含如下属性rowsfieldsrowCountcommand.您需要先从此响应中提取适当的数据,然后再将其返回到解析器中.

常见方案2:以数组代替对象

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

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

其中,Post是我们通过上下文注入的某些模型.如果使用的是sequelize,则可以调用findAll. mongoosetypeorm具有find.这些方法的共同之处在于,尽管它们允许我们指定WHERE条件,但它们返回的承诺仍然解析为数组而不是单个对象.虽然数据库中可能只有一个帖子具有特定ID,但是当您调用这些方法之一时,它仍被包装在数组中.因为数组仍然是对象,所以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 } })
}

关于INSERTUPDATE调用的特殊说明:我们通常希望插入或更新行或模型实例的方法返回插入或更新的行.他们经常这样做,但是有些方法却没有.例如,sequelizeupsert方法解析为布尔值,或者是加完后的记录和布尔值的元组(如果returning选项设置为true). mongoosefindOneAndUpdate解析为具有value属性的对象,其中包含修改后的行.在将其返回到解析器内部之前,请查阅您的ORM文档并适当地解析结果.

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

在我们的模式中,posts字段的类型是PostList,这意味着其解析器需要返回对象数组(或解析为一个的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,我们将在响应中看到一个错误,例如:

预期可迭代,但未在Query.posts字段中找到一个.

与上述两种情况不同,在这种情况下,GraphQL能够显式检查我们在解析器中返回的值的类型,如果不是承诺是对象表示异步操作的最终完成(或失败)及其结果值."接下来的几种情况概述了在解析器内部处理Promises时遇到的一些常见陷阱.但是,如果您不熟悉Promises和较新的async/await语法,则强烈建议您花一些时间来阅读基础知识.

注意:接下来的几个示例引用了getPost函数.该函数的实现细节并不重要-它只是一个返回Promise的函数,该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:未正确链接承诺

您决定将呼叫结果记录到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上.可以通过使用多个then这样将多个Promise链接在一起.链中的每个Promise都是按顺序解决的,最终值就是有效地解析为原始Promise的值.

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

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

如果您有多个Promises,则需要在解析器内部进行解析,您必须使用then并返回正确的值来正确地链接它们.例如,如果需要在调用getPost之前调用其他两个异步函数(getFoogetBar),则可以执行以下操作:

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

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

常见方案#6

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

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

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

大多数流行的库,包括mongoose支持开箱即用.那些不经常使用免费的包装"库来添加此功能的库. 使用GraphQL解析器时,应避免使用利用回调的方法,而应使用返回Promises的方法.

专业提示:同时支持回调和Promises的库经常重载其函数,使得如果不提供回调,该函数将返回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)
      }
    })
  })

I have an graphql/apollo-server/graphql-yoga endpoint. This endpoint exposes data returned from a database (or a REST endpoint or some other service).

I know my data source is returning the correct data -- if I log the result of the call to the data source inside my resolver, I can see the data being returned. However, my GraphQL field(s) always resolve to null.

If I make the field non-null, I see the following error inside the errors array in the response:

Cannot return null for non-nullable field

Why is GraphQL not returning the data?

解决方案

There's two common reasons your field or fields are resolving to null: 1) returning data in the wrong shape inside your resolver; and 2) not using Promises correctly.

Note: if you're seeing the following error:

Cannot return null for non-nullable field

the underlying issue is that your field is returning null. You can still follow the steps outlined below to try to resolve this error.

The following examples will refer to this simple schema:

type Query {
  post(id: ID): Post
  posts: [Post]
}

type Post {
  id: ID
  title: String
  body: String
}

Returning data in the wrong shape

Our schema, along with the requested query, defines the "shape" of the data object in the response returned by our endpoint. By shape, we mean what properties objects have, and whether those properties' values' are scalar values, other objects, or arrays of objects or scalars.

In the same way a schema defines the shape of the total response, the type of an individual field defines the shape of that field's value. The shape of the data we return in our resolver must likewise match this expected shape. When it doesn't, we frequently end up with unexpected nulls in our response.

Before we dive into specific examples, though, it's important to grasp how GraphQL resolves fields.

Understanding default resolver behavior

While you certainly can write a resolver for every field in your schema, it's often not necessary because GraphQL.js uses a default resolver when you don't provide one.

At a high level, what the default resolver does is simple: it looks at the value the parent field resolved to and if that value is a JavaScript object, it looks for a property on that Object with the same name as the field being resolved. If it finds that property, it resolves to the value of that property. Otherwise, it resolves to null.

Let's say in our resolver for the post field, we return the value { title: 'My First Post', bod: 'Hello World!' }. If we don't write resolvers for any of the fields on the Post type, we can still request the post:

query {
  post {
    id
    title
    body
  }
}

and our response will be

{
  "data": {
    "post" {
      "id": null,
      "title": "My First Post",
      "body": null,
    }
  }
}

The title field was resolved even though we didn't provide a resolver for it because the default resolver did the heavy lifting -- it saw there was a property named title on the Object the parent field (in this case post) resolved to and so it just resolved to that property's value. The id field resolved to null because the object we returned in our post resolver did not have an id property. The body field also resolved to null because of a typo -- we have a property called bod instead of body!

Pro tip: If bod is not a typo but what an API or database actually returns, we can always write a resolver for the body field to match our schema. For example: (parent) => parent.bod

One important thing to keep in mind is that in JavaScript, almost everything is an Object. So if the post field resolves to a String or a Number, the default resolver for each of the fields on the Post type will still try to find an appropriately named property on the parent object, inevitably fail and return null. If a field has an object type but you return something other than object in its resolver (like a String or an Array), you will not see any error about the type mismatch but the child fields for that field will inevitably resolve to null.

Common Scenario #1: Wrapped Responses

If we're writing the resolver for the post query, we might fetch our code from some other endpoint, like this:

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

The post field has the type Post, so our resolver should return an object with properties like id, title and body. If this is what our API returns, we're all set. However, it's common for the response to actually be an object which contains additional metadata. So the object we actually get back from the endpoint might look something like this:

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

In this case, we can't just return the response as-is and expect the default resolver to work correctly, since the object we're returning doesn't have the id , title and body properties we need. Our resolver isn't needs to do something like:

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

Note: The above example fetches data from another endpoint; however, this sort of wrapped response is also very common when using a database driver directly (as opposed to using an ORM)! For example, if you're using node-postgres, you'll get a Result object that includes properties like rows, fields, rowCount and command. You'll need to extract the appropriate data from this response before returning it inside your resolver.

Common Scenario #2: Array Instead of Object

What if we fetch a post from the database, our resolver might look something like this:

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

where Post is some model we're injecting through the context. If we're using sequelize, we might call findAll. mongoose and typeorm have find. What these methods have in common is that while they allow us to specify a WHERE condition, the Promises they return still resolve to an array instead of a single object. While there's probably only one post in your database with a particular ID, it's still wrapped in an array when you call one of these methods. Because an Array is still an Object, GraphQL will not resolve the post field as null. But it will resolve all of the child fields as null because it won't be able to find the appropriately named properties on the array.

You can easily fix this scenario by just grabbing the first item in the array and returning that in your resolver:

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

If you're fetching data from another API, this is frequently the only option. On the other hand, if you're using an ORM, there's often a different method that you can use (like findOne) that will explicitly return only a single row from the DB (or null if it doesn't exist).

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

A special note on INSERT and UPDATE calls: We often expect methods that insert or update a row or model instance to return the inserted or updated row. Often they do, but some methods don't. For example, sequelize's upsert method resolves to a boolean, or tuple of the the upserted record and a boolean (if the returning option is set to true). mongoose's findOneAndUpdate resolves to an object with a value property that contains the modified row. Consult your ORM's documentation and parse the result appropriately before returning it inside your resolver.

Common Scenario #3: Object Instead of Array

In our schema, the posts field's type is a List of Posts, which means its resolver needs to return an Array of objects (or a Promise that resolves to one). We might fetch the posts like this:

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

However, the actual response from our API might be an object that wraps the the array of posts:

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

We can't return this object in our resolver because GraphQL is expecting an Array. If we do, the field will resolve to null and we'll see an error included in our response like:

Expected Iterable, but did not find one for field Query.posts.

Unlike the two scenarios above, in this case GraphQL is able to explicitly check the type of the value we return in our resolver and will throw if it's not an Iterable like an Array.

Like we discussed in the first scenario, in order to fix this error, we have to transform the response into the appropriate shape, for example:

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

Not Using Promises Correctly

GraphQL.js makes use of the Promise API under the hood. As such, a resolver can return some value (like { id: 1, title: 'Hello!' }) or it can return a Promise that will resolve to that value. For fields that have a List type, you may also return an array of Promises. If a Promise rejects, that field will return null and the appropriate error will be added to the errors array in the response. If a field has an Object type, the value the Promise resolves to is what will be passed down as the parent value to the resolvers of any child fields.

A Promise is an "object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value." The next few scenarios outline some common pitfalls encountered when dealing with Promises inside resolvers. However, if you're not familiar with Promises and the newer async/await syntax, it's highly recommended you spend some time reading up on the fundamentals.

Note: the next few examples refer to a getPost function. The implementation details of this function are not important -- it's just a function that returns a Promise, which will resolve to a post object.

Common Scenario #4: Not Returning a Value

A working resolver for the post field might looks like this:

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

getPosts returns a Promise and we're returning that Promise. Whatever that Promise resolves to will become the value our field resolves to. Looking good!

But what happens if we do this:

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

We're still creating a Promise that will resolve to a post. However, we're not returning the Promise, so GraphQL is not aware of it and it will not wait for it to resolve. In JavaScript functions without an explicit return statement implicitly return undefined. So our function creates a Promise and then immediately returns undefined, causing GraphQL to return null for the field.

If the Promise returned by getPost rejects, we won't see any error listed in our response either -- because we didn't return the Promise, the underlying code doesn't care about whether it resolves or rejects. In fact, if the Promise rejects, you'll see an UnhandledPromiseRejectionWarning in your server console.

Fixing this issue is simple -- just add the return.

Common Scenario #5: Not chaining Promises correctly

You decide to log the result of your call to getPost, so you change your resolver to look something like this:

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

When you run your query, you see the result logged in your console, but GraphQL resolves the field to null. Why?

When we call then on a Promise, we're effectively taking the value the Promise resolved to and returning a new Promise. You can think of it kind of like Array.map except for Promises. then can return a value, or another Promise. In either case, what's returned inside of then is "chained" onto the original Promise. Multiple Promises can be chained together like this by using multiple thens. Each Promise in the chain is resolved in sequence, and the final value is what's effectively resolved as the value of the original Promise.

In our example above, we returned nothing inside of the then, so the Promise resolved to undefined, which GraphQL converted to a null. To fix this, we have to return the posts:

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

If you have multiple Promises you need to resolve inside your resolver, you have to chain them correctly by using then and returning the correct value. For example, if we need to call two other asynchronous functions (getFoo and getBar) before we can call getPost, we can do:

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

Pro tip: If you're struggling with correctly chaining Promises, you may find async/await syntax to be cleaner and easier to work with.

Common Scenario #6

Before Promises, the standard way to handle asynchronous code was to use callbacks, or functions that would be called once the asynchronous work was completed. We might, for example, call mongoose's findOne method like this:

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

The problem here is two-fold. One, a value that's returned inside a callback isn't used for anything (i.e. it's not passed to the underlying code in any way). Two, when we use a callback, Post.findOne doesn't return a Promise; it just returns undefined. In this example, our callback will be called, and if we log the value of post we'll see whatever was returned from the database. However, because we didn't use a Promise, GraphQL doesn't wait for this callback to complete -- it takes the return value (undefined) and uses that.

Most more popular libraries, including mongoose support Promises out of the box. Those that don't frequently have complimentary "wrapper" libraries that add this functionality. When working with GraphQL resolvers, you should avoid using methods that utilize a callback, and instead use ones that return Promises.

Pro tip: Libraries that support both callbacks and Promises frequently overload their functions in such a way that if a callback is not provided, the function will return a Promise. Check the library's documentation for details.

If you absolutely have to use a callback, you can also wrap the callback in a 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)
      }
    })
  })

这篇关于为什么GraphQL查询返回null?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆