스타벅스 상품페이지 django 모델링을 살펴보자
class Size(models.Model):
sizeName = models.CharField(max_length = 50, null=True)
sizeGauge = models.DecimalField(max_digits=5, decimal_places=2, null=True)
class Meta:
db_table = 'sizes'
class Nutrition(models.Model):
totalAmount = models.DecimalField(max_digits=5, decimal_places=2, null=True)
fat = models.DecimalField(max_digits=5, decimal_places=2, null=True)
protein = models.DecimalField(max_digits=5, decimal_places=2, null=True)
natryum = models.DecimalField(max_digits=5, decimal_places=2, null=True)
suger = models.DecimalField(max_digits=5, decimal_places=2, null=True)
caffeine = models.DecimalField(max_digits=5, decimal_places=2, null=True)
class Meta:
db_table = 'nutritions'
#테이블명이 있기 때문에 컬럼들은 too much detail할 필요없다.
#컬럼 명 줄때 장고가 포린키 주면 장고에서 알아서 _id해준다.
#컬럼 명 줄때 _포함해도 됨.
class DrinkCategories(models.Model):
name = models.CharField(max_length = 50, null=True)
class Meta:
db_table = 'drinkCategories'
#만약 subcategory말고 drinkcategory로 만들어주면 카테코리 만들때마다 테이블만들어야함. subcategory로
class MainCategory(models.Model):
drink_category = models.ForeignKey('DrinkCategories', models.SET_NULL,null=True)
class Meta:
db_table = 'mainCategories'
class Allergy(models.Model):
allergyType = models.CharField(max_length = 50, null=True)
class Meta:
db_table = 'allergies'
class Item(models.Model):
name = models.CharField(max_length = 50, null=True)
nameEnglish = models.CharField(max_length = 50, null=True)
description = models.CharField(max_length = 50, null=True)
thumbnail = models.CharField(max_length = 200, null=True)
drink_category = models.ForeignKey('DrinkCategories', on_delete = models.SET_NULL, null = True)
nutrition = models.ForeignKey('Nutrition', on_delete = models.SET_NULL, null = True)
size = models.ForeignKey('Size', on_delete = models.SET_NULL, null = True)
allergy = models.ManyToManyField('Allergy', through='ItemAllergy')
class Meta:
db_table = 'items'
class ItemAllergy(models.Model):
item = models.ForeignKey('Item', on_delete = models.SET_NULL, null = True)
allergy = models.ForeignKey('Allergy', on_delete = models.SET_NULL, null = True)
class Meta:
db_table = 'itemAllergies'
특징은 Item테이블이 대부분의 테이블을 정참조하고, allergy테이블의 경우 ManyToMany관계를 형성하고 있다.
select_related를 사용해서 ORM의 쿼리를 줄여보자.
ORM과 데이터베이스간의 소통에서 쿼리가 발생해 값을 주고받는 과정은 웹서비스 차원에서 많은 트레픽을 가져온다.
Item.objects.all()[3].size.sizeName
위의 코드는 객체를 쿼리셋으로 전부 들고와서 인덱싱해주고 객체안에 size_id(Item필드 안의 size_id 컬럼)에 해당하는 size테이블로 이동 후 sizeName값을 가져온다.
적어도 두번이상(처음에 all로불러올때, size테이블로갈때)의 쿼리가발생한다.
Item.objects.select_related('size')[3].size.sizeName - 보기에는 더 길어져 보이지만 select_related라는 매서드가 size_id필드(Size테이블을 참조하는)가 참조하는 Size테이블을 caching하고 size 테이블로 access해서 sizeName을 가져온다.
즉, foriegnKey를 가지고있는 컬럼의 테이블까지 caching을 해옴으로서 query를 줄여주는 효과를 가져온다.
ManyToMany관계에서 중간관리자 테이블의 입장에서 보면 양측 테이블 모두 정참조이다. 이경우에 데이터의 러올 때 중간 테이블의 관점에서 select_related를 사용할 수 있다.
(1)Item.objects.select_related('item').filter(item_id=1)
(2)Item.objects.filter(item_id=1)
(1)코드와 (2)코드의 차이는 무엇일까?
(2)의 코드가 단순히 item_id=1과인 객체를 전부 불러오는 매서드라면, (1)의 코드는 item필드가 Item테이블과 연관되어 있기 때문에 이 객체중(이과정에서 Item테이블을 caching으로 저장한다) item_id=1인 객체를 불러오는것이다.
(1)과(2)는 이단계까지는 한번의 쿼리스트링으로 그친다. 하지만 좀 더 파고들어 실재로 Item테이블의 객체를 가져와보자.
(1)Item.objects.select_related('Item').filter(item_id=1)[0].item.name
(2)Item.objects.filter(item_id=1)[0].item.name
(1)과(2)는 마찬가지로 item테이블에 접근해서 name value를 가져온다. 하지만 1번의 경우 select_related로 caching된 item table을 가져오기 때문에 쿼리스트링이 발생하지않는다.
ManyToMany관계는 둘의 관계를 중간관리 테이블에서 관리한다. 알러지를 유발하는 제료를 예로들어보면 우유, 대두, 견과류 등이 있다. 상품의 종류도 아이스라떼, 프라푸치노, 카페모카 등 이 있다. 여기서 우유는 아이스라떼에도 카페모카에도 들어간다. 그리고 프라푸치노또한 우유, 대두, 견과류를 포함할 수 있다. 즉, 하나의 객체가 여러가지 대상을 가질 수 있다는 뜻이다. 이를 ManyToMany관계라고 한다.
Item.objects.prefetch_related('allergy').filter(name='아이스라떼')
코드를 보자. 위의 코드는 allergy테이블을 caching한다는 뜻이다. 즉, allergy테이블을 일단 캐시데이터로 가져와 저장한다. 그리고 제품명이 아이스라떼인 객체를 쿼리셋에 담아서 가져온다.
<QuerySet [<Item: Item object (1)>]>
하나의 객체가 딸려왔다.
이제 allergy테이블로 접근해보자.
Item.objects.prefetch_related('allergy').filter(name='아이스라떼')[0].allergy
하나의 객체를 인덱싱해주고 select_related에서 했던 것 처럼 .allergy를 통해서 allergy테이블로 접근한다.(여기서 allergy는 Item테이블안에 있는 allergy_id이다!)
<django.db.models.fields.related_descriptors.create_forward_many_to_many_manager..ManyRelatedManager at 0x7fdd4a141dd8>
select_related의 경우에는 조건에 해당하는 allergy객체를 리턴했지만 원하지않는 값이 리턴되었다.
In [138]: Item.objects.prefetch_related('allergy').filter(name='아이스라떼')[0].allergy.values()
Out[138]: <QuerySet [{'id': 1, 'allergyType': '대두'}, {'id': 2, 'allergyType': '우유'}]>
values를 값을 불러와보면 해당 filter에 맞는 값이 쿼리셋으로 value형태로 리턴되었다. 즉, django ORM이 알아서 '아이스라떼'에 해당하는 id를 ItemAllergy중간테이블과 Allergy테이블을 비교해서 매칭되는 값을 리턴한 것이다.
In [140]: Item.objects.prefetch_related('allergy').filter(name='아이스라떼')[0].allergy.all()
Out[140]: <QuerySet [<Allergy: Allergy object (1)>, <Allergy: Allergy object (2)>]>
all()을 통해서 값을 불러오면 쿼리셋으로 객체자체를 가져온다. 즉, ManyToMany의 관계이기 때문에 한개이상의 값을 가정하고 쿼리를 보내야 한다는 것이다.
class Item(models.Model):
name = models.CharField(max_length = 50, null=True)
nameEnglish = models.CharField(max_length = 50, null=True)
description = models.CharField(max_length = 50, null=True)
thumbnail = models.CharField(max_length = 200, null=True)
drink_category = models.ForeignKey('DrinkCategories', on_delete = models.SET_NULL, null = True)
nutrition = models.ForeignKey('Nutrition', on_delete = models.SET_NULL, null = True)
size = models.ForeignKey('Size', on_delete = models.SET_NULL, null = True)
allergy = models.ManyToManyField('Allergy', through='ItemAllergy')
class Meta:
db_table = 'items'
class Size(models.Model):
sizeName = models.CharField(max_length = 50, null=True)
sizeGauge = models.DecimalField(max_digits=5, decimal_places=2, null=True)
class Meta:
db_table = 'sizes'
Item테이블은 Size테이블을 정참조하고, Size테이블은 Item테이블을 역참조한다. 그렇다면 Size테이블의 관점에서 참조당하는 객체를 가져오려면 어떻게해야할까?
class Item(models.Model):
size = models.ForeignKey('Size', related_name='items' on_delete = models.SET_NULL, null = True)
위와같이 테이블 모델링부터 참조하는(size테이블을 보고있는)테이블이 자기가 참조당하는 입장이되었을 때 related_name='items'와 같이 이름을 붙혀준다.
In [190]: Size.objects.prefetch_related('item_set')
Out[190]: <QuerySet [<Size: Size object (1)>, <Size: Size object (2)>, <Size: Size object (3)>, <Size: Size object (4)>]>
위와같이 기존에 prefetch_related('item')만 lowercase로 적어줬던 것과 달리 'item'뒤에 '_set'을 붙혀주면 Size테이블입장에서 자신을 참조하는 테이블의 객체를 쿼리셋으로 가져온다. 그렇다면 여기서 Item테이블로 어떻게 갈까??(여기서 item_set의 item은 Item테이블이다.)
In [203]: Size.objects.prefetch_related('item_set')[3].item_set
Out[203]: <django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager..RelatedManager at 0x7fdd4a08aac8>
쿼리셋에서 3번값을 인덱싱해주고 .item_set을 사용해서 Item테이블로 넘어간다. select_related와 ManyToMany관계에서의 prefetch_related와 다르게 역참조의 경우 'item'뒤에 마찬가지로 '_set'을 붙혀준다.
위에서 output으로 객체가 나오지 않았지만 이미 .item_sit을 사용해줌으로 인해서 Item테이블로 넘어간 상태이기 때문에, .values() 또는 .all() 또는 filter()을 사용해서 원하는 값들을 가져올 수 있다.
In [206]: Size.objects.prefetch_related('item_set')[2].item_set.values()
Out[206]: <QuerySet [{'id': 4, 'name': '프라푸치노', 'nameEnglish': 'frafchino', 'description': '맛있음', 'thumbnail': 'url@url.com', 'drink_category_id': 1, 'nutrition_id': 1, 'size_id': 3}]>
values()를 다들고 왔는데 Item 중에서 id가 4인 하나의 객체만 가져왔다. 그 이유는 오직 이 객체만이 Size테이블을 정참조 하고 있기 때문이다.(Size테이블 입장에서는 역참조)
즉, size테이블에서 역참조로 타고 온거니깐, Item테이블에 size_id값이 Null이 아닌 경우에만 값을가져온다.
그렇다면 Size테이블의 id가3인 객체의 사이즈이름과 그 객체를 보고있는 Item테이블의 객체의 이름을 알고싶다면 어떻게 해야할까. 즉, 해당테이블의 객체에 있는 value와 그객체를 역참조하고있는 테이블의 객체의 이름을 알고싶다면??
lookup key를 사용하면 된다.
In [240]: Size.objects.filter(id=3).prefetch_related('item_set').values('id', 'sizeName', 'item__name')
Out[240]: <QuerySet [{'id': 3, 'sizeName': 'benti', 'item__name': '프라푸치노'}]>
위의와같이 Size테이블에서 id=3인 필터를 걸어주고, prefetch_related로 item테이블에있는 객체를 caching한다.(caching했다고 해서 size테이블에서 item테이블로 이동했다는 것이아니다)그리고 Size테이블의 id, sizeName을 불러오고 itemname으로 Item테이블의 name 값을 불러온다. 'name'이라는 lookup 규칙(?)을 사용해서 Size테이블의 객체와함께 Item테이블의 객체값을 불러온 것이다.
In [255]: Size.objects.filter(id=3).prefetch_related('item_set')[0].item_set.values()
Out[255]: <QuerySet [{'id': 4, 'name': '프라푸치노', 'nameEnglish': 'frafchino', 'description': '맛있음', 'thumbnail': 'url@url.com', 'drink_category_id': 1, 'nutrition_id': 1, 'size_id': 3}]>
In [256]: Size.objects.filter(id=3)[0].item_set.values()
Out[256]: <QuerySet [{'id': 4, 'name': '프라푸치노', 'nameEnglish': 'frafchino', 'description': '맛있음', 'thumbnail': 'url@url.com', 'drink_category_id': 1, 'nutrition_id': 1, 'size_id': 3}]>
위의 두가지경우는 '_set'을 통해서 역참조를 한 경우이다. 두가지 경우 다 id=3인 Size테이블의 객체를 바라보고있는 Item테이블의 객체의 values를 가져왔다.
그렇다면 prefetch_related는 왜 쓰는 걸까?
답은 쿼리를 줄이는데 있다. 앞서설명했듯이 prefetch_related와 select_related를 사용하면 참조관계에 있는 테이블을 캐싱한다고 했다.
prefetch_related를 사용하지않은 두번째 경우는 같은결과값을 가져왔지만 item_set 매서드로 Item 테이블로 acess했을 때 Item테이블을 캐싱하지 않았기 때문에 쿼리를 한번 더 추가한다.
prefetch_related를 사용해서 코드는 길어졌지만 쿼리의 효율성을 위해서 경우에 따라 추가해주는것이 좋을 것 같다