[Django] 관계 테이블 설정과 ManyToManyField

송진수·2021년 7월 21일
0

Model 정참조/역참조

Django 모델에서 일반적인 1:N 관계를 가진 테이블을 연결시키고자 한다면, 'N'측의 테이블에 '1'측의 테이블을 참조하는 ForeignKey 필드를 추가한다.

예를 들어, 주인이 1명이고, 주인이 가진 강아지가 N마리라면 아래 코드와 같이 강아지 테이블에서 주인 테이블을 참조할 수 있는 필드를 추가한다면 강아지의 주인에 대한 정보(인스턴스)에 아래와 같이 쉽게 접근할 수 있다.

# owners/models.py
class Owner(models.Model):
    name = models.CharField(max_length=45)
    email = models.CharField(max_length=300)
    age = models.IntegerField()

    class Meta:
        db_table = "owners"

class Dog(models.Model):
    owner = models.ForeignKey("Owner",on_delete=models.CASCADE)
    name = models.CharField(max_length=45)
    age = models.IntegerField()
    
    class Meta:
        db_table = "dogs"


# $ python manage.py shell
from owners.models import *

dog = Dog.objects.get(id=1) # dogs 테이블에서 id=1인 데이터 인스턴스 (name='진돌이', age='29', owner_id = 1)

강아지 테이블에서 주인 테이블은 FK를 통해 접근할 수 있으며, 주인 테이블의 클래스명을 메서드로 직접 사용할 수 있다. 이를 정참조라 한다.

dog.owner  # <Owner: Owner object (1)> (owners 테이블에서 id=1인 데이터 인스턴스) 
dog.owner.name # '송진수'

하지만 주인의 입장에서 강아지를 참조하고자 한다면 이야기가 달라지는데, 주인은 강아지를 여러 마리('N')을 데리고 있기 때문이다.

Django에서는 정참조의 반대 방향으로 참조(역참조)를 하고자 할 때, 메서드 이름 형식이 달라진다(default= '이름_set', related_name 옵션을 통해 따로 지정 가능). 기본 이름에 _set이 달리는 이유는 역참조가 기본적으로 인스턴스가 아닌 쿼리셋(QuerySet)과 관련이 있기 때문인 듯 싶다. 이렇게 역참조해서 반환되는 객체는 all(), filter()같은 쿼리셋 메서드를 통해 접근할 수 있다.

owner = Owner.objects.get(id=1) # 즉, '송진수'
owner.dog_set.all()
<QuerySet [<Dog: Dog object (1)>, <Dog: Dog object (2)>, <Dog: Dog object (3)>]>

for each in owner.dog_set.all():
    print(each.name)
    
 # 진돌이
 # 삐삐
 # 사랑이

ManyToManyField

위 1:N 관계에서는 정참조/역참조의 방향이 명확히 드러나지만, N:N 관계에서는 N과 N이 연결 테이블(Junction Table)을 통해 관계가 설정되어 있다.

이를 정참조/역참조 도식으로 나타낸다면 아래와 같다.

ManyToManyField를 안 쓴다면..

연결 테이블 클래스를 직접 작성하고, ForeignKey 필드로만 테이블 관계를 구성할 경우 테이블에서 다른 테이블로 참조하는 과정이 길고 복잡하다.

아래 코드를 참고해보자.

# movies/models.py
class Actor(models.Model):
    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):
    title = models.CharField(max_length=45)
    release_date = models.DateField()
    running_time = models.IntegerField()

    class Meta:
        db_table = "movies"

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

    class Meta:
        db_table = "actors_movies"
        
# $ python manage.py shell
from movies.models import *

movie = Movie.objects.get(id=1)

movies 테이블의 데이터에서 actors 테이블의 데이터를 참조하려면 위 도식처럼 movies 테이블에서 연결 테이블 역참조 → 연결 테이블에서 actors 테이블 정참조 하는 과정을 거쳐야만 한다!

am_set = movie.actor_movie_set.all()

for inst in am_set:
    print(inst.actor.first_name + ' ' + inst.actor.last_name)

# John Adams
# Sam Hammington
# Sean McDonald

ManyToManyField를 쓴다면

ManyToManyField는 ForeignKey 필드만 사용한 위 경우와 다르게 연결 테이블 참조 과정을 생략하고, 한번의 참조로 N:N 관계인 테이블을 참조할 수 있게 해주며, ManyToManyField가 들어간 테이블을 기준으로 정참조/역참조 관계를 만들어 준다.

class Actor(models.Model):
    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):
    title = models.CharField(max_length=45)
    release_date = models.DateField()
    running_time = models.IntegerField()
    actor = models.ManyToManyField('Actor')

    class Meta:
        db_table = "movies"

위 코드와 같이 ManyToManyField는 연결 테이블 클래스를 따로 만들지 않아도, migration 단계에서 자동으로 연결 테이블을 생성시켜준다. 다만 연결 테이블 클래스를 만들었을 경우에는 through 옵션으로 연결 테이블을 따로 지정할 수 있다.

또한 연결 테이블 클래스를 따로 만들지 않았을 경우, ManyToManyField를 지정한 클래스에서 데이터를 생성한 후에, 생성한 해당 인스턴스.참조테이블명.add(*참조테이블 인스턴스) 메서드로 관계를 지정해주어야 한다.

# $ python manage.py shell
from movies.models import *

movie = Movie.objects.get(id=1) # 'Titanic'
movie.actor.add(Actor.objects.get(first_name="John"))
movie.actor.add(Actor.objects.get(id=2))
movie.actor.add(Actor.objects.get(last_name="McDonald"))

for in inst in movie.actor.all():
    print(inst.first_name + ' ' + inst.last_name)
    
 # John Adams
 # Sam Hammington
 # Sean McDonald
 
 movie2 = Movie.objects.get(title="Everyone")
 
 # 참조하는 테이블의 참조 id(pk)로도 관계 지정 가능한 듯
 movie2.actor.add(1,2,3,4,5,6) 

 
 for inst in movie2.actor.all():
     print(inst.date_of_birth)
 
# 1987-05-05
# 1995-08-18
# 2005-04-24
# 1915-07-21
# 1935-11-19
# 1995-10-05

위와 같이 movies 데이터에서는 actor 테이블을 정참조하고, actor 데이터에서는 movies 필드를 역참조해야한다.

actor = Actor.objects.get(id=1)
actor.movie_set.all()
# <QuerySet [<Movie: Movie object (1)>, <Movie: Movie object (2)>]>

for each in actor.movie_set.all():
    print(each.title)
    
 # 'Titanic'
 # 'Everyone'

연결 테이블을 따로 지정하기

ManyToManyField에 옵션을 따로 지정하지 않을 경우, django migration이 연결 테이블을 자동으로 생성하지만, 연결 테이블에 Foreign Key 2개 말고도 관계를 설명하는 또 다른 필드가 필요할 때에는 연결 테이블로 사용할 모델 클래스를 생성하고 through 옵션을 통해 N:N관계를 설정해주어야 한다.

예를 들어 위 movies App 에서는 배우가 출연한 영화에서 그 배우의 연기 수준을 나타내고 싶다면, 연기점수 필드가 들어가있는 테이블을 생성하여 연결 테이블로 지정하면 될 것이다..

profile
보초

0개의 댓글