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