(번역) How to Create Django Data Migrations

|


장고를 공부하면서 많은 도움을 받고 있는 simple is better than complexHow to Create Django Data Migrations 번역글입니다. 기분좋게 선뜻 번역을 허락해준 Vitor에게 감사드립니다.


featured-data-migrations (Picture: https://www.pexels.com/photo/administration-articles-bank-black-and-white-261949/)

데이터 마이그레이션은 스키마와 함께 DB의 데이터를 변경하는 편리한 방법이다. 장고는 의존성과 실행순서를 기록하고 주어진 데이터 마이그레이션이 이미 어플리케이션에 적용되었는지 확인한다.

일반적인 데이터 마이그레이션의 사용 케이스는 not-nullable인 새로운 필드를 추가할 때이다. 혹은 캐시된 카운트를 저장하는 필드를 생성하거나 할 때 우리는 새로운 필드를 생성하고 초기 카운트 값을 추가할 수 있다.

이 포스트에서는 당신의 필요를 충족시키기 위해 쉽게 확장하고 수정할 수 있는 간단한 예시를 살펴보려고 한다.


Data Migrations

blog 라는 이름의 앱이 프로젝트의 INSTALLED_APPS에 설정되어 있다고 가정하자. blog 는 아래와 같은 모델을 갖고 있다.

blog/models.py

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=255)
    date = models.DateTimeField(auto_now_add=True)
    content = models.TextField()

    def __str__(self):
        return self.title

어플리케이션은 이미 Post 모델을 사용하고 있으며, 프로덕션에서는 이미 많은 양의 데이터를 보유하고 있다.

id title date content
1 How to Render Django Form Manually 2017-09-26 11:01:20.547000 […]
2 How to Use Celery and RabbitMQ with Django 2017-09-26 11:01:39.251000 […]
3 How to Setup Amazon S3 in a Django Project 2017-09-26 11:01:49.669000 […]
4 How to Configure Mailgun To Send Emails in a Django Project 2017-09-26 11:02:00.131000 […]

자, 이제 slug 라는 이름의 새로운 필드를 추가하고, blog의 새로운 url을 구성하는데 사용한다고 해보자. slug 필드는 unique 하고 not null 인 필드이다.

일반적으로 새로운 필드를 생성할 때는 null=True 혹은 default 값을 함께 추가한다. default 파라미터로 문제를 해결할 수 없다면, 우선은 null=True로 필드를 정의하고, 데이터 마이그레이션을 생성한다. 그리고 null=False 필드 옵션을 추가한 새로운 마이그레이션을 생성한다.

우리는 아래와 같이 해볼 수 있다.

blog/models.py

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=255)
    date = models.DateTimeField(auto_now_add=True)
    content = models.TextField()
    slug = models.SlugField(null=True)

    def __str__(self):
        return self.title

마이그레이션을 생성한다.

python manage.py makemigrations blog

Migrations for 'blog':
  blog/migrations/0002_post_slug.py
    - Add field slug to post

적용한다.

python manage.py migrate blog

Operations to perform:
  Apply all migrations: blog
Running migrations:
  Applying blog.0002_post_slug... OK

이 시점에 데이터베이스는 이미 slug 컬럼을 갖게된다.

id title date content slug
1 How to Render Django Form Manually 2017-09-26 11:01:20.547000 […] (null)
2 How to Use Celery and RabbitMQ with Django 2017-09-26 11:01:39.251000 […] (null)
3 How to Setup Amazon S3 in a Django Project 2017-09-26 11:01:49.669000 […] (null)
4 How to Configure Mailgun To Send Emails in a Django Project 2017-09-26 11:02:00.131000 […] (null)

아래 명령어로 empty 마이그레이션을 생성한다.

python manage.py makemigrations blog --empty

Migrations for 'blog':
  blog/migrations/0003_auto_20170926_1105.py

이제 blog/migrations/0003_auto_20170926_1105.py 파일을 확인하면 아래와 같은 내용을 포함하고 있다.

blog/migrations/0003_auto_20170926_1105.py

# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-26 11:05
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('blog', '0002_post_slug'),
    ]

    operations = [
    ]

이 파일에 RunPython 명령어로 실행되는 함수를 생성할 수 있다.

blog/migrations/0003_auto_20170926_1105.py

# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-26 11:05
from __future__ import unicode_literals

from django.db import migrations
from django.utils.text import slugify


