DRF - 역참조와 Model.objects.filter()의 관계

Dongwoo Kim·2022년 7월 12일
0

DRF

목록 보기
4/9
post-custom-banner

0. 서문

내일배움캠프 아침퀴즈에서 알 수 있었던
ManyToManyField와 ForiegnKey 관계에서
역참조를 통한 Model.objects.filter()의 작동 원리에 대해 정리해보자.

1. Background

1) Model 구조

# post/models.py
class SkillSet(models.Model):
    name = models.CharField(max_length=128)
    job_posts = models.ManyToManyField('JobPost', through='JobPostSkillSet')
    class Meta:
        db_table = 'skill_sets'

class JobPostSkillSet(models.Model):
    skill_set = models.ForeignKey('SkillSet', on_delete=models.SET_NULL, null=True)
    job_post = models.ForeignKey('JobPost', on_delete=models.SET_NULL, null=True)

class JobType(models.Model):
    job_type = models.CharField(max_length=128)
    class Meta:
        db_table = 'job_types'

class JobPost(models.Model):
    job_type = models.ForeignKey(JobType, on_delete=models.SET_NULL, null=True)
    company = models.ForeignKey('Company', on_delete=models.SET_NULL, null=True)
    job_description = models.TextField()
    salary = models.IntegerField()

    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = 'job_posts'

class Company(models.Model):
    company_name = models.CharField(max_length=128)
    business_area = models.ManyToManyField('BusinessArea', through='CompanyBusinessArea')

    class Meta:
        db_table = 'companies'

2) Model 관계 요약

SkillSet Model과 JobPost Model은 ManyToMany 관계
JobPost Model과 Company Model은 OneToMany 관계

3) 아침퀴즈 문제 中

5. JobPostSerializer를 사용해서 고정적으로 두개의 기술스택 예시( django | mysql )을 기술스택으로 요구하는 회사를 검색 API 구현

  • 고정적으로 두개의 기술을 입력받아 검색하는 로직을 구현한 후, 입력 기술 스택을 가변적으로 처리하는 로직을 구현해볼것 (선택)
  • 최대한 response 데이터로 리턴하도록 구현해볼것 (선택)

4) 문제 요약

skillset을 django 또는 mysql로 등록한 jobpost를 찾는 것

2. Problem

1) 해결 방향

jobpost에서 역참조하여 skillset이 django 또는 mysql인 object를 찾자.

class SkillView(APIView):
    def get(self, request):
        skills = self.request.query_params.getlist('skills', '')
       
        query = Q()
        for skill in skills:
            skill_set = SkillSet.objects.get(name=skill)
            query |= Q(skillset=skill_set)
        job_posts = JobPost.objects.filter(query)

        print(job_posts)

        return Response(JobPostSerializer(job_posts, many=True).data, status=status.HTTP_200_OK)

2) 실행 결과

3) 문제점

JobPost object (2) 가 2번 포함됨

4) 이유

ManyToMany관계로 JobPost object (2) 하나가 django SkillSet object와 mysql SkillSet object 모두 참조 하고있기 때문

5) 결론

위 코드에서

job_posts = JobPost.objects.filter(query)

는 JobPost Table에서 검색하는 것이 아니라 역참조 Model 해당하는 SkillSet Table에서 (또는 ManyToMany의 중간 테이블인 JobPostSkillSet Table에서) query에 해당하는 JobPost를 찾는다. 따라서 query에 해당하는 jobpost가 중복될 경우 결과 QuerySet에 중복으로 포함됨.

3. Try & Solution

1) Try

: jobpost의 id를 list로 받아와 set으로 중복제거 후 jobpost object를 탐색

  • 중복을 해결할 수 있었지만 임시방편같아 맘에들지 않았음
class SkillView(APIView):
    def get(self, request):
        skills = self.request.query_params.getlist('skills', '')
        query = Q()
        for skill in skills:
            skill_set = SkillSet.objects.get(name=skill)
            query |= Q(skill_set=skill_set)
        
        job_post_skill_sets = JobPostSkillSet.objects.filter(query)

        job_post_ids = [job_post_skill_set.job_post_id for job_post_skill_set in job_post_skill_sets]
        job_post_ids = list(set(job_post_ids))
        job_posts = [JobPost.objects.get(id=id) for id in job_post_ids]
        
        return Response(JobPostSerializer(job_posts, many=True).data, status=status.HTTP_200_OK)

2) Solution

  • QuerySet에서 attribute을 기준으로 중복을 제거할 수 있는 문법을 찾음
  • QeurySet.distinct() 를 사용하여 중복 제거
  • 중복제거 기준 : job_post_id
class SkillView(APIView):
    permission_classes = [permissions.AllowAny]

    def get(self, request):
        skills = self.request.query_params.getlist('skills', '')

        query = Q()
        for skill in skills:
            skill_set = SkillSet.objects.get(name=skill)
            query |= Q(skill_set=skill_set)
        
        job_post_skill_sets = JobPostSkillSet.objects.filter(query)
        
        job_post_ids = job_post_skill_sets.distinct().values_list('job_post_id')
        job_posts = [JobPost.objects.get(id=id[0]) for id in job_post_ids]

        # job_post_ids = [job_post_skill_set.job_post_id for job_post_skill_set in job_post_skill_sets]
        # job_post_ids = list(set(job_post_ids))
        # job_posts = [JobPost.objects.get(id=id) for id in job_post_ids]

        return Response(JobPostSerializer(job_posts, many=True).data, status=status.HTTP_200_OK)

4. 코드

https://github.com/kimphysicsman/nbcamp-quiz-220624

profile
kimphysicsman
post-custom-banner

0개의 댓글