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の定義を見てみましょう
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です。
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の上の方に書かれています。
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もなかなかおもしろい処理が行われています。どういうものかというと、
- model_keysとして指定されているモデルを一つ取り出す(firstとrestに分ける)
- firstのモデルがまだ登録されてないなら後回し(ちなみにmodel_keysの一番先頭は今処理中のモデルなので確実に後回しになります)
- モデルが登録されているなら渡された関数に、そのモデルクラスを関数の引数に部分適用(partialを利用)
- restを引数にして、lazy_model_operationを呼び出す
つまり、一つずつモデルクラスを引数として設定していくという再帰処理が行われています。こうすることで任意個の引数に対応しているんですね。
RelatedField.contribute_to_related_class †
さて、話をRelatedFieldに戻しましょう。なんやかんやでresolve_related_classが呼ばれ、do_related_classメソッドが実行されます。
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を見てみます。
1
2
3
4
5
6
7
8
9
10
|
-
|
!
-
|
|
!
| def contribute_to_related_class(self, cls, related):
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))
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に書かれています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-
|
|
|
|
|
!
-
!
| def get_accessor_name(self, model=None):
opts = model._meta if model else self.related_model._meta
model = model or self.related_model
if self.multiple:
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のインスタンスが作成されています。これが何者かというと、
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__を見てみます。
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。プロパティです。
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です。
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メソッドを確認しましょう。
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メソッドです。
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をばっさり省略しているのでそれも含めて改めて検索処理を眺めることにしましょう。