Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

2 ➡ でんぞう (@denzowill, denzow) ➡ Python歴 5年 (仕事で半年) ➡ scouty,inc シニアエンジニア(前職はDBサポートエンジニア) ➡ DBスペシャリスト(PostgreSQLが好き、●racleは得意だけど苦⼿) ➡ StartPythonClubスタッフ お前誰よ?

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

のミッション ⾃分のまわりには、⾃分でも気づいていないたくさんの可能性や偶然性が存在するはずなのに、
 ⼈はいつもそれに巡り会えるとは限りません。
 そしてその結果、仕事や⼈材におけるミスマッチに悩む⼈も少なくはないでしょう。 scoutyは、インターネット上にあふれるデータと最先端の⼈⼯知能技術を使って情報と機会を適切にお届け することで、偶然を必然に変え、世の中のミスマッチをなくしていくことを⽬指します。 そして、それは結 果として、個⼈の市場価値や⽣活の質を⾼め、企業の競争⼒を⾼めることにつながると考えています。 「世の中のミスマッチを無くす」

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Python と Django と

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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)

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

14

Slide 15

Slide 15 text

15 Django と scouty ➡ Django 1.11 ➡ Django Celery ➡ Django でDDD的な設計 ➡ Django Channels でのWebsocket(予定) ➡ daphneでの本番運⽤

Slide 16

Slide 16 text

マイグレーションの流れ

Slide 17

Slide 17 text

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)

Slide 18

Slide 18 text

18 DBに反映するためのコマンド QZUIPONBOBHFQZNBLFNJHSBUJPOT QZUIPONBOBHFQZNJHSBUF

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

makemigrations

Slide 21

Slide 21 text

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を通してマイグレーションファイルを書き出し

Slide 22

Slide 22 text

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)

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

24 2. MigrationLoader で既存のマイグレーションファイルからProjectStateを構成 {('admin', '0001_initial'): , ('admin', '0002_logentry_remove_auto_add'): , ('auth', '0001_initial'): , ('auth', '0002_alter_permission_name_max_length'): , ('auth', '0003_alter_user_email_max_length'): , ('auth', '0004_alter_user_username_opts'): , ('auth', '0005_alter_user_last_login_null'): , ('auth', '0006_require_contenttypes_0002'): , ('auth', '0007_alter_validators_add_error_messages'): , ('auth', '0008_alter_user_username_max_length'): , ('auth', '0009_alter_user_last_name_max_length'): , ('contenttypes', '0001_initial'): , ('contenttypes', '0002_remove_content_type_name'): , ('sessions', '0001_initial'): } load_diskの結果

Slide 25

Slide 25 text

25 3. (唯⼀のDB接続) MigrationLoader.check_consistent_historyで⼀貫性チェック loader.check_consistent_history(connection) 適⽤済マイグレーションが⾒つかったらその全ての親が 正しく適⽤されているかをチェックしている。

Slide 26

Slide 26 text

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を チェックしている

Slide 27

Slide 27 text

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経由で 処理をしている

Slide 28

Slide 28 text

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) これ。

Slide 29

Slide 29 text

29 4. リーフが集約しているかのチェック(detect_conflicts) # @@ マイグレーションファイルのコンフリクトチェック # 同じAppで複数のリーフが存在していないかを⾒ている conflicts = loader.detect_conflicts()

Slide 30

Slide 30 text

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`に流れる

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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': []} 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を⽐較して差分を計算している

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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を列挙して マスターデータを作成している

Slide 37

Slide 37 text

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 変更内容ごとに個別に検知処理をしている

Slide 38

Slide 38 text

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同⼠を⽐較した差分を⾒る

Slide 39

Slide 39 text

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インスタンスを登録している。

Slide 40

Slide 40 text

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)を持っている

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

42 5. MigrationAutodetector で現在のAppStateとマイグレーションファイルのStateの差分取得 changes = {‘app1': []}

Slide 43

Slide 43 text

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に移動する

Slide 44

Slide 44 text

44 7. MigrationWriterを通して各migrationをマイグレーションファイルとして書き出し def write_migration_files(self, changes): : # changes は {'app1': []} for app_label, app_migrations in changes.items(): # @@ app1 [] : 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で⽣成される

Slide 45

Slide 45 text

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を利⽤して コンストラクタに渡す引数を逆に戻している

Slide 46

Slide 46 text

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'), ), ]

Slide 47

Slide 47 text

migrate

Slide 48

Slide 48 text

48 1. connection = connections[db] でDBへの接続を取得 2. MigrationExecutorで整合性のチェックやファイル指定の判定 3. MigrationPlanの⽣成(migrationやbackwordの判定等) 4. マイグレーション前のProjectStateを構成する 5. マイグレーションの実⾏ 6. マイグレーションの実⾏(SQLの発⾏) migrateの⼤まかな流れ

Slide 49

Slide 49 text

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`

