Ticket #17003: prefetch_singly_related_objs.diff

File prefetch_singly_related_objs.diff, 29.7 KB (added by Luke Plant, 13 years ago)

Implementation

  • django/contrib/contenttypes/generic.py

    diff -r 554e071b5c4a django/contrib/contenttypes/generic.py
    a b  
    11"""
    22Classes allowing "generic" relations through ContentType and object-id fields.
    33"""
     4from collections import defaultdict
     5from functools import partial
    46
    5 from functools import partial
    67from django.core.exceptions import ObjectDoesNotExist
    78from django.db import connection
    89from django.db.models import signals
     
    5960            # This should never happen. I love comments like this, don't you?
    6061            raise Exception("Impossible arguments to GFK.get_content_type!")
    6162
     63    def get_prefetch_query_set(self, instances):
     64        # For efficiency, group the instances by content type and then do one
     65        # query per model
     66        fk_dict = defaultdict(list)
     67        # We need one instance for each group in order to get the right db:
     68        instance_dict = {}
     69        ct_attname = self.model._meta.get_field(self.ct_field).get_attname()
     70        for instance in instances:
     71            # We avoid looking for values if either ct_id or fkey value is None
     72            ct_id = getattr(instance, ct_attname)
     73            if ct_id is not None:
     74                fk_val = getattr(instance, self.fk_field)
     75                if fk_val is not None:
     76                    fk_dict[ct_id].append(fk_val)
     77                    instance_dict[ct_id] = instance
     78
     79        ret_val = []
     80        for ct_id, fkeys in fk_dict.items():
     81            instance = instance_dict[ct_id]
     82            ct = self.get_content_type(id=ct_id, using=instance._state.db)
     83            ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys))
     84
     85        # For doing the join in Python, we have to match both the FK val and the
     86        # content type, so the 'attr' vals we return need to be callables that
     87        # will return a (fk, class) pair.
     88        return (ret_val,
     89                lambda obj: (obj.pk, obj.__class__),
     90                lambda obj: (getattr(obj, self.fk_field),
     91                             self.get_content_type(id=getattr(obj, ct_attname),
     92                                                   using=obj._state.db).model_class()),
     93                True,
     94                self.cache_attr)
     95
     96    def is_cached(self, instance):
     97        return hasattr(instance, self.cache_attr)
     98
    6299    def __get__(self, instance, instance_type=None):
    63100        if instance is None:
    64101            return self
     
    282319                    [obj._get_pk_val() for obj in instances]
    283320                }
    284321            qs = super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**query)
    285             return (qs, self.object_id_field_name, 'pk')
     322            return (qs, self.object_id_field_name, 'pk', False, self.prefetch_cache_name)
    286323
    287324        def add(self, *objs):
    288325            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
    230233    def __get__(self, instance, instance_type=None):
    231234        if instance is None:
    232235            return self
     
    283286    # ReverseSingleRelatedObjectDescriptor instance.
    284287    def __init__(self, field_with_rel):
    285288        self.field = field_with_rel
     289        self.cache_name = self.field.get_cache_name()
     290
     291    def is_cached(self, instance):
     292        return hasattr(instance, self.cache_name)
     293
     294    def get_query_set(self, instance=None):
     295        hints = {}
     296        if instance is not None:
     297            hints['instance'] = instance
     298        db = router.db_for_read(self.field.rel.to, **hints)
     299        rel_mgr = self.field.rel.to._default_manager
     300        # If the related manager indicates that it should be used for
     301        # related fields, respect that.
     302        if getattr(rel_mgr, 'use_for_related_fields', False):
     303            return rel_mgr.using(db)
     304        else:
     305            return QuerySet(self.field.rel.to).using(db)
     306
     307    def get_prefetch_query_set(self, instances):
     308        """
     309        Creates a prefetch query for a list of instances.
     310
     311        Returns a tuple:
     312        (queryset of instances of self.model that are related to passed in instances,
     313        attr of returned instances needed for matching,
     314        attr of passed in instances needed for matching,
     315        boolean that is True for singly related objects,
     316        cache name to assign to).
     317
     318        The 'attr' return values can also be callables which return the values
     319        needed, if a simple 'getattr(obj, attr)' will not suffice.
     320        """
     321
     322        vals = [getattr(instance, self.field.attname) for instance in instances]
     323        other_field = self.field.rel.get_related_field()
     324        if other_field.rel:
     325            params = {'%s__pk__in' % self.field.rel.field_name: vals}
     326        else:
     327            params = {'%s__in' % self.field.rel.field_name: vals}
     328        #from IPython.Shell import IPShellEmbed; IPShellEmbed([])()
     329        return (self.get_query_set().filter(**params),
     330                self.field.rel.field_name,
     331                self.field.attname,
     332                True,
     333                self.cache_name)
    286334
    287335    def __get__(self, instance, instance_type=None):
    288336        if instance is None:
    289337            return self
    290338
    291         cache_name = self.field.get_cache_name()
    292339        try:
    293             return getattr(instance, cache_name)
     340            return getattr(instance, self.cache_name)
    294341        except AttributeError:
    295342            val = getattr(instance, self.field.attname)
    296343            if val is None:
     
    303350                params = {'%s__pk' % self.field.rel.field_name: val}
    304351            else:
    305352                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)
     353            qs = self.get_query_set(instance=instance)
     354            rel_obj = qs.get(**params)
     355            setattr(instance, self.cache_name, rel_obj)
    316356            return rel_obj
    317357
    318358    def __set__(self, instance, value):
     
    433473                query = {'%s__%s__in' % (rel_field.name, attname):
    434474                             [getattr(obj, attname) for obj in instances]}
    435475                qs = super(RelatedManager, self).get_query_set().using(db).filter(**query)
    436                 return (qs, rel_field.get_attname(), attname)
     476                return (qs, rel_field.get_attname(), attname,
     477                        False, rel_field.related_query_name())
    437478
    438479            def add(self, *objs):
    439480                for obj in objs:
     
    507548                return super(ManyRelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**self.core_filters)
    508549
    509550        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             """
    516551            from django.db import connections
    517552            db = self._db or router.db_for_read(self.model)
    518553            query = {'%s__pk__in' % self.query_field_name:
     
    534569            qs = qs.extra(select={'_prefetch_related_val':
    535570                                      '%s.%s' % (qn(join_table), qn(source_col))})
    536571            select_attname = fk.rel.get_related_field().get_attname()
    537             return (qs, '_prefetch_related_val', select_attname)
     572            return (qs, '_prefetch_related_val', select_attname,
     573                    False, self.prefetch_cache_name)
    538574
    539575        # If the ManyToMany relation has an intermediary model,
    540576        # 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
     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
    16521700def prefetch_one_level(instances, relmanager, attname):
    16531701    """
    16541702    Helper function for prefetch_related_objects
     
    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    rel_qs, rel_obj_attr, instance_attr, single, cache_name =\
     1712        relmanager.get_prefetch_query_set(instances)
    16641713    # We have to handle the possibility that the default manager itself added
    16651714    # prefetch_related lookups to the QuerySet we just got back. We don't want to
    16661715    # trigger the prefetch_related functionality by evaluating the query.
     
    16761725
    16771726    rel_obj_cache = {}
    16781727    for rel_obj in all_related_objects:
    1679         rel_attr_val = getattr(rel_obj, rel_obj_attr)
     1728        if callable(rel_obj_attr):
     1729            rel_attr_val = rel_obj_attr(rel_obj)
     1730        else:
     1731            rel_attr_val = getattr(rel_obj, rel_obj_attr)
    16801732        if rel_attr_val not in rel_obj_cache:
    16811733            rel_obj_cache[rel_attr_val] = []
    16821734        rel_obj_cache[rel_attr_val].append(rel_obj)
    16831735
    16841736    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
     1737        if callable(instance_attr):
     1738            instance_attr_val = instance_attr(obj)
     1739        else:
     1740            instance_attr_val = getattr(obj, instance_attr)
     1741        vals = rel_obj_cache.get(instance_attr_val, [])
     1742        if single:
     1743            # Need to assign to single cache on instance
     1744            if vals:
     1745                setattr(obj, cache_name, vals[0])
     1746        else:
     1747            # Multi, attribute represents a manager with an .all() method that
     1748            # returns a QuerySet
     1749            qs = getattr(obj, attname).all()
     1750            qs._result_cache = vals
     1751            # We don't want the individual qs doing prefetch_related now, since we
     1752            # have merged this into the current work.
     1753            qs._prefetch_done = True
     1754            obj._prefetched_objects_cache[cache_name] = qs
    16921755    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')]
    317340
    318341
    319342class ForeignKeyToFieldTest(TestCase):
     
    406429        worker2 = Employee.objects.create(name="Angela", boss=boss)
    407430
    408431    def test_traverse_nullable(self):
     432        # Because we use select_related() for 'boss', it doesn't need to be
     433        # prefetched, but we can still traverse it although it contains some nulls
    409434        with self.assertNumQueries(2):
    410435            qs = Employee.objects.select_related('boss').prefetch_related('boss__serfs')
    411436            co_serfs = [list(e.boss.serfs.all()) if e.boss is not None else []
     
    416441                        for e in qs2]
    417442
    418443        self.assertEqual(co_serfs, co_serfs2)
     444
     445    def test_prefetch_nullable(self):
     446        # One for main employee, one for boss, one for serfs
     447        with self.assertNumQueries(3):
     448            qs = Employee.objects.prefetch_related('boss__serfs')
     449            co_serfs = [list(e.boss.serfs.all()) if e.boss is not None else []
     450                        for e in qs]
     451
     452        qs2 =  Employee.objects.all()
     453        co_serfs2 =  [list(e.boss.serfs.all()) if e.boss is not None else []
     454                        for e in qs2]
     455
     456        self.assertEqual(co_serfs, co_serfs2)
Back to Top