TIL 25 | ManyToManyField

임종성·2021년 7월 20일
0

Django

목록 보기
7/17
post-thumbnail

CRUD#2를 통해 클라이언트의 Request를 처리하는 View를 구현하고 그에 맞는 Response를 수행했다. 지난 Assignment에서는 One-To-Many 관계를 가진 Table만 있었지만, 추가 과제로 Many-to-Many 관계를 가진 Database에 대해 Backend API를 구현해보자.

Assignment#2

주어진 Many to Many 관계도는 다음과 같다.

처음 머릿속으로 그린 구성은 Actor와 Model Table을 만들고, 2개의 Table을 ForeignKey로 참조하는 중간 테이블을 생성하여 총 3개의 Table을 만드는 것이었다. 이를 토대로 Models.py를 구현해봤다.

Model Class

from django.db import models
from datetime import date

# Create your models here.

class Actor(models.Model):
    first_name    = models.CharField(max_length=20)
    last_name     = models.CharField(max_length=20)
    date_of_birth = models.DateTimeField()

    class Meta:
        db_table = 'actors'
    
class Movie(models.Model):
    title = models.CharField(max_length=100)
    release_date = models.DateTimeField()
    running_time = models.IntegerField(default=0)

    class Meta:
        db_table = 'movies'
    
    def __str__(self):
        return self.title

class ActorMovie(models.Model):
    actor = models.ForeignKey('Actor', on_delete=models.CASCADE, default='')
    movie = models.ForeignKey('Movie', on_delete=models.CASCADE, default='')

    class Meta:
        db_table = 'actormovies'

작성한 models.py를 migrate해서 Database Table을 생성하고 각 Table에 Data를 입력해봤다.

View Class

Model Class를 작성했으니, 이제 View Class를 작성해야 한다. GET Method로 return해야 하는 목록은 다음과 같다.

  • 배우의 이름, 성, 출연한 영화 제목 목록
  • 영화의 제목, 상영시간, 출연한 배우 목록

이전 Assignment와 다르게 이번엔 Many-to-Many 관계로, ForeignKey를 2개 가지고 있는 중간 Table이 존재해서 특정 배우가 출연한 영화 목록을 살펴보려면 반드시 중간 Table을 거쳐야 한다. 구현한 View는 다음과 같다.

import json

from django.http     import JsonResponse
from django.views    import View

from movies.models import *

class ActorView(View):
    def get(self, request):
        actors = Actor.objects.all()
        actormovies = ActorMovie.objects.all()
        movies = Movie.objects.all()
        results  = []
        for actor in actors:	# 영화목록을 가져오고 싶은 배우 iteration
            list = []
            for i in actormovies.filter(actor_id = actor.id): 
            # 중간 Table에서 (배우 id,배우가 찍은 영화 id) QuerySet을 가져온다.
                list.append(Movie.objects.get(id=i.movie_id).title) 
                # QuerySet 각 Instance의 id와 일치하는 영화의 title을 리스트에 추가
            results.append(
                {
                    "first_name"         : actor.first_name,
                    "last_name"          : actor.last_name,
                    "movie_list"         : list
                    }
            )
        return JsonResponse({'resutls':results}, status=200)

class MovieView(View):
    def get(self, request):
        movies = Movie.objects.all()
        actormovies = ActorMovie.objects.all()
        actors = Actor.objects.all()
        results  = []
        for movie in movies:	# 배우목록을 가져오고싶은 영화 iteration
            list = []
            for i in actormovies.filter(movie_id = movie.id):
            # 중간 Table에서 (영화 id, 출연한 배우 Id) QuerySet을 가져온다.
                list.append(Actor.objects.get(id=i.actor_id).first_name)
                # QuerySet 각 Instance의 id와 일치하는 배우의 이름을 리스트에 추가
            results.append(
                {
                    "title"                 : movie.title,
                    "running_time"          : movie.running_time,
                    "actor_list"            : list
                    }
            )
        return JsonResponse({'resutls':results}, status=200)

이처럼 View Class를 구현한 뒤 urls mapping을 완료하고 httpie 코드로 Django Server에 요청을 보내보았다. 결과는 제대로 나왔다!

  • 배우정보와 출연한 영화 목록
  • 영화정보와 출연한 배우 이름

ManyToManyField

이렇게 httpie로 Server에 Request하여 올바른 응답을 받긴 했지만, Query Method를 사용하여 Database에 Data를 추가하거나 View Class를 짤 경우 조금 불편한 점이 있었다.

나는 Many-to-Many Relationship의 Model Class를 구현할 때 2개의 ForeignKey를 사용하여 Movie와 Actor 모두를 참조하는 중간 Table인 ActorMovie를 만들었다. 이렇게 하고 보니

  • Value를 추가할 경우 중간 테이블을 통해 일일히 저장해줘야 한다.
  • Value를 불러올 경우에도 코드에 일일히 중간 테이블을 사용해야 한다.
  • 중간 테이블이 자주 사용되기에 코드 또한 불필요하게 길어지고 복잡해져 가독성이 떨어진다.

결국 어떤 작업을 수행하던지 간에 중간 Table을 거쳐야 한다라는 점이다.

