Ticket #16937: prefetch_5.diff
File prefetch_5.diff, 43.3 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 271 db = self._db or router.db_for_read(self.model, instance=self.instance) 271 272 return super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**self.core_filters) 272 273 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 273 294 def add(self, *objs): 274 295 for obj in objs: 275 296 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 435 435 db = self._db or router.db_for_read(self.model, instance=self.instance) 436 436 return super(RelatedManager, self).get_query_set().using(db).filter(**(self.core_filters)) 437 437 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 438 455 def add(self, *objs): 439 456 for obj in objs: 440 457 if not isinstance(obj, self.model): … … 482 499 """Creates a manager that subclasses 'superclass' (which is a Manager) 483 500 and adds behavior for many-to-many related objects.""" 484 501 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, 486 503 source_field_name=None, target_field_name=None, reverse=False, 487 through=None ):504 through=None, prefetch_cache_name=None): 488 505 super(ManyRelatedManager, self).__init__() 489 506 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()} 491 509 self.instance = instance 492 510 self.symmetrical = symmetrical 493 511 self.source_field_name = source_field_name 494 512 self.target_field_name = target_field_name 495 513 self.reverse = reverse 496 514 self.through = through 515 self.prefetch_cache_name = prefetch_cache_name 497 516 self._pk_val = self.instance.pk 498 517 if self._pk_val is None: 499 518 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) … … 502 521 db = self._db or router.db_for_read(self.instance.__class__, instance=self.instance) 503 522 return super(ManyRelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**(self.core_filters)) 504 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. 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 505 561 # If the ManyToMany relation has an intermediary model, 506 562 # the add and remove methods do not exist. 507 563 if rel.through._meta.auto_created: … … 683 739 684 740 manager = self.related_manager_cls( 685 741 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(), 687 744 instance=instance, 688 745 symmetrical=False, 689 746 source_field_name=self.related.field.m2m_reverse_field_name(), … … 739 796 740 797 manager = self.related_manager_cls( 741 798 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, 743 801 instance=instance, 744 802 symmetrical=self.field.rel.symmetrical, 745 803 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 = set() 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 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: 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: 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) 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, *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 652 696 def dup_select_related(self, other): 653 697 """ 654 698 Copies the related selection status from the QuerySet 'other' to the … … 798 842 query.filter_is_sticky = True 799 843 c = klass(model=self.model, query=query, using=self._db) 800 844 c._for_write = self._for_write 845 c._prefetch_related = self._prefetch_related 801 846 c.__dict__.update(kwargs) 802 847 if setup and hasattr(c, '_setup_query'): 803 848 c._setup_query() … … 1484 1529 query = sql.InsertQuery(model) 1485 1530 query.insert_values(fields, objs, raw=raw) 1486 1531 return query.get_compiler(using=using).execute_sql(return_id) 1532 1533 1534 def 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 1627 def _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 690 690 A :class:`~django.db.models.OneToOneField` is not traversed in the reverse 691 691 direction if you are performing a depth-based ``select_related()`` call. 692 692 693 prefetch_related 694 ~~~~~~~~~~~~~~~~ 695 696 .. method:: prefetch_related(*fields) 697 698 .. versionadded:: 1.4 699 700 Returns a ``QuerySet`` that will automatically retrieve, in a single batch, 701 related many-to-many and many-to-one objects for the specified fields. 702 703 This is similar to ``select_related`` for the 'many related objects' case, but 704 note that ``prefetch_related`` causes a separate query to be issued for each set 705 of related objects that you request, unlike ``select_related`` which modifies 706 the 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 708 begins to be evaluated. 709 710 For 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 723 and run this code:: 724 725 >>> Pizza.objects.all() 726 [u"Hawaiian (ham, pineapple)", u"Seafood (prawns, smoked salmon)"... 727 728 The 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 730 be reduced to two: 731 732 >>> pizzas = Pizza.objects.all().prefetch_related('toppings') 733 734 All 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 738 Please note that use of ``prefetch_related`` will mean that the additional 739 queries run will **always** be executed - even if you never use the related 740 objects - and it always fully populates the result cache on the primary 741 ``QuerySet`` (which can sometimes be avoided in other cases). 742 743 Also remember that, as always with QuerySets, any subsequent chained methods 744 will ignore previously cached results, and retrieve data using a fresh database 745 query. 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 751 you - in fact it hurts performance, since you have done a database query that 752 you haven't used. So use this feature with caution! 753 754 The fields that must be supplied to this method can be any attributes on the 755 model instances which represent related queries that return multiple 756 objects. This includes attributes representing the 'many' side of ``ForeignKey`` 757 relationships, forward and reverse ``ManyToManyField`` attributes, and also any 758 ``GenericRelations``. 759 760 You can also use the normal join syntax to do related fields of related 761 fields. 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 767 The following are all legal: 768 769 >>> Restaurant.objects.prefetch_related('pizzas__toppings') 770 771 This will prefetch all pizzas belonging to restaurants, and all toppings 772 belonging to those pizzas. This will result in a total of 3 database queries - 773 one 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 777 This will fetch the best pizza and all the toppings for the best pizza for each 778 restaurant. This will be done in 2 database queries - one for the restaurants 779 and 'best pizzas' combined (achieved through use of ``select_related``), and one 780 for the toppings. 781 782 Chaining ``prefetch_related`` calls will accumulate the fields that should have 783 this behavior applied. To clear any ``prefetch_related`` behavior, pass `None` 784 as a parameter:: 785 786 >>> unprefetch = qs.prefetch_related(None) 787 788 When using ``prefetch_related``, one difference is that, in some circumstances, 789 objects created by a query can be shared between the different objects that they 790 are related to i.e. a single Python model instance can appear at more than one 791 point in the tree of objects that are returned. Normally this behavior will not 792 be a problem, and will in fact save both memory and CPU time. 793 693 794 extra 694 795 ~~~~~ 695 796 -
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) -
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) 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_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 198 class 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 235 class 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 282 class 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 330 class 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 )