프로젝트에서 프론트 작업을 하던 중 게시글의 조회수와 추천수, 그리고 특정 카테고리 선택 시 해당 카테고리의 값을 프론트에서 url 파라미터로 요청을 보내어 백엔드에서 요청에 따른 Queryset을 정렬하여 response로 내려주는 것을 하나의 View에서 구현했다.
우선 모델은 아래와 같으며, Article 모델에서 like필드는 User와 ManyToMany 필드로 연결되어 있다.
class Article(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='article_user')
category = models.ForeignKey(Hobby, on_delete=models.CASCADE, related_name='article_category')
title = models.CharField(max_length=500)
content = models.TextField()
article_image = models.ImageField(upload_to=rename_imagefile_to_uuid, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
like = models.ManyToManyField(User, blank=True, related_name='article_like')
views = models.BigIntegerField(default=0)
def __str__(self):
return str(self.title)
class User(AbstractBaseUser):
email = models.EmailField("이메일" , max_length=255, unique=True)
nickname = models.CharField("닉네임", max_length=15, unique=True)
hobby = models.ManyToManyField('workshops.Hobby')
profile_image = models.ImageField(default='/default_profile/default.PNG', upload_to=rename_imagefile_to_uuid)
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['nickname']
def __str__(self):
return self.nickname
먼저 queryset은 all()을 사용하여 전체 게시글로 기본 세팅을 해두었고, get 요청이 들어왔을 때 url 파라미터 값이 있는 경우를 고려하여 queryset을 요청에 맞게 정렬하였다.
첫 번째 난관은 queryset을 like 내림차순으로 정렬을 할 때 아래와 같이 .order_by로 like 필드를 뽑았더니 특정 게시글을 좋아하는 유저의 수 만큼 게시글이 중복되어 출력되는 현상이 있었다.
Article.objects.all().order_by('-like') # 1번 게시글 like.count()가 5인 경우 해당 게시글이 5개 중복으로 출력됨
serializer로 해당 필드를 like.count()로 커스텀 해놓은 상태라 당연히 위와 같이 order_by로 필터링 할 수 있었을 것 같았지만 like필드는 다른 모델을 참조하는 필드라 참조하는 모델의 객체를 모두 가져온다는 것을 깨달았다.
이를 해결하기 위해 annotate 라는 것을 알게되었고, Article 모델에 아래와 같이 입력하여 like_count라는 필드를 만들고 Count() 함수를 사용하여 like를 Count 함수안에 인자로 넣어 like의 개수를 나타내는 필드를 만들 수 있었다.
그리고 like_count 필드를 order_by를 사용하여 원하는 기준에 맞게 정렬할 수 있었고, 추가적으로 like의 개수가 같을 때 다른 조건인 생성시간 내림차순 조건을 주어 보다 디테일한 정렬 기준을 만들 수 있었다.
Article.objects.annotate(like_count=Count('like')).order_by('-like_count', '-created_at')
그 다음 문제는 sort와 category가 url 파라미터로 각각 들어오는 경우는 그다지 어렵지 않았는데 동시에 같이 들어오는 경우는 두 가지 요청을 어떻게 같이 처리해줄 수 있을지 고민이 많았다.
정답을 생각해내는데는 한참이 걸렸지만 생각보다 어렵지 않은 간단한 문제였다.
먼저 if문에 and 연산으로 두 가지 url 파라미터 값이 있는 경우가 참일 때를 조건으로 두었고, 요청한 정렬 기준이 최신순인지 추천순인지 if문으로 한 번더 구분한 다음 filter 함수에 카테고리 값을 넣어 해당 카테고리의 queryset을 모두 가져오고 난 후 order_by와 annotate를 사용하여 각각 기준에 맞는 정렬을 아래와 같이 해주었다.
class ArticleView(ListAPIView):
permission_classes = [permissions.AllowAny]
pagination_class = article_total_page
serializer_class = ArticleListSerializer
queryset = Article.objects.all()
def get(self, request):
get_category_id = self.request.GET.get('category')
sort = self.request.GET.get('sort')
# category & sort 둘 다 있는 경우
if get_category_id and sort:
if sort == 'latest':
self.queryset = Article.objects.filter(category=get_category_id).order_by('-created_at')
elif sort == 'like':
self.queryset = Article.objects.filter(category=get_category_id).annotate(like_count=Count('like')).order_by('-like_count', '-created_at')
# category만 있는 경우
if get_category_id and not sort:
self.queryset = Article.objects.filter(category=get_category_id)
# sort만 있는 경우
if sort and not get_category_id:
if sort == 'latest':
self.queryset = Article.objects.all().order_by('-created_at')
elif sort == 'like':
self.queryset = Article.objects.annotate(like_count=Count('like')).order_by('-like_count', '-created_at')
pages = self.paginate_queryset(self.get_queryset())
slz = self.get_serializer(pages, many=True)
return self.get_paginated_response(slz.data)
결과는 원했던 것 처럼 최신순/추천순 정렬, 카테고리 별 게시글 정렬, 그리고 카테고리 별 최신순/추천순 정렬 모두 구현할 수 있었다.
이번 경험으로 항상 배웠던 방식으로만 기계적으로 ORM을 했다는 생각이 들어 자책감이 들었고, 장고에서 ORM을 하는 여러 방법들에 대해서 깊게 공부해야겠다는 깨달음을 얻었다.