개발기록장

[Django] ManyToManyField 설정과 데이터 조회 방법 본문

TIL/Django

[Django] ManyToManyField 설정과 데이터 조회 방법

yangahh 2021. 1. 27. 00:58

 

Django의 Model에서는 M:N 관계 테이블을 설정할 수 있는 2가지 방법이 있다.

 

1) 교차 테이블을 만들어서 두 개의 테이블에 ForeignKey를 걸어서 설정하는 방법과

2) ManyToManyField를 사용해서 설정하는 방법이 있다.

 

이번 포스팅에서는 좀 더 쉽게 M:N 관계를 설정할 수 있는 ManyToManyField 사용 방법을 정리해보았다.

 


 

1. 모델 정보

구현할 모델

  • 제품(음료) 정보를 가지고 있는 Product 클래스(테이블)

  • 알레르기 정보를 가지고 있는 Allergy 클래스(테이블)

하나의 제품에는 알레르기가 없거나 하나 이상이 올 수 있고, 하나의 알레르기 역시 다수의 제품에 들어갈 수 있으므로 M:N 관계가 성립한다.

 

 

2. 설정 방법 (views.py)

from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=20)
    menu = models.ForeignKey('Menu', on_delete=models.SET_NULL, null=True)

    class Meta:
        db_table = 'categories'


# M:N 테이블 1
class Product(models.Model):
    category     = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
    korean_name  = models.CharField(max_length=55)
    english_name = models.CharField(max_length=55)
    description  = models.TextField()
    is_new       = models.BooleanField(default=False)
    allergy      = models.ManyToManyField('Allergy', through='AllergyProduct', related_name='product')

    class Meta:
        db_table = 'products'


# M:N 테이블 2
class Allergy(models.Model):
    name = models.CharField(max_length=45)

    class Meta:
        db_table = 'allergies'


# 두 테이블들의 중간 테이블  >>  없어도 ManyToManyField에 의해 생김. 하지만 테이블의 확장성을 고려하여 이렇게 명시하는게 좋다
class AllergyProduct(models.Model):
    allergy = models.ForeignKey('Allergy', on_delete=models.CASCADE)
    product = models.ForeignKey('Product', on_delete=models.CASCADE)

    class Meta:
        db_table = 'allergy_product'


- ManyToManyField를 지정할 attribute 추가

  • 위 코드와 같이 Drink classallergy라는 attribute를 추가하고 models.ManyToManyField를 지정해주면 된다. 이 allergy라는 attribute는 실제 DB에 필드로 추가되는 것이 아니고, 두 테이블에서 forward-many-to-many manager 역할을 한다.

  • 이렇게 ManyToManyField만 설정해 주면 Django가 자동으로 DB에 실제 중간 테이블을 만들어 준다. (models.py에 중간 테이블을 명시하지 않아도 생김)

  • 여기서 주의할 점은 둘 중 어느 테이블에 ManyToManyField를 지정해야 하는 것이냐인데, 둘 중 기준이 되는 테이블에 추가를 하는 것이 좋다. 
    (이유는 아래에 설명할 many-to-many manager를 이용하여 데이터를 조회하는 부분에서 설명)

 

- through 옵션

  • ManyToManyField에 through 옵션을 주면, 여기에 명시한 class가 두 테이블의 중간 테이블로 생성된다.

  • 위에서 말했듯 ManyToManyField를 지정해 주기만 해도 중간 테이블이 생기는데 through를 지정하는 이유는 테이블의 확장성을 고려하기 때문이다.

  • through 옵션을 사용하지 않으면 두 테이블의 PK만 중간 테이블의 필드로 가지고 있는데(여기서는 product_id와 allergy_id) 만약 이 중간 테이블에 다른 필드를 추가하고 싶다면 through 옵션을 사용하여 중간 테이블 이름을 지정해준 뒤, 위 코드의 AllergyProduct class처럼 중간 테이블을 위한 class를 만들어 줘야 한다. 이 테이블에 여러 컬럼을 추가할 수 있다는 장점이 있다.

 

- related_name 옵션

  • related_name 옵션은 ManyToManyField 뿐만 아니라 ForeignKey 에도 사용할 수 있는 옵션이다.

  • related_name 옵션은 참조 관계의 테이블에서 손쉽게 역참조/정참조를 할 수 있게 해 준다. (아래에서 설명)

  • 옵션 값으로는 상대 테이블에서 해당 테이블을 참조할 때 쓰는 이름을 값으로 주면 된다.

  • 이 옵션은 M:N관계에서 사용하기보단 셀프 참조를 하는 테이블에 유용하게 쓰인다.(self 참조 시에는 related_name이 필수다)

 

 

3. 실제 테이블 확인

models.py를 migrate 하면 DB에 실제 테이블은 아래와 같이 생성된다.

 

- products 테이블

: Product 클래스로 생성된 테이블

allergy 속성은 product 클래스에 논리적으로만 존재하는 속성임을 확인할 수 있다.

 

- allergies 테이블

: Allergy 클래스로 생성된 테이블

 

- allergy_product 테이블

: AllergyProduct 클래스로 생성된 테이블이자 through 옵션으로 product와 allergy의 중간 테이블로 지정해 준 테이블.

 

 

4. 데이터 조회, 추가, 삭제 방법

ManyToManyField로 생겨난 forward_many_to_many_manager를 이용하여 ForeignKey를 이용했을 때보다 쉽게 데이터를 조회, 추가, 삭제할 수 있다.

 

-  ManyToManyField를 가지고 있는 테이블을 기준으로 데이터를 조회, 추가, 삭제

