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

(번역) 다운타임 없는 장고 마이그레이션

|

Django Migrations without Downtime 번역글입니다.

아래의 지침은 장고 마이그레이션을 수행할 때 실제 운영중인 웹 서비스가 다운되지 않도록 하기 위한 절차이다. 이는 명시적으로 수동으로 수행되며, 자동 배포 프로세스의 일부가 아님을 밝혀둔다.

필드 혹은 테이블 추가

null을 허용하는 필드 혹은 새로운 테이블 추가

  1. 새로운 모델 혹은 컬럼을 추가한다.
  2. 로컬에서 모델 마이그레이션을 수행한다.
  3. 2개의 분리된 풀리퀘스트를 만든다. 하나는 모델 변경사항을 포함하고, 다른 하나는 새롭게 수행된 마이그레이션을 포함한다. 모델 변경 마이그레이션은 오직 모델 변경 코드만 포함해야하며, 변경한 모델을 활용한 다른 추가적인 코드를 포함해서는 안된다.
  4. 마이그레이션 풀리퀘스트를 머지하고 배포한다.
  5. 프로덕션 DB에서 새로운 마이그레이션을 수행한다. 프로덕션 모델 코드가 거기에 있지 않을 것으로 기대하지 않는다면, 추가되는 필드는 프로덕션 DB에서 문제를 일으키지 않을 것이다.
  6. 모델변경 풀리퀘스트를 머지한다.
  7. 모델변경사항을 포함한 코드를 배포한다. 이전에 작성한 컬럼/테이블에서 심리스하게 읽기 작업을 수행한다.

NOT NULL 필드 추가

null을 허용하지 않는 필드를 추가하기 위해서 위의 절차를 먼저 수행해야한다. 그리고 아래의 추가적인 작업을 수행한다.

  1. null=False로 모델을 수정한다. 또한 당신이 NOT NULL로 변경하는 필드에 절대로 Node을 추가하거나 업데이트하지 않도록 한다.
  2. 변경사항에 대해서 마이그레이션을 수행한다.
  3. 2개의 분리된 풀리퀘스트를 만든다. 하나는 모델 변경을 위해서, 다른 하나는 마이그레이션을 위한 것이다.
  4. 모델 변경 풀리퀘스트를 머지하고 배포한다.
  5. 마이그레이션 풀리퀘스트를 머지한다.
  6. 프로덕션 DB에서 마이그레이션을 수행한다.

아주 큰 사이즈의 테이블에 NOT NULL 컬럼을 추가할 때

장고 마이그레이션은 NOT NULL 필드를 추가할 때 디폴트 값을 추가하도록 강요할 것이다. 해당 값은 마이그레이션을 수행시 빈 컬럼을 채우기 위해서 사용된다. 이와 같은 업데이트 절차는 마이그레이션이 끝날 때까지 postgres 테이블이 쓰기 작업을 수행하지 못하도록 잠근다. 비교적 작은 사이즈의 테이블 (10만개 row 이하)은 괜찮다. 하지만 아주 큰 테이블은 문제가 될 수 있다. 왜냐하면 테이블락은 마이그레이션이 완료되는 동안 요청을 실행하는 것을 막을 수 있기 때문이다. 이러한 이유로 특히 큰 테이블에는 새로운 필드를 null을 허용하도록 하는 것을 검토해 볼 수 있다. 이는 어플리케이션 코드에서는 비용이 들지만, 요청에 응답하지 못하게 되는 것보다는 더 나을 것이다.


필드 혹은 테이블 삭제

null을 허용하는 필드 혹은 테이블 삭제

본질적으로 추가 작업과 동일하지만 순서가 약간 다르다.

  1. 어플리케이션 코드에서 삭제할 모델 혹은 컬럼을 사용하는 부분을 삭제한다.
  2. 삭제할 모델/컬럼이 남아 있지만, 코드에서 해당 부분을 사용하는 곳이 없는 버전을 먼저 배포한다.
  3. 너의 어플리케이션 코드에서 해당 모델 혹은 컬럼을 삭제한다.
  4. 삭제 마이그레이션을 수행한다.
  5. 모델 수정 풀리퀘스트를 머지한다.
  6. 모델 변경사항이 포함된 코드를 배포한다. 삭제될 부분을 참고함으로써 발생하는 에러가 없는지 확인한다.
  7. 마이그레이션 풀리퀘스트를 머지한다.
  8. 마이그레이션이 포함된 코드를 배포한다.
  9. 프로덕션 DB에서 명시적으로 바이그레이션을 수행한다.

NOT NULL 필드 삭제

