いまさら振り返る Django Migration

7d46f2037fed74f249a4c85e8635da7d?s=47 Denzow
May 19, 2018

いまさら振り返る Django Migration

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

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

7d46f2037fed74f249a4c85e8635da7d?s=128

Denzow

May 19, 2018
Tweet

Transcript

  1. ͍·͞ΒৼΓฦΔ Django Migration Migrationͷ಺෦ಈ࡞͔Β΍ͬͪΌͬͨࣄྫ·Ͱ 20180519 DjangoCongress @denzowill or denzow

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

    scouty,inc シニアエンジニア(前職はDBサポートエンジニア) ➡ DBスペシャリスト(PostgreSQLが好き、•racleは得意だけど苦⼿) ➡ StartPythonClubスタッフ お前誰よ?
  3. None
  4. のミッション ⾃分のまわりには、⾃分でも気づいていないたくさんの可能性や偶然性が存在するはずなのに、
 ⼈はいつもそれに巡り会えるとは限りません。
 そしてその結果、仕事や⼈材におけるミスマッチに悩む⼈も少なくはないでしょう。 scoutyは、インターネット上にあふれるデータと最先端の⼈⼯知能技術を使って情報と機会を適切にお届け することで、偶然を必然に変え、世の中のミスマッチをなくしていくことを⽬指します。 そして、それは結 果として、個⼈の市場価値や⽣活の質を⾼め、企業の競争⼒を⾼めることにつながると考えています。 「世の中のミスマッチを無くす」

  5. ⽉1で麹町あたりで 100⼈くらいの勉強会やってます

  6. 6 QZUIPONBOBHFQZNBLFNJHSBUJPOT QZUIPONBOBHFQZNJHSBUF 実⾏したことがある⼈"

  7. 7 ぶっちゃけ 不思議じゃないですか?

  8. 今⽇話すこと 8 ➡migrateとmakemigrationsのソースを読んで
 知的好奇⼼を満たす ➡⼿書きmigrateでやらかさないために

  9. 9 ➡ とPython, Django ➡ マイグレーションの呼び出しを追ってみる ➡ ⼿動でマイグレーションファイルを作成する ➡ やらかした事例

    アジェンダ
  10. Python と Django と

  11. 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
  12. 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)
  13. 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) ᶃ ᶄ ᶆ ᶇ ᶅ ᶈ ᶈ ᶈ ᶉ ᶉ ᶉ インフラ構成図 (フレームワーク)
  14. 14

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

    ➡ Django でDDD的な設計 ➡ Django Channels でのWebsocket(予定) ➡ daphneでの本番運⽤
  16. マイグレーションの流れ

  17. 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)
  18. 18 DBに反映するためのコマンド QZUIPONBOBHFQZNBLFNJHSBUJPOT QZUIPONBOBHFQZNJHSBUF

  19. 19 DBに反映するためのコマンド QZUIPONBOBHFQZNBLFNJHSBUJPOT QZUIPONBOBHFQZNJHSBUF ▶ django.core.management.commands.makemigrations.Command ▶ django.core.management.commands.migrate.Command

  20. makemigrations

  21. 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を通してマイグレーションファイルを書き出し
  22. 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)
  23. 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()
  24. 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の結果
  25. 25 3. (唯⼀のDB接続) MigrationLoader.check_consistent_historyで⼀貫性チェック loader.check_consistent_history(connection) 適⽤済マイグレーションが⾒つかったらその全ての親が 正しく適⽤されているかをチェックしている。

  26. 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を チェックしている
  27. 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経由で 処理をしている
  28. 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) これ。
  29. 29 4. リーフが集約しているかのチェック(detect_conflicts) # @@ マイグレーションファイルのコンフリクトチェック # 同じAppで複数のリーフが存在していないかを⾒ている conflicts =

    loader.detect_conflicts()
  30. 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`に流れる
  31. 31 4. リーフが集約しているかのチェック(detect_conflicts) app1: 0001_initial.py app1: 0002_book_author app1: 0003_other_migration app1:

    0001_initial.py app1: 0002_book_author app1: 0003_other_migration1 app1: 0003_other_migration2
  32. 32 4. リーフが集約しているかのチェック(detect_conflicts) app1: 0001_initial.py app1: 0002_book_author app1: 0003_other_migration1 app1:

    0003_other_migration2 app1: 0004_merge_migration
  33. 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を⽐較して差分を計算している
  34. 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
  35. 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
  36. 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を列挙して マスターデータを作成している
  37. 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 変更内容ごとに個別に検知処理をしている
  38. 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同⼠を⽐較した差分を⾒る
  39. 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インスタンスを登録している。
  40. 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)を持っている
  41. 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
  42. 42 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 changes = {‘app1': [<Migration app1.0002_book_author>]}

  43. 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に移動する
  44. 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で⽣成される
  45. 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を利⽤して コンストラクタに渡す引数を逆に戻している
  46. 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'), ), ]
  47. migrate

  48. 48 1. connection = connections[db] でDBへの接続を取得 2. MigrationExecutorで整合性のチェックやファイル指定の判定 3. MigrationPlanの⽣成(migrationやbackwordの判定等)

    4. マイグレーション前のProjectStateを構成する 5. マイグレーションの実⾏ 6. マイグレーションの実⾏(SQLの発⾏) migrateの⼤まかな流れ
  49. 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`
  50. 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`
  51. 51 2. MigrationExecutorで整合性のチェックやファイル指定の判定 # @@ executorの取得 # migration_progress_callbackは進捗をstdoutにいい感じにだすための処理 # DBから状態を取り出し、適⽤をするクラス

    executor = MigrationExecutor(connection, self.migration_progress_callback)
  52. 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は ここでも出てきている
  53. 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でも実施していた 適⽤済マイグレーションとのチェック
  54. 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でも実施していた マイグレーションファイルのチェック
  55. 55 3. MigrationPlanの⽣成(migration fileのセットやbackwordの判定とか) targets = executor.loader.graph.leaf_nodes() # 実際に適⽤すべきマイグレーションを決定する plan

    = executor.migration_plan(targets)
  56. 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が列挙されている
  57. 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対象にしていく
  58. 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が使われている
  59. 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"]) ここで定義されていた
  60. 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 については追いきれていない
  61. 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) 普通はこれ
  62. 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に流れる
  63. 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 にいく
  64. 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ごとに 実装がことなる。
  65. 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側の制限のため実際は 列を増やしたテーブルを再作成している
  66. 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
  67. ⼿動での マイグレーションファイル

  68. 68 ⼿動作成のMigrationの作成の基本 https://docs.djangoproject.com/ja/2.0/topics/migrations/#data-migrations ⼿動SQL実⾏ 任意のモデル操作実⾏ 任意のPythonコード実⾏

  69. 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ファイル作成 これを書き換えていく
  70. 70 ⼿動作成のMigrationの作成の基本 ⼿動SQL実⾏ 任意のPythonコード実⾏ migrations.RunSQL (django.db.migrations.operations.special.RunSQL) migrations.RunPython (django.db.migrations.operations.special.RunPython)

  71. 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), ] こんな感じで使⽤する
  72. 72 ⼿動マイグレーションの利⽤ケース 後からユニーク + NOT NULLな列追加をする場合 列変更に伴ってデータクレンジングが必要になる場合 列定義変更と同時に関連データ変更 が必要になる場合に多い

  73. 73 Djangoのモデルであとからユニーク + NOT NULLな列を追加する http://www.denzow.me/entry/2017/12/23/150501

  74. denzowが migrateでやらかした事例

  75. 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.
  76. 76 ValueError: Cannot query "<class '__fake__.Book'>": Must be “Book" instance.

    他のモデルとの突き合わせをしながらのマイグレーションで死亡
  77. 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), ] 他のモデルとの突き合わせをしながらのマイグレーションで死亡
  78. 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() 他のモデルとの突き合わせをしながらのマイグレーションで死亡
  79. 79 何が悪いかわかりましたか?

  80. 80 答え

  81. 81 他のモデルとの突き合わせをしながらのマイグレーションで死亡 # 関数 def get_by_book_list(book): return list(OtherModel.objects.filter(book=book)) ORM側のバリデーションで 落ちていた

    ValueError: Cannot query "<class '__fake__.Book'>": Must be “Book" instance.
  82. 82 book_model = apps.get_model('app1', 'Book') from app1.models import Book 他のモデルとの突き合わせをしながらのマイグレーションで死亡

  83. 83 book_model = apps.get_model('app1', 'Book') from app1.models import Book Migrate適⽤時点のProjectStateでのapp1.Book

    現在のコードレベルでのapp1.Book 他のモデルとの突き合わせをしながらのマイグレーションで死亡
  84. 84 book_model = apps.get_model('app1', ‘Book’) print(book_model) 他のモデルとの突き合わせをしながらのマイグレーションで死亡

  85. 85 book_model = apps.get_model('app1', ‘Book’) print(book_model) <class '__fake__.Book'> 他のモデルとの突き合わせをしながらのマイグレーションで死亡

  86. 86 他のモデルとの突き合わせをしながらのマイグレーションで死亡 # 関数 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))
  87. 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すれば⼤丈夫だった
  88. まとめ

  89. 89 さらっとまとめ * makemigrationsは処理⾃体にはDBはいらない(接続してるけど) * migrate時はSchemeEditorががんばってくれている * Migrationクラスが両者において重要な位置にいる(というかシリアライズ・デシリアライズ) * migrateではapps.get_modelちゃんと使おう。間違ってもImportしちゃだめだ

  90. 知的好奇⼼は満たされましたか?