1
0
mirror of https://github.com/django/django.git synced 2025-07-05 18:29:11 +00:00

queryset-refactor: Allow specifying of specific relations to follow in

select_related(). Refs #5020.


git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@6899 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2007-12-09 06:24:17 +00:00
parent 3dce17ddc4
commit 3064a211bf
4 changed files with 143 additions and 22 deletions

View File

@ -85,13 +85,17 @@ class _QuerySet(object):
database.
"""
fill_cache = self.query.select_related
if isinstance(fill_cache, dict):
requested = fill_cache
else:
requested = None
max_depth = self.query.max_depth
index_end = len(self.model._meta.fields)
extra_select = self.query.extra_select.keys()
for row in self.query.results_iter():
if fill_cache:
obj, index_end = get_cached_row(klass=self.model, row=row,
index_start=0, max_depth=max_depth)
obj, index_end = get_cached_row(self.model, row, 0, max_depth,
requested=requested)
else:
obj = self.model(*row[:index_end])
for i, k in enumerate(extra_select):
@ -298,10 +302,25 @@ class _QuerySet(object):
else:
return self._filter_or_exclude(None, **filter_obj)
def select_related(self, true_or_false=True, depth=0):
"""Returns a new QuerySet instance that will select related objects."""
def select_related(self, *fields, **kwargs):
"""
Returns a new QuerySet instance that will select related objects. If
fields are specified, they must be ForeignKey fields and only those
related objects are included in the selection.
"""
depth = kwargs.pop('depth', 0)
# TODO: Remove this? select_related(False) isn't really useful.
true_or_false = kwargs.pop('true_or_false', True)
if kwargs:
raise TypeError('Unexpected keyword arguments to select_related: %s'
% (kwargs.keys(),))
obj = self._clone()
obj.query.select_related = true_or_false
if fields:
if depth:
raise TypeError('Cannot pass both "depth" and fields to select_related()')
obj.query.add_select_related(fields)
else:
obj.query.select_related = true_or_false
if depth:
obj.query.max_depth = depth
return obj
@ -370,7 +389,7 @@ else:
class ValuesQuerySet(QuerySet):
def __init__(self, *args, **kwargs):
super(ValuesQuerySet, self).__init__(*args, **kwargs)
# select_related isn't supported in values().
# select_related isn't supported in values(). (FIXME -#3358)
self.query.select_related = False
# QuerySet.clone() will also set up the _fields attribute with the
@ -490,18 +509,26 @@ class QOperator(Q):
QOr = QAnd = QOperator
def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0):
def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0,
requested=None):
"""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:
if max_depth and requested is None and cur_depth > max_depth:
# We've recursed deeply enough; stop now.
return None
restricted = requested is not 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:
cached_row = get_cached_row(f.rel.to, row, index_end, max_depth, cur_depth+1)
if f.rel and ((not restricted and not f.null) or
(restricted and f.name in requested)):
if restricted:
next = requested[f.name]
else:
next = None
cached_row = get_cached_row(f.rel.to, row, index_end, max_depth,
cur_depth+1, next)
if cached_row:
rel_obj, index_end = cached_row
setattr(obj, f.get_cache_name(), rel_obj)

View File

@ -636,15 +636,15 @@ class Query(object):
return alias
def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
used=None):
used=None, requested=None, restricted=None):
"""
Fill in the information needed for a select_related query. The current
"depth" is measured as the number of connections away from the root
model (cur_depth == 1 means we are looking at models with direct
depth is measured as the number of connections away from the root model
(for example, cur_depth=1 means we are looking at models with direct
connections to the root model).
"""
if self.max_depth and cur_depth > self.max_depth:
# We've recursed too deeply; bail out.
if not restricted and self.max_depth and cur_depth > self.max_depth:
# We've recursed far enough; bail out.
return
if not opts:
opts = self.model._meta
@ -653,8 +653,18 @@ class Query(object):
if not used:
used = []
# Setup for the case when only particular related fields should be
# included in the related selection.
if requested is None and restricted is not False:
if isinstance(self.select_related, dict):
requested = self.select_related
restricted = True
else:
restricted = False
for f in opts.fields:
if not f.rel or f.null:
if (not f.rel or (restricted and f.name not in requested) or
(not restricted and f.null)):
continue
table = f.rel.to._meta.db_table
alias = self.join((root_alias, table, f.column,
@ -662,8 +672,12 @@ class Query(object):
used.append(alias)
self.select.extend([(alias, f2.column)
for f2 in f.rel.to._meta.fields])
if restricted:
next = requested.get(f.name, {})
else:
next = False
self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1,
used)
used, next, restricted)
def add_filter(self, filter_expr, connector=AND, negate=False):
"""
@ -1006,6 +1020,19 @@ class Query(object):
self.select = [select]
self.extra_select = SortedDict()
def add_select_related(self, fields):
"""
Sets up the select_related data structure so that we only select
certain related models (as opposed to all models, when
self.select_related=True).
"""
field_dict = {}
for field in fields:
d = field_dict
for part in field.split(LOOKUP_SEP):
d = d.setdefault(part, {})
self.select_related = field_dict
def execute_sql(self, result_type=MULTI):
"""
Run the query against the database and returns the result(s). The

