Study/django

[UDEMY]Django DRF Project: eCommerce RESTful API 강의 정리

bluebamus 2024. 3. 1.

 - 강좌 정보 : https://www.udemy.com/course/django-drf-project-ecommerce/

 

 - 강좌 설명 : 해당 강좌는 중급 이상의 수준을 요구하는 강좌로 database 설계에 대한 간단한 요구 명세와 viewset을 이용한 api 설계, pytest를 이용한 테스트 등을 다룬다. 개별적으로 선수 지식을 가지고 있다면 사실 그렇게 높은 수준이 아니다. 오히려 너무 어설프게 맛만 본다고 할 수 있다. 하지만 요구 명세에서 database 설계, 모델 정의, view와 시리얼라이즈 정의, url의 정의 그리고 중요한 test를 설명하고 이에 필요한 정말 괜찮은 다양한 라이브러리를 이용한다는 점에서 높게 평가할 수 있다.

 

 - 단점 및 유의사항 : 라이브러리에 대한 설명이 거의 없다 예를 들어 MPTT의 경우 웹사이트의 메뉴와 같이 트리 형식의 다단이 나뉘어지는 구조를 쉽게 구현할 수 있다. 하지만 재귀호출로 인해 처리속도를 많이 느리게 만든다. 때문에 자주 변환이 되거나 호출에 의해 계산이 반복되게 만들면 효율이 좋지 않고 변경되지 않는 구조에서 캐시를 사용한다면 딱 좋다. pytest-cov와 coverage도 사용하지만 깊지는 않다. 하지만 이 라이브러리들도 꽤 휼륭하다. factoryboy를 사용하지만 fake는 사용하지 않는게 아쉽긴하다. 하지만 이러한 부분들은 사실 강좌에서 모두 다루기 어렵기 때문에 개인이 개별로 더 깊게 공부하는게 맞다. 하지만 어느정도 설명이 있었으면 좋겠지만 너무 짧거나 거의 없는게 아쉽다.

 - viewset 뿐만 아니라 generateview 등을 사용하는 경우도 시나리오를 만들어 좀 더 다양한 설계 접근이 있었으면 하지만 그런 부분이 없어 아쉬웠고, 생각보다 실무에 가까운 소프트웨어 설계가 아니라 학습을 위한 가상의 간단한 시나리오 기반의 설계라 사실 얻고자 했던 실무 지식의 기대를 채우지 못한게 아쉬웠다.

 

 - 아래에서 부터 시작하는 정리는 내 기준 필요한 부분만 정리했다. 대부분 아는 내용이기 때문에 이전에 정리했더라도 한번 더 자세한 팁으로 기록을 남길 필요가 있거나 내가 모르는 부분들에 한해서만 정리한다.

 

 1. 강좌에 사용되는 라이브러리 정리

   1) black :

      - black은 Python 코드 포맷터로, 코드 스타일을 일관되게 유지해 줍니다. 코드를 자동으로 포맷팅(수정)하여 가독성을 향상시키고, 개발자들 간에 일관된 코드 스타일을 유지할 수 있도록 도와줍니다.

 

   2) flake8 :

      - flake8은 Python 코드 정적 분석 도구입니다. 코드의 문법적 오류, 스타일 가이드 위반, 잠재적인 버그 등을 검사하여 코드의 품질을 향상시킵니다. PEP 8 스타일 가이드를 준수하도록 도와줍니다.


   3) pytest :

      - pytest는 Python의 테스트 프레임워크로, 단위 테스트 및 통합 테스트를 작성하고 실행할 수 있습니다. 간편한 문법과 다양한 기능을 제공하여 테스트 작성과 실행을 용이하게 합니다.


   4) pytest-cov :

      - pytest-cov는 pytest의 확장입니다. 코드의 테스트 커버리지를 측정하는 데 사용됩니다. 테스트가 얼마나 많은 코드를 실행하는지 확인하여 코드의 품질을 평가할 수 있습니다.

 

   5) coverage :

      - coverage는 Python 코드의 테스트 커버리지를 측정하는 라이브러리입니다. 코드의 어느 부분이 테스트되지 않았는지 확인하고, 테스트 커버리지를 통해 코드의 품질을 평가할 수 있습니다.

 

   6) django-mptt :

      - django-mptt는 Django에서 MPTT(Move Per Threaded Tree) 알고리즘을 이용한 트리 구조 데이터를 다루기 위한 라이브러리입니다. 트리 구조 데이터를 쉽게 저장, 조회, 조작할 수 있도록 도와줍니다.

 

   7) python-dotenv :

      - python-dotenv는 Python 애플리케이션에서 환경 변수를 관리하기 위한 라이브러리입니다. .env 파일을 사용하여 환경 변수를 설정하고, 애플리케이션에서 이를 쉽게 로드할 수 있습니다.

 

   8) drf-spectacular :

      - drf-spectacular은 Django REST Framework(DRF)를 위한 API 문서 생성 도구입니다. DRF의 스키마를 분석하여 자동으로 API 문서를 생성하고, Swagger UI와 같은 인터페이스를 제공하여 API 사용자들이 쉽게 문서를 확인하고 테스트할 수 있도록 합니다.

 

   9) sqlparse :

      - SQLParse는 Python에서 SQL 구문을 분석하고 조작하는 라이브러리입니다. SQL 구문을 분리(split), 포맷팅(format), 파싱(parse) 하는 기능을 제공합니다.

         1. split: SQL 구문을 각 부분으로 나누는 기능 (예: SELECT, FROM, WHERE 절 분리)
         2. format: SQL 구문을 보기 좋게 포맷팅하는 기능
         3. parse: SQL 구문을 구문별로 파싱하여 tuple 등으로 가져오는 기능

import sqlparse

# SQL 구문을 split하는 예시
sql = 'select * from db; select * from db_slave;'
print(sqlparse.split(sql))  # 결과: ['select * from db;', 'select * from db_slave;']

# SQL 구문을 포맷팅하는 예시
sql = 'select * from db where id in (select id from db);'
print(sqlparse.format(sql, reindent=False, keyword_case='lower'))
print(sqlparse.format(sql, reindent=False, keyword_case='upper'))

 

   - 사용 예시 : 

sqlparse
from django.db import connection
from pygments import highlight
from pygments.lexers import SqlLexer
from pygments.formatters import TerminalFormatter

...
 q = list(connection.queries)
        print(len(q))
        for qs in q:
            sqlformatted = sqlparse.format(str(qs["sql"]), reindent=True)
            print(highlight(sqlformatted, SqlLexer(), TerminalFormatter()))

   10) Pygments :

      - Pygments는 파이썬으로 작성된 구문 하이라이팅 패키지입니다. 코드를 글로 남기거나 웹 페이지에 표시할 때 가독성을 높이기 위해 다양한 언어의 소스 코드에 색상과 스타일을 적용할 수 있습니다.

      - Pygments를 활용하여 sqlparse로 분석한 SQL 구문을 하이라이팅 하는 간단한 코드 예시입니다.

from pygments import highlight
from pygments.lexers import SqlLexer
from pygments.formatters import HtmlFormatter
import sqlparse

# SQL 구문
raw_sql = "SELECT * FROM Users WHERE age > 20;"

# sqlparse를 사용하여 SQL 구문 분리
parsed_sql = sqlparse.split(raw_sql)

# SQL 구문을 하이라이팅
for sql in parsed_sql:
    highlighted_sql = highlight(sql, SqlLexer(), HtmlFormatter())
    # 결과를 출력하거나 웹 페이지에 표시
    print(highlighted_sql)

   

 2. ViewSet에서 class에 serializer를 정의하지 않고 "list()"를 오버라이딩 하여 함수 내부에서 직접 호출하여 사용하는 경우

   - serializer를 정의하지 않은 경우, 응답 요청에 사용되는 serializer를 list 함수는 사전에 알 수 없기 때문에 extend_schema(responses="") 데코레이션을 사용해 정의를 해줘야 에러가 발생하지 않는다.

