Ticket #17003: prefetch_singly_related_objs.2.diff

File prefetch_singly_related_objs.2.diff, 31.7 KB (added by Luke Plant, 6 years ago)

Added support for reverse one-to-one, which had been forgotten, other cleanups

  • django/contrib/contenttypes/generic.py

    diff -r 554e071b5c4a django/contrib/contenttypes/generic.py
    a b  
    22Classes allowing "generic" relations through ContentType and object-id fields.
    33"""
    44
     5from collections import defaultdict
    56from functools import partial
     7
    68from django.core.exceptions import ObjectDoesNotExist
    79from django.db import connection
    810from django.db.models import signals
     
    5961            # This should never happen. I love comments like this, don't you?
    6062            raise Exception("Impossible arguments to GFK.get_content_type!")
    6163
     64    def get_prefetch_query_set(self, instances):
     65        # For efficiency, group the instances by content type and then do one
     66        # query per model
     67        fk_dict = defaultdict(list)
     68        # We need one instance for each group in order to get the right db:
     69        instance_dict = {}
     70        ct_attname = self.model._meta.get_field(self.ct_field).get_attname()
     71        for instance in instances:
     72            # We avoid looking for values if either ct_id or fkey value is None
     73            ct_id = getattr(instance, ct_attname)
     74            if ct_id is not None:
     75                fk_val = getattr(instance, self.fk_field)
     76                if fk_val is not None:
     77                    fk_dict[ct_id].append(fk_val)
     78                    instance_dict[ct_id] = instance
     79
     80        ret_val = []
     81        for ct_id, fkeys in fk_dict.items():
     82            instance = instance_dict[ct_id]
     83            ct = self.get_content_type(id=ct_id, using=instance._state.db)
     84            ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys))
     85
     86        # For doing the join in Python, we have to match both the FK val and the
     87        # content type, so the 'attr' vals we return need to be callables that
     88        # will return a (fk, class) pair.
     89        return (ret_val,
     90                lambda obj: (obj.pk, obj.__class__),
     91                lambda obj: (getattr(obj, self.fk_field),
     92                             self.get_content_type(id=getattr(obj, ct_attname),
     93                                                   using=obj._state.db).model_class()),
     94                True,
     95                self.cache_attr)
     96
     97    def is_cached(self, instance):
     98        return hasattr(instance, self.cache_attr)
     99
    62100    def __get__(self, instance, instance_type=None):
    63101        if instance is None:
    64102            return self
     
    282320                    [obj._get_pk_val() for obj in instances]
    283321                }
    284322            qs = super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**query)
    285             return (qs, self.object_id_field_name, 'pk')
     323            return (qs,
     324                    self.object_id_field_name,
     325                    lambda obj: obj._get_pk_val(),
     326                    False,
     327                    self.prefetch_cache_name)
    286328
    287329        def add(self, *objs):
    288330            for obj in objs:
  • django/contrib/contenttypes/models.py

    diff -r 554e071b5c4a django/contrib/contenttypes/models.py
    a b  
    113113        """
    114114        return self.model_class()._base_manager.using(self._state.db).get(**kwargs)
    115115
     116    def get_all_objects_for_this_type(self, **kwargs):
     117        """
     118        Returns all objects of this type for the keyword arguments given.
     119        """
     120        return self.model_class()._base_manager.using(self._state.db).filter(**kwargs)
     121
    116122    def natural_key(self):
    117123        return (self.app_label, self.model)
  • django/db/models/fields/related.py

    diff -r 554e071b5c4a django/db/models/fields/related.py
    a b  
    227227        self.related = related
    228228        self.cache_name = related.get_cache_name()
    229229
     230    def is_cached(self, instance):
     231        return hasattr(instance, self.cache_name)
     232
     233    def get_query_set(self, **db_hints):
     234        db = router.db_for_read(self.related.model, **db_hints)
     235        return self.related.model._base_manager.using(db)
     236
     237    def get_prefetch_query_set(self, instances):
     238        vals = [instance._get_pk_val() for instance in instances]
     239        params = {'%s__pk__in' % self.related.field.name: vals}
     240        return (self.get_query_set(),
     241                self.related.field.attname,
     242                lambda obj: obj._get_pk_val(),
     243                True,
     244                self.cache_name)
     245
    230246    def __get__(self, instance, instance_type=None):
    231247        if instance is None:
    232248            return self
     
    234250            return getattr(instance, self.cache_name)
    235251        except AttributeError:
    236252            params = {'%s__pk' % self.related.field.name: instance._get_pk_val()}
    237             db = router.db_for_read(self.related.model, instance=instance)
    238             rel_obj = self.related.model._base_manager.using(db).get(**params)
     253            rel_obj = self.get_query_set(instance=instance).get(**params)
    239254            setattr(instance, self.cache_name, rel_obj)
    240255            return rel_obj
    241256
     
    283298    # ReverseSingleRelatedObjectDescriptor instance.
    284299    def __init__(self, field_with_rel):
    285300        self.field = field_with_rel
     301        self.cache_name = self.field.get_cache_name()
     302
     303    def is_cached(self, instance):
     304        return hasattr(instance, self.cache_name)
     305
     306    def get_query_set(self, **db_hints):
     307        db = router.db_for_read(self.field.rel.to, **db_hints)
     308        rel_mgr = self.field.rel.to._default_manager
     309        # If the related manager indicates that it should be used for
     310        # related fields, respect that.
     311        if getattr(rel_mgr, 'use_for_related_fields', False):
     312            return rel_mgr.using(db)
     313        else:
     314            return QuerySet(self.field.rel.to).using(db)
     315
     316    def get_prefetch_query_set(self, instances):
     317        vals = [getattr(instance, self.field.attname) for instance in instances]
     318        other_field = self.field.rel.get_related_field()
     319        if other_field.rel:
     320            params = {'%s__pk__in' % self.field.rel.field_name: vals}
     321        else:
     322            params = {'%s__in' % self.field.rel.field_name: vals}
     323        return (self.get_query_set().filter(**params),
     324                self.field.rel.field_name,
     325                self.field.attname,
     326                True,
     327                self.cache_name)
    286328
    287329    def __get__(self, instance, instance_type=None):
    288330        if instance is None:
    289331            return self
    290332
    291         cache_name = self.field.get_cache_name()
    292333        try:
    293             return getattr(instance, cache_name)
     334            return getattr(instance, self.cache_name)
    294335        except AttributeError:
    295336            val = getattr(instance, self.field.attname)
    296337            if val is None:
     
    303344                params = {'%s__pk' % self.field.rel.field_name: val}
    304345            else:
    305346                params = {'%s__exact' % self.field.rel.field_name: val}
    306 
    307             # If the related manager indicates that it should be used for
    308             # related fields, respect that.
    309             rel_mgr = self.field.rel.to._default_manager
    310             db = router.db_for_read(self.field.rel.to, instance=instance)
    311             if getattr(rel_mgr, 'use_for_related_fields', False):
    312                 rel_obj = rel_mgr.using(db).get(**params)
    313             else:
    314                 rel_obj = QuerySet(self.field.rel.to).using(db).get(**params)
    315             setattr(instance, cache_name, rel_obj)
     347            qs = self.get_query_set(instance=instance)
     348            rel_obj = qs.get(**params)
     349            setattr(instance, self.cache_name, rel_obj)
    316350            return rel_obj
    317351
    318352    def __set__(self, instance, value):
     
    425459                    return super(RelatedManager, self).get_query_set().using(db).filter(**self.core_filters)
    426460
    427461            def get_prefetch_query_set(self, instances):
    428                 """
    429                 Return a queryset that does the bulk lookup needed
    430                 by prefetch_related functionality.
    431                 """
    432462                db = self._db or router.db_for_read(self.model)
    433463                query = {'%s__%s__in' % (rel_field.name, attname):
    434464                             [getattr(obj, attname) for obj in instances]}
    435465                qs = super(RelatedManager, self).get_query_set().using(db).filter(**query)
    436                 return (qs, rel_field.get_attname(), attname)
     466                return (qs, rel_field.get_attname(), attname,
     467                        False, rel_field.related_query_name())
    437468
    438469            def add(self, *objs):
    439470                for obj in objs:
     
    507538                return super(ManyRelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**self.core_filters)
    508539
    509540        def get_prefetch_query_set(self, instances):
    510             """
    511             Returns a tuple:
    512             (queryset of instances of self.model that are related to passed in instances
    513              attr of returned instances needed for matching
    514              attr of passed in instances needed for matching)
    515             """
    516541            from django.db import connections
    517542            db = self._db or router.db_for_read(self.model)
    518543            query = {'%s__pk__in' % self.query_field_name:
     
    534559            qs = qs.extra(select={'_prefetch_related_val':
    535560                                      '%s.%s' % (qn(join_table), qn(source_col))})
    536561            select_attname = fk.rel.get_related_field().get_attname()
    537             return (qs, '_prefetch_related_val', select_attname)
     562            return (qs, '_prefetch_related_val', select_attname,
     563                    False, self.prefetch_cache_name)
    538564
    539565        # If the ManyToMany relation has an intermediary model,
    540566        # the add and remove methods do not exist.
  • django/db/models/query.py

    diff -r 554e071b5c4a django/db/models/query.py
    a b  
    16121612                break
    16131613
    16141614            # Descend down tree
    1615             try:
    1616                 rel_obj = getattr(obj_list[0], attr)
    1617             except AttributeError:
     1615
     1616            # We assume that objects retrieved are homogenous (which is the premise
     1617            # of prefetch_related), so what applies to first object applies to all.
     1618            first_obj = obj_list[0]
     1619            prefetcher, attr_found, is_fetched = get_prefetcher(first_obj, attr)
     1620
     1621            if not attr_found:
    16181622                raise AttributeError("Cannot find '%s' on %s object, '%s' is an invalid "
    16191623                                     "parameter to prefetch_related()" %
    1620                                      (attr, obj_list[0].__class__.__name__, lookup))
     1624                                     (attr, first_obj.__class__.__name__, lookup))
    16211625
    1622             can_prefetch = hasattr(rel_obj, 'get_prefetch_query_set')
    1623             if level == len(attrs) - 1 and not can_prefetch:
    1624                 # Last one, this *must* resolve to a related manager.
    1625                 raise ValueError("'%s' does not resolve to a supported 'many related"
    1626                                  " manager' for model %s - this is an invalid"
    1627                                  " parameter to prefetch_related()."
    1628                                  % (lookup, model.__name__))
     1626            if level == len(attrs) - 1 and prefetcher is None:
     1627                # Last one, this *must* resolve to something that supports
     1628                # prefetching, otherwise there is no point adding it and the
     1629                # developer asking for it has made a mistake.
     1630                raise ValueError("'%s' does not resolve to a item that supports "
     1631                                 "prefetching - this is an invalid parameter to "
     1632                                 "prefetch_related()." % lookup)
    16291633
    1630             if can_prefetch:
     1634            if prefetcher is not None and not is_fetched:
    16311635                # Check we didn't do this already
    16321636                current_lookup = LOOKUP_SEP.join(attrs[0:level+1])
    16331637                if current_lookup in done_queries:
    16341638                    obj_list = done_queries[current_lookup]
    16351639                else:
    1636                     relmanager = rel_obj
    1637                     obj_list, additional_prl = prefetch_one_level(obj_list, relmanager, attr)
     1640                    obj_list, additional_prl = prefetch_one_level(obj_list, prefetcher, attr)
    16381641                    for f in additional_prl:
    16391642                        new_prl = LOOKUP_SEP.join([current_lookup, f])
    16401643                        related_lookups.append(new_prl)
    16411644                    done_queries[current_lookup] = obj_list
    16421645            else:
    1643                 # Assume we've got some singly related object. We replace
    1644                 # the current list of parent objects with that list.
     1646                # Either a singly related object that has already been fetched
     1647                # (e.g. via select_related), or hopefully some other property
     1648                # that doesn't support prefetching but needs to be traversed.
     1649
     1650                # We replace the current list of parent objects with that list.
    16451651                obj_list = [getattr(obj, attr) for obj in obj_list]
    16461652
    16471653                # Filter out 'None' so that we can continue with nullable
     
    16491655                obj_list = [obj for obj in obj_list if obj is not None]
    16501656
    16511657
    1652 def prefetch_one_level(instances, relmanager, attname):
     1658def get_prefetcher(instance, attr):
     1659    """
     1660    For the attribute 'attr' on the given instance, finds
     1661    an object that has a get_prefetch_query_set().
     1662    Return a 3 tuple containing:
     1663    (the object with get_prefetch_query_set (or None),
     1664     a boolean that is False if the attribute was not found at all,
     1665     a boolean that is True if the attribute has already been fetched)
     1666    """
     1667    prefetcher = None
     1668    attr_found = False
     1669    is_fetched = False
     1670
     1671    # For singly related objects, we have to avoid getting the attribute
     1672    # from the object, as this will trigger the query. So we first try
     1673    # on the class, in order to get the descriptor object.
     1674    rel_obj_descriptor = getattr(instance.__class__, attr, None)
     1675    if rel_obj_descriptor is None:
     1676        try:
     1677            rel_obj = getattr(instance, attr)
     1678            attr_found = True
     1679        except AttributeError:
     1680            pass
     1681    else:
     1682        attr_found = True
     1683        if rel_obj_descriptor:
     1684            # singly related object, descriptor object has the
     1685            # get_prefetch_query_set() method.
     1686            if hasattr(rel_obj_descriptor, 'get_prefetch_query_set'):
     1687                prefetcher = rel_obj_descriptor
     1688                if rel_obj_descriptor.is_cached(instance):
     1689                    is_fetched = True
     1690            else:
     1691                # descriptor doesn't support prefetching, so we go ahead and get
     1692                # the attribute on the instance rather than the class to
     1693                # support many related managers
     1694                rel_obj = getattr(instance, attr)
     1695                if hasattr(rel_obj, 'get_prefetch_query_set'):
     1696                    prefetcher = rel_obj
     1697    return prefetcher, attr_found, is_fetched
     1698
     1699
     1700def prefetch_one_level(instances, prefetcher, attname):
    16531701    """
    16541702    Helper function for prefetch_related_objects
    16551703
    1656     Runs prefetches on all instances using the manager relmanager,
    1657     assigning results to queryset against instance.attname.
     1704    Runs prefetches on all instances using the prefetcher object,
     1705    assigning results to relevant caches in instance.
    16581706
    16591707    The prefetched objects are returned, along with any additional
    16601708    prefetches that must be done due to prefetch_related lookups
    16611709    found from default managers.
    16621710    """
    1663     rel_qs, rel_obj_attr, instance_attr = relmanager.get_prefetch_query_set(instances)
     1711    # prefetcher must have a method get_prefetch_query_set() which takes a list
     1712    # of instances, and returns a tuple:
     1713
     1714    # (queryset of instances of self.model that are related to passed in instances,
     1715    #  attr of returned instances needed for matching,
     1716    #  attr of passed in instances needed for matching,
     1717    #  boolean that is True for singly related objects,
     1718    #  cache name to assign to).
     1719
     1720    # The 'attr' return values can also be callables which return the values
     1721    # needed, if a simple 'getattr(obj, attr)' will not suffice, and such
     1722    # callables could return a compound value, as long as it can be used as a
     1723    # dictionary key.
     1724
     1725    rel_qs, rel_obj_attr, instance_attr, single, cache_name =\
     1726        prefetcher.get_prefetch_query_set(instances)
    16641727    # We have to handle the possibility that the default manager itself added
    16651728    # prefetch_related lookups to the QuerySet we just got back. We don't want to
    16661729    # trigger the prefetch_related functionality by evaluating the query.
     
    16761739
    16771740    rel_obj_cache = {}
    16781741    for rel_obj in all_related_objects:
    1679         rel_attr_val = getattr(rel_obj, rel_obj_attr)
     1742        if callable(rel_obj_attr):
     1743            rel_attr_val = rel_obj_attr(rel_obj)
     1744        else:
     1745            rel_attr_val = getattr(rel_obj, rel_obj_attr)
    16801746        if rel_attr_val not in rel_obj_cache:
    16811747            rel_obj_cache[rel_attr_val] = []
    16821748        rel_obj_cache[rel_attr_val].append(rel_obj)
    16831749
    16841750    for obj in instances:
    1685         qs = getattr(obj, attname).all()
    1686         instance_attr_val = getattr(obj, instance_attr)
    1687         qs._result_cache = rel_obj_cache.get(instance_attr_val, [])
    1688         # We don't want the individual qs doing prefetch_related now, since we
    1689         # have merged this into the current work.
    1690         qs._prefetch_done = True
    1691         obj._prefetched_objects_cache[attname] = qs
     1751        if callable(instance_attr):
     1752            instance_attr_val = instance_attr(obj)
     1753        else:
     1754            instance_attr_val = getattr(obj, instance_attr)
     1755        vals = rel_obj_cache.get(instance_attr_val, [])
     1756        if single:
     1757            # Need to assign to single cache on instance
     1758            if vals:
     1759                setattr(obj, cache_name, vals[0])
     1760        else:
     1761            # Multi, attribute represents a manager with an .all() method that
     1762            # returns a QuerySet
     1763            qs = getattr(obj, attname).all()
     1764            qs._result_cache = vals
     1765            # We don't want the individual qs doing prefetch_related now, since we
     1766            # have merged this into the current work.
     1767            qs._prefetch_done = True
     1768            obj._prefetched_objects_cache[cache_name] = qs
    16921769    return all_related_objects, additional_prl
  • docs/ref/models/querysets.txt

    diff -r 554e071b5c4a docs/ref/models/querysets.txt
    a b  
    696696.. versionadded:: 1.4
    697697
    698698Returns a ``QuerySet`` that will automatically retrieve, in a single batch,
    699 related many-to-many and many-to-one objects for each of the specified lookups.
    700 
    701 This is similar to ``select_related`` for the 'many related objects' case, but
    702 note that ``prefetch_related`` causes a separate query to be issued for each set
    703 of related objects that you request, unlike ``select_related`` which modifies
    704 the original query with joins in order to get the related objects. With
    705 ``prefetch_related``, the additional queries are done as soon as the QuerySet
    706 begins to be evaluated.
     699related objects for each of the specified lookups.
     700
     701This has a similar purpose to ``select_related``, in that both are designed to
     702stop accessing related objects causing a deluge of database queries, but the
     703strategy is quite different.
     704
     705``select_related`` works by creating a SQL join and including the fields of the
     706related object in the SELECT statement. For this reason, ``select_related`` gets
     707the related object in the same database query. However, to avoid the much larger
     708result set that would result from joining across a 'many' relationship,
     709``select_related`` is limited to the single-valued foreign key and one-to-one
     710relationships.
     711
     712``prefetch_related``, on the other hand, does a separate lookup for each
     713relationship, and does the 'joining' in Python. This allows it to prefetch
     714many-to-many and many-to-one objects, which cannot be done using
     715``select_related``, in addition to the foreign key and one-to-one relationships
     716that are supported by ``select_related``. It also supports prefetching of
     717``GenericRelations`` and ``GenericForeignKey``.
    707718
    708719For example, suppose you have these models::
    709720
     
    733744``QuerySets`` that have a pre-filled cache of the relevant results. These
    734745``QuerySets`` are then used in the ``self.toppings.all()`` calls.
    735746
    736 Please note that use of ``prefetch_related`` will mean that the additional
    737 queries run will **always** be executed - even if you never use the related
    738 objects - and it always fully populates the result cache on the primary
    739 ``QuerySet`` (which can sometimes be avoided in other cases).
     747The additional queries are executed after the QuerySet has begun to be evaluated
     748and the primary query has been executed. Note that the result cache of the
     749primary QuerySet and all specified related objects will then be fully loaded
     750into memory, which is often avoided in other cases - even after a query has been
     751executed in the database, QuerySet normally tries to make uses of chunking
     752between the database to avoid loading all objects into memory before you need
     753them.
    740754
    741755Also remember that, as always with QuerySets, any subsequent chained methods
    742 will ignore previously cached results, and retrieve data using a fresh database
    743 query. So, if you write the following:
     756which imply a different database query will ignore previously cached results,
     757and retrieve data using a fresh database query. So, if you write the following:
    744758
    745759    >>> pizzas = Pizza.objects.prefetch_related('toppings')
    746760    >>> [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]
     
    749763you - in fact it hurts performance, since you have done a database query that
    750764you haven't used. So use this feature with caution!
    751765
    752 The lookups that must be supplied to this method can be any attributes on the
    753 model instances which represent related queries that return multiple
    754 objects. This includes attributes representing the 'many' side of ``ForeignKey``
    755 relationships, forward and reverse ``ManyToManyField`` attributes, and also any
    756 ``GenericRelations``.
    757 
    758766You can also use the normal join syntax to do related fields of related
    759767fields. Suppose we have an additional model to the example above::
    760768
     
    770778belonging to those pizzas. This will result in a total of 3 database queries -
    771779one for the restaurants, one for the pizzas, and one for the toppings.
    772780
     781    >>> Restaurant.objects.prefetch_related('best_pizza__toppings')
     782
     783This will fetch the best pizza and all the toppings for the best pizza for each
     784restaurant. This will be done in 3 database queries - one for the restaurants,
     785one for the 'best pizzas', and one for one for the toppings.
     786
     787Of course, the ``best_pizza`` relationship could also be fetched using
     788``select_related`` to reduce the query count to 2:
     789
    773790    >>> Restaurant.objects.select_related('best_pizza').prefetch_related('best_pizza__toppings')
    774791
    775 This will fetch the best pizza and all the toppings for the best pizza for each
    776 restaurant. This will be done in 2 database queries - one for the restaurants
    777 and 'best pizzas' combined (achieved through use of ``select_related``), and one
    778 for the toppings.
     792Since the prefetch is executed after the main query (which includes the joins
     793needed by ``select_related``), it is able to detect that the ``best_pizza``
     794objects have already been fetched, and it will skip fetching them again.
    779795
    780796Chaining ``prefetch_related`` calls will accumulate the fields that should have
    781797this behavior applied. To clear any ``prefetch_related`` behavior, pass `None`
     
    783799
    784800   >>> non_prefetched = qs.prefetch_related(None)
    785801
    786 One difference when using ``prefetch_related`` is that, in some circumstances,
    787 objects created by a query can be shared between the different objects that they
    788 are related to i.e. a single Python model instance can appear at more than one
    789 point in the tree of objects that are returned. Normally this behavior will not
    790 be a problem, and will in fact save both memory and CPU time.
     802One difference to note when using ``prefetch_related`` is that objects created
     803by a query can be shared between the different objects that they are related to
     804i.e. a single Python model instance can appear at more than one point in the
     805tree of objects that are returned. This will be normally happens with foreign
     806key relationships. Normally this behavior will not be a problem, and will in
     807fact save both memory and CPU time.
     808
     809While ``prefetch_related`` supports prefetching ``GenericForeignKey``
     810relationships, the number of queries will depend on the data. Since a
     811``GenericForeignKey`` can reference data in multiple tables, one query per table
     812referenced is needed, rather than one query for all the items.
    791813
    792814extra
    793815~~~~~
  • tests/modeltests/prefetch_related/tests.py

    diff -r 554e071b5c4a tests/modeltests/prefetch_related/tests.py
    a b  
    5454        normal_lists = [list(a.books.all()) for a in Author.objects.all()]
    5555        self.assertEqual(lists, normal_lists)
    5656
     57    def test_foreignkey_forward(self):
     58        with self.assertNumQueries(2):
     59            books = [a.first_book for a in Author.objects.prefetch_related('first_book')]
     60
     61        normal_books = [a.first_book for a in Author.objects.all()]
     62        self.assertEqual(books, normal_books)
     63
    5764    def test_foreignkey_reverse(self):
    5865        with self.assertNumQueries(2):
    5966            lists = [list(b.first_time_authors.all())
     
    175182        self.assertTrue('prefetch_related' in str(cm.exception))
    176183
    177184    def test_invalid_final_lookup(self):
    178         qs = Book.objects.prefetch_related('authors__first_book')
     185        qs = Book.objects.prefetch_related('authors__name')
    179186        with self.assertRaises(ValueError) as cm:
    180187            list(qs)
    181188
    182189        self.assertTrue('prefetch_related' in str(cm.exception))
    183         self.assertTrue("first_book" in str(cm.exception))
     190        self.assertTrue("name" in str(cm.exception))
    184191
    185192
    186193class DefaultManagerTests(TestCase):
     
    222229
    223230class GenericRelationTests(TestCase):
    224231
    225     def test_traverse_GFK(self):
    226         """
    227         Test that we can traverse a 'content_object' with prefetch_related()
    228         """
    229         # In fact, there is no special support for this in prefetch_related code
    230         # - we can traverse any object that will lead us to objects that have
    231         # related managers.
    232 
     232    def setUp(self):
    233233        book1 = Book.objects.create(title="Winnie the Pooh")
    234234        book2 = Book.objects.create(title="Do you like green eggs and spam?")
     235        book3 = Book.objects.create(title="Three Men In A Boat")
    235236
    236237        reader1 = Reader.objects.create(name="me")
    237238        reader2 = Reader.objects.create(name="you")
     239        reader3 = Reader.objects.create(name="someone")
    238240
    239         book1.read_by.add(reader1)
     241        book1.read_by.add(reader1, reader2)
    240242        book2.read_by.add(reader2)
     243        book3.read_by.add(reader3)
    241244
    242         TaggedItem.objects.create(tag="awesome", content_object=book1)
    243         TaggedItem.objects.create(tag="awesome", content_object=book2)
     245        self.book1, self.book2, self.book3 = book1, book2, book3
     246        self.reader1, self.reader2, self.reader3 = reader1, reader2, reader3
     247
     248    def test_prefetch_GFK(self):
     249        TaggedItem.objects.create(tag="awesome", content_object=self.book1)
     250        TaggedItem.objects.create(tag="great", content_object=self.reader1)
     251        TaggedItem.objects.create(tag="stupid", content_object=self.book2)
     252        TaggedItem.objects.create(tag="amazing", content_object=self.reader3)
     253
     254        # 1 for TaggedItem table, 1 for Book table, 1 for Reader table
     255        with self.assertNumQueries(3):
     256            qs = TaggedItem.objects.prefetch_related('content_object')
     257            list(qs)
     258
     259    def test_traverse_GFK(self):
     260        """
     261        Test that we can traverse a 'content_object' with prefetch_related() and
     262        get to related objects on the other side (assuming it is suitably
     263        filtered)
     264        """
     265        TaggedItem.objects.create(tag="awesome", content_object=self.book1)
     266        TaggedItem.objects.create(tag="awesome", content_object=self.book2)
     267        TaggedItem.objects.create(tag="awesome", content_object=self.book3)
     268        TaggedItem.objects.create(tag="awesome", content_object=self.reader1)
     269        TaggedItem.objects.create(tag="awesome", content_object=self.reader2)
    244270
    245271        ct = ContentType.objects.get_for_model(Book)
    246272
    247         # We get 4 queries - 1 for main query, 2 for each access to
    248         # 'content_object' because these can't be handled by select_related, and
    249         # 1 for the 'read_by' relation.
    250         with self.assertNumQueries(4):
     273        # We get 3 queries - 1 for main query, 1 for content_objects since they
     274        # all use the same table, and 1 for the 'read_by' relation.
     275        with self.assertNumQueries(3):
    251276            # If we limit to books, we know that they will have 'read_by'
    252277            # attributes, so the following makes sense:
    253             qs = TaggedItem.objects.select_related('content_type').prefetch_related('content_object__read_by').filter(tag='awesome').filter(content_type=ct, tag='awesome')
    254             readers_of_awesome_books = [r.name for tag in qs
    255                                         for r in tag.content_object.read_by.all()]
    256             self.assertEqual(readers_of_awesome_books, ["me", "you"])
    257 
     278            qs = TaggedItem.objects.filter(content_type=ct, tag='awesome').prefetch_related('content_object__read_by')
     279            readers_of_awesome_books = set([r.name for tag in qs
     280                                            for r in tag.content_object.read_by.all()])
     281            self.assertEqual(readers_of_awesome_books, set(["me", "you", "someone"]))
    258282
    259283    def test_generic_relation(self):
    260284        b = Bookmark.objects.create(url='http://www.djangoproject.com/')
     
    311335        self.assertEquals(lst, lst2)
    312336
    313337    def test_parent_link_prefetch(self):
    314         with self.assertRaises(ValueError) as cm:
    315             qs = list(AuthorWithAge.objects.prefetch_related('author'))
    316         self.assertTrue('prefetch_related' in str(cm.exception))
     338        with self.assertNumQueries(2):
     339            [a.author for a in AuthorWithAge.objects.prefetch_related('author')]
     340
     341    def test_child_link_prefetch(self):
     342        with self.assertNumQueries(2):
     343            l = [a.authorwithage for a in Author.objects.prefetch_related('authorwithage')]
     344
     345        self.assertEqual(l, [a.authorwithage for a in Author.objects.all()])
    317346
    318347
    319348class ForeignKeyToFieldTest(TestCase):
     
    406435        worker2 = Employee.objects.create(name="Angela", boss=boss)
    407436
    408437    def test_traverse_nullable(self):
     438        # Because we use select_related() for 'boss', it doesn't need to be
     439        # prefetched, but we can still traverse it although it contains some nulls
    409440        with self.assertNumQueries(2):
    410441            qs = Employee.objects.select_related('boss').prefetch_related('boss__serfs')
    411442            co_serfs = [list(e.boss.serfs.all()) if e.boss is not None else []
     
    416447                        for e in qs2]
    417448
    418449        self.assertEqual(co_serfs, co_serfs2)
     450
     451    def test_prefetch_nullable(self):
     452        # One for main employee, one for boss, one for serfs
     453        with self.assertNumQueries(3):
     454            qs = Employee.objects.prefetch_related('boss__serfs')
     455            co_serfs = [list(e.boss.serfs.all()) if e.boss is not None else []
     456                        for e in qs]
     457
     458        qs2 =  Employee.objects.all()
     459        co_serfs2 =  [list(e.boss.serfs.all()) if e.boss is not None else []
     460                        for e in qs2]
     461
     462        self.assertEqual(co_serfs, co_serfs2)
Back to Top