Djangoを読む

はじめに

というわけで引き続き、

# 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

前回も見たRelatedManager、この中で検索に関係のありそうなものを探してみると以下のメソッドがあります。get_querysetメソッドはallメソッドとかを呼び出すと裏で実行され、その名の通りQuerySetオブジェクトを返します。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
 
 
 
 
 
        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に進みます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 
-
|
!
 
 
 
 
 
 
 
 
 
 
 
 
        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__で初期化されていました。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
 
 
 
 
 
 
 
        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

ここまでわかったらQueryクラスについて前回は端折った関連、JOIN周りについて確認していきます。

filter→add_q→_add_q→build_filterと前に見たフローを進んでいき、build_filterが呼び出しているsetup_joinsに注目します。名前からして明らかにJOINを処理してそうです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
 
-
!
 
 
-
|
|
!
 
 
 
 
 
 
 
 
 
 
    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

前はnames_to_pathもほぼ無視しましたが今回はこれが効いてきます。まずはこちらを見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
 
 
 
 
 
 
 
 
-
!
-
!
 
 
 
-
!
 
 
 
 
 
 
 
-
!
    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っで定義されていて、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
 
-
|
!
 
 
 
    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です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
-
|
|
!
# 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はプロパティで、追いかけていくと、

  1. related_fieldsプロパティ
  2. 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

さて、setup_joinsに戻ってnames_to_path呼び出しの後を再掲、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 
 
 
 
 
 
 
 
 
 
 
        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メソッド。メソッドドキュメントは引数の説明が古いので省略。最新だと直っているようです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
    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残り

build_filterメソッドに戻って、trim_joinsはまあtrimされることはないだろうと無視、aliasがしれっと'polls_choice'から'polls_question'に切り替わるところだけ注意です。

Everything is expanded.Everything is shortened.
  1
 
        targets, alias, join_list = self.trim_joins(sources, join_list, path)

その後、lookupを取得しているところ、関連フィールドなので以下を通ります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
 
 
 
 
 
 
 
 
-
        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クラスに書かれています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
 
 
 
 
-
    def get_lookup(self, lookup_name):
        if lookup_name == 'in':
            return RelatedIn
        elif lookup_name == 'exact':
            return RelatedExact
        # 以下略

RelatedExtractはrelated_lookups.pyに書かれています。

Everything is expanded.Everything is shortened.
  1
  2
 
 
class RelatedExact(RelatedLookupMixin, Exact):
    pass

targets[0]はQuestionのidフィールド、ですがget_colメソッドの動作が前に見たときと違うので注意が必要です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
 
 
 
 
 
 
 
    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

RelatedLookupMixin、get_prep_lookupメソッドがオーバーライドされています。このメソッドはオブジェクト構築時(つまり__init__メソッド)で呼び出されるようです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 
 
 
-
!
-
|
|
|
!
-
|
!
 
 
 
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は関数です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 
 
 
 
 
 
 
 
 
 
-
|
!
 
 
 
 
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

get_from_clause

さて、Queryオブジェクトが構築されたので次はSQLCompilerです。as_sqlメソッド、そこから呼ばれているメソッドの中でJOIN関連の処理をしてそうなのはget_from_clauseメソッドでしょう。前に見たときのように一部省略

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
-
|
!
 
 
 
 
 
 
 
 
 
 
 
 
 
-
|
!
 
 
 
 
    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のみが取得されるということになります。

おわりに

今回はchoice_set経由での検索の際にどうやってQuestionインスタンスの値が設定されるのか、また、JOINに関する処理を見てきました。複数フィールドでの結合考慮、参照先がNULLの場合考慮など汎用的に書かれているため、特定のケースの場合にどう動くのかを考えてトレースする必要がありました。

さて、長く見てきたモデル(チュートリアル2)もようやく終わりです。モデルはアプリの根幹のため非常に強敵でした。次からはチュートリアル3に進み最後の要素であるテンプレートについて見ていくことにしましょう。


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-09-10 (日) 22:40:15 (2418d)