장고 model의 ManyToManyField는 유용하긴 하지만 헷갈리는 것들이 많아 정리가 필요하다
먼저 왜 foreignkey대신 manytomanyfield를 써야하는지 생각해봐야 한다.
영화배우와 영화의 관계나 피자와 토핑의 관계같은걸 연상하면 쉽다. 둘 다 다대다 관계의 좋은 예시이다. 이런 유형의 테이블들은 manytomanyfield를 사용하는게 여러 장점이 있다.
단 장고 ORM에는 manytomanyfield 기능이 있지만 다른 프레임워크에는 없을수도 있기 때문에 manytomanyfield 쓰지 않고 foreignkey로만 연결하는것도 연습해봐야 한다.
from django.db import models
class Pizza(models.Model):
name = models.CharField(max_length=30)
toppings = models.ManyToManyField('Topping')
def __str__(self):
return self.name
class Topping(models.Model):
name = models.CharField(max_length=30)
def __str__(self):
return self.name
다대다 관계로 묶인 테이블은 서로 간의 데이터 입력 순서가 중요하다.
배우와 영화 테이블이 있고 장고 models에서 영화테이블에서 배우테이블을 manytomanyfield로 설정한 경우, 배우 테이블부터 데이터를 입력해야 한다. 또한 두 테이블간의 관계를 만들때 반드시 두 테이블에 해당하는 데이터가 미리 입력돼있어야 한다. 예를 들어 파인애플 피자의 토핑은 파인애플과 베이컨이라고 할때, 파인애플 피자와 파인애플 토핑은 입력돼있지만 베이컨 토핑은 없는 상황을 생각해보자. 이 상태에서 파인애플 피자의 토핑 목록에 베이컨 토핑을 추가한다고 입력해보면 에러가 난다.
related_name 옵션을 사용하지 않았다면 관계되는 모델의 소문자 클래스명_set을 사용해서 관계되는 값을 찾을수 있다.
canadian_bacon.pizza_set.all()
<QuerySet [<Pizza: Hawaiian>]>
canadian_bacon.pizzas.all()
<QuerySet [<Pizza: Hawaiian>]>
related_name 옵션을 사용한 경우 _set 대신 좀더 알기쉬운 이름을 사용할수있다.
related_name 옵션값은 소문자로 해당 모델 클래스명의 복수형으로 쓰는게 좋으며 이름짓기가 매우 중요하다. related_name을 엉뚱한 값으로 해놓으면 코드 유지보수나 로직에 문제가 많아진다.
참조해준 객체 입장에서 related_name을 설정해주는 것이 좋다.
class Pizza(models.Model):
...
toppings = models.ManyToManyField('Topping', related_name='pizzas')
위 예시에서 related_name='toppings'가 아닌 이유? toppings로 하면
canadian_bacon.toppings.all()
이런식으로 입력해야 되는데 직관적이지 못하다. 나는 베이컨 토핑이 들어가는 피자를 찾고싶은 상황이다.
canadian_bacon.pizzas.all()
이렇게 찾는게 직관적이다.
>> cheese_pizza = Pizza.objects.create(name='Cheese')
>> mozzarella = Topping.objects.create(name='mozzarella')
>> mozzarella.pizzas.add(cheese_pizza)
>> mozzarella.pizzas.all()
<QuerySet [<Pizza: Cheese>]>
위 코드에서 add()로 모짜렐라 토핑과 치즈피자의 관계가 생성됐다.
변수를 여러개 만들어 놓고 add()로 한꺼번에 추가하면 되기 때문에 FK 필드보다 편리하다.
>> Pizza.objects.filter(toppings__name__startswith='p')
<QuerySet [<Pizza: Pepperoni Pizza>, <Pizza: Hawaiian Pizza>]>
>> Topping.objects.filter(pizzas__name__contains='Hawaiian')
<QuerySet [<Topping: pineapple>, <Topping: Canadian bacon>]>
참고자료
https://www.revsys.com/tidbits/tips-using-djangos-manytomanyfield/