오늘도 개발

ManyToManyField VS ForeignKey 본문

웹 프로그래밍/Django

ManyToManyField VS ForeignKey

Sueeeeeee 2022. 7. 5. 11:08

두 모델이 다대다 관계인 경우 ManyToManyField를 사용할 수도 ForeignKey를 사용할 수도 있다. 

 

ManyToManyField를 사용지 않는 경우엔 직접 ForeignKey를 사용하여 연결 테이블을 만들어야 한다.

이 경우 views.py에서 연결 테이블을 불러와 직접 작업을 수행해야 한다.

 

ManyToManyField를 사용하는 경우 두 모델 중 하나에만 ManyToManyField를 정의한다.

이 경우 따로 연결 테이블을 만들지 않아도 DB에 자동으로 연결 테이블(through table)이 생성된다.

(연결 테이블을 커스터마이징 해야 하는 경우, Foreign키로 된 연결 테이블을 직접 만들고

ManyToManyField의 옵션 through=에 지정할 수 있다.)

views.py에서 연결 테이블을 따로 불러올 필요 없이

한 테이블에서 바로 연결된 항목에 접근할 수 있어 직관적이며 편리하다.

 

아래 예시를 통해 두 경우를 더 자세히 살펴보자.

영화, 배우 예시 1

한 영화에는 여러 배우가 등장할 수 있고, 한 배우는 여러 영화에 등장할 수 있으므로 영화-배우는 다대다 관계이다.

백엔드에서 모든 배우 정보를 json으로 만들어서 다음과 같이 프론트엔드에게 보내주어야 하는 상황을 가정해보자.

한 배우 안에는 배우가 출연한 모든 영화 제목이 들어가야 한다.  

{
    "results": [
        {
            "date_of_birth": "1990-01-10",
            "first_name": "봄",
            "last_name": "김",
            "starred_in": [
                "인터스텔라",
                "그래비티"
            ]
        },
        {
            "date_of_birth": "1982-08-04",
            "first_name": "여름",
            "last_name": "이",
            "starred_in": [
                "인터스텔라"
            ]
        }
 }

1. ForeignKey로 연결 테이블을 만드는 경우

<models.py> : 직접 연결테이블 Actor_movie를 정의한다.

class Actor(models.Model):
    id = models.AutoField(primary_key=True)
    first_name = models.CharField(max_length=45)
    last_name = models.CharField(max_length=45)
    date_of_birth = models.DateField()

    class Meta:
        db_table = 'actors'

class Movie(models.Model):
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=45)
    release_date = models.DateField()
    running_time = models.IntegerField()

    class Meta:
        db_table = 'movies'

class Actor_movie(models.Model):
    id = models.AutoField(primary_key=True)
    actor = models.ForeignKey(Actor, on_delete=models.CASCADE)
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE)

    class Meta:
        db_table = 'actors_movies'

<views.py> 

1) all()로 모든 배우 쿼리셋을 불러온다.

2) for문으로 각 배우 오브젝트에 접근한다.

     - 각 배우 오브젝트에서 : 연결 테이블에서 현재 배우와 연결된 영화-배우 쿼리셋을 직접 불러온다.

     - 영화-배우 쿼리셋에서 for문으로 각 영화-배우 오브젝트에 접근한다.

     - 각 영화-배우 오브젝트에서 : 해당 영화-배우 오브젝트와 연결된 영화 오브젝트를 가져온다.

     - 영화 오브젝트를 결과 리스트에 추가한다. 

class ActorsView(View):
    def get(self, request):
        results = []
        
        # 배우 쿼리셋 생성
        actors = Actor.objects.all()

        # for문으로 배우 쿼리셋의 각 배우 오브젝트에 접근 
        for actor in actors:
            
            # 연결 테이블에서 현재 배우가 포함된 row 모두 선택
            # 영화-배우 쿼리셋 생성
            actor_movies = Actor_movie.objects.filter(actor=actor)
            starred_in = []

            # for문으로 영화-배우 쿼리셋의 각 영화-배우 오브젝트에 접근
            for actor_movie in actor_movies:
                # 현재 영화-배우 오브젝트와 연결된 영화 오브젝트 생성
                movie = actor_movie.movie.title
                # 생성한 영화 오브젝트를 starred_in 리스트에 추가
                starred_in.append(movie)

            results.append({
                "first_name": actor.first_name,
                "last_name": actor.last_name,
                "date_of_birth" : actor.date_of_birth,
                "starred_in" : starred_in
            })
        return JsonResponse({'results' : results}, status = 200)

2. ManyToManyField를 사용하는 경우

<models.py>

from django.db import models

