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)
# 진돌이
# 삐삐
# 사랑이
위 1:N 관계에서는 정참조/역참조의 방향이 명확히 드러나지만, N:N 관계에서는 N과 N이 연결 테이블(Junction Table)을 통해 관계가 설정되어 있다.
이를 정참조/역참조 도식으로 나타낸다면 아래와 같다.
연결 테이블 클래스를 직접 작성하고, 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는 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 에서는 배우가 출연한 영화에서 그 배우의 연기 수준을 나타내고 싶다면, 연기점수 필드가 들어가있는 테이블을 생성하여 연결 테이블로 지정하면 될 것이다..