다 대다 (ManyToMany relationships)
서로 여러개의 관계를 가지는 형태. A모델이 B모델을 가질 수 있고 B모델도 A모델을 여러개 가질 수 있는 것.
class Pizza(models.Model):
name = models.CharField(max_length=20)
toppings = models.ManyToManyField('Topping')
class Topping(models.Model):
name = models.CharField(max_length=20)
둘 중 하나에서 상대방을 참조하는 필드를 만들고 ManyToManyField를 정의하면 된다. 여기서는 피자 테이블에서 토핑 테이블을 참조하는 toppings 필드를 생성했다.
ManyToManyField를 정의하면, 자동으로 두 테이블 사이의 관계를 관리해주는 중간 테이블이 생성된다.
_
로 이어준 별도의 테이블이 생성된 것을 볼 수 있다.위에서 작성한 예시를 보면, 피자 테이블에 토핑 테이블을 참조하는 필드가 존재한다. 그래서 한 피자에 있는 토핑들을 가져오는 것은 크게 문제가 없다.
하지만 반대로 토핑이 들어간 피자들을 가져오기 위해
cheeze.pizzas.all()
을 하면, AttributeError: 'Topping' object has no attribute 'pizzas'
와 같이 '그런 속성 없다' 는 에러가 뜬다. 사실 조금 생각해보면 토핑 테이블에는 피자와 관련된 데이터는 아무것도 없다. 그렇기 때문에 직접적으로 가져올 수 없는 것이다.
🌹
_set
그래서 참조되는 테이블에서 참조하는 테이블의 데이터를 가져오기 위해, 장고에서는 참조하는 테이블의 이름 뒤에 _set
를 붙여주어야 한다고 명시했다.
>> pineapple.pizza_set.all()
<QuerySet [<Pizza: Hawaiian>]>
그런데 _set
을 붙여주기 싫다면? 필드 속성에 related_name
을 아래와 같이 정의해 줘도 된다.
🌹
related_name
class Pizza(models.Model):
...
toppings = models.ManyToManyField('Topping', related_name='pizzas')
처음에 related_name
을 보면 뭔가 의아할 것이다. 필드명은 토핑인데 왜 이름을 'pizzas'
라고 붙였을까? 그 이유는 related_name이 '참조되는 테이블이 참조하는 테이블의 데이터를 가져오고 싶을 때 사용하는 이름'을 정의하는 것이기 때문이다. 이 경우 토핑이 피자의 데이터를 가져오고 싶을 때 pizza_set
대신 pizzas
를 쓸 수 있다는 것이다.
데이터 간의 관계성을 가지게 하기 위해 hawaiian_pizza.toppings.add(pineapple)
피자 테이블에서 토핑을 추가할 수도 있지만, 반대로 아래와 같이 토핑 테이블에서 토핑이 들어간 피자를 추가할 수도 있다.
mozzarella = Topping.objects.create(name='mozzarella')
>> mozzarella.pizzas.add(cheese_pizza)
데이터를 쿼리할 때도 한 테이블에서 상대 테이블에 있는 데이터를 연관시켜 조회할 수 있다. 다음과 같이 피자 테이블에서 필터로 토핑 테이블의 데이터 중 p로 시작하는 피자들만 추출할 수도 있다.
>> Pizza.objects.filter(toppings__name__startswith='p')
<QuerySet [<Pizza: Pepperoni Pizza>, <Pizza: Hawaiian Pizza>]>
# pepperoni, pineapple
마찬가지로 토핑 테이블에서 필터로 피자 테이블의 데이터 중 이름에 'Hawaiian'이 들어가는 피자의 토핑만 추출할 수 있다.
>> Topping.objects.filter(pizzas__name__contains='Hawaiian')
<QuerySet [<Topping: pineapple>, <Topping: Canadian bacon>]>
a = Movie.objects.get(id = 1)
b = Movie.objects.get(id = 2)
# 추가
Actor.objects.get(id = 1).movie.add(a,b)
# 제거
Actor.objects.get(id = 1).movie.remove(b)
a = Actor.objects.get(id= 1)
b = Actor.objects.get(id= 2)
# 추가
Movie.objects.get(id = 1).actor_set.add(a,b)
# 제거
Movie.objects.get(id = 1).actor_set.remove(a)
# 정참조의 경우
Actors.objects.get(id = 1).movie.clear()
# 역참조의 경우
Movie.objects.get(id = 1).actor_set.clear()
위에서도 언급했지만, ManyToManyField로 데이터를 정의하면 → 자동으로 두 테이블의 관계를 관리해주는 테이블을 생성한다고 했다. 이것을 through model 이라고 하는데, 개발자가 직접 through model을 정의하면 필드를 추가한 중간 테이블을 생성할 수 있다.(자동 생성되는 테이블에는 테이블의 고유id와 두 테이블의 id만 존재한다)
그냥 장고가 알아서 만들어준거 쓰면 되지 왜 귀찮게 개발자가 직접 정의해야 하냐고 묻는다면, 더욱 자세한 데이터를 구축할 수 있기 때문이다.