그런데 ForeignKey를 사용하지 않고 ManyToManyField를 사용하면 중간 Table을 생성하지 않고도 다대다 관계를 정의할 수 있다! 이제부터 ManyToManyField를 사용해 Model Class부터 다시 구현해보자.

Model Class

기존에 존재했던 중간 Table인 class ActorMovie를 삭제하고 class Actorclass Movie와 연결되는 ManyToManyField를 추가했다.

from django.db import models
from datetime import date

# Create your models here.

class Actor(models.Model):
    first_name    = models.CharField(max_length=20)
    last_name     = models.CharField(max_length=20)
    date_of_birth = models.DateTimeField()
    movies        = models.ManyToManyField('Movie', related_name='actors')

    class Meta:
        db_table = 'actors'
    
class Movie(models.Model):
    title = models.CharField(max_length=100)
    release_date = models.DateTimeField()
    running_time = models.IntegerField(default=0)

    class Meta:
        db_table = 'movies'
    
    def __str__(self):
        return self.title

그 후 models.py를 다시 Migrate하고 Database를 살펴보니 원래 존재했던 Table인 actormovies는 사라지고 actors_movies Table이 생성되었다!

  1. ManyToManyFiled를 사용하면 사용자가 직접 중간 Table을 생성하지 않아도 자동으로 Class_Class 의 형태로 Database에 Table을 생성해준다! 이를 Through Model이라고 하는데, 개발자가 직접 정의해서 만들어준다면 연관된 Class의 Id 말고도 Field를 추가한 Table을 생성할 수 있다.

중간 Table의 Data가 삭제되었으니 영화배우와 출연한 영화를 다시 연결해주자.

다음과 같이 배우와 영화간 관계성을 가지게 하기 위해 배우의 Table에서 영화를 추가한다. 반대로 영화 Table에서 배우를 추가하여 관계성을 가지게 할 수 있다. 이는 ManyToManyField를 정의할때 related_nameactors를 대입해서 그런것이고, 대입하지 않았다면 b.actors_set.add(a)와 같은 Query Method를 실행해야 한다.

  1. ManyToManyField를 사용하면 Data의 추가,확인이 간단한 Method로 양쪽 Class에서 가능하다. 물론 ForeignKey를 사용해도 가능했지만, 반드시 중간 Table을 거쳐야 했기에 ManyToManyField를 사용함으로써 간편한 Query Method로 데이터 CRUD가 가능해졌다.

이렇게 ManyToManyField를 이용한 Model로 Database에 아까와 동일한 Data를 Input했다. 이제 View Class도 수정해보자!

View Class

다음과 같이 기존 View Class를 수정해봤다.

import json

from django.http     import JsonResponse
from django.views    import View

from movies.models import *

class ActorView(View):
    def get(self, request):
        actors = Actor.objects.all()
        # actormovies = ActorMovie.objects.all()
        # movies = Movie.objects.all()
        results  = []
        for actor in actors:
            list = []
            for i in actor.movies.all():
                list.append(i.title)    
            # for i in actormovies.filter(actor_id = actor.id):
            #     list.append(Movie.objects.get(id=i.movie_id).title)
            results.append(
                {
                    "first_name"         : actor.first_name,
                    "last_name"          : actor.last_name,
                    "movie_list"         : list
                    }
            )
        return JsonResponse({'resutls':results}, status=200)

class MovieView(View):
    def get(self, request):
        movies = Movie.objects.all()
        # actormovies = ActorMovie.objects.all()
        # actors = Actor.objects.all()
        results  = []
        for movie in movies:
            list = []
            for i in movie.actors.all():
                list.append(i.first_name)
            # for i in actormovies.filter(movie_id = movie.id):
            #     list.append(Actor.objects.get(id=i.actor_id).first_name)
            results.append(
                {
                    "title"                 : movie.title,
                    "running_time"          : movie.running_time,
                    "actor_list"            : list
                    }
            )
        return JsonResponse({'resutls':results}, status=200)

ActorView Class에서 더이상 중간 Table과 Movie Table을 가져와서 사용할 필요가 없기에 주석처리를 해주었다. MoviewView Class도 마찬가지 처리를 했고, Nested for문에서 안쪽 Iteration의 코드가 actormovies를 사용하지 않아도 되기에 많이 짧아지고 가독성도 좋아진 모습이 보인다.

  1. ManyToManyFiled를 사용하면 Databasee에 중간 Table이 자동으로 생성되기는 하나, 연결된 테이블의 정보를 가져올 경우 중간 Table을 불러올 필요가 없기에 정보를 가져오는 과정이 훨씬 간편해진다.

수정한 views.py를 토대로 server에 요청한 결과 2개의 ForeginKey를 사용했을때와 같은 Json Response를 보여줬다. ForeginKey로 구성했던 Model 구조를 성공적으로 ManyToManyField를 사용한 Model로 바꿔주었다.


Want To Learn

  • DateTimeField()의 기능, Input Format 조절하는 방법(models.가 아닌 forms.에서만 input_formats 속성 사용가능)
  • through model을 정의해서 사용해보기
  • QuerySet 정참조/역참조 개념과 select_reltated, prefetch_related?
  • Query, Python에서 성능이 좋은 방식이란 뭘까? 효율은 어떻게 비교할까? Hit이란?
profile
어디를 가든 마음을 다해 가자

0개의 댓글