[Django] Spanning multi-valued relationships, 다중 값 관계 확장
Spanning multi-valued relationships
에 대해서 설명하기 전에 어쩌다가 해당 문제에 부딪혔는지 설명을 우선적으로 하도록 하겠습니다.
배경 설명
이번 예시에서 사용할 모델은 3개입니다.
실제로는 필드 수가 훨씬 많지만 원활한 이해를 이해 필드를 많이 줄여 적절한 예시를 만들어 보았습니다.
class Sutra(models.Model):
name_kor = models.CharField(max_length=255, null=True, blank=True)
description = models.TextField(blank=True, null=True)
def __str__(self):
return self.name_kor
class Evaluation(models.Model):
RECOMMEND_TYPE_CHOICES = (
("RECOMMEND", "추천"), ("UNRECOMMEND", "비추천"), ("NOTYET", "안해봄"))
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
sutra = models.ForeignKey(
'Sutra', related_name='evaluations', on_delete=models.CASCADE)
recommend_type = models.CharField(
max_length=20, choices=RECOMMEND_TYPE_CHOICES)
class Meta:
unique_together = [['user', 'sutra']]
def __str__(self):
return f'{self.user}의 {self.sutra}에 대한 eval'
class User(AbstractUser):
# 생략
각 모델에 대해서 간략하게 설명하고 넘어가자면
- Sutra : 게시물과 같은 역할입니다. 이름과 설명이 필드로 있습니다.
- User : 이름에서부터 더 설명할 것이 없습니다. 필드는 전부 생략하였습니다.
- Evaluation : User의 Sutra에 대한 평가입니다. 평가는
recommend_type
의 값에 따라 다양한 평가가 가능합니다.
예상치 못한 Query
이 상황에서 제가 날리고 싶은 쿼리는 다음과 같았습니다.
SELECT *
FROM sutra
INNER JOIN evaluation
ON sutra.id = evaluation.sutra_id
WHERE evaluation.user_id = {user_id} and evaluation.recommend_type = {recommend_type}
user_id를 id로 하는 유저(현재 로그인한 유저)가 recommend_type의 종류의 평가를 한 Sutra를 가져오고 싶었습니다.
즉 필터링 기능이었죠.
그런데 필터링을 할 것이 recommend_type 뿐 아니라 범용적으로 필요했기 때문에 상단에서 로그인한 유저에 대해 queryset을 만들고 chaining filter으로 추가하였습니다.
queryset = Sutra.objects.filter(evaluations__user=self.request.user)
if filtering in ["RECOMMEND", "UNRECOMMEND", "NOTYET"]:
queryset = queryset.filter(evaluations__recommend_type=recommend_type)
하지만 쿼리를 확인한 결과 예상치 못한 쿼리가 발생하였습니다.
조인이 한 번 더 발생하며 예상과는 다른 결과를 출력하였습니다.
SELECT *
FROM `labs_sutra`
INNER JOIN `labs_evaluation`
ON (`labs_sutra`.`id` = `labs_evaluation`.`sutra_id`)
INNER JOIN `labs_evaluation` T4
ON (`labs_sutra`.`id` = T4.`sutra_id`)
WHERE (`labs_evaluation`.`user_id` = 107 AND T4.`recommend_type` = 'RECOMMEND')
Spanning multi-valued relationships
몇 시간을 삽질한 결과, Django Document에서 관련된 개념을 찾았습니다.
https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships
저와 같은 상황을 겪은, 8년전에 올라왔던 이슈도 있었습니다.
https://code.djangoproject.com/ticket/18437#no1
하지만 이는 버그가 아닌 Django에서 지원하는 기능입니다.
ManyToManyField또는 reverse를 기준으로 객체를 필터링 할 때 ForeignKey가 관심을 가질 수있는 두 가지 종류의 필터가 있습니다.
- 두 조건을 모두 충족하는 동일한 항목
- 두 조건을 각각 만족하는 다른 항목
첫 번째 조건은 제가 생각했던 요구사항이었습니다. 같은 Sutra에 대해 유저 id도 동일하고, recommend_type도 동일하기를 기대했던 것이죠.
하지만 Sutra와 Evaluation을 조인하면 아래와 같이 같은 Sutra가 여러 행에 걸쳐 존재할 수 있습니다.
sutra.id (evaluation.sutra_id) | evaluation.user | evaluation.recommend_type |
---|---|---|
1 | minsung | RECOMMEND |
2 | minsung | UNRECOMMEND |
2 | heejae | RECOMMEND |
여기서 바로 문제가 발생합니다.
sutra 2번의 경우 두 조건을 각각 만족하는 다른 항목
에 포함되기 때문이죠.
이렇게 두 가지 사항을 따로 다뤄주기 위해서 Django에서는 Spanning multi-valued relationships 이라는 개념을 도입하여 해결하고 있었습니다.
따라서 제가 원하는 두 조건을 모두 충족하는 동일한 항목
은 아래와 같이 작성해야했습니다.
queryset = Sutra.objects.filter(
evaluations__user=self.request.user,
evaluations__recommend_type=recommend_type)