Skip to content

Commit

Permalink
queryset-refactor: Allow specifying of specific relations to follow in
Browse files Browse the repository at this point in the history
select_related(). Refs #5020.


git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@6899 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
malcolmt committed Dec 9, 2007
1 parent 3dce17d commit 3064a21
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 22 deletions.
49 changes: 38 additions & 11 deletions django/db/models/query.py
Expand Up @@ -85,13 +85,17 @@ def iterator(self):
database. database.
""" """
fill_cache = self.query.select_related fill_cache = self.query.select_related
if isinstance(fill_cache, dict):
requested = fill_cache
else:
requested = None
max_depth = self.query.max_depth max_depth = self.query.max_depth
index_end = len(self.model._meta.fields) index_end = len(self.model._meta.fields)
extra_select = self.query.extra_select.keys() extra_select = self.query.extra_select.keys()
for row in self.query.results_iter(): for row in self.query.results_iter():
if fill_cache: if fill_cache:
obj, index_end = get_cached_row(klass=self.model, row=row, obj, index_end = get_cached_row(self.model, row, 0, max_depth,
index_start=0, max_depth=max_depth) requested=requested)
else: else:
obj = self.model(*row[:index_end]) obj = self.model(*row[:index_end])
for i, k in enumerate(extra_select): for i, k in enumerate(extra_select):
Expand Down Expand Up @@ -298,10 +302,25 @@ def complex_filter(self, filter_obj):
else: else:
return self._filter_or_exclude(None, **filter_obj) return self._filter_or_exclude(None, **filter_obj)


def select_related(self, true_or_false=True, depth=0): def select_related(self, *fields, **kwargs):
"""Returns a new QuerySet instance that will select related objects.""" """
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 = 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: if depth:
obj.query.max_depth = depth obj.query.max_depth = depth
return obj return obj
Expand Down Expand Up @@ -370,7 +389,7 @@ def _get_data(self):
class ValuesQuerySet(QuerySet): class ValuesQuerySet(QuerySet):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ValuesQuerySet, self).__init__(*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 self.query.select_related = False


# QuerySet.clone() will also set up the _fields attribute with the # QuerySet.clone() will also set up the _fields attribute with the
Expand Down Expand Up @@ -490,18 +509,26 @@ def __init__(self, *args, **kwargs):


QOr = QAnd = QOperator 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""" """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 requested is None and cur_depth > max_depth:
if max_depth and cur_depth > max_depth: # We've recursed deeply enough; stop now.
return None return None


restricted = requested is not None
index_end = index_start + len(klass._meta.fields) index_end = index_start + len(klass._meta.fields)
obj = klass(*row[index_start:index_end]) obj = klass(*row[index_start:index_end])
for f in klass._meta.fields: for f in klass._meta.fields:
if f.rel and not f.null: if f.rel and ((not restricted and not f.null) or
cached_row = get_cached_row(f.rel.to, row, index_end, max_depth, cur_depth+1) (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: if cached_row:
rel_obj, index_end = cached_row rel_obj, index_end = cached_row
setattr(obj, f.get_cache_name(), rel_obj) setattr(obj, f.get_cache_name(), rel_obj)
Expand Down
41 changes: 34 additions & 7 deletions django/db/models/sql/query.py
Expand Up @@ -636,15 +636,15 @@ def join(self, (lhs, table, lhs_col, col), always_create=False,
return alias return alias


def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1, 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 Fill in the information needed for a select_related query. The current
"depth" is measured as the number of connections away from the root depth is measured as the number of connections away from the root model
model (cur_depth == 1 means we are looking at models with direct (for example, cur_depth=1 means we are looking at models with direct
connections to the root model). connections to the root model).
""" """
if self.max_depth and cur_depth > self.max_depth: if not restricted and self.max_depth and cur_depth > self.max_depth:
# We've recursed too deeply; bail out. # We've recursed far enough; bail out.
return return
if not opts: if not opts:
opts = self.model._meta opts = self.model._meta
Expand All @@ -653,17 +653,31 @@ def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
if not used: if not used:
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: 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 continue
table = f.rel.to._meta.db_table table = f.rel.to._meta.db_table
alias = self.join((root_alias, table, f.column, alias = self.join((root_alias, table, f.column,
f.rel.get_related_field().column), exclusions=used) f.rel.get_related_field().column), exclusions=used)
used.append(alias) used.append(alias)
self.select.extend([(alias, f2.column) self.select.extend([(alias, f2.column)
for f2 in f.rel.to._meta.fields]) 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, 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): def add_filter(self, filter_expr, connector=AND, negate=False):
""" """
Expand Down Expand Up @@ -1006,6 +1020,19 @@ def add_count_column(self):
self.select = [select] self.select = [select]
self.extra_select = SortedDict() 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): def execute_sql(self, result_type=MULTI):
""" """
Run the query against the database and returns the result(s). The Run the query against the database and returns the result(s). The
Expand Down
39 changes: 37 additions & 2 deletions docs/db-api.txt
Expand Up @@ -744,8 +744,8 @@ related ``Person`` *and* the related ``City``::
p = b.author # Hits the database. p = b.author # Hits the database.
c = p.hometown # Hits the database. c = p.hometown # Hits the database.


Note that ``select_related()`` does not follow foreign keys that have Note that, by default, ``select_related()`` does not follow foreign keys that
``null=True``. have ``null=True``.


Usually, using ``select_related()`` can vastly improve performance because your Usually, using ``select_related()`` can vastly improve performance because your
app can avoid many database calls. However, in situations with deeply nested app can avoid many database calls. However, in situations with deeply nested
Expand All @@ -762,6 +762,41 @@ follow::


The ``depth`` argument is new in the Django development version. 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)`` ``extra(select=None, where=None, params=None, tables=None, order_by=None)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Expand Down
36 changes: 34 additions & 2 deletions tests/modeltests/select_related/models.py
Expand Up @@ -129,7 +129,7 @@ def create_tree(stringtree):
>>> pea.genus.family.order.klass.phylum.kingdom.domain >>> pea.genus.family.order.klass.phylum.kingdom.domain
<Domain: Eukaryota> <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) >>> len(db.connection.queries)
7 7
Expand All @@ -151,6 +151,38 @@ def create_tree(stringtree):
>>> s.id + 10 == s.a >>> s.id + 10 == s.a
True 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 >>> settings.DEBUG = False
"""} """}

0 comments on commit 3064a21

Please sign in to comment.