class CategoryViewSet(viewsets.ViewSet):
    """
    A simple Viewset for viewing all categories
    """

    queryset = Category.objects.all()

    @extend_schema(responses=CategorySerializer)
    def list(self, request):
        serializer = CategorySerializer(self.queryset, many=True)
        return Response(serializer.data)

 

 3. Testing - factory를 사용해 model의 더미 데이터 만들기

   - database와 연동하여 실재 데이터를 생성하고 테스트 하는 과정은 환경을 구축하고 테스트를 하는 과정에서 많은 시간이 소요된다. 하지만 factoryboy와 faker를 사용하면 databse의 연동 없이 더미 데이터로 테스트를 수행할 수 있다.

      - factoryboy는 factory를 사용하여 테스트 데이터를 정의하고 생성하는 기능을 제공한다.

      - faker는 직접 수동으로 데이터를 정의하는 것이 아니라, 자동으로 정해진 범위 내에서 랜덤하게 데이터를 생성해주며 관련한 부과적인 다양한 기능을 제공한다.

 

   - 사용 방법 :

      1) factories.py를 만들어 생성되는 데이터를 정의한다.

         - 구현된 코드에는 faker를 사용하지 않고 직접 데이터 유형을 정의하였다.

# factories.py

import factory

from drfecommerce.product.models import Brand, Category, Product


class CategoryFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Category

    name = factory.Sequence(lambda n: "Category_%d" % n)


class BrandFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Brand

    name = factory.Sequence(lambda n: "Brand_%d" % n)


class ProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Product

    name = "test_product"
    description = "test_description"
    is_digital = True
    brand = factory.SubFactory(BrandFactory)
    category = factory.SubFactory(CategoryFactory)

 

      2) conftest.py를 만들어 factories에서 정의한 class들을 등록한다.

         - 만약 다른 사전 데이터 및 환경이 필요하다면 해당 파일에 등록하여 테스트 수행시 사용 가능하다.

            - 예) API_Client

# conftest.py

import pytest
from pytest_factoryboy import register
from rest_framework.test import APIClient

from .factories import BrandFactory, CategoryFactory, ProductFactory

register(CategoryFactory)
register(BrandFactory)
register(ProductFactory)


@pytest.fixture
def api_client():
    return APIClient

 

      3) 테스트 함수의 인자로 conftest.py에 등록한 정보 기반으로 자동 생성된 인스턴스 이름을 정의한다.

         - db에 접근하는 테스트는 글로벌 인자인 pytestmark에 django_db를 등록하거나 데코레이션을 정의해야 한다.

         - 받은 인자를 함수로 사용할 수 있다. 이름이 자동으로 생성되는데 원칙은 conftest.py에서 register에 등록되는 class의 이름에 대문자를 기준으로 언더바(_)가 생성된다. 수동으로 정의하는 방법도 있다. 

# test_models.py

import pytest

pytestmark = pytest.mark.django_db


class TestCategoryModel:
    def test_str_method(self, category_factory):
        # Arrange
        # Act
        obj = category_factory(name="test_cat")
        # Assert
        assert obj.__str__() == "test_cat"


class TestBrandModel:
    def test_str_method(self, brand_factory):
        # Arrange
        # Act
        obj = brand_factory(name="test_brand")
        # Assert
        assert obj.__str__() == "test_brand"


class TestProductModel:
    def test_str_method(self, product_factory):
        # Arrange
        # Act
        obj = product_factory(name="test_product")
        # Assert
        assert obj.__str__() == "test_product"

 

   - reference : 

2023.04.02 - [Django Web Framework/Django Pytest] - [very academy] Pytest Mastery with Django

 

[very academy] Pytest Mastery with Django

import pytest # 테스트를 패스함 @pytest.mark.skip def test_example(): print("mark skip!!!") assert 1 == 1 # 현재는 실패하는 것이 정상으로 앞으로 개발, 개선될 코드임 # 결과 : ===== * passed, 1 xpassed ===== @pytest.mark.xfai

devspoon.tistory.com

2023.04.14 - [Django Web Framework/Django Pytest] - [udemy] Real World Python Test Automation with Pytest 학습

 

[udemy] Real World Python Test Automation with Pytest 학습

강좌 정보 : https://www.udemy.com/course/pytest-course/ * 이 포스팅에서는 "[very academy] Pytest Mastery with Django"에 언급된 내용을 제외하고 처음으로 언급되는 방법들에 대해서만 기록을 한다. - 참고 포스팅 :

devspoon.tistory.com

2023.04.09 - [Django Web Framework/Django Pytest] - pytest 확장 라이브러리 정리

 

pytest 확장 라이브러리 정리

1. pytest-cov: 이 라이브러리는 pytest로 테스트 커버리지를 측정하는 데 사용됩니다. HTML, XML 및 JSON과 같은 다양한 형식으로 커버리지 보고서를 생성합니다. Python 코드와 Cython 코드 모두의 적용 범

devspoon.tistory.com

 

 4. Testing - APIClient를 사용해 end to end 테스트 만들기

   1) conftest.py에 APIClient를 반환하는 fixture를 정의한다.

# conftest.py

import pytest
from pytest_factoryboy import register
from rest_framework.test import APIClient

from .factories import BrandFactory, CategoryFactory, ProductFactory

register(CategoryFactory)
register(BrandFactory)
register(ProductFactory)


@pytest.fixture
def api_client():
    return APIClient

 

   2) test_endpoints.py의 테스트 함수에 api_clent 인자가 추가된다.

      - 여러개의 더미데이터를 한번에 생성하기위해 create_batch()함수를 사용한다.

      - 정의된 api_client()에 원하는 요청( get, post etc ) 을 정의하고 url을 전달하여 응답 결과를 얻는다.

      - assert를 이용해 결과를 확인한다.

# test_endpoints.py

import json

import pytest

pytestmark = pytest.mark.django_db


class TestCategoryEndpoints:

    endpoint = "/api/category/"

    def test_category_get(self, category_factory, api_client):
        # Arrange
        category_factory.create_batch(4)
        # Act
        response = api_client().get(self.endpoint)
        # Assert
        assert response.status_code == 200
        assert len(json.loads(response.content)) == 4


class TestBrandEndpoints:

    endpoint = "/api/brand/"

    def test_brand_get(self, brand_factory, api_client):
        # Arrange
        brand_factory.create_batch(4)
        # Act
        response = api_client().get(self.endpoint)
        # Assert
        assert response.status_code == 200
        assert len(json.loads(response.content)) == 4


class TestProductEndpoints:

    endpoint = "/api/product/"

    def test_product_get(self, product_factory, api_client):
        # Arrange
        product_factory.create_batch(4)
        # Act
        response = api_client().get(self.endpoint)
        # Assert
        assert response.status_code == 200
        assert len(json.loads(response.content)) == 4

 

 - reference : 

https://www.youtube.com/watch?v=KIIdbVs7e8I&list=PLP1DxoSC17LZTTzgfq0Dimkm6eWJQC9ki

 