NOT NULL 필드를 삭제하기 위해서 당신은 먼저 null을 허용하는 필드로 마이그레이션을 해아한다. 그리고 위의 null을 허용하는 필드를 삭제할 때와 같은 절차를 수행한다. NOT NULL 필드를 null을 허용하는 필드로 변경하는 절차는 아래와 같다.

  1. 필드 null=True로 변경한다.
  2. 변경사항 적용을 위해서 마이그레이션을 수행한다.
  3. 2개의 분리된 풀리퀘스트를 만든다. 하나는 모델 변경을 위해서, 다른 하나는 마이그레이션을 위한 것이다.
  4. 마이그레이션 풀리퀘스트를 머지하고 배포한다.
  5. 프로덕션 DB에서 마이그레이션을 수행한다.
  6. 모델 변경 코드를 머지하고 배포한다.

위 작업을 수행한 이후에 null을 허용하는 필드를 삭제할 때와 같은 절차를 수행한다.

(번역) Django Tips #8 Blank or Null?

|


장고를 공부하면서 많은 도움을 받고 있는 simple is better than complexDjango Tips #8 Blank or Null? 번역글입니다. 기분좋게 선뜻 번역을 허락해준 Vitor에게 감사드립니다.


featured-post-image

장고 모델 API는 많은 개발자들이 헷갈려하는 nullblank 라는 2가지 비슷한 옵션을 제공한다. 내가 처음 장고를 시작했을 때, 나는 둘의 차이점에 대해서 설명할 수 없었고 항상 둘 다 사용하곤 했다. 그리고 때때로 부적절하게 사용하기도 했다.

이름에서 알 수 있는 것 처럼 이 두가지는 거의 비슷한 역할을 하지만 차이점도 있다.

  • Null : DB와 관련되어 있다. (database-related) 주어진 데이터베이스 컬럼이 null 값을 가질 것인지 아닌지를 정의한다.
  • Blank : 유효성과 관련되어 있다. (validation-related) form.is_valid()가 호출될 때 폼 유효성 검사에 사용된다.

그러므로 즉, null=True, blank=False 옵션을 가진 필드를 정의하는 것에는 문제가 없다. 이는 DB레벨에서는 해당 필드가 NULL 될 수 있지만, application 레벨에서는 required 필드인 것을 의미한다.

자, 개발자들이 가장 실수하는 부분은 CharField, TextField와 같은 문자열 기반 필드에 null=True를 정의하는 것이다. 이 같은 실수를 피해야한다. 그렇지 않으면 “데이터 없음”에 대해 두 가지 값, 즉 None빈 문자열 을 갖게된다. “데이터 없음”에 대해 두 가지 값을 갖는 것은 중복이다. 그리고 Null이 아닌 빈 문자열을 사용하는 것이 장고 컨벤션이다.

따라서 만약 문자열 기반 모델 필드를 “nullable” 하게 만들고 싶다면 다음과 같이 하자.

class Person(models.Model):
  name = models.CharField(max_length=255)  # 필수
  bio = models.TextField(max_length=500, blank=True)  # 선택 (null=True를 넣지 말자)
  birth_date = models.DateField(null=True, blank=True)  # 선택 (여기서는 null=True를 넣을 수 있다.)

null과 blank 옵션의 티폴트 값은 False 이다.

또한 특별한 케이스가 있는데, 만약 BooleanField에 NULL 값을 받고 싶다면, NullBooleanField를 대신 사용하자.

2017년 09월 TIL

|

9월 11일 - 9월 17일

학습

  • django migration 파일을 직접 수정해서 원하는 sql문으로 변경해보았다. 그동안은 makemigrations의 결과물을 그대로 migrate 했었는데, 필드type을 변경하면 기존의 레코드가 삭제되는 문제가 있었다. 이를 방지하기 위해서 migrations 폴더의 파일을 열어서 Migration 클래스의 operations 필드에 AlterField, RenameField를 적용하여 문제를 수정할 수 있었다.
    • 장고문서-Migration Operations
    • python manage.py showmigrations : migrate 현황 확인
    • python manage.py sqlmigrate : 해당 파일의 Migration 클래스가 생성하는 sql문을 확인
  • EC2(Amazon Elastic Compute Cloud) Linux 인스턴스를 만들고 SSH를 사용해서 연결해보았다.
    • chmod를 사용해서 프라이빗 키 파일(.pem) 권한을 수정
    • 인스턴스에 직접 zsh, 가상환경, django를 설치
    • SCP(Secure Copy)를 사용해서 로컬 컴퓨터 파일을 인스턴스에 전송

출퇴근 지하철

  • 출퇴근에 3시간 정도가 걸리는데 지하철에서 보내는 시간만 2시간이다. 이 시간을 잘 보내고 싶어서 앞으로 뭘 했는지 기록해보려고 한다.
  • 생활코딩 AWS 수업을 듣고 있다. EC2 인스턴스 생성, SSH를 통한 원격 접속, chomod를 활용한 key 파일 권한변경 등을 배웠는데 회사에서 초기에 개발환경 세팅 했을때 왜 하는건지 몰랐던 것들의 의문이 아주 조금 풀렸다.

9월 18일 - 9월 24일

출퇴근 지하철

