Moved join path generation to Field

Refs #19385
This commit is contained in:
Anssi Kääriäinen 2012-12-17 17:09:07 +02:00
parent 9ef3cab40b
commit 4511aeb6b8
5 changed files with 91 additions and 100 deletions

View File

@ -11,6 +11,7 @@ from django.db import connection
from django.db.models import signals from django.db.models import signals
from django.db import models, router, DEFAULT_DB_ALIAS from django.db import models, router, DEFAULT_DB_ALIAS
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
from django.db.models.related import PathInfo
from django.forms import ModelForm from django.forms import ModelForm
from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
@ -160,6 +161,16 @@ class GenericRelation(RelatedField, Field):
kwargs['serialize'] = False kwargs['serialize'] = False
Field.__init__(self, **kwargs) Field.__init__(self, **kwargs)
def get_path_info(self):
from_field = self.model._meta.pk
opts = self.rel.to._meta
target = opts.get_field_by_name(self.object_id_field_name)[0]
# Note that we are using different field for the join_field
# than from_field or to_field. This is a hack, but we need the
# GenericRelation to generate the extra SQL.
return ([PathInfo(from_field, target, self.model._meta, opts, self, True, False)],
opts, target, self)
def get_choices_default(self): def get_choices_default(self):
return Field.get_choices(self, include_blank=False) return Field.get_choices(self, include_blank=False)

View File

@ -5,7 +5,7 @@ from django.db.backends import util
from django.db.models import signals, get_model from django.db.models import signals, get_model
from django.db.models.fields import (AutoField, Field, IntegerField, from django.db.models.fields import (AutoField, Field, IntegerField,
PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist) PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist)
from django.db.models.related import RelatedObject from django.db.models.related import RelatedObject, PathInfo
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.db.models.query_utils import QueryWrapper from django.db.models.query_utils import QueryWrapper
from django.db.models.deletion import CASCADE from django.db.models.deletion import CASCADE
@ -16,7 +16,6 @@ from django.utils.functional import curry, cached_property
from django.core import exceptions from django.core import exceptions
from django import forms from django import forms
RECURSIVE_RELATIONSHIP_CONSTANT = 'self' RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
pending_lookups = {} pending_lookups = {}
@ -1004,6 +1003,31 @@ class ForeignKey(RelatedField, Field):
) )
Field.__init__(self, **kwargs) Field.__init__(self, **kwargs)
def get_path_info(self):
"""
Get path from this field to the related model.
"""
opts = self.rel.to._meta
target = self.rel.get_related_field()
from_opts = self.model._meta
return [PathInfo(self, target, from_opts, opts, self, False, True)], opts, target, self
def get_reverse_path_info(self):
"""
Get path from the related model to this field's model.
"""
opts = self.model._meta
from_field = self.rel.get_related_field()
from_opts = from_field.model._meta
pathinfos = [PathInfo(from_field, self, from_opts, opts, self, not self.unique, False)]
if from_field.model is self.model:
# Recursive foreign key to self.
target = opts.get_field_by_name(
self.rel.field_name)[0]
else:
target = opts.pk
return pathinfos, opts, target, self
def validate(self, value, model_instance): def validate(self, value, model_instance):
if self.rel.parent_link: if self.rel.parent_link:
return return
@ -1198,6 +1222,30 @@ class ManyToManyField(RelatedField, Field):
msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.')
self.help_text = string_concat(self.help_text, ' ', msg) self.help_text = string_concat(self.help_text, ' ', msg)
def _get_path_info(self, direct=False):
"""
Called by both direct an indirect m2m traversal.
"""
pathinfos = []
int_model = self.rel.through
linkfield1 = int_model._meta.get_field_by_name(self.m2m_field_name())[0]
linkfield2 = int_model._meta.get_field_by_name(self.m2m_reverse_field_name())[0]
if direct:
join1infos, _, _, _ = linkfield1.get_reverse_path_info()
join2infos, opts, target, final_field = linkfield2.get_path_info()
else:
join1infos, _, _, _ = linkfield2.get_reverse_path_info()
join2infos, opts, target, final_field = linkfield1.get_path_info()
pathinfos.extend(join1infos)
pathinfos.extend(join2infos)
return pathinfos, opts, target, final_field
def get_path_info(self):
return self._get_path_info(direct=True)
def get_reverse_path_info(self):
return self._get_path_info(direct=False)
def get_choices_default(self): def get_choices_default(self):
return Field.get_choices(self, include_blank=False) return Field.get_choices(self, include_blank=False)

