Browse Source

Fixed #33651 -- Added support for prefetching GenericForeignKey.

Co-authored-by: revanthgss <revanthgss@almabase.com>
Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Clément Escolano 1 year ago
parent
commit
cac94dd8aa

+ 52 - 8
django/contrib/contenttypes/fields.py

@@ -1,5 +1,6 @@
 import functools
 import itertools
+import warnings
 from collections import defaultdict
 
 from asgiref.sync import sync_to_async
@@ -19,6 +20,7 @@ from django.db.models.query_utils import PathInfo
 from django.db.models.sql import AND
 from django.db.models.sql.where import WhereNode
 from django.db.models.utils import AltersData
+from django.utils.deprecation import RemovedInDjango60Warning
 from django.utils.functional import cached_property
 
 
@@ -163,20 +165,44 @@ class GenericForeignKey(FieldCacheMixin):
     def get_cache_name(self):
         return self.name
 
-    def get_content_type(self, obj=None, id=None, using=None):
+    def get_content_type(self, obj=None, id=None, using=None, model=None):
         if obj is not None:
             return ContentType.objects.db_manager(obj._state.db).get_for_model(
                 obj, for_concrete_model=self.for_concrete_model
             )
         elif id is not None:
             return ContentType.objects.db_manager(using).get_for_id(id)
+        elif model is not None:
+            return ContentType.objects.db_manager(using).get_for_model(
+                model, for_concrete_model=self.for_concrete_model
+            )
         else:
             # This should never happen. I love comments like this, don't you?
             raise Exception("Impossible arguments to GFK.get_content_type!")
 
     def get_prefetch_queryset(self, instances, queryset=None):
-        if queryset is not None:
-            raise ValueError("Custom queryset can't be used for this lookup.")
+        warnings.warn(
+            "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+            "instead.",
+            RemovedInDjango60Warning,
+            stacklevel=2,
+        )
+        if queryset is None:
+            return self.get_prefetch_querysets(instances)
+        return self.get_prefetch_querysets(instances, [queryset])
+
+    def get_prefetch_querysets(self, instances, querysets=None):
+        custom_queryset_dict = {}
+        if querysets is not None:
+            for queryset in querysets:
+                ct_id = self.get_content_type(
+                    model=queryset.query.model, using=queryset.db
+                ).pk
+                if ct_id in custom_queryset_dict:
+                    raise ValueError(
+                        "Only one queryset is allowed for each content type."
+                    )
+                custom_queryset_dict[ct_id] = queryset
 
         # For efficiency, group the instances by content type and then do one
         # query per model
@@ -195,9 +221,13 @@ class GenericForeignKey(FieldCacheMixin):
 
         ret_val = []
         for ct_id, fkeys in fk_dict.items():
-            instance = instance_dict[ct_id]
-            ct = self.get_content_type(id=ct_id, using=instance._state.db)
-            ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys))
+            if ct_id in custom_queryset_dict:
+                # Return values from the custom queryset, if provided.
+                ret_val.extend(custom_queryset_dict[ct_id].filter(pk__in=fkeys))
+            else:
+                instance = instance_dict[ct_id]
+                ct = self.get_content_type(id=ct_id, using=instance._state.db)
+                ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys))
 
         # For doing the join in Python, we have to match both the FK val and the
         # content type, so we use a callable that returns a (fk, class) pair.
@@ -616,9 +646,23 @@ def create_generic_related_manager(superclass, rel):
                 return self._apply_rel_filters(queryset)
 
         def get_prefetch_queryset(self, instances, queryset=None):
+            warnings.warn(
+                "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+                "instead.",
+                RemovedInDjango60Warning,
+                stacklevel=2,
+            )
             if queryset is None:
-                queryset = super().get_queryset()
-
+                return self.get_prefetch_querysets(instances)
+            return self.get_prefetch_querysets(instances, [queryset])
+
+        def get_prefetch_querysets(self, instances, querysets=None):
+            if querysets and len(querysets) != 1:
+                raise ValueError(
+                    "querysets argument of get_prefetch_querysets() should have a "
+                    "length of 1."
+                )
+            queryset = querysets[0] if querysets else super().get_queryset()
             queryset._add_hints(instance=instances[0])
             queryset = queryset.using(queryset._db or self._db)
             # Group instances by content types.

+ 36 - 0
django/contrib/contenttypes/prefetch.py

