Ticket #16937: prefetch_6.diff
File prefetch_6.diff, 47.9 KB (added by , 13 years ago) |
---|
-
django/contrib/contenttypes/generic.py
diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
a b 225 225 content_type = content_type, 226 226 content_type_field_name = self.field.content_type_field_name, 227 227 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, 233 229 ) 234 230 235 231 return manager … … 250 246 """ 251 247 252 248 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, 254 250 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): 256 253 257 254 super(GenericRelatedObjectManager, self).__init__() 258 self.core_filters = core_filters259 255 self.model = model 260 256 self.content_type = content_type 261 257 self.symmetrical = symmetrical … … 264 260 self.target_col_name = target_col_name 265 261 self.content_type_field_name = content_type_field_name 266 262 self.object_id_field_name = object_id_field_name 263 self.prefetch_cache_name = prefetch_cache_name 267 264 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 } 268 269 269 270 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') 272 286 273 287 def add(self, *objs): 274 288 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 432 432 self.model = rel_model 433 433 434 434 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) 437 451 438 452 def add(self, *objs): 439 453 for obj in objs: … … 482 496 """Creates a manager that subclasses 'superclass' (which is a Manager) 483 497 and adds behavior for many-to-many related objects.""" 484 498 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, 486 500 source_field_name=None, target_field_name=None, reverse=False, 487 through=None ):501 through=None, prefetch_cache_name=None): 488 502 super(ManyRelatedManager, self).__init__() 489 503 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()} 491 506 self.instance = instance 492 507 self.symmetrical = symmetrical 493 508 self.source_field_name = source_field_name 494 509 self.target_field_name = target_field_name 495 510 self.reverse = reverse 496 511 self.through = through 512 self.prefetch_cache_name = prefetch_cache_name 497 513 self._pk_val = self.instance.pk 498 514 if self._pk_val is None: 499 515 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) 500 516 501 517 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) 504 553 505 554 # If the ManyToMany relation has an intermediary model, 506 555 # the add and remove methods do not exist. … … 683 732 684 733 manager = self.related_manager_cls( 685 734 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(), 687 737 instance=instance, 688 738 symmetrical=False, 689 739 source_field_name=self.related.field.m2m_reverse_field_name(), … … 739 789 740 790 manager = self.related_manager_cls( 741 791 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, 743 794 instance=instance, 744 795 symmetrical=self.field.rel.symmetrical, 745 796 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 172 172 def select_related(self, *args, **kwargs): 173 173 return self.get_query_set().select_related(*args, **kwargs) 174 174 175 def prefetch_related(self, *args, **kwargs): 176 return self.get_query_set().prefetch_related(*args, **kwargs) 177 175 178 def values(self, *args, **kwargs): 176 179 return self.get_query_set().values(*args, **kwargs) 177 180 -
django/db/models/query.py
diff --git a/django/db/models/query.py b/django/db/models/query.py
a b 36 36 self._iter = None 37 37 self._sticky_filter = False 38 38 self._for_write = False 39 self._prefetch_related_lookups = [] 40 self._prefetch_done = False 39 41 40 42 ######################## 41 43 # PYTHON MAGIC METHODS # … … 81 83 self._result_cache = list(self.iterator()) 82 84 elif self._iter: 83 85 self._result_cache.extend(self._iter) 86 if self._prefetch_related_lookups and not self._prefetch_done: 87 self._prefetch_related_objects() 84 88 return len(self._result_cache) 85 89 86 90 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 87 97 if self._result_cache is None: 88 98 self._iter = self.iterator() 89 99 self._result_cache = [] … … 106 116 self._fill_cache() 107 117 108 118 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 109 125 if self._result_cache is not None: 110 126 return bool(self._result_cache) 111 127 try: … … 526 542 return self.query.has_results(using=self.db) 527 543 return bool(self._result_cache) 528 544 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 529 550 ################################################## 530 551 # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS # 531 552 ################################################## … … 649 670 obj.query.max_depth = depth 650 671 return obj 651 672 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 652 690 def dup_select_related(self, other): 653 691 """ 654 692 Copies the related selection status from the QuerySet 'other' to the … … 798 836 query.filter_is_sticky = True 799 837 c = klass(model=self.model, query=query, using=self._db) 800 838 c._for_write = self._for_write 839 c._prefetch_related_lookups = self._prefetch_related_lookups[:] 801 840 c.__dict__.update(kwargs) 802 841 if setup and hasattr(c, '_setup_query'): 803 842 c._setup_query() … … 863 902 # empty" result. 864 903 value_annotation = True 865 904 905 866 906 class ValuesQuerySet(QuerySet): 867 907 def __init__(self, *args, **kwargs): 868 908 super(ValuesQuerySet, self).__init__(*args, **kwargs) … … 992 1032 % self.__class__.__name__) 993 1033 return self 994 1034 1035 995 1036 class ValuesListQuerySet(ValuesQuerySet): 996 1037 def iterator(self): 997 1038 if self.flat and len(self._fields) == 1: … … 1475 1516 self._model_fields[converter(column)] = field 1476 1517 return self._model_fields 1477 1518 1519 1478 1520 def insert_query(model, objs, fields, return_id=False, raw=False, using=None): 1479 1521 """ 1480 1522 Inserts a new record for the given model. This provides an interface to … … 1484 1526 query = sql.InsertQuery(model) 1485 1527 query.insert_values(fields, objs, raw=raw) 1486 1528 return query.get_compiler(using=using).execute_sql(return_id) 1529 1530 1531 def 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 1621 def 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 571 571 manager or a ``QuerySet`` and do further filtering on the result. After calling 572 572 ``all()`` on either object, you'll definitely have a ``QuerySet`` to work with. 573 573 574 .. _select-related:575 576 574 select_related 577 575 ~~~~~~~~~~~~~~ 578 576 … … 690 688 A :class:`~django.db.models.OneToOneField` is not traversed in the reverse 691 689 direction if you are performing a depth-based ``select_related()`` call. 692 690 691 prefetch_related 692 ~~~~~~~~~~~~~~~~ 693 694 .. method:: prefetch_related(*lookups) 695 696 .. versionadded:: 1.4 697 698 Returns a ``QuerySet`` that will automatically retrieve, in a single batch, 699 related many-to-many and many-to-one objects for each of the specified lookups. 700 701 This is similar to ``select_related`` for the 'many related objects' case, but 702 note that ``prefetch_related`` causes a separate query to be issued for each set 703 of related objects that you request, unlike ``select_related`` which modifies 704 the original query with joins in order to get the related objects. With 705 ``prefetch_related``, the additional queries are done as soon as the QuerySet 706 begins to be evaluated. 707 708 For 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 721 and run this code:: 722 723 >>> Pizza.objects.all() 724 [u"Hawaiian (ham, pineapple)", u"Seafood (prawns, smoked salmon)"... 725 726 The 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 728 be reduced to two: 729 730 >>> Pizza.objects.all().prefetch_related('toppings') 731 732 All 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 736 Please note that use of ``prefetch_related`` will mean that the additional 737 queries run will **always** be executed - even if you never use the related 738 objects - and it always fully populates the result cache on the primary 739 ``QuerySet`` (which can sometimes be avoided in other cases). 740 741 Also remember that, as always with QuerySets, any subsequent chained methods 742 will ignore previously cached results, and retrieve data using a fresh database 743 query. So, if you write the following: 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 749 you - in fact it hurts performance, since you have done a database query that 750 you haven't used. So use this feature with caution! 751 752 The lookups that must be supplied to this method can be any attributes on the 753 model instances which represent related queries that return multiple 754 objects. This includes attributes representing the 'many' side of ``ForeignKey`` 755 relationships, forward and reverse ``ManyToManyField`` attributes, and also any 756 ``GenericRelations``. 757 758 You can also use the normal join syntax to do related fields of related 759 fields. 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 765 The following are all legal: 766 767 >>> Restaurant.objects.prefetch_related('pizzas__toppings') 768 769 This will prefetch all pizzas belonging to restaurants, and all toppings 770 belonging to those pizzas. This will result in a total of 3 database queries - 771 one 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 775 This will fetch the best pizza and all the toppings for the best pizza for each 776 restaurant. This will be done in 2 database queries - one for the restaurants 777 and 'best pizzas' combined (achieved through use of ``select_related``), and one 778 for the toppings. 779 780 Chaining ``prefetch_related`` calls will accumulate the fields that should have 781 this behavior applied. To clear any ``prefetch_related`` behavior, pass `None` 782 as a parameter:: 783 784 >>> non_prefetched = qs.prefetch_related(None) 785 786 One difference when using ``prefetch_related`` is that, in some circumstances, 787 objects created by a query can be shared between the different objects that they 788 are related to i.e. a single Python model instance can appear at more than one 789 point in the tree of objects that are returned. Normally this behavior will not 790 be a problem, and will in fact save both memory and CPU time. 791 693 792 extra 694 793 ~~~~~ 695 794 -
docs/releases/1.4.txt
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
a b 63 63 See the :meth:`~django.db.models.query.QuerySet.bulk_create` docs for more 64 64 information. 65 65 66 ``QuerySet.prefetch_related`` 67 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 68 69 Analagous to :meth:`~django.db.models.query.QuerySet.select_related` but for 70 many-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`` 73 that will prefetch in a single batch each of the specified related lookups as 74 soon as it begins to be evaluated (e.g. by iterating over it). This enables you 75 to fix many instances of a very common performance problem, in which your code 76 ends 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 66 79 HTML5 67 80 ~~~~~ 68 81 -
docs/topics/db/optimization.txt
diff --git a/docs/topics/db/optimization.txt b/docs/topics/db/optimization.txt
a b 141 141 query that is executed in a loop, and could therefore end up doing many database 142 142 queries, when only one was needed. So: 143 143 144 Use ``QuerySet.select_related()`` 145 --------------------------------- 144 Use ``QuerySet.select_related()`` and ``prefetch_related()`` 145 ------------------------------------------------------------ 146 146 147 Understand :ref:`QuerySet.select_related() <select-related>` thoroughly, and use it: 147 Understand :meth:`~django.db.models.query.QuerySet.select_related` and 148 :meth:`~django.db.models.query.QuerySet.prefetch_related` thoroughly, and use 149 them: 148 150 149 151 * in view code, 150 152 -
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
- + 1 from django.contrib.contenttypes.models import ContentType 2 from django.contrib.contenttypes import generic 3 from django.db import models 4 5 ## Basic tests 6 7 class 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 20 class AuthorWithAge(Author): 21 author = models.OneToOneField(Author, parent_link=True) 22 age = models.IntegerField() 23 24 25 class 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 33 class 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 44 class 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 54 class 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 61 class 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 74 class Qualification(models.Model): 75 name = models.CharField(max_length=10) 76 77 class Meta: 78 ordering = ['id'] 79 80 81 class TeacherManager(models.Manager): 82 def get_query_set(self): 83 return super(TeacherManager, self).get_query_set().prefetch_related('qualifications') 84 85 86 class 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 99 class 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 109 class 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 119 class Bookmark(models.Model): 120 url = models.URLField() 121 tags = generic.GenericRelation(TaggedItem) 122 123 124 ## Models for lookup ordering tests 125 126 127 class House(models.Model): 128 address = models.CharField(max_length=255) 129 130 class Meta: 131 ordering = ['id'] 132 133 class 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 141 class 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
- + 1 from django.contrib.contenttypes.models import ContentType 2 from django.test import TestCase 3 from django.utils import unittest 4 5 from models import (Author, Book, Reader, Qualification, Teacher, Department, 6 TaggedItem, Bookmark, AuthorAddress, FavoriteAuthors, 7 AuthorWithAge, BookWithYear, Person, House, Room) 8 9 class 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 183 class 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 220 class 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 267 class 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 315 class 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 355 class 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