Skip to content

Latest commit

 

History

History
379 lines (251 loc) · 14.6 KB

06.md

File metadata and controls

379 lines (251 loc) · 14.6 KB

6. Model Best Practices

Model은 Django 프로젝트의 핵심이다. model을 생각없이 짜는 것은 문제를 일으키기 쉽다.

따라서 Model을 설계하고자 하거나 기존의 모델을 수정하려고 할 때는 깊게 생각해라.

6.1 Basics

6.1.1 Break Up Apps With Too Many Models

만약 하나의 app에 20개 이상의 모델이 있다면, app을 분리할 것을 생각해봐라.
실제로는 한 app에 5개에서 10개 정도로 유지하는 것을 권장한다.

6.1.2 Be Careful With Model Inheritance

Model class 상속하는 방법은 세가지가 있는데 각각의 장단점을 생각해보자.

  1. Model 상속 없음
  • 장점
    • Django model이 어떻게 database에 연결되어있는지 확인하기 쉬움.
  • 단점
    • Model별로 반복해서 쓰이는 Field들(ex. created_at)은 반복돼서 사용되고, 유지하기 힘들다.
  1. Abstract base classes(Django abstract model class)
  • 장점
    • 반복해서 쓰이는 field들을 한번에 관리할 수 있다.
    • multi table 상속으로 생기는 복잡성을 피할 수 있다.
  • 단점
    • 부모 클래스를 분리해서 사용할 수 없다.
  1. Multi-table 상속: Table은 부모 클래스와 자식 클래스 모두에게서 생성될 수 있다. 부모 클래스와 자식 클래스가 OneToOneField가 존재
  • 장점
    • 각 모델들은 각자의 테이블을 가져서 부모나 자식 클래스에 쿼리를 날릴 수 있다.
    • 부모 모델에서 자식 모델을 parent.child로 접근할 수 있다.
  • 단점
    • 각 자식 테이블이 부모 테이블의 조인을 요구하기 때문에 불필요한 overhead가 생길 수 있다.
    • 우리는 Multi table 상속을 하지 않기를 강력하게 추천한다.
  1. Proxy models: original model로만 테이블을 만든다.
  • 장점
    • model들이 다른 Python의 기능을 사용할 수 있게 해준다.
    • 우리가 model field를 수정할 수 없다.

Thumbs of rule

  1. 모델 간의 겹치는 영역을 최소화하라
  2. 만약 created_at과 같이 전체 모델에서 사용되는 공통 필드가 있다면 Abstract base class로 관리해라
  3. Proxy model은 유용한 상속모델이지만 다른 상속 스타일과는 너무 다르다.
  4. multi-table inheritance는 피하는 것이 좋다. 대신에 OneToOneFieldForeignKeys를 이용해라

6.1.3 Model Inheritance in Practice: The TimeStampedModel

created, modified timestampfield를 전체 모델에 넣는 것은 일반적이다. 다음과 같은 Abstract Base Model을 적는 것을 장려한다.

from django.db import models
class TimeStampedModel(models.Model):
  """
  An abstract base class model that provides self-
  updating ``created`` and ``modified`` fields.
  """
  created = models.DateTimeField(auto_now_add=True)
  modified = models.DateTimeField(auto_now=True)

  class Meta:
    abstract = True

이를 다음과 같이 사용할 수 있다.

# flavors/models.py
from django.db import models

from core.models import TimeStampedModel

class Flavor(TimeStampedModel):
  title = models.CharField(max_length=200)

6.2 Database Migrations

6.2.1 Tips for Creating Migrations

  • 앱이나 모델이 생성되면, makemigrations command로 migration 파일을 생성하라
  • migration 전에 sqlmigrate command로 SQL을 확인하라
  • MIGRATION_MODULES를 사용해서 third-party 앱 마이그레이션을 관리해라
  • 얼마나 많은 마이그레이션 파일이 생성되는지는 신경쓰지마라. 만약 마이그레이션 파일이 너무 많아지면 squashmigrations command를 사용해서 migrate 파일을 합쳐라
  • migration 전에 데이터를 항상 백업하라

6.2.2 Adding Python Functions and Custom SQL to Migrations

RunSQLRunPython을 통해서 마이그레이션 시 custom SQL이나 python 함수를 실행시켜줄 수 있다.

6.3 Overcoming Common Obstacles of RunPython

6.3.1 Getting Access to a Custom Model Manager's Methods

use_in_migrations = True flag를 사용해서 custom manager를 마이그레이션 파일에서 사용해줄 수 있다.

6.3.2 Getting Access to a Custom Model Method

