Переглянути джерело

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
Martin Hauser 1 день тому
батько
коміт
2b3b9e62e7

+ 1 - 1
netbox/ipam/tests/query_counts.json

@@ -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,

+ 1 - 1
netbox/users/tests/query_counts.json

@@ -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,

+ 2 - 2
netbox/utilities/api.py

@@ -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

+ 36 - 1
netbox/utilities/tests/test_api.py

@@ -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