From d756fd7ad9d89ec41c551399b1733c885ee173f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarek=20G=C5=82owacki?= Date: Thu, 22 Jul 2021 20:34:28 +1000 Subject: [PATCH 1/6] Remove AutoPrefetchMixin --- CHANGELOG.md | 1 + .../tests/unit/test_filter_schema_params.py | 2 - rest_framework_json_api/views.py | 55 ++----------------- 3 files changed, 6 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9eaf504..9c34963f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ any parts of the framework not mentioned in the documentation should generally b ### Changed * Moved resolving of `included_serialzers` and `related_serializers` classes to serializer's meta class. +* Removed `PreloadIncludesMixin`, as the logic did not work when nesting includes, and the laborious effort needed in its manual config was unnecessary. ### Deprecated diff --git a/example/tests/unit/test_filter_schema_params.py b/example/tests/unit/test_filter_schema_params.py index 9c78dc61..6dd187ce 100644 --- a/example/tests/unit/test_filter_schema_params.py +++ b/example/tests/unit/test_filter_schema_params.py @@ -20,8 +20,6 @@ class DummyEntryViewSet(EntryViewSet): } def __init__(self, **kwargs): - # dummy up self.request since PreloadIncludesMixin expects it to be defined - self.request = None super(DummyEntryViewSet, self).__init__(**kwargs) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 6b739582..7d07dfc5 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -11,6 +11,7 @@ from django.db.models.manager import Manager from django.db.models.query import QuerySet from django.urls import NoReverseMatch +from django.utils.module_loading import import_string as import_class_from_dotted_path from rest_framework import generics, viewsets from rest_framework.exceptions import MethodNotAllowed, NotFound from rest_framework.fields import get_attribute @@ -30,54 +31,6 @@ ) -class PreloadIncludesMixin(object): - """ - This mixin provides a helper attributes to select or prefetch related models - based on the include specified in the URL. - - __all__ can be used to specify a prefetch which should be done regardless of the include - - - .. code:: python - - # When MyViewSet is called with ?include=author it will prefetch author and authorbio - class MyViewSet(viewsets.ModelViewSet): - queryset = Book.objects.all() - prefetch_for_includes = { - '__all__': [], - 'category.section': ['category'] - } - select_for_includes = { - '__all__': [], - 'author': ['author', 'author__authorbio'], - } - """ - - def get_select_related(self, include): - return getattr(self, "select_for_includes", {}).get(include, None) - - def get_prefetch_related(self, include): - return getattr(self, "prefetch_for_includes", {}).get(include, None) - - def get_queryset(self, *args, **kwargs): - qs = super(PreloadIncludesMixin, self).get_queryset(*args, **kwargs) - - included_resources = get_included_resources( - self.request, self.get_serializer_class() - ) - for included in included_resources + ["__all__"]: - - select_related = self.get_select_related(included) - if select_related is not None: - qs = qs.select_related(*select_related) - - prefetch_related = self.get_prefetch_related(included) - if prefetch_related is not None: - qs = qs.prefetch_related(*prefetch_related) - - return qs - - class AutoPrefetchMixin(object): def get_queryset(self, *args, **kwargs): """This mixin adds automatic prefetching for OneToOne and ManyToMany fields.""" @@ -182,6 +135,8 @@ def get_related_serializer_class(self): False ), 'Either "included_serializers" or "related_serializers" should be configured' + if not isinstance(_class, type): + return import_class_from_dotted_path(_class) return _class return parent_serializer_class @@ -215,13 +170,13 @@ def get_related_instance(self): class ModelViewSet( - AutoPrefetchMixin, PreloadIncludesMixin, RelatedMixin, viewsets.ModelViewSet + AutoPrefetchMixin, RelatedMixin, viewsets.ModelViewSet ): http_method_names = ["get", "post", "patch", "delete", "head", "options"] class ReadOnlyModelViewSet( - AutoPrefetchMixin, PreloadIncludesMixin, RelatedMixin, viewsets.ReadOnlyModelViewSet + AutoPrefetchMixin, RelatedMixin, viewsets.ReadOnlyModelViewSet ): http_method_names = ["get", "post", "patch", "delete", "head", "options"] From ae3c12f81e2ecda0be01b31e7c0e47ad7eac55f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarek=20G=C5=82owacki?= Date: Thu, 22 Jul 2021 21:00:23 +1000 Subject: [PATCH 2/6] Add nested prefetching & ondemand logic --- CHANGELOG.md | 2 + rest_framework_json_api/serializers.py | 43 +++++++ rest_framework_json_api/settings.py | 1 + rest_framework_json_api/utils.py | 153 ++++++++++++++++++++++++- rest_framework_json_api/views.py | 111 +++++++++--------- 5 files changed, 253 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c34963f..58c0fb8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ any parts of the framework not mentioned in the documentation should generally b ### Changed * Moved resolving of `included_serialzers` and `related_serializers` classes to serializer's meta class. +* `AutoPrefetchMixin` updated to be more clever about how relationships are prefetched, with recursion all the way down. +* Expensive reverse relations are now automatically excluded from queries that don't explicitly name them in sparsefieldsets. Set `INCLUDE_EXPENSVE_FIELDS` to revert to old behaviour. * Removed `PreloadIncludesMixin`, as the logic did not work when nesting includes, and the laborious effort needed in its manual config was unnecessary. ### Deprecated diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index bc60f193..2f572918 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -23,12 +23,15 @@ from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.relations import ResourceRelatedField from rest_framework_json_api.utils import ( + get_expensive_relational_fields, get_included_resources, get_resource_type_from_instance, get_resource_type_from_model, get_resource_type_from_serializer, ) +from .settings import json_api_settings + class ResourceIdentifierObjectSerializer(BaseSerializer): default_error_messages = { @@ -153,6 +156,43 @@ def validate_path(serializer_class, field_path, path): super(IncludedResourcesValidationMixin, self).__init__(*args, **kwargs) +class OnDemandFieldsMixin: + """ + Automatically certain fields from the serializer that have been deemed expensive. + In order to see these fields, the client must explcitly request them. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Pop any fields off the serializer that shouldn't come through. + for field in self.get_excluded_ondemand_fields(): + self.fields.pop(field, None) + + def get_excluded_ondemand_fields(self) -> list[str]: + """ + Determine which fields should be popped off if not explicitly asked for. + Will not nominate any fields that have been designated as `demanded_fields` in context. + Ondemand fields are determined in like so: + - Fields that we automatically determine to be expensive, and thus automatically remove + from the default offering. Currently such fields are M2Ms and reverse FKs. + """ + if json_api_settings.INCLUDE_EXPENSVE_FIELDS: + return set() + + # If we've instantiated the serializer ourselves, we'll have fed `demanded_fields` into its context. + # If it's happened as part of drf render internals, then we have a fallback where the view + # has provided the entire sparsefields context for us to pick through. + if 'demanded_fields' in self.context: + demanded_fields = set(self.context.get('demanded_fields')) + else: + resource_name = get_resource_type_from_serializer(type(self)) + demanded_fields = set(self.context.get('all_sparsefields', {}).get(resource_name, [])) + + # We only want to exclude those ondemand fields that haven't been explicitly requested. + return set(get_expensive_relational_fields(type(self))) - set(demanded_fields) + + class LazySerializersDict(Mapping): """ A dictionary of serializers which lazily import dotted class path and self. @@ -207,6 +247,7 @@ def __new__(cls, name, bases, attrs): # If user imports serializer from here we can catch class definition and check # nested serializers for depricated use. class Serializer( + OnDemandFieldsMixin, IncludedResourcesValidationMixin, SparseFieldsetsMixin, Serializer, @@ -230,6 +271,7 @@ class Serializer( class HyperlinkedModelSerializer( + OnDemandFieldsMixin, IncludedResourcesValidationMixin, SparseFieldsetsMixin, HyperlinkedModelSerializer, @@ -250,6 +292,7 @@ class HyperlinkedModelSerializer( class ModelSerializer( + OnDemandFieldsMixin, IncludedResourcesValidationMixin, SparseFieldsetsMixin, ModelSerializer, diff --git a/rest_framework_json_api/settings.py b/rest_framework_json_api/settings.py index 0e790847..b63ac896 100644 --- a/rest_framework_json_api/settings.py +++ b/rest_framework_json_api/settings.py @@ -15,6 +15,7 @@ "FORMAT_RELATED_LINKS": False, "PLURALIZE_TYPES": False, "UNIFORM_EXCEPTIONS": False, + "INCLUDE_EXPENSVE_FIELDS": False, } diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 19c72809..18549863 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -1,23 +1,33 @@ import inspect +import logging import operator import warnings from collections import OrderedDict import inflection from django.conf import settings -from django.db.models import Manager +from django.db.models import Manager, Prefetch from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, ManyToManyDescriptor, ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, ) +from django.db.models.query import QuerySet from django.http import Http404 from django.utils import encoding from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions from rest_framework.exceptions import APIException +from rest_framework.relations import RelatedField +from rest_framework.request import Request + +from rest_framework_json_api.serializers import ModelSerializer, ValidationError from .settings import json_api_settings +logger = logging.getLogger(__name__) + # Generic relation descriptor from django.contrib.contenttypes. if "django.contrib.contenttypes" not in settings.INSTALLED_APPS: # pragma: no cover # Target application does not use contenttypes. Importing would cause errors. @@ -472,3 +482,144 @@ def format_errors(data): if len(data) > 1 and isinstance(data, list): data.sort(key=lambda x: x.get("source", {}).get("pointer", "")) return {"errors": data} + + +def get_expensive_relational_fields(serializer_class: ModelSerializer) -> list[str]: + """ + We define 'expensive' as relational fields on the serializer that don't correspond to a + forward relation on the model. + """ + return [ + field + for field in getattr(serializer_class, 'included_serializers', {}) + if not isinstance(getattr(serializer_class.Meta.model, field, None), ForwardManyToOneDescriptor) + ] + + +def get_cheap_relational_fields(serializer_class: ModelSerializer) -> list[str]: + """ + We define 'cheap' as relational fields on the serializer that _do_ correspond to a + forward relation on the model. + """ + return [ + field + for field in getattr(serializer_class, 'included_serializers', {}) + if isinstance(getattr(serializer_class.Meta.model, field, None), ForwardManyToOneDescriptor) + ] + + +def get_queryset_for_field(field: RelatedField) -> QuerySet: + model_field_descriptor = getattr(field.parent.Meta.model, field.field_name) + # NOTE: Important to check in this order, as some of these classes are ancestors of one + # another (ie `ManyToManyDescriptor` subclasses `ReverseManyToOneDescriptor`) + if isinstance(model_field_descriptor, ForwardManyToOneDescriptor): + if (qs := field.queryset) is None: + qs = model_field_descriptor.field.related_model._default_manager + elif isinstance(model_field_descriptor, ManyToManyDescriptor): + qs = field.child_relation.queryset + elif isinstance(model_field_descriptor, ReverseManyToOneDescriptor): + if (qs := field.child_relation.queryset) is None: + qs = model_field_descriptor.field.model._default_manager + elif isinstance(model_field_descriptor, ReverseOneToOneDescriptor): + qs = model_field_descriptor.get_queryset() + + # Note: We call `.all()` before returning, as `_default_manager` may on occasion return a Manager + # instance rather than a QuerySet, and we strictly want to be working with the latter. + # (_default_manager is being used both direclty by us here, and by drf behind the scenes) + # See: https://github.com/encode/django-rest-framework/blame/master/rest_framework/utils/field_mapping.py#L243 + return qs.all() + + +def add_nested_prefetches_to_qs( + serializer_class: ModelSerializer, + qs: QuerySet, + request: Request, + sparsefields: dict[str, list[str]], + includes: dict, # TODO: Define typing as recursive once supported. + select_related: str = '', +) -> QuerySet: + """ + Prefetch all required data onto the supplied queryset, calling this method recursively for child + serializers where needed. + There is some added built-in optimisation here, attempting to opt for select_related calls over + prefetches where possible -- it's only possible if the child serializers are interested + exclusively in select_relating also. This is controlled with the `select_related` param. + If `select_related` comes through, will attempt to instead build further onto this and return + a dundered list of strings for the caller to use in a select_related call. If that fails, + returns a qs as normal. + """ + # Determine fields that'll be returned by this serializer. + resource_name = get_resource_type_from_serializer(serializer_class) + logger.debug(f'ADDING NESTED PREFETCHES FOR: {resource_name}') + dummy_serializer = serializer_class(context={'request': request, 'demanded_fields': sparsefields.get(resource_name, [])}) + requested_fields = dummy_serializer.fields.keys() + + # Ensure any requested includes are in the fields list, else error loudly! + if not includes.keys() <= requested_fields: + errors = {f'{resource_name}.{field}': 'Field marked as include but not requested for serialization.' for field in includes.keys() - requested_fields} + raise ValidationError(errors) + + included_serializers = get_included_serializers(serializer_class) + + # Iterate over all expensive relations and prefetch_related where needed. + for field in get_expensive_relational_fields(serializer_class): + if field in requested_fields: + logger.debug(f'EXPENSIVE_FIELD: {field}') + select_related = '' # wipe, cannot be used. :( + if not hasattr(qs.model, field): + # We might fall into here if, for example, there's an expensive + # SerializerMethodResourceRelatedField defined. + continue + if field in includes: + logger.debug('- PREFETCHING DEEP') + # Prefetch and recurse. + child_serializer_class = included_serializers[field] + prefetch_qs = add_nested_prefetches_to_qs( + child_serializer_class, + get_queryset_for_field(dummy_serializer.fields[field]), + request=request, + sparsefields=sparsefields, + includes=includes[field], + ) + qs = qs.prefetch_related(Prefetch(field, prefetch_qs)) + else: + logger.debug('- PREFETCHING SHALLOW') + # Prefetch "shallowly"; we only care about ids. + qs = qs.prefetch_related(field) # TODO: Still use ResourceRelatedField.qs if present! + + # Iterate over all cheap (forward) relations and select_related (or prefetch) where needed. + new_select_related = [select_related] + for field in get_cheap_relational_fields(serializer_class): + if field in requested_fields: + logger.debug(f'CHEAP_FIELD: {field}') + if field in includes: + logger.debug('- present in includes') + # Recurse and see if we get a prefetch qs back, or a select_related string. + child_serializer_class = included_serializers[field] + prefetch_qs_or_select_related_str = add_nested_prefetches_to_qs( + child_serializer_class, + get_queryset_for_field(dummy_serializer.fields[field]), + request=request, + sparsefields=sparsefields, + includes=includes[field], + select_related=field, + ) + if isinstance(prefetch_qs_or_select_related_str, list): + logger.debug(f'SELECTING RELATED: {prefetch_qs_or_select_related_str}') + # Prefetch has come back as a list of (dundered) strings. + # We append onto existing select_related string, to potentially pass back up + # and also feed it directly into a select_related call in case the former + # falls through. + if select_related: + for sr in prefetch_qs_or_select_related_str: + new_select_related.append(f'{select_related}__{sr}') + qs = qs.select_related(*prefetch_qs_or_select_related_str) + else: + # Select related option fell through, we need to do a prefetch. :( + logger.debug(f'PREFETCHING RELATED: {field}') + select_related = '' + qs = qs.prefetch_related(Prefetch(field, prefetch_qs_or_select_related_str)) + + if select_related: + return new_select_related + return qs diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 7d07dfc5..da7e293d 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -1,13 +1,8 @@ +import re from collections.abc import Iterable from django.core.exceptions import ImproperlyConfigured from django.db.models import Model -from django.db.models.fields.related_descriptors import ( - ForwardManyToOneDescriptor, - ManyToManyDescriptor, - ReverseManyToOneDescriptor, - ReverseOneToOneDescriptor, -) from django.db.models.manager import Manager from django.db.models.query import QuerySet from django.urls import NoReverseMatch @@ -25,59 +20,67 @@ from rest_framework_json_api.utils import ( Hyperlink, OrderedDict, - get_included_resources, + add_nested_prefetches_to_qs, get_resource_type_from_instance, + includes_to_dict, undo_format_link_segment, ) class AutoPrefetchMixin(object): - def get_queryset(self, *args, **kwargs): - """This mixin adds automatic prefetching for OneToOne and ManyToMany fields.""" - qs = super(AutoPrefetchMixin, self).get_queryset(*args, **kwargs) - - included_resources = get_included_resources( - self.request, self.get_serializer_class() - ) - - for included in included_resources + ["__all__"]: - # If include was not defined, trying to resolve it automatically - included_model = None - levels = included.split(".") - level_model = qs.model - for level in levels: - if not hasattr(level_model, level): - break - field = getattr(level_model, level) - field_class = field.__class__ - - is_forward_relation = issubclass( - field_class, (ForwardManyToOneDescriptor, ManyToManyDescriptor) - ) - is_reverse_relation = issubclass( - field_class, (ReverseManyToOneDescriptor, ReverseOneToOneDescriptor) - ) - if not (is_forward_relation or is_reverse_relation): - break + """ Hides "expensive" fields by default, and calculates automatic prefetching when said fields + are explicitly requested. - if level == levels[-1]: - included_model = field - else: - - if issubclass(field_class, ReverseOneToOneDescriptor): - model_field = field.related.field - else: - model_field = field.field - - if is_forward_relation: - level_model = model_field.related_model - else: - level_model = model_field.model + "Expensive" fields are ones that require additional SQL queries to prepare, such as + reverse or M2M relations. + """ + def __init_subclass__(cls, **kwargs): + """Run a smidge of validation at class declaration, to avoid silly mistakes.""" + + # Throw error if a `prefetch_for_includes` is defined. + if hasattr(cls, 'prefetch_for_includes'): + raise AttributeError( + f"{cls.__name__!r} defines `prefetch_for_includes`. This manual legacy form of" + " prefetching is no longer supported! It's all automatically handled now." + ) - if included_model is not None: - qs = qs.prefetch_related(included.replace(".", "__")) + return super().__init_subclass__(**kwargs) + + def get_sparsefields_as_dict(self): + if not hasattr(self, '_sparsefields'): + self._sparsefields = { + match.groupdict()['resource_name']: queryvalues.split(',') + for queryparam, queryvalues in self.request.query_params.items() + if (match := re.match(r'fields\[(?P\w+)\]', queryparam)) + } + return self._sparsefields + + def get_queryset(self, *args, **kwargs) -> QuerySet: + qs = super().get_queryset(*args, **kwargs) + # Since we're going to be recursing through serializers (to cover nested cases), we hand + # the prefetching work off to the top-level serializer here. We give it: + # - the base qs. + # - the request, in case the serializer wants to perform any user-permission-based logic. + # - sparsefields & includes. + # The serializer will return a qs with all required prefetches, select_related calls and + # annotations tacked on. If the serializer encounters any includes, it'll + # itself pass the work down to additional serializers to get their contribution. + return add_nested_prefetches_to_qs( + self.get_serializer_class(), + qs, + request=self.request, + sparsefields=self.get_sparsefields_as_dict(), + includes=includes_to_dict(self.request.query_params.get('include', '').replace(',', ' ').split()), # See https://bugs.python.org/issue28937#msg282923 + ) - return qs + def get_serializer_context(self): + """ Pass args into the serializer's context, for field-level access. """ + context = super().get_serializer_context() + # We don't have direct control over some serializers, so we can't always feed them their + # specific `demanded_fields` into context how we'd like. Next best thing is to make the + # entire sparsefields dict available for them to pick through. + context['all_sparsefields'] = self.get_sparsefields_as_dict() + return context class RelatedMixin(object): @@ -169,15 +172,11 @@ def get_related_instance(self): raise NotFound -class ModelViewSet( - AutoPrefetchMixin, RelatedMixin, viewsets.ModelViewSet -): +class ModelViewSet(AutoPrefetchMixin, RelatedMixin, viewsets.ModelViewSet): http_method_names = ["get", "post", "patch", "delete", "head", "options"] -class ReadOnlyModelViewSet( - AutoPrefetchMixin, RelatedMixin, viewsets.ReadOnlyModelViewSet -): +class ReadOnlyModelViewSet(AutoPrefetchMixin, RelatedMixin, viewsets.ReadOnlyModelViewSet): http_method_names = ["get", "post", "patch", "delete", "head", "options"] From dacce806335d0eba0d74437216d01fbe8ba3d313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarek=20G=C5=82owacki?= Date: Thu, 22 Jul 2021 21:01:04 +1000 Subject: [PATCH 3/6] Move utils into a folder --- rest_framework_json_api/{utils.py => utils/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rest_framework_json_api/{utils.py => utils/__init__.py} (100%) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils/__init__.py similarity index 100% rename from rest_framework_json_api/utils.py rename to rest_framework_json_api/utils/__init__.py From 76d297d7a6bea1a443f33b0d5d76df2f942f12fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarek=20G=C5=82owacki?= Date: Thu, 22 Jul 2021 21:18:59 +1000 Subject: [PATCH 4/6] Utils, tests, docs --- CHANGELOG.md | 2 +- docs/usage.md | 60 +------ example/views.py | 9 - rest_framework_json_api/serializers.py | 2 +- rest_framework_json_api/utils/__init__.py | 175 +++---------------- rest_framework_json_api/utils/serializers.py | 161 +++++++++++++++++ rest_framework_json_api/views.py | 9 +- tests/test_utils.py | 22 +++ 8 files changed, 228 insertions(+), 212 deletions(-) create mode 100644 rest_framework_json_api/utils/serializers.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c0fb8d..17b42099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ any parts of the framework not mentioned in the documentation should generally b * Moved resolving of `included_serialzers` and `related_serializers` classes to serializer's meta class. * `AutoPrefetchMixin` updated to be more clever about how relationships are prefetched, with recursion all the way down. * Expensive reverse relations are now automatically excluded from queries that don't explicitly name them in sparsefieldsets. Set `INCLUDE_EXPENSVE_FIELDS` to revert to old behaviour. -* Removed `PreloadIncludesMixin`, as the logic did not work when nesting includes, and the laborious effort needed in its manual config was unnecessary. +* Removed `PreloadIncludesMixin`, as the logic did not work when nesting includes, and the laborious effort needed in its manual config was unnecessary. This removes support for `prefetch_for_includes` and `select_for_includes` ### Deprecated diff --git a/docs/usage.md b/docs/usage.md index 1e45976b..719ec21e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -227,7 +227,7 @@ class MyViewset(ModelViewSet): queryset = MyModel.objects.all() serializer_class = MyModelSerializer filter_backends = (filters.QueryParameterValidationFilter, filters.OrderingFilter, - django_filters.DjangoFilterBackend, SearchFilter) + django_filters.DjangoFilterBackend, SearchFilter) filterset_fields = { 'id': ('exact', 'lt', 'gt', 'gte', 'lte', 'in'), 'descriptuon': ('icontains', 'iexact', 'contains'), @@ -387,7 +387,7 @@ Example without format conversion: ``` js { - "data": [{ + "data": [{ "type": "blog_identity", "id": "3", "attributes": { @@ -410,7 +410,7 @@ When set to dasherize: ``` js { - "data": [{ + "data": [{ "type": "blog-identity", "id": "3", "attributes": { @@ -436,7 +436,7 @@ Example without pluralization: ``` js { - "data": [{ + "data": [{ "type": "identity", "id": "3", "attributes": { @@ -459,7 +459,7 @@ When set to pluralize: ``` js { - "data": [{ + "data": [{ "type": "identities", "id": "3", "attributes": { @@ -643,7 +643,7 @@ and increase performance. #### SerializerMethodResourceRelatedField -`relations.SerializerMethodResourceRelatedField` combines behaviour of DRF `SerializerMethodField` and +`relations.SerializerMethodResourceRelatedField` combines behaviour of DRF `SerializerMethodField` and `ResourceRelatedField`, so it accepts `method_name` together with `model` and links-related arguments. `data` is rendered in `ResourceRelatedField` manner. @@ -940,28 +940,10 @@ class QuestSerializer(serializers.ModelSerializer): #### Performance improvements -Be aware that using included resources without any form of prefetching **WILL HURT PERFORMANCE** as it will introduce m\*(n+1) queries. +Be aware that reverse relationships can be expensive to prepare. -A viewset helper was therefore designed to automatically preload data when possible. Such is automatically available when subclassing `ModelViewSet` or `ReadOnlyModelViewSet`. +As a result, these are excluded by default unless explicitly demanded with sparsefieldsets. -It also allows to define custom `select_related` and `prefetch_related` for each requested `include` when needed in special cases: - -`rest_framework_json_api.views.ModelViewSet`: -```python -from rest_framework_json_api import views - -# When MyViewSet is called with ?include=author it will dynamically prefetch author and author.bio -class MyViewSet(views.ModelViewSet): - queryset = Book.objects.all() - select_for_includes = { - 'author': ['author__bio'], - } - prefetch_for_includes = { - '__all__': [], - 'all_authors': [Prefetch('all_authors', queryset=Author.objects.select_related('bio'))], - 'category.section': ['category'] - } -``` An additional convenience DJA class exists for read-only views, just as it does in DRF. ```python @@ -971,31 +953,6 @@ class MyReadOnlyViewSet(views.ReadOnlyModelViewSet): # ... ``` -The special keyword `__all__` can be used to specify a prefetch which should be done regardless of the include, similar to making the prefetch yourself on the QuerySet. - -Using the helper to prefetch, rather than attempting to minimise queries via `select_related` might give you better performance depending on the characteristics of your data and database. - -For example: - -If you have a single model, e.g. Book, which has four relations e.g. Author, Publisher, CopyrightHolder, Category. - -To display 25 books and related models, you would need to either do: - -a) 1 query via selected_related, e.g. SELECT * FROM books LEFT JOIN author LEFT JOIN publisher LEFT JOIN CopyrightHolder LEFT JOIN Category - -b) 4 small queries via prefetch_related. - -If you have 1M books, 50k authors, 10k categories, 10k copyrightholders -in the `select_related` scenario, you've just created a in-memory table -with 1e18 rows which will likely exhaust any available memory and -slow your database to crawl. - -The `prefetch_related` case will issue 4 queries, but they will be small and fast queries. - - ## Generating an OpenAPI Specification (OAS) 3.0 schema document DRF >= 3.12 has a [new OAS schema functionality](https://www.django-rest-framework.org/api-guide/schemas/) to generate an @@ -1115,4 +1072,3 @@ urlpatterns = [ ... ] ``` - diff --git a/example/views.py b/example/views.py index 0b35d4e4..c2a05dd5 100644 --- a/example/views.py +++ b/example/views.py @@ -236,11 +236,6 @@ def get_serializer_class(self): class CommentViewSet(ModelViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer - select_for_includes = {"writer": ["author__bio"]} - prefetch_for_includes = { - "__all__": [], - "author": ["author__bio", "author__entries"], - } def get_queryset(self, *args, **kwargs): entry_pk = self.kwargs.get("entry_pk", None) @@ -285,7 +280,3 @@ class AuthorRelationshipView(RelationshipView): class LabResultViewSet(ReadOnlyModelViewSet): queryset = LabResults.objects.all() serializer_class = LabResultsSerializer - prefetch_for_includes = { - "__all__": [], - "author": ["author__bio", "author__entries"], - } diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 2f572918..fef3dd1c 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -23,7 +23,6 @@ from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.relations import ResourceRelatedField from rest_framework_json_api.utils import ( - get_expensive_relational_fields, get_included_resources, get_resource_type_from_instance, get_resource_type_from_model, @@ -31,6 +30,7 @@ ) from .settings import json_api_settings +from .utils.serializers import get_expensive_relational_fields class ResourceIdentifierObjectSerializer(BaseSerializer): diff --git a/rest_framework_json_api/utils/__init__.py b/rest_framework_json_api/utils/__init__.py index 18549863..565e7e77 100644 --- a/rest_framework_json_api/utils/__init__.py +++ b/rest_framework_json_api/utils/__init__.py @@ -1,32 +1,22 @@ import inspect -import logging import operator import warnings from collections import OrderedDict import inflection from django.conf import settings -from django.db.models import Manager, Prefetch +from django.db.models import Manager from django.db.models.fields.related_descriptors import ( - ForwardManyToOneDescriptor, ManyToManyDescriptor, ReverseManyToOneDescriptor, - ReverseOneToOneDescriptor, ) -from django.db.models.query import QuerySet from django.http import Http404 from django.utils import encoding from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions from rest_framework.exceptions import APIException -from rest_framework.relations import RelatedField -from rest_framework.request import Request -from rest_framework_json_api.serializers import ModelSerializer, ValidationError - -from .settings import json_api_settings - -logger = logging.getLogger(__name__) +from ..settings import json_api_settings # Generic relation descriptor from django.contrib.contenttypes. if "django.contrib.contenttypes" not in settings.INSTALLED_APPS: # pragma: no cover @@ -484,142 +474,31 @@ def format_errors(data): return {"errors": data} -def get_expensive_relational_fields(serializer_class: ModelSerializer) -> list[str]: +def includes_to_dict(includes: list[str]) -> dict: """ - We define 'expensive' as relational fields on the serializer that don't correspond to a - forward relation on the model. - """ - return [ - field - for field in getattr(serializer_class, 'included_serializers', {}) - if not isinstance(getattr(serializer_class.Meta.model, field, None), ForwardManyToOneDescriptor) + Converts a bunch of jsonapi includes + [ + 'property.client', + 'property.client.clientgroup', + 'property.client.task_set.branch', + 'property.branch', ] - - -def get_cheap_relational_fields(serializer_class: ModelSerializer) -> list[str]: - """ - We define 'cheap' as relational fields on the serializer that _do_ correspond to a - forward relation on the model. + to a nested dict, ready for traversal + { + property: { + client: { + clientgroup: {}, + task_set: { + branch: {}, + }, + }, + branch: {}, + }, + } """ - return [ - field - for field in getattr(serializer_class, 'included_serializers', {}) - if isinstance(getattr(serializer_class.Meta.model, field, None), ForwardManyToOneDescriptor) - ] - - -def get_queryset_for_field(field: RelatedField) -> QuerySet: - model_field_descriptor = getattr(field.parent.Meta.model, field.field_name) - # NOTE: Important to check in this order, as some of these classes are ancestors of one - # another (ie `ManyToManyDescriptor` subclasses `ReverseManyToOneDescriptor`) - if isinstance(model_field_descriptor, ForwardManyToOneDescriptor): - if (qs := field.queryset) is None: - qs = model_field_descriptor.field.related_model._default_manager - elif isinstance(model_field_descriptor, ManyToManyDescriptor): - qs = field.child_relation.queryset - elif isinstance(model_field_descriptor, ReverseManyToOneDescriptor): - if (qs := field.child_relation.queryset) is None: - qs = model_field_descriptor.field.model._default_manager - elif isinstance(model_field_descriptor, ReverseOneToOneDescriptor): - qs = model_field_descriptor.get_queryset() - - # Note: We call `.all()` before returning, as `_default_manager` may on occasion return a Manager - # instance rather than a QuerySet, and we strictly want to be working with the latter. - # (_default_manager is being used both direclty by us here, and by drf behind the scenes) - # See: https://github.com/encode/django-rest-framework/blame/master/rest_framework/utils/field_mapping.py#L243 - return qs.all() - - -def add_nested_prefetches_to_qs( - serializer_class: ModelSerializer, - qs: QuerySet, - request: Request, - sparsefields: dict[str, list[str]], - includes: dict, # TODO: Define typing as recursive once supported. - select_related: str = '', -) -> QuerySet: - """ - Prefetch all required data onto the supplied queryset, calling this method recursively for child - serializers where needed. - There is some added built-in optimisation here, attempting to opt for select_related calls over - prefetches where possible -- it's only possible if the child serializers are interested - exclusively in select_relating also. This is controlled with the `select_related` param. - If `select_related` comes through, will attempt to instead build further onto this and return - a dundered list of strings for the caller to use in a select_related call. If that fails, - returns a qs as normal. - """ - # Determine fields that'll be returned by this serializer. - resource_name = get_resource_type_from_serializer(serializer_class) - logger.debug(f'ADDING NESTED PREFETCHES FOR: {resource_name}') - dummy_serializer = serializer_class(context={'request': request, 'demanded_fields': sparsefields.get(resource_name, [])}) - requested_fields = dummy_serializer.fields.keys() - - # Ensure any requested includes are in the fields list, else error loudly! - if not includes.keys() <= requested_fields: - errors = {f'{resource_name}.{field}': 'Field marked as include but not requested for serialization.' for field in includes.keys() - requested_fields} - raise ValidationError(errors) - - included_serializers = get_included_serializers(serializer_class) - - # Iterate over all expensive relations and prefetch_related where needed. - for field in get_expensive_relational_fields(serializer_class): - if field in requested_fields: - logger.debug(f'EXPENSIVE_FIELD: {field}') - select_related = '' # wipe, cannot be used. :( - if not hasattr(qs.model, field): - # We might fall into here if, for example, there's an expensive - # SerializerMethodResourceRelatedField defined. - continue - if field in includes: - logger.debug('- PREFETCHING DEEP') - # Prefetch and recurse. - child_serializer_class = included_serializers[field] - prefetch_qs = add_nested_prefetches_to_qs( - child_serializer_class, - get_queryset_for_field(dummy_serializer.fields[field]), - request=request, - sparsefields=sparsefields, - includes=includes[field], - ) - qs = qs.prefetch_related(Prefetch(field, prefetch_qs)) - else: - logger.debug('- PREFETCHING SHALLOW') - # Prefetch "shallowly"; we only care about ids. - qs = qs.prefetch_related(field) # TODO: Still use ResourceRelatedField.qs if present! - - # Iterate over all cheap (forward) relations and select_related (or prefetch) where needed. - new_select_related = [select_related] - for field in get_cheap_relational_fields(serializer_class): - if field in requested_fields: - logger.debug(f'CHEAP_FIELD: {field}') - if field in includes: - logger.debug('- present in includes') - # Recurse and see if we get a prefetch qs back, or a select_related string. - child_serializer_class = included_serializers[field] - prefetch_qs_or_select_related_str = add_nested_prefetches_to_qs( - child_serializer_class, - get_queryset_for_field(dummy_serializer.fields[field]), - request=request, - sparsefields=sparsefields, - includes=includes[field], - select_related=field, - ) - if isinstance(prefetch_qs_or_select_related_str, list): - logger.debug(f'SELECTING RELATED: {prefetch_qs_or_select_related_str}') - # Prefetch has come back as a list of (dundered) strings. - # We append onto existing select_related string, to potentially pass back up - # and also feed it directly into a select_related call in case the former - # falls through. - if select_related: - for sr in prefetch_qs_or_select_related_str: - new_select_related.append(f'{select_related}__{sr}') - qs = qs.select_related(*prefetch_qs_or_select_related_str) - else: - # Select related option fell through, we need to do a prefetch. :( - logger.debug(f'PREFETCHING RELATED: {field}') - select_related = '' - qs = qs.prefetch_related(Prefetch(field, prefetch_qs_or_select_related_str)) - - if select_related: - return new_select_related - return qs + res = {} + for include in includes: + pos = res + for relational_field in include.split('.'): + pos = pos.setdefault(relational_field, {}) + return res diff --git a/rest_framework_json_api/utils/serializers.py b/rest_framework_json_api/utils/serializers.py new file mode 100644 index 00000000..640bf808 --- /dev/null +++ b/rest_framework_json_api/utils/serializers.py @@ -0,0 +1,161 @@ +import logging + +from django.db.models import Prefetch +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) +from django.db.models.query import QuerySet +from rest_framework.relations import RelatedField +from rest_framework.request import Request +from rest_framework.serializers import ModelSerializer, ValidationError + +from rest_framework_json_api.utils import ( + get_included_serializers, + get_resource_type_from_serializer, +) + +logger = logging.getLogger(__name__) + + +def get_expensive_relational_fields(serializer_class) -> list[str]: + """ + We define 'expensive' as relational fields on the serializer that don't correspond to a + forward relation on the model. + """ + return [ + field + for field in getattr(serializer_class, 'included_serializers', {}) + if not isinstance(getattr(serializer_class.Meta.model, field, None), ForwardManyToOneDescriptor) + ] + + +def get_cheap_relational_fields(serializer_class) -> list[str]: + """ + We define 'cheap' as relational fields on the serializer that _do_ correspond to a + forward relation on the model. + """ + return [ + field + for field in getattr(serializer_class, 'included_serializers', {}) + if isinstance(getattr(serializer_class.Meta.model, field, None), ForwardManyToOneDescriptor) + ] + + +def get_queryset_for_field(field: RelatedField) -> QuerySet: + model_field_descriptor = getattr(field.parent.Meta.model, field.field_name) + # NOTE: Important to check in this order, as some of these classes are ancestors of one + # another (ie `ManyToManyDescriptor` subclasses `ReverseManyToOneDescriptor`) + if isinstance(model_field_descriptor, ForwardManyToOneDescriptor): + if (qs := field.queryset) is None: + qs = model_field_descriptor.field.related_model._default_manager + elif isinstance(model_field_descriptor, ManyToManyDescriptor): + qs = field.child_relation.queryset + elif isinstance(model_field_descriptor, ReverseManyToOneDescriptor): + if (qs := field.child_relation.queryset) is None: + qs = model_field_descriptor.field.model._default_manager + elif isinstance(model_field_descriptor, ReverseOneToOneDescriptor): + qs = model_field_descriptor.get_queryset() + + # Note: We call `.all()` before returning, as `_default_manager` may on occasion return a Manager + # instance rather than a QuerySet, and we strictly want to be working with the latter. + # (_default_manager is being used both direclty by us here, and by drf behind the scenes) + # See: https://github.com/encode/django-rest-framework/blame/master/rest_framework/utils/field_mapping.py#L243 + return qs.all() + + +def add_nested_prefetches_to_qs( + serializer_class: ModelSerializer, + qs: QuerySet, + request: Request, + sparsefields: dict[str, list[str]], + includes: dict, # TODO: Define typing as recursive once supported. + select_related: str = '', +) -> QuerySet: + """ + Prefetch all required data onto the supplied queryset, calling this method recursively for child + serializers where needed. + There is some added built-in optimisation here, attempting to opt for select_related calls over + prefetches where possible -- it's only possible if the child serializers are interested + exclusively in select_relating also. This is controlled with the `select_related` param. + If `select_related` comes through, will attempt to instead build further onto this and return + a dundered list of strings for the caller to use in a select_related call. If that fails, + returns a qs as normal. + """ + # Determine fields that'll be returned by this serializer. + resource_name = get_resource_type_from_serializer(serializer_class) + logger.debug(f'ADDING NESTED PREFETCHES FOR: {resource_name}') + dummy_serializer = serializer_class(context={'request': request, 'demanded_fields': sparsefields.get(resource_name, [])}) + requested_fields = dummy_serializer.fields.keys() + + # Ensure any requested includes are in the fields list, else error loudly! + if not includes.keys() <= requested_fields: + errors = {f'{resource_name}.{field}': 'Field marked as include but not requested for serialization.' for field in includes.keys() - requested_fields} + raise ValidationError(errors) + + included_serializers = get_included_serializers(serializer_class) + + # Iterate over all expensive relations and prefetch_related where needed. + for field in get_expensive_relational_fields(serializer_class): + if field in requested_fields: + logger.debug(f'EXPENSIVE_FIELD: {field}') + select_related = '' # wipe, cannot be used. :( + if not hasattr(qs.model, field): + # We might fall into here if, for example, there's an expensive + # SerializerMethodResourceRelatedField defined. + continue + if field in includes: + logger.debug('- PREFETCHING DEEP') + # Prefetch and recurse. + child_serializer_class = included_serializers[field] + prefetch_qs = add_nested_prefetches_to_qs( + child_serializer_class, + get_queryset_for_field(dummy_serializer.fields[field]), + request=request, + sparsefields=sparsefields, + includes=includes[field], + ) + qs = qs.prefetch_related(Prefetch(field, prefetch_qs)) + else: + logger.debug('- PREFETCHING SHALLOW') + # Prefetch "shallowly"; we only care about ids. + qs = qs.prefetch_related(field) # TODO: Still use ResourceRelatedField.qs if present! + + # Iterate over all cheap (forward) relations and select_related (or prefetch) where needed. + new_select_related = [select_related] + for field in get_cheap_relational_fields(serializer_class): + if field in requested_fields: + logger.debug(f'CHEAP_FIELD: {field}') + if field in includes: + logger.debug('- present in includes') + # Recurse and see if we get a prefetch qs back, or a select_related string. + child_serializer_class = included_serializers[field] + prefetch_qs_or_select_related_str = add_nested_prefetches_to_qs( + child_serializer_class, + get_queryset_for_field(dummy_serializer.fields[field]), + request=request, + sparsefields=sparsefields, + includes=includes[field], + select_related=field, + ) + if isinstance(prefetch_qs_or_select_related_str, list): + logger.debug(f'SELECTING RELATED: {prefetch_qs_or_select_related_str}') + # Prefetch has come back as a list of (dundered) strings. + # We append onto existing select_related string, to potentially pass back up + # and also feed it directly into a select_related call in case the former + # falls through. + if select_related: + for sr in prefetch_qs_or_select_related_str: + new_select_related.append(f'{select_related}__{sr}') + qs = qs.select_related(*prefetch_qs_or_select_related_str) + else: + # Select related option fell through, we need to do a prefetch. :( + logger.debug(f'PREFETCHING RELATED: {field}') + select_related = '' + qs = qs.prefetch_related(Prefetch(field, prefetch_qs_or_select_related_str)) + + if select_related: + return new_select_related + return qs diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index da7e293d..a8639b88 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -20,12 +20,13 @@ from rest_framework_json_api.utils import ( Hyperlink, OrderedDict, - add_nested_prefetches_to_qs, get_resource_type_from_instance, includes_to_dict, undo_format_link_segment, ) +from .utils.serializers import get_expensive_relational_fields + class AutoPrefetchMixin(object): """ Hides "expensive" fields by default, and calculates automatic prefetching when said fields @@ -43,6 +44,12 @@ def __init_subclass__(cls, **kwargs): f"{cls.__name__!r} defines `prefetch_for_includes`. This manual legacy form of" " prefetching is no longer supported! It's all automatically handled now." ) + # Throw error if a `select_for_includes` is defined. + if hasattr(cls, 'select_for_includes'): + raise AttributeError( + f"{cls.__name__!r} defines `select_for_includes`. This manual legacy form of" + " prefetching is no longer supported! It's all automatically handled now." + ) return super().__init_subclass__(**kwargs) diff --git a/tests/test_utils.py b/tests/test_utils.py index 43c12dcf..2b904103 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,6 +15,7 @@ get_included_serializers, get_related_resource_type, get_resource_name, + includes_to_dict, undo_format_field_name, undo_format_field_names, undo_format_link_segment, @@ -377,3 +378,24 @@ class Meta: } assert included_serializers == expected_included_serializers + + +def test_includes_to_dict(): + result = includes_to_dict([ + 'property.client', + 'property.client.clientgroup', + 'property.client.task_set.branch', + 'property.branch', + ]) + expected = { + 'property': { + 'client': { + 'clientgroup': {}, + 'task_set': { + 'branch': {}, + }, + }, + 'branch': {}, + }, + } + assert result == expected From e88c67fd33df74a1b5b4ad98596f6d09e2c514b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarek=20G=C5=82owacki?= Date: Thu, 22 Jul 2021 21:21:59 +1000 Subject: [PATCH 5/6] Add author --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 8d2af17d..0e22345b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,6 +11,7 @@ David Vogt Felix Viernickel Greg Aker Jamie Bliss +Jarek Głowacki Jason Housley Jeppe Fihl-Pearson Jerel Unruh From be5c46b6263d98b3a5c88fa6a99ba4f9eebe7a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarek=20G=C5=82owacki?= Date: Thu, 22 Jul 2021 22:27:23 +1000 Subject: [PATCH 6/6] Fix up undesired changes. --- docs/usage.md | 18 +++++++++++------- rest_framework_json_api/views.py | 9 +++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 719ec21e..e49d52b7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -226,8 +226,10 @@ from models import MyModel class MyViewset(ModelViewSet): queryset = MyModel.objects.all() serializer_class = MyModelSerializer - filter_backends = (filters.QueryParameterValidationFilter, filters.OrderingFilter, - django_filters.DjangoFilterBackend, SearchFilter) + filter_backends = ( + filters.QueryParameterValidationFilter, filters.OrderingFilter, + django_filters.DjangoFilterBackend, SearchFilter + ) filterset_fields = { 'id': ('exact', 'lt', 'gt', 'gte', 'lte', 'in'), 'descriptuon': ('icontains', 'iexact', 'contains'), @@ -387,7 +389,7 @@ Example without format conversion: ``` js { - "data": [{ + "data": [{ "type": "blog_identity", "id": "3", "attributes": { @@ -410,7 +412,7 @@ When set to dasherize: ``` js { - "data": [{ + "data": [{ "type": "blog-identity", "id": "3", "attributes": { @@ -436,7 +438,7 @@ Example without pluralization: ``` js { - "data": [{ + "data": [{ "type": "identity", "id": "3", "attributes": { @@ -459,7 +461,7 @@ When set to pluralize: ``` js { - "data": [{ + "data": [{ "type": "identities", "id": "3", "attributes": { @@ -940,10 +942,12 @@ class QuestSerializer(serializers.ModelSerializer): #### Performance improvements -Be aware that reverse relationships can be expensive to prepare. +Be aware that reverse relationships and M2Ms can be expensive to prepare. As a result, these are excluded by default unless explicitly demanded with sparsefieldsets. +You can opt out of this auto-exclusion with the `JSON_API_INCLUDE_EXPENSVE_FIELDS` setting. + An additional convenience DJA class exists for read-only views, just as it does in DRF. ```python diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index a8639b88..7e3cf3bb 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -6,7 +6,6 @@ from django.db.models.manager import Manager from django.db.models.query import QuerySet from django.urls import NoReverseMatch -from django.utils.module_loading import import_string as import_class_from_dotted_path from rest_framework import generics, viewsets from rest_framework.exceptions import MethodNotAllowed, NotFound from rest_framework.fields import get_attribute @@ -17,15 +16,15 @@ from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer -from rest_framework_json_api.utils import ( + +from .utils import ( Hyperlink, OrderedDict, get_resource_type_from_instance, includes_to_dict, undo_format_link_segment, ) - -from .utils.serializers import get_expensive_relational_fields +from .utils.serializers import add_nested_prefetches_to_qs class AutoPrefetchMixin(object): @@ -145,8 +144,6 @@ def get_related_serializer_class(self): False ), 'Either "included_serializers" or "related_serializers" should be configured' - if not isinstance(_class, type): - return import_class_from_dotted_path(_class) return _class return parent_serializer_class pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy