수학과의 좌충우돌 프로그래밍

[Django] 한 모델이 여러 개의 모델과 관계를 맺어야하는 순간, contenttypes framework 본문

웹프로그래밍/Django

[Django] 한 모델이 여러 개의 모델과 관계를 맺어야하는 순간, contenttypes framework

ssung.k 2020. 8. 21. 06:15

모델링을 하다보면 한 모델이 여러 개의 모델과 관계를 맺어야하는 순간이 있습니다.

많이 접해봤을 블로그 프로젝트만 해도 게시물과 댓글에 좋아요 기능을 넣기 위해서는 다음과 같은 모델링이 필요합니다.

class Post(models.Model):
    # 생략

    
class Comment(models.Model):
    # 생략
    
    
class Like(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    comment = models.ForeignKey(Comment, on_delete=models.CASCADE)

 

물론 모델링에는 정답이 없으니 LikePostLikeCommentLike 로 나눌 수도 있고 방법은 다양합니다.

하지만 위에 소개한 방법은 postcomment 필드 중 하나는 Null이 들어가야한다는 문제점이 있고 그렇다고 모델을 나누는 것도 마음에 들지가 않았습니다.

그러다가 Django의 contenttypes framework을 찾게 되어서 이를 정리해보았습니다.

 

contenttypes framework

contenttypes framework 는 위와 같은 상황, 즉 하나의 테이블에 선택적으로 다른 테이블과 관계를 맺고 싶을 때 사용할 수 있습니다.

 

별도의 설치없이 django에서 기본적으로 제공하고 있습니다.

INSTALLED_APPS = [
    'django.contrib.contenttypes',
     # 생략
]

 

따라서 migrate만 해주면 db에서 확인할 수 있습니다.

python manage.py migrate

 

이제 contenttypes framework 을 적용해봅시다.

우선 완성된 전체코드를 확인한 후 각각에 대해서 알아봅시다.

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


class Post(models.Model):
    # 생략
    like = GenericRelation('Like', related_query_name='post')
    
    
class Comment(models.Model):
    # 생략
    like = GenericRelation('Like', related_query_name='comment')
    
    
class Like(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField() 
    content_object = GenericForeignKey('content_type', 'object_id')
    # contenttypes과는 무관한 user
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE)

 

우선 Like 모델부터 알아봅시다.

class Like(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField() 
    content_object = GenericForeignKey('content_type', 'object_id')
  • cotent_type

    cotent_type 필드는 ContentType 모델과 FK로 연결되는 필드입니다. ContentType는 Django에서 기본적으로 제공하는 모델입니다.

    아래를 보면 id, app_label, model을 필드로 가지며 app_label에는 app이름이, model에는 model의 이름이 들어갑니다.

    CREATE TABLE `django_content_type` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `app_label` varchar(100) NOT NULL,
      `model` varchar(100) NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `django_content_type_app_label_model_76bd3d3b_uniq` (`app_label`,`model`)
    ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    
    

    그렇기 때문에 ContentType과 FK를 맺었다는 것은 현재 프로젝트 안에 특정 모델과 범용적으로 관계를 맺을 수 있는 것을 의미합니다.

  • object_id

    관련될 모델의 PK를 저장할 수 있는 필드입니다.

  • content_object

    GenericForeignKey 에 위에 설명한 두 필드를 전달합니다. 위에서 설명한 cotent_typeobject_id 는 db에 반영되지만 해당 필드는 db와는 무관하게 django에서 사용됩니다.

 

다음으로는 Post와 Comment 는 동일하게 사용가능하니 Post만 다루도록 하겠습니다.

class Post(models.Model):
    # 생략
    like = GenericRelation('Like', related_query_name='post')

 

related_query_name 를 통해서 관련 개체에서 조회를 할 수 있습니다.

post의 title이 '첫번째' 라는 키워드를 포함하고 있는 Like 객체들을 필터링합니다.

like = Like.objects.filter(post__title__contains='첫번째')

 

orm, sql example

아래는 contenttypes을 사용할 시 사용할 여러 orm과 그에 해당되는 sql에서 대해서 정리해보았습니다.

 

Like 객체 생성

user = User.objects.get(id=user_id)
post = Post.objects.get(id=post_id)
like = Like.objects.create(content_object=post, user=user)
INSERT INTO `post_like` (`content_type_id`, `object_id`, `user_id`, `created_at`, `updated_at`) 
VALUES (9, 2, 2, '2020-08-20 18:20:02.885304', '2020-08-20 18:20:02.885373'); 
args=[9, 2, 2, b'2020-08-20 18:20:02.885304', b'2020-08-20 18:20:02.885373']

 

Like 객체 조회

post = Post.objects.get(id=post_id)
ct = ContentType.objects.get_for_model(post)
like = Like.objects.get(
    content_type=ct,
    object_id=post.id,
    user=request.user
)
SELECT `post_like`.`id`, `post_like`.`content_type_id`, `post_like`.`object_id`, `post_like`.`user_id`, `post_like`.`created_at`, `post_like`.`updated_at` 
FROM `post_like` 
WHERE (`post_like`.`content_type_id` = 9 AND `post_like`.`object_id` = 2 AND `post_like`.`user_id` = 2); 
args=(9, 2, 2)

 

특정 User의 특정 Post에 대한 Like 조회

post = Post.objects.get(id=post_id)
ct = ContentType.objects.get_for_model(post)
likes = Like.objects.filter(content_type=ct, user=request.user)
SELECT post_like.id, post_like.content_type_id, post_like.object_id, post_like.user_id, post_like.created_at, post_like.updated_at 
FROM post_like
WHERE (post_like.content_type_id = 9 AND post_like.user_id = 2)  
LIMIT 21; 
args=(9, 2)

 

특정 Post에 대한 Like의 수 조회

post = Post.objects.get(id=post_id)
ct = ContentType.objects.get_for_model(post)
like_cnt = Like.objects.filter(content_type=ct, object_id=post.id).count()
SELECT COUNT(*) AS `__count` 
FROM `post_like` 
WHERE (`post_like`.`content_type_id` = 9 AND `post_like`.`object_id` = 2); 
args=(9, 2)

 

 

Comments