Djangoを読む

はじめに

migrateコマンドを実行したときの処理の流れについて見たので、チュートリアルを進み、自アプリでのモデル定義、マイグレーションファイルの作成、マイグレーションの実行、がどう行われるかについて確認します。

チュートリアルではまず以下のファイルを作成しています。

polls/models.py

Everything is expanded.Everything is shortened.
  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

Everything is expanded.Everything is shortened.
  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からの情報取得は行わないようです。

Everything is expanded.Everything is shortened.
  1
  2
  3
-
|
!
        # 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メソッドの残り(一部省略)を示すと以下のようになります。

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
-
!
 
 
 
 
 
-
!
 
 
 
 
 
 
 
-
!
 
        # 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

まずはMigrationAutodetectorに渡されている引数がなんなのかを確認するためにコンストラクタを見てみましょう。

Everything is expanded.Everything is shortened.
  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について見てみましょう。

Everything is expanded.Everything is shortened.
  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に続く。(一部省略)

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 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

何をしているかというと、

  1. 各アプリについて末端(最後)のマイグレーションを取得
  2. 末端のマイグレーションに到達するまでの各マイグレーションを取得
  3. 各マイグレーションを適用し、プロジェクトの状態を更新

ということをしています。

Migration.mutate_state

今回はまだマイグレーションファイルがないので、と端折るのは乱暴なので、マイグレーションファイルがあった場合にどのような処理が行われるのか見ていきます。nodesの各値はMigrationオブジェクトです。Migrationクラスはdjango/db/migrations/migration.pyに記述されています。

Everything is expanded.Everything is shortened.
  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を見てみましょう。

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
 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

Everything is expanded.Everything is shortened.
  1
  2
 
 
from .migration import Migration, swappable_dependency  # NOQA
from .operations import *  # NOQA

djangp/db/migrations/operations/__init__.py

Everything is expanded.Everything is shortened.
  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メソッド、

Everything is expanded.Everything is shortened.
  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に書かれています。

Everything is expanded.Everything is shortened.
  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__:  # hasattr would cache the property
            self.reload_model(app_label, model_name)

このような形でmodelsに情報が記録されていくようです。

ProjectState.from_apps

次に、「models.pyに書かれている」ProjectStateです。

Everything is expanded.Everything is shortened.
  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に戻って確認すると以下のようになっています。

Everything is expanded.Everything is shortened.
  1
 
from django.apps import apps

django/apps

これまでにもappsは何回か見かけていましたがここで詳しく確認しましょう。まず、django.apps.appsは何ものかというと、

django/apps/__init__.py

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
from .config import AppConfig
from .registry import apps
 
__all__ = ['AppConfig', 'apps']

django/apps/registry.py

Everything is expanded.Everything is shortened.
  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

Everything is expanded.Everything is shortened.
  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メソッドは長いので要点だけ。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
-
!
 
 
 
 
 
 
-
!
 
 
            # 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に書かれている各アプリの読み込みを行っています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
-
!
 
 
            # 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は以下のように初期化されています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
-
|
|
|
|
|
|
!
        # 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メソッド、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
-
|
|
|
!
 
 
 
 
    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に戻ります。

Everything is expanded.Everything is shortened.
  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。

Everything is expanded.Everything is shortened.
  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に戻る。再掲します。

Everything is expanded.Everything is shortened.
  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メソッドで行われます。

まずは呼び出し部分を再確認します。

Everything is expanded.Everything is shortened.
  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メソッド。

Everything is expanded.Everything is shortened.
  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はちょっと長いので順に眺めていきます。

Everything is expanded.Everything is shortened.
  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インスタンス...]}

という辞書を返すことがわかります。

メソッド本体に入ります。

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
 22
 23
 24
 25
 26
 27
-
|
|
|
!
 
-
|
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
-
        # 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を生成していると思われます。

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
 22
 23
 24
-
!
 
-
!
 
 
-
!
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
        # 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にまとめて返しています。

Everything is expanded.Everything is shortened.
  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行以上あるので要点だけ、

Everything is expanded.Everything is shortened.
  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に書かれているのの逆順で処理」ということになります。

Everything is expanded.Everything is shortened.
  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
            # 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に記録されるはず。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
-
!
 
 
 
 
 
 
 
 
 
 
 
            # 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に記録されているフィールドは含まない、という点です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
-
!
 
-
!
-
!
 
 
 
 
 
 
 
 
            # 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

operationが収集できたら依存関係を確認して並び替えます。

Everything is expanded.Everything is shortened.
  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()):
            # 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は淡々と依存関係チェックしているだけなので省略。ともかくこのメソッドが実行されることにより、

  1. CreateModel('Choice')
  2. AddField('choice', 'question', ForeignKey)
  3. CreateModel('Question')

と並んでいたものが

  1. CreateModel('Choice')
  2. CreateModel('Question')
  3. AddField('choice', 'question', ForeignKey)

と依存関係を満たすように並び替えられます。

_build_migration_list

operationの収集、並び替えができたのでようやくMigrationにまとめる処理が行われます。こちらも長いので要点だけ

Everything is expanded.Everything is shortened.
  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が減算されると予想できます。

Everything is expanded.Everything is shortened.
  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関数を使っているので別のリストオブジェクトを使って繰り返しが行われているようです。

Everything is expanded.Everything is shortened.
  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になります。

Everything is expanded.Everything is shortened.
  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が返されることになります。

Everything is expanded.Everything is shortened.
  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:
            # 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

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に依存した処理をしないで差分の計算が行えるね、ということが発見できてよかったです。


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