Djangoを読む

はじめに

前回までで単独テーブルのINSERT、JOINについて見てきました。しかしやはりRDBと言えば複数テーブルの関連がミソです。というわけで、引き続き「APIで遊んでみる」の

# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. Django creates
# a set to hold the "other side" of a ForeignKey relation
# (e.g. a question's choice) which can be accessed via the API.
>>> q = Question.objects.get(pk=1)

# Create three choices.
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)

について見ていきます。

django.db.models.fields.related

今回の問題は、「Questionにchoice_setなどという属性はいつの間に設定されたのか」です。念のため、Question、あと、Choiceの定義を見てみましょう

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
 
 
 
 
 
 
 
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
 
class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

Question側にchoice_setなどという属性はありません。一方、Choice側はQuestionに対して関連を設定しています。このあたりが鍵になりそうです。

以前にメタ情報構築について見たときは関連が設定されている様子については眺めましたがそれ以上先は突っ込みませんでした。今回は「それ以上先」を見ていくことにします。

Fieldが追加されるとcontribute_to_classメソッドが呼び出されるという仕組みでした。というわけで見てみましょう。RelatedFieldクラスのcontribute_to_classです。

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
 
 
 
 
 
 
 
 
 
 
 
 
-
!
 
-
!
 
 
 
 
    def contribute_to_class(self, cls, name, private_only=False, **kwargs):
 
        super(RelatedField, self).contribute_to_class(cls, name, private_only=private_only, **kwargs)
 
        self.opts = cls._meta
 
        if not cls._meta.abstract:
            if self.remote_field.related_name:
                related_name = self.remote_field.related_name
            else:
                related_name = self.opts.default_related_name
            if related_name:
                # 省略
 
            if self.remote_field.related_query_name:
                # 省略
 
            def resolve_related_class(model, related, field):
                field.remote_field.model = related
                field.do_related_class(related, model)
            lazy_related_operation(resolve_related_class, cls, self.remote_field.model, field=self)

related_name, related_query_nameはともに設定していないので省略します。 で、遅延処理っぽいことが行われています。lazy_related_operation関数はrelate.pyの上の方に書かれています。

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
 
-
|
|
|
|
|
|
|
|
|
|
|
|
!
 
 
 
 
def lazy_related_operation(function, model, *related_models, **kwargs):
    """
    Schedule `function` to be called once `model` and all `related_models`
    have been imported and registered with the app registry. `function` will
    be called with the newly-loaded model classes as its positional arguments,
    plus any optional keyword arguments.
 
    The `model` argument must be a model class. Each subsequent positional
    argument is another model, or a reference to another model - see
    `resolve_relation()` for the various forms these may take. Any relative
    references will be resolved relative to `model`.
 
    This is a convenience wrapper for `Apps.lazy_model_operation` - the app
    registry model used is the one found in `model._meta.apps`.
    """
    models = [model] + [resolve_relation(model, rel) for rel in related_models]
    model_keys = (make_model_tuple(m) for m in models)
    apps = model._meta.apps
    return apps.lazy_model_operation(partial(function, **kwargs), *model_keys)

まあコメントに書いてある通り全部のモデルが登録されたらfunctionが実行されるんだろうということでこれ以上詳細に行くことはやめますが、lazy_model_operationもなかなかおもしろい処理が行われています。どういうものかというと、

  1. model_keysとして指定されているモデルを一つ取り出す(firstとrestに分ける)
  2. firstのモデルがまだ登録されてないなら後回し(ちなみにmodel_keysの一番先頭は今処理中のモデルなので確実に後回しになります)
  3. モデルが登録されているなら渡された関数に、そのモデルクラスを関数の引数に部分適用(partialを利用)
  4. restを引数にして、lazy_model_operationを呼び出す

つまり、一つずつモデルクラスを引数として設定していくという再帰処理が行われています。こうすることで任意個の引数に対応しているんですね。

RelatedField.contribute_to_related_class

さて、話をRelatedFieldに戻しましょう。なんやかんやでresolve_related_classが呼ばれ、do_related_classメソッドが実行されます。

Everything is expanded.Everything is shortened.
  1
  2
  3
 
 
 
    def do_related_class(self, other, cls):
        self.set_attributes_from_rel()
        self.contribute_to_related_class(other, self.remote_field)

このうち、set_attributes_from_relは自分(今はChoiceクラス)のフィールドを設定してるようなので省略、contribute_to_related_classメソッドに進みます。「関連クラスに貢献する」というぐらいだからかなり怪しいです。

contribute_to_related_classはForeignKeyでも定義されていますがぱっと見choice_setは設定されてなさそうなので親クラスのForeignObjectを見てみます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
-
|
!
 
-
|
|
!
 
    def contribute_to_related_class(self, cls, related):
        # Internal FK's - i.e., those with a related name ending with '+' -
        # and swapped models don't get a related descriptor.
        if not self.remote_field.is_hidden() and not related.related_model._meta.swapped:
            setattr(cls._meta.concrete_model, related.get_accessor_name(), self.related_accessor_class(related))
            # While 'limit_choices_to' might be a callable, simply pass
            # it along for later - this is too early because it's still
            # model load time.
            if self.remote_field.limit_choices_to:
                cls._meta.related_fkey_lookups.append(self.remote_field.limit_choices_to)

