logo
Published on

Django contenttypes Framework, Generic Relation 살펴보기

Authors

TL;DR

Django 에는 contenttype framework 라는것이 존재한다. 사용하는 예시를 보면 Model 에 ContentType 이라는 djagno 에서 제공하는 model 에 대한 ForeignKey Field 를 정의하고 있다. 그냥 연결하고 싶은 모델에 ForeignKey 를 지정하지 않고 왜 ContentType 이라는 것을 통해 연결하는 것일까?

class TaggedItem(models.Model):
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey("content_type", "object_id")

contenttype framework 를 사용하는 이유

  1. 다형성 지원
    • ContentType Framework를 사용하면 모델 간의 관계를 유연하게 설정할 수 있습니다.
    • 예를 들어, 한 모델에서 다른 모델로의 외부 키 관계를 설정할 때 실제 모델의 이름을 직접 사용하는 대신 ContentType을 사용하여 모델 간의 관계를 지정할 수 있습니다.
    • 이를 통해 모델의 유형을 동적으로 변경할 수 있으며, 다형성을 구현할 수 있습니다.
  2. 데이터베이스의 결합을 피하기
    • ContentType Framework를 사용하면 애플리케이션의 다른 부분 간의 강력한 결합을 피할 수 있습니다.
    • 모델이나 뷰 등에서 직접적으로 모델 이름을 사용하는 대신 ContentType을 통해 모델에 접근하면 데이터베이스 스키마의 변경이나 모델의 변경에 더 유연하게 대응할 수 있습니다.
  3. 동적 쿼리 및 검색
    • ContentType을 사용하면 애플리케이션에서 동적으로 쿼리를 작성하고 데이터를 검색할 수 있습니다.
    • 예를 들어, 특정 모델 유형의 모든 객체를 검색하거나 특정 모델과 연관된 객체를 찾는 등의 작업을 수행할 수 있습니다.
  4. 관리자 패널에서의 활용
    • ContentType Framework는 Django의 관리자 패널에서 모델 간의 관계를 관리하는 데 도움을 줍니다.
    • 관리자 패널을 사용하여 다른 모델과의 관계를 설정하고 관리할 때 ContentType을 사용하여 모델을 동적으로 선택할 수 있습니다.

한마디로 말하면 contenttype framework 는 Django 의 model 간 관계에 유연성과 확장성을 향상시켜준다고 할 수 있다. 공식 문서에서는 이를 어떻게 안내하고 있는지 확인해보자

Overview in Django Documentation

Django includes a contenttypes application that can track all of the models installed in your Django-powered project, providing a high-level, generic interface for working with your models.

Django 는 우리의 Django project 에 설치된 모든 모델을 추적할 수 있는 contenttypes 라는 고수준, 일반 인터페이스를 제공하는 어플래케이션이 포함한다. 그 핵심에는 django.contrib.contenttypes.models.ContentType 모델이 존재하는데 이는 우리 프로젝트에 설치된 모델에 대한 정보를 저장한다. 새로운 모델이 설치되면 새로운 ContentType 모델 레코드 또한 생성된다.

ContentType 모델의 instance 는 그들을 나타내는 모델 클래스와 쿼리 객체를 리턴하는 메소드를 가진다. ContentType 은 또한 ContentType 과 함께 작동하고 특정 모델을 위해 ContentType 의 인스턴스를 획득하는 custom manager 를 가진다.

우리가 생성한 모델과 ContentType 같 관계는 또한 우리의 모델과 그 어떤 모델 인스턴스 간 'generic' 관계로 활용될 수 있다.

Installing contenttypes framework

아래와 같이 settings.py 의 INSTALLED_APPS 에 정의해 사용할 수 있다.

INSTALLED_APPS = [
    ...
    'django.contrib.contenttypes',
    ...
]

The ContentType Model

ContentType 의 모든 instance 들은 함께 유니크 하게 기재된 모델에 해다오디는 두개의 필드를 가진다.

  • app_label

    • 모델이 속해있는 app 의 이름이다. app_label 에서 가져와 저장되며 import path 의 마지막 부분으로 정의된다. 예를 들면 django.contrib.contenttypes 에서 contenttypes 만 가져온다.
  • model

    • 모델 클래스 명

추가적으로 아래의 항목이 가능하다.

  • name
    • 사람이 읽을 수 있는 content type. 모델 attribute 중 verbose_name 을 가져온다. verbose 는 장황한 이라는 뜻인데, 직관적으로 이해가 가능한 수준으로 설명한 이라는 뜻일 것이라 생각된다.

Methods on ContentType instances

모든 ContenType 인스턴스는 가르키거나 그 모델에서 가져온 객체 혹은 어떠한 모델에 대한 ContentType instance 에서 비롯된 메소드를 가진다.

ContentType.get_object_for_this_type(**kwargs)

ContentType 을 나타내는 모델을 위해 적절한 lookup arguments 를 취하며, 모델의 get() lookup 은 일치하는 객체를 반환한다.

ContentType.model_class()

ContentType 을 나타내는 Model class 를 반환한다.

예를 들어 우리는 User model 의 ContentType 을 참조 할 수 있다.

>>> from django.contrib.contenttypes.models import ContentType
>>> user_type = ContentType.objects.get(app_label="auth", model="user")
>>> user_type
<ContentType: user>

그리고 이것을 통해 특정 User 를 쿼리 하거나 User model class 에 접속할 수 있다.

>>> user_type.model_class()
<class 'django.contrib.auth.models.User'>
>>> user_type.get_object_for_this_type(username="Guido")
<User: Guido>

get_object_for_this_type() 와 model_class() 가능하게 하는 매우 중요한 사례

  1. 위 2개의 메소드를 사용할 때, 우리는 설치된 어떠한 모델에 쿼리를 실행하는 고수준 제네릭 코드를 작성할 수 있다. 이는 단일 모델 클래스를 import 하여 사용하는 방식에 대비하여 런타임 시 app_label과 모델을 ContentType 반환 시 pass 시킬 수 있다.
  2. 그리고 이것은 model class 를 사용하거나 이것에서 객체를 받아오는데 사용된다. 우리는 ContentType에 서로 다른 모델을 연결할 수 있는데 아래의 방식으로써 수행할 수 있다.
    • 특정 모델 클래스의 인스턴스에 대한 형식을 정하거나 (typing instances)
    • 두 메소드를 모델 클래스에 접근하기 위해 사용하기 위해 사용한다.

몇몇 번들된 Django 앱들의 경우 마지막 테크닉을 유용하게 한다. 예를 들어, Django authentication framework 의 permission 시스템의 경우 Permission model 을 ContentType ForeignKey 와 함께 사용한다. 이것은 Permission 이 "블로그 요소를 추가할 수 있음" 또는 "뉴스 스토리를 삭제할 수 있음" 과 같은 컨셉을 가능하게 한다.

ContentType Manager

ContentType 은 또한 아래의 메소드가 포함된 custom manager, ContentTypeManager 를 가진다.

class ContentTypeManager

clear_cache()

내부 캐시를 클리어하는것은 ContentType 이 ContentType 인스턴스에 의해 만들어진 models 추적에 사용한다. 아마 우리는 우리가 스스로 실행할 일은 없을 것이다. Django 는 필요할때 알아서 이 메소드를 실행한다.

get_for_id(id)

ID 를 통해 ContentType 을 참조한다. 이 메소드는 동일한 공유된 캐시를 사용하는데, 이 메소드는 일반적으로는 ContentType.objects.get(pk=id) 메소드를 실행하는것이 더 선호된다.

get_for_model(model, for_concrete_model=True)

모델 클래스나 모델 인스턴스를 취하고 모델이 나타내는 ContentType 인스턴스를 반환한다. for_concrete_model=False 는 proxy model 의 ContentType 을 fetch 하는것을 허용한다.

get_for_models(*models, for_concrete_models=True)

다양한 수의 모델 클래스를 취하고 ContentType 인스턴스가 바라보는 모델 클래스의 딕셔너리 매핑을 반환한다. for_concrete_models=False 는 proxy models 의 ContentType 을 fetch 하는것을 허용한다.

get_by_natural_key(app_label, model)

주어진 app_label, model 명에 의해 유일하게 식별 가능한(uniquely identified) ContentType 인스턴스를 반한한다. 이 메소드의 주된 목적은 ContentType 객체를 deserialization 시 natural key 에 의해 참조되도록 허용하는 것이다.

우리가 ContentType 를 사용해야 할 때 임을 안다면 get_for_model() 메소드가 특히 유용할 수 있다. 하지만 수동 참조를 한다면 모델의 메타데이터를 획득하는데에 문제가 발생할 수 있다.

>>> from django.contrib.auth.models import User
>>> ContentType.objects.get_for_model(User)
<ContentType: user>

Generic Relation

Content Type 에 대해 어느정도 이해했으니, 이제 Generic Relation 을 살펴볼 때가 왔다.

다른 모델에서 ContentType 모델로 foreign key 를 추가하는 것은 추가한 아래에 제시된 Permission model 의 예시와 같이 모델로 하여금 다른 모델과 효과적으로 묶일수 있도록 해준다. 하지만 한걸음 더 나아가게 하고 ContentType 모델을 사용하는 것은 진정으로 generic(때로는 "polymorphic" 이라고도 불린다.) 한 관계를 가능하게 한다.

Tagging system 에서의 예시는 아래와 같다.

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models


class TaggedItem(models.Model):
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey("content_type", "object_id")

    def __str__(self):
        return self.tag

    class Meta:
        indexes = [
            models.Index(fields=["content_type", "object_id"]),
        ]

일반적인 ForeignKey 는 다른 모델을 가르킬(point to) 수 있다. 이것은 만약 TaggedItem 모델이 ForeignKey 로 사용되었을 때 단 하나뿐인 모델을 태그 저장을 위해 선택해야 한다는 것을 말한다. contenttypes 어플리케이션은 이와 같은 상황에서 작동하고 어떠한 모델과의 관계도 허용하는 GenericForeignKey 라는 특수한 필드를 제공한다.

class GenericForeignKey

GenericForeignKey 는 아래의 3가지 설정을 가진다.

  1. 내가 사용하고자 하는 ForeignKey 에 ContentType 을 부여한다. 본 필드를 위한 일반적인 이름은 content_type 이다.
  2. 내가 작성하는 모델에 내가 연결하고자 하는 모델의 pk 를 저장할 수 있는 필드를 제공한다. 대다수의 모델에서 ㄱ이것은 PositiveIntegerField 이다. 일반적인 이름은 object_id 로 한다.
  3. 내가 작성하는 모델에 GenericForeignKey 를 부여하고 상기한 두 필드를 통과시킨다. ('content_type', 'object_id') 이것은 GenericForeignKey 의 일반적인 모습이 될 것이다.

ForeignKey 와는 다르게 GenericForeignKey 에서는 database index 가 자동생성되지 않는다. 때문에 database index 가 필요하다면 Meta.indexes 를 활용하자. 이 부분은 미래에 변경될 소지 가 있다.

for concrete_model option

위 옵션이 False 라면 해당 필드는 proxy models 로 참조될 수 있다. Default 는 True 이다. 이것은 get_for_model() 에 for_concrete_model argument 를 그대로 반영(mirror) 한다.

  1. Primary key 타잎 호환성
    • "object_id" 필드는 pk 로서 동일한 타잎 일 필요는 없으나, 그들의 pk 값들은 get_db_prep_value() 메소드에 의해 "object_id" 필드로서 반드시 강제될 수 있다.
    • 예를 들어, 우리가 모델에 IntegerField 나 CharField 로 이루어진 pk 와 함께 generic 한 관계를 허용하고자 한다면, integer 가 get_db_prep_value() 에 의해 string 으로 강제된 상황에서 우리의 모델에 "object_id" 필드에 CharField 를 사용할 수 있다.
    • 유연성을 최대치로 적용하기 위해서 우리는 최대 길이가 설정되지 않은 TextField 를 우리의 database 에 사용할 수 있지만, 이것은 우리의 database 시스템에 따라 심각한 성능 저하를 야기할 수 있다.
    • 어떤 타잎의 필드가 최선인지에 대해서는 만능(one-size-fits-all) 솔루션은 없다. 우리는 우리가 어떤 모델을 가르키길 기대하는지, 그리고 어떠한 솔루션이 우리 유즈케이스에 가장 효과적인지 직접 판별해야 한다.
  2. ContentType 객체에 참조를 serialize 하기
    • 만약 우리가 데이터를 generic relation 을 포함하는 모델로부터 serializing (예를들어 fixture 를 만들 때) 한다면 우리는 반드시 단일(unique) 관계륵 가지는 ContentType 객체로 natural key 를 사용할 것이다.
    • natural keys 에서 dumpdata-natural-foreign 를 살펴보자.

이것은 일반 ForeignKey 에 사용된 것과 유사한 API 를 사용 가능하게 한다. 개별 TaggedItem 은 각각 관계를 가지는 객체를 리턴하는 content_object 를 가지게 될것이다. 그리고 우리는 그 필드에 등록할 수 있게 되거나 이것이 TaggedItem 을 생성할 때 사용하게 될 것이다.

>>> from django.contrib.auth.models import User
>>> guido = User.objects.get(username="Guido")
>>> t = TaggedItem(content_object=guido, tag="bdfl")
>>> t.save()
>>> t.content_object
<User: Guido>

만약 관계된 객체가 삭제된다면, content_type 과 object_id 필드는 원본 값에 대한 구성과 None 을 반환하는 GenericForeignKey 를 남기게 된다.

>>> guido.delete()
>>> t.content_object  # returns None

GenericForeignKey 가 포함된것에 대하여 우리는 filter, exclude 와 같은 필터를 이러한 필드에 직접 database API 를 통해 사용할 수 없다. 왜냐하면 GenericForeignKey 는 일반적인 필드 오브젝트가 아니기 때문에며 아래의 예시처럼 사용할 수 없다.

# This will fail
>>> TaggedItem.objects.filter(content_object=guido)
# This will also fail
>>> TaggedItem.objects.get(content_object=guido)

마찬가지로, GenericForeignKeys 는 ModelForms 에 나타나지 않는다.