View File

@ -1,6 +1,15 @@
from collections import namedtuple
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.db.models.fields import BLANK_CHOICE_DASH from django.db.models.fields import BLANK_CHOICE_DASH
# PathInfo is used when converting lookups (fk__somecol). The contents
# describe the relation in Model terms (model Options and Fields for both
# sides of the relation. The join_field is the field backing the relation.
PathInfo = namedtuple('PathInfo',
'from_field to_field from_opts to_opts join_field '
'm2m direct')
class BoundRelatedObject(object): class BoundRelatedObject(object):
def __init__(self, related_object, field_mapping, original): def __init__(self, related_object, field_mapping, original):
self.relation = related_object self.relation = related_object
@ -67,3 +76,6 @@ class RelatedObject(object):
def get_cache_name(self): def get_cache_name(self):
return "_%s_cache" % self.get_accessor_name() return "_%s_cache" % self.get_accessor_name()
def get_path_info(self):
return self.field.get_reverse_path_info()

View File

@ -26,12 +26,6 @@ JoinInfo = namedtuple('JoinInfo',
'table_name rhs_alias join_type lhs_alias ' 'table_name rhs_alias join_type lhs_alias '
'lhs_join_col rhs_join_col nullable join_field') 'lhs_join_col rhs_join_col nullable join_field')
# PathInfo is used when converting lookups (fk__somecol). The contents
# describe the join in Model terms (model Options and Fields for both
# sides of the join. The rel_field is the field we are joining along.
PathInfo = namedtuple('PathInfo',
'from_field to_field from_opts to_opts join_field')
# Pairs of column clauses to select, and (possibly None) field for the clause. # Pairs of column clauses to select, and (possibly None) field for the clause.
SelectInfo = namedtuple('SelectInfo', 'col field') SelectInfo = namedtuple('SelectInfo', 'col field')

View File

