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. ͍·͞ΒৼΓฦΔ
    Django Migration
    Migrationͷ಺෦ಈ࡞͔Β΍ͬͪΌͬͨࣄྫ·Ͱ
    20180519 DjangoCongress
    @denzowill or denzow

    View full-size slide

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

    View full-size slide

  3. のミッション
    ⾃分のまわりには、⾃分でも気づいていないたくさんの可能性や偶然性が存在するはずなのに、

    ⼈はいつもそれに巡り会えるとは限りません。

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. 今⽇話すこと 8
    ➡migrateとmakemigrationsのソースを読んで

    知的好奇⼼を満たす
    ➡⼿書きmigrateでやらかさないために

    View full-size slide

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

    View full-size slide

  9. Python と
    Django と

    View full-size slide

  10. 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

    View full-size slide

  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
    crawler
    ϝΠϯαʔϏε
    Ϋϩʔϧͨ͠

    ੜσʔλͷdiff
    ੔ܗ͞Εͨσʔλ
    event 

    (time-based)











    インフラ構成図 (Python)

    View full-size slide

  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)











    インフラ構成図 (フレームワーク)

    View full-size slide

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

    View full-size slide

  14. マイグレーションの流れ

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  18. makemigrations

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. 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の結果

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  29. 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

    View full-size slide

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

    View full-size slide

  31. 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を⽐較して差分を計算している

    View full-size slide

  32. 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

    View full-size slide

  33. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  39. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  42. 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で⽣成される

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  46. 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`

    View full-size slide

  47. 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`

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  63. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  72. 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.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  76. 79

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  81. 85
    book_model = apps.get_model('app1', ‘Book’)
    print(book_model)

    他のモデルとの突き合わせをしながらのマイグレーションで死亡

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide