When you have a one-to-many relation in a model, so either a reverse lookup of a foreign key or a many to many field, the optimizer will prefetch it (good π). If the field in GraphQL however is declared as not returning a list or connection but just a single type, then the default resolver will use QuerySet#first
/ QuerySet#get
to get the result, which will bypass the prefetched objects and issue new queries.
Example:
class Project(models.Model):
name = models.CharField(max_length=255)
class Issue(models.Model)
project = models.ForeignKey(Project, related_name='issues')
@strawberry_django.type(Issue, name='Issue')
class IssueType:
id: int
@strawberry_django.type(Project, name='Project')
class ProjectType:
id: int
name: str
latest_issue: IssueType | None = strawberry_django.field(field_name='issues')
@strawberry.type
class Query:
projects: ListConnectionWithTotalCount[ProjectType] = strawberry_django.connection()
This example seems a bit contrived, however it should show the bug. When querying as follows:
query {
projects {
name latestIssue { id }
}
There will be a query for projects (π), a prefetch-query for issues (π) and then another query for each project fetching the first issue (π).
A fix could look like this, implemented as a custom StrawberryDjangoField
subclass here:
class StrawberryDjangoFieldWithFixedPrefetchInSomeCases(StrawberryDjangoField):
def get_queryset_hook(self, info: Info, **kwargs):
if self.is_connection or self.is_paginated or self.is_list:
return super().get_queryset_hook(info, **kwargs)
elif self.is_optional:
def qs_hook(qs: QuerySet): # type: ignore
qs = self.get_queryset(qs, info, **kwargs)
if is_optimized_by_prefetching(qs):
return next(iter(qs), None)
else:
return qs.first()
return qs_hook
else:
def qs_hook(qs: QuerySet): # type: ignore
qs = self.get_queryset(qs, info, **kwargs)
if is_optimized_by_prefetching(qs):
try:
return next(iter(qs))
except StopIteration:
raise qs.model.DoesNotExist(
"%s matching query does not exist." % qs.model._meta.object_name
)
else:
return qs.get()
return qs_hook
I will submit a proper pull request if there are no objections.
Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too