使用Dataloader时出现Cannot return null for non-nullable字段错误
问题背景
已创建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

