Add prefetch hint for GraphQL tags on list endpoints (#22570)

GraphQL list queries that request tags were issuing one tag lookup query
per object (N+1). TagsMixin declared the field without a prefetch hint;
django-taggit's M2M is not batched by DjangoOptimizerExtension the way
GenericRelations are. Add prefetch_related=['tags'] on the mixin field,
following the pattern from #22061 for journal entries and image
attachments.

Closes #22551
This commit is contained in:
jkburges 2026-07-01 03:13:49 +10:00 committed by GitHub
parent 3d73b2a166
commit 614d50a564
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 62 additions and 1 deletions

View File

@ -77,7 +77,9 @@ class JournalEntriesMixin:
@strawberry.type
class TagsMixin:
tags: list[Annotated['TagType', strawberry.lazy('.types')]]
tags: list[Annotated['TagType', strawberry.lazy('.types')]] = strawberry_django.field(
prefetch_related=['tags'],
)
@strawberry.type

View File

@ -3,7 +3,9 @@ import re
import strawberry
from django.contrib.contenttypes.models import ContentType
from django.db import connection
from django.test import override_settings
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from rest_framework import status
from strawberry.extensions import QueryDepthLimiter
@ -333,6 +335,63 @@ class GraphQLAPITestCase(APITestCase):
self.assertNotIn('errors', data)
self.assertEqual(int(data['data']['table_config']['object_type']['id']), site_ct.pk)
@override_settings(LOGIN_REQUIRED=True)
def test_graphql_device_list_tags_are_prefetched(self):
"""
Requesting tags on device_list must batch tag lookups (no N+1 per device).
"""
self.add_permissions('dcim.view_device', 'extras.view_tag')
manufacturer = Manufacturer.objects.create(name='Prefetch Manufacturer', slug='prefetch-manufacturer')
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='Prefetch Model',
slug='prefetch-model',
)
device_role = DeviceRole.objects.create(name='Prefetch Role', slug='prefetch-role')
site = Site.objects.first()
tag_alpha = Tag.objects.create(name='Prefetch Alpha', slug='prefetch-alpha')
tag_beta = Tag.objects.create(name='Prefetch Beta', slug='prefetch-beta')
devices = Device.objects.bulk_create([
Device(
name=f'Prefetch Device {index}',
device_type=device_type,
role=device_role,
site=site,
)
for index in range(10)
])
for device in devices:
device.tags.set([tag_alpha, tag_beta])
query = """
{
device_list(filters: {role: {slug: {exact: "prefetch-role"}}}) {
name
tags {
slug
}
}
}
"""
url = reverse('graphql')
with CaptureQueriesContext(connection) as context:
response = self.client.post(url, data={'query': query}, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
data = json.loads(response.content)
self.assertNotIn('errors', data)
self.assertEqual(len(data['data']['device_list']), 10)
tag_queries = sum(1 for query_record in context.captured_queries if 'extras_tag' in query_record['sql'])
self.assertLessEqual(
tag_queries,
2,
msg=f'Expected batched tag prefetch, got {tag_queries} tag queries for 10 devices',
)
def test_offset_pagination(self):
self.add_permissions('dcim.view_site')
url = reverse('graphql')