@@ -0,0 +1,36 @@
+from django.db.models import Prefetch
+from django.db.models.query import ModelIterable, RawQuerySet
+
+
+class GenericPrefetch(Prefetch):
+    def __init__(self, lookup, querysets=None, to_attr=None):
+        for queryset in querysets:
+            if queryset is not None and (
+                isinstance(queryset, RawQuerySet)
+                or (
+                    hasattr(queryset, "_iterable_class")
+                    and not issubclass(queryset._iterable_class, ModelIterable)
+                )
+            ):
+                raise ValueError(
+                    "Prefetch querysets cannot use raw(), values(), and values_list()."
+                )
+        self.querysets = querysets
+        super().__init__(lookup, to_attr=to_attr)
+
+    def __getstate__(self):
+        obj_dict = self.__dict__.copy()
+        obj_dict["querysets"] = []
+        for queryset in self.querysets:
+            if queryset is not None:
+                queryset = queryset._chain()
+                # Prevent the QuerySet from being evaluated
+                queryset._result_cache = []
+                queryset._prefetch_done = True
+                obj_dict["querysets"].append(queryset)
+        return obj_dict
+
+    def get_current_querysets(self, level):
+        if self.get_current_prefetch_to(level) == self.prefetch_to:
+            return self.querysets
+        return None

+ 64 - 4
django/db/models/fields/related_descriptors.py

@@ -62,6 +62,7 @@ and two directions (forward and reverse) for a total of six combinations.
    If you're looking for ``ForwardManyToManyDescriptor`` or
    ``ReverseManyToManyDescriptor``, use ``ManyToManyDescriptor`` instead.
 """
+import warnings
 
 from asgiref.sync import sync_to_async
 
@@ -79,6 +80,7 @@ from django.db.models.lookups import GreaterThan, LessThanOrEqual
 from django.db.models.query import QuerySet
 from django.db.models.query_utils import DeferredAttribute
 from django.db.models.utils import AltersData, resolve_callables
+from django.utils.deprecation import RemovedInDjango60Warning
 from django.utils.functional import cached_property
 
 
@@ -153,8 +155,23 @@ class ForwardManyToOneDescriptor:
         return self.field.remote_field.model._base_manager.db_manager(hints=hints).all()
 
     def get_prefetch_queryset(self, instances, queryset=None):
+        warnings.warn(
+            "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+            "instead.",
+            RemovedInDjango60Warning,
+            stacklevel=2,
+        )
         if queryset is None:
-            queryset = self.get_queryset()
+            return self.get_prefetch_querysets(instances)
+        return self.get_prefetch_querysets(instances, [queryset])
+
+    def get_prefetch_querysets(self, instances, querysets=None):
+        if querysets and len(querysets) != 1:
+            raise ValueError(
+                "querysets argument of get_prefetch_querysets() should have a length "
+                "of 1."
+            )
+        queryset = querysets[0] if querysets else self.get_queryset()
         queryset._add_hints(instance=instances[0])
 
         rel_obj_attr = self.field.get_foreign_related_value
@@ -427,8 +444,23 @@ class ReverseOneToOneDescriptor:
         return self.related.related_model._base_manager.db_manager(hints=hints).all()
 
     def get_prefetch_queryset(self, instances, queryset=None):
+        warnings.warn(
+            "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+            "instead.",
+            RemovedInDjango60Warning,
+            stacklevel=2,
+        )
         if queryset is None:
-            queryset = self.get_queryset()
+            return self.get_prefetch_querysets(instances)
+        return self.get_prefetch_querysets(instances, [queryset])
+
+    def get_prefetch_querysets(self, instances, querysets=None):
+        if querysets and len(querysets) != 1:
+            raise ValueError(
+                "querysets argument of get_prefetch_querysets() should have a length "
+                "of 1."
+            )
+        queryset = querysets[0] if querysets else self.get_queryset()
         queryset._add_hints(instance=instances[0])
 
         rel_obj_attr = self.related.field.get_local_related_value
@@ -728,9 +760,23 @@ def create_reverse_many_to_one_manager(superclass, rel):
                 return self._apply_rel_filters(queryset)
 
         def get_prefetch_queryset(self, instances, queryset=None):
+            warnings.warn(
+                "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+                "instead.",
+                RemovedInDjango60Warning,
+                stacklevel=2,
+            )
             if queryset is None:
-                queryset = super().get_queryset()
+                return self.get_prefetch_querysets(instances)
+            return self.get_prefetch_querysets(instances, [queryset])
 
+        def get_prefetch_querysets(self, instances, querysets=None):
+            if querysets and len(querysets) != 1:
+                raise ValueError(
+                    "querysets argument of get_prefetch_querysets() should have a "
+                    "length of 1."
+                )
+            queryset = querysets[0] if querysets else super().get_queryset()
             queryset._add_hints(instance=instances[0])
             queryset = queryset.using(queryset._db or self._db)
 
@@ -1087,9 +1133,23 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
                 return self._apply_rel_filters(queryset)
 
         def get_prefetch_queryset(self, instances, queryset=None):
+            warnings.warn(
+                "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+                "instead.",
+                RemovedInDjango60Warning,
+                stacklevel=2,
+            )
             if queryset is None:
-                queryset = super().get_queryset()
+                return self.get_prefetch_querysets(instances)
+            return self.get_prefetch_querysets(instances, [queryset])
 
+        def get_prefetch_querysets(self, instances, querysets=None):
+            if querysets and len(querysets) != 1:
+                raise ValueError(
+                    "querysets argument of get_prefetch_querysets() should have a "
+                    "length of 1."
+                )
+            queryset = querysets[0] if querysets else super().get_queryset()
             queryset._add_hints(instance=instances[0])
             queryset = queryset.using(queryset._db or self._db)
             queryset = _filter_prefetch_queryset(

+ 58 - 16
django/db/models/query.py

@@ -33,6 +33,7 @@ from django.db.models.utils import (
     resolve_callables,
 )
 from django.utils import timezone
+from django.utils.deprecation import RemovedInDjango60Warning
 from django.utils.functional import cached_property, partition
 
 # The maximum number of results to fetch in a get() query.
@@ -2236,8 +2237,21 @@ class Prefetch:
         return to_attr, as_attr
 
     def get_current_queryset(self, level):
-        if self.get_current_prefetch_to(level) == self.prefetch_to:
-            return self.queryset
+        warnings.warn(
+            "Prefetch.get_current_queryset() is deprecated. Use "
+            "get_current_querysets() instead.",
+            RemovedInDjango60Warning,
+            stacklevel=2,
+        )
+        querysets = self.get_current_querysets(level)
+        return querysets[0] if querysets is not None else None
+
+    def get_current_querysets(self, level):
+        if (
+            self.get_current_prefetch_to(level) == self.prefetch_to
+            and self.queryset is not None
+        ):
+            return [self.queryset]
         return None
 
     def __eq__(self, other):
@@ -2425,9 +2439,9 @@ async def aprefetch_related_objects(model_instances, *related_lookups):
 def get_prefetcher(instance, through_attr, to_attr):
     """
     For the attribute 'through_attr' on the given instance, find
