You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Dataloader时出现Cannot return null for non-nullable字段错误

问题:使用Dataloader后GraphQL查询返回非空字段为null错误

问题背景

已创建Episode、Character和Director模型,编写了对应的types、queries及mutations,未使用Dataloader前一切正常,返回QuerySet时运行正常。但尝试用Dataloader提升性能,在resolver中返回Promise对象后,执行指定GraphQL查询出现错误:Cannot return null for non-nullable field EpisodeTypeConnection.edges.

文件夹结构

models.py

import graphene
from graphene import ObjectType, relay
from graphene_django import DjangoObjectType

from general.models import Character, Director, Episode


class CharacterType(DjangoObjectType):
    pk = graphene.Int(source="pk", required=True)

    class Meta:
        model = Character
        filter_fields = {
            "name": ["exact", "icontains", "istartswith"],
            "character_species": ["exact", "icontains"],
            "location": ["exact"],
            "status": ["exact"],
        }
        fields = "__all__"
        interfaces = (relay.Node,)


class EpisodeType(DjangoObjectType):
    pk = graphene.Int(source="pk")

    class Meta:
        model = Episode
        filter_fields = {
            "name": ["exact", "icontains", "istartswith"],
            "directed_by__name": ["exact", "icontains"],
            "aired_date": ["exact"],
        }
        fields = "__all__"
        interfaces = (relay.Node,)


class DirectorType(DjangoObjectType):
    pk = graphene.Int(source="pk", required=True)

    class Meta:
        model = Director
        filter_fields = {
            "name": ["exact", "icontains", "istartswith"],
            "first_directed_episode__name": ["exact", "icontains"],
            "last_directed_episode__name": ["exact", "icontains"],
            "age": ["exact"],
        }
        fields = "__all__"
        interfaces = (relay.Node,)

types.py

import graphene
from graphene import ObjectType, relay
from graphene_django import DjangoObjectType

from general.models import Character, Director, Episode


class CharacterType(DjangoObjectType):
    pk = graphene.Int(source="pk", required=True)

    class Meta:
        model = Character
        filter_fields = {
            "name": ["exact", "icontains", "istartswith"],
            "character_species": ["exact", "icontains"],
            "location": ["exact"],
            "status": ["exact"],
        }
        fields = "__all__"
        interfaces = (relay.Node,)


class EpisodeType(DjangoObjectType):
    pk = graphene.Int(source="pk")

    class Meta:
        model = Episode
        filter_fields = {
            "name": ["exact", "icontains", "istartswith"],
            "directed_by__name": ["exact", "icontains"],
            "aired_date": ["exact"],
        }
        fields = "__all__"
        interfaces = (relay.Node,)


class DirectorType(DjangoObjectType):
    pk = graphene.Int(source="pk", required=True)

    class Meta:
        model = Director
        filter_fields = {
            "name": ["exact", "icontains", "istartswith"],
            "first_directed_episode__name": ["exact", "icontains"],
            "last_directed_episode__name": ["exact", "icontains"],
            "age": ["exact"],
        }
        fields = "__all__"
        interfaces = (relay.Node,)

loaders.py

from collections import defaultdict
from promise import Promise
from promise.dataloader import DataLoader
from general.models import Episode


class EpisodesByDirectorIdLoader(DataLoader):
    def batch_load_fn(self, director_ids):
        episodes_by_director_ids = defaultdict(list)
        all_episodes_of_directors = Episode.objects.filter(
            directed_by__pk__in=director_ids
        )
        for episode in all_episodes_of_directors.iterator():
            episodes_by_director_ids[episode.directed_by.pk].append(episode)

        return Promise.resolve(
            [
                episodes_by_director_ids.get(director_id, [])
                for director_id in director_ids
            ]
        )

queries.py

import graphene
from graphene_django.filter import DjangoFilterConnectionField

from general.models import Character, Director, Episode

from .types import EpisodeType


class Query(graphene.ObjectType):
    all_episodes = DjangoFilterConnectionField(EpisodeType)

    @staticmethod
    def resolve_all_episodes(root, info):
        # return Episode.objects.all() # RETURNS QUERYSET
        return info.context.episodes_by_director_id_loader.load(1) # RETURNS PROMISE OBJECT

执行的GraphQL查询

query{
  allEpisodes{
    edges{
      node{
        name
      }
    }
  }
}

错误输出

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field EpisodeTypeConnection.edges.",
      "locations": [
        {
          "line": 26,
          "column": 5
        }
      ],
      "path": [
        "allEpisodes",
        "edges"
      ]
    }
  ],
  "data": {
    "allEpisodes": null
  }
}

问题原因及解决方法

核心原因

你用DjangoFilterConnectionField定义的all_episodes字段,期望返回Connection类型(包含edges、pageInfo等结构),但resolver返回的是Dataloader加载的Episode对象列表,类型不匹配导致GraphQL无法解析成Connection结构,最终返回null触发非空字段错误。

另外,Dataloader的核心作用是解决N+1查询问题,比如批量加载关联数据,而非直接替换顶层QuerySet的返回。

解决步骤

1. 修正Dataloader的使用场景(推荐用法)

如果目标是优化关联查询(比如查询Director时加载其执导的Episodes),应在DirectorType中添加自定义字段,用Dataloader批量加载:

修改types.py中的DirectorType:

class DirectorType(DjangoObjectType):
    pk = graphene.Int(source="pk", required=True)
    # 添加自定义字段,返回该导演的所有剧集
    directed_episodes = graphene.List(EpisodeType)

    class Meta:
        model = Director
        filter_fields = {
            "name": ["exact", "icontains", "istartswith"],
            "first_directed_episode__name": ["exact", "icontains"],
            "last_directed_episode__name": ["exact", "icontains"],
            "age": ["exact"],
        }
        fields = "__all__"
        interfaces = (relay.Node,)
    
    # 为directed_episodes字段添加resolver,使用Dataloader
    def resolve_directed_episodes(self, info):
        return info.context.episodes_by_director_id_loader.load(self.pk)

恢复queries.py中的resolve_all_episodes为原QuerySet返回:

@staticmethod
def resolve_all_episodes(root, info):
    return Episode.objects.all()

这样查询Director及其执导的剧集时,Dataloader会批量加载所有关联Episodes,避免N+1查询,同时保持顶层查询的Connection结构正常工作。

2. 确认Dataloader已注册到context

确保已在GraphQL视图中将Dataloader实例添加到context,比如自定义视图:

from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
from .loaders import EpisodesByDirectorIdLoader

class CustomGraphQLView(GraphQLView):
    def get_context(self, request):
        context = super().get_context(request)
        # 注册Dataloader到context
        context.episodes_by_director_id_loader = EpisodesByDirectorIdLoader()
        return context

在urls.py中使用该自定义视图:

urlpatterns = [
    path("graphql/", csrf_exempt(CustomGraphQLView.as_view(graphiql=True))),
]

内容的提问来源于stack exchange,提问作者HarilalAV

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 04:30:57