일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- clone coding
- CSS
- 파이썬리스트컴프리헨션
- 인증인가
- 리스트컴프리헨션
- 파이썬입출력
- JavaScript
- 윈도우우분투듀얼부팅
- bcrypt
- RESTfulAPI
- 백준
- 알고리즘
- DP
- django
- clone-coding
- decorator
- Python
- QuerySet
- promise
- **kwargs
- *args
- 해시충돌
- 인터넷 네트워크
- 자료구조
- docker
- 코딩테스트파이썬
- 파이썬문법
- wecode
- 파이썬
- 자바스크립트
- Today
- Total
개발기록장
[Django] ManyToManyField 설정과 데이터 조회 방법 본문
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 class
에allergy라는
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를 정확하게 설정하지 않으면 자칫 데이터 조회 시 의미를 헷갈릴 수 있으니 주의하는 게 좋을 것 같다.
'TIL > Django' 카테고리의 다른 글
[Django] 인스타그램 클론 코딩(2) - 로그인 기능 구현 (0) | 2021.02.02 |
---|---|
[Django] 인스타그램 클론 코딩(1) - 회원가입 기능 구현 (0) | 2021.02.02 |
[Django] QuerySet 메소드 정리 (2) - CRUD (5) | 2021.01.25 |
[Django] QuerySet 메소드 정리 (1) - 연결된 테이블에서 데이터 조회(정참조/역참조) (1) | 2021.01.21 |
[Django] Miniconda를 이용하여 파이썬 가상환경 설정하기 (1) | 2021.01.20 |