본문 바로가기

Computer Engineering/Django

같은 Django model class에서 다른 schema가?(feat. Django는 이중인격?)

Django!

 

(Update) Django에 해당 bug를 repoorting하고, PR을 올려서 merge되었습니다! https://github.com/django/django/pull/13982

 

Fixed #32425 -- Fixed adding nullable field with default on MySQL. by baidoosik · Pull Request #13982 · django/django

ticket link: https://code.djangoproject.com/ticket/32425#ticket Hi, team. I wanna fix some picky issue. I suggest below code for solving this problem. (same code creates different schemas) but It h...

github.com

Django Migration파일으로 table schema를 변경할 때 Default Value와 관련해서 주의 해야 할 사항이 있다

똑같은 Django model class의 정의지만 create table로 적용하냐 alter table로 적용하냐에 따라서 차이가 난다.

이 부분을 정확히 이해를 못하고 column을 변경하거나 delete하면 큰 incident가 발생할 수 있다.

 

Django Migration파일으로 table schema를 변경할 때 Default Value와 관련해서 주의 해야 할 사항이 있다

(이렇게 다르게 동작하는 부분을 django가 의도하고 구현 것인지는 확인하지 못 했다.)

 

바로 코드로 확인해보자. (글쓰기도 잘하고 싶지만 저보다는 코드가 설명을 잘하는 것 같습니다ㅠ.ㅠ)

 

1) Default column made by Create table

먼저 create table로 default 값을 가진 column을 생성해보자.

 

다음과 같은 코드가 있다.

class PhoneBook(models.Model):
    name = models.CharField(max_length=32, null=True, blank=True, default='jordan')
    phone_number = models.CharField(max_length=32, null=True, blank=True)

 

위의 코드는 아래와 같은 migrations 파일을 만든다.

operations = [
        migrations.CreateModel(
            name='PhoneBook',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(blank=True, default='jordan', max_length=32, null=True)),
                ('phone_number', models.CharField(blank=True, max_length=32, null=True)),
            ],
        ),
]

 

 

Migrate하면 아래와 같은 SQL이 실행된다.

