[[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に進み最後の要素であるテンプレートについて見ていくことにしましょう。