[[Djangoを読む]] #contents *はじめに [#o689d560] migrateコマンドを実行したときの処理の流れについて見たので、チュートリアルを進み、自アプリでのモデル定義、マイグレーションファイルの作成、マイグレーションの実行、がどう行われるかについて確認します。 チュートリアルではまず以下のファイルを作成しています。 polls/models.py #code(Python){{ 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 #code(Python){{ 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 [#r9bd3e54] いつも通りにコマンド名と対応するファイルのCommandクラスから開始します。 handleメソッド、まず初めにmigrateコマンドでも出てきたMigrationLoaderクラスを使用してマイグレーションを読み込んでいます。ただし、コメントにあるようにDBからの情報取得は行わないようです。 #code(Python){{ # Load the current graph state. Pass in None for the connection so # the loader doesn't try to resolve replaced migrations from DB. loader = MigrationLoader(None, ignore_no_migrations=True) }} 次に整合性チェックなどが行われていますが普通に使っていれば引っかかることもないのでさくっと無視します。 整合性のチェックが終わるとquestionerというマイグレーション作成時に自動で決定しきれなかった事項をユーザーに確認するためのオブジェクトを作成しています。まあ今回は初回でそんな問い合わせも発生しないのでこちらも無視。 次に、MigrationAutodetectorオブジェクトが作成されます。名前的にもこのオブジェクトがマイグレーション作成の鍵を握っていそうな雰囲気です。先にhandleメソッドの残り(一部省略)を示すと以下のようになります。 #code(Python){{ # Set up autodetector autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) # Detect changes 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 [#x81c10f0] まずはMigrationAutodetectorに渡されている引数がなんなのかを確認するためにコンストラクタを見てみましょう。 #code(Python){{ 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 [#y7122d5a] まずは「今時点であるマイグレーションファイル」を表したProjectStateについて見てみましょう。 #code(Python){{ 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に続く。(一部省略) #code(Python){{ 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 [#e3e74a40] 今回はまだマイグレーションファイルがないので、と端折るのは乱暴なので、マイグレーションファイルがあった場合にどのような処理が行われるのか見ていきます。nodesの各値はMigrationオブジェクトです。Migrationクラスはdjango/db/migrations/migration.pyに記述されています。 #code(Python){{ 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を見てみましょう。 #code(Python){{ 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 #code(Python){{ from .migration import Migration, swappable_dependency # NOQA from .operations import * # NOQA }} djangp/db/migrations/operations/__init__.py #code(Python){{ 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メソッド、 #code(Python){{ 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に書かれています。 #code(Python){{ 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__: # hasattr would cache the property self.reload_model(app_label, model_name) }} このような形でmodelsに情報が記録されていくようです。 **ProjectState.from_apps [#r17b00f9] 次に、「models.pyに書かれている」ProjectStateです。 #code(Python){{ @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に戻って確認すると以下のようになっています。 #code(Python){{ from django.apps import apps }} ***django/apps [#vd90b8d5] これまでにもappsは何回か見かけていましたがここで詳しく確認しましょう。まず、django.apps.appsは何ものかというと、 django/apps/__init__.py #code(Python){{ from .config import AppConfig from .registry import apps __all__ = ['AppConfig', 'apps'] }} django/apps/registry.py #code(Python){{ 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 #code(Python){{ 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メソッドは長いので要点だけ。 #code(Python){{ # Load app configs and app modules. 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に書かれている各アプリの読み込みを行っています。 #code(Python){{ # Load models. 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は以下のように初期化されています。 #code(Python){{ # Mapping of app labels => model names => model classes. Every time a # model is imported, ModelBase.__new__ calls apps.register_model which # creates an entry in all_models. All imported models are registered, # regardless of whether they're defined in an installed application # and whether the registry has been populated. Since it isn't possible # to reimport a module safely (it could reexecute initialization code) # all_models is never overridden or reset. self.all_models = defaultdict(OrderedDict) }} つまり、キーがない状態で参照されるとOrderDictオブジェクトが作成され、それが返されます。 話をAppConfigに移します。import_modelsメソッド、 #code(Python){{ def import_models(self, all_models): # Dictionary of models for this app, primarily maintained in the # 'all_models' attribute of the Apps this AppConfig is attached to. # Injected as a parameter because it gets populated when models are # imported, which might happen before populate() imports 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に戻ります。 #code(Python){{ 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。 #code(Python){{ 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に戻る。再掲します。 #code(Python){{ @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 [#s8194a85] さてと、2つのProjectStateがどのように取得されているかの確認がかなり長くなりましたが、これでようやく差分を計算できます。差分計算はMigrationAutodetectorに戻ってchangesメソッドで行われます。 まずは呼び出し部分を再確認します。 #code(Python){{ 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メソッド。 #code(Python){{ 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はちょっと長いので順に眺めていきます。 #code(Python){{ 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インスタンス...]} という辞書を返すことがわかります。 メソッド本体に入ります。 #code(Python){{ # The first phase is generating all the operations for each app # and gathering them into a big per-app list. # We'll then go through that list later and order it and split # into migrations to resolve dependencies caused by M2Ms and FKs. self.generated_operations = {} # Prepare some old/new state and model lists, separating # proxy models and ignoring unmigrated apps. 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)) # new_*について同じような処理 }} まずは初期化です。初めのコメントにあるようにMigrationインスタンスをいきなり作成するのではなく、まずはoperationを収集し、その後、Migrationを作成するようです。 なお、from_state、to_stateからappsを取得していますがこれは先ほど見たAppsではなく、state.pyで定義されているStateAppsというものです(Appsのサブクラス)。見ていくと長くなるので、イメージ的にはAppsと同じようなものと考えていいでしょう。 続き。Stateの差分を取り、モデルの作成、フィールド追加などのoperationを生成していると思われます。 #code(Python){{ # Renames have to come first self.generate_renamed_models() # Prepare lists of fields and generate through model map self._prepare_field_lists() self._generate_through_model_map() # Generate non-rename model operations self.generate_deleted_models() self.generate_created_models() self.generate_deleted_proxies() self.generate_created_proxies() self.generate_altered_options() self.generate_altered_managers() # Generate field operations 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にまとめて返しています。 #code(Python){{ self._sort_migrations() self._build_migration_list(graph) self._optimize_migrations() return self.migrations }} **generate_created_models [#j384be94] 今回は初めてモデルを書いたという前提で読み進めているので、実際に処理が行われそうなgenerate_created_modelsを見てみましょう。とはいうものの、generate_created_modelsは100行以上あるので要点だけ、 #code(Python){{ 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に書かれているのの逆順で処理」ということになります。 #code(Python){{ 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 # Gather related fields 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に記録されるはず。 #code(Python){{ # Generate creation operation 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に記録されているフィールドは含まない、という点です。 #code(Python){{ # Generate operations for each related field for name, field in sorted(related_fields.items()): dependencies = self._get_dependecies_for_foreign_key(field) # Depend on our own model being created dependencies.append((app_label, model_name, None, True)) # Make operation self.add_operation( app_label, operations.AddField( model_name=model_name, name=name, field=field, ), dependencies=list(set(dependencies)), ) }} 除外しておいたrelated_fieldsに対するAddFieldを追加しています。このようにしているのは、あらかじめモデルを全部作っておいてから参照設定を行うためと思われます。依存関係として、参照先のモデルを設定しています。 **_sort_migrations [#baf1bc49] operationが収集できたら依存関係を確認して並び替えます。 #code(Python){{ 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()): # construct a dependency graph for intra-app dependencies 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) # we use a stable sort for deterministic tests & general behavior 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 [#ze0faa89] operationの収集、並び替えができたのでようやくMigrationにまとめる処理が行われます。こちらも長いので要点だけ #code(Python){{ self.migrations = {} num_ops = sum(len(x) for x in self.generated_operations.values()) chop_mode = False while num_ops: }} operationの数を取得し、whileで回しています。条件になっているので、処理が行われるとnum_opsが減算されると予想できます。 #code(Python){{ 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関数を使っているので別のリストオブジェクトを使って繰り返しが行われているようです。 #code(Python){{ 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関数>https://docs.python.jp/3/library/functions.html#type]]を用いて動的にクラスを作成しています。紛らわしいですが、第二引数(スーパークラス)のMigrationはフルパスで書くとdjango.db.migrations.migration.Migrationクラスです。 その後、インスタンスを作成し、インスタンス変数を設定しています。今回の場合、アプリで初めてのマイグレーションになるのでinitialがTrueになります。 #code(Python){{ 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メソッドの残り部分 [#pd68a27f] changesメソッドに戻ってきてarrange_for_graphが呼び出されます。graphオブジェクトをチェックし、作成したマイグレーションの名前(連番)を決定しています。今回は初回なので0001_initialになります。 はいいのですが、初回かの確認にquestionerのask_initialメソッドが呼び出されています。私も初め勘違いしていたのですが、アプリを作成した初期状態でもmigrationsというディレクトリは存在している(__init__.pyだけある)ので以下のコードは最後の行のチェックが行われ、Trueが返されることになります。 #code(Python){{ migrations_import_path = MigrationLoader.migrations_module(app_config.label) if migrations_import_path is None: # It's an application with migrations disabled. 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 [#ab117f32] 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メソッドが呼び出され再構成に必要な情報の取得が行われているようです。 ということが淡々と行われています。コードは貼っていると長くなるので省略。 *おわりに [#f1143ae8] 今回は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に依存した処理をしないで差分の計算が行えるね、ということが発見できてよかったです。