Skip to content

Commit e0f1814

Browse files
committed
Add test validating query performance with select_related + prefetch_related
This test passes after reverting the `CustomField` resolver change introduced in #1315, but fails with that resolver code present. For instance, adding back the resolver code gives a test failure showing: ``` Failed: Expected to perform 2 queries but 11 were done ``` This should ensure there aren't regressions that prevent query-optimization in the future.
1 parent aebc4a3 commit e0f1814

File tree

1 file changed

+147
-3
lines changed

1 file changed

+147
-3
lines changed

graphene_django/tests/test_fields.py

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import datetime
2-
from django.db.models import Count
2+
import re
3+
from django.db.models import Count, Prefetch
34

45
import pytest
56

67
from graphene import List, NonNull, ObjectType, Schema, String
78

89
from ..fields import DjangoListField
910
from ..types import DjangoObjectType
10-
from .models import Article as ArticleModel
11-
from .models import Reporter as ReporterModel
11+
from .models import (
12+
Article as ArticleModel,
13+
Film as FilmModel,
14+
FilmDetails as FilmDetailsModel,
15+
Reporter as ReporterModel,
16+
)
1217

1318

1419
class TestDjangoListField:
@@ -500,3 +505,142 @@ class Query(ObjectType):
500505

501506
assert not result.errors
502507
assert result.data == {"reporters": [{"firstName": "Tara"}]}
508+
509+
def test_select_related_and_prefetch_related_are_respected(self, django_assert_num_queries):
510+
class Article(DjangoObjectType):
511+
class Meta:
512+
model = ArticleModel
513+
fields = ("headline", "editor", "reporter")
514+
515+
class Film(DjangoObjectType):
516+
class Meta:
517+
model = FilmModel
518+
fields = ("genre", "details")
519+
520+
class FilmDetail(DjangoObjectType):
521+
class Meta:
522+
model = FilmDetailsModel
523+
fields = ("location",)
524+
525+
class Reporter(DjangoObjectType):
526+
class Meta:
527+
model = ReporterModel
528+
fields = ("first_name", "articles", "films")
529+
530+
class Query(ObjectType):
531+
articles = DjangoListField(Article)
532+
533+
@staticmethod
534+
def resolve_articles(root, info):
535+
# Optimize for querying associated editors and reporters, and the films and film
536+
# details of those reporters. This is similar to what would happen using a library
537+
# like https://github.com/tfoxy/graphene-django-optimizer for a query like the one
538+
# below (albeit simplified and hardcoded here).
539+
return ArticleModel.objects.select_related("editor", "reporter").prefetch_related(
540+
Prefetch("reporter__films", queryset=FilmModel.objects.select_related("details")),
541+
)
542+
543+
schema = Schema(query=Query)
544+
545+
query = """
546+
query {
547+
articles {
548+
headline
549+
550+
editor {
551+
firstName
552+
}
553+
554+
reporter {
555+
firstName
556+
557+
films {
558+
genre
559+
560+
details {
561+
location
562+
}
563+
}
564+
}
565+
}
566+
}
567+
"""
568+
569+
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
570+
r2 = ReporterModel.objects.create(first_name="Debra", last_name="Payne")
571+
572+
ArticleModel.objects.create(
573+
headline="Amazing news",
574+
reporter=r1,
575+
pub_date=datetime.date.today(),
576+
pub_date_time=datetime.datetime.now(),
577+
editor=r2,
578+
)
579+
ArticleModel.objects.create(
580+
headline="Not so good news",
581+
reporter=r2,
582+
pub_date=datetime.date.today(),
583+
pub_date_time=datetime.datetime.now(),
584+
editor=r1,
585+
)
586+
587+
film1 = FilmModel.objects.create(genre="ac")
588+
film2 = FilmModel.objects.create(genre="ot")
589+
film3 = FilmModel.objects.create(genre="do")
590+
FilmDetailsModel.objects.create(location="Hollywood", film=film1)
591+
FilmDetailsModel.objects.create(location="Antarctica", film=film3)
592+
r1.films.add(film1, film2)
593+
r2.films.add(film3)
594+
595+
# We expect 2 queries to be performed based on the above resolver definition: one for all
596+
# articles joined with the reporters model (for associated editors and reporters), and one
597+
# for the films prefetch (which includes its `select_related` JOIN logic in its queryset)
598+
with django_assert_num_queries(2) as captured:
599+
result = schema.execute(query)
600+
601+
assert not result.errors
602+
assert result.data == {
603+
"articles": [
604+
{
605+
"headline": "Amazing news",
606+
"editor": {
607+
"firstName": "Debra"
608+
},
609+
"reporter": {
610+
"firstName": "Tara",
611+
"films": [
612+
{"genre": "AC", "details": {"location": "Hollywood"}},
613+
{"genre": "OT", "details": None},
614+
]
615+
},
616+
},
617+
{
618+
"headline": "Not so good news",
619+
"editor": {
620+
"firstName": "Tara"
621+
},
622+
"reporter": {
623+
"firstName": "Debra",
624+
"films": [
625+
{"genre": "DO", "details": {"location": "Antarctica"}},
626+
]
627+
},
628+
},
629+
]
630+
}
631+
632+
assert len(captured.captured_queries) == 2 # Sanity-check
633+
634+
# First we should have queried for all articles in a single query, joining on the reporters
635+
# model (for the editors and reporters ForeignKeys)
636+
assert re.match(
637+
r'SELECT .* "tests_article" INNER JOIN "tests_reporter"',
638+
captured.captured_queries[0]["sql"],
639+
)
640+
641+
# Then we should have queried for all of the films of all reporters, joined with the film
642+
# details for each film, using a single query
643+
assert re.match(
644+
r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"',
645+
captured.captured_queries[1]["sql"],
646+
)

0 commit comments

Comments
 (0)