def slugify_title(apps, schema_editor):
    '''
    We can't import the Post model directly as it may be a newer
    version than this migration expects. We use the historical version.
    '''
    Post = apps.get_model('blog', 'Post')
    for post in Post.objects.all():
        post.slug = slugify(post.title)
        post.save()


class Migration(migrations.Migration):

    dependencies = [
        ('blog', '0002_post_slug'),
    ]

    operations = [
        migrations.RunPython(slugify_title),
    ]

위의 예시에서 우리는 slugify 유틸리티 함수를 사용하며 이는 string을 파라미터로 받고 이를 slug로 변환한다. 아래의 예를 살펴보자.


from django.utils.text import slugify

slugify('Hello, World!')
'hello-world'

slugify('How to Extend the Django User Model')
'how-to-extend-the-django-user-model'

어쨌든 데이터 마이그레이션을 생성하기 위한 RunPython 메소드에 의해서 사용된 이 함수는 apps, schema_editor 2가지 파라미터를 받는다. RunPython은 해당 파라미터를 제공한다. 또한 apps.get_model('app_name', 'model_name') 메소드를 사용하여 모델을 import 하는 것을 기억하자.

일반적인 모델 마이그레이션과 동일하게 파일을 저장하고 마이그레이션을 실행한다.

python manage.py migrate blog
Operations to perform:
  Apply all migrations: blog
Running migrations:
  Applying blog.0003_auto_20170926_1105... OK

이제 데이터베이스를 확인하면

id title date content slug
1 How to Render Django Form Manually 2017-09-26 11:01:20.547000 […] how-to-render-django-form-manually
2 How to Use Celery and RabbitMQ with Django 2017-09-26 11:01:39.251000 […] how-to-use-celery-and-rabbitmq-with-django
3 How to Setup Amazon S3 in a Django Project 2017-09-26 11:01:49.669000 […] how-to-setup-amazon-s3-in-a-django-project
4 How to Configure Mailgun To Send Emails in a Django Project 2017-09-26 11:02:00.131000 […] how-to-configure-mailgun-to-send-emails-in-a-django-project

모든 POST 엔트리가 값을 갖고 있고, 이제 안전하게 필드옵션을 null=True에서 null=False로 변경할 수 있다. 그리고 모든 값이 unique 하기 때문에 unique=True flag를 추가할 수 있다.

모델을 변경하자

blog/models.py

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=255)
    date = models.DateTimeField(auto_now_add=True)
    content = models.TextField()
    slug = models.SlugField(null=False, unique=True)

    def __str__(self):
        return self.title

마이그레이션 생성

python manage.py makemigrations blog

이번에는 아래와 같은 메시지가 나온다.

You are trying to change the nullable field 'slug' on post to non-nullable without a default; we can't do that
(the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now, and let me handle existing rows with NULL myself (e.g. because you added a RunPython or RunSQL
 operation to handle NULL values in a previous data migration)
 3) Quit, and let me add a default in models.py
Select an option:

옵션 2를 선택하자

Migrations for 'blog':
  blog/migrations/0004_auto_20170926_1422.py
    - Alter field slug on post

이제 안전하게 마이그레이션을 적용할 수 있다.

python manage.py migrate blog
Operations to perform:
  Apply all migrations: blog
Running migrations:
  Applying blog.0004_auto_20170926_1422... OK

결론

데이터 마이그레이션은 종종 까다로운 일이다. 당신의 프로젝트에 데이터 마이그레이션을 생성할 때, 항상 프로덕션의 데이터를 먼저 검사해야한다. 내가 예시에서 사용한 slugify_title 은 약간 나이브한 편이다. 왜냐하면 큰 데이터에서는 중복된 타이틀이 생성될 수 있기 때문이다. 데이터 마이그레이션을 스테이지 환경에서 먼저 테스트 함으로써 프로덕션에서의 문제를 피할 수 있다.

당신이 도입하려는 변경사항이 잘 통제되고 있다고 느끼려면 step-by-step으로 진행하는 것이 중요하다. 간단한 데이터 마이그레이션을 위해서 내가 생성한 3개의 마이그레이션 파일을 참고하자.

보시다시피 이러한 마이그레이션을 만드는 것은 쉬운 편이고 매우 유연하다. 당신은 외부의 텍스트 파일을 로드해서 새로운 열에 데이터를 추가할 수도 있을 것이다.

이 블로그 포스팅에 사용된 소스코드는 Github에서 확인할 수 있다. https://github.com/sibtc/data-migrations-example