# Create your models here.
class Actor(models.Model):
    id = models.AutoField(primary_key=True)
    first_name = models.CharField(max_length=45)
    last_name = models.CharField(max_length=45)
    date_of_birth = models.DateField()

    class Meta:
        db_table = 'actors'

class Movie(models.Model):
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=45)
    release_date = models.DateField()
    running_time = models.IntegerField()
    # ManyToManyField는 다대다 관계의 테이블 중 하나에만 설정하면 된다
    actor = models.ManyToManyField(Actor)

    class Meta:
        db_table = 'movies'

클래스를 두 개만 만들어도 DB에는 세 개의 테이블(movies, actors, movies_actor)이 생성된다.

ManyToManyField를 설정한 테이블 Movie에는 actor 칼럼이 생성되지 않는다.

연결 테이블에는 다음과 같이 데이터를 입력한다.

m1 = Movie.objects.get(id=1)
a1 = Actor.objects.get(id=1)
a2 = Actor.objects.get(id=2)

# Movie 클래스에 actor라는 ManyToManyField를 설정했으므로
# Movie 인스턴스에 actor.add()로 Actor 인스턴스 추가
m1.actor.add(a1)
m1.actor.add(a2)

입력을 완료하면 다음과 같이 된다.

<views.py>

1) all()로 모든 배우 쿼리셋을 불러온다.

2) for문으로 각 배우 오브젝트에 접근한다.

     - 각 배우 오브젝트에서 : 현재 배우와 연결된 영화 쿼리셋을 불러온다.

     - for문으로 영화 쿼리셋의 각 영화에 접근한다.

     - 영화 오브젝트를 결과 리스트에 추가한다. 

class ActorsView(View):
    def get(self, request):
        results = []
        
        # 배우 쿼리셋 생성
        actors = Actor.objects.all()

        # for문으로 배우 쿼리셋의 각 배우 오브젝트에 접근 
        for actor in actors:
            results.append({
                "first_name": actor.first_name,
                "last_name": actor.last_name,
                "date_of_birth" : actor.date_of_birth,
                # 현재 배우와 연결된 영화 쿼리셋 생성
                # for문으로 영화 쿼리셋의 각 영화 오브젝트에 접근
                "starred_in" : [movie.title for movie in actor.movie_set.all()]
            })
        return JsonResponse({'results' : results}, status = 200)

3. 결론

ForeignKey나 ManyToManyField 중 어느 것을 사용해도 상관없는 경우에는 ManyToMayField를 사용하는 것이 낫다.

코드를 더 쉽고 간결하게 짤 수 있기 때문이다. 

영화, 배우 예시 2

배우는 영화마다 다른 배역을 연기한다.

한 배우가 각 영화에서 어떤 배역을 맡았는지도 보여주려면 어떻게 해야 할까?

1. ForeignKey로 연결 테이블을 만드는 경우

models.py의 Actor_movie 클래스에 필드를 하나 더 추가하고 위와 같은 방식으로 처리하면 된다.

<models.py>

class Actor(models.Model):
    id = models.AutoField(primary_key=True)
    first_name = models.CharField(max_length=45)
    last_name = models.CharField(max_length=45)
    date_of_birth = models.DateField()

    class Meta:
        db_table = 'actors'

class Movie(models.Model):
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=45)
    release_date = models.DateField()
    running_time = models.IntegerField()

    class Meta:
        db_table = 'movies'

class Actor_movie(models.Model):
    id = models.AutoField(primary_key=True)
    actor = models.ForeignKey(Actor, on_delete=models.CASCADE)
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
    role = models.CharField(max_length=45)
    
    class Meta:
        db_table = 'actors_movies'

2. ManyToManyField를 사용하는 경우

<models.py>

ForeignKey를 사용하여 연결 테이블을 직접 만든다.

ManyToManyField에 인자 through로 연결 테이블을 넣는다.

from django.db import models

class Actor(models.Model):
    id = models.AutoField(primary_key=True)
    first_name = models.CharField(max_length=45)
    last_name = models.CharField(max_length=45)
    date_of_birth = models.DateField()

    class Meta:
        db_table = 'actors'

class Movie(models.Model):
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=45)
    release_date = models.DateField()
    running_time = models.IntegerField()
    # Role 클래스를 연결 테이블로 사용하겠다는 뜻 
    actor = models.ManyToManyField(Actor, through='Role')

    class Meta:
        db_table = 'movies'

# 명시적으로 연결 테이블 정의
class Role(models.Model):
    id = models.AutoField(primary_key=True)
    actor = models.ForeignKey(Actor, on_delete=models.CASCADE)
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
    character = models.CharField(max_length=45)
    
     class Meta:
        db_table = 'roles'

 

 

 

 

참고

Adding a through table to existing M2M fields