Two Scoops of Django 3.x를 읽고 정리한 글입니다.
각 앱이 가진 모델의 수가 다섯 개를 넘지 않아야 한다는게 우리의 의견이다.
장소는 세 가지 모델 상속 방법을 제공한다.
모델의 상속 스타일 | 장점 | 단점 |
---|---|---|
상속을 이용하지 않는 경우: 모델들 사이에 공통 필드가 존재할 경우, 두 모델에 전부 해당 필드를 만들어 준다. | 데이터베이스 테이블에 어떤 식으로 매핑되는지 상관없이 장고 모델을 한눈에 이해하기 쉽게 구성된다. | 모델들 사이에 서로 중복되는 테이블이 많을 경우 이를 지속적으로 관리하는 데 어려움이 따른다. |
추상화 기초 클래스: 오직 상속받아 생성된 모델들의 테이블만 생성된다. | 추상화된 클래스에 공통적인 부분을 추려놓음으로써 한 번만 타이핑을 하면 된다. 추가 테이블이 생성되지 않고 여러 테이블에 걸쳐 조인을 함으로써 발생하는 성능 저하도 없다. | 부모 클래스를 독립적으로 이용할 수 없다. |
멀티 테이블 상속: 부모와 자식 모델에 대해서도 모두 테이블이 생성된다. OneToOneField는 부모와 자식 간에 적용된다. | 각 모델에 대해 매칭되는 테이블이 생성된다. 따라서 부모 또는 자식 모델 어디로든지 쿼리를 할 수 있다. 부모 객체로부터 자식 객체를 호출하는 것이 가능하다: parent.child | 자식 테이블에 대한 각 쿼리에 대해 부모 테이블로의 조인이 필요하므로 이에 따른 상당한 부하가 발생한다. 멀티테이블 상속을 이용하지 않기를 권한다. |
프락시 모델: 원래 모델에 대해서만 테이블이 생성된다. | 각기 다른 파이썬 작용(behavior)을 하는 모델들의 별칭을 가질 수 있다. | 모델의 필드를 변경할 수 없다. |
데이터베이스를 설계한다고 생각해보자.
설계해보니까 모든 테이블에 생성시간(created), 수정시간(modified)이 필요하다.
어떻게 해야할까? 모든 모델에 created, modified를 추가하면 된다. 하지만 Django의 추상화 기초 클래스를 활용하면 더 간결해진다.
TimeStampedModel이라는 추상화 기초 클래스를 정의한다. 이 모델을 상속한 모델은 DB에서도 동일한 필드가 추가된다.
# core/models.py
from django.db import models
class TimeStampedModel(models.Model):
"""
'created'와 'modified'필드를 자동으로 업데이트해 주는 추상화 기반 클래스 모델
"""
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
abstract = True # 추상화 기초 클래스로 선언하는 코드
# 추상화 기초클래스로 선언되면 마이그레이션을 실행할 때 core_timestampmodel테이블이 생성되지 않는다.
# flavors/models.py
from django.db import models
from core.models import TimeStampedModel
class Flavor(TimeStampedModel):
"""
추상화 기초클래스인 TimeStampedModel를 상속하였다.
TimeStampedModel의 'created', 'modified'필드가 Flavor모델에 포함된다.
"""
title = models.CharField(max_length=200)
추상화 기초클래스인 TimeStampedModel은 마이그레이션 시 테이블이 생성되지 않고, Flavor테이블만 생성된다. 또한 Flavor모델은 TimeStampedModel이라는 추상화 기초 클래스를 상속하였으므로 created/modified가 포함되어 테이블이 생성된다.
Migrations are Django’s way of propagating changes you make to your models (adding a field, deleting a model, etc.) into your database schema. They’re designed to be mostly automatic, but you’ll need to know when to make migrations, when to run them, and the common problems you might run into.
- django documentation
마이그레이션은 모델에 대한 변경 사항을 데이터베이스 스키마에 적용하는 것이다.
장고는 django.db.migrations
라는 강력한 데이터베이스 마이그레이션(migration) 도구를 제공한다.
python manage.py makemigrations
python manage.py sqlmigrate
django.db.migrations는 데이터와 상호 작용하는 외부 구성 요소에대한 복잡한 변경에 대한 처리가 불가능하다.
이는 마이그레이션 실행을 지원하기 위해 파이썬 또는 SQL을 어떻게 작성해야 하는지 조사할 필요성이 생길 수 있다는 뜻이다. (저자는 RunPython을 추천하고 있다.)
프로덕션 환경에서 프로젝트가 어느정도 궤도에 오르면 RunPython 또는 RunSQL클래스를 사용해야 할 시점이 올 것이다.
custom model manager에서도 레코드를 필터링, 제외, 생성 또는 수정이 가능하다. 그러나 이러한 작업이 기본 migrations명령에서는 제외되며, use_in_migrations = True
를 추가해서 재정의하면 된다. (예제)
django.db.migrations의 모델 직렬화 방식 구조상 마이그레이션 중에는 호출이 불가능하다. (설명)
모델의 저장 및 삭제 메서드를 재정의하면 RunPython에서 호출할 때 호출되지 않으므로 치명적인 문제가 발생할 수 있다.
다음과 같이 RunPython을 활용해 add_cones작업을 수행하였고 추후에 역 마이그레이션이 동작할 수 있게 하려면 RunPython의 reverse_code 파라미터를 이용하라. (ex. migrations.RunPython(add_cones, reverse_code=migrations.RunPython.noop
) - link
from django.db import migrations, models
def add_cones(apps, schema_editor):
Scoop = apps.get_model('scoop', 'Scoop')
Cone = apps.get_model('cone', 'Cone')
for scoop in Scoop.objects.all():
Cone.objects.create(
scoop=scoop,
style='sugar'
)
class Migration(migrations.Migration):
initial = True
dependencies = [
('scoop', '0051_auto_20670724'),
]
operations = [
migrations.CreateModel(
name='Cone',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('style', models.CharField(max_length=10), choices=[('sugar', 'Sugar'), ('waffle', , 'Waffle')]),
('scoop', models.OneToOneField(null=True, to='scoop.Scoop', on_delete=django.db.models.deletion.SET_NULL,)),
],
),
# RunPython.noop does nothing but allows reverse migrations to occur
migrations.RunPython(add_cones, reverse_code=migrations.RunPython.noop)
]
좋은 장고 모델을 디자인 하는 방법을 위한 전략은 다음과 같다.
비정규화를 최대한 피하고 캐시를 세팅하자. 상당부분 비정규화로 인한 문제를 해결해주기도 한다. (챕터26의 Finding and Reducing Bottlenecks에서 자세히 다룬다.)
class Person(models.Model):
name1 = models.CharField(max_length=255) # 폼에서 빈값으로 제출하면 허용하지 않는다.
name2 = models.CharField(max_length=255, blank=True) # 폼에서 빈값으로 제출하면 DB에 빈 문자열로 저장된다.
name3 = models.CharField(max_length=255, null=True, blank=True) # 폼에서 빈값으로 제출하면 DB에 null로 저장된다.
다음 문제가 야기될 수 있다.
모델 필드에 선택 항목을 추가할 수 있다. 튜플을 이용해 구조를 정의한다. (튜플 기반 접근)
# orders/models.py
from django.db import models
class IceCreamOrder(models.Model):
FLAVOR_CHOCOLATE = 'ch'
FLAVOR_VANILLA = 'vn'
FLAVOR_STRAWBERRY = 'st'
FLAVOR_CHUNKY_MUNKY = 'cm'
FLAVOR_CHOICES = (
(FLAVOR_CHOCOLATE, 'Chocolate'),
(FLAVOR_VANILLA, 'Vanilla'),
(FLAVOR_STRAWBERRY, 'Strawberry'),
(FLAVOR_CHUNKY_MUNKY, 'Chunky Munky')
)
flavor = models.CharField(
max_length=2,
choices=FLAVOR_CHOICES
)
다음과 같이 활용이 가능하다.
>>> from orders.models import IceCreamOrder
>>>IceCreamOrder.objects.filter(flavor=IceCreamOrder.FLAVOR_CHOCOLATE)
[<icecreamorder: 35>, <icecreamorder: 42>, <icecreamorder: 49>]
Django에서 choices에 대해 열거형 타입을 사용하기를 권장한다. 3.0부터 사용이 가능하다.
from django.db import models
class IceCreamOrder(models.Model):
class Flavors(models.TextChoices):
CHOCOLATE = 'ch', 'Chocolate'
VANILLA = 'vn', 'Vanilla'
STRAWBERRY = 'st', 'Strawberry'
CHUNKY_MUNKY = 'cm', 'Chunky Munky'
flavor = models.CharField(
max_length=2,
choices=Flavors.choices
)
다음과 같이 활용이 가능하다.
>>> from orders.models import IceCreamOrder
>>> IceCreamOrder.objects.filter(flavor=IceCreamOrder.Flavors.CHOCOLATE)
[<icecreamorder: 35>, <icecreamorder: 42>, <icecreamorder: 49>]
열거형을 주로 사용하고 위의 단점에 부딪히면 튜플 기반 방법으로 전환하라.
모델에 질의를 하게 되면 장고의 ORM을 통한다. 이 때 모델 매니저(Model Manager)라는 데이터베이스와 연동되는 인터페이스를 호출하게 된다.
다음과 같이 커스텀 모델 매니저를 정의할 수 있다.
from django.db import models
from django.utils import timezone
class PublishedManager(models.Manager):
def published(self, **kwargs):
return self.filter(pub_date__lte=timezone.now(), **kwargs)
class FlavorReview(models.Model):
review = models.CharField(max_length=255)
pub_date = models.DateTimeField()
# add our custom model manager
objects = PublishedManager()
다음과 같이 사용이 가능하다
>>> from reviews.models import FlavorReview
>>> FlavorReview.objects.count()
35
>>> FlavorReview.objects.published().count()
31
그러나 기존 커스텀 매니저를 교체하는것이 아닌 두번째 모델 매니저를 추가하는것이 더 합리적일 수 있다.
from django.db import models
from django.utils import timezone
class PublishedManager(models.Manager):
def get_queryset(self):
return super(UseredManager, self).get_queryset().filter(pub_date__lte=timezone.now())
class FlavorReview(models.Model):
review = models.CharField(max_length=255)
pub_date = models.DateTimeField()
# add our custom model manager
objects = models.Manager()
published = PublishedManager()
다음과 같이 사용이 가능하다
>>> from reviews.models import FlavorReview
>>> FlavorReview.objects.filter().count()
35
>>> FlavorReview.published.filter().count()
31
표면적으로는 기존 model manager를 교체하는게 현명하게 보이지만 이 방법을 사용할 때에는 신중해야 한다.
Model Manager의 작동 순서를 항상 염두해야 한다.
⇒ 새 이름을 가진 사용자 정의 모델 관리자 위에objects=models.Manager()
를 설정하라.
참고 사이트 : link
모델 행동은 믹스인을 통한 캡슐화와 구성화의 개념으로 이루어진다. 모델은 추상화 모델로부터 로직들을 상속 받는다.
Section10.2의 Using Mixins With CBV.에서 자세히 살펴본다.