https://www.youtube.com/watch?v=7dgQRVqF1N0

 

 5. Custom Managers와 QuerySet Methods 만들기

   - Django에서 from_queryset과 as_manager는 모델 매니저와 쿼리셋의 기능을 확장하고 재사용하기 위해 사용됩니다. 여기서 매니저(Manager)는 데이터베이스 쿼리 연산을 모델에 제공하는 인터페이스이며, 쿼리셋(QuerySet)은 데이터베이스 쿼리를 생성하고 실행하는데 사용되는 클래스입니다.

 

   - 차이점 : 

      - 방법 모두 커스텀 QuerySet을 Manager와 연결하는 기능을 하지만 from_queryset은 재사용성을, as_manager는 코드의 간결함을 제공하는 데 초점을 맞춥니다. 

      - from_queryset는 기존의 QuerySet 클래스로부터 새로운 Manager 서브클래스를 생성합니다. 이 방식은 특히 한 QuerySet 클래스를 여러 모델에 재사용할 때 유용합니다.

      - as_manager는 QuerySet 인스턴스에 대한 매니저 인스턴스를 직접 생성하여 모델 클래스 내에서 바로 사용할 수 있게 합니다

 

   1) from_queryset :

      - 설명 : Django에서 from_queryset() 메서드를 사용하면, 사용자가 정의한 QuerySet 메서드를 Manager 클래스에서도 이용할 수 있게 됩니다. 즉, 사용자 지정 QuerySet의 메서드를 Manager 인스턴스를 통해 직접 호출할 수 있습니다

      - 사용 예 : 커스텀 쿼리셋을 정의한 후 from_queryset 함수를 사용하여 매니저 클래스를 생성하고 모델에 할당합니다.

from django.db import models

# 사용자 정의 QuerySet 생성
class CustomQuerySet(models.QuerySet):
    # 사용자 정의 메서드
    def active(self):
        return self.filter(is_active=True)
    
    def recent(self):
        return self.order_by('-created_at')

# QuerySet을 상속받아 Manager 생성
class CustomManager(models.Manager.from_queryset(CustomQuerySet)):
    pass

class MyModel(models.Model):
    # 필드 정의
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    # 생성한 매니저를 'objects'로 설정
    objects = CustomManager()

 

   2) as_manager :

      - 설명: as_manager 메서드는 쿼리셋 인스턴스를 매니저로 변환하는데 사용됩니다. 쿼리셋에 정의된 메소드를 사용자 정의 매니저로 바로 가져오는 방식입니다. 이렇게 하면 기존 쿼리셋의 체인이 가능한 메소드이면서 매니저 메소드도 필요할 때 유용합니다. as_manager()를 사용하는 주된 이유는 쿼리셋 메소드 체이닝의 장점을 활용하려는 경우입니다. 쿼리셋을 정의함으로써 필터링, 정렬 등의 메소드를 체인으로 연결해서 호출할 수 있으며, 동일한 쿼리셋을 여러 모델에서 재사용할 수 있습니다. 일반적으로 특정 쿼리셋 로직을 여러 모델 매니저에서 사용해야 할 때 as_manager() 방식이 유용합니다. 반면, 특정 모델에 대해서만 사용할 메소드를 정의하려면 매니저 클래스를 직접 정의하는 것이 더 간단할 수 있습니다.

      - 사용 예: 쿼리셋 클래스에 as_manager 메소드를 호출하여 모델의 매니저로 사용할 수 있습니다.

from django.db import models

# Custom QuerySet
class BookQuerySet(models.QuerySet):
    def published(self):
        return self.filter(is_published=True)

# Book 모델
class Book(models.Model):
    title = models.CharField(max_length=200)
    is_published = models.BooleanField(default=False)
    publication_date = models.DateField()

    # Custom manager
    objects = BookQuerySet.as_manager()

# 사용 예:
# Book.objects.published()를 호출하여 출판된 책만 가져올 수 있습니다.

 

   - 주의 사항 :

      1. 커스텀 메서드의 상속 문제 :
         - as_manager()를 사용할 때 생성된 매니저는 Manager 클래스의 인스턴스가 됩니다. 이 때문에 커스텀 매니저에서 정의한 메소드들은 자동으로 상속되지 않습니다. 쿼리셋 메서드를 매니저 메서드로 변환하기 위해서는 해당 메서드를 쿼리셋 클래스에 명시적으로 정의하고, 그 클래스의 인스턴스를 as_manager() 메서드에 전달해야 합니다.


      2. 혼합 사용과 혼돈 :
         - 기존 매니저 메서드와 쿼리셋 메서드가 혼합될 경우, 어떤 메서드가 매니저를 통해 호출되고 어떤 것이 쿼리셋을 통해 호출되는지 혼돈할 수 있습니다. 이를 방지하기 위해 기능을 명확히 구분하고 일관된 패턴을 사용하는 것이 좋습니다.


      3. 체이닝(chaining) 시의 주의 :
         - 매니저에서 바로 쿼리셋 메서드를 체이닝하는 것이 가능하지만, 체이닝이 복잡해질 경우 예상치 못한 동작이 발생할 수 있습니다. 체이닝을 사용할 때는 각 단계에서 어떤 쿼리셋이 반환되고 있는지 명확히 확인해야 합니다.

 

      4. 모델의 _default_manager :
         - as_manager()로 생성된 매니저는 모델의 _default_manager로 자동으로 설정되지 않습니다. 디폴트 매니저를 as_manager()로 생성된 것으로 사용하고 싶다면 명시적으로 모델에서 첫 번째 매니저로 설정해야 합니다.

 

      5. 매니저의 상속 관계 :
         - as_manager()를 사용하면 매니저의 커스텀 메서드가 사라지므로, 큰 프로젝트나 라이브러리에서는 기존 매니저를 상속받아 커스텀 메서드를 추가하는 방식을 선호할 수 있습니다.

 

      6. 데이터 마이그레이션 중의 매니저 호출 :
         - 데이터 마이그레이션 시점에 매니저 메서드를 호출하면 예상치 못한 결과를 초래할 수 있으므로 주의가 필요합니다. 이는 마이그레이션 중에는 데이터베이스가 일시적으로 일관되지 않은 상태에 있을 수 있기 때문입니다.

 

      7. 다중 데이터베이스 환경 :

         - 여러 데이터베이스를 사용하는 경우, using() 메서드를 통해 명시적으로 데이터베이스를 선택해야 할 수 있습니다. as_manager()로 생성된 매니저가 특정 데이터베이스에 바인딩되지 않도록 주의해야 합니다.

 

      8. 디버깅과 트러블슈팅 :
         - 커스텀 매니저와 쿼리셋을 혼용할 때는 디버깅과 트러블슈팅이 복잡해질 수 있습니다. 매니저와 쿼리셋간의 관계를 명확히 이해하고, 이러한 코드에 대한 적절한 테스트 케이스를 작성하여 유지보수를 용이하게 해야 합니다.

 

 - reference : 

https://blog.hwahae.co.kr/all/tech/tech-tech/4108

 

DRY를 위해 Django Manager를 적용해봅시다. – 화해 블로그 | 기술 블로그

화해팀의 백엔드 플랫폼에서는 다양한 언어와 프레임워크를 사용하여 개발을 진행해 나가고 있습니다. 그중 가장 많이 사용되고 있는 웹 프레임워크는 Django입니다. ORM QuerySet을 적극적으로 사

blog-wp.hwahae.co.kr

https://renine94.tistory.com/32

 

Custom Manager, QuerySet

Manager Django 모델에서 데이터베이스와 상호 작용하는 인터페이스 기본적으로 Manager는 Model.objects 속성을 통해 사용할 수 있다. Django 모델마다 기본적으로 사용되는 기본 관리자는 django.db.models.Mana