-    an object that has a get_prefetch_queryset().
+    an object that has a get_prefetch_querysets().
     Return a 4 tuple containing:
-    (the object with get_prefetch_queryset (or None),
+    (the object with get_prefetch_querysets (or None),
      the descriptor object representing this relationship (or None),
      a boolean that is False if the attribute was not found at all,
      a function that takes an instance and returns a boolean that is True if
@@ -2462,8 +2476,12 @@ def get_prefetcher(instance, through_attr, to_attr):
         attr_found = True
         if rel_obj_descriptor:
             # singly related object, descriptor object has the
-            # get_prefetch_queryset() method.
-            if hasattr(rel_obj_descriptor, "get_prefetch_queryset"):
+            # get_prefetch_querysets() method.
+            if (
+                hasattr(rel_obj_descriptor, "get_prefetch_querysets")
+                # RemovedInDjango60Warning.
+                or hasattr(rel_obj_descriptor, "get_prefetch_queryset")
+            ):
                 prefetcher = rel_obj_descriptor
                 # If to_attr is set, check if the value has already been set,
                 # which is done with has_to_attr_attribute(). Do not use the
@@ -2476,7 +2494,11 @@ def get_prefetcher(instance, through_attr, to_attr):
                 # the attribute on the instance rather than the class to
                 # support many related managers
                 rel_obj = getattr(instance, through_attr)
-                if hasattr(rel_obj, "get_prefetch_queryset"):
+                if (
+                    hasattr(rel_obj, "get_prefetch_querysets")
+                    # RemovedInDjango60Warning.
+                    or hasattr(rel_obj, "get_prefetch_queryset")
+                ):
                     prefetcher = rel_obj
                 if through_attr == to_attr:
 
@@ -2497,7 +2519,7 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
     Return the prefetched objects along with any additional prefetches that
     must be done due to prefetch_related lookups found from default managers.
     """
-    # prefetcher must have a method get_prefetch_queryset() which takes a list
+    # prefetcher must have a method get_prefetch_querysets() which takes a list
     # of instances, and returns a tuple:
 
     # (queryset of instances of self.model that are related to passed in instances,
@@ -2510,14 +2532,34 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
     # The 'values to be matched' must be hashable as they will be used
     # in a dictionary.
 
-    (
-        rel_qs,
-        rel_obj_attr,
-        instance_attr,
-        single,
-        cache_name,
-        is_descriptor,
-    ) = prefetcher.get_prefetch_queryset(instances, lookup.get_current_queryset(level))
+    if hasattr(prefetcher, "get_prefetch_querysets"):
+        (
+            rel_qs,
+            rel_obj_attr,
+            instance_attr,
+            single,
+            cache_name,
+            is_descriptor,
+        ) = prefetcher.get_prefetch_querysets(
+            instances, lookup.get_current_querysets(level)
+        )
+    else:
+        warnings.warn(
+            "The usage of get_prefetch_queryset() in prefetch_related_objects() is "
+            "deprecated. Implement get_prefetch_querysets() instead.",
+            RemovedInDjango60Warning,
+            stacklevel=2,
+        )
+        (
+            rel_qs,
+            rel_obj_attr,
+            instance_attr,
+            single,
+            cache_name,
+            is_descriptor,
+        ) = prefetcher.get_prefetch_queryset(
+            instances, lookup.get_current_querysets(level)
+        )
     # We have to handle the possibility that the QuerySet we just got back
     # contains some prefetch_related lookups. We don't want to trigger the
     # prefetch_related functionality by evaluating the query. Rather, we need

+ 8 - 0
docs/internals/deprecation.txt

@@ -45,6 +45,14 @@ details on these changes.
 * The ``ChoicesMeta`` alias to ``django.db.models.enums.ChoicesType`` will be
   removed.
 
+* The ``Prefetch.get_current_queryset()`` method will be removed.
+
+* The ``get_prefetch_queryset()`` method of related managers and descriptors
+  will be removed.
+
+* ``get_prefetcher()`` and ``prefetch_related_objects()`` will no longer
+  fallback to ``get_prefetch_queryset()``.
+
 .. _deprecation-removed-in-5.1:
 
 5.1

+ 26 - 0
docs/ref/contrib/contenttypes.txt

@@ -590,3 +590,29 @@ information.
 
     Subclasses of :class:`GenericInlineModelAdmin` with stacked and tabular
     layouts, respectively.
+
+.. module:: django.contrib.contenttypes.prefetch
+
+``GenericPrefetch()``
+---------------------
+
+.. versionadded:: 5.0
+
+.. class:: GenericPrefetch(lookup, querysets=None, to_attr=None)
+
+This lookup is similar to ``Prefetch()`` and it should only be used on
+``GenericForeignKey``. The ``querysets`` argument accepts a list of querysets,
+each for a different ``ContentType``. This is useful for ``GenericForeignKey``
+with non-homogeneous set of results.
+
+.. code-block:: pycon
+
+    >>> bookmark = Bookmark.objects.create(url="https://www.djangoproject.com/")
+    >>> animal = Animal.objects.create(name="lion", weight=100)
+    >>> TaggedItem.objects.create(tag="great", content_object=bookmark)
+    >>> TaggedItem.objects.create(tag="awesome", content_object=animal)
+    >>> prefetch = GenericPrefetch(
+    ...     "content_object", [Bookmark.objects.all(), Animal.objects.only("name")]
+    ... )
+    >>> TaggedItem.objects.prefetch_related(prefetch).all()
+    <QuerySet [<TaggedItem: Great>, <TaggedItem: Awesome>]>

+ 9 - 4
docs/ref/models/querysets.txt

@@ -1154,10 +1154,15 @@ many-to-many, many-to-one, and
 cannot be done using ``select_related``, in addition to the foreign key and
 one-to-one relationships that are supported by ``select_related``. It also
 supports prefetching of
-:class:`~django.contrib.contenttypes.fields.GenericForeignKey`, however, it
-must be restricted to a homogeneous set of results. For example, prefetching
-objects referenced by a ``GenericForeignKey`` is only supported if the query
-is restricted to one ``ContentType``.
+:class:`~django.contrib.contenttypes.fields.GenericForeignKey`, however, the
+queryset for each ``ContentType`` must be provided in the ``querysets``
+parameter of :class:`~django.contrib.contenttypes.prefetch.GenericPrefetch`.
+
+.. versionchanged:: 5.0
+
+    Support for prefetching
+    :class:`~django.contrib.contenttypes.fields.GenericForeignKey` with
+    non-homogeneous set of results was added.
 
 For example, suppose you have these models::
 

+ 11 - 1
docs/releases/5.0.txt

@@ -243,7 +243,9 @@ Minor features
 :mod:`django.contrib.contenttypes`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-* ...
+* :meth:`.QuerySet.prefetch_related` now supports prefetching
+  :class:`~django.contrib.contenttypes.fields.GenericForeignKey` with
+  non-homogeneous set of results.
 
 :mod:`django.contrib.gis`
 ~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -711,6 +713,14 @@ Miscellaneous
 * The ``django.db.models.enums.ChoicesMeta`` metaclass is renamed to
   ``ChoicesType``.
 
+* The ``Prefetch.get_current_queryset()`` method is deprecated.
+
+* The ``get_prefetch_queryset()`` method of related managers and descriptors
+  is deprecated. Starting with Django 6.0, ``get_prefetcher()`` and
+  ``prefetch_related_objects()`` will no longer fallback to
+  ``get_prefetch_queryset()``. Subclasses should implement
+  ``get_prefetch_querysets()`` instead.
+
 .. _`oracledb`: https://oracle.github.io/python-oracledb/
 
 Features removed in 5.0

+ 54 - 8
tests/contenttypes_tests/test_fields.py

@@ -1,9 +1,11 @@
 import json
 
 from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.prefetch import GenericPrefetch
 from django.db import models
 from django.test import TestCase
 from django.test.utils import isolate_apps
+from django.utils.deprecation import RemovedInDjango60Warning
 
 from .models import Answer, Post, Question
 
@@ -22,14 +24,6 @@ class GenericForeignKeyTests(TestCase):
         ):
             Answer.question.get_content_type()
 
-    def test_incorrect_get_prefetch_queryset_arguments(self):
-        with self.assertRaisesMessage(
-            ValueError, "Custom queryset can't be used for this lookup."
-        ):
-            Answer.question.get_prefetch_queryset(
-                Answer.objects.all(), Answer.objects.all()
-            )
-
     def test_get_object_cache_respects_deleted_objects(self):
         question = Question.objects.create(text="Who?")
         post = Post.objects.create(title="Answer", parent=question)
@@ -59,3 +53,55 @@ class GenericRelationTests(TestCase):
         answer2 = Answer.objects.create(question=question)
         result = json.loads(Question.answer_set.field.value_to_string(question))
         self.assertCountEqual(result, [answer1.pk, answer2.pk])
+
+
+class GetPrefetchQuerySetDeprecation(TestCase):
+    def test_generic_relation_warning(self):
+        Question.objects.create(text="test")
+        questions = Question.objects.all()
+        msg = (
+            "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+            "instead."
+        )
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            questions[0].answer_set.get_prefetch_queryset(questions)
+
+    def test_generic_foreign_key_warning(self):
+        answers = Answer.objects.all()
+        msg = (
+            "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+            "instead."
+        )
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            Answer.question.get_prefetch_queryset(answers)
+
+
+class GetPrefetchQuerySetsTests(TestCase):
+    def test_duplicate_querysets(self):
+        question = Question.objects.create(text="What is your name?")
+        answer = Answer.objects.create(text="Joe", question=question)
+        answer = Answer.objects.get(pk=answer.pk)
+        msg = "Only one queryset is allowed for each content type."
+        with self.assertRaisesMessage(ValueError, msg):
+            models.prefetch_related_objects(
+                [answer],
+                GenericPrefetch(
+                    "question",
+                    [
+                        Question.objects.all(),
+                        Question.objects.filter(text__startswith="test"),
+                    ],
+                ),
+            )
+
+    def test_generic_relation_invalid_length(self):
+        Question.objects.create(text="test")
+        questions = Question.objects.all()
+        msg = (
+            "querysets argument of get_prefetch_querysets() should have a length of 1."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            questions[0].answer_set.get_prefetch_querysets(
+                instances=questions,
+                querysets=[Answer.objects.all(), Question.objects.all()],
+            )

+ 15 - 0
tests/contenttypes_tests/test_models.py

@@ -1,5 +1,6 @@
 from django.apps import apps
 from django.contrib.contenttypes.models import ContentType, ContentTypeManager
+from django.contrib.contenttypes.prefetch import GenericPrefetch
 from django.db import models
 from django.db.migrations.state import ProjectState
 from django.test import TestCase, override_settings
@@ -328,3 +329,17 @@ class ContentTypesMultidbTests(TestCase):
             1, using="other"
         ):
             ContentType.objects.get_for_model(Author)
+
+
+class GenericPrefetchTests(TestCase):
+    def test_values_queryset(self):
+        msg = "Prefetch querysets cannot use raw(), values(), and values_list()."
+        with self.assertRaisesMessage(ValueError, msg):
+            GenericPrefetch("question", [Author.objects.values("pk")])
+        with self.assertRaisesMessage(ValueError, msg):
+            GenericPrefetch("question", [Author.objects.values_list("pk")])
+
+    def test_raw_queryset(self):
+        msg = "Prefetch querysets cannot use raw(), values(), and values_list()."
+        with self.assertRaisesMessage(ValueError, msg):
+            GenericPrefetch("question", [Author.objects.raw("select pk from author")])

+ 34 - 1
tests/generic_relations/tests.py

@@ -1,6 +1,7 @@
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.prefetch import GenericPrefetch
 from django.core.exceptions import FieldError
-from django.db.models import Q
+from django.db.models import Q, prefetch_related_objects
 from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
 
 from .models import (
@@ -747,6 +748,38 @@ class GenericRelationsTests(TestCase):
                 comparison.first_obj.comparisons.all(), [comparison]
             )
 
+    def test_generic_prefetch(self):
+        tagged_vegetable = TaggedItem.objects.create(
+            tag="great", content_object=self.bacon
+        )
+        tagged_animal = TaggedItem.objects.create(
+            tag="awesome", content_object=self.platypus
+        )
+        # Getting the instances again so that content object is deferred.
+        tagged_vegetable = TaggedItem.objects.get(pk=tagged_vegetable.pk)
+        tagged_animal = TaggedItem.objects.get(pk=tagged_animal.pk)
+
+        with self.assertNumQueries(2):
+            prefetch_related_objects(
+                [tagged_vegetable, tagged_animal],
+                GenericPrefetch(
+                    "content_object",
+                    [Vegetable.objects.all(), Animal.objects.only("common_name")],
+                ),
+            )
+        with self.assertNumQueries(0):
+            self.assertEqual(tagged_vegetable.content_object.name, self.bacon.name)
+        with self.assertNumQueries(0):
+            self.assertEqual(
+                tagged_animal.content_object.common_name,
+                self.platypus.common_name,
+            )
+        with self.assertNumQueries(1):
+            self.assertEqual(
+                tagged_animal.content_object.latin_name,
+                self.platypus.latin_name,
+            )
+
 
 class ProxyRelatedModelTest(TestCase):
     def test_default_behavior(self):

+ 21 - 0
tests/many_to_many/tests.py

@@ -2,6 +2,7 @@ from unittest import mock
 
 from django.db import transaction
 from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
+from django.utils.deprecation import RemovedInDjango60Warning
 
 from .models import Article, InheritedArticleA, InheritedArticleB, Publication, User
 
@@ -561,3 +562,23 @@ class ManyToManyTests(TestCase):
         self.assertEqual(
             self.p3.article_set.exists(), self.p3.article_set.all().exists()
         )
+
+    def test_get_prefetch_queryset_warning(self):
+        articles = Article.objects.all()
+        msg = (
+            "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+            "instead."
+        )
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            self.a1.publications.get_prefetch_queryset(articles)
+
+    def test_get_prefetch_querysets_invalid_querysets_length(self):
+        articles = Article.objects.all()
+        msg = (
+            "querysets argument of get_prefetch_querysets() should have a length of 1."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            self.a1.publications.get_prefetch_querysets(
+                instances=articles,
+                querysets=[Publication.objects.all(), Publication.objects.all()],
+            )

+ 47 - 0
tests/many_to_one/tests.py

@@ -4,6 +4,7 @@ from copy import deepcopy
 from django.core.exceptions import FieldError, MultipleObjectsReturned
 from django.db import IntegrityError, models, transaction
 from django.test import TestCase
+from django.utils.deprecation import RemovedInDjango60Warning
 from django.utils.translation import gettext_lazy
 
 from .models import (
@@ -877,3 +878,49 @@ class ManyToOneTests(TestCase):
             usa.cities.remove(chicago.pk)
         with self.assertRaisesMessage(TypeError, msg):
             usa.cities.set([chicago.pk])
+
+    def test_get_prefetch_queryset_warning(self):
+        City.objects.create(name="Chicago")
+        cities = City.objects.all()
+        msg = (
+            "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+            "instead."
+        )
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            City.country.get_prefetch_queryset(cities)
+
+    def test_get_prefetch_queryset_reverse_warning(self):
+        usa = Country.objects.create(name="United States")
+        City.objects.create(name="Chicago")
+        countries = Country.objects.all()
+        msg = (
+            "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+            "instead."
+        )
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            usa.cities.get_prefetch_queryset(countries)
+
+    def test_get_prefetch_querysets_invalid_querysets_length(self):
+        City.objects.create(name="Chicago")
+        cities = City.objects.all()
+        msg = (
+            "querysets argument of get_prefetch_querysets() should have a length of 1."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            City.country.get_prefetch_querysets(
+                instances=cities,
+                querysets=[Country.objects.all(), Country.objects.all()],
+            )
+
+    def test_get_prefetch_querysets_reverse_invalid_querysets_length(self):
+        usa = Country.objects.create(name="United States")
+        City.objects.create(name="Chicago")
+        countries = Country.objects.all()
+        msg = (
+            "querysets argument of get_prefetch_querysets() should have a length of 1."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            usa.cities.get_prefetch_querysets(
+                instances=countries,
+                querysets=[City.objects.all(), City.objects.all()],
+            )

+ 21 - 0
tests/one_to_one/tests.py

@@ -1,5 +1,6 @@
 from django.db import IntegrityError, connection, transaction
 from django.test import TestCase
+from django.utils.deprecation import RemovedInDjango60Warning
 
 from .models import (
     Bar,
@@ -606,3 +607,23 @@ class OneToOneTests(TestCase):
         self.b1.place_id = self.p2.pk
         self.b1.save()
         self.assertEqual(self.b1.place, self.p2)
+
+    def test_get_prefetch_queryset_warning(self):
+        places = Place.objects.all()
+        msg = (
+            "get_prefetch_queryset() is deprecated. Use get_prefetch_querysets() "
+            "instead."
+        )
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            Place.bar.get_prefetch_queryset(places)
+
+    def test_get_prefetch_querysets_invalid_querysets_length(self):
+        places = Place.objects.all()
+        msg = (
+            "querysets argument of get_prefetch_querysets() should have a length of 1."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            Place.bar.get_prefetch_querysets(
+                instances=places,
+                querysets=[Bar.objects.all(), Bar.objects.all()],
+            )

+ 18 - 1
tests/prefetch_related/tests.py

@@ -13,6 +13,7 @@ from django.test import (
     skipUnlessDBFeature,
 )
 from django.test.utils import CaptureQueriesContext
+from django.utils.deprecation import RemovedInDjango60Warning
 
 from .models import (
     Article,
@@ -1696,7 +1697,7 @@ class Ticket21760Tests(TestCase):
 
     def test_bug(self):
         prefetcher = get_prefetcher(self.rooms[0], "house", "house")[0]
-        queryset = prefetcher.get_prefetch_queryset(list(Room.objects.all()))[0]
+        queryset = prefetcher.get_prefetch_querysets(list(Room.objects.all()))[0]
         self.assertNotIn(" JOIN ", str(queryset.query))
 
 
@@ -1994,3 +1995,19 @@ class PrefetchLimitTests(TestDataMixin, TestCase):
         )
         with self.assertRaisesMessage(NotSupportedError, msg):
             list(Book.objects.prefetch_related(Prefetch("authors", authors[1:])))
+
+
+class GetCurrentQuerySetDeprecation(TestCase):
+    def test_get_current_queryset_warning(self):
+        msg = (
+            "Prefetch.get_current_queryset() is deprecated. Use "
+            "get_current_querysets() instead."
+        )
+        authors = Author.objects.all()
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            self.assertEqual(
+                Prefetch("authors", authors).get_current_queryset(1),
+                authors,
+            )
+        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
+            self.assertIsNone(Prefetch("authors").get_current_queryset(1))