Commit c92123cc authored by Simon Charette's avatar Simon Charette
Browse files

Fixed #26226 -- Made related managers honor the queryset used for prefetching their results.

Thanks Loïc for the suggested improvements and Tim for the review.
parent 5d240b07
Loading
Loading
Loading
Loading
+9 −2
Original line number Diff line number Diff line
@@ -521,12 +521,19 @@ def create_generic_related_manager(superclass, rel):
        def __str__(self):
            return repr(self)

        def _apply_rel_filters(self, queryset):
            """
            Filter the queryset for the instance this manager is bound to.
            """
            db = self._db or router.db_for_read(self.model, instance=self.instance)
            return queryset.using(db).filter(**self.core_filters)

        def get_queryset(self):
            try:
                return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
            except (AttributeError, KeyError):
                db = self._db or router.db_for_read(self.model, instance=self.instance)
                return super(GenericRelatedObjectManager, self).get_queryset().using(db).filter(**self.core_filters)
                queryset = super(GenericRelatedObjectManager, self).get_queryset()
                return self._apply_rel_filters(queryset)

        def get_prefetch_queryset(self, instances, queryset=None):
            if queryset is None:
+30 −18
Original line number Diff line number Diff line
@@ -502,23 +502,29 @@ def create_reverse_many_to_one_manager(superclass, rel):
            return manager_class(self.instance)
        do_not_call_in_templates = True

        def get_queryset(self):
            try:
                return self.instance._prefetched_objects_cache[self.field.related_query_name()]
            except (AttributeError, KeyError):
        def _apply_rel_filters(self, queryset):
            """
            Filter the queryset for the instance this manager is bound to.
            """
            db = self._db or router.db_for_read(self.model, instance=self.instance)
            empty_strings_as_null = connections[db].features.interprets_empty_strings_as_nulls
                qs = super(RelatedManager, self).get_queryset()
                qs._add_hints(instance=self.instance)
            queryset._add_hints(instance=self.instance)
            if self._db:
                    qs = qs.using(self._db)
                qs = qs.filter(**self.core_filters)
                queryset = queryset.using(self._db)
            queryset = queryset.filter(**self.core_filters)
            for field in self.field.foreign_related_fields:
                val = getattr(self.instance, field.attname)
                if val is None or (val == '' and empty_strings_as_null):
                        return qs.none()
                qs._known_related_objects = {self.field: {self.instance.pk: self.instance}}
                return qs
                    return queryset.none()
            queryset._known_related_objects = {self.field: {self.instance.pk: self.instance}}
            return queryset

        def get_queryset(self):
            try:
                return self.instance._prefetched_objects_cache[self.field.related_query_name()]
            except (AttributeError, KeyError):
                queryset = super(RelatedManager, self).get_queryset()
                return self._apply_rel_filters(queryset)

        def get_prefetch_queryset(self, instances, queryset=None):
            if queryset is None:
@@ -776,15 +782,21 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
                filters |= symmetrical_filters
            return filters

        def _apply_rel_filters(self, queryset):
            """
            Filter the queryset for the instance this manager is bound to.
            """
            queryset._add_hints(instance=self.instance)
            if self._db:
                queryset = queryset.using(self._db)
            return queryset._next_is_sticky().filter(**self.core_filters)

        def get_queryset(self):
            try:
                return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
            except (AttributeError, KeyError):
                qs = super(ManyRelatedManager, self).get_queryset()
                qs._add_hints(instance=self.instance)
                if self._db:
                    qs = qs.using(self._db)
                return qs._next_is_sticky().filter(**self.core_filters)
                queryset = super(ManyRelatedManager, self).get_queryset()
                return self._apply_rel_filters(queryset)

        def get_prefetch_queryset(self, instances, queryset=None):
            if queryset is None:
+21 −2
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ from django.db.models.query_utils import (
)
from django.db.models.sql.constants import CURSOR
from django.utils import six, timezone
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.functional import partition
from django.utils.version import get_version

@@ -1608,6 +1609,9 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
            msg = 'to_attr={} conflicts with a field on the {} model.'
            raise ValueError(msg.format(to_attr, model.__name__))

    # Whether or not we're prefetching the last part of the lookup.
    leaf = len(lookup.prefetch_through.split(LOOKUP_SEP)) - 1 == level

    for obj in instances:
        instance_attr_val = instance_attr(obj)
        vals = rel_obj_cache.get(instance_attr_val, [])
@@ -1621,8 +1625,23 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
                setattr(obj, to_attr, vals)
                obj._prefetched_objects_cache[cache_name] = vals
            else:
                # Cache in the QuerySet.all().
                qs = getattr(obj, to_attr).all()
                manager = getattr(obj, to_attr)
                if leaf and lookup.queryset is not None:
                    try:
                        apply_rel_filter = manager._apply_rel_filters
                    except AttributeError:
                        warnings.warn(
                            "The `%s.%s` class must implement a `_apply_rel_filters()` "
                            "method that accepts a `QuerySet` as its single "
                            "argument and returns an appropriately filtered version "
                            "of it." % (manager.__class__.__module__, manager.__class__.__name__),
                            RemovedInDjango20Warning,
                        )
                        qs = manager.get_queryset()
                    else:
                        qs = apply_rel_filter(lookup.queryset)
                else:
                    qs = manager.get_queryset()
                qs._result_cache = vals
                # We don't want the individual qs doing prefetch_related now,
                # since we have merged this into the current work.
+3 −0
Original line number Diff line number Diff line
@@ -144,6 +144,9 @@ details on these changes.
* The ``__search`` query lookup and the
  ``DatabaseOperations.fulltext_search_sql()`` method will be removed.

* The shim for supporting custom related manager classes without a
  ``_apply_rel_filters()`` method will be removed.

.. _deprecation-removed-in-1.10:

1.10
+11 −0
Original line number Diff line number Diff line
@@ -762,6 +762,17 @@ features, is deprecated. Replace it with a custom lookup::
    models.CharField.register_lookup(Search)
    models.TextField.register_lookup(Search)

Custom manager classes available through ``prefetch_related`` must define a ``_apply_rel_filters()`` method
-----------------------------------------------------------------------------------------------------------

If you defined a custom manager class available through
:meth:`~django.db.models.query.QuerySet.prefetch_related` you must make sure
it defines a ``_apply_rel_filters()`` method.

This method must accept a :class:`~django.db.models.query.QuerySet` instance
as its single argument and return a filtered version of the queryset for the
model instance the manager is bound to.

Miscellaneous
-------------

Loading