Django | ManyToManyField, 역참조

celeste·2022년 4월 13일
0

Django Basics

목록 보기
7/7
post-thumbnail

1. Many-to-Many relationship

다 대다 (ManyToMany relationships)
서로 여러개의 관계를 가지는 형태. A모델이 B모델을 가질 수 있고 B모델도 A모델을 여러개 가질 수 있는 것.

2. ManyToManyField

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를 정의하면, 자동으로 두 테이블 사이의 관계를 관리해주는 중간 테이블이 생성된다.

  • 이 테이블은 우리가 작성한 모델에는 없지만, 데이터베이스를 확인하면 다대다관계의 두 테이블 이름을 _로 이어준 별도의 테이블이 생성된 것을 볼 수 있다.
  • 이 중간 테이블은 두 테이블의 id를 각각 필드로 가지고 있다. 그렇기 때문에 각 테이블에 데이터가 존재하지 않으면 중간 테이블에도 primary key가 존재하지 않는다.

2-1. 특징1 - 데이터를 가져올 때 특별한 방법이 필요하다.

위에서 작성한 예시를 보면, 피자 테이블에 토핑 테이블을 참조하는 필드가 존재한다. 그래서 한 피자에 있는 토핑들을 가져오는 것은 크게 문제가 없다.
하지만 반대로 토핑이 들어간 피자들을 가져오기 위해
cheeze.pizzas.all()을 하면, AttributeError: 'Topping' object has no attribute 'pizzas' 와 같이 '그런 속성 없다' 는 에러가 뜬다. 사실 조금 생각해보면 토핑 테이블에는 피자와 관련된 데이터는 아무것도 없다. 그렇기 때문에 직접적으로 가져올 수 없는 것이다.

🌹 _set

그래서 참조되는 테이블에서 참조하는 테이블의 데이터를 가져오기 위해, 장고에서는 참조하는 테이블의 이름 뒤에 _set를 붙여주어야 한다고 명시했다.

>> pineapple.pizza_set.all()
<QuerySet [<Pizza: Hawaiian>]>

그런데 _set을 붙여주기 싫다면? 필드 속성에 related_name을 아래와 같이 정의해 줘도 된다.

class Pizza(models.Model):
    ...
    toppings = models.ManyToManyField('Topping', related_name='pizzas')

처음에 related_name을 보면 뭔가 의아할 것이다. 필드명은 토핑인데 왜 이름을 'pizzas'라고 붙였을까? 그 이유는 related_name이 '참조되는 테이블이 참조하는 테이블의 데이터를 가져오고 싶을 때 사용하는 이름'을 정의하는 것이기 때문이다. 이 경우 토핑이 피자의 데이터를 가져오고 싶을 때 pizza_set 대신 pizzas를 쓸 수 있다는 것이다.

2-2 특징2 - 데이터 추가/쿼리는 양쪽에서 가능하다.

데이터 간의 관계성을 가지게 하기 위해 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()

2-3. 특징3 - through model

위에서도 언급했지만, ManyToManyField로 데이터를 정의하면 → 자동으로 두 테이블의 관계를 관리해주는 테이블을 생성한다고 했다. 이것을 through model 이라고 하는데, 개발자가 직접 through model을 정의하면 필드를 추가한 중간 테이블을 생성할 수 있다.(자동 생성되는 테이블에는 테이블의 고유id와 두 테이블의 id만 존재한다)
그냥 장고가 알아서 만들어준거 쓰면 되지 왜 귀찮게 개발자가 직접 정의해야 하냐고 묻는다면, 더욱 자세한 데이터를 구축할 수 있기 때문이다.

0개의 댓글