renine94.tistory.com

 

 6.  Custom Field Ordered List - 커스텀 필드로 유니크 속성을 만들기

   - 해당 부분에 대해서 이 프로젝트에서 다루는만큼 깊게 다뤄본적이 없어서 이해하는데 많은 어려움을 겪었다. 때문에 코드 리뷰를 하고 넘어가는 수준으로 정리하고 아래 reference의 영상에서 관련된 부분만 따로 학습하고 새로운 글로 내용을 정리할 계획이다.

 

   1) custom field 구현 :

   - Django Custom Field의 check() 메서드에 대한 정리 :

      - init를 통해 저장된 class 변수 혹은 self.model로 접근한 모델 데이터 등을 기반으로 제약 조건을 검사한다.

      - 만약 문제가 있다면, check.Error 함수를 이용해 적절한 에러를 정의한다.

      - 모든 조건을 통과한다면 return은 [] 빈 배열 값이 전달된다.

 

      - check 메서드는 Django의 시스템 체크 프레임워크(system check framework)의 일부입니다. 이 프레임워크는 개발자가 manage.py check 명령어를 실행할 때나 서버를 시작할 때(starup 시에 자동으로) 호출되어 모델, 필드, 설정 등의 설정이 올바른지 검증합니다. 커스텀 필드에서 check 메서드를 구현하는 경우, 이 메서드는 필드 정의에 문제가 없는지 확인하는데 사용되며, 필드와 관련된 오류나 경고를 반환할 수 있습니다. 반환된 오류는 Django가 문제를 식별하고 디버그하는 데 도움을 줍니다. check 메서드는 일반적으로 데이터베이스로의 변경사항을 체크하기 전에 호출됩니다.


      - check() 메서드는 모델의 유효성 검사 시스템을 통해 실행되며, 모델 필드의 자체 유효성 검사를 추가하는 데 사용됩니다. 예를 들어, 커스텀 필드에 특정한 제약 조건이나 규칙이 있다면, check() 메서드를 오버라이드하여 커스텀 로직을 구현할 수 있습니다.

 

   - check() 메서드의 사용 예 : 

      - 속성 값의 유효성 검사 :

         - max_length나 null 옵션과 같은 필드 옵션이 유효한 범위 내에 있는지를 검사하고자 할 때 사용됩니다.
      - 커스텀 유효성 검사 :

         - 필드가 특정한 형식의 데이터만을 허용하도록 하고 싶을 때, 예를 들어 이메일 주소나 URL 형식만을 받도록 지정하고자 한다면, 이를 검사하는 로직을 check() 내에 구현할 수 있습니다.
      - 데이터베이스 레벨의 제약 조건 :

         - 필드가 데이터베이스상에서 특정한 제약 조건을 준수해야 할 때(예: 외래키 참조의 무결성), 이러한 조건들이 주어진 모델과 호환되는지를 검사할 수 있습니다.

 

   - * 연산자는 파이썬에서 "unpack" 연산자로 알려져 있으며, 리스트나 튜플 같은 이터러블(iterable) 객체들의 요소를 개별 아이템으로 분해하는 데 사용됩니다. 이를 통해 함수에 인자를 전달하거나, 여러 변수에 한 번에 값을 할당할 수 있습니다.

a = [1, 2, 3]
b = [4, 5, 6]
c = [*a, *b]  # c는 [1, 2, 3, 4, 5, 6]

 

   - 코드에서 check의 역할 :

      - unique_for_field의 데이터가 정의되어 있는지 검사

      - unique_for_field의 정보가 모델에 정의된 필드 이름과 동일한게 있는지 검사

 

   - pre_save 메서드 :

      - pre_save 메서드는 모델 인스턴스가 데이터베이스에 저장되기 바로 전에 호출됩니다. 커스텀 필드가 정의될 때 이 메서드를 오버라이드할 수 있으며, 이를 통해 필드 데이터를 최종적으로 조작하거나 필드 값에 대한 추가적인 처리를 수행할 수 있습니다. 예를 들어, 특정 필드 데이터를 암호화하거나 형식을 변경하는 등의 작업을 수행할 때 pre_save 메서드를 사용할 수 있습니다. 모델의 save 메서드가 호출될 때 해당 필드의 pre_save 메서드가 호출되며, 모델 인스턴스의 필드 데이터를 조절한 후의 값을 데이터베이스에 저장합니다.

 

      - model_instance 매개변수는 pre_save 메소드에 전달되는, 모델의 인스턴스로, 데이터베이스에 저장되기 전의 모델 인스턴스의 상태를 나타냅니다. 즉, 모델 인스턴스에 정의된 에트리뷰트(필드)에 접근해서 값을 얻거나 수정할 수 있습니다.

         - 예를 들어, 어떤 모델 MyModel이 있고 여기에 pre_save 메소드가 있는 커스텀 필드 MyCustomField가 정의되어 있다면, pre_save 메소드는 MyModel의 인스턴스를 model_instance로 받게 됩니다. model_instance를 통해 MyModel 인스턴스의 다른 필드 값을 읽거나 특정 필드 값을 새로 조정할 수 있습니다.

   - def pre_save(self, model_instance, add) 함수의 동작 분석 :

      - 이 코드에 정의된 pre_save의 목적은 만약 사용자가 order을 공란으로 두었을 경우, 자동으로 해당 필드 정보를 +1 한 값으로 채워주는 동작이다.

      - if getattr(model_instance, self.attname) is None : 저장 하려는 객체에서 커스텀 필드를 정의하는 필드이름과 동일한 필드가 있는지 확인한다 ( 모델에서는 black가 가능한 설정이기 때문에 없을 수 있다 )

      - 저장하려는 객체에 self.unique_for_field에 저장된 필드 정보를 기반으로 데이터를 가져온다. ("product")

      - 이후 만들어지는 query는 예) {'product': <Product: p1>} 가 된다.

      - 모든 product 중 p1 이라는 name을 가지는 결과를 가져와 qs로 저장한다.

      - self.attname의 값, order를 기준으로 가장 마지막 객체를 가져와 last_item으로 저장한다.

      -  last_item.order에 1을 더한 값을 value에 저장하고 return 한다.

      - 만약 에러가 발생하면 value는 무조건 1이 된다.

     - 최초 if 조건이 맞지 않는다면 상속받은 부모의 pre_save 원형을 실행한다.

 

from django.core import checks
from django.core.exceptions import ObjectDoesNotExist
from django.db import models

class OrderField(models.PositiveIntegerField):

    description = "Ordering field on a unique field"

    def __init__(self, unique_for_field=None, *args, **kwargs):
        # Helper: 클래스 초기화 함수. `unique_for_field` 인자를 설정한다.
        # `unique_for_field`: 이 필드가 종속되는 다른 필드의 이름
        # `*args`와 `**kwargs`는 추가 인자들로 기본 필드 옵션을 설정하는데 사용된다.

        self.unique_for_field = unique_for_field 
        super().__init__(*args, **kwargs) # PositiveIntegerField의 초기화 함수를 호출한다.

    def check(self, **kwargs):
        # Helper: 모델 시스템 체크 시에 실행되며 필드의 유효성 검사를 수행한다.
        # `**kwargs`: 시스템 체커에 전달되는 추가적인 키워드 인자들

        return [
            *super().check(**kwargs), # 상위 클래스의 체크를 실행한다.
            *self._check_for_field_attribute(**kwargs), # 생성한 유효성 검사 함수를 실행한다.
        ]

    def _check_for_field_attribute(self, **kwargs):
        # Helper: 'unique_for_field' 속성이 유효한지 검사하는 내부 함수.
        # `**kwargs`: 시스템 체커에 전달되는 추가적인 키워드 인자들

        if self.unique_for_field is None:
            return [
                checks.Error("OrderField must define a 'unique_for_field' attribute")
            ]
        elif self.unique_for_field not in [
            f.name for f in self.model._meta.get_fields()
        ]:
            return [
                checks.Error(
                    "OrderField entered does not match an existing model field"
                )
            ]
        return []

    def pre_save(self, model_instance, add):
        # Helper: 객체가 저장되기 전에 호출되며 필드 값 계산을 수행한다.
        # `model_instance`: 현재 저장하려는 모델 인스턴스.
        # `add`: 객체가 새로 생성되는 경우 True, 아닐 경우 False.

        if getattr(model_instance, self.attname) is None:
            # 현재 필드 값이 없을 경우(order 값이 설정되지 않은 경우) 새로운 order 값 계산
            qs = self.model.objects.all() # 모든 객체를 가져오는 쿼리셋 생성
            try:
                query = {
                    self.unique_for_field: getattr(
                        model_instance, self.unique_for_field
                    )
                }
                qs = qs.filter(**query) # unique_for_field 값에 따라 필터링
                last_item = qs.latest(self.attname) # 가장 높은 order 값을 가진 객체를 가져옴
                value = last_item.order + 1 # 가장 높은 order 값에 1을 더한 값을 새로운 order 값으로 사용
            except ObjectDoesNotExist:
                value = 1 # 객체가 없을 경우 첫 번째 순서 값으로 1을 사용
            return value
        else:
            # 현재 필드 값이 있을 경우 그대로 반환
            return super().pre_save(model_instance, add) # 상위 클래스의 pre_save 메소드 실행

 

   - reference : 