# id가 7인 음료가 가진 알레르기 정보 조회
In : product = Product.objects.get(id=134)


# forward_many_to_many_manager 확인
In : product.allergy
Out: <django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager at 0x7feef10b96d0>


# forward_many_to_many_manager로 중간 테이블(allergy_product)에서 product가 일치하는 데이터 조회
In : product.allergy.all()
Out: <QuerySet [<Allergy: 우유>, <Allergy: 대두>, <Allergy: 밀>]>


# 조회해 온 각 객체의 속성에 접근
In : product.allergies.all()[0]
Out: <Allergy: 우유>

In : product.allergy.all()[0].name
Out: '우유'

In : product.allergy.all()[1].name
Out: '대두'


# 중간 테이블(allergy_product)에 데이터 추가
In : product.allergy.add(Allergy.objects.get(id=4))

In : product.allergy.all()
Out: <QuerySet [<Allergy: 우유>, <Allergy: 대두>, <Allergy: 밀>, <Allergy: 토마토>]>


# 중간 테이블에 데이터 삭제
In : product.allergy.remove(Allergy.objects.get(id=4))

In : product.allergy.all()
Out: <QuerySet [<Allergy: 우유>, <Allergy: 대두>, <Allergy: 밀>]>


# 중간 테이블에 데이터 전부 삭제
In : product.allergy.clear()

In : product.allergy.all()
Out: <QuerySet []>

 

- ManyToManyField를 가지고 있지 않은 테이블을  기준으로 데이터를 조회, 추가, 삭제 (related_name 옵션이 없을 때)

# id가 7인 알레르기를 가진 음료 정보 조회
In : allergy = Allergy.objects.get(id=7)


# 중간 테이블에서 allergy 정보가 일치하는 데이터 모두 조회(역참조를 사용할 수 밖에 없음)
In : allergy.product_set.all()
Out: <QuerySet [<Product: 피치 & 레몬 블렌디드>, <Product: 피치 젤리 티>]>


# 조회해 온 각 객체의 속성에 접근
In : allergy.product_set.all()[0].korean_name
Out: '피치 & 레몬 블렌디드'


# 중간 테이블에 데이터 추가 >> AllergyProduct에 직접 추가해야 함
In : AllergyProduct.objects.create(allergy=allergy, product=Product.objects.get(id=114))
Out: <AllergyProduct: 제주 별빛 애플 주스 : 복숭아>

In : allergy.product_set.all()
Out: <QuerySet [<Product: 피치 & 레몬 블렌디드>, <Product: 피치 젤리 티>, <Product: 제주 별빛 애플 주스>]>


# 중간 테이블에 데이터 삭제
In : a = AllergyProduct.objects.get(allergy=allergy, product=Product.objects.get(id=114))

In : a
Out: <AllergyProduct: 제주 별빛 애플 주스 : 복숭아>

In : a.delete()
Out: (1, {'products.AllergyProduct': 1})

In : allergy.product_set.all()
Out: <QuerySet [<Product: 피치 & 레몬 블렌디드>, <Product: 피치 젤리 티>]>

 

  • 위 두 개의 예시처럼 forward_many_to_many_manager는 ManyToManyField를 가지고 있는 테이블 기준에서 조회할 때 해당 attribute 이름으로 .을 통해 손쉽게 조회가 가능하다.

  • 뿐만 아니라 추가, 삭제 할 때는 forward_many_to_many_manager가 제공하는 add(), remove() 메소드를 통해 편리하게 처리할 수 있다.

  • 하지만 반대의 경우, 이 테이블에서 중간 테이블을 볼 땐 역참조를 사용하는 방법 그대로 사용해야하므로 번거로움이 있다. 따라서 ManyToManyField를 추가할 땐 좀 더 데이터의 기준이 되는 테이블에 추가하는 것이 좋다.

 

- related_name을 이용하여 데이터를 조회, 추가, 삭제

related_name 옵션을 이용하면 바로 위의 경우 처럼 역참조의 불편함을 해결할 수 있다. 

# 알레르기 id가 6인 알레르기 정보 조회
In : allergy = Allergy.objects.get(id=6)

In : allergy
Out: <Allergy: 오징어>


# related_name에 설정된 manager를 통해 중간 테이블에서 allergy 정보가 일치하는 데이터 조회
In : allergy.product.all()
Out: <QuerySet [<Product: 아이스 제주 까망 라떼>, <Product: 제주 까망 라떼>]>


# 중간 테이블에 데이터 추가 >> 역참조 상황에서도 forward-many-to-many-manager를 이용한 것 같이 쓸 수 있음
In : allergy.product.add(Product.objects.get(id=115))

In : allergy.product.all()
Out: <QuerySet [<Product: 아이스 제주 까망 라떼>, <Product: 제주 까망 라떼>, <Product: 제주 쑥쑥 라떼>]>


# 중간 테이블에 데이터 삭제
In : allergy.product.remove(Product.objects.get(id=115))

In : allergy.product.all()
Out: <QuerySet [<Product: 아이스 제주 까망 라떼>, <Product: 제주 까망 라떼>]>

 

  • allergy.product.all()에서 product는 Product 클래스에서 ManyToManyField의 옵션으로 설정한 related_name 값이다.

  • ManyToManyField를 가지고 있지 않은 테이블에서도 역참조 상황에서 손쉽게 중간 테이블의 데이터에 접근할 수 있다.

  • 단, 이를 사용할 때 related_name를 정확하게 설정하지 않으면 자칫 데이터 조회 시 의미를 헷갈릴 수 있으니 주의하는 게 좋을 것 같다.