Ticket #16937: prefetch_7.diff

File prefetch_7.diff, 49.4 KB (added by Luke Plant, 13 years ago)

Added fix for traversing nullable relations

  • django/contrib/contenttypes/generic.py

    diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
    a b  
    225225            content_type = content_type,
    226226            content_type_field_name = self.field.content_type_field_name,
    227227            object_id_field_name = self.field.object_id_field_name,
    228             core_filters = {
    229                 '%s__pk' % self.field.content_type_field_name: content_type.id,
    230                 '%s__exact' % self.field.object_id_field_name: instance._get_pk_val(),
    231             }
    232 
     228            prefetch_cache_name = self.field.attname,
    233229        )
    234230
    235231        return manager
     
    250246    """
    251247
    252248    class GenericRelatedObjectManager(superclass):
    253         def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
     249        def __init__(self, model=None, instance=None, symmetrical=None,
    254250                     source_col_name=None, target_col_name=None, content_type=None,
    255                      content_type_field_name=None, object_id_field_name=None):
     251                     content_type_field_name=None, object_id_field_name=None,
     252                     prefetch_cache_name=None):
    256253
    257254            super(GenericRelatedObjectManager, self).__init__()
    258             self.core_filters = core_filters
    259255            self.model = model
    260256            self.content_type = content_type
    261257            self.symmetrical = symmetrical
     
    264260            self.target_col_name = target_col_name
    265261            self.content_type_field_name = content_type_field_name
    266262            self.object_id_field_name = object_id_field_name
     263            self.prefetch_cache_name = prefetch_cache_name
    267264            self.pk_val = self.instance._get_pk_val()
     265            self.core_filters = {
     266                '%s__pk' % content_type_field_name: content_type.id,
     267                '%s__exact' % object_id_field_name: instance._get_pk_val(),
     268            }
    268269
    269270        def get_query_set(self):
    270             db = self._db or router.db_for_read(self.model, instance=self.instance)
    271             return super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**self.core_filters)
     271            try:
     272                return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
     273            except (AttributeError, KeyError):
     274                db = self._db or router.db_for_read(self.model, instance=self.instance)
     275                return super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**self.core_filters)
     276
     277        def get_prefetch_query_set(self, instances):
     278            db = self._db or router.db_for_read(self.model)
     279            query = {
     280                '%s__pk' % self.content_type_field_name: self.content_type.id,
     281                '%s__in' % self.object_id_field_name:
     282                    [obj._get_pk_val() for obj in instances]
     283                }
     284            qs = super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**query)
     285            return (qs, self.object_id_field_name, 'pk')
    272286
    273287        def add(self, *objs):
    274288            for obj in objs:
  • django/db/models/fields/related.py

    diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
    a b  
    432432                self.model = rel_model
    433433
    434434            def get_query_set(self):
    435                 db = self._db or router.db_for_read(self.model, instance=self.instance)
    436                 return super(RelatedManager, self).get_query_set().using(db).filter(**(self.core_filters))
     435                try:
     436                    return self.instance._prefetched_objects_cache[rel_field.related_query_name()]
     437                except (AttributeError, KeyError):
     438                    db = self._db or router.db_for_read(self.model, instance=self.instance)
     439                    return super(RelatedManager, self).get_query_set().using(db).filter(**self.core_filters)
     440
     441            def get_prefetch_query_set(self, instances):
     442                """
     443                Return a queryset that does the bulk lookup needed
     444                by prefetch_related functionality.
     445                """
     446                db = self._db or router.db_for_read(self.model)
     447                query = {'%s__%s__in' % (rel_field.name, attname):
     448                             [getattr(obj, attname) for obj in instances]}
     449                qs = super(RelatedManager, self).get_query_set().using(db).filter(**query)
     450                return (qs, rel_field.get_attname(), attname)
    437451
    438452            def add(self, *objs):
    439453                for obj in objs:
     
    482496    """Creates a manager that subclasses 'superclass' (which is a Manager)
    483497    and adds behavior for many-to-many related objects."""
    484498    class ManyRelatedManager(superclass):
    485         def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
     499        def __init__(self, model=None, query_field_name=None, instance=None, symmetrical=None,
    486500                     source_field_name=None, target_field_name=None, reverse=False,
    487                      through=None):
     501                     through=None, prefetch_cache_name=None):
    488502            super(ManyRelatedManager, self).__init__()
    489503            self.model = model
    490             self.core_filters = core_filters
     504            self.query_field_name = query_field_name
     505            self.core_filters = {'%s__pk' % query_field_name: instance._get_pk_val()}
    491506            self.instance = instance
    492507            self.symmetrical = symmetrical
    493508            self.source_field_name = source_field_name
    494509            self.target_field_name = target_field_name
    495510            self.reverse = reverse
    496511            self.through = through
     512            self.prefetch_cache_name = prefetch_cache_name
    497513            self._pk_val = self.instance.pk
    498514            if self._pk_val is None:
    499515                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
    500516
    501517        def get_query_set(self):
    502             db = self._db or router.db_for_read(self.instance.__class__, instance=self.instance)
    503             return super(ManyRelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**(self.core_filters))
     518            try:
     519                return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
     520            except (AttributeError, KeyError):
     521                db = self._db or router.db_for_read(self.instance.__class__, instance=self.instance)
     522                return super(ManyRelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**self.core_filters)
     523
     524        def get_prefetch_query_set(self, instances):
     525            """
     526            Returns a tuple:
     527            (queryset of instances of self.model that are related to passed in instances
     528             attr of returned instances needed for matching
     529             attr of passed in instances needed for matching)
     530            """
     531            from django.db import connections
     532            db = self._db or router.db_for_read(self.model)
     533            query = {'%s__pk__in' % self.query_field_name:
     534                         [obj._get_pk_val() for obj in instances]}
     535            qs = super(ManyRelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**query)
     536
     537            # M2M: need to annotate the query in order to get the primary model
     538            # that the secondary model was actually related to. We know that
     539            # there will already be a join on the join table, so we can just add
     540            # the select.
     541
     542            # For non-autocreated 'through' models, can't assume we are
     543            # dealing with PK values.
     544            fk = self.through._meta.get_field(self.source_field_name)
     545            source_col = fk.column
     546            join_table = self.through._meta.db_table
     547            connection = connections[db]
     548            qn = connection.ops.quote_name
     549            qs = qs.extra(select={'_prefetch_related_val':
     550                                      '%s.%s' % (qn(join_table), qn(source_col))})
     551            select_attname = fk.rel.get_related_field().get_attname()
     552            return (qs, '_prefetch_related_val', select_attname)
    504553
    505554        # If the ManyToMany relation has an intermediary model,
    506555        # the add and remove methods do not exist.
     
    683732
    684733        manager = self.related_manager_cls(
    685734            model=rel_model,
    686             core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()},
     735            query_field_name=self.related.field.name,
     736            prefetch_cache_name=self.related.field.related_query_name(),
    687737            instance=instance,
    688738            symmetrical=False,
    689739            source_field_name=self.related.field.m2m_reverse_field_name(),
     
    739789
    740790        manager = self.related_manager_cls(
    741791            model=self.field.rel.to,
    742             core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()},
     792            query_field_name=self.field.related_query_name(),
     793            prefetch_cache_name=self.field.name,
    743794            instance=instance,
    744795            symmetrical=self.field.rel.symmetrical,
    745796            source_field_name=self.field.m2m_field_name(),
  • django/db/models/manager.py

    diff --git a/django/db/models/manager.py b/django/db/models/manager.py
    a b  
    172172    def select_related(self, *args, **kwargs):
    173173        return self.get_query_set().select_related(*args, **kwargs)
    174174
     175    def prefetch_related(self, *args, **kwargs):
     176        return self.get_query_set().prefetch_related(*args, **kwargs)
     177
    175178    def values(self, *args, **kwargs):
    176179        return self.get_query_set().values(*args, **kwargs)
    177180
  • django/db/models/query.py

    diff --git a/django/db/models/query.py b/django/db/models/query.py
    a b  
    3636        self._iter = None
    3737        self._sticky_filter = False
    3838        self._for_write = False
     39        self._prefetch_related_lookups = []
     40        self._prefetch_done = False
    3941
    4042    ########################
    4143    # PYTHON MAGIC METHODS #
     
    8183                self._result_cache = list(self.iterator())
    8284        elif self._iter:
    8385            self._result_cache.extend(self._iter)
     86        if self._prefetch_related_lookups and not self._prefetch_done:
     87            self._prefetch_related_objects()
    8488        return len(self._result_cache)
    8589
    8690    def __iter__(self):
     91        if self._prefetch_related_lookups and not self._prefetch_done:
     92            # We need all the results in order to be able to do the prefetch
     93            # in one go. To minimize code duplication, we use the __len__
     94            # code path which also forces this, and also does the prefetch
     95            len(self)
     96
    8797        if self._result_cache is None:
    8898            self._iter = self.iterator()
    8999            self._result_cache = []
     
    106116                self._fill_cache()
    107117
    108118    def __nonzero__(self):
     119        if self._prefetch_related_lookups and not self._prefetch_done:
     120            # We need all the results in order to be able to do the prefetch
     121            # in one go. To minimize code duplication, we use the __len__
     122            # code path which also forces this, and also does the prefetch
     123            len(self)
     124
    109125        if self._result_cache is not None:
    110126            return bool(self._result_cache)
    111127        try:
     
    526542            return self.query.has_results(using=self.db)
    527543        return bool(self._result_cache)
    528544
     545    def _prefetch_related_objects(self):
     546        # This method can only be called once the result cache has been filled.
     547        prefetch_related_objects(self._result_cache, self._prefetch_related_lookups)
     548        self._prefetch_done = True
     549
    529550    ##################################################
    530551    # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS #
    531552    ##################################################
     
    649670            obj.query.max_depth = depth
    650671        return obj
    651672
     673    def prefetch_related(self, *lookups):
     674        """
     675        Returns a new QuerySet instance that will prefetch the specified
     676        Many-To-One and Many-To-Many related objects when the QuerySet is
     677        evaluated.
     678
     679        When prefetch_related() is called more than once, the list of lookups to
     680        prefetch is appended to. If prefetch_related(None) is called, the
     681        the list is cleared.
     682        """
     683        clone = self._clone()
     684        if lookups == (None,):
     685            clone._prefetch_related_lookups = []
     686        else:
     687            clone._prefetch_related_lookups.extend(lookups)
     688        return clone
     689
    652690    def dup_select_related(self, other):
    653691        """
    654692        Copies the related selection status from the QuerySet 'other' to the
     
    798836            query.filter_is_sticky = True
    799837        c = klass(model=self.model, query=query, using=self._db)
    800838        c._for_write = self._for_write
     839        c._prefetch_related_lookups = self._prefetch_related_lookups[:]
    801840        c.__dict__.update(kwargs)
    802841        if setup and hasattr(c, '_setup_query'):
    803842            c._setup_query()
     
    863902    # empty" result.
    864903    value_annotation = True
    865904
     905
    866906class ValuesQuerySet(QuerySet):
    867907    def __init__(self, *args, **kwargs):
    868908        super(ValuesQuerySet, self).__init__(*args, **kwargs)
     
    9921032                    % self.__class__.__name__)
    9931033        return self
    9941034
     1035
    9951036class ValuesListQuerySet(ValuesQuerySet):
    9961037    def iterator(self):
    9971038        if self.flat and len(self._fields) == 1:
     
    14751516                self._model_fields[converter(column)] = field
    14761517        return self._model_fields
    14771518
     1519
    14781520def insert_query(model, objs, fields, return_id=False, raw=False, using=None):
    14791521    """
    14801522    Inserts a new record for the given model. This provides an interface to
     
    14841526    query = sql.InsertQuery(model)
    14851527    query.insert_values(fields, objs, raw=raw)
    14861528    return query.get_compiler(using=using).execute_sql(return_id)
     1529
     1530
     1531def prefetch_related_objects(result_cache, related_lookups):
     1532    """
     1533    Helper function for prefetch_related functionality
     1534
     1535    Populates prefetched objects caches for a list of results
     1536    from a QuerySet
     1537    """
     1538    from django.db.models.sql.constants import LOOKUP_SEP
     1539
     1540    if len(result_cache) == 0:
     1541        return # nothing to do
     1542
     1543    model = result_cache[0].__class__
     1544
     1545    # We need to be able to dynamically add to the list of prefetch_related
     1546    # lookups that we look up (see below).  So we need some book keeping to
     1547    # ensure we don't do duplicate work.
     1548    done_lookups = set() # list of lookups like foo__bar__baz
     1549    done_queries = {}    # dictionary of things like 'foo__bar': [results]
     1550    related_lookups = list(related_lookups)
     1551
     1552    # We may expand related_lookups, so need a loop that allows for that
     1553    for lookup in related_lookups:
     1554        if lookup in done_lookups:
     1555            # We've done exactly this already, skip the whole thing
     1556            continue
     1557        done_lookups.add(lookup)
     1558
     1559        # Top level, the list of objects to decorate is the the result cache
     1560        # from the primary QuerySet. It won't be for deeper levels.
     1561        obj_list = result_cache
     1562
     1563        attrs = lookup.split(LOOKUP_SEP)
     1564        for level, attr in enumerate(attrs):
     1565            # Prepare main instances
     1566            if len(obj_list) == 0:
     1567                break
     1568
     1569            good_objects = True
     1570            for obj in obj_list:
     1571                if not hasattr(obj, '_prefetched_objects_cache'):
     1572                    try:
     1573                        obj._prefetched_objects_cache = {}
     1574                    except AttributeError:
     1575                        # Must be in a QuerySet subclass that is not returning
     1576                        # Model instances, either in Django or 3rd
     1577                        # party. prefetch_related() doesn't make sense, so quit
     1578                        # now.
     1579                        good_objects = False
     1580                        break
     1581                else:
     1582                    # We already did this list
     1583                    break
     1584            if not good_objects:
     1585                break
     1586
     1587            # Descend down tree
     1588            try:
     1589                rel_obj = getattr(obj_list[0], attr)
     1590            except AttributeError:
     1591                raise AttributeError("Cannot find '%s' on %s object, '%s' is an invalid "
     1592                                     "parameter to prefetch_related()" %
     1593                                     (attr, obj_list[0].__class__.__name__, lookup))
     1594
     1595            can_prefetch = hasattr(rel_obj, 'get_prefetch_query_set')
     1596            if level == len(attrs) - 1 and not can_prefetch:
     1597                # Last one, this *must* resolve to a related manager.
     1598                raise ValueError("'%s' does not resolve to a supported 'many related"
     1599                                 " manager' for model %s - this is an invalid"
     1600                                 " parameter to prefetch_related()."
     1601                                 % (lookup, model.__name__))
     1602
     1603            if can_prefetch:
     1604                # Check we didn't do this already
     1605                current_lookup = LOOKUP_SEP.join(attrs[0:level+1])
     1606                if current_lookup in done_queries:
     1607                    obj_list = done_queries[current_lookup]
     1608                else:
     1609                    relmanager = rel_obj
     1610                    obj_list, additional_prl = prefetch_one_level(obj_list, relmanager, attr)
     1611                    for f in additional_prl:
     1612                        new_prl = LOOKUP_SEP.join([current_lookup, f])
     1613                        related_lookups.append(new_prl)
     1614                    done_queries[current_lookup] = obj_list
     1615            else:
     1616                # Assume we've got some singly related object. We replace
     1617                # the current list of parent objects with that list.
     1618                obj_list = [getattr(obj, attr) for obj in obj_list]
     1619
     1620                # Filter out 'None' so that we can continue with nullable
     1621                # relations.
     1622                obj_list = [obj for obj in obj_list if obj is not None]
     1623
     1624
     1625def prefetch_one_level(instances, relmanager, attname):
     1626    """
     1627    Helper function for prefetch_related_objects
     1628
     1629    Runs prefetches on all instances using the manager relmanager,
     1630    assigning results to queryset against instance.attname.
     1631
     1632    The prefetched objects are returned, along with any additional
     1633    prefetches that must be done due to prefetch_related lookups
     1634    found from default managers.
     1635    """
     1636    rel_qs, rel_obj_attr, instance_attr = relmanager.get_prefetch_query_set(instances)
     1637    # We have to handle the possibility that the default manager itself added
     1638    # prefetch_related lookups to the QuerySet we just got back. We don't want to
     1639    # trigger the prefetch_related functionality by evaluating the query.
     1640    # Rather, we need to merge in the prefetch_related lookups.
     1641    additional_prl = getattr(rel_qs, '_prefetch_related_lookups', [])
     1642    if additional_prl:
     1643        # Don't need to clone because the manager should have given us a fresh
     1644        # instance, so we access an internal instead of using public interface
     1645        # for performance reasons.
     1646        rel_qs._prefetch_related_lookups = []
     1647
     1648    all_related_objects = list(rel_qs)
     1649
     1650    rel_obj_cache = {}
     1651    for rel_obj in all_related_objects:
     1652        rel_attr_val = getattr(rel_obj, rel_obj_attr)
     1653        if rel_attr_val not in rel_obj_cache:
     1654            rel_obj_cache[rel_attr_val] = []
     1655        rel_obj_cache[rel_attr_val].append(rel_obj)
     1656
     1657    for obj in instances:
     1658        qs = getattr(obj, attname).all()
     1659        instance_attr_val = getattr(obj, instance_attr)
     1660        qs._result_cache = rel_obj_cache.get(instance_attr_val, [])
     1661        # We don't want the individual qs doing prefetch_related now, since we
     1662        # have merged this into the current work.
     1663        qs._prefetch_done = True
     1664        obj._prefetched_objects_cache[attname] = qs
     1665    return all_related_objects, additional_prl
  • docs/ref/models/querysets.txt

    diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
    a b  
    571571manager or a ``QuerySet`` and do further filtering on the result. After calling
    572572``all()`` on either object, you'll definitely have a ``QuerySet`` to work with.
    573573
    574 .. _select-related:
    575 
    576574select_related
    577575~~~~~~~~~~~~~~
    578576
     
    690688A :class:`~django.db.models.OneToOneField` is not traversed in the reverse
    691689direction if you are performing a depth-based ``select_related()`` call.
    692690
     691prefetch_related
     692~~~~~~~~~~~~~~~~
     693
     694.. method:: prefetch_related(*lookups)
     695
     696.. versionadded:: 1.4
     697
     698Returns a ``QuerySet`` that will automatically retrieve, in a single batch,
     699related many-to-many and many-to-one objects for each of the specified lookups.
     700
     701This is similar to ``select_related`` for the 'many related objects' case, but
     702note that ``prefetch_related`` causes a separate query to be issued for each set
     703of related objects that you request, unlike ``select_related`` which modifies
     704the 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
     706begins to be evaluated.
     707
     708For example, suppose you have these models::
     709
     710    class Topping(models.Model):
     711        name = models.CharField(max_length=30)
     712
     713    class Pizza(models.Model):
     714        name = models.CharField(max_length=50)
     715        toppings = models.ManyToManyField(Topping)
     716
     717        def __unicode__(self):
     718            return u"%s (%s)" % (self.name, u", ".join([topping.name
     719                                                        for topping in self.toppings.all()]))
     720
     721and run this code::
     722
     723    >>> Pizza.objects.all()
     724    [u"Hawaiian (ham, pineapple)", u"Seafood (prawns, smoked salmon)"...
     725
     726The problem with this code is that it will run a query on the Toppings table for
     727**every** item in the Pizza ``QuerySet``.  Using ``prefetch_related``, this can
     728be reduced to two:
     729
     730    >>> Pizza.objects.all().prefetch_related('toppings')
     731
     732All the relevant toppings will be fetched in a single query, and used to make
     733``QuerySets`` that have a pre-filled cache of the relevant results. These
     734``QuerySets`` are then used in the ``self.toppings.all()`` calls.
     735
     736Please note that use of ``prefetch_related`` will mean that the additional
     737queries run will **always** be executed - even if you never use the related
     738objects - and it always fully populates the result cache on the primary
     739``QuerySet`` (which can sometimes be avoided in other cases).
     740
     741Also remember that, as always with QuerySets, any subsequent chained methods
     742will ignore previously cached results, and retrieve data using a fresh database
     743query. So, if you write the following:
     744
     745    >>> pizzas = Pizza.objects.prefetch_related('toppings')
     746    >>> [list(pizza.topppings.filter(spicy=True) for pizza in pizzas]
     747
     748...then the fact that `pizza.toppings.all()` has been prefetched will not help
     749you - in fact it hurts performance, since you have done a database query that
     750you haven't used. So use this feature with caution!
     751
     752The lookups that must be supplied to this method can be any attributes on the
     753model instances which represent related queries that return multiple
     754objects. This includes attributes representing the 'many' side of ``ForeignKey``
     755relationships, forward and reverse ``ManyToManyField`` attributes, and also any
     756``GenericRelations``.
     757
     758You can also use the normal join syntax to do related fields of related
     759fields. Suppose we have an additional model to the example above::
     760
     761    class Restaurant(models.Model):
     762        pizzas = models.ManyToMany(Pizza, related_name='restaurants')
     763        best_pizza = models.ForeignKey(Pizza, related_name='championed_by')
     764
     765The following are all legal:
     766
     767    >>> Restaurant.objects.prefetch_related('pizzas__toppings')
     768
     769This will prefetch all pizzas belonging to restaurants, and all toppings
     770belonging to those pizzas. This will result in a total of 3 database queries -
     771one for the restaurants, one for the pizzas, and one for the toppings.
     772
     773    >>> Restaurant.objects.select_related('best_pizza').prefetch_related('best_pizza__toppings')
     774
     775This will fetch the best pizza and all the toppings for the best pizza for each
     776restaurant. This will be done in 2 database queries - one for the restaurants
     777and 'best pizzas' combined (achieved through use of ``select_related``), and one
     778for the toppings.
     779
     780Chaining ``prefetch_related`` calls will accumulate the fields that should have
     781this behavior applied. To clear any ``prefetch_related`` behavior, pass `None`
     782as a parameter::
     783
     784   >>> non_prefetched = qs.prefetch_related(None)
     785
     786One difference when using ``prefetch_related`` is that, in some circumstances,
     787objects created by a query can be shared between the different objects that they
     788are related to i.e. a single Python model instance can appear at more than one
     789point in the tree of objects that are returned. Normally this behavior will not
     790be a problem, and will in fact save both memory and CPU time.
     791
    693792extra
    694793~~~~~
    695794
  • docs/releases/1.4.txt

    diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
    a b  
    6363See the :meth:`~django.db.models.query.QuerySet.bulk_create` docs for more
    6464information.
    6565
     66``QuerySet.prefetch_related``
     67~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     68
     69Analagous to :meth:`~django.db.models.query.QuerySet.select_related` but for
     70many-to-many relationships,
     71:meth:`~django.db.models.query.QuerySet.prefetch_related` has been added to
     72:class:`~django.db.models.query.QuerySet`. This method returns a new ``QuerySet``
     73that will prefetch in a single batch each of the specified related lookups as
     74soon as it begins to be evaluated (e.g. by iterating over it). This enables you
     75to fix many instances of a very common performance problem, in which your code
     76ends up doing O(n) database queries (or worse) if objects on your primary
     77``QuerySet`` each have many related objects that you also need.
     78
    6679HTML5
    6780~~~~~
    6881
  • docs/topics/db/optimization.txt

    diff --git a/docs/topics/db/optimization.txt b/docs/topics/db/optimization.txt
    a b  
    141141query that is executed in a loop, and could therefore end up doing many database
    142142queries, when only one was needed. So:
    143143
    144 Use ``QuerySet.select_related()``
    145 ---------------------------------
     144Use ``QuerySet.select_related()`` and ``prefetch_related()``
     145------------------------------------------------------------
    146146
    147 Understand :ref:`QuerySet.select_related() <select-related>` thoroughly, and use it:
     147Understand :meth:`~django.db.models.query.QuerySet.select_related` and
     148:meth:`~django.db.models.query.QuerySet.prefetch_related` thoroughly, and use
     149them:
    148150
    149151* in view code,
    150152
  • new file tests/modeltests/prefetch_related/models.py

    diff --git a/tests/modeltests/prefetch_related/__init__.py b/tests/modeltests/prefetch_related/__init__.py
    new file mode 100644
    diff --git a/tests/modeltests/prefetch_related/models.py b/tests/modeltests/prefetch_related/models.py
    new file mode 100644
    - +  
     1from django.contrib.contenttypes.models import ContentType
     2from django.contrib.contenttypes import generic
     3from django.db import models
     4
     5## Basic tests
     6
     7class Author(models.Model):
     8    name = models.CharField(max_length=50, unique=True)
     9    first_book = models.ForeignKey('Book', related_name='first_time_authors')
     10    favorite_authors = models.ManyToManyField(
     11        'self', through='FavoriteAuthors', symmetrical=False, related_name='favors_me')
     12
     13    def __unicode__(self):
     14        return self.name
     15
     16    class Meta:
     17        ordering = ['id']
     18
     19
     20class AuthorWithAge(Author):
     21    author = models.OneToOneField(Author, parent_link=True)
     22    age = models.IntegerField()
     23
     24
     25class FavoriteAuthors(models.Model):
     26    author = models.ForeignKey(Author, to_field='name', related_name='i_like')
     27    likes_author = models.ForeignKey(Author, to_field='name', related_name='likes_me')
     28
     29    class Meta:
     30         ordering = ['id']
     31
     32
     33class AuthorAddress(models.Model):
     34    author = models.ForeignKey(Author, to_field='name', related_name='addresses')
     35    address = models.TextField()
     36
     37    class Meta:
     38        ordering = ['id']
     39
     40    def __unicode__(self):
     41        return self.address
     42
     43
     44class Book(models.Model):
     45    title = models.CharField(max_length=255)
     46    authors = models.ManyToManyField(Author, related_name='books')
     47
     48    def __unicode__(self):
     49        return self.title
     50
     51    class Meta:
     52        ordering = ['id']
     53
     54class BookWithYear(Book):
     55    book = models.OneToOneField(Book, parent_link=True)
     56    published_year = models.IntegerField()
     57    aged_authors = models.ManyToManyField(
     58        AuthorWithAge, related_name='books_with_year')
     59
     60
     61class Reader(models.Model):
     62    name = models.CharField(max_length=50)
     63    books_read = models.ManyToManyField(Book, related_name='read_by')
     64
     65    def __unicode__(self):
     66        return self.name
     67
     68    class Meta:
     69        ordering = ['id']
     70
     71
     72## Models for default manager tests
     73
     74class Qualification(models.Model):
     75    name = models.CharField(max_length=10)
     76
     77    class Meta:
     78        ordering = ['id']
     79
     80
     81class TeacherManager(models.Manager):
     82    def get_query_set(self):
     83        return super(TeacherManager, self).get_query_set().prefetch_related('qualifications')
     84
     85
     86class Teacher(models.Model):
     87    name = models.CharField(max_length=50)
     88    qualifications = models.ManyToManyField(Qualification)
     89
     90    objects = TeacherManager()
     91
     92    def __unicode__(self):
     93        return "%s (%s)" % (self.name, ", ".join(q.name for q in self.qualifications.all()))
     94
     95    class Meta:
     96        ordering = ['id']
     97
     98
     99class Department(models.Model):
     100    name = models.CharField(max_length=50)
     101    teachers = models.ManyToManyField(Teacher)
     102
     103    class Meta:
     104        ordering = ['id']
     105
     106
     107## Generic relation tests
     108
     109class TaggedItem(models.Model):
     110    tag = models.SlugField()
     111    content_type = models.ForeignKey(ContentType, related_name="taggeditem_set2")
     112    object_id = models.PositiveIntegerField()
     113    content_object = generic.GenericForeignKey('content_type', 'object_id')
     114
     115    def __unicode__(self):
     116        return self.tag
     117
     118
     119class Bookmark(models.Model):
     120    url = models.URLField()
     121    tags = generic.GenericRelation(TaggedItem)
     122
     123
     124## Models for lookup ordering tests
     125
     126
     127class House(models.Model):
     128    address = models.CharField(max_length=255)
     129
     130    class Meta:
     131        ordering = ['id']
     132
     133class Room(models.Model):
     134    name = models.CharField(max_length=50)
     135    house = models.ForeignKey(House, related_name='rooms')
     136
     137    class Meta:
     138        ordering = ['id']
     139
     140
     141class Person(models.Model):
     142    name = models.CharField(max_length=50)
     143    houses = models.ManyToManyField(House, related_name='occupants')
     144
     145    @property
     146    def primary_house(self):
     147        # Assume business logic forces every person to have at least one house.
     148        return sorted(self.houses.all(), key=lambda house: -house.rooms.count())[0]
     149
     150    class Meta:
     151        ordering = ['id']
     152
     153
     154## Models for nullable FK tests
     155
     156class Employee(models.Model):
     157    name = models.CharField(max_length=50)
     158    boss = models.ForeignKey('self', null=True,
     159                             related_name='serfs')
     160
     161    def __unicode__(self):
     162        return self.name
  • new file tests/modeltests/prefetch_related/tests.py

    diff --git a/tests/modeltests/prefetch_related/tests.py b/tests/modeltests/prefetch_related/tests.py
    new file mode 100644
    - +  
     1from django.contrib.contenttypes.models import ContentType
     2from django.test import TestCase
     3from django.utils import unittest
     4
     5from models import (Author, Book, Reader, Qualification, Teacher, Department,
     6                    TaggedItem, Bookmark, AuthorAddress, FavoriteAuthors,
     7                    AuthorWithAge, BookWithYear, Person, House, Room,
     8                    Employee)
     9
     10class PrefetchRelatedTests(TestCase):
     11
     12    def setUp(self):
     13
     14        self.book1 = Book.objects.create(title="Poems")
     15        self.book2 = Book.objects.create(title="Jane Eyre")
     16        self.book3 = Book.objects.create(title="Wuthering Heights")
     17        self.book4 = Book.objects.create(title="Sense and Sensibility")
     18
     19        self.author1 = Author.objects.create(name="Charlotte",
     20                                             first_book=self.book1)
     21        self.author2 = Author.objects.create(name="Anne",
     22                                             first_book=self.book1)
     23        self.author3 = Author.objects.create(name="Emily",
     24                                             first_book=self.book1)
     25        self.author4 = Author.objects.create(name="Jane",
     26                                             first_book=self.book4)
     27
     28        self.book1.authors.add(self.author1, self.author2, self.author3)
     29        self.book2.authors.add(self.author1)
     30        self.book3.authors.add(self.author3)
     31        self.book4.authors.add(self.author4)
     32
     33        self.reader1 = Reader.objects.create(name="Amy")
     34        self.reader2 = Reader.objects.create(name="Belinda")
     35
     36        self.reader1.books_read.add(self.book1, self.book4)
     37        self.reader2.books_read.add(self.book2, self.book4)
     38
     39    def test_m2m_forward(self):
     40        with self.assertNumQueries(2):
     41            lists = [list(b.authors.all()) for b in Book.objects.prefetch_related('authors')]
     42
     43        normal_lists = [list(b.authors.all()) for b in Book.objects.all()]
     44        self.assertEqual(lists, normal_lists)
     45
     46
     47    def test_m2m_reverse(self):
     48        with self.assertNumQueries(2):
     49            lists = [list(a.books.all()) for a in Author.objects.prefetch_related('books')]
     50
     51        normal_lists = [list(a.books.all()) for a in Author.objects.all()]
     52        self.assertEqual(lists, normal_lists)
     53
     54    def test_foreignkey_reverse(self):
     55        with self.assertNumQueries(2):
     56            lists = [list(b.first_time_authors.all())
     57                     for b in Book.objects.prefetch_related('first_time_authors')]
     58
     59        self.assertQuerysetEqual(self.book2.authors.all(), [u"<Author: Charlotte>"])
     60
     61    def test_survives_clone(self):
     62        with self.assertNumQueries(2):
     63            lists = [list(b.first_time_authors.all())
     64                     for b in Book.objects.prefetch_related('first_time_authors').exclude(id=1000)]
     65
     66    def test_len(self):
     67        with self.assertNumQueries(2):
     68            qs = Book.objects.prefetch_related('first_time_authors')
     69            length = len(qs)
     70            lists = [list(b.first_time_authors.all())
     71                     for b in qs]
     72
     73    def test_bool(self):
     74        with self.assertNumQueries(2):
     75            qs = Book.objects.prefetch_related('first_time_authors')
     76            x = bool(qs)
     77            lists = [list(b.first_time_authors.all())
     78                     for b in qs]
     79
     80    def test_count(self):
     81        with self.assertNumQueries(2):
     82            qs = Book.objects.prefetch_related('first_time_authors')
     83            [b.first_time_authors.count() for b in qs]
     84
     85    def test_exists(self):
     86        with self.assertNumQueries(2):
     87            qs = Book.objects.prefetch_related('first_time_authors')
     88            [b.first_time_authors.exists() for b in qs]
     89
     90    def test_clear(self):
     91        """
     92        Test that we can clear the behavior by calling prefetch_related()
     93        """
     94        with self.assertNumQueries(5):
     95            with_prefetch = Author.objects.prefetch_related('books')
     96            without_prefetch = with_prefetch.prefetch_related(None)
     97            lists = [list(a.books.all()) for a in without_prefetch]
     98
     99    def test_m2m_then_m2m(self):
     100        """
     101        Test we can follow a m2m and another m2m
     102        """
     103        with self.assertNumQueries(3):
     104            qs = Author.objects.prefetch_related('books__read_by')
     105            lists = [[[unicode(r) for r in b.read_by.all()]
     106                      for b in a.books.all()]
     107                     for a in qs]
     108            self.assertEqual(lists,
     109            [
     110                [[u"Amy"], [u"Belinda"]],  # Charlotte - Poems, Jane Eyre
     111                [[u"Amy"]],                # Anne - Poems
     112                [[u"Amy"], []],            # Emily - Poems, Wuthering Heights
     113                [[u"Amy", u"Belinda"]],    # Jane - Sense and Sense
     114            ])
     115
     116    def test_overriding_prefetch(self):
     117        with self.assertNumQueries(3):
     118            qs = Author.objects.prefetch_related('books', 'books__read_by')
     119            lists = [[[unicode(r) for r in b.read_by.all()]
     120                      for b in a.books.all()]
     121                     for a in qs]
     122            self.assertEqual(lists,
     123            [
     124                [[u"Amy"], [u"Belinda"]],  # Charlotte - Poems, Jane Eyre
     125                [[u"Amy"]],                # Anne - Poems
     126                [[u"Amy"], []],            # Emily - Poems, Wuthering Heights
     127                [[u"Amy", u"Belinda"]],    # Jane - Sense and Sense
     128            ])
     129        with self.assertNumQueries(3):
     130            qs = Author.objects.prefetch_related('books__read_by', 'books')
     131            lists = [[[unicode(r) for r in b.read_by.all()]
     132                      for b in a.books.all()]
     133                     for a in qs]
     134            self.assertEqual(lists,
     135            [
     136                [[u"Amy"], [u"Belinda"]],  # Charlotte - Poems, Jane Eyre
     137                [[u"Amy"]],                # Anne - Poems
     138                [[u"Amy"], []],            # Emily - Poems, Wuthering Heights
     139                [[u"Amy", u"Belinda"]],    # Jane - Sense and Sense
     140            ])
     141
     142    def test_get(self):
     143        """
     144        Test that objects retrieved with .get() get the prefetch behaviour
     145        """
     146        # Need a double
     147        with self.assertNumQueries(3):
     148            author = Author.objects.prefetch_related('books__read_by').get(name="Charlotte")
     149            lists = [[unicode(r) for r in b.read_by.all()]
     150                      for b in author.books.all()]
     151            self.assertEqual(lists, [[u"Amy"], [u"Belinda"]])  # Poems, Jane Eyre
     152
     153    def test_foreign_key_then_m2m(self):
     154        """
     155        Test we can follow an m2m relation after a relation like ForeignKey
     156        that doesn't have many objects
     157        """
     158
     159        with self.assertNumQueries(2):
     160            qs = Author.objects.select_related('first_book').prefetch_related('first_book__read_by')
     161            lists = [[unicode(r) for r in a.first_book.read_by.all()]
     162                     for a in qs]
     163            self.assertEqual(lists, [[u"Amy"],
     164                                     [u"Amy"],
     165                                     [u"Amy"],
     166                                     [u"Amy", "Belinda"]])
     167
     168    def test_attribute_error(self):
     169        qs = Reader.objects.all().prefetch_related('books_read__xyz')
     170        with self.assertRaises(AttributeError) as cm:
     171            list(qs)
     172
     173        self.assertTrue('prefetch_related' in cm.exception.message)
     174
     175    def test_invalid_final_lookup(self):
     176        qs = Book.objects.prefetch_related('authors__first_book')
     177        with self.assertRaises(ValueError) as cm:
     178            list(qs)
     179
     180        self.assertTrue('prefetch_related' in cm.exception.message)
     181        self.assertTrue("first_book" in cm.exception.message)
     182
     183
     184class DefaultManagerTests(TestCase):
     185
     186    def setUp(self):
     187        self.qual1 = Qualification.objects.create(name="BA")
     188        self.qual2 = Qualification.objects.create(name="BSci")
     189        self.qual3 = Qualification.objects.create(name="MA")
     190        self.qual4 = Qualification.objects.create(name="PhD")
     191
     192        self.teacher1 = Teacher.objects.create(name="Mr Cleese")
     193        self.teacher2 = Teacher.objects.create(name="Mr Idle")
     194        self.teacher3 = Teacher.objects.create(name="Mr Chapman")
     195
     196        self.teacher1.qualifications.add(self.qual1, self.qual2, self.qual3, self.qual4)
     197        self.teacher2.qualifications.add(self.qual1)
     198        self.teacher3.qualifications.add(self.qual2)
     199
     200        self.dept1 = Department.objects.create(name="English")
     201        self.dept2 = Department.objects.create(name="Physics")
     202
     203        self.dept1.teachers.add(self.teacher1, self.teacher2)
     204        self.dept2.teachers.add(self.teacher1, self.teacher3)
     205
     206    def test_m2m_then_m2m(self):
     207        with self.assertNumQueries(3):
     208            # When we prefetch the teachers, and force the query, we don't want
     209            # the default manager on teachers to immediately get all the related
     210            # qualifications, since this will do one query per teacher.
     211            qs = Department.objects.prefetch_related('teachers')
     212            depts = "".join(["%s department: %s\n" %
     213                             (dept.name, ", ".join(unicode(t) for t in dept.teachers.all()))
     214                             for dept in qs])
     215
     216            self.assertEqual(depts,
     217                             "English department: Mr Cleese (BA, BSci, MA, PhD), Mr Idle (BA)\n"
     218                             "Physics department: Mr Cleese (BA, BSci, MA, PhD), Mr Chapman (BSci)\n")
     219
     220
     221class GenericRelationTests(TestCase):
     222
     223    def test_traverse_GFK(self):
     224        """
     225        Test that we can traverse a 'content_object' with prefetch_related()
     226        """
     227        # In fact, there is no special support for this in prefetch_related code
     228        # - we can traverse any object that will lead us to objects that have
     229        # related managers.
     230
     231        book1 = Book.objects.create(title="Winnie the Pooh")
     232        book2 = Book.objects.create(title="Do you like green eggs and spam?")
     233
     234        reader1 = Reader.objects.create(name="me")
     235        reader2 = Reader.objects.create(name="you")
     236
     237        book1.read_by.add(reader1)
     238        book2.read_by.add(reader2)
     239
     240        TaggedItem.objects.create(tag="awesome", content_object=book1)
     241        TaggedItem.objects.create(tag="awesome", content_object=book2)
     242
     243        ct = ContentType.objects.get_for_model(Book)
     244
     245        # We get 4 queries - 1 for main query, 2 for each access to
     246        # 'content_object' because these can't be handled by select_related, and
     247        # 1 for the 'read_by' relation.
     248        with self.assertNumQueries(4):
     249            # If we limit to books, we know that they will have 'read_by'
     250            # attributes, so the following makes sense:
     251            qs = TaggedItem.objects.select_related('content_type').prefetch_related('content_object__read_by').filter(tag='awesome').filter(content_type=ct, tag='awesome')
     252            readers_of_awesome_books = [r.name for tag in qs
     253                                        for r in tag.content_object.read_by.all()]
     254            self.assertEqual(readers_of_awesome_books, ["me", "you"])
     255
     256
     257    def test_generic_relation(self):
     258        b = Bookmark.objects.create(url='http://www.djangoproject.com/')
     259        t1 = TaggedItem.objects.create(content_object=b, tag='django')
     260        t2 = TaggedItem.objects.create(content_object=b, tag='python')
     261
     262        with self.assertNumQueries(2):
     263            tags = [t.tag for b in Bookmark.objects.prefetch_related('tags')
     264                    for t in b.tags.all()]
     265            self.assertEqual(sorted(tags), ["django", "python"])
     266
     267
     268class MultiTableInheritanceTest(TestCase):
     269    def setUp(self):
     270        self.book1 = BookWithYear.objects.create(
     271            title="Poems", published_year=2010)
     272        self.book2 = BookWithYear.objects.create(
     273            title="More poems", published_year=2011)
     274        self.author1 = AuthorWithAge.objects.create(
     275            name='Jane', first_book=self.book1, age=50)
     276        self.author2 = AuthorWithAge.objects.create(
     277            name='Tom', first_book=self.book1, age=49)
     278        self.author3 = AuthorWithAge.objects.create(
     279            name='Robert', first_book=self.book2, age=48)
     280        self.authorAddress = AuthorAddress.objects.create(
     281            author=self.author1, address='SomeStreet 1')
     282        self.book2.aged_authors.add(self.author2, self.author3)
     283
     284    def test_foreignkey(self):
     285        with self.assertNumQueries(2):
     286            qs = AuthorWithAge.objects.prefetch_related('addresses')
     287            addresses = [[unicode(address) for address in obj.addresses.all()]
     288                         for obj in qs]
     289        self.assertEquals(addresses, [[unicode(self.authorAddress)], [], []])
     290
     291    def test_m2m_to_inheriting_model(self):
     292        qs = AuthorWithAge.objects.prefetch_related('books_with_year')
     293        with self.assertNumQueries(2):
     294            lst = [[unicode(book) for book in author.books_with_year.all()]
     295                   for author in qs]
     296        qs = AuthorWithAge.objects.all()
     297        lst2 = [[unicode(book) for book in author.books_with_year.all()]
     298                for author in qs]
     299        self.assertEquals(lst, lst2)
     300
     301        qs = BookWithYear.objects.prefetch_related('aged_authors')
     302        with self.assertNumQueries(2):
     303            lst = [[unicode(author) for author in book.aged_authors.all()]
     304                   for book in qs]
     305        qs = BookWithYear.objects.all()
     306        lst2 = [[unicode(author) for author in book.aged_authors.all()]
     307               for book in qs]
     308        self.assertEquals(lst, lst2)
     309
     310    def test_parent_link_prefetch(self):
     311        with self.assertRaises(ValueError) as cm:
     312            qs = list(AuthorWithAge.objects.prefetch_related('author'))
     313        self.assertTrue('prefetch_related' in cm.exception.message)
     314
     315
     316class ForeignKeyToFieldTest(TestCase):
     317    def setUp(self):
     318        self.book = Book.objects.create(title="Poems")
     319        self.author1 = Author.objects.create(name='Jane', first_book=self.book)
     320        self.author2 = Author.objects.create(name='Tom', first_book=self.book)
     321        self.author3 = Author.objects.create(name='Robert', first_book=self.book)
     322        self.authorAddress = AuthorAddress.objects.create(
     323            author=self.author1, address='SomeStreet 1'
     324        )
     325        FavoriteAuthors.objects.create(author=self.author1,
     326                                       likes_author=self.author2)
     327        FavoriteAuthors.objects.create(author=self.author2,
     328                                       likes_author=self.author3)
     329        FavoriteAuthors.objects.create(author=self.author3,
     330                                       likes_author=self.author1)
     331
     332    def test_foreignkey(self):
     333        with self.assertNumQueries(2):
     334            qs = Author.objects.prefetch_related('addresses')
     335            addresses = [[unicode(address) for address in obj.addresses.all()]
     336                         for obj in qs]
     337        self.assertEquals(addresses, [[unicode(self.authorAddress)], [], []])
     338
     339    def test_m2m(self):
     340        with self.assertNumQueries(3):
     341            qs = Author.objects.all().prefetch_related('favorite_authors', 'favors_me')
     342            favorites = [(
     343                 [unicode(i_like) for i_like in author.favorite_authors.all()],
     344                 [unicode(likes_me) for likes_me in author.favors_me.all()]
     345                ) for author in qs]
     346            self.assertEquals(
     347                favorites,
     348                [
     349                    ([unicode(self.author2)],[unicode(self.author3)]),
     350                    ([unicode(self.author3)],[unicode(self.author1)]),
     351                    ([unicode(self.author1)],[unicode(self.author2)])
     352                ]
     353            )
     354
     355
     356class LookupOrderingTest(TestCase):
     357    """
     358    Test cases that demonstrate that ordering of lookups is important, and
     359    ensure it is preserved.
     360    """
     361
     362    def setUp(self):
     363        self.person1 = Person.objects.create(name="Joe")
     364        self.person2 = Person.objects.create(name="Mary")
     365
     366        self.house1 = House.objects.create(address="123 Main St")
     367        self.house2 = House.objects.create(address="45 Side St")
     368        self.house3 = House.objects.create(address="6 Downing St")
     369        self.house4 = House.objects.create(address="7 Regents St")
     370
     371        self.room1_1 = Room.objects.create(name="Dining room", house=self.house1)
     372        self.room1_2 = Room.objects.create(name="Lounge", house=self.house1)
     373        self.room1_3 = Room.objects.create(name="Kitchen", house=self.house1)
     374
     375        self.room2_1 = Room.objects.create(name="Dining room", house=self.house2)
     376        self.room2_2 = Room.objects.create(name="Lounge", house=self.house2)
     377
     378        self.room3_1 = Room.objects.create(name="Dining room", house=self.house3)
     379        self.room3_2 = Room.objects.create(name="Lounge", house=self.house3)
     380        self.room3_3 = Room.objects.create(name="Kitchen", house=self.house3)
     381
     382        self.room4_1 = Room.objects.create(name="Dining room", house=self.house4)
     383        self.room4_2 = Room.objects.create(name="Lounge", house=self.house4)
     384
     385        self.person1.houses.add(self.house1, self.house2)
     386        self.person2.houses.add(self.house3, self.house4)
     387
     388    def test_order(self):
     389        with self.assertNumQueries(4):
     390            # The following two queries must be done in the same order as written,
     391            # otherwise 'primary_house' will cause non-prefetched lookups
     392            qs = Person.objects.prefetch_related('houses__rooms',
     393                                                 'primary_house__occupants')
     394            [list(p.primary_house.occupants.all()) for p in qs]
     395
     396
     397class NullableTest(TestCase):
     398
     399    def setUp(self):
     400        boss = Employee.objects.create(name="Peter")
     401        worker1 = Employee.objects.create(name="Joe", boss=boss)
     402        worker2 = Employee.objects.create(name="Angela", boss=boss)
     403
     404    def test_traverse_nullable(self):
     405        with self.assertNumQueries(2):
     406            qs = Employee.objects.select_related('boss').prefetch_related('boss__serfs')
     407            co_serfs = [list(e.boss.serfs.all()) if e.boss is not None else []
     408                        for e in qs]
     409
     410        qs2 =  Employee.objects.select_related('boss')
     411        co_serfs2 =  [list(e.boss.serfs.all()) if e.boss is not None else []
     412                        for e in qs]
     413
     414        self.assertEqual(co_serfs, co_serfs2)
Back to Top