View File

@ -744,8 +744,8 @@ related ``Person`` *and* the related ``City``::
p = b.author # Hits the database.
c = p.hometown # Hits the database.
Note that ``select_related()`` does not follow foreign keys that have
``null=True``.
Note that, by default, ``select_related()`` does not follow foreign keys that
have ``null=True``.
Usually, using ``select_related()`` can vastly improve performance because your
app can avoid many database calls. However, in situations with deeply nested
@ -762,6 +762,41 @@ follow::
The ``depth`` argument is new in the Django development version.
**New in Django development version:** Sometimes you only need to access
specific models that are related to your root model, not all of the related
models. In these cases, you can pass the related field names to
``select_related()`` and it will only follow those relations. You can even do
this for models that are more than one relation away by separating the field
names with double underscores, just as for filters. For example, if we have
thise model::
class Room(models.Model):
# ...
building = models.ForeignKey(...)
class Group(models.Model):
# ...
teacher = models.ForeignKey(...)
room = models.ForeignKey(Room)
subject = models.ForeignKey(...)
...and we only needed to work with the ``room`` and ``subject`` attributes, we
could write this::
g = Group.objects.select_related('room', 'subject')
This is also valid::
g = Group.objects.select_related('room__building', 'subject')
...and would also pull in the ``building`` relation.
You can only refer to ``ForeignKey`` relations in the list of fields passed to
``select_related``. You *can* refer to foreign keys that have ``null=True``
(unlike the default ``select_related()`` call). It's an error to use both a
list of fields and the ``depth`` parameter in the same ``select_related()``
call, since they are conflicting options.
``extra(select=None, where=None, params=None, tables=None, order_by=None)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -129,7 +129,7 @@ __test__ = {'API_TESTS':"""
>>> pea.genus.family.order.klass.phylum.kingdom.domain
<Domain: Eukaryota>
# Notice: one few query than above because of depth=1
# Notice: one fewer queries than above because of depth=1
>>> len(db.connection.queries)
7
@ -151,6 +151,38 @@ __test__ = {'API_TESTS':"""
>>> s.id + 10 == s.a
True
# Reset DEBUG to where we found it.
# The optional fields passed to select_related() control which related models
# we pull in. This allows for smaller queries and can act as an alternative
# (or, in addition to) the depth parameter.
# In the next two cases, we explicitly say to select the 'genus' and
# 'genus.family' models, leading to the same number of queries as before.
>>> db.reset_queries()
>>> world = Species.objects.select_related('genus__family')
>>> [o.genus.family for o in world]
[<Family: Drosophilidae>, <Family: Hominidae>, <Family: Fabaceae>, <Family: Amanitacae>]
>>> len(db.connection.queries)
1
>>> db.reset_queries()
>>> world = Species.objects.filter(genus__name='Amanita').select_related('genus__family')
>>> [o.genus.family.order for o in world]
[<Order: Agaricales>]
>>> len(db.connection.queries)
2
>>> db.reset_queries()
>>> Species.objects.all().select_related('genus__family__order').order_by('id')[0:1].get().genus.family.order.name
u'Diptera'
>>> len(db.connection.queries)
1
# Specifying both "depth" and fields is an error.
>>> Species.objects.select_related('genus__family__order', depth=4)
Traceback (most recent call last):
...
TypeError: Cannot pass both "depth" and fields to select_related()
# Reser DEBUG to where we found it.
>>> settings.DEBUG = False
"""}