Ticket #16937: prefetch_6.diff

File prefetch_6.diff, 47.9 KB (added by Luke Plant, 13 years ago)

Various updates, see last comment

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