Djangoを読む
はじめに †
やっとチュートリアルがその2になりました。しばらく読んでない間に1.11が出てしまったのですが(汗)、読解は引き続き1.10を使って進めます。
さて、チュートリアルその2の話題はモデルです。モデルの定義、モデルに対応するデータベーステーブルの作成、モデルの操作などが読解対象になります。
まずは、チュートリアルで初めに
$ python manage.py migrate
と打てと書いてあるのでmigrateコマンドの中身を見てDjangoがデータベースにどのようにテーブルを作っていくか見ていきましょう。(ここではまだこれから作成していくアプリのモデルはなく、プロジェクト作成時にデフォルトで設定されているDjango付属のアプリが使うモデルについて処理が行われます)
ちなみに、打ってみると以下のように出力されます。
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying sessions.0001_initial... OK
django/core/management/commands/migrate.py †
スタート地点はいつものようにCommandクラスが書かれているコマンド名と対応したファイルです。
migrateコマンドのhandleは結構長いです。ざっと見た感じでは以下の処理を行っています。
- データベースへの接続
- 実行するマイグレーションの決定、順序付け
- マイグレーションの実行
ひとつずつ見ていきましょう。
django/db †
データベースの接続を行っていると思われる個所は以下のところです。
1
2
3
4
5
6
| -
!
-
!
| db = options['database']
connection = connections[db]
connection.prepare_database()
|
connectionsとローカル変数の辞書のようにしれっと書いてありますがこの実体は、
1
|
| from django.db import DEFAULT_DB_ALIAS, connections, router, transaction
|
と、django.dbモジュールの属性です。
というわけで、視点をdjango/db/__init__.pyに向けると、
1
2
3
4
5
6
7
8
|
| from django.db.utils import (
DEFAULT_DB_ALIAS, DJANGO_VERSION_PICKLE_KEY, ConnectionHandler,
ConnectionRouter, DatabaseError, DataError, Error, IntegrityError,
InterfaceError, InternalError, NotSupportedError, OperationalError,
ProgrammingError,
)
connections = ConnectionHandler()
|
utils.pyに移ってConnectionHandlerクラスの__getitem__メソッド
1
2
3
4
5
6
7
8
9
10
11
|
| def __getitem__(self, alias):
if hasattr(self._connections, alias):
return getattr(self._connections, alias)
self.ensure_defaults(alias)
self.prepare_test_settings(alias)
db = self.databases[alias]
backend = load_backend(db['ENGINE'])
conn = backend.DatabaseWrapper(db, alias)
setattr(self._connections, alias, conn)
return conn
|
load_backend関数を見るとENGINEとして設定したもののbase、デフォルトのままのsettings.pyならdjango/db/backends/sqlite3/base.pyが読み込まれて返されることがわかります。
なお、self.databasesはプロパティでsettings.pyのDATABASESが設定されています。まあこれについてはもう詳しい説明は必要ないでしょう。
で、prepare_databaseで接続を行っているのかと思ったらしてないみたいですね。migrate.pyに戻って先に進むことにしましょう。
django/db/migrations/loader.py †
次は、実行するマイグレーションの決定、です。関係ありそうなところだけ抜き出すと以下のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| -
!
-
!
-
!
-
!
-
!
| executor = MigrationExecutor(connection, self.migration_progress_callback)
target_app_labels_only = True
if options['app_label'] and options['migration_name']:
elif options['app_label']:
else:
targets = executor.loader.graph.leaf_nodes()
plan = executor.migration_plan(targets)
|
キーになるのはMigrationExecutorのようです。上にあるimportを見ると、MigrationExecutorクラスはdjango.db.migrations.executorモジュールにあることがわかります。
また、executorの属性としてloaderとありますが、これはexecutorモジュールと同じ階層にあるloaderモジュールで定義されているMigrationLoaderクラスのインスタンスです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
-
|
|
!
| class MigrationLoader(object):
"""
Loads migration files from disk, and their status from the database.
Migration files are expected to live in the "migrations" directory of
an app. Their names are entirely unimportant from a code perspective,
but will probably follow the 1234_name.py convention.
On initialization, this class will scan those directories, and open and
read the python files, looking for a class called Migration, which should
inherit from django.db.migrations.Migration. See
django.db.migrations.migration for what that looks like.
以下略
"""
def __init__(self, connection, load=True, ignore_no_migrations=False):
self.connection = connection
self.disk_migrations = None
self.applied_migrations = None
self.ignore_no_migrations = ignore_no_migrations
if load:
self.build_graph()
|
クラスコメントを読むとこのクラスが何をしているのかがわかります。つまり、アプリのmigrationsディレクトリにあるファイルの読み込みと、データベースを読んでマイグレーションの状況を管理しているようです。build_graphとあるので、これらは初期化時に行われているようです。
build_graphメソッドの先頭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
-
|
|
|
!
-
!
-
!
| def build_graph(self):
"""
Builds a migration dependency graph using both the disk and database.
You'll need to rebuild the graph if you apply migrations. This isn't
usually a problem as generally migration stuff runs in a one-shot process.
"""
self.load_disk()
if self.connection is None:
self.applied_migrations = set()
else:
recorder = MigrationRecorder(self.connection)
self.applied_migrations = recorder.applied_migrations()
|
load_diskメソッドでは先ほどクラスコメントにあったように、アプリのmigrationsディレクトリにある各マイグレーション設定ファイルを読み込んでいます。淡々と読んでいるだけなのでコードの説明は省略します。
load_diskメソッドの実行が終わると、
{(アプリ名, マイグレーション名): Migrationインスタンス}
というような辞書オブジェクトdisk_migrationsが構築されます。
load_diskメソッドから返ってくると次はMigrationRecorderクラスを使ってデータベースから適用済みのマイグレーションを取得しています。初回なので実際には空になります。詳細はまた後で、ということになりますが、マイグレーション情報自体もDjangoのモデルを利用して実装が行われています。
さて、これでアプリのマイグレーション情報および、それがどこまで適用されているかがわかりました(繰り返しますが、初回なので実際には何も適用されていません)。次はマイグレーションの依存関係を調べて実行準を示すグラフの構築を行います。replacesが設定されているものはなさそうなのでさくっと省略します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| -
|
!
-
!
-
!
-
!
-
!
-
| self.graph = MigrationGraph()
self.replacements = {}
for key, migration in self.disk_migrations.items():
self.graph.add_node(key, migration)
self.add_internal_dependencies(key, migration)
if migration.replaces:
self.replacements[key] = migration
for key, migration in self.disk_migrations.items():
self.add_external_dependencies(key, migration)
for key, migration in self.replacements.items():
|
各Migrationにはdependenciesで依存(自分よりも前に実行しておくべきマイグレーション)が書かれています。例えば、authモジュールの0002だと以下のような感じ。
1
2
3
4
5
|
| class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
]
|
グラフの構築ステップとしては、
- 各マイグレーションをノードとして追加
- インターナル(自アプリ内)の依存を追加。これは通常、0008→0007→0006のように前の番号のマイグレーションに依存するということになります
- エクスターナル(別アプリ)の依存を追加。これを別のループで行っているのは依存対象のマイグレーション(ノード)がグラフ内に存在することを保証するためですね。ちなみに、authの0001はcontenttypesモジュールの初めのマイグレーションに依存してるようです
この後、構築したグラフに誤りがないかのバリデーションがされていますがまあそれは置いといて、ちょっと長くなりましたがロード処理は終わりです。
migrate.pyに戻って、今見てるところ再掲、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| -
!
-
!
-
!
-
!
-
!
| executor = MigrationExecutor(connection, self.migration_progress_callback)
target_app_labels_only = True
if options['app_label'] and options['migration_name']:
elif options['app_label']:
else:
targets = executor.loader.graph.leaf_nodes()
plan = executor.migration_plan(targets)
|
graph.leaf_nodesはなんとなく想像がつくので省略します。
で、migration_planの方。これも全部載せていると長くなるので必要なところだけ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
-
|
!
-
!
-
|
|
!
-
!
| def migration_plan(self, targets, clean_start=False):
"""
Given a set of targets, returns a list of (Migration instance, backwards?).
"""
plan = []
if clean_start:
applied = set()
else:
applied = set(self.loader.applied_migrations)
for target in targets:
if target[1] is None:
elif target in applied:
else:
for migration in self.loader.graph.forwards_plan(target):
if migration not in applied:
plan.append((self.loader.graph.nodes[migration], False))
applied.add(migration)
return plan
|
forwards_planの中まで追いかけるのはやめますが、コメントなどに書いてあるように、target(ノードの末端、通常、アプリの最後のマイグレーション)を適用するために必要なマイグレーション群(依存マイグレーション)をリストアップし、それを実行するマイグレーション一覧に記録しています。
結局、planには
(Migrationインスタンス, False)
というタプルのリストが格納されることになります。Falseというのは、backward(マイグレーションの巻き戻し)がFalse(つまり、マイグレーションを進める)という意味です。
django/db/migrations/executor.py †
実行するマイグレーションとその順序まで決定できたので残りは実行です。
migrate.pyに戻ってhandleメソッドの続きを見ると、重要そうなのは以下の部分です。
1
2
3
4
5
6
|
-
!
| pre_migrate_state = executor._create_project_state(with_applied_migrations=True)
post_migrate_state = executor.migrate(
targets, plan=plan, state=pre_migrate_state.clone(), fake=fake,
fake_initial=fake_initial,
)
|
executorのmigrateメソッド。いろいろ分岐していますが結局、普通にmigrateする場合は_migrate_all_forwardsが呼ばれます。
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
|
-
|
|
|
|
!
-
!
-
!
-
!
-
!
-
|
!
| def migrate(self, targets, plan=None, state=None, fake=False, fake_initial=False):
"""
Migrates the database up to the given targets.
Django first needs to create all project states before a migration is
(un)applied and in a second step run all the database operations.
"""
if plan is None:
plan = self.migration_plan(targets)
full_plan = self.migration_plan(self.loader.graph.leaf_nodes(), clean_start=True)
all_forwards = all(not backwards for mig, backwards in plan)
all_backwards = all(backwards for mig, backwards in plan)
if not plan:
elif all_forwards == all_backwards:
elif all_forwards:
if state is None:
state = self._create_project_state(with_applied_migrations=True)
state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial)
else:
state = self._migrate_all_backwards(plan, full_plan, fake=fake)
self.check_replacements()
return state
|
_migrate_all_forwardsメソッド。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
-
|
|
!
-
|
|
|
!
| def _migrate_all_forwards(self, state, plan, full_plan, fake, fake_initial):
"""
Take a list of 2-tuples of the form (migration instance, False) and
apply them in the order they occur in the full_plan.
"""
migrations_to_run = {m[0] for m in plan}
for migration, _ in full_plan:
if not migrations_to_run:
break
if migration in migrations_to_run:
if 'apps' not in state.__dict__:
if self.progress_callback:
self.progress_callback("render_start")
state.apps if self.progress_callback:
self.progress_callback("render_success")
state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
migrations_to_run.remove(migration)
return state
|
各マイグレーションについてapply_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
|
-
|
!
-
!
-
!
-
!
-
!
| def apply_migration(self, state, migration, fake=False, fake_initial=False):
"""
Runs a migration forwards.
"""
if self.progress_callback:
self.progress_callback("apply_start", migration, fake)
if not fake:
if fake_initial:
applied, state = self.detect_soft_applied(state, migration)
if applied:
fake = True
if not fake:
with self.connection.schema_editor(atomic=migration.atomic) as schema_editor:
state = migration.apply(state, schema_editor)
if migration.replaces:
for app_label, name in migration.replaces:
self.recorder.record_applied(app_label, name)
else:
self.recorder.record_applied(migration.app_label, migration.name)
if self.progress_callback:
self.progress_callback("apply_success", migration, fake)
return state
|
マイグレーションの肝はここ。
1
2
|
| with self.connection.schema_editor(atomic=migration.atomic) as schema_editor:
state = migration.apply(state, schema_editor)
|
ですが、今回はmigrateコマンドで各マイグレーションが実行される流れの確認までで止めておいて、具体的に各マイグレーションが適用されデータベースにテーブルが作られる様については改めて自分のアプリのマイグレーションを行う際に見ていきたいと思います。
おわりに †
今回はマイグレーションの流れについて見てきました。各アプリのmigrationsフォルダにあるマイグレーション定義を集めてきて、依存関係を解決して、ひとつずつ実行していくという当たり前と言えば当たり前の処理です。ほーと思ったのはそれらを一つのクラスで行わず、executor, loader, recorder, graphと役割分担ができている点です。(ただ、executorとloader両方でrecorderインスタンス持ってるのは微妙に思いましたが)
そういえばデータベースへの接続、recorderはデータベースに接続するはずだから・・・、うーんここか。自アプリのマイグレーションの時に説明します。明示的に接続するのではなくて必要になった時に接続するというよくあるやり方ですね。