custom model method를 마이그레이션에 넣지마라.

docs.djangoproject.com/en/3.2/topics/migrations/#historical-model

6.3.3 Use RunPython.noop to Do Nothing

RunPython은 migration을 다시 돌리는 경우를 위해서 reverse_code가 필수적이다. 다만 변화를 여러번 반복해도 결과가 바뀌지 않는 멱등성 함수에게는 굳이 reverse_code가 필요 없다. 그때 RunPython.noopreverse_code로 사용하라

6.3.4 Deployment and Management of Migrations

  • migration을 하기 전에 항상 데이터를 백업하라

  • migration을 하기 전 roll back이 가능한지를 판단하라

  • 만약 프로젝트가 수백만개의 행을 가지고 있는 테이블이 있다면, staging server에서 미리 마이그레이션을 테스트해라.

  • 만약 MySQL을 사용한다면, schema 변경전 데이터베이스를 무조건 백업해라

  • 가능하다면, migration 전 db를 read-only 모드로 변경해라

  • 조심하지 않는다면 schema 변경에 엄청난 시간이 걸리게 된다

  • 항상 migration 파일를 VCS에 포함하라

6.4 Django Model Design

6.4.1 Start Normalized

데이터를 정규화하라. 다른 모델이 가지고 있는 정보를 또다른 모델이 소유하지 않도록 해라.

6.4.2 Cache Before Denormalizing

속도를 위해 비정규화하기 전에 cache를 먼저 도입하라

6.4.3 Denormalize Only if Absolutely Needed

정말 필요한 경우가 아니라면 비정규화하지마라

6.4.4 When to Use Null and Blank

원문 참고

모델 필드를 정의할 때 null, blank를 지정할 수 있다. 기본은 둘다 False이다.

null은 DB와 연관되어 있으며 주어진 데이터베이스 컬럼이 null값을 가질것인지 아닌지를 정의한다. blank는 유효성과 관련되어 있으며 form.is_valid()가 호출될 때 유효성 검사에 이용된다.

null=True, blank=Flase는 DB 레벨에서는 null이 될 수 있지만, application 레벨에서는 required 필드인것을 의미한다.

일반적으로 CharField, TextField와 같은 문자열 기반 필드는 null=True를 정의해서는 안된다. "데이터 없음"에 대해 두 가지 값인 None빈 문자열인 두 가지 상태를 갖게되기 때문이다. 일반적으로 빈 문자열을 "데이터 없음"으로 갖게 하는 것이 Django 컨벤션이다.

문자열 관련 필드를 지정할 때 blank=True만 지정하면 된다. 빈값이 빈 문자열로 저장된다.

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로 저장된다.
  1. CharField, TextField, SlugField, EmailField, CommaSeparated-IntegerField, UUID Field

null=True → unique=True와 blank=True로 모두 설정했다면 괜찮다. 공백으로 여러 개체를 저장할 때 고유 제약조건 위반 방지를 위해 null=True가 필요하다. blank=True → 위젯이 빈 값을 허용하기를 원한다면 설정한다. null=True일 때, 데이터베이스에서는 빈 값이 Null로 저장되고 null=False일 때 빈 문자열이 저장된다.

  1. FileField, ImageField

null=True → 사용하지 않는걸 추천한다. 내부적으로는 CharField를 통해 이미지 경로 등을 저장하므로 CharField와 동일한 패턴을 따른다. blank=True → 사용할 수 있다. CharField와 동일한 패턴을 따른다.

  1. BooleanField null=True → 사용한다. blank=True → 기본값은 blank=True이다.

  2. IntegerField, FlaotField, DecimalFiled, DurationField 등 null=True → 해당 값이 데이터베이스에 NULL로 들어가도 문제가 없다면 이용한다. blank=True → 위젯에서 해당 값이 빈 값을 받아와도 문제가 없다면 이용한다. (null=True와 함께 이용한다.)

  3. DateTimeField, DateField, TimeField 등 null=True → 해당 값이 데이터베이스에 NULL로 들어가도 문제가 없다면 이용한다. blank=True → 위젯에서 빈 값을 받아와도 문제 없거나 auto_now, auto_now_add를 이용하고 있다면 이용한다. (null=True와 함께 이용한다.)

  4. ForeignKey, OneToOneField, ManyToManyField, GenericIPAddressField null=True → 해당 값이 데이터베이스에 NULL로 들어가도 문제가 없다면 이용한다. blank=True → 위젯에서 해당 값이 빈 값을 받아와도 문제가 없다면 이용한다. (null=True와 함께 이용한다.)

  5. ManyToManyField null=True → NULL은 아무런 영향이 없다. blank=True → 위젯에서 해당 값이 빈 값을 받아와도 문제가 없다면 이용한다.

  6. JSONField null=True → 써도 좋다. blank=True → 써도 좋다.

