[[Djangoを読む]]
 
 #contents
 
 *はじめに [#g9093d2c]
 
 というわけで引き続き、
 
  # And vice versa: Question objects get access to Choice objects.
  >>> q.choice_set.all()
  <QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
 
 を見ていきます。前に見たallですが今度は複数テーブルになっているところが異なります。
 
 *django.db.models.fields.related_descriptors [#b5c171be]
 
 前回も見たRelatedManager、この中で検索に関係のありそうなものを探してみると以下のメソッドがあります。get_querysetメソッドはallメソッドとかを呼び出すと裏で実行され、その名の通りQuerySetオブジェクトを返します。
 
 #code(Python){{
         def get_queryset(self):
             try:
                 return self.instance._prefetched_objects_cache[self.field.related_query_name()]
             except (AttributeError, KeyError):
                 queryset = super(RelatedManager, self).get_queryset()
                 return self._apply_rel_filters(queryset)
 }}
 
 キャッシュはされてないとして、_apply_rel_filtersに進みます。
 
 #code(Python){{
         def _apply_rel_filters(self, queryset):
             """
             Filter the queryset for the instance this manager is bound to.
             """
             db = self._db or router.db_for_read(self.model, instance=self.instance)
             empty_strings_as_null = connections[db].features.interprets_empty_strings_as_nulls
             queryset._add_hints(instance=self.instance)
             if self._db:
                 queryset = queryset.using(self._db)
             queryset = queryset.filter(**self.core_filters)
             for field in self.field.foreign_related_fields:
                 val = getattr(self.instance, field.attname)
                 if val is None or (val == '' and empty_strings_as_null):
                     return queryset.none()
             queryset._known_related_objects = {self.field: {self.instance.pk: self.instance} }
             return queryset
 }}
 
 いろいろやっていますが、鍵となるのは真ん中あたりにあるfilterでしょう。core_filtersは__init__で初期化されていました。
 
 #code(Python){{
         def __init__(self, instance):
             super(RelatedManager, self).__init__()
 
             self.instance = instance
             self.model = rel.related_model
             self.field = rel.field
 
             self.core_filters = {self.field.name: instance}
 }}
 
 relはManyToOneRelオブジェクトでfieldとはquestion、instanceはQuestionオブジェクトです。つまり、
 
  {'question': Questionオブジェクト}
 
 というフィルタがchoice_set経由だと常に設定されるということになります。
 
 *Query [#gf44aa37]
 
 ここまでわかったらQueryクラスについて前回は端折った関連、JOIN周りについて確認していきます。
 
 filter→add_q→_add_q→build_filterと前に見たフローを進んでいき、build_filterが呼び出しているsetup_joinsに注目します。名前からして明らかにJOINを処理してそうです。
 
 #code(Python){{
     def setup_joins(self, names, opts, alias, can_reuse=None, allow_many=True):
         """
         Compute the necessary table joins for the passage through the fields
         given in 'names'. 'opts' is the Options class for the current model
         (which gives the table we are starting from), 'alias' is the alias for
         the table to start the joining from.
 
         The 'can_reuse' defines the reverse foreign key joins we can reuse. It
         can be None in which case all joins are reusable or a set of aliases
         that can be reused. Note that non-reverse foreign keys are always
         reusable when using setup_joins().
 
         If 'allow_many' is False, then any reverse foreign key seen will
         generate a MultiJoin exception.
 
         Returns the final field involved in the joins, the target field (used
         for any 'where' constraint), the final 'opts' value, the joins and the
         field path travelled to generate the joins.
 
         The target field is the field containing the concrete value. Final
         field can be something different, for example foreign key pointing to
         that value. Final field is needed for example in some value
         conversions (convert 'obj' in fk__id=obj to pk val using the foreign
         key field for example).
         """
         joins = [alias]
         # First, generate the path for the names
         path, final_field, targets, rest = self.names_to_path(
             names, opts, allow_many, fail_on_missing=True)
 
         # Then, add the path to the query's joins. Note that we can't trim
         # joins at this stage - we will need the information about join type
         # of the trimmed joins.
         for join in path:
             opts = join.to_opts
             if join.direct:
                 nullable = self.is_nullable(join.join_field)
             else:
                 nullable = True
             connection = Join(opts.db_table, alias, None, INNER, join.join_field, nullable)
             reuse = can_reuse if join.m2m else None
             alias = self.join(connection, reuse=reuse)
             joins.append(alias)
         return final_field, targets, opts, joins, path
 }}
 
 **names_to_path [#wee88086]
 
 前はnames_to_pathもほぼ無視しましたが今回はこれが効いてきます。まずはこちらを見てみましょう。
 
 #code(Python){{
     def names_to_path(self, names, opts, allow_many=True, fail_on_missing=False):
         """
         Walks the list of names and turns them into PathInfo tuples. Note that
         a single name in 'names' can generate multiple PathInfos (m2m for
         example).
 
         'names' is the path of names to travel, 'opts' is the model Options we
         start the name resolving from, 'allow_many' is as for setup_joins().
         If fail_on_missing is set to True, then a name that can't be resolved
         will generate a FieldError.
 
         Returns a list of PathInfo tuples. In addition returns the final field
         (the last used join field), and target (which is a field guaranteed to
         contain the same value as the final field). Finally, the method returns
         those names that weren't found (which are likely transforms and the
         final lookup).
         """
         path, names_with_path = [], []
         for pos, name in enumerate(names):
             cur_names_with_path = (name, [])
 
             field = None
             try:
                 field = opts.get_field(name)
             except FieldDoesNotExist:
                 # 省略
 
             # 親クラスのフィールドだった場合の処理
 
             if hasattr(field, 'get_path_info'):
                 pathinfos = field.get_path_info()
                 if not allow_many:
                     # 省略
                 last = pathinfos[-1]
                 path.extend(pathinfos)
                 final_field = last.join_field
                 opts = last.to_opts
                 targets = last.target_fields
                 cur_names_with_path[1].extend(pathinfos)
                 names_with_path.append(cur_names_with_path)
             else:
                 # 省略
         return path, final_field, targets, names[pos + 1:]
 }}
 
 前回は見なかった方、つまり、Fieldクラス(のサブクラス)がget_path_infoメソッドを持っているという方に進みます。get_path_infoはForeignKeyの親クラスのForeignObjectっで定義されていて、
 
 #code(Python){{
     def get_path_info(self):
         """
         Get path from this field to the related model.
         """
         opts = self.remote_field.model._meta
         from_opts = self.model._meta
         return [PathInfo(from_opts, opts, self.foreign_related_fields, self, False, True)]
 }}
 
 PathInfoはdjango/db/models/query_utils.pyで定義されているnamedtupleです。
 
 #code(Python){{
 # 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_opts to_opts target_fields join_field m2m direct')
 }}
 
 foreign_related_fieldsはプロパティで、追いかけていくと、
 
 +related_fieldsプロパティ
 +resolve_related_fieldsメソッド
 
 と進み、from(Choiceのquestionフィールド)からto(Questionのidフィールド)への関連を設定、その右側(つまり、Questionのidフィールド)が設定されます。
 
 話をnames_to_pathメソッドに戻して処理を追いかけていくと結局以下のように返されることがわかります。ちなみに、names_with_pathは今回の範囲では特に使われないようです。
 
 :path|[PathInfo(ChoiceのOptions, QuestionのOptions, Questionのid, Choiceのquestion, False, True)]
 :final_field|Choiceのquestionフィールド
 :targets:[Questionのidフィールド]
 :names|[]
 
 **setup_joins [#k0b39577]
 
 さて、setup_joinsに戻ってnames_to_path呼び出しの後を再掲、
 
 #code(Python){{
         for join in path:
             opts = join.to_opts
             if join.direct:
                 nullable = self.is_nullable(join.join_field)
             else:
                 nullable = True
             connection = Join(opts.db_table, alias, None, INNER, join.join_field, nullable)
             reuse = can_reuse if join.m2m else None
             alias = self.join(connection, reuse=reuse)
             joins.append(alias)
         return final_field, targets, opts, joins, path
 }}
 
 Joinクラスはdjango/db/models/sql/datastructures.pyに記述されています。
 
 joinメソッド。メソッドドキュメントは引数の説明が古いので省略。最新だと直っているようです。
 
 #code(Python){{
     def join(self, join, reuse=None):
         reuse = [a for a, j in self.alias_map.items()
                  if (reuse is None or a in reuse) and j == join]
         if reuse:
             self.ref_alias(reuse[0])
             return reuse[0]
 
         # No reuse is possible, so we need a new alias.
         alias, _ = self.table_alias(join.table_name, create=True)
         if join.join_type:
             if self.alias_map[join.parent_alias].join_type == LOUTER or join.nullable:
                 join_type = LOUTER
             else:
                 join_type = INNER
             join.join_type = join_type
         join.table_alias = alias
         self.alias_map[alias] = join
         return alias
 }}
 
 aliasはまだ設定されてないので後半に進みます。途中、join_typeの調整が行われていますが結局は元のまま、INNERになります。なお、join.parent_aliasで参照されるのはChoiceを表すBaseTableです。join_typeなんてなさそうだけど?と確認したらクラス属性として定義されていました。
 
 setup_joinsメソッドに戻ると、ループは一周だけなので結果、以下のような戻り値となります。
 
 :final_field||Choiceのquestionフィールド
 :targets|[Questionのidフィールド]
 :opts|QuestionのOptions
 :joins|['polls_choice', 'polls_question']
 :path|[PathInfo(ChoiceのOptions, QuestionのOptions, Questionのid, Choiceのquestion, False, True)]
 
 **build_filter残り [#a594f459]
 
 build_filterメソッドに戻って、trim_joinsはまあtrimされることはないだろうと無視、aliasがしれっと'polls_choice'から'polls_question'に切り替わるところだけ注意です。
 
 #code(Python){{
         targets, alias, join_list = self.trim_joins(sources, join_list, path)
 }}
 
 その後、lookupを取得しているところ、関連フィールドなので以下を通ります。
 
 #code(Python){{
         if field.is_relation:
             lookup_class = field.get_lookup(lookups[0])
             if len(targets) == 1:
                 lhs = targets[0].get_col(alias, field)
             else:
                 lhs = MultiColSource(alias, targets, sources, field)
             condition = lookup_class(lhs, value)
             lookup_type = lookup_class.lookup_name
         else:
             # 省略
 }}
 
 field、実体はForeignKey、get_lookupメソッド自体はForeignObjectクラスに書かれています。
 
 #code(Python){{
     def get_lookup(self, lookup_name):
         if lookup_name == 'in':
             return RelatedIn
         elif lookup_name == 'exact':
             return RelatedExact
         # 以下略
 }}
 
 RelatedExtractはrelated_lookups.pyに書かれています。
 
 #code(Python){{
 class RelatedExact(RelatedLookupMixin, Exact):
     pass
 }}
 
 targets[0]はQuestionのidフィールド、ですがget_colメソッドの動作が前に見たときと違うので注意が必要です。
 
 #code(Python){{
     def get_col(self, alias, output_field=None):
         if output_field is None:
             output_field = self
         if alias != self.model._meta.db_table or output_field != self:
             from django.db.models.expressions import Col
             return Col(alias, self, output_field)
         else:
             return self.cached_col
 }}
 
 前はelseに行きましたが今回はifの方が実行されます。つまり、
 
 :alias|'polls_question'
 :target|Questionのidフィールド
 :output_field||Choiceのquestionフィールド
 
 と設定されます。
 
 ***RelatedLookupMixin [#g46ac610]
 
 RelatedLookupMixin、get_prep_lookupメソッドがオーバーライドされています。このメソッドはオブジェクト構築時(つまり__init__メソッド)で呼び出されるようです。
 
 #code(Python){{
 class RelatedLookupMixin(object):
     def get_prep_lookup(self):
         if not isinstance(self.lhs, MultiColSource) and self.rhs_is_direct_value():
             # If we get here, we are dealing with single-column relations.
             self.rhs = get_normalized_value(self.rhs, self.lhs)[0]
             # We need to run the related field's get_prep_value(). Consider case
             # ForeignKey to IntegerField given value 'abc'. The ForeignKey itself
             # doesn't have validation for non-integers, so we must run validation
             # using the target field.
             if self.prepare_rhs and hasattr(self.lhs.output_field, 'get_path_info'):
                 # Get the target field. We can safely assume there is only one
                 # as we don't get to the direct value branch otherwise.
                 target_field = self.lhs.output_field.get_path_info()[-1].target_fields[-1]
                 self.rhs = target_field.get_prep_value(self.rhs)
 
         return super(RelatedLookupMixin, self).get_prep_lookup()
 }}
 
 get_normalized_valueは関数です。
 
 #code(Python){{
 def get_normalized_value(value, lhs):
     from django.db.models import Model
     if isinstance(value, Model):
         value_list = []
         sources = lhs.output_field.get_path_info()[-1].target_fields
         for source in sources:
             while not isinstance(value, source.model) and source.remote_field:
                 source = source.remote_field.model._meta.get_field(source.remote_field.field_name)
             try:
                 value_list.append(getattr(value, source.attname))
             except AttributeError:
                 # A case like Restaurant.objects.filter(place=restaurant_instance),
                 # where place is a OneToOneField and the primary key of Restaurant.
                 return (value.pk,)
         return tuple(value_list)
     if not isinstance(value, tuple):
         return (value,)
     return value
 }}
 
 lhsは先ほど出てきたColオブジェクト、whileは回らないはずでgetattrによりQuestionオブジェクトのid属性を取得、結果それがrhsとして利用されます。
 
 この後、JOINの方法を外部結合にするのか内部結合にするのかなどの処理が行われていますがまあ普通は内部結合になると思うので無視します。
 
 *SQLCompiler [#a722cd6c]
 
 **get_from_clause [#c337ba4c]
 
 さて、Queryオブジェクトが構築されたので次はSQLCompilerです。as_sqlメソッド、そこから呼ばれているメソッドの中でJOIN関連の処理をしてそうなのはget_from_clauseメソッドでしょう。前に見たときのように一部省略
 
 #code(Python){{
     def get_from_clause(self):
         """
         Returns a list of strings that are joined together to go after the
         "FROM" part of the query, as well as a list any extra parameters that
         need to be included. Sub-classes, can override this to create a
         from-clause via a "select".
 
         This should only be called after any SQL construction methods that
         might change the tables we need. This means the select columns,
         ordering and distinct must be done first.
         """
         result = []
         params = []
         for alias in self.query.tables:
             try:
                 from_clause = self.query.alias_map[alias]
             except KeyError:
                 # Extra tables can end up in self.tables, but not in the
                 # alias_map if they aren't in a join. That's OK. We skip them.
                 continue
             clause_sql, clause_params = self.compile(from_clause)
             result.append(clause_sql)
             params.extend(clause_params)
         return result, params
 }}
 
 今回はquery.tablesは2つ、['polls_choice', 'polls_question']です。'polls_choice'はBaseTableなのでそのまま'polls_choice'になります。Joinのas_sqlは対応するSQL片作っているだけなので省略。
 
 後は、whereに設定されているRelatedExact(もう関連モデルを取得するためのキーは取得済みなので実際の動作としてはExact)のas_sqlが呼び出され、検索時にQuestionインスタンスに関係のあるChoiceのみが取得されるということになります。
 
 *おわりに [#w302ec5d]
 
 今回はchoice_set経由での検索の際にどうやってQuestionインスタンスの値が設定されるのか、また、JOINに関する処理を見てきました。複数フィールドでの結合考慮、参照先がNULLの場合考慮など汎用的に書かれているため、特定のケースの場合にどう動くのかを考えてトレースする必要がありました。
 
 さて、長く見てきたモデル(チュートリアル2)もようやく終わりです。モデルはアプリの根幹のため非常に強敵でした。次からはチュートリアル3に進み最後の要素であるテンプレートについて見ていくことにしましょう。
 

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS