Migration - Tirrilee/TechTalk GitHub Wiki

Migration

์ฐธ๊ณ  : Django Migration

  • model์˜ ๋ณ€๊ฒฝ ์ด๋ ฅ
  • Database Version Control System



Command

  • migrate : ์ ์šฉ ์•ˆ๋œ migrations ํŒŒ์ผ๋“ค์„ ์ ์šฉ์‹œํ‚ค๋Š” ๋ช…๋ น์–ด
  • makemigrations : model์— ๋ณ€ํ™”๋œ ๊ฒƒ๋“ค์„ ๊ธฐ๋ณธ์œผ๋กœ ์ƒˆ๋กœ์šด migration ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ
  • sqlmigrate : migration์„ sql ํ˜•์‹์œผ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ (DB์—๋Š” SQL๋กœ ์ ์šฉ๋œ๋‹ค.)
  • showmigrations : migration์˜ ์ ์šฉ ์—ฌ๋ถ€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ



Backend Support

PostgreSQL

  • default ๊ฐ’๊ณผ ํ•จ๊ป˜ columns๋ฅผ ์ถ”๊ฐ€, table full rewrite ๋“ฑ ๋ชจ๋“  ๋ณ€ํ™” ๊ณผ์ •์ด ๊ฑฐ์˜ ์ง€์ฒด์—†์ด ์ผ์–ด๋‚จ

MySQL

  • schema๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ ๋“ฑ์˜ ์ฒ˜๋ฆฌ๊ฐ€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ๋‹ค. ๋งŒ์•ฝ ๋ช‡๊ฐœ ๋˜์ง€ ์•Š์œผ๋ฉด ๊ดœ์ฐฎ์ง€๋งŒ, ์ˆ˜๋งŒ๊ฐœ๋ผ๋ฉด site๋ฅผ down์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

SQLite

  • ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ง€์›ํ•˜๋‚˜, ๋ฒ„๊ทธ๊ฐ€ ์กด์žฌํ•œ๋‹ค. ๋งŒ์•ฝ ์„œ๋น„์Šค๊ฐ€ risk๋‚˜ ์ œ์•ฝ์— ๋งค์šฐ ๋ฏผ๊ฐํ•˜๋‹ค๋ฉด ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค.
  • Django๋Š” local machine์—์„œ full database๊ฐ€ ํ•„์š”์—†์ด ๊ฐœ๋ฐœํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด SQLite๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.



Version Control

  • Django์—์„œ ์“ฐ์—ฌ์ง„ Number๋Š” ๋‹จ์ง€ Developer๋ฅผ ์œ„ํ•œ ๊ฒƒ์ด๋‹ค. Django๋Š” ๋‹จ์ง€ migration ํŒŒ์ผ ๊ฐ„์˜ ์ฐจ์ด๋ฅผ ํ™•์ธํ•œ๋‹ค.
  • migration๊ฐ„์˜ ์ˆœ์„œ๋Š” migration ํŒŒ์ผ์— ์ž‘์„ฑ๋œ ์˜์กด์„ฑ์„ ๋ณด๊ณ  ํ™•์ธํ•œ๋‹ค.



Initial Migration

  • App์— ์žˆ๋Š” ํ…Œ์ด๋ธ”์„ ์ ์šฉํ•œ ์ฒซ๋ฒˆ์งธ Version
  • initial = True ๊ฐ€ ์žˆ๋Š” migration ํŒŒ์ผ
  • migrate --fake-initial : Database์— ํ•ด๋‹น ํ…Œ์ด๋ธ”์ด ์žˆ๋‹ค๋ฉด, Fake๋กœ migration์„ ์ง„ํ–‰
  • ํ•ด๋‹น ์ž‘์—…์„ ํ•ด์•ผํ•˜๋Š” ์ด์œ ๋Š” table์„ ์ƒ์„ฑํ•œ ์ดํ›„๋กœ migration์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•˜์–ด๋„, ์ดํ›„ ์ƒˆ๋กœ์šด ๋ณ€๊ฒฝ์„ ์ ์šฉํ•˜๋ ค๋ฉด initialํŒŒ์ผ์ด ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.



Multiple Database

  • Django์—์„œ๋Š” ๊ฐ Database์˜ ์—ญํ• ์— ๋”ฐ๋ผ Multiple Database๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.
  • Database Router์—์„œ ํ•ด๋‹น Database์˜ ์—ญํ• ์„ ์ง€์ •ํ•ด์ค€ ์ดํ›„, ์ง€์ •ํ•œ ์—ญํ• ๋Œ€๋กœ Database๊ฐ€ ์‹คํ–‰๋œ๋‹ค.

์˜ˆ์‹œ

settings.py

settings.py์— ์‚ฌ์šฉํ•  ์—ฌ๋Ÿฌ database specification์„ ์ž‘์„ฑํ•ด ๋†“๋Š”๋‹ค.

DATABASES = {
    'default': {},
    'auth_db': {},
}

Database Router

class MultiDBRouter(object):
	def db_for_read(self, model, **hints):
		if model._meta.app_label == 'auth':
			return 'auth_db'
		return 'default'

	def db_for_write(self, model, **hints):
		if model._meta.app_label == 'auth':
			return 'auth_db'
		return 'default'

	# migrate ์ ์šฉํ•  DB๋ฅผ ์„ ํƒ
    def allow_migrate(self, db, model):
	    if db == 'auth_db':
	        if model._meta.app_label == 'auth':
	            return True
	        else:
	            return False
	    return True

Database Router Settings

DATABASE_ROUTERS = [
    '<router_path>',
]



Custom Migrations

from django.db import migrations, models

# ํ˜„์žฌ ํ•ด๋‹น ๋ชจ๋ธ์— ์žˆ๋Š” ๊ฐ’๋“ค์— ์ ์šฉ
# ์ดํ›„์— ์ƒ์„ฑ๋˜๋Š” ๊ฐ’๋“ค์— ๋Œ€ํ•œ ์ ์šฉ์€ ํ•˜์ง€ ์•Š์Œ
# Django Python2๋Š” migrations ํŒŒ์ผ ์•ˆ์— ์ •์˜ํ•œ ํ•จ์ˆ˜๋“ค์— ๋Œ€ํ•œ ์ง€์›์ด ๋ถ€์กฑ
def combine_names(apps, schema_editor):
    Profile = apps.get_model('app', 'Profile')
    for profile in Profile.objects.all():
        profile.first_name = '%s %s' % (profile.first_name, profile.last_name)
        profile.save()

class Migration(migrations.Migration):
    dependencies = [
        ('app', '0001_initial'),
    ]
    operations = [
        migrations.RunPython(combine_names),
    ]



Squash Migration

  • Migrations ํŒŒ์ผ์ด ๋„ˆ๋ฌด ๋งŽ์„ ๋•Œ, ํ•ด๋‹น ํŒŒ์ผ์„ ์ตœ์ ํ™”ํ•˜์—ฌ ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์ฃผ๋Š” ๊ธฐ๋Šฅ

$ python manage.py squashmigrations app_name migrations_file_name : migrations squash

์ฃผ์˜ : migrate ๋˜์ง€ ์•Š์€ ํŒŒ์ผ์€ squashmigrations์— ํฌํ•จํ•˜์ง€ ์•Š๋Š”๋‹ค.

Squash Migration์„ ์ง„ํ–‰ํ•˜๋ฉด squashํ•œ ํŒŒ์ผ ์ด์™ธ์˜ ๋‹ค๋ฅธ ํŒŒ์ผ๋“ค์€ ์‚ญ์ œํ•ด๋„ ์—๋Ÿฌ๊ฐ€ ๋‚˜์ง€ ์•Š๋Š”๋‹ค.


์ถ”๊ฐ€ : ๊ทธ๋Ÿฐ๋ฐ ์—๋Ÿฌ๊ฐ€ ๋‚ฌ์–ด์š”! :(

migrations ํŒŒ์ผ ์•ˆ์— ์ƒˆ๋กœ์šด ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ–ˆ๋Š”๋ฐ ํ•ด๋‹น ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ๊ณ„์†ํ•ด์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

๋”ฐ๋ผ์„œ ํ•ด๋‹น ํŒŒ์ผ์˜ ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ๊ฐ€์ ธ์™€์„œ suqshํ•œ ํŒŒ์ผ์— ์žฌ์ •์˜ํ–ˆ๋‹ค.


Squash File

from django.db import migrations, models

class Migration(migrations.Migration):
    replaces = [('app', '0001_initial'), ('app', '0002_custom_migrations'), ('app', '0003_profile_name')]
    initial = True
    dependencies = [
    ]
    operations = [
    	# 0001_initial
        migrations.CreateModel( ... ),
        # 0002_custom_migrations
        migrations.RunPython(
            code=app.migrations.0002_custom_migrations.combine_names,
        ),
        # 0003_profile_name
        migrations.AddField(...),
    ]

Edited Squash File

from django.db import migrations, models

def combine_names(apps, schema_editor):
    pass

class Migration(migrations.Migration):
    replaces = [('app', '0001_initial'), ('app', '0002_custom_migrations'), ('app', '0003_profile_name')]
    initial = True
    dependencies = [
    ]
    operations = [
    	# 0001_initial
        migrations.CreateModel( ... ),
        # 0002_custom_migrations
        migrations.RunPython(
            code=combine_names,
        ),
        # 0003_profile_name
        migrations.AddField(...),
    ]



deconstruct

model migrations์„ ์‹คํ–‰ํ•  ๋•Œ ์ž๋™์œผ๋กœ system์— ์˜ํ•ด์„œ ๋‹ค๋ค„์ง€์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ๋“ค์„ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค.

customField ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด, migrations์‹œ ํ•ด๋‹น ํ•„๋“œ๋ฅผ system์ด ์ฝ์ง€ ๋ชปํ•ด ์—๋Ÿฌ๊ฐ€ ๋‚˜๊ฒŒ ๋œ๋‹ค.

๋”ฐ๋ผ์„œ ํ•ด๋‹น ํ•„๋“œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , System์ด ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ Customํ•˜๋Š”๋ฐ deconstruct() ๋Š” Django๊ฐ€ customํ•œ class instance๋ฅผ serializeํ•˜๋Š” ๊ฒƒ์„ ๊ทธ๋งŒ ๋‘๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

์ด ํ•จ์ˆ˜๋Š” ์–ด๋–ค argument๋„ ์—†๊ณ , path, args, kwargs ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

(๋งŒ์•ฝ superclass์— default value๋ฅผ ์„ธํŒ…ํ–ˆ๋‹ค๋ฉด, ์ด default value๋Š” ํ•ญ์ƒ ํฌํ•จ๋˜์–ด์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์•ผํ•œ๋‹ค.)

์˜ˆ๋ฅผ ๋“ค์–ด

class ExclusiveBooleanField(models.BooleanField):
    def __init__(self, on=None, *args, **kwargs):
        if isinstance(on, string_types):
            on = (on, )
        self._on_fields = on or ()
        super(ExclusiveBooleanField, self).__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(
            ExclusiveBooleanField, self).deconstruct()
        if self._on_fields:
            kwargs['on'] = self._on_fields
        return name, path, args, kwargs



Writing database migrations

automic

์›์ž์„ฑ์ด๋ž€, Transaction์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ชจ๋‘ ๋ฐ˜์˜๋˜๋˜๊ฐ€, ์•„๋‹ˆ๋ฉด ์ „ํ˜€ ๋ฐ˜์˜๋˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

Postgresql, SQLite๋Š” ๋ชจ๋‘ DDL Transaction ์„ ์ง€์›ํ•œ๋‹ค.

DDL(Database Definition Language) Transaction ์€ DDL์„ ์ง„ํ–‰ํ•˜๋Š” ๋ช…๋ น์–ด CREATE, ALTER, DROP, RENAME, TRUNCATEฬ€ ์˜ ๋ช…๋ น์–ด๋ฅผ Transactionํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

Transaction์ด๋ž€ ์—ฌ๋Ÿฌ ๋ช…๋ น์–ด๋ฅผ ํ•œ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๊ณ , ๋งŒ์•ฝ ํ•œ๋ฒˆ์— ๋˜์ง€ ๋ชปํ•œ๋‹ค๋ฉด, Rollback์„ ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์˜๋ฏธํ•œ๋‹ค.

DDL Transaction์„ Postgresql์ด ์ง€์›ํ•˜๋Š” ์ด์œ ๋Š” Write-ahead Log ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

Write-Ahaed Log๋ž€ Database์— ์ž‘์„ฑํ•˜๊ธฐ ์ด์ „์— Log๋ฅผ ์ž‘์„ฑํ•ด ๋†“๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

๋‹จ DROP ๊ณผ ๊ฐ™์€ ๊ฒƒ๋“ค์€ Rollback ์ด ๋ถˆ๊ฐ€๋Šฅ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— Transaction์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.

import uuid

from django.db import migrations, transaction

# Model์˜ ๊ฐ’๋“ค์„ Chunk๋กœ ๋‚˜๋ˆ ์„œ update
def gen_uuid(apps, schema_editor):
    MyModel = apps.get_model('myapp', 'MyModel')
    while MyModel.objects.filter(uuid__isnull=True).exists():
        # atomic์„ ์œ ์ง€
        with transaction.atomic():
            for row in MyModel.objects.filter(uuid__isnull=True)[:1000]:
                row.uuid = uuid.uuid4()
                row.save()

class Migration(migrations.Migration):
    # DDL Transaction์„ ์ง€์›Œํ•˜๋Š” ์•Š๋Š” DB์—๋Š” ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค. 
    atomic = False

    operations = [
        migrations.RunPython(gen_uuid),
    ]



runbefore

class Migration(migrations.Migration):
    run_before = [
        ('third_party_app', '0001_do_awesome'),
    ]



Migrating data between third party app

from django.apps import apps as global_apps
from django.db import migrations

def forwards(apps, schema_editor):
    try:
        OldModel = apps.get_model('old_app', 'OldModel')
    except LookupError:
        # The old app isn't installed.
        return

    NewModel = apps.get_model('new_app', 'NewModel')
    NewModel.objects.bulk_create(
        NewModel(new_attribute=old_object.old_attribute)
        for old_object in OldModel.objects.all()
    )

class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(forwards, migrations.RunPython.noop),
    ]
    dependencies = [
        ('myapp', '0123_the_previous_migration'),
        ('new_app', '0001_initial'),
    ]

    if global_apps.is_installed('old_app'):
        dependencies.append(('old_app', '0001_initial'))



Changing an unmanaged model to managed

๊ธฐ๋ณธ์ ์œผ๋กœ True๋กœ ์„ค์ •์ด ๋˜์–ด์žˆ๊ณ , True์˜ ์˜๋ฏธ๋Š” Django๊ฐ€ Database Table์˜ lifecycle์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.

False๋ผ๋ฉด, database tabel ์ƒ์„ฑ ๋˜๋Š” ์‚ญ์ œ๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์—†๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด automatic primary key field๋ฅผ model์— ์ƒ์„ฑํ–ˆ๋Š”๋ฐ, ๋งŒ์•ฝ ์ •์˜ํ•ด ๋†“์ง€ ์•Š์•˜๋‹ค๋ฉด, ๋‹ค์Œ ๊ฐœ๋ฐœ์ž๊ฐ€ ํ—ท๊ฐˆ๋ฆด ๊ฒƒ์„ ํ”ผํ•˜๊ธฐ์œ„ํ•ด, Database์˜ ๋ชจ๋“  Column ๋ช…์€ ์ ์–ด๋†“์ง€๋งŒ ์ ์šฉ์€ ์‹œํ‚ค์ง€ ์•Š๋Š”๋‹ค.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