@ -18,9 +18,10 @@ from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import ExpressionNode from django.db.models.expressions import ExpressionNode
from django.db.models.fields import FieldDoesNotExist from django.db.models.fields import FieldDoesNotExist
from django.db.models.loading import get_model from django.db.models.loading import get_model
from django.db.models.related import PathInfo
from django.db.models.sql import aggregates as base_aggregates_module from django.db.models.sql import aggregates as base_aggregates_module
from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE, from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE,
ORDER_PATTERN, JoinInfo, SelectInfo, PathInfo) ORDER_PATTERN, JoinInfo, SelectInfo)
from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin
from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.expressions import SQLEvaluator
from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode, from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode,
@ -1294,7 +1295,6 @@ class Query(object):
contain the same value as the final field). contain the same value as the final field).
""" """
path = [] path = []
multijoin_pos = None
for pos, name in enumerate(names): for pos, name in enumerate(names):
if name == 'pk': if name == 'pk':
name = opts.pk.name name = opts.pk.name
@ -1328,92 +1328,19 @@ class Query(object):
target = final_field.rel.get_related_field() target = final_field.rel.get_related_field()
opts = int_model._meta opts = int_model._meta
path.append(PathInfo(final_field, target, final_field.model._meta, path.append(PathInfo(final_field, target, final_field.model._meta,
opts, final_field)) opts, final_field, False, True))
# We have five different cases to solve: foreign keys, reverse if hasattr(field, 'get_path_info'):
# foreign keys, m2m fields (also reverse) and non-relational pathinfos, opts, target, final_field = field.get_path_info()
# fields. We are mostly just using the related field API to path.extend(pathinfos)
# fetch the from and to fields. The m2m fields are handled as else:
# two foreign keys, first one reverse, the second one direct.
if direct and not field.rel and not m2m:
# Local non-relational field. # Local non-relational field.
final_field = target = field final_field = target = field
break break
elif direct and not m2m: multijoin_pos = None
# Foreign Key for m2mpos, pathinfo in enumerate(path):
opts = field.rel.to._meta if pathinfo.m2m:
target = field.rel.get_related_field() multijoin_pos = m2mpos
final_field = field break
from_opts = field.model._meta
path.append(PathInfo(field, target, from_opts, opts, field))
elif not direct and not m2m:
# Revere foreign key
final_field = to_field = field.field
opts = to_field.model._meta
from_field = to_field.rel.get_related_field()
from_opts = from_field.model._meta
path.append(
PathInfo(from_field, to_field, from_opts, opts, to_field))
if from_field.model is to_field.model:
# Recursive foreign key to self.
target = opts.get_field_by_name(
field.field.rel.field_name)[0]
else:
target = opts.pk
elif direct and m2m:
if not field.rel.through:
# Gotcha! This is just a fake m2m field - a generic relation
# field).
from_field = opts.pk
opts = field.rel.to._meta
target = opts.get_field_by_name(field.object_id_field_name)[0]
final_field = field
# Note that we are using different field for the join_field
# than from_field or to_field. This is a hack, but we need the
# GenericRelation to generate the extra SQL.
path.append(PathInfo(from_field, target, field.model._meta, opts,
field))
else:
# m2m field. We are travelling first to the m2m table along a
# reverse relation, then from m2m table to the target table.
from_field1 = opts.get_field_by_name(
field.m2m_target_field_name())[0]
opts = field.rel.through._meta
to_field1 = opts.get_field_by_name(field.m2m_field_name())[0]
path.append(
PathInfo(from_field1, to_field1, from_field1.model._meta,
opts, to_field1))
final_field = from_field2 = opts.get_field_by_name(
field.m2m_reverse_field_name())[0]
opts = field.rel.to._meta
target = to_field2 = opts.get_field_by_name(
field.m2m_reverse_target_field_name())[0]
path.append(
PathInfo(from_field2, to_field2, from_field2.model._meta,
opts, from_field2))
elif not direct and m2m:
# This one is just like above, except we are travelling the
# fields in opposite direction.
field = field.field
from_field1 = opts.get_field_by_name(
field.m2m_reverse_target_field_name())[0]
int_opts = field.rel.through._meta
to_field1 = int_opts.get_field_by_name(
field.m2m_reverse_field_name())[0]
path.append(
PathInfo(from_field1, to_field1, from_field1.model._meta,
int_opts, to_field1))
final_field = from_field2 = int_opts.get_field_by_name(
field.m2m_field_name())[0]
opts = field.opts
target = to_field2 = opts.get_field_by_name(
field.m2m_target_field_name())[0]
path.append(PathInfo(from_field2, to_field2, from_field2.model._meta,
opts, from_field2))
if m2m and multijoin_pos is None:
multijoin_pos = pos
if not direct and not path[-1].to_field.unique and multijoin_pos is None:
multijoin_pos = pos
if pos != len(names) - 1: if pos != len(names) - 1:
if pos == len(names) - 2: if pos == len(names) - 2:
@ -1463,16 +1390,15 @@ class Query(object):
# joins at this stage - we will need the information about join type # joins at this stage - we will need the information about join type
# of the trimmed joins. # of the trimmed joins.
for pos, join in enumerate(path): for pos, join in enumerate(path):
from_field, to_field, from_opts, opts, join_field = join opts = join.to_opts
direct = join_field == from_field if join.direct:
if direct: nullable = self.is_nullable(join.from_field)
nullable = self.is_nullable(from_field)
else: else:
nullable = True nullable = True
connection = alias, opts.db_table, from_field.column, to_field.column connection = alias, opts.db_table, join.from_field.column, join.to_field.column
reuse = None if direct or to_field.unique else can_reuse reuse = can_reuse if join.m2m else None
alias = self.join(connection, reuse=reuse, alias = self.join(connection, reuse=reuse,
nullable=nullable, join_field=join_field) nullable=nullable, join_field=join.join_field)
joins.append(alias) joins.append(alias)
return final_field, target, opts, joins, path return final_field, target, opts, joins, path