https://docs.djangoproject.com/en/5.0/howto/custom-model-fields/

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

   2) models.py에서 clean 함수와 save 함수 정의하기 : 

      - clean 메소드는 full_clean()에 의해 호출되는 함수 중 하나다. full_clean()은 폼을 통해 제출될때(예를 들어 ModelForm의 is_valid() 메서드 내에서) 자동으로 수행되지만, 모델 인스턴스에 저장하는 경우 개발자가 명시적으로 호출해야 한다.

 

      - 이러한 full_clean() 메소드의 유효성 검사 단계들은 각각의 목적과 기능에 따라 명확한 역할을 수행합니다, 절차적으로 엄격한 데이터 관리를 가능하게 합니다. 모델 인스턴스가 항상 일관된 상태로 데이터베이스에 저장되도록 보장해 줌으로써, 데이터 무결성을 강화하고 애플리케이션의 신뢰성을 높일 수 있습니다.


      - 데이터의 유효성 검사와 관련된 코드를 짤 때는 이러한 단계들을 충분히 이해하고 적절히 활용해야 하며, 특히 save 메소드를 사용할 때 full_clean()을 명시적으로 호출하는 습관을 들이면 데이터의 안정성을 더욱 높일 수 있습니다.

 

      - full_clean()의 주요 단계 : 

         1. clean_fields() : 

            - 모델에 정의된 필드들을 검증합니다. 만약 필드의 데이터 타입이 해당 필드의 정의와 일치하지 않을 경우, ValidationError가 발생됩니다. exclude 속성을 사용하면 검증에서 제외할 필드를 지정할 수 있습니다.


         2. clean() :

            - 사용자가 정의한 모델의 clean 메소드를 호출하여 추가적인 유효성 검사를 할 수 있습니다. 여기서 비즈니스 로직에 따른 유효성 규칙을 적용할 수 있으며, 규칙에 위배될 경우 ValidationError를 발생시킬 수 있습니다.


         3. validate_unique() :

            - unique 또는 unique_together 등과 같이 유니크 제약 조건을 갖는 필드들이 데이터베이스 내에서 유니크한 값으로 유지되는지 검증합니다. 유니크 제약 조건을 위반하는 경우 ValidationError가 발생됩니다.

 

      - save()와 clean()의 동작 분석 :

         - 앞서 말한대로 model에 정의된 clean()은 자동으로 수행되지 않는다. 때문에 save()에서 self.full_clean()을 직접 호출하여 이후 clean()이 호출되게 유도한다.

         - 저장된 productline 데이터 중에 동일한 id가 없는 경우(신규 데이터), 저장하려는 order와 기존 order중 같은 값이 있다면 에러를 발생시키는 동작을 한다.

 

class ProductLine(models.Model):
    price = models.DecimalField(decimal_places=2, max_digits=5)
    sku = models.CharField(max_length=100)
    stock_qty = models.IntegerField()
    product = models.ForeignKey(
        Product, on_delete=models.CASCADE, related_name="product_line"
    )
    is_active = models.BooleanField(default=False)
    order = OrderField(unique_for_field="product", blank=True)
    objects = ActiveQueryset.as_manager()

    def clean(self):
        qs = ProductLine.objects.filter(product=self.product)
        for obj in qs:
            if self.id != obj.id and self.order == obj.order:
                raise ValidationError("Duplicate value.")

    def save(self, *args, **kwargs):
        self.full_clean()
        return super(ProductLine, self).save(*args, **kwargs)

    def __str__(self):
        return str(self.sku)

 

   - reference : 

https://velog.io/@gaya309/Django-fullclean-vs-cleanfields-vs-clean

 

Django : full_clean() vs clean_fields() vs clean()

개념 정리!

velog.io

https://docs.djangoproject.com/en/5.0/ref/models/instances/

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

   3) 1.0.2 강의 중간 정리 :

      - 커스텀 필드는 스택오버 플로우나 여러 포스트에서 언급하기 다루기 까다롭고 어렵다는 평이다.

      - 아직 product 필드에 대한 유니크 코드가 구현되지 않았다.

      -  clean()에서 무조건 동작을 처리하기 보다, if로 분기 조건을 두어 실행 과정을 줄이는게 더 효울적이라 생각된다. 

 

   - reference : 

https://www.youtube.com/watch?v=Iwcl6K2FJds&list=PLOLrQ9Pn6cayYycbeBdxHUFrzTqrNE7Pe&index=1

https://www.youtube.com/watch?v=davrrlaKEOI&list=PLOLrQ9Pn6cazL1rwTY2d66M9VppexGL-_&index=1

 

 7. admin 페이지에 inline urls 링크 달기

   - 현재 만들어진 admin의 Products의 페이지에는 inlines가 정의되어 있다.

   - Products의 하단에 추가되는 Product lines의 에디트 부분에 product image를 추가하는 부분을 만들고자 한다.

      - 해당 기능을 추가하는 것이 아니라, 수정할 수 있는 상세 페이지로 이동하는 링크를 각 항목별로 추가하는 것을 목표로 한다.

      - 이동되는 페이지는 관련된 항목의 product lines 상세 페이지로 이동하게 한다.

   -  class EditLinkInline(object) 클래스를 만든다. 해당 클래스는 def edit(self, instance) 함수를 가지고 있다.

      - 해당 함수는 instance.pk가 있는 경우 edit라는 이름의 상세 페이지로 이동하는 링크를 만들어 준다.

   - ProductImageInline, ProductLineInline를 각각 만들어 주고 ProductLineInline에 edit 필드를 정의해 준다.

   - ProductAdmin(admin.ModelAdmin), ProductLineAdmin(admin.ModelAdmin)에 각각 inlines를 정의해 준다.

from django.contrib import admin
from django.urls import reverse
from django.utils.safestring import mark_safe

from .models import Brand, Category, Product, ProductImage, ProductLine


class EditLinkInline(object):
    def edit(self, instance):
        url = reverse(
            f"admin:{instance._meta.app_label}_{instance._meta.model_name}_change",
            args=[instance.pk],
        )
        if instance.pk:
            link = mark_safe('<a href="{u}">edit</a>'.format(u=url))
            return link
        else:
            return ""


class ProductImageInline(admin.TabularInline):
    model = ProductImage


class ProductLineInline(EditLinkInline, admin.TabularInline):
    model = ProductLine
    readonly_fields = ("edit",)


