Ticket #16937: prefetch_5.diff

File prefetch_5.diff, 43.3 KB (added by Luke Plant, 13 years ago)

Updated patch, see comments

  • 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):
    270271            db = self._db or router.db_for_read(self.model, instance=self.instance)
    271272            return super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**self.core_filters)
    272273
     274        def get_prefetch_query_set(self, instances):
     275            if not instances:
     276                return self.model._default_manager.none()
     277
     278            db = self._db or router.db_for_read(self.model, instance=instances[0])
     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')
     286
     287        def all(self):
     288            try:
     289                return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
     290            except (AttributeError, KeyError):
     291                return super(GenericRelatedObjectManager, self).all()
     292
     293
    273294        def add(self, *objs):
    274295            for obj in objs:
    275296                if not isinstance(obj, self.model):
  • django/db/models/fields/related.py

    diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
    a b  
    435435                db = self._db or router.db_for_read(self.model, instance=self.instance)
    436436                return super(RelatedManager, self).get_query_set().using(db).filter(**(self.core_filters))
    437437
     438            def get_prefetch_query_set(self, instances):
     439                """
     440                Return a queryset that does the bulk lookup needed
     441                by prefetch_related functionality.
     442                """
     443                db = self._db or router.db_for_read(self.model)
     444                query = {'%s__%s__in' % (rel_field.name, attname):
     445                             [getattr(obj, attname) for obj in instances]}
     446                qs = super(RelatedManager, self).get_query_set().using(db).filter(**query)
     447                return (qs, rel_field.get_attname(), attname)
     448
     449            def all(self):
     450                try:
     451                    return self.instance._prefetched_objects_cache[rel_field.related_query_name()]
     452                except (AttributeError, KeyError):
     453                    return super(RelatedManager, self).all()
     454
    438455            def add(self, *objs):
    439456                for obj in objs:
    440457                    if not isinstance(obj, self.model):
     
    482499    """Creates a manager that subclasses 'superclass' (which is a Manager)
    483500    and adds behavior for many-to-many related objects."""
    484501    class ManyRelatedManager(superclass):
    485         def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
     502        def __init__(self, model=None, query_field_name=None, instance=None, symmetrical=None,
    486503                     source_field_name=None, target_field_name=None, reverse=False,
    487                      through=None):
     504                     through=None, prefetch_cache_name=None):
    488505            super(ManyRelatedManager, self).__init__()
    489506            self.model = model
    490             self.core_filters = core_filters
     507            self.query_field_name = query_field_name
     508            self.core_filters = {'%s__pk' % query_field_name: instance._get_pk_val()}
    491509            self.instance = instance
    492510            self.symmetrical = symmetrical
    493511            self.source_field_name = source_field_name
    494512            self.target_field_name = target_field_name
    495513            self.reverse = reverse
    496514            self.through = through
     515            self.prefetch_cache_name = prefetch_cache_name
    497516            self._pk_val = self.instance.pk
    498517            if self._pk_val is None:
    499518                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
     
    502521            db = self._db or router.db_for_read(self.instance.__class__, instance=self.instance)
    503522            return super(ManyRelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**(self.core_filters))
    504523
     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.
     539
     540            # We know that there will already be a join on the join table, so we
     541            # can just add the select.
     542
     543            # For non-autocreated 'through' models, can't assume we are
     544            # dealing with PK values.
     545            fk = self.through._meta.get_field(self.source_field_name)
     546            source_col = fk.column
     547            join_table = self.through._meta.db_table
     548            connection = connections[db]
     549            qn = connection.ops.quote_name
     550            qs = qs.extra(select={'_prefetch_related_val':
     551                                      '%s.%s' % (qn(join_table), qn(source_col))})
     552            select_attname = fk.rel.get_related_field().get_attname()
     553            return (qs, '_prefetch_related_val', select_attname)
     554
     555        def all(self):
     556            try:
     557                return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
     558            except (AttributeError, KeyError):
     559                return super(ManyRelatedManager, self).all()
     560
    505561        # If the ManyToMany relation has an intermediary model,
    506562        # the add and remove methods do not exist.
    507563        if rel.through._meta.auto_created:
     
    683739
    684740        manager = self.related_manager_cls(
    685741            model=rel_model,
    686             core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()},
     742            query_field_name=self.related.field.name,
     743            prefetch_cache_name=self.related.field.related_query_name(),
    687744            instance=instance,
    688745            symmetrical=False,
    689746            source_field_name=self.related.field.m2m_reverse_field_name(),
     
    739796
    740797        manager = self.related_manager_cls(
    741798            model=self.field.rel.to,
    742             core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()},
     799            query_field_name=self.field.related_query_name(),
     800            prefetch_cache_name=self.field.name,
    743801            instance=instance,
    744802            symmetrical=self.field.rel.symmetrical,
    745803            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 = set()
     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 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:
     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:
     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)
     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, *fields):
     674        """
     675        Returns a new QuerySet instance that will prefetch Many-To-One
     676        and Many-To-Many related objects when the QuerySet is evaluated.
     677
     678        The fields specified must be attributes that return a RelatedManager of
     679        some kind when used on instances of the evaluated QuerySet.
     680
     681        These RelatedManagers will be modified so that their 'all()' method will
     682        return a QuerySet whose cache is already filled with objects that were
     683        looked up in a single batch, rather than one query per object in the
     684        current QuerySet.
     685
     686        When prefetch_related() is called more than once, the list of fields to
     687        prefetch is added to. If prefetch_related() is called with no arguments
     688        the list is cleared.
     689        """
     690        if fields == (None,):
     691            new_fields = set()
     692        else:
     693            new_fields = self._prefetch_related.union(set(fields))
     694        return self._clone(_prefetch_related=new_fields)
     695
    652696    def dup_select_related(self, other):
    653697        """
    654698        Copies the related selection status from the QuerySet 'other' to the
     
    798842            query.filter_is_sticky = True
    799843        c = klass(model=self.model, query=query, using=self._db)
    800844        c._for_write = self._for_write
     845        c._prefetch_related = self._prefetch_related
    801846        c.__dict__.update(kwargs)
    802847        if setup and hasattr(c, '_setup_query'):
    803848            c._setup_query()
     
    14841529    query = sql.InsertQuery(model)
    14851530    query.insert_values(fields, objs, raw=raw)
    14861531    return query.get_compiler(using=using).execute_sql(return_id)
     1532
     1533
     1534def prefetch_related_objects(result_cache, fields):
     1535    """
     1536    Populates prefetched objects caches for a list of results
     1537    from a QuerySet
     1538    """
     1539    from django.db.models.sql.constants import LOOKUP_SEP
     1540
     1541    if len(result_cache) == 0:
     1542        return # nothing to do
     1543
     1544    model = result_cache[0].__class__
     1545
     1546    # We need to be able to dynamically add to the list of prefetch_related
     1547    # fields that we look up (see below).  So we need some book keeping to
     1548    # ensure we don't do duplicate work.
     1549    done_fields = set() # list of fields like foo__bar__baz
     1550    done_lookups = {}   # dictionary of things like 'foo__bar': [results]
     1551    fields = list(fields)
     1552
     1553    # We may expand fields, so need a loop that allows for that
     1554    i = 0
     1555    while i < len(fields):
     1556        # 'field' can span several relationships, and so represent multiple
     1557        # lookups.
     1558        field = fields[i]
     1559
     1560        if field in done_fields:
     1561            # We've done exactly this already, skip the whole thing
     1562            i += 1
     1563            continue
     1564        done_fields.add(field)
     1565
     1566        # Top level, the list of objects to decorate is the the result cache
     1567        # from the primary QuerySet. It won't be for deeper levels.
     1568        obj_list = result_cache
     1569
     1570        attrs = field.split(LOOKUP_SEP)
     1571        for level, attr in enumerate(attrs):
     1572            # Prepare main instances
     1573            if len(obj_list) == 0:
     1574                break
     1575
     1576            good_objects = True
     1577            for obj in obj_list:
     1578                if not hasattr(obj, '_prefetched_objects_cache'):
     1579                    try:
     1580                        obj._prefetched_objects_cache = {}
     1581                    except AttributeError:
     1582                        # Must be in a QuerySet subclass that is not returning
     1583                        # Model instances, either in Django or 3rd
     1584                        # party. prefetch_related() doesn't make sense, so quit
     1585                        # now.
     1586                        good_objects = False
     1587                        break
     1588            if not good_objects:
     1589                break
     1590
     1591            # Descend down tree
     1592            try:
     1593                rel_obj = getattr(obj_list[0], attr)
     1594            except AttributeError:
     1595                raise AttributeError("Cannot find '%s' on %s object, '%s' is an invalid "
     1596                                     "parameter to prefetch_related()" %
     1597                                     (attr, obj_list[0].__class__.__name__, field))
     1598
     1599            can_prefetch = hasattr(rel_obj, 'get_prefetch_query_set')
     1600            if level == len(attrs) - 1 and not can_prefetch:
     1601                # Last one, this *must* resolve to a related manager.
     1602                raise ValueError("'%s' does not resolve to a supported 'many related"
     1603                                 " manager' for model %s - this is an invalid"
     1604                                 " parameter to prefetch_related()."
     1605                                 % (field, model.__name__))
     1606
     1607            if can_prefetch:
     1608                # Check we didn't do this already
     1609                lookup = LOOKUP_SEP.join(attrs[0:level+1])
     1610                if lookup in done_lookups:
     1611                    obj_list = done_lookups[lookup]
     1612                else:
     1613                    relmanager = rel_obj
     1614                    obj_list, additional_prf = _prefetch_one_level(obj_list, relmanager, attr)
     1615                    for f in additional_prf:
     1616                        new_prf = LOOKUP_SEP.join([lookup, f])
     1617                        fields.append(new_prf)
     1618                    done_lookups[lookup] = obj_list
     1619            else:
     1620                # Assume we've got some singly related object. We replace
     1621                # the current list of parent objects with that list.
     1622                obj_list = [getattr(obj, attr) for obj in obj_list]
     1623
     1624        i += 1
     1625
     1626
     1627def _prefetch_one_level(instances, relmanager, attname):
     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 fields
     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 fields 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 fields.
     1641    additional_prf = list(getattr(rel_qs, '_prefetch_related', []))
     1642    if additional_prf:
     1643        rel_qs = rel_qs.prefetch_related(None)
     1644    all_related_objects = list(rel_qs)
     1645    for obj in instances:
     1646        qs = getattr(obj, attname).all()
     1647        instance_attr_val = getattr(obj, instance_attr)
     1648        qs._result_cache = [rel_obj for rel_obj in all_related_objects
     1649                            if getattr(rel_obj, rel_obj_attr) == instance_attr_val]
     1650        # We don't want the individual qs doing prefetch_related now, since we
     1651        # have merged this into the current work.
     1652        qs._prefetch_done = True
     1653        obj._prefetched_objects_cache[attname] = qs
     1654    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  
    690690A :class:`~django.db.models.OneToOneField` is not traversed in the reverse
    691691direction if you are performing a depth-based ``select_related()`` call.
    692692
     693prefetch_related
     694~~~~~~~~~~~~~~~~
     695
     696.. method:: prefetch_related(*fields)
     697
     698.. versionadded:: 1.4
     699
     700Returns a ``QuerySet`` that will automatically retrieve, in a single batch,
     701related many-to-many and many-to-one objects for the specified fields.
     702
     703This is similar to ``select_related`` for the 'many related objects' case, but
     704note that ``prefetch_related`` causes a separate query to be issued for each set
     705of related objects that you request, unlike ``select_related`` which modifies
     706the original query with joins in order to get the related objects. With
     707``prefetch_related``, the additional queries are done as soon as the QuerySet
     708begins to be evaluated.
     709
     710For example, suppose you have these models::
     711
     712    class Topping(models.Model):
     713        name = models.CharField(max_length=30)
     714
     715    class Pizza(models.Model):
     716        name = models.CharField(max_length=50)
     717        toppings = models.ManyToManyField(Topping)
     718
     719        def __unicode__(self):
     720            return u"%s (%s)" % (self.name, u", ".join([topping.name
     721                                                        for topping in self.toppings.all()]))
     722
     723and run this code::
     724
     725    >>> Pizza.objects.all()
     726    [u"Hawaiian (ham, pineapple)", u"Seafood (prawns, smoked salmon)"...
     727
     728The problem with this code is that it will run a query on the Toppings table for
     729**every** item in the Pizza ``QuerySet``.  Using ``prefetch_related``, this can
     730be reduced to two:
     731
     732    >>> pizzas = Pizza.objects.all().prefetch_related('toppings')
     733
     734All the relevant toppings will be fetched in a single query, and used to make
     735``QuerySets`` that have a pre-filled cache of the relevant results. These
     736``QuerySets`` are then used in the ``self.toppings.all()`` calls.
     737
     738Please note that use of ``prefetch_related`` will mean that the additional
     739queries run will **always** be executed - even if you never use the related
     740objects - and it always fully populates the result cache on the primary
     741``QuerySet`` (which can sometimes be avoided in other cases).
     742
     743Also remember that, as always with QuerySets, any subsequent chained methods
     744will ignore previously cached results, and retrieve data using a fresh database
     745query. So, if you write the following:
     746
     747    >>> pizzas = Pizza.objects.prefetch_related('toppings')
     748    >>> [list(pizza.topppings.filter(spicy=True) for pizza in pizzas]
     749
     750...then the fact that `pizza.toppings.all()` has been prefetched will not help
     751you - in fact it hurts performance, since you have done a database query that
     752you haven't used. So use this feature with caution!
     753
     754The fields that must be supplied to this method can be any attributes on the
     755model instances which represent related queries that return multiple
     756objects. This includes attributes representing the 'many' side of ``ForeignKey``
     757relationships, forward and reverse ``ManyToManyField`` attributes, and also any
     758``GenericRelations``.
     759
     760You can also use the normal join syntax to do related fields of related
     761fields. Suppose we have an additional model to the example above::
     762
     763    class Restaurant(models.Model):
     764        pizzas = models.ManyToMany(Pizza, related_name='restaurants')
     765        best_pizza = models.ForeignKey(Pizza, related_name='championed_by')
     766
     767The following are all legal:
     768
     769    >>> Restaurant.objects.prefetch_related('pizzas__toppings')
     770
     771This will prefetch all pizzas belonging to restaurants, and all toppings
     772belonging to those pizzas. This will result in a total of 3 database queries -
     773one for the restaurants, one for the pizzas, and one for the toppings.
     774
     775    >>> Restaurant.objects.select_related('best_pizza').prefetch_related('best_pizza__toppings')
     776
     777This will fetch the best pizza and all the toppings for the best pizza for each
     778restaurant. This will be done in 2 database queries - one for the restaurants
     779and 'best pizzas' combined (achieved through use of ``select_related``), and one
     780for the toppings.
     781
     782Chaining ``prefetch_related`` calls will accumulate the fields that should have
     783this behavior applied. To clear any ``prefetch_related`` behavior, pass `None`
     784as a parameter::
     785
     786   >>> unprefetch = qs.prefetch_related(None)
     787
     788When using ``prefetch_related``, one difference is that, in some circumstances,
     789objects created by a query can be shared between the different objects that they
     790are related to i.e. a single Python model instance can appear at more than one
     791point in the tree of objects that are returned. Normally this behavior will not
     792be a problem, and will in fact save both memory and CPU time.
     793
    693794extra
    694795~~~~~
    695796
  • 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)
  • 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)
     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_clear(self):
     80        """
     81        Test that we can clear the behavior by calling prefetch_related()
     82        """
     83        with self.assertNumQueries(5):
     84            with_prefetch = Author.objects.prefetch_related('books')
     85            without_prefetch = with_prefetch.prefetch_related(None)
     86            lists = [list(a.books.all()) for a in without_prefetch]
     87
     88    def test_m2m_then_m2m(self):
     89        """
     90        Test we can follow a m2m and another m2m
     91        """
     92        with self.assertNumQueries(3):
     93            qs = Author.objects.prefetch_related('books__read_by')
     94            lists = [[[unicode(r) for r in b.read_by.all()]
     95                      for b in a.books.all()]
     96                     for a in qs]
     97            self.assertEqual(lists,
     98            [
     99                [[u"Amy"], [u"Belinda"]],  # Charlotte - Poems, Jane Eyre
     100                [[u"Amy"]],                # Anne - Poems
     101                [[u"Amy"], []],            # Emily - Poems, Wuthering Heights
     102                [[u"Amy", u"Belinda"]],    # Jane - Sense and Sense
     103            ])
     104
     105    def test_overriding_prefetch(self):
     106        with self.assertNumQueries(3):
     107            qs = Author.objects.prefetch_related('books', 'books__read_by')
     108            lists = [[[unicode(r) for r in b.read_by.all()]
     109                      for b in a.books.all()]
     110                     for a in qs]
     111            self.assertEqual(lists,
     112            [
     113                [[u"Amy"], [u"Belinda"]],  # Charlotte - Poems, Jane Eyre
     114                [[u"Amy"]],                # Anne - Poems
     115                [[u"Amy"], []],            # Emily - Poems, Wuthering Heights
     116                [[u"Amy", u"Belinda"]],    # Jane - Sense and Sense
     117            ])
     118        with self.assertNumQueries(3):
     119            qs = Author.objects.prefetch_related('books__read_by', 'books')
     120            lists = [[[unicode(r) for r in b.read_by.all()]
     121                      for b in a.books.all()]
     122                     for a in qs]
     123            self.assertEqual(lists,
     124            [
     125                [[u"Amy"], [u"Belinda"]],  # Charlotte - Poems, Jane Eyre
     126                [[u"Amy"]],                # Anne - Poems
     127                [[u"Amy"], []],            # Emily - Poems, Wuthering Heights
     128                [[u"Amy", u"Belinda"]],    # Jane - Sense and Sense
     129            ])
     130
     131    def test_get(self):
     132        """
     133        Test that objects retrieved with .get() get the prefetch behaviour
     134        """
     135        # Need a double
     136        with self.assertNumQueries(3):
     137            author = Author.objects.prefetch_related('books__read_by').get(name="Charlotte")
     138            lists = [[unicode(r) for r in b.read_by.all()]
     139                      for b in author.books.all()]
     140            self.assertEqual(lists, [[u"Amy"], [u"Belinda"]])  # Poems, Jane Eyre
     141
     142    def test_foreign_key_then_m2m(self):
     143        """
     144        Test we can follow an m2m relation after a relation like ForeignKey
     145        that doesn't have many objects
     146        """
     147
     148        with self.assertNumQueries(2):
     149            qs = Author.objects.select_related('first_book').prefetch_related('first_book__read_by')
     150            lists = [[unicode(r) for r in a.first_book.read_by.all()]
     151                     for a in qs]
     152            self.assertEqual(lists, [[u"Amy"],
     153                                     [u"Amy"],
     154                                     [u"Amy"],
     155                                     [u"Amy", "Belinda"]])
     156
     157    def test_reuse(self):
     158        # Check re-use of objects.
     159        qs1 = Reader.objects.all()
     160        qs2 = Reader.objects.prefetch_related('books_read__first_time_authors')
     161
     162        authors1 = [a for r in qs1
     163                    for b in r.books_read.all()
     164                    for a in b.first_time_authors.all()]
     165        authors2 = [a for r in qs2
     166                    for b in r.books_read.all()
     167                    for a in b.first_time_authors.all()]
     168
     169        # The second prefetch_related lookup is a reverse foreign key. This
     170        # means the query for it can only be something like
     171        # "first_time_authors__pk__in = [...]" and cannot return more rows then
     172        # the number of Author objects in the database.  This means that these
     173        # objects will be reused (since in our data we've arranged for there
     174        # len(authors1) > Author.objects.count())
     175
     176        total_authors = Author.objects.count()
     177        self.assertEqual(len(authors1), len(authors2))
     178        self.assertTrue(len(authors1) > total_authors)
     179        self.assertTrue(len(set(map(id, authors1))) > len(set(map(id, authors2))))
     180        self.assertEqual(total_authors, len(set(map(id, authors2))))
     181
     182    def test_attribute_error(self):
     183        qs = Reader.objects.all().prefetch_related('books_read__xyz')
     184        with self.assertRaises(AttributeError) as cm:
     185            list(qs)
     186
     187        self.assertTrue('prefetch_related' in cm.exception.message)
     188
     189    def test_invalid_final_lookup(self):
     190        qs = Book.objects.prefetch_related('authors__first_book')
     191        with self.assertRaises(ValueError) as cm:
     192            list(qs)
     193
     194        self.assertTrue('prefetch_related' in cm.exception.message)
     195        self.assertTrue("first_book" in cm.exception.message)
     196
     197
     198class DefaultManagerTests(TestCase):
     199
     200    def setUp(self):
     201        self.qual1 = Qualification.objects.create(name="BA")
     202        self.qual2 = Qualification.objects.create(name="BSci")
     203        self.qual3 = Qualification.objects.create(name="MA")
     204        self.qual4 = Qualification.objects.create(name="PhD")
     205
     206        self.teacher1 = Teacher.objects.create(name="Mr Cleese")
     207        self.teacher2 = Teacher.objects.create(name="Mr Idle")
     208        self.teacher3 = Teacher.objects.create(name="Mr Chapman")
     209
     210        self.teacher1.qualifications.add(self.qual1, self.qual2, self.qual3, self.qual4)
     211        self.teacher2.qualifications.add(self.qual1)
     212        self.teacher3.qualifications.add(self.qual2)
     213
     214        self.dept1 = Department.objects.create(name="English")
     215        self.dept2 = Department.objects.create(name="Physics")
     216
     217        self.dept1.teachers.add(self.teacher1, self.teacher2)
     218        self.dept2.teachers.add(self.teacher1, self.teacher3)
     219
     220    def test_m2m_then_m2m(self):
     221        with self.assertNumQueries(3):
     222            # When we prefetch the teachers, and force the query, we don't want
     223            # the default manager on teachers to immediately get all the related
     224            # qualifications, since this will do one query per teacher.
     225            qs = Department.objects.prefetch_related('teachers')
     226            depts = "".join(["%s department: %s\n" %
     227                             (dept.name, ", ".join(unicode(t) for t in dept.teachers.all()))
     228                             for dept in qs])
     229
     230            self.assertEqual(depts,
     231                             "English department: Mr Cleese (BA, BSci, MA, PhD), Mr Idle (BA)\n"
     232                             "Physics department: Mr Cleese (BA, BSci, MA, PhD), Mr Chapman (BSci)\n")
     233
     234
     235class GenericRelationTests(TestCase):
     236
     237    def test_traverse_GFK(self):
     238        """
     239        Test that we can traverse a 'content_object' with prefetch_related()
     240        """
     241        # In fact, there is no special support for this in prefetch_related code
     242        # - we can traverse any object that will lead us to objects that have
     243        # related managers.
     244
     245        book1 = Book.objects.create(title="Winnie the Pooh")
     246        book2 = Book.objects.create(title="Do you like green eggs and spam?")
     247
     248        reader1 = Reader.objects.create(name="me")
     249        reader2 = Reader.objects.create(name="you")
     250
     251        book1.read_by.add(reader1)
     252        book2.read_by.add(reader2)
     253
     254        TaggedItem.objects.create(tag="awesome", content_object=book1)
     255        TaggedItem.objects.create(tag="awesome", content_object=book2)
     256
     257        ct = ContentType.objects.get_for_model(Book)
     258
     259        # We get 4 queries - 1 for main query, 2 for each access to
     260        # 'content_object' because these can't be handled by select_related, and
     261        # 1 for the 'read_by' relation.
     262        with self.assertNumQueries(4):
     263            # If we limit to books, we know that they will have 'read_by'
     264            # attributes, so the following makes sense:
     265            qs = TaggedItem.objects.select_related('content_type').prefetch_related('content_object__read_by').filter(tag='awesome').filter(content_type=ct, tag='awesome')
     266            readers_of_awesome_books = [r.name for tag in qs
     267                                        for r in tag.content_object.read_by.all()]
     268            self.assertEqual(readers_of_awesome_books, ["me", "you"])
     269
     270
     271    def test_generic_relation(self):
     272        b = Bookmark.objects.create(url='http://www.djangoproject.com/')
     273        t1 = TaggedItem.objects.create(content_object=b, tag='django')
     274        t2 = TaggedItem.objects.create(content_object=b, tag='python')
     275
     276        with self.assertNumQueries(2):
     277            tags = [t.tag for b in Bookmark.objects.prefetch_related('tags')
     278                    for t in b.tags.all()]
     279            self.assertEqual(sorted(tags), ["django", "python"])
     280
     281
     282class MultiTableInheritanceTest(TestCase):
     283    def setUp(self):
     284        self.book1 = BookWithYear.objects.create(
     285            title="Poems", published_year=2010)
     286        self.book2 = BookWithYear.objects.create(
     287            title="More poems", published_year=2011)
     288        self.author1 = AuthorWithAge.objects.create(
     289            name='Jane', first_book=self.book1, age=50)
     290        self.author2 = AuthorWithAge.objects.create(
     291            name='Tom', first_book=self.book1, age=49)
     292        self.author3 = AuthorWithAge.objects.create(
     293            name='Robert', first_book=self.book2, age=48)
     294        self.authorAddress = AuthorAddress.objects.create(
     295            author=self.author1, address='SomeStreet 1')
     296        self.book2.aged_authors.add(self.author2, self.author3)
     297
     298    def test_foreignkey(self):
     299        with self.assertNumQueries(2):
     300            qs = AuthorWithAge.objects.prefetch_related('addresses')
     301            addresses = [[unicode(address) for address in obj.addresses.all()]
     302                         for obj in qs]
     303        self.assertEquals(addresses, [[unicode(self.authorAddress)], [], []])
     304
     305    def test_m2m_to_inheriting_model(self):
     306        qs = AuthorWithAge.objects.prefetch_related('books_with_year')
     307        with self.assertNumQueries(2):
     308            lst = [[unicode(book) for book in author.books_with_year.all()]
     309                   for author in qs]
     310        qs = AuthorWithAge.objects.all()
     311        lst2 = [[unicode(book) for book in author.books_with_year.all()]
     312                for author in qs]
     313        self.assertEquals(lst, lst2)
     314
     315        qs = BookWithYear.objects.prefetch_related('aged_authors')
     316        with self.assertNumQueries(2):
     317            lst = [[unicode(author) for author in book.aged_authors.all()]
     318                   for book in qs]
     319        qs = BookWithYear.objects.all()
     320        lst2 = [[unicode(author) for author in book.aged_authors.all()]
     321               for book in qs]
     322        self.assertEquals(lst, lst2)
     323
     324    def test_parent_link_prefetch(self):
     325        with self.assertRaises(ValueError) as cm:
     326            qs = list(AuthorWithAge.objects.prefetch_related('author'))
     327        self.assertTrue('prefetch_related' in cm.exception.message)
     328
     329
     330class ForeignKeyToFieldTest(TestCase):
     331    def setUp(self):
     332        self.book = Book.objects.create(title="Poems")
     333        self.author1 = Author.objects.create(name='Jane', first_book=self.book)
     334        self.author2 = Author.objects.create(name='Tom', first_book=self.book)
     335        self.author3 = Author.objects.create(name='Robert', first_book=self.book)
     336        self.authorAddress = AuthorAddress.objects.create(
     337            author=self.author1, address='SomeStreet 1'
     338        )
     339        FavoriteAuthors.objects.create(author=self.author1,
     340                                       likes_author=self.author2)
     341        FavoriteAuthors.objects.create(author=self.author2,
     342                                       likes_author=self.author3)
     343        FavoriteAuthors.objects.create(author=self.author3,
     344                                       likes_author=self.author1)
     345
     346    def test_foreignkey(self):
     347        with self.assertNumQueries(2):
     348            qs = Author.objects.prefetch_related('addresses')
     349            addresses = [[unicode(address) for address in obj.addresses.all()]
     350                         for obj in qs]
     351        self.assertEquals(addresses, [[unicode(self.authorAddress)], [], []])
     352
     353    def test_m2m(self):
     354        with self.assertNumQueries(3):
     355            qs = Author.objects.all().prefetch_related('favorite_authors', 'favors_me')
     356            favorites = [(
     357                 [unicode(i_like) for i_like in author.favorite_authors.all()],
     358                 [unicode(likes_me) for likes_me in author.favors_me.all()]
     359                ) for author in qs]
     360            self.assertEquals(
     361                favorites,
     362                [
     363                    ([unicode(self.author2)],[unicode(self.author3)]),
     364                    ([unicode(self.author3)],[unicode(self.author1)]),
     365                    ([unicode(self.author1)],[unicode(self.author2)])
     366                ]
     367            )
Back to Top