6.4.5 When to Use BinaryField

  • MessagePack-formatted content
  • Raw sensor data
  • Compressed data(ex. BLOB)

일 때 쓸 수 있다. 하지만 binary data는 거대한 병목이 될 수 있다는걸 기억해라. 사용해야될 일이 있다면 FileField를 사용하는 것을 추천한다.

6.4.6 Try to Avoid Using Generic Relations

  • model간 인덱싱이 없기 때문에 속도가 저하된다
  • 존재하지 않는 모델을 참조할 가능성이 있다.

왠만하면 Generic Relations를 쓰지 말고, 꼭 써야한다면 third-party를 사용하라 해결법 참고

6.4.7 Make Choices and Sub-Choices Model Constants

모델을 생성할 때 선택지를 생성하는 것이 좋다. 다음과 같은 식으로 코드를 작성할 수 있다.

# 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
    )

6.5 The Model _meta API

_meta API는 다음과 같은 특성을 가집니다.

  • '_'로 시작하지만 public하고 문서화된 API입니다.

보통의 경우 _meta는 필요없지만,

  • 모델 안 필드 목록을 볼 때
  • 특정 필드의 class를 확인할 때,

필요합니다.

  • Django model 관리 tool을 만들 때
  • Django에 특화된 form field를 만들 때
  • Admin과 같은 역할을 하는 model data를 만들 때
  • 장고의 시각화나 분석 라이브러리를 만들 때 도움이 됩니다.

6.6 Model Managers

Django ORM을 사용할 때 Model Manager라는 인터페이스를 사용해서 database와 통신합니다. Django에서 제공해주는 default도 있지만 다음과 같이 직접 만들어줄 수도 있습니다.

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

두번째 Manager를 추가해줘도 된다.

from django.db import models
from django.utils import timezone

class PublishedManager(models.Manager):
    def get_queryset(self, **kwargs):
        return super().get_queryset().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 = models.Manager()
    published = PublishedManager()
>>> from reviews.models import FlavorReview
>>> FlavorReview.objects.all().count()
35
>>> FlavorReview.published.all().count()
31

둘 중에 Default model manager를 교체해주는 것이 좋아보이지만 실제 프로젝트에서는 일을 어렵게 만들 수도 있다.

  1. 모델 상속 시 자식 클래스는 부모 클래스의 모델 메니저를 상속받는다.
  2. 첫번째 매니저가 적용될 때 Django는 이를 default로 여긴다. 일반적인 Python pattern과는 반하기 때문에 원하지 않는 queryset 결과를 받을 수 있다.

그렇기 때문에 두번째 접근법을 추천한다.

6.7 Understanding Fat Models

fat model 컨셉은 view 말고 model에 비즈니스 로직을 추가하는 것이다.

예를 들어, 다음과 같은 로직을 처리한다고 할 때, 이를 모두 model method에 추가하라.

  • Review.create_review(cls, user, rating, title, description)

    리뷰를 만드는 classmethod. view에서 이를 사용

  • Review.product_average

    리뷰된 제품의 평균 점수를 구하는 property

  • Review.found_useful(self, user, yes)

    Review 응답을 기록하는 method

이렇게 추가하다보면 모델에 너무 많은 로직들이 쌓여서 god object가 만들어질 수 있다. 그러면 한 모델에 1000줄이 넘는 코드가 쌓이게 된다.

따라서 모델로 로직을 옮길 때 중요한 것은 OOP의 개념이다. 큰 문제들은 작은 문제로 나뉘어질 때 더 쉬워진다.

만약 모델이 엄청나게 커진다면 다른 모델에서도 사용할 수 있게 로직을 분리해보라. Model Behaviors or Stateless Helper Function으로 이러한 로직을 옮길 수도 있다.

6.7.1 Model Behaviours a.k.a Mixins

Model Behaviour는 mixin을 통해 캡슐화과 합성(composition)를 사용하는 방법이다.

6.7.2 Stateless Helper Functions

자주 사용되는 method를 Model Behaviour를 사용하지 않고 utility.py에 별도로 저장해도 된다.

6.7.3 Model Behaviours vs Helper Functions

둘 중 어떠한 방법도 완벽하지는 않다. 적당한 상황에 적절한 함수를 이용하는 것이 중요하다.

6.8 Additional Resource