setattrの行が明らかに怪しいです。

related、その実体はreverse_relatedモジュールのManyToOneRelです。get_accessor_nameメソッドは親クラスのForeignObjectRelに書かれています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 
-
|
|
|
|
|
!
 
 
-
!
 
 
 
 
    def get_accessor_name(self, model=None):
        # This method encapsulates the logic that decides what name to give an
        # accessor descriptor that retrieves related many-to-one or
        # many-to-many objects. It uses the lower-cased object_name + "_set",
        # but this can be overridden with the "related_name" option.
        # Due to backwards compatibility ModelForms need to be able to provide
        # an alternate model. See BaseInlineFormSet.get_default_prefix().
        opts = model._meta if model else self.related_model._meta
        model = model or self.related_model
        if self.multiple:
            # If this is a symmetrical m2m relation on self, there is no reverse accessor.
            if self.symmetrical and model == self.model:
                return None
        if self.related_name:
            return self.related_name
        return opts.model_name + ('_set' if self.multiple else '')

個々に追いかけるのはやめますが、ともかくこれで'choice_set'という名前が得られました(少しややこしいのですが、このメソッドでrelatedと言っているのはQuestionではなくChoiceです)

relatedモジュールに戻って、contribute_to_related_classのインスタンスが作成されています。これが何者かというと、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
 
 
 
 
 
 
 
from .related_descriptors import (
    ForwardManyToOneDescriptor, ManyToManyDescriptor,
    ReverseManyToOneDescriptor, ReverseOneToOneDescriptor,
)
 
class ForeignObject(RelatedField):
    related_accessor_class = ReverseManyToOneDescriptor

です。つまり、choice_setとはデスクリプタのようです。

django.db.models.fields.related_descriptors

さて、デスクリプタなので__get__を見てみます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 
-
|
|
|
|
|
|
|
!
 
 
 
 
    def __get__(self, instance, cls=None):
        """
        Get the related objects through the reverse relation.
 
        With the example above, when getting ``parent.children``:
 
        - ``self`` is the descriptor managing the ``children`` attribute
        - ``instance`` is the ``parent`` instance
        - ``cls`` is the ``Parent`` class (unused)
        """
        if instance is None:
            return self
 
        return self.related_manager_cls(instance)

related_manager_cls。プロパティです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
 
 
 
 
 
 
 
    @cached_property
    def related_manager_cls(self):
        related_model = self.rel.related_model
 
        return create_reverse_many_to_one_manager(
            related_model._default_manager.__class__,
            self.rel,
        )

確認。self.relとはManyToOneRelで、related_modelとはChoiceクラスです。なお、_default_managerはobjectsになるようですが詳細な追っかけは省略します。

さて、create_reverse_many_to_one_managerです。

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
 
-
|
|
|
|
!
 
 
 
 
 
 
 
 
 
 
 
-
!
 
def create_reverse_many_to_one_manager(superclass, rel):
    """
    Create a manager for the reverse side of a many-to-one relation.
 
    This manager subclasses another manager, generally the default manager of
    the related model, and adds behaviors specific to many-to-one relations.
    """
 
    class RelatedManager(superclass):
        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}
 
        # 省略
 
    return RelatedManager

なんの冗談?と思われるかもしれませんがマジです。動的にクラスが定義され返されています。

RelatedManager.create

さて、というわけでchoice_setの(__get__時の)正体がわかったので、その先、createメソッドを確認しましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
        def create(self, **kwargs):
            kwargs[self.field.name] = self.instance
            db = router.db_for_write(self.model, instance=self.instance)
            return super(RelatedManager, self.db_manager(db)).create(**kwargs)

self.fieldはChoiceのqustionフィールド、self.instanceはQuestionインスタンス、self.modelはChoiceです。

親クラス、というかQuerySetのcreateメソッドです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 
-
|
|
!
 
 
 
 
    def create(self, **kwargs):
        """
        Creates a new object with the given kwargs, saving it to the database
        and returning the created object.
        """
        obj = self.model(**kwargs)
        self._for_write = True
        obj.save(force_insert=True, using=self.db)
        return obj

というわけでChoiceモデルを作って保存する際に、ForeignKeyとしてQuestionインスタンスが設定されsaveが行われています。 ForeignKeyが相手のid取得するまで結構長いのだけど、めんどくさいので省略します(笑)

おわりに

今回は複数テーブルの関連、手始めにQuestionにいつの間にか設定されているchoice_setについて見てきました。その正体はデスクリプタで、また、__get__するとManagerが返されるようになっていました。

Managerは動的に作られ、Questionのインスタンスと関連付けられています。そのため、createするとForeignKeyフィールドとしてQuestionインスタンスが設定されINSERTが実行されるという仕組みになっていました。

SELECTについても同じようにQuestionインスタンスが設定されるわけですが、前に見たときはJOINをばっさり省略しているのでそれも含めて改めて検索処理を眺めることにしましょう。


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-08-27 (日) 21:03:05 (353d)