Django/makemigrations時の処理を読む
をテンプレートにして作成
[
トップ
] [
新規
|
一覧
|
単語検索
|
最終更新
|
ヘルプ
]
開始行:
[[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=mode...
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 [#r9bd...
いつも通りにコマンド名と対応するファイルのCommandクラスか...
handleメソッド、まず初めにmigrateコマンドでも出てきたMigr...
#code(Python){{
# Load the current graph state. Pass in None for ...
# the loader doesn't try to resolve replaced migr...
loader = MigrationLoader(None, ignore_no_migratio...
}}
次に整合性チェックなどが行われていますが普通に使っていれ...
整合性のチェックが終わるとquestionerというマイグレーショ...
次に、MigrationAutodetectorオブジェクトが作成されます。名...
#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 s...
first would need doing to make it match the second (t...
usually being the project's current state).
Note that this naturally operates on entire projects ...
as it's likely that changes interact (for example, yo...
add a ForeignKey without having a migration to add th...
depends on first). A user interface may offer single-...
if it wishes, with the caveat that it may not always ...
"""
def __init__(self, from_state, to_state, questioner=N...
self.from_state = from_state
self.to_state = to_state
self.questioner = questioner or MigrationQuestion...
self.existing_apps = {app for app, model in from_...
}}
:from_state|loader.project_state()
:to_state|ProjectState.from_apps(apps)
ということになります。loaderから取得しているのは「今時点...
**MigrationLoader.project_state [#y7122d5a]
まずは「今時点であるマイグレーションファイル」を表したPro...
#code(Python){{
def project_state(self, nodes=None, at_end=True):
"""
Returns a ProjectState object representing the mo...
that the migrations we loaded represent.
See graph.make_state for the meaning of "nodes" a...
"""
return self.graph.make_state(nodes=nodes, at_end=...
}}
MigrationGraphに続く。(一部省略)
#code(Python){{
def make_state(self, nodes=None, at_end=True, real_ap...
"""
Given a migration node or nodes, returns a comple...
If at_end is False, returns the state before the ...
If nodes is not provided, returns the overall mos...
"""
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...
return project_state
}}
何をしているかというと、
+各アプリについて末端(最後)のマイグレーションを取得
+末端のマイグレーションに到達するまでの各マイグレーション...
+各マイグレーションを適用し、プロジェクトの状態を更新
ということをしています。
***Migration.mutate_state [#e3e74a40]
今回はまだマイグレーションファイルがないので、と端折るの...
#code(Python){{
def mutate_state(self, project_state, preserve=True):
"""
Takes a ProjectState and returns a new one with t...
operations applied to it. Preserves the original ...
default and will return a mutated state from a co...
"""
new_state = project_state
if preserve:
new_state = project_state.clone()
for operation in self.operations:
operation.state_forwards(self.app_label, new_...
return new_state
}}
operationsの例を確認するために、チュートリアルで作成され...
#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...
('choice_text', models.CharField(max_leng...
('votes', models.IntegerField(default=0)),
],
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.AutoField(auto_created=True...
('question_text', models.CharField(max_le...
('pub_date', models.DateTimeField(verbose...
],
),
migrations.AddField(
model_name='choice',
name='question',
field=models.ForeignKey(on_delete=django.db.m...
),
]
}}
CreateModelとかがどこにあるのかは少しややこしいのでちゃん...
django/db/migrations/__init__.py
#code(Python){{
from .migration import Migration, swappable_dependency #...
from .operations import * # NOQA
}}
djangp/db/migrations/operations/__init__.py
#code(Python){{
from .fields import AddField, AlterField, RemoveField, Re...
from .models import (
AlterIndexTogether, AlterModelManagers, AlterModelOpt...
AlterOrderWithRespectTo, AlterUniqueTogether, CreateM...
RenameModel,
)
from .special import RunPython, RunSQL, SeparateDatabaseA...
__all__ = [
'CreateModel', 'DeleteModel', 'AlterModelTable', 'Alt...
'RenameModel', 'AlterIndexTogether', 'AlterModelOptio...
'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メソッドが呼び出されています。Proj...
#code(Python){{
def add_model(self, model_state):
app_label, model_name = model_state.app_label, mo...
self.models[(app_label, model_name)] = model_state
if 'apps' in self.__dict__: # hasattr would cach...
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 matc...
app_models = {}
for model in apps.get_models(include_swapped=True):
model_state = ModelState.from_model(model)
app_models[(model_state.app_label, model_stat...
return cls(app_models)
}}
appsは何ものか。makemigrations.pyに戻って確認すると以下の...
#code(Python){{
from django.apps import apps
}}
***django/apps [#vd90b8d5]
これまでにも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...
It also keeps track of models eg. to provide reverse-...
"""
# 省略
apps = Apps(installed_apps=None)
}}
というわけで、appsはregistry.pyに記述されているAppsオブジ...
ファイルの最後で作成されているappsではinstalled_appsとし...
django/__init__.py
#code(Python){{
def setup(set_prefix=True):
"""
Configure the settings (this happens as a side effect...
first setting), configure logging and populate the ap...
Set the thread-local urlresolvers script prefix if `s...
"""
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.L...
if set_prefix:
set_script_prefix(
'/' if settings.FORCE_SCRIPT_NAME is None els...
)
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.lab...
self.app_configs[app_config.label] = app_...
}}
INSTALLED_APPSに書かれている各アプリの読み込みを行ってい...
#code(Python){{
# Load models.
for app_config in self.app_configs.values():
all_models = self.all_models[app_config.l...
app_config.import_models(all_models)
}}
モデルのインポートを行っています。なお、self.all_modelsは...
#code(Python){{
# Mapping of app labels => model names => model c...
# model is imported, ModelBase.__new__ calls apps...
# creates an entry in all_models. All imported mo...
# regardless of whether they're defined in an ins...
# and whether the registry has been populated. Si...
# to reimport a module safely (it could reexecute...
# 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 ma...
# 'all_models' attribute of the Apps this AppConf...
# Injected as a parameter because it gets populat...
# imported, which might happen before populate() ...
self.models = all_models
if module_has_submodule(self.module, MODELS_MODUL...
models_module_name = '%s.%s' % (self.name, MO...
self.models_module = import_module(models_mod...
}}
モデルがインポートされたのでAppsのget_modelsに戻ります。
#code(Python){{
def get_models(self, include_auto_created=False, incl...
result = []
for app_config in self.app_configs.values():
result.extend(list(app_config.get_models(incl...
return result
}}
再びAppConfigに移り、AppConfigのget_models。
#code(Python){{
def get_models(self, include_auto_created=False, incl...
"""
Returns an iterable of models.
省略
"""
for model in self.models.values():
if model._meta.auto_created and not include_a...
continue
if model._meta.swapped and not include_swapped:
continue
yield model
}}
returnではなくyieldになっているのはメソッドドキュメントに...
さて、と、_metaという単語が出てきました。そもそも、いつの...
ProjectStateのfrom_appsに戻る。再掲します。
#code(Python){{
@classmethod
def from_apps(cls, apps):
"Takes in an Apps and returns a ProjectState matc...
app_models = {}
for model in apps.get_models(include_swapped=True):
model_state = ModelState.from_model(model)
app_models[(model_state.app_label, model_stat...
return cls(app_models)
}}
from_modelsは100行近くあるのでコードを貼るのはやめますが...
-モデルのメタ情報として格納されているフィールドを取得
-オプション、スーパークラス、マネージャ(DBとの接続管理?...
-上記を使ってModelStateオブジェクトを作成
ということをしています。
*MigrationAutodetector.changes [#s8194a85]
さてと、2つのProjectStateがどのように取得されているかの確...
まずは呼び出し部分を再確認します。
#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_na...
さて、changesメソッド。
#code(Python){{
def changes(self, graph, trim_to_apps=None, convert_a...
"""
Main entry point to produce a list of applicable ...
Takes a graph to base names on and an optional se...
to try and restrict to (restriction is not guaran...
"""
changes = self._detect_changes(convert_apps, graph)
changes = self.arrange_for_graph(changes, graph, ...
if trim_to_apps:
changes = self._trim_to_apps(changes, trim_to...
return changes
}}
ドキュメントにあるようにこのメソッドはエントリーポイント...
_detect_changesはちょっと長いので順に眺めていきます。
#code(Python){{
def _detect_changes(self, convert_apps=None, graph=No...
"""
Returns a dict of migration plans which will achi...
change from from_state to to_state. The dict has ...
as keys and a list of migrations as values.
The resulting migrations aren't specially named, ...
do matter for dependencies inside the set.
convert_apps is the list of apps to convert to us...
(i.e. to make initial migrations for, in the usua...
graph is an optional argument that, if provided, ...
dependency generation and avoid potential circula...
"""
}}
まず先頭のメソッドドキュメント。一段落目を見ると、このメ...
{アプリ名: [Migrationインスタンス...]}
という辞書を返すことがわかります。
メソッド本体に入ります。
#code(Python){{
# The first phase is generating all the operation...
# and gathering them into a big per-app list.
# We'll then go through that list later and order...
# into migrations to resolve dependencies caused ...
self.generated_operations = {}
# Prepare some old/new state and model lists, sep...
# 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イン...
なお、from_state、to_stateからappsを取得していますがこれ...
続き。Stateの差分を取り、モデルの作成、フィールド追加など...
#code(Python){{
# Renames have to come first
self.generate_renamed_models()
# Prepare lists of fields and generate through mo...
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]
今回は初めてモデルを書いたという前提で読み進めているので...
#code(Python){{
old_keys = set(self.old_model_keys).union(self.ol...
added_models = set(self.new_model_keys) - old_keys
added_unmanaged_models = set(self.new_unmanaged_k...
all_added_models = chain(
sorted(added_models, key=self.swappable_first...
sorted(added_unmanaged_models, key=self.swapp...
)
}}
新しいモデル(models.pyに書かれているモデル)と古いモデル...
#code(Python){{
for app_label, model_name in all_added_models:
model_state = self.to_state.models[app_label,...
model_opts = self.new_apps.get_model(app_labe...
# 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.remot...
elif not field.remote_field.paren...
related_fields[field.name] = ...
}}
モデルのフィールドを走査し、remote_field(他のモデルへの...
#code(Python){{
# Generate creation operation
self.add_operation(
app_label,
operations.CreateModel(
name=model_state.name,
fields=[d for d in model_state.fields...
options=model_state.options,
bases=model_state.bases,
managers=model_state.managers,
),
dependencies=dependencies,
beginning=True,
)
}}
その後、依存関係の処理などが行われたうえでCreateModelオブ...
#code(Python){{
# Generate operations for each related field
for name, field in sorted(related_fields.item...
dependencies = self._get_dependecies_for_...
# Depend on our own model being created
dependencies.append((app_label, model_nam...
# 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 hav...
but we need to pull a few things around so FKs wo...
same app
"""
for app_label, ops in sorted(self.generated_opera...
# construct a dependency graph for intra-app ...
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,...
dependency_graph[op].add(...
# we use a stable sort for deterministic test...
self.generated_operations[app_label] = stable...
}}
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_oper...
chop_mode = False
while num_ops:
}}
operationの数を取得し、whileで回しています。条件になって...
#code(Python){{
for app_label in sorted(self.generated_operat...
chopped = []
dependencies = set()
for operation in list(self.generated_oper...
deps_satisfied = True
operation_dependencies = set()
# 依存関係の処理。省略
if deps_satisfied:
chopped.append(operation)
dependencies.update(operation_dep...
self.generated_operations[app_lab...
else:
break
}}
アプリごと、operationごとに処理を行っています。依存関係の...
一瞬、ループ内でループ対象を更新して大丈夫?と思いました...
#code(Python){{
if dependencies or chopped:
if not self.generated_operations[app_...
subclass = type(str("Migration"),...
instance = subclass("auto_%i" % (...
instance.dependencies = list(depe...
instance.operations = chopped
instance.initial = app_label not ...
self.migrations.setdefault(app_la...
chop_mode = False
}}
一つ目のifはchoppedが空じゃないから真、二つ目のifはgenera...
条件が満たされればついにMigrationインスタンスの作成です。
まず[[type関数>https://docs.python.jp/3/library/functions...
その後、インスタンスを作成し、インスタンス変数を設定して...
#code(Python){{
new_num_ops = sum(len(x) for x in self.genera...
if new_num_ops == num_ops:
if not chop_mode:
chop_mode = True
else:
raise ValueError("Cannot resolve oper...
num_ops = new_num_ops
}}
num_opsの更新。この後、whileに戻って繰り返しが行われます...
_detect_changesメソッドでは後、_optimize_migrationsメソッ...
**changesメソッドの残り部分 [#pd68a27f]
changesメソッドに戻ってきてarrange_for_graphが呼び出され...
はいいのですが、初回かの確認にquestionerのask_initialメソ...
#code(Python){{
migrations_import_path = MigrationLoader.migratio...
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(m...
except ImportError:
return self.defaults.get("ask_initial", False)
else:
if hasattr(migrations_module, "__file__"):
filenames = os.listdir(os.path.dirname(mi...
elif hasattr(migrations_module, "__path__"):
if len(migrations_module.__path__) > 1:
return False
filenames = os.listdir(list(migrations_mo...
return not any(x.endswith(".py") for x in fil...
}}
その後に呼び出されている_trim_to_appsメソッドでは依存関係...
以上で差分の計算が完了しました。
*MigrationAutodetector.write_migration_files [#ab117f32]
Migrationオブジェクトが作成できたので最後に書き込みです。...
Migrationオブジェクトに対してそれをファイルの形にするのに...
operationの書き込みにはさらに下請けとしてOperationWriter...
__init__メソッドの引数については、serializerモジュールで...
ということが淡々と行われています。コードは貼っていると長...
*おわりに [#f1143ae8]
今回はmakemigrations時に行われる処理について見てきました。
長い!ひたすら長かったです。今まで読解に直接必要ないから...
また、Field, Operation, Serializerなど本気のオブジェクト...
おさらいしましょう。
-マイグレーションファイルからfrom_stateを構築する(現在あ...
-モデルからto_stateを構築する
--django.apps.appsが用いられる。appsはコマンドの共通処理...
-from_stateとto_stateを比較し、差分を埋めるoperationリス...
-operationリストをまとめてMigrationオブジェクトを作り、Mi...
このうち、「モデルをインポートすると自動的に登録が行われ...
実はDjangoを使っていて一番謎だった部分はここでした。どう...
終了行:
[[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=mode...
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 [#r9bd...
いつも通りにコマンド名と対応するファイルのCommandクラスか...
handleメソッド、まず初めにmigrateコマンドでも出てきたMigr...
#code(Python){{
# Load the current graph state. Pass in None for ...
# the loader doesn't try to resolve replaced migr...
loader = MigrationLoader(None, ignore_no_migratio...
}}
次に整合性チェックなどが行われていますが普通に使っていれ...
整合性のチェックが終わるとquestionerというマイグレーショ...
次に、MigrationAutodetectorオブジェクトが作成されます。名...
#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 s...
first would need doing to make it match the second (t...
usually being the project's current state).
Note that this naturally operates on entire projects ...
as it's likely that changes interact (for example, yo...
add a ForeignKey without having a migration to add th...
depends on first). A user interface may offer single-...
if it wishes, with the caveat that it may not always ...
"""
def __init__(self, from_state, to_state, questioner=N...
self.from_state = from_state
self.to_state = to_state
self.questioner = questioner or MigrationQuestion...
self.existing_apps = {app for app, model in from_...
}}
:from_state|loader.project_state()
:to_state|ProjectState.from_apps(apps)
ということになります。loaderから取得しているのは「今時点...
**MigrationLoader.project_state [#y7122d5a]
まずは「今時点であるマイグレーションファイル」を表したPro...
#code(Python){{
def project_state(self, nodes=None, at_end=True):
"""
Returns a ProjectState object representing the mo...
that the migrations we loaded represent.
See graph.make_state for the meaning of "nodes" a...
"""
return self.graph.make_state(nodes=nodes, at_end=...
}}
MigrationGraphに続く。(一部省略)
#code(Python){{
def make_state(self, nodes=None, at_end=True, real_ap...
"""
Given a migration node or nodes, returns a comple...
If at_end is False, returns the state before the ...
If nodes is not provided, returns the overall mos...
"""
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...
return project_state
}}
何をしているかというと、
+各アプリについて末端(最後)のマイグレーションを取得
+末端のマイグレーションに到達するまでの各マイグレーション...
+各マイグレーションを適用し、プロジェクトの状態を更新
ということをしています。
***Migration.mutate_state [#e3e74a40]
今回はまだマイグレーションファイルがないので、と端折るの...
#code(Python){{
def mutate_state(self, project_state, preserve=True):
"""
Takes a ProjectState and returns a new one with t...
operations applied to it. Preserves the original ...
default and will return a mutated state from a co...
"""
new_state = project_state
if preserve:
new_state = project_state.clone()
for operation in self.operations:
operation.state_forwards(self.app_label, new_...
return new_state
}}
operationsの例を確認するために、チュートリアルで作成され...
#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...
('choice_text', models.CharField(max_leng...
('votes', models.IntegerField(default=0)),
],
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.AutoField(auto_created=True...
('question_text', models.CharField(max_le...
('pub_date', models.DateTimeField(verbose...
],
),
migrations.AddField(
model_name='choice',
name='question',
field=models.ForeignKey(on_delete=django.db.m...
),
]
}}
CreateModelとかがどこにあるのかは少しややこしいのでちゃん...
django/db/migrations/__init__.py
#code(Python){{
from .migration import Migration, swappable_dependency #...
from .operations import * # NOQA
}}
djangp/db/migrations/operations/__init__.py
#code(Python){{
from .fields import AddField, AlterField, RemoveField, Re...
from .models import (
AlterIndexTogether, AlterModelManagers, AlterModelOpt...
AlterOrderWithRespectTo, AlterUniqueTogether, CreateM...
RenameModel,
)
from .special import RunPython, RunSQL, SeparateDatabaseA...
__all__ = [
'CreateModel', 'DeleteModel', 'AlterModelTable', 'Alt...
'RenameModel', 'AlterIndexTogether', 'AlterModelOptio...
'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メソッドが呼び出されています。Proj...
#code(Python){{
def add_model(self, model_state):
app_label, model_name = model_state.app_label, mo...
self.models[(app_label, model_name)] = model_state
if 'apps' in self.__dict__: # hasattr would cach...
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 matc...
app_models = {}
for model in apps.get_models(include_swapped=True):
model_state = ModelState.from_model(model)
app_models[(model_state.app_label, model_stat...
return cls(app_models)
}}
appsは何ものか。makemigrations.pyに戻って確認すると以下の...
#code(Python){{
from django.apps import apps
}}
***django/apps [#vd90b8d5]
これまでにも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...
It also keeps track of models eg. to provide reverse-...
"""
# 省略
apps = Apps(installed_apps=None)
}}
というわけで、appsはregistry.pyに記述されているAppsオブジ...
ファイルの最後で作成されているappsではinstalled_appsとし...
django/__init__.py
#code(Python){{
def setup(set_prefix=True):
"""
Configure the settings (this happens as a side effect...
first setting), configure logging and populate the ap...
Set the thread-local urlresolvers script prefix if `s...
"""
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.L...
if set_prefix:
set_script_prefix(
'/' if settings.FORCE_SCRIPT_NAME is None els...
)
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.lab...
self.app_configs[app_config.label] = app_...
}}
INSTALLED_APPSに書かれている各アプリの読み込みを行ってい...
#code(Python){{
# Load models.
for app_config in self.app_configs.values():
all_models = self.all_models[app_config.l...
app_config.import_models(all_models)
}}
モデルのインポートを行っています。なお、self.all_modelsは...
#code(Python){{
# Mapping of app labels => model names => model c...
# model is imported, ModelBase.__new__ calls apps...
# creates an entry in all_models. All imported mo...
# regardless of whether they're defined in an ins...
# and whether the registry has been populated. Si...
# to reimport a module safely (it could reexecute...
# 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 ma...
# 'all_models' attribute of the Apps this AppConf...
# Injected as a parameter because it gets populat...
# imported, which might happen before populate() ...
self.models = all_models
if module_has_submodule(self.module, MODELS_MODUL...
models_module_name = '%s.%s' % (self.name, MO...
self.models_module = import_module(models_mod...
}}
モデルがインポートされたのでAppsのget_modelsに戻ります。
#code(Python){{
def get_models(self, include_auto_created=False, incl...
result = []
for app_config in self.app_configs.values():
result.extend(list(app_config.get_models(incl...
return result
}}
再びAppConfigに移り、AppConfigのget_models。
#code(Python){{
def get_models(self, include_auto_created=False, incl...
"""
Returns an iterable of models.
省略
"""
for model in self.models.values():
if model._meta.auto_created and not include_a...
continue
if model._meta.swapped and not include_swapped:
continue
yield model
}}
returnではなくyieldになっているのはメソッドドキュメントに...
さて、と、_metaという単語が出てきました。そもそも、いつの...
ProjectStateのfrom_appsに戻る。再掲します。
#code(Python){{
@classmethod
def from_apps(cls, apps):
"Takes in an Apps and returns a ProjectState matc...
app_models = {}
for model in apps.get_models(include_swapped=True):
model_state = ModelState.from_model(model)
app_models[(model_state.app_label, model_stat...
return cls(app_models)
}}
from_modelsは100行近くあるのでコードを貼るのはやめますが...
-モデルのメタ情報として格納されているフィールドを取得
-オプション、スーパークラス、マネージャ(DBとの接続管理?...
-上記を使ってModelStateオブジェクトを作成
ということをしています。
*MigrationAutodetector.changes [#s8194a85]
さてと、2つのProjectStateがどのように取得されているかの確...
まずは呼び出し部分を再確認します。
#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_na...
さて、changesメソッド。
#code(Python){{
def changes(self, graph, trim_to_apps=None, convert_a...
"""
Main entry point to produce a list of applicable ...
Takes a graph to base names on and an optional se...
to try and restrict to (restriction is not guaran...
"""
changes = self._detect_changes(convert_apps, graph)
changes = self.arrange_for_graph(changes, graph, ...
if trim_to_apps:
changes = self._trim_to_apps(changes, trim_to...
return changes
}}
ドキュメントにあるようにこのメソッドはエントリーポイント...
_detect_changesはちょっと長いので順に眺めていきます。
#code(Python){{
def _detect_changes(self, convert_apps=None, graph=No...
"""
Returns a dict of migration plans which will achi...
change from from_state to to_state. The dict has ...
as keys and a list of migrations as values.
The resulting migrations aren't specially named, ...
do matter for dependencies inside the set.
convert_apps is the list of apps to convert to us...
(i.e. to make initial migrations for, in the usua...
graph is an optional argument that, if provided, ...
dependency generation and avoid potential circula...
"""
}}
まず先頭のメソッドドキュメント。一段落目を見ると、このメ...
{アプリ名: [Migrationインスタンス...]}
という辞書を返すことがわかります。
メソッド本体に入ります。
#code(Python){{
# The first phase is generating all the operation...
# and gathering them into a big per-app list.
# We'll then go through that list later and order...
# into migrations to resolve dependencies caused ...
self.generated_operations = {}
# Prepare some old/new state and model lists, sep...
# 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イン...
なお、from_state、to_stateからappsを取得していますがこれ...
続き。Stateの差分を取り、モデルの作成、フィールド追加など...
#code(Python){{
# Renames have to come first
self.generate_renamed_models()
# Prepare lists of fields and generate through mo...
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]
今回は初めてモデルを書いたという前提で読み進めているので...
#code(Python){{
old_keys = set(self.old_model_keys).union(self.ol...
added_models = set(self.new_model_keys) - old_keys
added_unmanaged_models = set(self.new_unmanaged_k...
all_added_models = chain(
sorted(added_models, key=self.swappable_first...
sorted(added_unmanaged_models, key=self.swapp...
)
}}
新しいモデル(models.pyに書かれているモデル)と古いモデル...
#code(Python){{
for app_label, model_name in all_added_models:
model_state = self.to_state.models[app_label,...
model_opts = self.new_apps.get_model(app_labe...
# 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.remot...
elif not field.remote_field.paren...
related_fields[field.name] = ...
}}
モデルのフィールドを走査し、remote_field(他のモデルへの...
#code(Python){{
# Generate creation operation
self.add_operation(
app_label,
operations.CreateModel(
name=model_state.name,
fields=[d for d in model_state.fields...
options=model_state.options,
bases=model_state.bases,
managers=model_state.managers,
),
dependencies=dependencies,
beginning=True,
)
}}
その後、依存関係の処理などが行われたうえでCreateModelオブ...
#code(Python){{
# Generate operations for each related field
for name, field in sorted(related_fields.item...
dependencies = self._get_dependecies_for_...
# Depend on our own model being created
dependencies.append((app_label, model_nam...
# 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 hav...
but we need to pull a few things around so FKs wo...
same app
"""
for app_label, ops in sorted(self.generated_opera...
# construct a dependency graph for intra-app ...
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,...
dependency_graph[op].add(...
# we use a stable sort for deterministic test...
self.generated_operations[app_label] = stable...
}}
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_oper...
chop_mode = False
while num_ops:
}}
operationの数を取得し、whileで回しています。条件になって...
#code(Python){{
for app_label in sorted(self.generated_operat...
chopped = []
dependencies = set()
for operation in list(self.generated_oper...
deps_satisfied = True
operation_dependencies = set()
# 依存関係の処理。省略
if deps_satisfied:
chopped.append(operation)
dependencies.update(operation_dep...
self.generated_operations[app_lab...
else:
break
}}
アプリごと、operationごとに処理を行っています。依存関係の...
一瞬、ループ内でループ対象を更新して大丈夫?と思いました...
#code(Python){{
if dependencies or chopped:
if not self.generated_operations[app_...
subclass = type(str("Migration"),...
instance = subclass("auto_%i" % (...
instance.dependencies = list(depe...
instance.operations = chopped
instance.initial = app_label not ...
self.migrations.setdefault(app_la...
chop_mode = False
}}
一つ目のifはchoppedが空じゃないから真、二つ目のifはgenera...
条件が満たされればついにMigrationインスタンスの作成です。
まず[[type関数>https://docs.python.jp/3/library/functions...
その後、インスタンスを作成し、インスタンス変数を設定して...
#code(Python){{
new_num_ops = sum(len(x) for x in self.genera...
if new_num_ops == num_ops:
if not chop_mode:
chop_mode = True
else:
raise ValueError("Cannot resolve oper...
num_ops = new_num_ops
}}
num_opsの更新。この後、whileに戻って繰り返しが行われます...
_detect_changesメソッドでは後、_optimize_migrationsメソッ...
**changesメソッドの残り部分 [#pd68a27f]
changesメソッドに戻ってきてarrange_for_graphが呼び出され...
はいいのですが、初回かの確認にquestionerのask_initialメソ...
#code(Python){{
migrations_import_path = MigrationLoader.migratio...
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(m...
except ImportError:
return self.defaults.get("ask_initial", False)
else:
if hasattr(migrations_module, "__file__"):
filenames = os.listdir(os.path.dirname(mi...
elif hasattr(migrations_module, "__path__"):
if len(migrations_module.__path__) > 1:
return False
filenames = os.listdir(list(migrations_mo...
return not any(x.endswith(".py") for x in fil...
}}
その後に呼び出されている_trim_to_appsメソッドでは依存関係...
以上で差分の計算が完了しました。
*MigrationAutodetector.write_migration_files [#ab117f32]
Migrationオブジェクトが作成できたので最後に書き込みです。...
Migrationオブジェクトに対してそれをファイルの形にするのに...
operationの書き込みにはさらに下請けとしてOperationWriter...
__init__メソッドの引数については、serializerモジュールで...
ということが淡々と行われています。コードは貼っていると長...
*おわりに [#f1143ae8]
今回はmakemigrations時に行われる処理について見てきました。
長い!ひたすら長かったです。今まで読解に直接必要ないから...
また、Field, Operation, Serializerなど本気のオブジェクト...
おさらいしましょう。
-マイグレーションファイルからfrom_stateを構築する(現在あ...
-モデルからto_stateを構築する
--django.apps.appsが用いられる。appsはコマンドの共通処理...
-from_stateとto_stateを比較し、差分を埋めるoperationリス...
-operationリストをまとめてMigrationオブジェクトを作り、Mi...
このうち、「モデルをインポートすると自動的に登録が行われ...
実はDjangoを使っていて一番謎だった部分はここでした。どう...
ページ名: