본문 바로가기

python & django & scipy

[django] Model Manager - ORM 정복기 1탄

목차

  • 들어가기 전에,
  • objects?
  • django.db.models.manager.Manager
    • BaseManager
    • Manager
    • Python Descriptor
    • ManagerDescriptor
    • Model의 objects로 설정되는 부분
    • __new__ -> _prepare -> add_to_class -> contribute_to_class
  • example

 

숙원 사업(?), ORM이 SQL query를 만드는 과정

항상 엄두를 못내고 수박 겉핥기로 serializer만 뜯어보고 있었던 ORM

회사에서 스터디를 진행하면서 매번 팀원들과 ORM to SQL Query 과정을 뜯어보면 해갈될 것 같다는 말을 반년째 하다가 드디어 스타트를 끊었다

 

 

들어가기 전에,

django ORM의 기초를 잡고 가보자!

오늘도 chatGPT 선생님이 고생해주셨습니다.

 

Q. Django ORM에 대한 개괄적인 설명을 부탁해

A>

Django ORM은 "객체-관계 매핑"의 약자로, 데이터베이스와 객체 지향 프로그래밍 간의 간극을 줄이고 데이터베이스와 상호 작용을 추상화하는 기술입니다. Django ORM은 데이터베이스 테이블을 Python 클래스로 나타내고, Python 코드를 사용하여 데이터베이스 작업을 수행할 수 있게 해줍니다. 이를 통해 개발자는 SQL을 직접 작성하지 않아도 데이터베이스 조작을 수행할 수 있습니다.

Django ORM의 주요 개념과 특징은 다음과 같습니다:

  1. 모델 클래스 (Model Classes): Django ORM에서 데이터베이스 테이블은 Python 클래스로 정의됩니다. 각 클래스의 필드는 테이블의 열과 매핑됩니다. 이러한 모델 클래스는 데이터베이스 구조를 정의하는 동시에 데이터베이스 작업을 위한 기능을 제공합니다.
  2. 매니저 (Manager): ’objects’ 매니저는 모델 클래스에 자동으로 추가되며, 데이터베이스 작업을 수행하는 메서드를 포함합니다. 이를 사용하여 데이터를 검색, 생성, 수정, 삭제할 수 있습니다.
  3. 쿼리셋 (QuerySet): 쿼리셋은 데이터베이스로부터 가져온 데이터의 집합을 나타내는 객체입니다. 쿼리셋은 데이터베이스 쿼리를 생성하고 실행하며, 여러 데이터를 검색하거나 가공하는데 사용됩니다.
  4. 필드 타입 (Field Types): 모델 클래스의 각 필드는 데이터베이스 열과 대응되며, Django ORM은 다양한 데이터 타입에 대한 필드를 제공합니다. 예를 들어, 문자열, 숫자, 날짜 등의 데이터를 저장할 수 있는 필드 타입이 있습니다.
  5. Migrations: Django ORM은 데이터베이스 스키마를 관리하기 위해 "마이그레이션"을 사용합니다. 마이그레이션은 모델 클래스의 변경 사항을 데이터베이스에 반영하거나 역으로 취소할 수 있는 방법을 제공합니다.
  6. 쿼리 메서드 (Query Methods): Django ORM은 다양한 쿼리 메서드를 제공하여 데이터를 필터링하고 가공할 수 있습니다. filter(), exclude(), annotate() 등의 메서드를 사용하여 복잡한 쿼리 작업을 수행할 수 있습니다.
  7. 관계 (Relationships): 데이터베이스 테이블 간의 관계를 나타내기 위해 외래 키와 관련된 필드 타입을 사용할 수 있습니다. 이를 통해 데이터베이스의 관계성을 객체 간의 관계로 나타낼 수 있습니다.

Django ORM은 개발자가 데이터베이스와 상호 작용하는 과정을 단순화하고 추상화하여 개발 생산성을 향상시키는 강력한 도구입니다. SQL 작성 없이도 데이터베이스 조작을 할 수 있도록 해주며, 코드의 가독성과 유지 보수성을 높이는 역할을 합니다.

 

 

objects?

우리가 queryset을 작성할 때, 항상 모델 클래스에서 불러오는 objects라는 놈은, Django ORM Manager이다. 이 모델 매니저는 데이터베이스 쿼리와 연동되는 인터페이스이다.

커스텀으로 만들어서 사용할 수도 있다. 하지만, 지금 중요한 내용은 아니니까 패스하자

알다시피 기본적으로 모든 모델 클래스가 objects라는 Manager를 갖고있다.

 

해당 모델도 objects라는 attribute로 Manager를 갖고있는 것을 알 수 있다.

 

FoodProduct는 커스텀 매니저를 설정하지 않았으므로 기본 매니저로 django.db.models.manager.Manager를 들고있다.

FoodProduct.objects

 

django.db.models.manager

Manager class가 정의되어있는 django.db.models.manager.py 를 살펴보자.

manager.py에는 네가지 클래스가 정의돼있다.

django.db.models.manager structure

BaseManager

  • Django 모델 클래스의 매니저를 커스터마이징하고 확장하기 위한 기본 클래스
  • BaseManager를 사용하여 새로운 매니저를 정의할 때, 해당 매니저에서 반환하는 쿼리셋에 원하는 쿼리 메서드를 추가할 수 있습니다. 이를 통해 모델 클래스에 대한 다양한 데이터베이스 작업을 쉽게 수행할 수 있습니다.

django.db.models.manager.BaseManager 구성

 

공식 문서에 따르면 아래가 가능하다는 것

class PersonQuerySet(models.QuerySet):
    def authors(self):
        return self.filter(role="A")

    def editors(self):
        return self.filter(role="E")


class PersonManager(models.Manager):
    def get_queryset(self):
        return PersonQuerySet(self.model, using=self._db)

    def authors(self):
        return self.get_queryset().authors()

    def editors(self):
        return self.get_queryset().editors()


class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    role = models.CharField(
        max_length=1, choices=[("A", _("Author")), ("E", _("Editor"))]
    )
    people = PersonManager()


authors = Person.people.authors()

 

회사에서 자주 사용하는 util같은 queryset이나, ModelSerializer의 get_field 메소드에서 처리하도록 하는 로직을 쿼리셋에 적용할 수 있었다. 실제 코드는 아니지만 어쨌든

 

example 1)

class FoodProductManager(models.Manager):
    def get_queryset(self):
        return QuerySet(self.model, using=self._db)

    def is_true(self):
        return self.get_queryset().filter(a=True, b=True, c=True)


class FoodProduct(models.Model):
    # 생략
    food_products = FoodProductManager()


FoodProduct.food_products.is_true()

 

example 2)

class CustomManager(models.Manager):
    def subquery_applied(self, started_at=datetime.datetime(1960, 1, 1, tzinfo=KST), ended_at=datetime.datetime(2099, 12, 31, tzinfo=KST)):
        subquery1 = CustomModelState.objects.filter(
            status=1,
            id=OuterRef('id'),
            is_complete=True,
            updated_at__range=(started_at, ended_at),
        )
        subquery2 = FoodOrderState.objects.filter(
            status=2,
            id=OuterRef('id'),
            is_complete=True,
            updated_at__range=(started_at, ended_at),
        )

        subquery_applied = self.get_queryset().annotate(
            s1=Exists(subquery1),
            s2=Exists(subquery2)
        ).filter(s1=True, s2=False)

        return subquery_applied

class CustomModel(models.Model):
    manager = FoodOrderManger()
    

CustomModel.manager.subquery_applied()

 

 

Manager

default로 model의 manager(’objects’)로 사용되는 클래스이다.

django.db.models.manager.Manager 코드 전체

BaseManager.from_queryset(QuerySet)의 return 값을 상속받는다.

해당 method를 살펴보자.

django.db.models.manager.BaseManager.from_queryset

queryset classclass name을 인자로 받아 BaseManager를 상속받는 새로운 클래스를 생성해 return한다.

이 때 새로 생성되는 manager classQuerySet(default)의 method들(filter, all, annotate, values 등)도 갖는다.

이를 새로운 manager class에 set한다.

따라서 Model.objects.filter(), Model.objects.annotate()등, manager인 objects가 QuerySet 클래스의 method를 사용할 수 있는 것은 이 때문이다.

기본적으로 Manager 클래스를 사용하기 때문에 manager는 기본 QuerySet 클래스의 메소드를 가지게 된다.

Custom QuerySet method를 사용하고 싶다면 manager와 queryset 클래스를 재정의해서 사용하면 된다.

그 다음으로는 ManagerDescriptor라는 class가 눈에 보인다.

사실 눈에 안보였다. 원활한 설명을 위해 눈에 보였다고 하겠다.

 

 

Python Descriptor

ManagerDescriptor를 뜯어보기 전에 Python의 Descriptor를 알아보자

파이썬 디스크립터(Descriptor)는 클래스의 속성에 대한 접근, 할당, 삭제 등의 동작을 커스터마이징할 수 있도록 도와주는 프로토콜이다. 이를 통해 속성에 접근하는 방식을 제어하거나 유효성 검사를 수행하거나 다른 특별한 동작을 정의할 수 있다.

디스크립터를 사용하는 가장 일반적인 경우는 프로퍼티(Properties)입니다. 프로퍼티는 클래스의 속성을 감싸고, 속성에 접근할 때 호출되는 메서드를 정의한다. 이를 통해 속성에 대한 읽기 및 쓰기 동작을 커스터마이징할 수 있다.

Descriptor를 구현하려면 __get__, __set__, __delete__ 중 적어도 하나를 정의해야한다.

* 파이썬 공식문서 : https://docs.python.org/ko/3/howto/descriptor.html

 

 

example)

 

example - 실행)

 

ManagerDescriptor

ManagerDescriptor는 django 내부에서 사용되는 descriptor 중 하나로, 모델 클래스의 매니저 객체를 인스턴스화하여 접근하는 기능을 제공하는 역할을 한다. Descriptor이기 때문에 ManagerDescriptor를 통해서 모델 클래스의 인스턴스에서 Manager를 호출하거나 사용할 수 있다.

ManagerDescriptor 코드를 살펴보자.

 

django.db.models.manager.ManagerDescriptor

class ManagerDescriptor:

    def __init__(self, manager):
        self.manager = manager

    def __get__(self, instance, cls=None):
        if instance is not None:
            raise AttributeError("Manager isn't accessible via %s instances" % cls.__name__)

        if cls._meta.abstract:
            raise AttributeError("Manager isn't available; %s is abstract" % (
                cls._meta.object_name,
            ))

        if cls._meta.swapped:
            raise AttributeError(
                "Manager isn't available; '%s' has been swapped for '%s'" % (
                    cls._meta.label,
                    cls._meta.swapped,
                )
            )

        return cls._meta.managers_map[self.manager.name]
				# return Address._meta.managers_map['objects']

 

 

 

Model의 objects로 설정되는 부분

그럼 Model 클래스에서 Manager는 언제 set될까?

Model 클래스들은 기본적으로 objects라는 이름으로 Manager()를 갖는다.

어딘지 뜯어보자

냅다 무식하게 objects를 검색때렸더니 ModelBase._prepare()라는 method가 눈에 띈다.

 

django.db.models.ModelBase._prepare(cls)

def _prepare(cls):
    """Create some methods once self._meta has been populated."""
    opts = cls._meta
    opts._prepare(cls)

    # 중략

    if not opts.managers:
        if any(f.name == 'objects' for f in opts.fields):
            raise ValueError(
                "Model %s must specify a custom Manager, because it has a "
                "field named 'objects'." % cls.__name__
            )
        manager = Manager()
        manager.auto_created = True
        cls.add_to_class('objects', manager)

    # 후략

Manager를 instantiate해서 클래스에 ‘objects’라는 이름으로 set한다.

cls에 설정하는 것이기 때문에, 한 모델당 manager 인스턴스는 하나이다.

 

Model._prepare(==ModelBase._prepare)는 언제 수행될까?

_prepare의 가장 첫줄에 bp를 걸고 알아보자

accounts.models.Address(Custom Model class)의 _prepare가 수행되는 과정

 

콜스택을 살펴보면 익숙한 부분이 눈에 띈다.

apps.populate 하는 부분의 Phase 2(import models modules)

django는 setup 중 모델을 전부 import 하는 부분에서 모델 클래스를 한 번씩 읽어온다.

Apps.registry - Phase 2

 

이 때, 모든 Model 클래스의 Metaclass인 ModelBase의 __new__()가 실행된다.

 

django.db.models.ModelBase.__new__

class ModelBase(type):
		"""Metaclass for all models."""
    def __new__(cls, name, bases, attrs, **kwargs):

        # 생략
        new_class._prepare() # 이 부분이다.
        new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
	      return new_class

ModelBase의 __new__()에서 _prepare()를 호출한다. 이 때, 특별히 지정된 manager가 없다면 django.db.models.Manager instance를 생성해서 objects로 지정한다.

 

__new__→ _prepare → add_to_class → contribute_to_class

실제로 Model에 set되는 코드를 뜯어보자. 왜냐면 즐거우니까

 

 

1. ModelBase.__new__


app.populate 시 import_models를 수행하면서 class를 생성한다.

class ModelBase(type):
		"""Metaclass for all models."""
    def __new__(cls, name, bases, attrs, **kwargs):

        # 생략

        new_class._prepare() # 이 부분이다.
        new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
	      return new_class

 

2. ModelBase._prepare

 

Manager 인스턴스를 생성하고, cls.add_to_class를 호출한다.

def _prepare(cls):
    """Create some methods once self._meta has been populated."""
    opts = cls._meta
    opts._prepare(cls)

    # 중략

    if not opts.managers:
        if any(f.name == 'objects' for f in opts.fields):
            raise ValueError(
                "Model %s must specify a custom Manager, because it has a "
                "field named 'objects'." % cls.__name__
            )
        manager = Manager()
        manager.auto_created = True
        cls.add_to_class('objects', manager)

    # 후략



3. ModelBase.add_to_class

 

value의 클래스에 contribute_to_class라는 메소드가 있으면 해당 메소드를 실행하고, 아니면 setattr를 수행한다.

Manager는 contribute_to_class가 있으니 manager의 contribute_to_class를 실행한다.

def add_to_class(cls, name, value): # name = 'objects', value = manager(==Manager())
    if _has_contribute_to_class(value):
        value.contribute_to_class(cls, name)
    else:
        setattr(cls, name, value)

 

4. BaseManager.contribute_to_class

 

def contribute_to_class(self, cls, name): 
    # self = manager, cls = accoutns.models.Address, name = 'objects'
    self.name = self.name or name
    self.model = cls

    setattr(cls, name, ManagerDescriptor(self))

    cls._meta.add_manager(self)

Model class에 ‘objects’라는 이름으로 manager가 아니라 ManagerDescriptor를 인스턴스화 시켜서 setattr한다.

그리고, {Model class}._meta에는 manager를 add_manager한다. 그냥 list에 append하는 메소드니 굳이 확인하지는 말자

 

여기까지 진행하면

class CustomModel:
    objects = ManagerDescriptor(Manager())

가 돼서 Descriptor pattern을 사용하게 된다.

 

 

오늘의 example

 

간단한 queryset을 호출해보자

class FoodProductViewSet(viewsets.GenericViewSet):
    permission_classes = [AllowAny]

    def list(self, request):
        food_products = FoodProduct.objects.all() # bp
        serializer = FoodProductSerializer(food_products, many=True)
        return Response(DTOResponseFormatter.run(serializer.data))

FoodProduct.objects를 한 순간 ManagerDescriptor의 __get__이 실행된다.

다음은 objects에서 정의하고 있는 all() method를 실행한다.

filter나 다른 queryset method도 동일하다.

사실 다르긴 한데, 일단은 여기서 마무리