class ProductAdmin(admin.ModelAdmin):
    inlines = [
        ProductLineInline,
    ]


class ProductLineAdmin(admin.ModelAdmin):
    inlines = [
        ProductImageInline,
    ]


admin.site.register(ProductLine, ProductLineAdmin)
admin.site.register(Product, ProductAdmin)
admin.site.register(Category)
admin.site.register(Brand)

 

   - reference : 

https://docs.djangoproject.com/en/5.0/ref/contrib/admin/

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

https://wayhome25.github.io/blog/page36/

 

Blog · 초보몽키의 개발공부로그

22 Mar 2017 | MySQL 데이터베이스 생활코딩 - MySQL 여러개의 테이블 사용하기 데이터의 규모가 커지면서 하나의 테이블로 정보를 수용하기가 어려워지면 테이블을 분활하고 테이블 간의 관계성을 부

wayhome25.github.io

https://devlog.jwgo.kr/2019/11/15/how-to-display-image-in-django-admin/

 

장고(Django) 어드민에서 이미지 썸네일 표시하는 방법 · Tonic

사이트 운영에 도움을 주실 수 있습니다. 고맙습니다. --> 장고(Django) 어드민에서 이미지 썸네일 표시하는 방법 2019년 11월 15일 목표 장고 어드민에서 이미지를 파일명이 아니라 썸네일로 표시하

devlog.jwgo.kr

 

 8. nesting(중첩) 시리얼라이즈 만들기 

   - 중첩 시리얼라이즈를 정의하는 경우, 참고하는 필드의 정보를 명확하게 정의해야 한다.

      - related_name이 정의되어 있다면, 해당 정보를 사용해야 한다.

# models.py
class ProductImage(models.Model):
    alternative_text = models.CharField(max_length=100)
    url = models.ImageField(upload_to=None, default="test.jpg")
    productline = models.ForeignKey(
        ProductLine, on_delete=models.CASCADE, related_name="product_image"
    )
    .
    .
    .
    
# serializers.py
class ProductImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProductImage
        exclude = ("id", "productline")


class ProductLineSerializer(serializers.ModelSerializer):
    product_image = ProductImageSerializer(many=True)

    class Meta:
        model = ProductLine
        fields = (
            "price",
            "sku",
            "stock_qty",
            "order",
            "product_image",
        )

 

 9. 성능: N+1 쿼리 문제 제거를 위한 다중 쿼리

   - 해당 문제는 select_related와 prefetch_related를 이용해 해결할 수 있다.

   - select_related은 join을 만들고, prefetch_related는 추가 query를 만든다. 

   - 이미 잘 알고 있는 문제로, 시리얼라이저의 동작에 의해 추가 발생할 수 있는 쿼리를 줄이는 방법의 코드를 정리한다.

      - 발생한 쿼리 수와 쿼리 내용을 출력하는 코드 포함

def retrieve(self, request, slug=None):
        serializer = ProductSerializer(
            Product.objects.filter(slug=slug)
            .select_related("category", "brand")
            .prefetch_related(Prefetch("product_line__product_image")),
            many=True,
        )
        data = Response(serializer.data)

        q = list(connection.queries)
        print(len(q))
        for qs in q:
            sqlformatted = format(str(qs["sql"]), reindent=True)
            print(highlight(sqlformatted, SqlLexer(), TerminalFormatter()))

        return data

 

 10. many to many 모델 설계하기 - 중계 모델을 직접 구현하기

   - 기본적으로 many-to-many의 모델은 아래와 같은 구조로 설계한다. 

   - 외래키는 unique 설정이 자동으로 되지 않기 때문에, 중복 데이터를 막기 위해 다중 unique 설정을 한다.

   - manytomanyfield는 실재 database table에는 존재하지 않는다. 시스템상 manytomany의 설정을 위해 정의한다.

from django.db import models

# ProductType 모델
class ProductType(models.Model):
    name = models.CharField(max_length=100, unique=True)
    attributes = models.ManyToManyField('Attribute', through='ProductTypeAttribute')

    class Meta:
        db_table = 'product_type'

# Attribute 모델
class Attribute(models.Model):
    name = models.CharField(max_length=100, unique=True)

    class Meta:
        db_table = 'attribute'

# ProductTypeAttribute 중계 테이블 모델
class ProductTypeAttribute(models.Model):
    product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE)
    attribute = models.ForeignKey(Attribute, on_delete=models.CASCADE)
    value = models.CharField(max_length=255) # 필요한 경우 여기에 추가 필드를 정의할 수 있습니다.

    class Meta:
        db_table = 'product_type_attribute'
        unique_together = ('product_type', 'attribute')

 

   - 프로젝트에 사용된 코드는 아래와 같다.

class Attribute(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)

    def __str__(self):
        return self.name
        
class ProductType(models.Model):
    name = models.CharField(max_length=100)
    attribute = models.ManyToManyField(
        Attribute,
        through="ProductTypeAttribute",
        related_name="product_type_attribute",
    )

    def __str__(self):
        return str(self.name)


class ProductTypeAttribute(models.Model):

    product_type = models.ForeignKey(
        ProductType,
        on_delete=models.CASCADE,
        related_name="product_type_attribute_pt",
    )
    attribute = models.ForeignKey(
        Attribute,
        on_delete=models.CASCADE,
        related_name="product_type_attribute_a",
    )

    class Meta:
        unique_together = ("product_type", "attribute")

 

 11. admin page에 many to many inline 구현하기

   - 기본적으로 admin.ModelAdmin을 상속받는 admin 구현 클래스는 내부 코드가 아무것도 없어도 기본적으로 register에 등록될 때 정의되는 models.py의 클래스를 기반으로 CURD의 기본 기능을 제공한다.

   - admin.TabularInline는 register에 등록되는 클래스가 아니기 때문에, 어떤 model을 사용하는지 모른다. 때문에 클래스 변수로 model을 정의해 준다.

   - manytomany의 설정을 한 경우 inlines의 설정을 하고 싶을 경우가 있다. 다음의 과정으로 구현된다.

      1) ManyToManyField가 정의되지 않은 model 클래스를 inline 클래스로 만든다.

         - A에 ManyToManyField가 있고 B는 없는 경우, B를 inline 클래스로 만들고 A에 정의한다.

 

      2) admin.TabularInline를 상속받는 inline 클래스의 모델은 상대 모델의 ManyToManyField에 정의된 related_name의 이름에 through를 붙인다.

         - AttributeValue에는 productline에 대한 외래키가 정의되어 있지 않다. 때문에, 중계 테이블의 외래키를 이용해 AttributeValue에 접근한다. 이때, 시작되는 모델은 접근하고자 하는 타겟 모델로 부터 시작되어야 한다.

class AttributeInline(admin.TabularInline):
    model = Attribute.product_type_attribute.through

 

      3) ManyToManyField가 정의된 model의 admin 클래스에 inline을 추가하고 admin.site.register()에 등록한다.

admin.site.register(ProductType, ProductTypeAdmin)

 

   - 코드에 구현된 admin 페이지 관련 코드는 다음과 같다.

      - model = Attribute.product_type_attribute.through 코드를 직접 정의한 중계 모델로 정의해도 된다.

class AttributeInline(admin.TabularInline):
    # model = Attribute.product_type_attribute.through
    model = ProductTypeAttribute

 

from django.contrib import admin
from django.urls import reverse
from django.utils.safestring import mark_safe

from .models import (
    Attribute,
    AttributeValue,
    Brand,
    Category,
    Product,
    ProductImage,
    ProductLine,
    ProductType,
)


class EditLinkInline(object):
    def edit(self, instance):
        url = reverse(
            f"admin:{instance._meta.app_label}_{instance._meta.model_name}_change",
            args=[instance.pk],
        )
        if instance.pk:
            link = mark_safe('<a href="{u}">edit</a>'.format(u=url))
            return link
        else:
            return ""


class ProductImageInline(admin.TabularInline):
    model = ProductImage


class ProductLineInline(EditLinkInline, admin.TabularInline):
    model = ProductLine
    readonly_fields = ("edit",)


class ProductAdmin(admin.ModelAdmin):
    inlines = [
        ProductLineInline,
    ]


class AttributeValueInline(admin.TabularInline):
    model = AttributeValue.product_line_attribute_value.through


class ProductLineAdmin(admin.ModelAdmin):
    inlines = [
        ProductImageInline,
        AttributeValueInline,
    ]


class AttributeInline(admin.TabularInline):
    # model = Attribute.product_type_attribute.through
    model = ProductTypeAttribute


class ProductTypeAdmin(admin.ModelAdmin):
    inlines = [
        AttributeInline,
    ]


admin.site.register(ProductLine, ProductLineAdmin)
admin.site.register(Product, ProductAdmin)
admin.site.register(Category)
admin.site.register(Brand)
admin.site.register(Attribute)
admin.site.register(ProductType, ProductTypeAdmin)
admin.site.register(AttributeValue)

 

 12. Customizing the Serialization Output

   1) 중첩 시리얼라이저와 필드의 출력 차이

      - serializer에서 fields 정의에 역참조로 정의된 외래키 필드도 정의할 수 있다. 이 때, related_name에 정의된 이름을 필드 정보에 정의한다.

class ProductLineSerializer(serializers.ModelSerializer):
    attribute_value = AttributeValueSerializer(many=True)

    class Meta:
        model = ProductLine
        fields = (
            "price",
            "sku",
            "stock_qty",
            "order",
            "product_image",
            "attribute_value",
        )

 

      - 이 경우 product_image의 결과는 pk 번호만 출력된다.

 

      - 만약, product_image의 모든 정보를 출력하고자 한다면 다음과 같이 정의할 수 있다.

class ProductLineSerializer(serializers.ModelSerializer):
    attribute_value = AttributeValueSerializer(many=True)
    product_image = ProductImageSerializer(many=True)

    class Meta:
        model = ProductLine
        fields = (
            "price",
            "sku",
            "stock_qty",
            "order",
            "product_image",
            "attribute_value",
        )

 

      - 출력되는 결과는 해당 모델의 모든 정보를 출력하게 된다.

 

   2) Custom Outputs, 출력을 커스텀 하기

      1. to_representation() allows us to change the serialization output

         - 서버에서 나가는 데이터 커스텀
      2. to_internal_value() allows us to change the deserialization output

         - 서버에 들어오는 데이터 커스텀

 

      - 이 프로젝트에서는 to_representation()를 사용하여 기존 attribute_value의 값을 다음과 같이 변경한다.

         - key["attribute"]["id"]: key["attribute_value"] 

            - attr_values 이름의 딕셔너리에 for을 이용해 모든 데이터를 순환하며 attribute의 id를 key로 정의하고 attribute_value의 값을 value로 정의한다.

            - 기존 키 값이 존재한다면, 값만 업데이트 하고 키 값이 없으면 생성하게 된다.

      - specification key를 만들고 attr_values에 만들어진 딕셔너리를 업데이트 혹은 추가한다.

class ProductLineSerializer(serializers.ModelSerializer):
    attribute_value = AttributeValueSerializer(many=True)

    class Meta:
        model = ProductLine
        fields = (
            "price",
            "sku",
            "stock_qty",
            "order",
            "product_image",
            "attribute_value",
        )

    def to_representation(self, instance):
        data = super().to_representation(instance)
        av_data = data.pop("attribute_value")
        attr_values = {}
        for key in av_data:
            attr_values.update({key["attribute"]["id"]: key["attribute_value"]})
        data.update({"specification": attr_values})

        return data

 

   - reference : 

https://testdriven.io/blog/drf-serializers/

 

Effectively Using Django REST Framework Serializers

This article looks at how to use Django REST Framework (DRF) serializers more efficiently and effectively.

testdriven.io

 

 13. product line 테이블에 중복되지 않는 속성값 할당하게 만들기.

   - 해당 부분은 코드만 분석한다.

class ProductLineAttributeValue(models.Model):
    attribute_value = models.ForeignKey(
        AttributeValue,
        on_delete=models.CASCADE,
        related_name="product_attribute_value_av",
    )
    product_line = models.ForeignKey(
        "ProductLine",
        on_delete=models.CASCADE,
        related_name="product_attribute_value_pl",
    )

    class Meta:
        unique_together = ("attribute_value", "product_line")

    def clean(self):
        qs = (
            ProductLineAttributeValue.objects.filter(
                attribute_value=self.attribute_value
            )
            .filter(product_line=self.product_line)
            .exists()
        )

        # 새로 추가가 되는 값이면

        if not qs:
            iqs = Attribute.objects.filter(
                attribute_value__product_line_attribute_value=self.product_line
            ).values_list("pk", flat=True)

            if self.attribute_value.attribute.id in list(iqs):
                raise ValidationError("Duplicate attribute exists")

    def save(self, *args, **kwargs):
        self.full_clean()
        return super(ProductLineAttributeValue, self).save(*args, **kwargs)

  

 

   - 아래 코드는 현재 입력된 값에서 새로 추가되는 값이 있는지 여부를 확인한다.

ProductLineAttributeValue.objects.filter(
                attribute_value=self.attribute_value
            )
            .filter(product_line=self.product_line)
            .exists()

---------------------------------------------------------------

In [12]: ProductLineAttributeValue.objects.filter(
    ...:                 attribute_value=6
    ...:             ).filter(product_line=7
    ...:            ).exists()
SELECT 1 AS "a"

 

   - 아래 코드는 Attribute 테이블을 attributevalu, productlineattributevalue와 join하여 아래와 같은 결과를 만든다.

 

   - self.product_line이 1이라 가정할 경우 아래와 같은 쿼리가 생성된다.

      - 해당 코드는 중계 테이블 ProductLineAttributeValue에 저장된 값 중 product_line가 1 결과의 id를 가져온다.

Attribute.objects.filter(
                attribute_value__product_line_attribute_value=self.product_line
            ).values_list("pk", flat=True)

----------------------------------------------------------

SELECT "product_attribute"."id",
       "product_attribute"."name",
       "product_attribute"."description"
  FROM "product_attribute"
 INNER JOIN "product_producttypeattribute"
    ON ("product_attribute"."id" = "product_producttypeattribute"."attribute_id")
 INNER JOIN "product_producttype"
    ON ("product_producttypeattribute"."product_type_id" = "product_producttype"."id")
 INNER JOIN "product_product"
    ON ("product_producttype"."id" = "product_product"."product_type_id")
 WHERE "product_product"."id" = 1
 LIMIT 21

 

   - 만약, 추가하려는 attribute의 값이 이전 중계 테이블의 값 중에 있다면 이는 중복이 된다.

if self.attribute_value.attribute.id in list(iqs):
                raise ValidationError("Duplicate attribute exists")

 

 14. serializer에 custom field 만들기

   - 필드의 속성을 serializers.SerializerMethodField()로 정의한다.

      - 해당 필드의 속성은 read-only이다.

   - 함수의 이름은 "get_정의한 필드명" 이다.

   - return 값으로 원하는 serializer에 추출한 데이터 쿼리셋을 넣고 .data를 호출하면 된다.

class ProductSerializer(serializers.ModelSerializer):
    brand_name = serializers.CharField(source="brand.name")
    category_name = serializers.CharField(source="category.name")
    product_line = ProductLineSerializer(many=True)
    attribute = serializers.SerializerMethodField()

    class Meta:
        model = Product
        fields = (
            "name",
            "slug",
            "description",
            "brand_name",
            "category_name",
            "product_line",
            "attribute",
        )

    def get_attribute(self, obj):
        attribute = Attribute.objects.filter(product_type_attribute__product__id=obj.id)
        return AttributeSerializer(attribute, many=True).data

 

   - reference : 

https://leffept.tistory.com/319

 

[Django]DRF SerializerMethodField() 란?

SerializerMethodFiled() 란? 연결되어 있는 serializer 클래스에서 메서드를 호출하여 값을 가져올 수 있는 읽기 전용 필드이다. 객체의 serializer 된 표현에 모든 종류의 데이터를 추가하는데 사용할 수 있

leffept.tistory.com

https://eunjin3786.tistory.com/268

 

[DRF] SerializerMethodField로 모델에서 변형된 JSON을 내려주기

[DRF] 모델과 ModelSerializer 만들기 에서 모델을 JSON으로 쉽게 바꿀 수 있도록 해주는 ModelSerializer를 알아봤는데요, 만약- 모델에 없는 필드인데 JSON에 특정 필드를 추가해서 내려주고 싶거나 - 모델

eunjin3786.tistory.com

https://velog.io/@oen/SerializerMethodField-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EB%B2%88%EC%97%AD

 

[Django] SerializerMethodField로 모델 필드 값을 변형해서 새로운 필드로 반환하기

이렇게 Video 모델에는 person이라는 필드만 있는데프론트팀에서 위와 같이객체의'original' 필드 값 중 'url' 키에 해당하는 값('a/b/c') 만 따로 name 이라는 필드에 추가해서 API를 보내달라는 요청이 했

velog.io

https://tech.toktokhan.dev/2021/04/28/SerializerField-query-optimize/

 

SerializerMethodField() 사용의 쿼리 최적화

SerializerMethodField() 사용의 쿼리 최적화 안녕하세요 똑똑한 개발자에서 백엔드 개발을 하고 있는 jujun입니다. SerializerMethodField는 원하는 key value를 만들 수 있게 도와주지만 잘못 사용하면 N+1 문제

tech.toktokhan.dev

 

 15. Many-to Many 관계를 Factory를 이용해 testing 하기

   - post_generation 데코레이션은 객체 생성 후에 추가적인 처리를 하고 싶을 때 유용하게 사용할 수 있다.

      - 1:N, N:N의 관계에서 자신의 객체 생성 후 추가적으로 다른 테이블에 데이터를 추가하고 싶은 경우

      - created_at와 같은 필드의 auto_now_add 속성은 객체가 생성되는 시점에 할당되어야 한다.

      - 기본 구현 예시는 아래와 같다.

import factory

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = 'myapp.User'

    username = factory.Sequence(lambda n: "user%d" % n)
    
    @factory.post_generation
    def groups(self, create, extracted, **kwargs):
        if not create:
            # Simple build, do nothing
            return

        if extracted:
            # A list of groups were passed in, use them
            for group in extracted:
                self.groups.add(group)
                

# 테스트 코드
# groups를 설정해야 하는 유닛 테스트가 있다고 가정
def test_user_creation_with_groups():
    group1 = Group.objects.create(name='Group1')
    group2 = Group.objects.create(name='Group2')
    user = UserFactory(groups=(group1, group2))
    # 이제 user는 group1과 group2에 속합니다.

         1) create (bool) :

            - 의미: 인스턴스가 생성(build)되었는지 아니면 실제로 저장(create)되었는지를 결정하는 불리언 값입니다.

            - 사용: create=True일 때 데이터베이스에 객체가 저장된 경우이므로, post_generate 후크 안에서 데이터베이스 상의 작업(예: 관계 설정)을 수행할 수 있습니다.

 

         2) extracted :

            - 의미: post_generation 메서드 호출 시 누락된 인자(즉, Factory 호출 시에 전달된 추가 인자)가 있는 경우 extracted에 전달됩니다. 즉, 객체 생성 시 전달한 추가적인 값을 수신하기 위해 사용됩니다.

            - 사용: 사용자가 Factory를 호출하면서 특정 필드에 대해 특정 값을 제공했을 때 그 값을 활용할 수 있습니다.

 

         3) build의 조건 :

            - 주로 사용되는 경우: 로직을 테스트할 때 데이터베이스에 영향을 주지 않고 객체의 인스턴스만 필요할 때 사용합니다.
            - 조건: 객체 생성 시 메모리에서만 인스턴스를 만들고 싶을 때 Factory.build() 또는 Factory()를 사용하고, strategy=BUILD 옵션을 전달합니다.
            - 저장: build는 데이터베이스에 객체를 저장하지 않습니다.

 

         4) create의 조건 :

            - 주로 사용되는 경우: 데이터베이스 상의 실제 객체가 필요할 때, 예를 들면 객체 간의 관계를 설정하거나, 실제 데이터에 기반한 테스트를 할 때 사용합니다.
            - 조건: 객체 생성 시 데이터베이스에 저장까지 하고 싶을 때 Factory.create() 또는 Factory()를 사용하고, strategy=CREATE 옵션을 전달합니다.
            - 저장: create는 객체를 데이터베이스에 실제로 저장합니다.

 

         5) extracted의 이해 :

            - 용도: Factory에서 객체를 생성한 후, 사용자가 지정한 추가적인 값을 처리하는데 사용됩니다.
            - post_generation 메서드: 파라미터로 **kwargs를 받아 해당 메서드 안에서 extracted 변수로 접근할 수 있습니다.

            - extracted 사용의 예 :

import factory

class MyModelFactory(factory.Factory):
    class Meta:
        model = MyModel
    
    # Post-generation method
    @factory.post_generation
    def add_attribute(obj, create, extracted, **kwargs):
        if extracted:
            # extracted에 전달된 값이 있는 경우, obj에 추가적인 작업 수행
            obj.attribute = extracted
# add_attribute에 '새로운 값'을 전달하여 객체 생성
my_instance = MyModelFactory(add_attribute='새로운 값')
print(my_instance.attribute) # '새로운 값'

 

   - 프로젝트 코드 분석 :

class ProductLineFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = ProductLine

    price = 10.00
    sku = "12345"
    stock_qty = 1
    product = factory.SubFactory(ProductFactory)
    is_active = True

    @factory.post_generation
    def attribute_value(self, create, extracted, **kwargs):
class TestProductLineModel:
    def test_str_method(self, product_line_factory, attribute_value_factory):
        attr = attribute_value_factory(attribute_value="test")
        obj = product_line_factory.create(sku="12345", attribute_value=(attr,))
        assert obj.__str__() == "12345"

 

   - reference :

https://cocook.tistory.com/177

 

[FactoryBoy] Trouble Shooting

테스트를 작성하면서 특정모델 A에 역참조인 모델을 A가 생성되면서 같이 생성해야하는 일이 있었는데 구현하면서 꽤 고생했다. 이 과정을 정리해둔다. 아래의 간단한 예시를 사용했다. class Comp

cocook.tistory.com

https://factoryboy.readthedocs.io/en/stable/reference.html

 

Reference — Factory Boy stable documentation

Post-generation hooks Some objects expect additional method calls or complex processing for proper definition. For instance, a User may need to have a related Profile, where the Profile is built from the User object. To support this pattern, factory_boy pr

factoryboy.readthedocs.io

https://medium.com/peter-kilczuk-software-engineer/factory-boy-post-generation-demystified-dc348c67e03c

 

factory_boy Post-Generation Demystified

Python’s Factory_boy post-generation is a super-powerful tool out there to help you generate quality test data. But there aren’t too many…

medium.com

 

댓글