perf(api): Prefetch reverse many-to-many serializer fields (#23064)

Recognize Django's ManyToManyRel in get_prefetches_for_serializer().
Because it is a sibling of ManyToOneRel rather than a subclass, reverse
many-to-many accessors were omitted from the generated prefetch paths and
fetched once per serialized object.

Add regression coverage for both automatically generated fields and
SerializedPKRelatedField(many=True), and regenerate the affected ASN and
ObjectPermission API query-count baselines.

Fixes #23060
This commit is contained in:
Martin Hauser 2026-08-31 19:15:21 +02:00 committed by GitHub
parent 0f22d67617
commit 2b3b9e62e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 40 additions and 5 deletions

View File

@ -1,7 +1,7 @@
{
"aggregate:api_list_objects": 13,
"aggregate:list_objects_with_permission": 21,
"asn:api_list_objects": 17,
"asn:api_list_objects": 16,
"asn:list_objects_with_permission": 28,
"asnrange:api_list_objects": 14,
"asnrange:list_objects_with_permission": 19,

View File

@ -1,7 +1,7 @@
{
"group:api_list_objects": 10,
"group:list_objects_with_permission": 16,
"objectpermission:api_list_objects": 14,
"objectpermission:api_list_objects": 12,
"objectpermission:list_objects_with_permission": 17,
"owner:api_list_objects": 11,
"owner:list_objects_with_permission": 18,

View File

@ -8,7 +8,7 @@ from django.core.exceptions import (
ObjectDoesNotExist,
ValidationError,
)
from django.db.models.fields.related import ManyToOneRel, RelatedField
from django.db.models.fields.related import ManyToManyRel, ManyToOneRel, RelatedField
from django.urls import reverse
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
@ -193,7 +193,7 @@ def get_prefetches_for_serializer(serializer_class, fields=None, omit=None, _ser
# If the serializer field does not map to a discrete model field, skip it.
try:
field = model._meta.get_field(model_field_name)
if isinstance(field, (RelatedField, ManyToOneRel, GenericForeignKey)):
if isinstance(field, (RelatedField, ManyToOneRel, ManyToManyRel, GenericForeignKey)):
prefetch_fields.append(field.name)
except FieldDoesNotExist:
continue

View File

@ -16,7 +16,7 @@ from netbox.api.serializers import BaseModelSerializer
from netbox.config import get_config
from netbox.plugins import register_serializer_resolver
from netbox.registry import registry
from users.models import ObjectPermission
from users.models import Group, ObjectPermission
from utilities.api import (
get_prefetches_for_serializer,
get_serializer_for_model,
@ -754,6 +754,41 @@ class GetPrefetchesForSerializerTestCase(TestCase):
['parent', 'parent__sites', 'children', 'children__sites'],
)
def test_reverse_many_to_many_relation_is_prefetched(self):
class ObjectPermissionSerializer(BaseModelSerializer):
class Meta:
model = ObjectPermission
fields = ('groups',)
self.assertListEqual(
get_prefetches_for_serializer(ObjectPermissionSerializer),
['groups'],
)
def test_reverse_many_to_many_serialized_related_field_is_prefetched(self):
class GroupSerializer(BaseModelSerializer):
class Meta:
model = Group
fields = ('id', 'name')
class ObjectPermissionSerializer(BaseModelSerializer):
groups = SerializedPKRelatedField(
queryset=Group.objects.all(),
serializer=GroupSerializer,
nested=True,
required=False,
many=True
)
class Meta:
model = ObjectPermission
fields = ('id', 'groups')
self.assertListEqual(
get_prefetches_for_serializer(ObjectPermissionSerializer),
['groups'],
)
class _ResolvedSerializerA(Serializer):
pass