[[Djangoを読む]]
 
 #contents
 
 *はじめに [#j1aa88c0]
 
 前回までで単独テーブルの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 [#g373d45d]
 
 今回の問題は、「Questionにchoice_setなどという属性はいつの間に設定されたのか」です。念のため、Question、あと、Choiceの定義を見てみましょう
 
 #code(Python){{
 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に対して関連を設定しています。このあたりが鍵になりそうです。
 
 [[以前>http://www.dzeta.jp/~junjis/code_reading/index.php?Django%2F%E3%83%A2%E3%83%87%E3%83%AB%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88%E6%99%82%E3%81%AE%E5%87%A6%E7%90%86%E3%82%92%E8%AA%AD%E3%82%80%EF%BC%88%E3%83%A1%E3%82%BF%E6%83%85%E5%A0%B1%E6%A7%8B%E7%AF%89%EF%BC%89#lffffc5c]]にメタ情報構築について見たときは関連が設定されている様子については眺めましたがそれ以上先は突っ込みませんでした。今回は「それ以上先」を見ていくことにします。
 
 Fieldが追加されるとcontribute_to_classメソッドが呼び出されるという仕組みでした。というわけで見てみましょう。RelatedFieldクラスのcontribute_to_classです。
 
 #code(Python){{
     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の上の方に書かれています。
 
 #code(Python){{
 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もなかなかおもしろい処理が行われています。どういうものかというと、
 
 +model_keysとして指定されているモデルを一つ取り出す(firstとrestに分ける)
 +firstのモデルがまだ登録されてないなら後回し(ちなみにmodel_keysの一番先頭は今処理中のモデルなので確実に後回しになります)
 +モデルが登録されているなら渡された関数に、そのモデルクラスを関数の引数に部分適用(partialを利用)
 +restを引数にして、lazy_model_operationを呼び出す
 
 つまり、一つずつモデルクラスを引数として設定していくという再帰処理が行われています。こうすることで任意個の引数に対応しているんですね。
 
 **RelatedField.contribute_to_related_class [#f0851c75]
 
 さて、話をRelatedFieldに戻しましょう。なんやかんやでresolve_related_classが呼ばれ、do_related_classメソッドが実行されます。
 
 #code(Python){{
     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を見てみます。
 
 #code(Python){{
     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に書かれています。
 
 #code(Python){{
     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のインスタンスが作成されています。これが何者かというと、
 
 #code(Python){{
 from .related_descriptors import (
     ForwardManyToOneDescriptor, ManyToManyDescriptor,
     ReverseManyToOneDescriptor, ReverseOneToOneDescriptor,
 )
 
 class ForeignObject(RelatedField):
     related_accessor_class = ReverseManyToOneDescriptor
 }}
 
 です。つまり、choice_setとはデスクリプタのようです。
 
 *django.db.models.fields.related_descriptors [#ib3f90aa]
 
 さて、デスクリプタなので__get__を見てみます。
 
 #code(Python){{
     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。プロパティです。
 
 #code(Python){{
     @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です。
 
 #code(Python){{
 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 [#o63381f0]
 
 さて、というわけでchoice_setの(__get__時の)正体がわかったので、その先、createメソッドを確認しましょう。
 
 #code(Python){{
         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メソッドです。
 
 #code(Python){{
     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取得するまで結構長いのだけど、めんどくさいので省略します(笑)
 
 *おわりに [#ud94ae10]
 
 今回は複数テーブルの関連、手始めにQuestionにいつの間にか設定されているchoice_setについて見てきました。その正体はデスクリプタで、また、__get__するとManagerが返されるようになっていました。
 
 Managerは動的に作られ、Questionのインスタンスと関連付けられています。そのため、createするとForeignKeyフィールドとしてQuestionインスタンスが設定されINSERTが実行されるという仕組みになっていました。
 
 SELECTについても同じようにQuestionインスタンスが設定されるわけですが、前に見たときはJOINをばっさり省略しているのでそれも含めて改めて検索処理を眺めることにしましょう。
 

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