Skip to content

Commit 20a6cec

Browse files
sjdemartinifiraskafri
authored andcommitted
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 9796e93 commit 20a6cec

File tree

1 file changed

+150
-3
lines changed

1 file changed

+150
-3
lines changed

Diff for: graphene_django/tests/test_fields.py

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

0 commit comments

Comments
 (0)