학습

  • TDD 염소책을 읽기 시작했다. 속도가 느려서 한참 걸릴 것 같지만 조금이라도 읽은 것에 의미를 두기로 하자.
  • React가 어떤건지 궁금해서 시작했는데 마크업 요소(?)들이 컴포넌트로 모듈화 되어 있는 것이 신기했다.
  • simple is better than complex 블로그의 글쓴이에게 글을 번역해도 될지 물어보았는데 기분좋게 허락해주었다. 앞으로 조금씩 번역해서 올려보려고 한다.
  • sql 기본 문법을 다시 살펴보았다.

다음주 목표

  • docker를 사용한 배포
  • React 기초
  • 생활코딩 AWS
  • TDD 염소책 실습

9월 25일 - 10월 1일

출퇴근 지하철

  • 생활코딩 AWS 영상을 보고있다.
    • Auto Scaling[http://docs.aws.amazon.com/ko_kr/autoscaling/latest/userguide/WhatIsAutoScaling.html]를 활용하여 일정 조건을 충족하면 자동으로 EC2 인스턴스를 추가하고 삭제
    • ab 테스트를 통해서 부하를 전달하고 top을 통해서 서버부하를 확인, Auto scaling 동작 확인
  • 초보를 위한 도커 안내서 자료를 읽었다.
  • DTI(총부채상환비율)에 대한 글을 읽었다.

학습

  • 두숟갈스터디
    • Two Scoops of Django 18-20장 내용을 공부했다.
    • 18장 장고 코어 모듈을 교체할 때 주의점
    • 19장 장고 어드민 이용하기
    • 20장 장고의 사용자 모델 다루기
  • React 기초를 공부하고 튜토리얼을 따라서 간단한 리엑트 페이지를 만들어보았다. API를 통해서 받은 자원을 프론트엔드에서 어떻게 표현하는지 아주 간단하게라도 이해할 수 있어서 좋았다.
    • stateless functional components
    • component state, setState
    • PropTypes
    • async, await
  • AWS, 독커를 사용한 배포를 공부하고 있는데 설정이 복잡해서 몇번씩 시도만 하고 중간에 멈추길 반복하고 있다. 방향을 바꿔서 아주 간단한 것부터 다시 해봐야지.

Django - Slacker를 활용하여 간단하게 슬랙 봇 메시지 보내기

|


하고 싶었던 것

도서관리 사이트에서 신규도서가 등록되거나, 연체도서가 발생하는 등의 이벤트가 있었을때 슬랙으로 알람을 보내는 기능을 추가하고 싶었다.

views
새책을 등록하면 슬랙으로 이런 메시지를 보내고 싶었다.


찾아보니 slack api 공식 사이트에서 추천하는 slacker 라는 오픈소스 라이브러리를 사용해서 간단하게 bot으로 원하는 채널에 메시지를 보낼 수 있었다.


준비과정

  • slack api 사이트에서 build 선택 slack1
  • create app 선택 slack2
  • Bot Users 메뉴에서 Add Bot User 선택 slack3
  • OAuth & Permissions 에서 Install App to Team 선택 slack4
  • 생성된 Token(Bot User OAuth Access Token)을 확인하고 복사해두기 slack5

코드

  • 여러 앱에서 범용적으로 사용할 수 있도록, utils 폴더의 slack.py 파일에 아래와 같은 코드를 작성했다.
  • token 값은 공개저장소에 노출되지 않도록 주의해야한다.
  • 비밀 값을 설정 파일에서 분리하는 방법은 다른 포스팅 (Django - settings.py 의 SECRET_KEY 변경 및 분리하기)에 정리해보았다.
# utils/slack.py
from slacker import Slacker

def slack_notify(text=None, channel='#test', username='알림봇', attachments=None):
    token = 'xoxb-235어쩌구-저쩌구-발급받은토큰을여기에' #토근값은 공개저장소에 공개되지 않도록 주의
    slack = Slacker(token)
    slack.chat.post_message(text=text, channel=channel, username=username, attachments=attachments)
  • 위의 slack_notify 함수를 views 에서 활용하는 예시는 아래와 같다.
  • 신규 도서가 저장되면 슬랙메시지를 발송한다.
# views.py
from utils.slack import slack_notify

@require_POST
def register_save(request):
    isbn = request.POST['isbn']
    message, book = register_book(isbn)
    messages.info(request, message)
    if book:
        slack_message = "*[신규도서]* {}".format(book.title)
        slack_notify(slack_message, '#new_book', username='새책 알림봇')
    return redirect('books:register')
  • 위 코드에서는 Slacker 활용 예시를 참고해서 단순 메시지만 보내도록 했지만, attachments등을 활용하면 더 화려하게 메시지를 보낼 수 있다.

screen 6

# attachments 활용예시
attachments = [{
    "color": "#36a64f",
    "title": "신규도서",
    "title_link": "http://127.0.0.1:7000/books/list/",
    "fallback": "신규도서 알림",
    "text": "{}".format(book.title)
}]
slack_notify(channel='#new_book', username='새책 알림봇', attachments=attachments)