Djangoを読む
はじめに †
migrateコマンドを実行したときの処理の流れについて見たので、チュートリアルを進み、自アプリでのモデル定義、マイグレーションファイルの作成、マイグレーションの実行、がどう行われるかについて確認します。
チュートリアルではまず以下のファイルを作成しています。
polls/models.py
1
2
3
4
5
6
7
8
9
10
11
|
| from django.db import models
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)
|
次に、settings.pyを編集してアプリを追加します。
mysite/settings.py
1
2
3
4
5
6
7
8
9
|
| INSTALLED_APPS = [
'polls.apps.PollsConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
|
続いて、マイグレーションファイルを作成。
$ python manage.py makemigrations polls
コマンドを実行すると、polls/migrations/0001_initial.pyが作られます。
とりあえずここまでで見ていくことにしましょう。
django/core/management/commands/makemigrations.py †
いつも通りにコマンド名と対応するファイルのCommandクラスから開始します。
handleメソッド、まず初めにmigrateコマンドでも出てきたMigrationLoaderクラスを使用してマイグレーションを読み込んでいます。ただし、コメントにあるようにDBからの情報取得は行わないようです。
1
2
3
| -
|
!
| loader = MigrationLoader(None, ignore_no_migrations=True)
|
次に整合性チェックなどが行われていますが普通に使っていれば引っかかることもないのでさくっと無視します。
整合性のチェックが終わるとquestionerというマイグレーション作成時に自動で決定しきれなかった事項をユーザーに確認するためのオブジェクトを作成しています。まあ今回は初回でそんな問い合わせも発生しないのでこちらも無視。
次に、MigrationAutodetectorオブジェクトが作成されます。名前的にもこのオブジェクトがマイグレーション作成の鍵を握っていそうな雰囲気です。先にhandleメソッドの残り(一部省略)を示すと以下のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| -
!
-
!
-
!
| autodetector = MigrationAutodetector(
loader.project_state(),
ProjectState.from_apps(apps),
questioner,
)
changes = autodetector.changes(
graph=loader.graph,
trim_to_apps=app_labels or None,
convert_apps=app_labels or None,
migration_name=self.migration_name,
)
if not changes:
else:
self.write_migration_files(changes)
|
django/db/migrations/autodetector.py †
まずはMigrationAutodetectorに渡されている引数がなんなのかを確認するためにコンストラクタを見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-
|
|
|
|
|
|
|
|
|
!
| class MigrationAutodetector(object):
"""
Takes a pair of ProjectStates, and compares them to see what the
first would need doing to make it match the second (the second
usually being the project's current state).
Note that this naturally operates on entire projects at a time,
as it's likely that changes interact (for example, you can't
add a ForeignKey without having a migration to add the table it
depends on first). A user interface may offer single-app usage
if it wishes, with the caveat that it may not always be possible.
"""
def __init__(self, from_state, to_state, questioner=None):
self.from_state = from_state
self.to_state = to_state
self.questioner = questioner or MigrationQuestioner()
self.existing_apps = {app for app, model in from_state.models}
|
- from_state
- loader.project_state()
- to_state
- ProjectState.from_apps(apps)
ということになります。loaderから取得しているのは「今時点であるマイグレーションファイル」を表したProjectState、from_appsメソッドで取得しているのは「models.pyに書かれている」ProjectStateと予想されます。この2つがあれば差分(どう変更すればto_stateになるのか)が計算できそうです。
MigrationLoader.project_state †
まずは「今時点であるマイグレーションファイル」を表したProjectStateについて見てみましょう。
1
2
3
4
5
6
7
8
|
-
|
|
|
!
| def project_state(self, nodes=None, at_end=True):
"""
Returns a ProjectState object representing the most recent state
that the migrations we loaded represent.
See graph.make_state for the meaning of "nodes" and "at_end"
"""
return self.graph.make_state(nodes=nodes, at_end=at_end, real_apps=list(self.unmigrated_apps))
|
MigrationGraphに続く。(一部省略)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
-
|
|
|
!
| def make_state(self, nodes=None, at_end=True, real_apps=None):
"""
Given a migration node or nodes, returns a complete ProjectState for it.
If at_end is False, returns the state before the migration has run.
If nodes is not provided, returns the overall most current project state.
"""
if nodes is None:
nodes = list(self.leaf_nodes())
plan = []
for node in nodes:
for migration in self.forwards_plan(node):
if migration not in plan:
if not at_end and migration in nodes:
continue
plan.append(migration)
project_state = ProjectState(real_apps=real_apps)
for node in plan:
project_state = self.nodes[node].mutate_state(project_state, preserve=False)
return project_state
|
何をしているかというと、
- 各アプリについて末端(最後)のマイグレーションを取得
- 末端のマイグレーションに到達するまでの各マイグレーションを取得
- 各マイグレーションを適用し、プロジェクトの状態を更新
ということをしています。
Migration.mutate_state †
今回はまだマイグレーションファイルがないので、と端折るのは乱暴なので、マイグレーションファイルがあった場合にどのような処理が行われるのか見ていきます。nodesの各値はMigrationオブジェクトです。Migrationクラスはdjango/db/migrations/migration.pyに記述されています。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-
|
|
|
!
| def mutate_state(self, project_state, preserve=True):
"""
Takes a ProjectState and returns a new one with the migration's
operations applied to it. Preserves the original object state by
default and will return a mutated state from a copy.
"""
new_state = project_state
if preserve:
new_state = project_state.clone()
for operation in self.operations:
operation.state_forwards(self.app_label, new_state)
return new_state
|
operationsの例を確認するために、チュートリアルで作成された0001_initial.pyを見てみましょう。
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
|
| from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Choice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice_text', models.CharField(max_length=200)),
('votes', models.IntegerField(default=0)),
],
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question_text', models.CharField(max_length=200)),
('pub_date', models.DateTimeField(verbose_name='date published')),
],
),
migrations.AddField(
model_name='choice',
name='question',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.Question'),
),
]
|
CreateModelとかがどこにあるのかは少しややこしいのでちゃんと見ます。
django/db/migrations/__init__.py
1
2
|
| from .migration import Migration, swappable_dependency from .operations import *
|
djangp/db/migrations/operations/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
| from .fields import AddField, AlterField, RemoveField, RenameField
from .models import (
AlterIndexTogether, AlterModelManagers, AlterModelOptions, AlterModelTable,
AlterOrderWithRespectTo, AlterUniqueTogether, CreateModel, DeleteModel,
RenameModel,
)
from .special import RunPython, RunSQL, SeparateDatabaseAndState
__all__ = [
'CreateModel', 'DeleteModel', 'AlterModelTable', 'AlterUniqueTogether',
'RenameModel', 'AlterIndexTogether', 'AlterModelOptions',
'AddField', 'RemoveField', 'AlterField', 'RenameField',
'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
'AlterOrderWithRespectTo', 'AlterModelManagers',
]
|
というわけで、
- CreateModel
- django/db/migrations/operations/models.py
- AddField
- django/db/migrations/operations/fields.py
に書かれていることになります。
で、CreateModelのstate_forwardsメソッド、
1
2
3
4
5
6
7
8
9
|
| def state_forwards(self, app_label, state):
state.add_model(ModelState(
app_label,
self.name,
list(self.fields),
dict(self.options),
tuple(self.bases),
list(self.managers),
))
|
ProjectStateのadd_modelメソッドが呼び出されています。ProjectStateはdjango/db/migrations/state.pyに記述されています。なお、ModelStateクラスもstate.pyに書かれています。
1
2
3
4
5
|
| def add_model(self, model_state):
app_label, model_name = model_state.app_label, model_state.name_lower
self.models[(app_label, model_name)] = model_state
if 'apps' in self.__dict__: self.reload_model(app_label, model_name)
|
このような形でmodelsに情報が記録されていくようです。
ProjectState.from_apps †
次に、「models.pyに書かれている」ProjectStateです。
1
2
3
4
5
6
7
8
|
| @classmethod
def from_apps(cls, apps):
"Takes in an Apps and returns a ProjectState matching it"
app_models = {}
for model in apps.get_models(include_swapped=True):
model_state = ModelState.from_model(model)
app_models[(model_state.app_label, model_state.name_lower)] = model_state
return cls(app_models)
|
appsは何ものか。makemigrations.pyに戻って確認すると以下のようになっています。
1
|
| from django.apps import apps
|
django/apps †
これまでにもappsは何回か見かけていましたがここで詳しく確認しましょう。まず、django.apps.appsは何ものかというと、
django/apps/__init__.py
1
2
3
4
|
| from .config import AppConfig
from .registry import apps
__all__ = ['AppConfig', 'apps']
|
django/apps/registry.py
1
2
3
4
5
6
7
8
9
10
|
-
|
|
|
!
-
!
| class Apps(object):
"""
A registry that stores the configuration of installed applications.
It also keeps track of models eg. to provide reverse-relations.
"""
apps = Apps(installed_apps=None)
|
というわけで、appsはregistry.pyに記述されているAppsオブジェクトです。
ファイルの最後で作成されているappsではinstalled_appsとしてNoneが渡されています。しかし、実際にはなんらかの値が設定されていて正しく動作します。次に、ではどこで値の設定が行われているか確認しましょう。答えとしては、初めにコマンドの実行を確認したときに無視したdjango.setupです。
django/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-
|
|
|
!
| def setup(set_prefix=True):
"""
Configure the settings (this happens as a side effect of accessing the
first setting), configure logging and populate the app registry.
Set the thread-local urlresolvers script prefix if `set_prefix` is True.
"""
from django.apps import apps
from django.conf import settings
from django.urls import set_script_prefix
from django.utils.encoding import force_text
from django.utils.log import configure_logging
configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
if set_prefix:
set_script_prefix(
'/' if settings.FORCE_SCRIPT_NAME is None else force_text(settings.FORCE_SCRIPT_NAME)
)
apps.populate(settings.INSTALLED_APPS)
|
populateメソッドは長いので要点だけ。
1
2
3
4
5
6
7
8
9
10
11
12
| -
!
-
!
| for entry in installed_apps:
if isinstance(entry, AppConfig):
app_config = entry
else:
app_config = AppConfig.create(entry)
if app_config.label in self.app_configs:
raise ImproperlyConfigured(
"Application labels aren't unique, "
"duplicates: %s" % app_config.label)
self.app_configs[app_config.label] = app_config
|
INSTALLED_APPSに書かれている各アプリの読み込みを行っています。
1
2
3
4
| -
!
| for app_config in self.app_configs.values():
all_models = self.all_models[app_config.label]
app_config.import_models(all_models)
|
モデルのインポートを行っています。なお、self.all_modelsは以下のように初期化されています。
1
2
3
4
5
6
7
8
| -
|
|
|
|
|
|
!
| self.all_models = defaultdict(OrderedDict)
|
つまり、キーがない状態で参照されるとOrderDictオブジェクトが作成され、それが返されます。
話をAppConfigに移します。import_modelsメソッド、
1
2
3
4
5
6
7
8
9
10
|
-
|
|
|
!
| def import_models(self, all_models):
self.models = all_models
if module_has_submodule(self.module, MODELS_MODULE_NAME):
models_module_name = '%s.%s' % (self.name, MODELS_MODULE_NAME)
self.models_module = import_module(models_module_name)
|
モデルがインポートされたのでAppsのget_modelsに戻ります。
1
2
3
4
5
|
| def get_models(self, include_auto_created=False, include_swapped=False):
result = []
for app_config in self.app_configs.values():
result.extend(list(app_config.get_models(include_auto_created, include_swapped)))
return result
|
再びAppConfigに移り、AppConfigのget_models。
1
2
3
4
5
6
7
8
9
10
11
12
|
-
|
|
|
!
| def get_models(self, include_auto_created=False, include_swapped=False):
"""
Returns an iterable of models.
省略
"""
for model in self.models.values():
if model._meta.auto_created and not include_auto_created:
continue
if model._meta.swapped and not include_swapped:
continue
yield model
|
returnではなくyieldになっているのはメソッドドキュメントにあるようにiterableにするためです。
さて、と、_metaという単語が出てきました。そもそも、いつの間にself.modelsに値が設定されたのでしょうか。これを理解するにはモデルインポート時に行われる処理を見る必要があるのですが、かなり複雑な動作になっていますので、モデルインポート時の処理については改めて見ることにします。
ProjectStateのfrom_appsに戻る。再掲します。
1
2
3
4
5
6
7
8
|
| @classmethod
def from_apps(cls, apps):
"Takes in an Apps and returns a ProjectState matching it"
app_models = {}
for model in apps.get_models(include_swapped=True):
model_state = ModelState.from_model(model)
app_models[(model_state.app_label, model_state.name_lower)] = model_state
return cls(app_models)
|
from_modelsは100行近くあるのでコードを貼るのはやめますが何をしているかというと、
- モデルのメタ情報として格納されているフィールドを取得
- オプション、スーパークラス、マネージャ(DBとの接続管理?)の情報を取得
- 上記を使ってModelStateオブジェクトを作成
ということをしています。
MigrationAutodetector.changes †
さてと、2つのProjectStateがどのように取得されているかの確認がかなり長くなりましたが、これでようやく差分を計算できます。差分計算はMigrationAutodetectorに戻ってchangesメソッドで行われます。
まずは呼び出し部分を再確認します。
1
2
3
4
5
6
|
| changes = autodetector.changes(
graph=loader.graph,
trim_to_apps=app_labels or None,
convert_apps=app_labels or None,
migration_name=self.migration_name,
)
|
app_labelsは指定したアプリ、今回はpollsのみ、migration_nameは指定してないのでNoneになります。
さて、changesメソッド。
1
2
3
4
5
6
7
8
9
10
11
|
-
|
|
|
!
| def changes(self, graph, trim_to_apps=None, convert_apps=None, migration_name=None):
"""
Main entry point to produce a list of applicable changes.
Takes a graph to base names on and an optional set of apps
to try and restrict to (restriction is not guaranteed)
"""
changes = self._detect_changes(convert_apps, graph)
changes = self.arrange_for_graph(changes, graph, migration_name)
if trim_to_apps:
changes = self._trim_to_apps(changes, trim_to_apps)
return changes
|
ドキュメントにあるようにこのメソッドはエントリーポイントで差分計算の本体は_detect_changesのようです。
_detect_changesはちょっと長いので順に眺めていきます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-
|
|
|
|
|
|
|
|
|
|
|
|
!
| def _detect_changes(self, convert_apps=None, graph=None):
"""
Returns a dict of migration plans which will achieve the
change from from_state to to_state. The dict has app labels
as keys and a list of migrations as values.
The resulting migrations aren't specially named, but the names
do matter for dependencies inside the set.
convert_apps is the list of apps to convert to use migrations
(i.e. to make initial migrations for, in the usual case)
graph is an optional argument that, if provided, can help improve
dependency generation and avoid potential circular dependencies.
"""
|
まず先頭のメソッドドキュメント。一段落目を見ると、このメソッドは、
{アプリ名: [Migrationインスタンス...]}
という辞書を返すことがわかります。
メソッド本体に入ります。
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
| -
|
|
|
!
-
|
!
-
| self.generated_operations = {}
self.old_apps = self.from_state.concrete_apps
self.new_apps = self.to_state.apps
self.old_model_keys = []
self.old_proxy_keys = []
self.old_unmanaged_keys = []
self.new_model_keys = []
self.new_proxy_keys = []
self.new_unmanaged_keys = []
for al, mn in sorted(self.from_state.models.keys()):
model = self.old_apps.get_model(al, mn)
if not model._meta.managed:
self.old_unmanaged_keys.append((al, mn))
elif al not in self.from_state.real_apps:
if model._meta.proxy:
self.old_proxy_keys.append((al, mn))
else:
self.old_model_keys.append((al, mn))
|
まずは初期化です。初めのコメントにあるようにMigrationインスタンスをいきなり作成するのではなく、まずはoperationを収集し、その後、Migrationを作成するようです。
なお、from_state、to_stateからappsを取得していますがこれは先ほど見たAppsではなく、state.pyで定義されているStateAppsというものです(Appsのサブクラス)。見ていくと長くなるので、イメージ的にはAppsと同じようなものと考えていいでしょう。
続き。Stateの差分を取り、モデルの作成、フィールド追加などのoperationを生成していると思われます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| -
!
-
!
-
!
-
!
| self.generate_renamed_models()
self._prepare_field_lists()
self._generate_through_model_map()
self.generate_deleted_models()
self.generate_created_models()
self.generate_deleted_proxies()
self.generate_created_proxies()
self.generate_altered_options()
self.generate_altered_managers()
self.generate_renamed_fields()
self.generate_removed_fields()
self.generate_added_fields()
self.generate_altered_fields()
self.generate_altered_unique_together()
self.generate_altered_index_together()
self.generate_altered_db_table()
self.generate_altered_order_with_respect_to()
|
最後にMigrationにまとめて返しています。
1
2
3
4
5
|
| self._sort_migrations()
self._build_migration_list(graph)
self._optimize_migrations()
return self.migrations
|
generate_created_models †
今回は初めてモデルを書いたという前提で読み進めているので、実際に処理が行われそうなgenerate_created_modelsを見てみましょう。とはいうものの、generate_created_modelsは100行以上あるので要点だけ、
1
2
3
4
5
6
7
|
| old_keys = set(self.old_model_keys).union(self.old_unmanaged_keys)
added_models = set(self.new_model_keys) - old_keys
added_unmanaged_models = set(self.new_unmanaged_keys) - old_keys
all_added_models = chain(
sorted(added_models, key=self.swappable_first_key, reverse=True),
sorted(added_unmanaged_models, key=self.swappable_first_key, reverse=True)
)
|
新しいモデル(models.pyに書かれているモデル)と古いモデル(現存するマイグレーションを適用しきった時点のモデル)で差分を取っています。unmanagedとかswappableとかありますが今回の場合はあまり気にする必要はありません、ともかく、「models.pyに書かれているのの逆順で処理」ということになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-
!
| for app_label, model_name in all_added_models:
model_state = self.to_state.models[app_label, model_name]
model_opts = self.new_apps.get_model(app_label, model_name)._meta
related_fields = {}
primary_key_rel = None
for field in model_opts.local_fields:
if field.remote_field:
if field.remote_field.model:
if field.primary_key:
primary_key_rel = field.remote_field.model
elif not field.remote_field.parent_link:
related_fields[field.name] = field
|
モデルのフィールドを走査し、remote_field(他のモデルへの参照)を記録しています。今回の場合、primary_keyではないので最終行の処理が行われrelated_fieldsに記録されるはず。
1
2
3
4
5
6
7
8
9
10
11
12
13
| -
!
| self.add_operation(
app_label,
operations.CreateModel(
name=model_state.name,
fields=[d for d in model_state.fields if d[0] not in related_fields],
options=model_state.options,
bases=model_state.bases,
managers=model_state.managers,
),
dependencies=dependencies,
beginning=True,
)
|
その後、依存関係の処理などが行われたうえでCreateModelオブジェクトがoperationとして追加されています。この際に注意が必要な点としては、related_fieldsに記録されているフィールドは含まない、という点です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| -
!
-
!
-
!
| for name, field in sorted(related_fields.items()):
dependencies = self._get_dependecies_for_foreign_key(field)
dependencies.append((app_label, model_name, None, True))
self.add_operation(
app_label,
operations.AddField(
model_name=model_name,
name=name,
field=field,
),
dependencies=list(set(dependencies)),
)
|
除外しておいたrelated_fieldsに対するAddFieldを追加しています。このようにしているのは、あらかじめモデルを全部作っておいてから参照設定を行うためと思われます。依存関係として、参照先のモデルを設定しています。
_sort_migrations †
operationが収集できたら依存関係を確認して並び替えます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-
|
|
|
!
-
!
-
!
| def _sort_migrations(self):
"""
Reorder to make things possible. The order we have already isn't bad,
but we need to pull a few things around so FKs work nicely inside the
same app
"""
for app_label, ops in sorted(self.generated_operations.items()):
dependency_graph = {op: set() for op in ops}
for op in ops:
for dep in op._auto_deps:
if dep[0] == app_label:
for op2 in ops:
if self.check_dependency(op2, dep):
dependency_graph[op].add(op2)
self.generated_operations[app_label] = stable_topological_sort(ops, dependency_graph)
|
check_dependencyは淡々と依存関係チェックしているだけなので省略。ともかくこのメソッドが実行されることにより、
- CreateModel('Choice')
- AddField('choice', 'question', ForeignKey)
- CreateModel('Question')
と並んでいたものが
- CreateModel('Choice')
- CreateModel('Question')
- AddField('choice', 'question', ForeignKey)
と依存関係を満たすように並び替えられます。
_build_migration_list †
operationの収集、並び替えができたのでようやくMigrationにまとめる処理が行われます。こちらも長いので要点だけ
1
2
3
4
|
| self.migrations = {}
num_ops = sum(len(x) for x in self.generated_operations.values())
chop_mode = False
while num_ops:
|
operationの数を取得し、whileで回しています。条件になっているので、処理が行われるとnum_opsが減算されると予想できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-
!
| for app_label in sorted(self.generated_operations.keys()):
chopped = []
dependencies = set()
for operation in list(self.generated_operations[app_label]):
deps_satisfied = True
operation_dependencies = set()
if deps_satisfied:
chopped.append(operation)
dependencies.update(operation_dependencies)
self.generated_operations[app_label] = self.generated_operations[app_label][1:]
else:
break
|
アプリごと、operationごとに処理を行っています。依存関係の処理を行っていますが今回の場合は関係ないようなのでさっくり無視します。というわけでchoppedにoperationがたまっていき、一方、インスタンス変数のgenerated_operationsからはoperationが取り除かれていきます。
一瞬、ループ内でループ対象を更新して大丈夫?と思いましたがlist関数を使っているので別のリストオブジェクトを使って繰り返しが行われているようです。
1
2
3
4
5
6
7
8
9
|
| if dependencies or chopped:
if not self.generated_operations[app_label] or chop_mode:
subclass = type(str("Migration"), (Migration,), {"operations": [], "dependencies": []})
instance = subclass("auto_%i" % (len(self.migrations.get(app_label, [])) + 1), app_label)
instance.dependencies = list(dependencies)
instance.operations = chopped
instance.initial = app_label not in self.existing_apps
self.migrations.setdefault(app_label, []).append(instance)
chop_mode = False
|
一つ目のifはchoppedが空じゃないから真、二つ目のifはgenerated_operationsが空だから真になります。
条件が満たされればついにMigrationインスタンスの作成です。
まずtype関数を用いて動的にクラスを作成しています。紛らわしいですが、第二引数(スーパークラス)のMigrationはフルパスで書くとdjango.db.migrations.migration.Migrationクラスです。
その後、インスタンスを作成し、インスタンス変数を設定しています。今回の場合、アプリで初めてのマイグレーションになるのでinitialがTrueになります。
1
2
3
4
5
6
7
|
| new_num_ops = sum(len(x) for x in self.generated_operations.values())
if new_num_ops == num_ops:
if not chop_mode:
chop_mode = True
else:
raise ValueError("Cannot resolve operation dependencies: %r" % self.generated_operations)
num_ops = new_num_ops
|
num_opsの更新。この後、whileに戻って繰り返しが行われます。普通にアプリのモデルを更新しマイグレーションを作成するだけならループは一回だけで終わると思われます。
_detect_changesメソッドでは後、_optimize_migrationsメソッドを呼び出して最適化を行ってますが省略します。
changesメソッドの残り部分 †
changesメソッドに戻ってきてarrange_for_graphが呼び出されます。graphオブジェクトをチェックし、作成したマイグレーションの名前(連番)を決定しています。今回は初回なので0001_initialになります。
はいいのですが、初回かの確認にquestionerのask_initialメソッドが呼び出されています。私も初め勘違いしていたのですが、アプリを作成した初期状態でもmigrationsというディレクトリは存在している(__init__.pyだけある)ので以下のコードは最後の行のチェックが行われ、Trueが返されることになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-
!
| migrations_import_path = MigrationLoader.migrations_module(app_config.label)
if migrations_import_path is None:
return self.defaults.get("ask_initial", False)
try:
migrations_module = importlib.import_module(migrations_import_path)
except ImportError:
return self.defaults.get("ask_initial", False)
else:
if hasattr(migrations_module, "__file__"):
filenames = os.listdir(os.path.dirname(migrations_module.__file__))
elif hasattr(migrations_module, "__path__"):
if len(migrations_module.__path__) > 1:
return False
filenames = os.listdir(list(migrations_module.__path__)[0])
return not any(x.endswith(".py") for x in filenames if x != "__init__.py")
|
その後に呼び出されている_trim_to_appsメソッドでは依存関係を考慮して必要のないアプリが取り除かれます。今回の場合は元々pollsアプリしか対象になっていないのでそのままになります。
以上で差分の計算が完了しました。
MigrationAutodetector.write_migration_files †
Migrationオブジェクトが作成できたので最後に書き込みです。write_migration_filesメソッドで処理が行われています。
Migrationオブジェクトに対してそれをファイルの形にするのにはdjango.db.migrations.writerモジュールのMigrationWriterクラスが用いられています。このメソッドのas_stringメソッドを呼び出すことでMigrationオブジェクトをファイルに書き込む文字列としています。
operationの書き込みにはさらに下請けとしてOperationWriterが用いられています。OperationWriterはoperationのdeconstructメソッドを呼び出して実際の値を取得、また、operationの__init__メソッドの引数名を取得して処理することで手で書いたようにoperationのオブジェクト作成を再構成しています。
__init__メソッドの引数については、serializerモジュールで定義されているSerializerを使って文字列化しています。この際、FieldオブジェクトについてはさらにFieldオブジェクトのdeconstructメソッドが呼び出され再構成に必要な情報の取得が行われているようです。
ということが淡々と行われています。コードは貼っていると長くなるので省略。
おわりに †
今回はmakemigrations時に行われる処理について見てきました。
長い!ひたすら長かったです。今まで読解に直接必要ないからと後回しにしていた部分が全部回ってきた印象でした(笑)
また、Field, Operation, Serializerなど本気のオブジェクト指向になってきたなという印象もありました。
おさらいしましょう。
- マイグレーションファイルからfrom_stateを構築する(現在あるマイグレーションを適用していき、from_stateの状態にする)
- モデルからto_stateを構築する
- django.apps.appsが用いられる。appsはコマンドの共通処理としてdjango.setupが呼び出されており、その中でapps.populateが実行されることでモデルのインポートが行われる。モデルはインポートされると自動的にappsへの登録処理が行われる
- from_stateとto_stateを比較し、差分を埋めるoperationリストを作成する
- operationリストをまとめてMigrationオブジェクトを作り、MigrationWriterを用いてPythonスクリプト化する
このうち、「モデルをインポートすると自動的に登録が行われる」という部分は後で読む、ということで省略しました。次回はこの部分について読んでいくことにします。
実はDjangoを使っていて一番謎だった部分はここでした。どうやってDBの状態と最新(書き換えた)モデルの差分を取ってマイグレーションを作成しているのかと。DBから情報取得してるのかなと思いましたが読んでみるとまあ確かにマイグレーションファイルさえあれば特定のDBMSに依存した処理をしないで差分の計算が行えるね、ということが発見できてよかったです。