Slide 50

Slide 50 text

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`

Slide 51

Slide 51 text

51 2. MigrationExecutorで整合性のチェックやファイル指定の判定 # @@ executorの取得 # migration_progress_callbackは進捗をstdoutにいい感じにだすための処理 # DBから状態を取り出し、適⽤をするクラス executor = MigrationExecutor(connection, self.migration_progress_callback)

Slide 52

Slide 52 text

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は ここでも出てきている

Slide 53

Slide 53 text

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でも実施していた 適⽤済マイグレーションとのチェック

Slide 54

Slide 54 text

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でも実施していた マイグレーションファイルのチェック

Slide 55

Slide 55 text

55 3. MigrationPlanの⽣成(migration fileのセットやbackwordの判定とか) targets = executor.loader.graph.leaf_nodes() # 実際に適⽤すべきマイグレーションを決定する plan = executor.migration_plan(targets)

Slide 56

Slide 56 text

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が列挙されている

Slide 57

Slide 57 text

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対象にしていく

Slide 58

Slide 58 text

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が使われている

Slide 59

Slide 59 text

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"]) ここで定義されていた

Slide 60

Slide 60 text

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 については追いきれていない

Slide 61

Slide 61 text

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) 普通はこれ

Slide 62

Slide 62 text

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に流れる

Slide 63

Slide 63 text

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 にいく

Slide 64

Slide 64 text

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ごとに 実装がことなる。

Slide 65

Slide 65 text

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側の制限のため実際は 列を増やしたテーブルを再作成している

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

⼿動での マイグレーションファイル

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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ファイル作成 これを書き換えていく

Slide 70

Slide 70 text

70 ⼿動作成のMigrationの作成の基本 ⼿動SQL実⾏ 任意のPythonコード実⾏ migrations.RunSQL (django.db.migrations.operations.special.RunSQL) migrations.RunPython (django.db.migrations.operations.special.RunPython)

Slide 71

Slide 71 text

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), ] こんな感じで使⽤する

Slide 72

Slide 72 text

72 ⼿動マイグレーションの利⽤ケース 後からユニーク + NOT NULLな列追加をする場合 列変更に伴ってデータクレンジングが必要になる場合 列定義変更と同時に関連データ変更 が必要になる場合に多い

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

denzowが migrateでやらかした事例

Slide 75

Slide 75 text

75 他のモデルとの突き合わせをしながらのマイグレーションで死亡 Traceback (most recent call last): File "manage.py", line 27, in 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 "": Must be "Book" instance.

Slide 76

Slide 76 text

76 ValueError: Cannot query "": Must be “Book" instance. 他のモデルとの突き合わせをしながらのマイグレーションで死亡

Slide 77

Slide 77 text

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), ] 他のモデルとの突き合わせをしながらのマイグレーションで死亡

Slide 78

Slide 78 text

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() 他のモデルとの突き合わせをしながらのマイグレーションで死亡

Slide 79

Slide 79 text

79 何が悪いかわかりましたか?

Slide 80

Slide 80 text

80 答え

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

82 book_model = apps.get_model('app1', 'Book') from app1.models import Book 他のモデルとの突き合わせをしながらのマイグレーションで死亡

Slide 83

Slide 83 text

83 book_model = apps.get_model('app1', 'Book') from app1.models import Book Migrate適⽤時点のProjectStateでのapp1.Book 現在のコードレベルでのapp1.Book 他のモデルとの突き合わせをしながらのマイグレーションで死亡

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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すれば⼤丈夫だった

Slide 88

Slide 88 text

まとめ

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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