Upgrade to Pro — share decks privately, control downloads, hide ads and more …

いまさら振り返る Django Migration

Denzow
May 19, 2018

いまさら振り返る Django Migration

#djangocongress 2018 での発表資料です。

Djangoのmigrateやmakemigrationsのソースコードを簡単に眺めながら、もう少し実践的なMigrationを作成する際の話です。

Denzow

May 19, 2018
Tweet

More Decks by Denzow

Other Decks in Technology

Transcript

  1. 2 ➡ でんぞう (@denzowill, denzow) ➡ Python歴 5年 (仕事で半年) ➡

    scouty,inc シニアエンジニア(前職はDBサポートエンジニア) ➡ DBスペシャリスト(PostgreSQLが好き、•racleは得意だけど苦⼿) ➡ StartPythonClubスタッフ お前誰よ?
  2. Amazon
 DynamoDB Amazon ECS Amazon ECS Amazon ECS Amazon ECS

    Amazon
 SQS Elastic Load Balancing* AWS Lambda Amazon CloudWatch Amazon
 RDS Aurora
 (MySQL 5.7) Amazon 
 ElastiCache sns-activity
 watcher worker ϝΠϯαʔϏε Ϋϩʔϧͨ͠
 ੜσʔλͷdiff ੔ܗ͞Εͨσʔλ event 
 (time-based) ᶃ ᶄ ᶆ ᶇ ᶅ ᶈ ᶈ ᶈ ᶉ ᶉ ᶉ インフラ構成図 crawler
  3. Amazon
 DynamoDB Amazon ECS Amazon ECS Amazon ECS Amazon ECS

    Amazon
 SQS Elastic Load Balancing* AWS Lambda Amazon CloudWatch Amazon
 RDS Aurora
 (MySQL 5.7) Amazon 
 ElastiCache sns-activity
 watcher worker crawler ϝΠϯαʔϏε Ϋϩʔϧͨ͠
 ੜσʔλͷdiff ੔ܗ͞Εͨσʔλ event 
 (time-based) ᶃ ᶄ ᶆ ᶇ ᶅ ᶈ ᶈ ᶈ ᶉ ᶉ ᶉ インフラ構成図 (Python)
  4. Amazon
 DynamoDB Amazon ECS Amazon ECS Amazon ECS Amazon ECS

    Amazon
 SQS Elastic Load Balancing* AWS Lambda Amazon CloudWatch Amazon
 RDS Aurora
 (MySQL 5.7) Amazon 
 ElastiCache sns-activity
 watcher worker crawler ϝΠϯαʔϏε Ϋϩʔϧͨ͠
 ੜσʔλͷdiff ੔ܗ͞Εͨσʔλ event 
 (time-based) ᶃ ᶄ ᶆ ᶇ ᶅ ᶈ ᶈ ᶈ ᶉ ᶉ ᶉ インフラ構成図 (フレームワーク)
  5. 14

  6. 15 Django と scouty ➡ Django 1.11 ➡ Django Celery

    ➡ Django でDDD的な設計 ➡ Django Channels でのWebsocket(予定) ➡ daphneでの本番運⽤
  7. 17 Book. authorを追加した時の動きを追う from django.db import models class Book(models.Model): name

    = models.CharField(max_length=200) author = models.ForeignKey('Author', on_delete=models.CASCADE, default=None, null=True) class Author(models.Model): name = models.CharField(max_length=200)
  8. 21 Makemigrationsの⼤まかな流れ 1. app_labelsの指定があればそれの妥当性チェック 2. 既存のマイグレーションファイルからProjectStateを構成 3. MigrationLoader.check_consistent_historyで⼀貫性チェック 4. リーフが集約しているかのチェック(detect_conflicts)

    5. MigrationAutodetector で現在のAppのStateとの差分取得 6. write_migration_filesでマイグレーションファイルの作成 7. MigrationWriterを通してマイグレーションファイルを書き出し
  9. 22 1. app_labelsの指定があればそれの妥当性チェック from django.apps import apps : # @@

    makemigrationsに指定されたappがある場合はそれが存在するかのチェック app_labels = set(app_labels) bad_app_labels = set() for app_label in app_labels: try: apps.get_app_config(app_label) except LookupError: bad_app_labels.add(app_label) if bad_app_labels: for app_label in bad_app_labels: self.stderr.write("App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label) sys.exit(2)
  10. 23 2. MigrationLoader で既存のマイグレーションファイルからProjectStateを構成 # @@ マイグレーションファイルからステートを構成する # connection=NoneにしているのでDBは⾒ない #

    コンストラクタの中でbuild_graphが呼ばれる # ローカルのマイグレーションファイルを読み込んでグラフを構成する loader = MigrationLoader(None, ignore_no_migrations=True) # django.db.migrations.loader.MigrationLoader#build_graph def build_graph(self): : # Load disk data self.load_disk() # -> ローカルのマイグレーションファイルを読み込んでいる # Load database data if self.connection is None: self.applied_migrations = set() else: recorder = MigrationRecorder(self.connection) self.applied_migrations = recorder.applied_migrations()
  11. 24 2. MigrationLoader で既存のマイグレーションファイルからProjectStateを構成 {('admin', '0001_initial'): <Migration admin.0001_initial>, ('admin', '0002_logentry_remove_auto_add'):

    <Migration admin.0002_logentry_remove_auto_add>, ('auth', '0001_initial'): <Migration auth.0001_initial>, ('auth', '0002_alter_permission_name_max_length'): <Migration auth.0002_alter_permission_name_max_length>, ('auth', '0003_alter_user_email_max_length'): <Migration auth.0003_alter_user_email_max_length>, ('auth', '0004_alter_user_username_opts'): <Migration auth.0004_alter_user_username_opts>, ('auth', '0005_alter_user_last_login_null'): <Migration auth.0005_alter_user_last_login_null>, ('auth', '0006_require_contenttypes_0002'): <Migration auth.0006_require_contenttypes_0002>, ('auth', '0007_alter_validators_add_error_messages'): <Migration auth.0007_alter_validators_add_error_messages>, ('auth', '0008_alter_user_username_max_length'): <Migration auth.0008_alter_user_username_max_length>, ('auth', '0009_alter_user_last_name_max_length'): <Migration auth.0009_alter_user_last_name_max_length>, ('contenttypes', '0001_initial'): <Migration contenttypes.0001_initial>, ('contenttypes', '0002_remove_content_type_name'): <Migration contenttypes.0002_remove_content_type_name>, ('sessions', '0001_initial'): <Migration sessions.0001_initial>} load_diskの結果
  12. 26 3. (唯⼀のDB接続) MigrationLoader.check_consistent_historyで⼀貫性チェック # django.db.migrations.loader.MigrationLoader#check_consistent_history def check_consistent_history(self, connection): :

    recorder = MigrationRecorder(connection) applied = recorder.applied_migrations() for migration in applied: # If the migration is unknown, skip it. if migration not in self.graph.nodes: continue for parent in self.graph.node_map[migration].parents: logger.debug('migration:{} parent:{} replacements:{}'.format(migration, parent, self.replacements)) if parent not in applied: : : raise InconsistentMigrationHistory( "Migration {}.{} is applied before its dependency " "{}.{} on database '{}'.".format( migration[0], migration[1], parent[0], parent[1], connection.alias, ) ) MigrationRecorder経由でdjango_migrationsを チェックしている
  13. 27 3. (唯⼀のDB接続) MigrationLoader.check_consistent_historyで⼀貫性チェック class MigrationRecorder: : class Migration(models.Model): app

    = models.CharField(max_length=255) name = models.CharField(max_length=255) applied = models.DateTimeField(default=now) class Meta: apps = Apps() app_label = "migrations" db_table = "django_migrations" def __str__(self): return "Migration %s for %s" % (self.name, self.app) django_migrationもDjangoのModel経由で 処理をしている
  14. 28 3. (唯⼀のDB接続) MigrationLoader.check_consistent_historyで⼀貫性チェック mysql > desc django_migrations; +---------+--------------+------+-----+---------+----------------+ |

    Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | app | varchar(255) | NO | | NULL | | | name | varchar(255) | NO | | NULL | | | applied | datetime(6) | NO | | NULL | | +---------+--------------+------+-----+---------+----------------+ 4 rows in set (0.01 sec) これ。
  15. 30 4. リーフが集約しているかのチェック(detect_conflicts) # @@ コンフリクトがあったときの通常対応 # mergeを指定しない限り、同じAppでリーフが複数の場合はmergeの実⾏を促して終了する if conflicts

    and not self.merge: name_str = "; ".join( "%s in %s" % (", ".join(names), app) for app, names in conflicts.items() ) raise CommandError( "Conflicting migrations detected; multiple leaf nodes in the " "migration graph: (%s).\nTo fix them run " "'python manage.py makemigrations --merge'" % name_str ) # If they want to merge and there's nothing to merge, then politely exit if self.merge and not conflicts: self.stdout.write("No conflicts detected to merge.") return # @@ merge action # --mergeを指定していればconflict解決のためのMergeを実⾏する if self.merge and conflicts: return self.handle_merge(loader, conflicts) —merge の場合はここで`self.handle_merge`に流れる
  16. 33 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 # @@ マイグレーション計算の肝であるDetectorを⽣成 # loader.project_state ->

    Migrationファイルから計算したState # ProjectState.from_apps(apps) -> 現在のプロジェクトの状態から求めたState # MigrationAutodetectorは両者の差分を元にマイグレーションファイルを⽣成する機能をもつ # MigrationAutodetectorをここで初期化する autodetector = MigrationAutodetector( loader.project_state(), ProjectState.from_apps(apps), questioner, ) : # @@ migrationの計算 # {'app1': [<Migration app1.0002_book_author>]} changes = autodetector.changes( graph=loader.graph, trim_to_apps=app_labels or None, convert_apps=app_labels or None, migration_name=self.migration_name, ) ここがmakemigrationsの肝 Stateを⽐較して差分を計算している
  17. 34 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 app1 Book Author app2 Order Name

    name book date price bio app1 Book Author app2 Order Name name book date price bio author 適⽤すべき Migrationが 特定される 最新のコードからのState 既存のMigrationからのState author
  18. 35 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 # django.db.migrations.autodetector.MigrationAutodetector#changes def changes(self, graph, trim_to_apps=None,

    convert_apps=None, migration_name=None): : changes = self._detect_changes(convert_apps, graph) # @@ マイグレーションファイル名の調整等 changes = self.arrange_for_graph(changes, graph, migration_name) # @@ app_labelが指定されている場合はそれ以外のChangeを捨てる if trim_to_apps: changes = self._trim_to_apps(changes, trim_to_apps) return changes
  19. 36 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 # django.db.migrations.autodetector.MigrationAutodetector#_detect_changes def _detect_changes(self, convert_apps=None, graph=None):

    : # StateからモデルやFieldを取得してマスターデータを作る for al, mn in self.from_state.models: model = self.old_apps.get_model(al, mn) if not model._meta.managed: self.old_unmanaged_keys.add((al, mn)) elif al not in self.from_state.real_apps: if model._meta.proxy: self.old_proxy_keys.add((al, mn)) else: self.old_model_keys.add((al, mn)) for al, mn in self.to_state.models: model = self.new_apps.get_model(al, mn) if not model._meta.managed: self.new_unmanaged_keys.add((al, mn)) elif ( al not in self.from_state.real_apps or (convert_apps and al in convert_apps) ): if model._meta.proxy: self.new_proxy_keys.add((al, mn)) else: self.new_model_keys.add((al, mn)) from/to のstateについてmodelやFieldを列挙して マスターデータを作成している
  20. 37 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 # @@ マイグレーションの検知処理 # 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_added_indexes() self.generate_altered_db_table() self.generate_altered_order_with_respect_to() self._sort_migrations() self._build_migration_list(graph) self._optimize_migrations() return self.migrations 変更内容ごとに個別に検知処理をしている
  21. 38 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 # django.db.migrations.autodetector.MigrationAutodetector#generate_added_fields def generate_added_fields(self): """Make AddField

    operations.""" # @@ 既存のフィールドとの差分を取って列追加を検知する # ('app1', 'book', 'author') のような形式 for app_label, model_name, field_name in sorted(self.new_field_keys - self.old_field_keys): #Migrationの元になるOperation(operations.AddField)が⽣成される self._generate_added_field(app_label, model_name, field_name) add_fieldの場合 差分はわりと豪快で、列のset同⼠を⽐較した差分を⾒る
  22. 39 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 def _generate_added_field(self, app_label, model_name, field_name): field

    = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name) # Fields that are foreignkeys/m2ms depend on stuff dependencies = [] : : <依存やFKなどの処理> : self.add_operation( app_label, operations.AddField( model_name=model_name, name=field_name, field=field, preserve_default=preserve_default, ), dependencies=dependencies, ) 最終的に対応する Operationインスタンスを登録している。
  23. 40 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 django.db.migrations.migration.Migration class Migration: operations = []

    dependencies = [] run_before = [] replaces = [] initial = None atomic = True def __init__(self, name, app_label): self.name = name self.app_label = app_label # Copy dependencies & other attrs as we might mutate them at runtime self.operations = list(self.__class__.operations) self.dependencies = list(self.__class__.dependencies) self.run_before = list(self.__class__.run_before) self.replaces = list(self.__class__.replaces) 適⽤される単位 複数の変更操作(Operation)を持っている
  24. 41 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 django.db.migrations.operations.fields.AddField class AddField(FieldOperation): """Add a field

    to a model.""" def __init__(self, model_name, name, field, preserve_default=True): self.field = field self.preserve_default = preserve_default super().__init__(model_name, name) def deconstruct(self): kwargs = { 'model_name': self.model_name, 'name': self.name, 'field': self.field, } if self.preserve_default is not True: kwargs['preserve_default'] = self.preserve_default return ( self.__class__.__name__, [], kwargs ) 変更処理の単位 処理ごとに実装がある。 例) 列追加であればAddFiled
  25. 43 6. 差分があればwrite_migration_filesでマイグレーションファイルの作成 if not changes: # No changes? Tell

    them. if self.verbosity >= 1: if app_labels: if len(app_labels) == 1: self.stdout.write("No changes detected in app '%s'" % app_labels.pop()) else: self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels))) else: self.stdout.write("No changes detected") else: # @@ マイグレーションファイルの作成 self.write_migration_files(changes) if check_changes: sys.exit(1) 差分があればwrite_migration_filesに移動する
  26. 44 7. MigrationWriterを通して各migrationをマイグレーションファイルとして書き出し def write_migration_files(self, changes): : # changes は

    {'app1': [<Migration app1.0002_book_author>]} for app_label, app_migrations in changes.items(): # @@ app1 [<Migration app1.0002_book_author>] : for migration in app_migrations: # Describe the migration # @@ Migrationから書き出し⽤のMigrationWriterを取得する writer = MigrationWriter(migration) : if not self.dry_run: : migration_string = writer.as_string() # @@ 実際にマイグレーションファイルを書き出す with open(writer.path, "w", encoding='utf-8') as fh: fh.write(migration_string) : 書き出す内容はMigrationWriterで⽣成される
  27. 45 7. MigrationWriterを通して各migrationをマイグレーションファイルとして書き出し # django.db.migrations.writer.MigrationWriter#as_string def as_string(self): """Return a string

    of the file contents.""" items = { "replaces_str": "", "initial_str": "", } imports = set() # Deconstruct operations operations = [] for operation in self.migration.operations: # @@ Operation単位でコマンドに変換 operation_string, operation_imports = OperationWriter(operation).serialize() # @@ importsは⼀回やればいいからsetになっている imports.update(operation_imports) operations.append(operation_string) 実際の内容はさらにOperationWriterに移っていく 基本的にはOperationのdeconstructを利⽤して コンストラクタに渡す引数を逆に戻している
  28. 46 from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration):

    dependencies = [ ('app1', '0001_initial'), ] operations = [ migrations.AddField( model_name='book', name='author', field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='app1.Author'), ), ]
  29. 48 1. connection = connections[db] でDBへの接続を取得 2. MigrationExecutorで整合性のチェックやファイル指定の判定 3. MigrationPlanの⽣成(migrationやbackwordの判定等)

    4. マイグレーション前のProjectStateを構成する 5. マイグレーションの実⾏ 6. マイグレーションの実⾏(SQLの発⾏) migrateの⼤まかな流れ
  30. 49 1. `connection = connections[db]`でDBへの接続を取得 db = options['database'] # @@

    対応するDBへのコネクションラッパを取得する # ConnectionHandler().__getitem__(db) # django.db.backends.sqlite3.base.DatabaseWrapper が戻る connection = connections[db] # @@ sqlite3では未実装なのでpass connection.prepare_database() # django.db.utils.ConnectionHandler connections = ConnectionHandler() prepare_database()の実装があるのは PostGISくらい。 `CREATE EXTENSION IF NOT EXISTS postgis`
  31. 50 1. `connection = connections[db]`でDBへの接続を取得 # django.db.utils.ConnectionHandler class ConnectionHandler: def

    __init__(self, databases=None): : : self._databases = databases self._connections = local() : 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] # @@ DBに応じたドライバクラスをimportする # i.e. django.db.backends.sqlite3.base backend = load_backend(db['ENGINE']) conn = backend.DatabaseWrapper(db, alias) # @@ # dbは設定のDict, aliasは設定名(default) setattr(self._connections, alias, conn) return conn `django.db.backends.base.base.BaseDatabaseWrapper`を 実装するクラスのインスタンスが戻る。 SQLite3は`django.db.backends.sqlite3.base.DatabaseWrapper`
  32. 52 2. MigrationExecutorで整合性のチェックやファイル指定の判定 # django.db.migrations.executor.MigrationExecutor class MigrationExecutor: """ End-to-end migration

    execution - load migrations and run them up or down to a specified set of targets. """ def __init__(self, connection, progress_callback=None): self.connection = connection # @@ makemigrationでも使っているローダ # connectionを渡しているのでDBから読み込まれる # Stateを作るため.今回はDBのMigrateテーブルから取得している # Loaderの中でもRecorder使ってるけど self.loader = MigrationLoader(self.connection) # @@ Recorder # 適⽤済のMigrationをDBに永続化する # Migrationというモデルをもっていて、ORM経由でそこにしまっている self.recorder = MigrationRecorder(self.connection) # 進捗管理の関数 self.progress_callback = progress_callback MigrationLoaderやMigrationRecorderは ここでも出てきている
  33. 53 2. MigrationExecutorで整合性のチェックやファイル指定の判定 # Raise an error if any migrations

    are applied before their dependencies. # @@ # executor.loaderはMigrationLoader # 適⽤済マイグレーションのツリーの⼀貫性チェック # 適⽤されているマイグレーションのparentがgraph.nodesにあるかを⾒ている # どこかで辿りきれなくなっているのはまずいので。どこかで流れが変わっている可能性がある executor.loader.check_consistent_history(connection) makemigrationsでも実施していた 適⽤済マイグレーションとのチェック
  34. 54 2. MigrationExecutorで整合性のチェックやファイル指定の判定 # Before anything else, see if there's

    conflicting apps and drop out # hard if there are any # @@ マイグレーションファイルのコンフリクトチェック # 同じAppで複数のリーフが存在していないかを⾒ている conflicts = executor.loader.detect_conflicts() if conflicts: name_str = "; ".join( "%s in %s" % (", ".join(names), app) for app, names in conflicts.items() ) raise CommandError( "Conflicting migrations detected; multiple leaf nodes in the " "migration graph: (%s).\nTo fix them run " "'python manage.py makemigrations --merge'" % name_str ) makemigrationsでも実施していた マイグレーションファイルのチェック
  35. 56 3. MigrationPlanの⽣成(migration fileのセットやbackwordの判定とか) [ ('admin', '0002_logentry_remove_auto_add'), ('app1', '0002_book_author'), ('auth',

    '0009_alter_user_last_name_max_length'), ('contenttypes', '0002_remove_content_type_name'), ('sessions', '0001_initial'), ] targetsの中⾝ 各appの最後のMigrationが列挙されている
  36. 57 3. MigrationPlanの⽣成(migration fileのセットやbackwordの判定とか) # django.db.migrations.executor.MigrationExecutor#migration_plan def migration_plan(self, targets, clean_start=False):

    """ Given a set of targets, return a list of (Migration instance, backwards?). """ # @@ # targets=[('admin', '0002_logentry_remove_auto_add'), ('app1', '0002_book_author'), ....] plan = [] if clean_start: applied = set() else: applied = set(self.loader.applied_migrations) for target in targets: : 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) backwords=False ⾃ノードの親を辿っていき未適⽤のものを Migrate対象にしていく
  37. 58 4. マイグレーション前のProjectStateを構成する # @@ マイグレーション前のProjectStateを構成する # 適⽤済の範囲だけ pre_migrate_state =

    executor._create_project_state(with_applied_migrations=True) pre_migrate_apps = pre_migrate_state.apps # @@ シグナルを投げる # 最終的にinject_rename_contenttypes_operationsが呼ばれる emit_pre_migrate_signal( self.verbosity, self.interactive, connection.alias, apps=pre_migrate_apps, plan=plan, ) DjangoのSignalが使われている
  38. 59 4. マイグレーション前のProjectStateを構成する # django/db/models/signals.py:52 pre_migrate = Signal(providing_args=["app_config", "verbosity", "interactive",

    "using", "apps", "plan"]) post_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using", "apps", "plan"]) ここで定義されていた
  39. 60 4. マイグレーション前のProjectStateを構成する # django.contrib.contenttypes.apps.ContentTypesConfig class ContentTypesConfig(AppConfig): name = 'django.contrib.contenttypes'

    verbose_name = _("Content Types") def ready(self): pre_migrate.connect(inject_rename_contenttypes_operations, sender=self) post_migrate.connect(create_contenttypes) checks.register(check_generic_foreign_keys, checks.Tags.models) checks.register(check_model_name_lengths, checks.Tags.models) 観測できる範囲ではここで Signalのハンドラがあった Inject については追いきれていない
  40. 61 5. マイグレーションの実⾏ # @@ migrateを実⾏する post_migrate_state = executor.migrate( targets,

    plan=plan, state=pre_migrate_state.clone(), fake=fake, fake_initial=fake_initial, ) # django.db.migrations.executor.MigrationExecutor#migrate def migrate(self, targets, plan=None, state=None, fake=False, fake_initial=False): : : elif all_forwards: if state is None: # The resulting state should still include applied migrations. state = self._create_project_state(with_applied_migrations=True) # @@ migrate実⾏ state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial) 普通はこれ
  41. 62 6. マイグレーションの実⾏(2) # django.db.migrations.executor.MigrationExecutor#_migrate_all_forwards 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: # process. break if migration in migrations_to_run: if 'apps' not in state.__dict__: if self.progress_callback: self.progress_callback("render_start") state.apps # Render all -- performance critical if self.progress_callback: self.progress_callback("render_success") # @@ 各Migrateの実⾏ state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial) migrations_to_run.remove(migration) return state さらにapply_migrationに流れる
  42. 63 6. マイグレーションの実⾏(2) def apply_migration(self, state, migration, fake=False, fake_initial=False): """Run

    a migration forwards:.””" : # Alright, do it normally # @@ ここがメインの処理 # sqlite3 ならdjango.db.backends.sqlite3.schema.DatabaseSchemaEditor # migrationを各種DBにあわせてSQLを発⾏する # 列追加ならmigrations.operations.filed.AddFiled with self.connection.schema_editor(atomic=migration.atomic) as schema_editor: state = migration.apply(state, schema_editor) : : # @@ 適⽤済のマイグレーションをdjango_migrationsに記録する self.recorder.record_applied(migration.app_label, migration.name) # Report progress if self.progress_callback: self.progress_callback("apply_success", migration, fake) return state migration.apply にschema_editorを 渡してさらに進むが、実際の処理は migration.operations.database_forwards にいく
  43. 64 6. マイグレーションの実⾏(2) class AddField(FieldOperation): """Add a field to a

    model.""" : # @@ 列追加の場合の処理 def database_forwards(self, app_label, schema_editor, from_state, to_state): to_model = to_state.apps.get_model(app_label, self.model_name) # @@ __fake__.Book if self.allow_migrate_model(schema_editor.connection.alias, to_model): from_model = from_state.apps.get_model(app_label, self.model_name) field = to_model._meta.get_field(self.name) if not self.preserve_default: field.default = self.field.default # @@ ここでSQLを組み⽴てて実⾏する # sqlite3 以外は結構デフォルト実装をみてるけど # sqlite3だけやたら複雑 schema_editor.add_field( from_model, field, ) if not self.preserve_default: field.default = NOT_PROVIDED 列追加であればAddFieldのOperationが格納されているので これが実⾏される。 schema_editor.add_filedが呼ばれるがこれはRDBMSごとに 実装がことなる。
  44. 65 6. マイグレーションの実⾏(2) # django.db.backends.sqlite3.schema.DatabaseSchemaEditor#add_field def add_field(self, model, field): """

    Create a field on a model. Usually involves adding a column, but may involve adding a table instead (for M2M fields). """ # Special-case implicit M2M tables if field.many_to_many and field.remote_field.through._meta.auto_created: return self.create_model(field.remote_field.through) self._remake_table(model, create_field=field) SQLite3の場合は、DB側の制限のため実際は 列を増やしたテーブルを再作成している
  45. 66 (djancon) denzowno:DjangoConSample denzow$ python manage.py migrate Operations to perform:

    Apply all migrations: admin, app1, auth, contenttypes, sessions Running migrations: Applying app1.0002_book_author... OK
  46. 69 ⼿動作成のMigrationの作成の基本 $ python manage.py makemigrations --empty yourappname # Generated

    by Django A.B on YYYY-MM-DD HH:MM from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('yourappname', '0001_initial'), ] operations = [ ] 空のmigrationファイル作成 これを書き換えていく
  47. 71 def test(apps, scheme_editor): book_model = apps.get_model('app1', 'Book') : class

    Migration(migrations.Migration): dependencies = [ ('app1', '0001_initial'), ] operations = [ migrations.RunPython(test, RunPython.noop), ] こんな感じで使⽤する
  48. 75 他のモデルとの突き合わせをしながらのマイグレーションで死亡 Traceback (most recent call last): File "manage.py", line

    27, in <module> execute_from_command_line(sys.argv) File "/Users/denzow/work/denzow/DjangoConSample/django/core/management/__init__.py", line 381, in execute_from_command_line utility.execute() self.check_related_objects(join_info.final_field, value, join_info.opts) File "/Users/denzow/work/denzow/DjangoConSample/django/db/models/sql/query.py", line 1063, in check_related_objects self.check_query_object_type(value, opts, field) File "/Users/denzow/work/denzow/DjangoConSample/django/db/models/sql/query.py", line 1046, in check_query_object_type (value, opts.object_name)) ValueError: Cannot query "<class '__fake__.Book'>": Must be "Book" instance.
  49. 76 ValueError: Cannot query "<class '__fake__.Book'>": Must be “Book" instance.

    他のモデルとの突き合わせをしながらのマイグレーションで死亡
  50. 77 from other_module import other_sv def test(apps, scheme_editor): book_model =

    apps.get_model('app1', 'Book') for book in book_model.objects.all(): # 他のサービスで使われいないBookを消す処理的なもの if not other_sv.get_by_book_list(book)): book.delete() class Migration(migrations.Migration): dependencies = [ ('app1', '0001_initial'), ] operations = [ migrations.RunPython(test, RunPython.noop), ] 他のモデルとの突き合わせをしながらのマイグレーションで死亡
  51. 78 from other_module import other_sv def test(apps, scheme_editor): book_model =

    apps.get_model('app1', 'Book') for book in book_model.objects.all(): # 他のサービスで使われいないBookを消す処理的なもの if not other_sv.get_by_book_list(book)): book.delete() 他のモデルとの突き合わせをしながらのマイグレーションで死亡
  52. 83 book_model = apps.get_model('app1', 'Book') from app1.models import Book Migrate適⽤時点のProjectStateでのapp1.Book

    現在のコードレベルでのapp1.Book 他のモデルとの突き合わせをしながらのマイグレーションで死亡
  53. 87 他のモデルとの突き合わせをしながらのマイグレーションで死亡 # 関数 def get_by_book_list(book): return list(OtherModel.objects.filter(book=book)) # 関数

    def get_by_book_list(book): return list(OtherModel.objects.filter(book_id=book.id)) オブジェクトではなくid指定で filterすれば⼤丈夫だった