mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	Added a "depth" argument to select_related() to control how many "levels" of relations select_related() is willing to follow (refs #3275).
Also added unit tests for select_related(). git-svn-id: http://code.djangoproject.com/svn/django/trunk@4645 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
		| @@ -84,6 +84,7 @@ class QuerySet(object): | ||||
|         self._filters = Q() | ||||
|         self._order_by = None        # Ordering, e.g. ('date', '-name'). If None, use model's ordering. | ||||
|         self._select_related = False # Whether to fill cache for related objects. | ||||
|         self._max_related_depth = 0  # Maximum "depth" for select_related | ||||
|         self._distinct = False       # Whether the query should use SELECT DISTINCT. | ||||
|         self._select = {}            # Dictionary of attname -> SQL. | ||||
|         self._where = []             # List of extra WHERE clauses to use. | ||||
| @@ -186,7 +187,8 @@ class QuerySet(object): | ||||
|                 raise StopIteration | ||||
|             for row in rows: | ||||
|                 if fill_cache: | ||||
|                     obj, index_end = get_cached_row(self.model, row, 0) | ||||
|                     obj, index_end = get_cached_row(klass=self.model, row=row,  | ||||
|                                                     index_start=0, max_depth=self._max_related_depth) | ||||
|                 else: | ||||
|                     obj = self.model(*row[:index_end]) | ||||
|                 for i, k in enumerate(extra_select): | ||||
| @@ -394,9 +396,9 @@ class QuerySet(object): | ||||
|         else: | ||||
|             return self._filter_or_exclude(None, **filter_obj) | ||||
|  | ||||
|     def select_related(self, true_or_false=True): | ||||
|     def select_related(self, true_or_false=True, depth=0): | ||||
|         "Returns a new QuerySet instance with '_select_related' modified." | ||||
|         return self._clone(_select_related=true_or_false) | ||||
|         return self._clone(_select_related=true_or_false, _max_related_depth=depth) | ||||
|  | ||||
|     def order_by(self, *field_names): | ||||
|         "Returns a new QuerySet instance with the ordering changed." | ||||
| @@ -430,6 +432,7 @@ class QuerySet(object): | ||||
|         c._filters = self._filters | ||||
|         c._order_by = self._order_by | ||||
|         c._select_related = self._select_related | ||||
|         c._max_related_depth = self._max_related_depth | ||||
|         c._distinct = self._distinct | ||||
|         c._select = self._select.copy() | ||||
|         c._where = self._where[:] | ||||
| @@ -483,7 +486,10 @@ class QuerySet(object): | ||||
|  | ||||
|         # Add additional tables and WHERE clauses based on select_related. | ||||
|         if self._select_related: | ||||
|             fill_table_cache(opts, select, tables, where, opts.db_table, [opts.db_table]) | ||||
|             fill_table_cache(opts, select, tables, where,  | ||||
|                              old_prefix=opts.db_table,  | ||||
|                              cache_tables_seen=[opts.db_table],  | ||||
|                              max_depth=self._max_related_depth) | ||||
|  | ||||
|         # Add any additional SELECTs. | ||||
|         if self._select: | ||||
| @@ -728,21 +734,33 @@ def get_where_clause(lookup_type, table_prefix, field_name, value): | ||||
|         return backend.get_fulltext_search_sql(table_prefix + field_name) | ||||
|     raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) | ||||
|  | ||||
| def get_cached_row(klass, row, index_start): | ||||
|     "Helper function that recursively returns an object with cache filled" | ||||
| def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0): | ||||
|     """Helper function that recursively returns an object with cache filled""" | ||||
|      | ||||
|     # If we've got a max_depth set and we've exceeded that depth, bail now. | ||||
|     if max_depth and cur_depth > max_depth: | ||||
|         return None | ||||
|      | ||||
|     index_end = index_start + len(klass._meta.fields) | ||||
|     obj = klass(*row[index_start:index_end]) | ||||
|     for f in klass._meta.fields: | ||||
|         if f.rel and not f.null: | ||||
|             rel_obj, index_end = get_cached_row(f.rel.to, row, index_end) | ||||
|             cached_row = get_cached_row(f.rel.to, row, index_end, max_depth, cur_depth+1) | ||||
|             if cached_row: | ||||
|                 rel_obj, index_end = cached_row | ||||
|                 setattr(obj, f.get_cache_name(), rel_obj) | ||||
|     return obj, index_end | ||||
|  | ||||
| def fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen): | ||||
| def fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen, max_depth=0, cur_depth=0): | ||||
|     """ | ||||
|     Helper function that recursively populates the select, tables and where (in | ||||
|     place) for select_related queries. | ||||
|     """ | ||||
|      | ||||
|     # If we've got a max_depth set and we've exceeded that depth, bail now. | ||||
|     if max_depth and cur_depth > max_depth: | ||||
|         return None | ||||
|      | ||||
|     qn = backend.quote_name | ||||
|     for f in opts.fields: | ||||
|         if f.rel and not f.null: | ||||
| @@ -757,7 +775,7 @@ def fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen) | ||||
|             where.append('%s.%s = %s.%s' % \ | ||||
|                 (qn(old_prefix), qn(f.column), qn(db_table), qn(f.rel.get_related_field().column))) | ||||
|             select.extend(['%s.%s' % (qn(db_table), qn(f2.column)) for f2 in f.rel.to._meta.fields]) | ||||
|             fill_table_cache(f.rel.to._meta, select, tables, where, db_table, cache_tables_seen) | ||||
|             fill_table_cache(f.rel.to._meta, select, tables, where, db_table, cache_tables_seen, max_depth, cur_depth+1) | ||||
|  | ||||
| def parse_lookup(kwarg_items, opts): | ||||
|     # Helper function that handles converting API kwargs | ||||
|   | ||||
| @@ -596,6 +596,19 @@ related ``Person`` *and* the related ``City``:: | ||||
| Note that ``select_related()`` does not follow foreign keys that have | ||||
| ``null=True``. | ||||
|  | ||||
| Usually, using ``select_related()`` can vastly improve performance since your | ||||
| app can avoid many database calls. However, in situations with deeply nested | ||||
| sets of relationships ``select_related()`` can sometimes end up following "too | ||||
| many" relations, and can generate queries so large that they end up being slow. | ||||
|  | ||||
| In these situations, you can use the ``depth`` argument to ``select_related()`` | ||||
| to control how many "levels" of relations ``select_related()`` will actually | ||||
| follow:: | ||||
|  | ||||
|     b = Book.objects.select_related(depth=1).get(id=4) | ||||
|     p = b.author         # Doesn't hit the database. | ||||
|     c = p.hometown       # Requires a database call. | ||||
|      | ||||
| ``extra(select=None, where=None, params=None, tables=None)`` | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
|   | ||||
							
								
								
									
										0
									
								
								tests/modeltests/select_related/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/modeltests/select_related/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										152
									
								
								tests/modeltests/select_related/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								tests/modeltests/select_related/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| """ | ||||
| XXX. Tests for ``select_related()`` | ||||
|  | ||||
| ``select_related()`` follows all relationships and pre-caches any foreign key | ||||
| values so that complex trees can be fetched in a single query. However, this | ||||
| isn't always a good idea, so the ``depth`` argument control how many "levels" | ||||
| the select-related behavior will traverse. | ||||
| """ | ||||
|  | ||||
| from django.db import models | ||||
|  | ||||
| # Who remembers high school biology? | ||||
|  | ||||
| class Domain(models.Model): | ||||
|     name = models.CharField(maxlength=50) | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
| class Kingdom(models.Model): | ||||
|     name = models.CharField(maxlength=50) | ||||
|     domain = models.ForeignKey(Domain) | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
| class Phylum(models.Model): | ||||
|     name = models.CharField(maxlength=50) | ||||
|     kingdom = models.ForeignKey(Kingdom) | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|      | ||||
| class Klass(models.Model): | ||||
|     name = models.CharField(maxlength=50) | ||||
|     phylum = models.ForeignKey(Phylum) | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|      | ||||
| class Order(models.Model): | ||||
|     name = models.CharField(maxlength=50) | ||||
|     klass = models.ForeignKey(Klass) | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
| class Family(models.Model): | ||||
|     name = models.CharField(maxlength=50) | ||||
|     order = models.ForeignKey(Order) | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
| class Genus(models.Model): | ||||
|     name = models.CharField(maxlength=50) | ||||
|     family = models.ForeignKey(Family) | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
| class Species(models.Model): | ||||
|     name = models.CharField(maxlength=50) | ||||
|     genus = models.ForeignKey(Genus) | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
| def create_tree(stringtree): | ||||
|     """Helper to create a complete tree""" | ||||
|     names = stringtree.split() | ||||
|     models = [Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species] | ||||
|     assert len(names) == len(models), (names, models) | ||||
|      | ||||
|     parent = None | ||||
|     for name, model in zip(names, models): | ||||
|         try: | ||||
|             obj = model.objects.get(name=name) | ||||
|         except model.DoesNotExist: | ||||
|             obj = model(name=name) | ||||
|         if parent: | ||||
|             setattr(obj, parent.__class__.__name__.lower(), parent) | ||||
|         obj.save() | ||||
|         parent = obj | ||||
|  | ||||
| __test__ = {'API_TESTS':""" | ||||
|  | ||||
| # Set up. | ||||
| # The test runner sets settings.DEBUG to False, but we want to gather queries | ||||
| # so we'll set it to True here and reset it at the end of the test suite. | ||||
| >>> from django.conf import settings | ||||
| >>> settings.DEBUG = True | ||||
|  | ||||
| >>> create_tree("Eukaryota Animalia Anthropoda Insecta Diptera Drosophilidae Drosophila melanogaster") | ||||
| >>> create_tree("Eukaryota Animalia Chordata Mammalia Primates Hominidae Homo sapiens") | ||||
| >>> create_tree("Eukaryota Plantae Magnoliophyta Magnoliopsida Fabales Fabaceae Pisum sativum") | ||||
| >>> create_tree("Eukaryota Fungi Basidiomycota Homobasidiomycatae Agaricales Amanitacae Amanita muscaria") | ||||
|  | ||||
| >>> from django import db | ||||
|  | ||||
| # Normally, accessing FKs doesn't fill in related objects: | ||||
| >>> db.reset_queries() | ||||
| >>> fly = Species.objects.get(name="melanogaster") | ||||
| >>> fly.genus.family.order.klass.phylum.kingdom.domain | ||||
| <Domain: Eukaryota> | ||||
| >>> len(db.connection.queries) | ||||
| 8 | ||||
|  | ||||
| # However, a select_related() call will fill in those related objects without any extra queries: | ||||
| >>> db.reset_queries() | ||||
| >>> person = Species.objects.select_related().get(name="sapiens") | ||||
| >>> person.genus.family.order.klass.phylum.kingdom.domain | ||||
| <Domain: Eukaryota> | ||||
| >>> len(db.connection.queries) | ||||
| 1 | ||||
|  | ||||
| # select_related() also of course applies to entire lists, not just items. | ||||
| # Without select_related() | ||||
| >>> db.reset_queries() | ||||
| >>> world = Species.objects.all() | ||||
| >>> [o.genus.family for o in world] | ||||
| [<Family: Drosophilidae>, <Family: Hominidae>, <Family: Fabaceae>, <Family: Amanitacae>] | ||||
| >>> len(db.connection.queries) | ||||
| 9 | ||||
|  | ||||
| # With select_related(): | ||||
| >>> db.reset_queries() | ||||
| >>> world = Species.objects.all().select_related() | ||||
| >>> [o.genus.family for o in world] | ||||
| [<Family: Drosophilidae>, <Family: Hominidae>, <Family: Fabaceae>, <Family: Amanitacae>] | ||||
| >>> len(db.connection.queries) | ||||
| 1 | ||||
|  | ||||
| # The "depth" argument to select_related() will stop the descent at a particular level: | ||||
| >>> db.reset_queries() | ||||
| >>> pea = Species.objects.select_related(depth=1).get(name="sativum") | ||||
| >>> pea.genus.family.order.klass.phylum.kingdom.domain | ||||
| <Domain: Eukaryota> | ||||
|  | ||||
| # Notice: one few query than above because of depth=1 | ||||
| >>> len(db.connection.queries) | ||||
| 7 | ||||
|  | ||||
| >>> db.reset_queries() | ||||
| >>> pea = Species.objects.select_related(depth=5).get(name="sativum") | ||||
| >>> pea.genus.family.order.klass.phylum.kingdom.domain | ||||
| <Domain: Eukaryota> | ||||
| >>> len(db.connection.queries) | ||||
| 3 | ||||
|  | ||||
| >>> db.reset_queries() | ||||
| >>> world = Species.objects.all().select_related(depth=2) | ||||
| >>> [o.genus.family.order for o in world] | ||||
| [<Order: Diptera>, <Order: Primates>, <Order: Fabales>, <Order: Agaricales>] | ||||
| >>> len(db.connection.queries) | ||||
| 5 | ||||
|  | ||||
| # Reset DEBUG to where we found it. | ||||
| >>> settings.DEBUG = False | ||||
| """} | ||||
		Reference in New Issue
	
	Block a user