CREATE TABLE `main_phonebook` (
	`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
	`name` varchar(32) NULL,
	`phone_number` varchar(32) NULL
)

혹시, migrate 명령어 실행시 어떤 SQL들이 실행되는지 궁금하면 django/db/backend/utils.py에 execute에 넘어오는 sql을 print하면 된다.

def execute(self, sql, params=None):
				# 실행되는 모든 sql을 만드는 sql문과 params를 출력
        print(sql, params)
        return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)

 

→여기서 주목 할 점은 두 가지이다.

1.default column은 db에 적용되는것이 아니라 django level에서 처리하는 default value이다. 여기까지는 대부분 많이 알고 계시고, 헷갈리지 않는다.

2.db level의 name column의 default value는 NULL이다. 이 부분이 alter로 만들었을 때 차이가 나는 부분이기 때문에 기억해두길 바란다. (중요)

 

2) Default column made by Alter table 

이제 alter table로 colum이 만들어졌을 때를 살펴보자.

alter table이 적용되는 상황을 위해서 처음에 model class를 아래와 같이 정의한 후에 name 칼럼을 추가해보자.

class PhoneBook2(models.Model):
    phone_number = models.CharField(max_length=32, null=True, blank=True)

 

create table 과정은 생략하고, 바로 name 칼럼을 추가하는 과정으로 넘어가보자.

class PhoneBook2(models.Model):
    name = models.CharField(max_length=32, null=True, blank=True, default='jordan')
    phone_number = models.CharField(max_length=32, null=True, blank=True)

 

위와 같이 column을 추가하면 아래와 같은 migrations 파일이 생성된다.

operations = [
        migrations.AddField(
            model_name='phonebook2',
            name='name',
            field=models.CharField(blank=True, default='jordan', max_length=32, null=True),
        ),
    ]

 

해당 migrations 파일을 실행시키면 아래와 같은 SQL이 실행된다.

ALTER TABLE `main_phonebook2` ADD COLUMN `name` varchar(32) DEFAULT %s NULL ['jordan']
ALTER TABLE `main_phonebook2` ALTER COLUMN `name` DROP DEFAULT []

 

default값에 jordan을 넣고, 다시 default value를 drop 한다.

drop을 하면서 default value에는 아무것도 들어가지 않게된다. 이 부분이 차이를 만들게 된다.

 

결과적으로 SHOW CREAETE TABLE 하면 다음과 같은 DDL이 나온다.

CREATE TABLE `main_phonebook2` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `phone_number` varchar(32) DEFAULT NULL,
  `name` varchar(32),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

 

이제 위에 create table로 만들어진 table과 비교해보자.

#create table로 만들어진 TALBE SCHEMA

CREATE TABLE `main_phonebook` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `phone_number` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;4


# 같은 코드지만 alter table로 필드를 추가해 만든 TABLE SCHMEA
CREATE TABLE `main_phonebook2` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `phone_number` varchar(32) DEFAULT NULL,
  `name` varchar(32),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

 

분명 같은 코드인데 name column에 대해서 다른 정의를 가지고 있다.....왜.......

위에 테이블은 name column의 default value가 NULL이다.

아래 테이블의 name column의 default value는 지정되어 있지 않다.

 

즉, 아래 테이블에서 column에 값을 주지 않으면 에러가 발생할 것 이다.

insert into main_phonebook (`phone_number`) values ('010-1234-1234')
Query 1 OK: 1 row affected

insert into main_phonebook2 (`phone_number`) values ('010-1234-1234')
Query 1 ERROR: Field 'name' doesn't have a default value

 

같은 쿼리에 대해서 다른 결과가 나오게 된다.

ORM을 사용하면 django level에서 default value를 주니깐 상관없다고 생각할 수 있겠지만, 필드를 더 이상 사용하지 않아서 지우려고 할 때 곤혹을 겪게 될 수 있다.

해당 column을 더 이상 사용하지 않아서 column을 먼저 아래와 같이 주석 처리하고, table을 online migration해서 지우려고 한다고 해보자.

class PhoneBook(models.Model):
    # name = models.CharField(max_length=32, null=True, blank=True, default='jordan')
    phone_number = models.CharField(max_length=32, null=True, blank=True)


class PhoneBook2(models.Model):
    # name = models.CharField(max_length=32, null=True, blank=True, default='jordan')
    phone_number = models.CharField(max_length=32, null=True, blank=True)

우리는 일반적으로 object.save()라는 코드를 가지고 있을 것 이다.

In [2]: p=PhoneBook(phone_number='010-1234-1234')                         

In [3]: p.save()                                                          
INSERT INTO `main_phonebook` (`phone_number`) VALUES (%s) ['010-1234-1234']

In [4]: p2=PhoneBook2(phone_number='010-1234-1234')                       

In [5]: p2.save()                                                         
INSERT INTO `main_phonebook2` (`phone_number`) VALUES (%s) ['010-1234-1234']

!!!!!!!!아래와 같은 에러가 발생한다!!!!!!!!!!!!!!!
IntegrityError: (1364, "Field 'name' doesn't have a default value")

 

분명 코드는 같은 코드인데 만들어진 table의 정의가 다르고, 이에 따라 동작도 다르다.

class PhoneBook(models.Model):
    name = models.CharField(max_length=32, null=True, blank=True, default='jordan')
    phone_number = models.CharField(max_length=32, null=True, blank=True)


class PhoneBook2(models.Model):
    name = models.CharField(max_length=32, null=True, blank=True, default='jordan')
    phone_number = models.CharField(max_length=32, null=True, blank=True)

 

어떻게 그럼 관리할 것이냐? 그건 팀이 상의해서 결정할 내역이다. 하지만, Django를 사용하는 회사의 DBA, 버 개발자들은 이 점을 주의하고 처음 부터 table에 alter로 field를 add할 때 pt-online-schema-change를 이용하거나(DBA 있는 회사들은 보통 table 사이즈가 일정 이상이기 때문에)  아니면 column을 변경할 때 같은 코드에도 schema가 다르다는 것을 알고 변경할 때 체크한 후에 변경할 수 있다.

 

 

이 글이 어떤 한 명의 엔지니어가 incident를 피하는데 도움이 되면 좋겠네요!

혹시, 제가 잘 못 알고 있는 부분이 있거나 제가 작성한 글에 문제가 있다면 알려주세요. 

도움이 되셔따면 공감이나 따뜻한 커피 한 잔 보내주세요!

 

이 글은 Django==3.0, MySQL5.6 기준으로 작성되었습니다.

 

 

 

 

 

 

 

 

 

태그

  • kj 2021.01.27 21:47

    2) Default column made by Alter table

    이 부분에서 name 칼럼을 추가하기 위해서 작성된 코드는 주석이 해제 되어야 하는게 아닌가요? 주석을 해제하고

    python manage.py makemigrations > python manage.py migrate 를 한다는 뜻인가요?

    • jordan 2021.01.27 22:33

      네 오타가 맞습니다. 주석해제 해놨습니다. 주석이 해제 된 상태에서 python manage.py makemigrations <app_name> 을 하면 해당 app에 database와 다른 부분이 있으면 migrations 파일이 만들어질텐데 그 부분이 제가 적어놓은 migrations 파일입니다. 그리고
      python manage.py migrate를 하면 실제 DB에는 제가 적어놓은 SQL이 실행됩니다.

  • kj 2021.01.28 15:09

    댓글 감사합니다!

  • kj 2021.01.28 15:14

    def execute(self, sql, params=None):
    # 실행되는 모든 sql을 만드는 sql문과 params를 출력
    print(sql, params)
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)

    이 부분을 활용하려면 이 함수를 manage.py 에 써야 하나요?

  • kj 2021.01.28 21:48

    @jordan_base jordan17 답변 정말 감사드립니다. 제가 아직 실력이 많이 부족해서 근데 말씀해주신 내용은 제가 pip install django 를 하는데 ㅜ 어떻게 저렇게 추가를 하나요? import 해서 재정의 해야하나요?

    • jordan_bae jordan17 2021.01.29 00:03 신고

      사용하시는 파이썬에 설치되어 있는 django경로를 사용하시면 됩니다. 예를 들어, 가상환경으로 만드신 파이썬 경로가 있으시면 그 안에 장고가 있을텐데 거기에 있는데요. 가상환경 이름을 venv로 만드셨다면 venv/lib/pythonx.x/site-packages/django/db/backends/utils.py 에 있습니다

  • kj 2021.01.31 14:34

    감사합니다! 많은 도움이 되었습니다!