2월 12, 2022

[Django]ORM QuerySet method

 Model method(또는 QuerySet method)를 통하여 Django에 내장된 데이터 관리 기능을 사용할 수 있습니다. 


우선, models.py의 코드는 다음과 같습니다. 


 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from tkinter import CASCADE
from unicodedata import category
from django.db import models

# Create your models here.
class Menu(models.Model):
    name = models.CharField(max_length=45)
    class Meta: #모델에 대한 다양한 사항 정의
        db_table = 'menu' # 테이블을 식별하는데 사용하는 이름 설정(db상 이름 menus)

class Categories(models.Model):
    name = models.CharField(max_length=45)
    menu = models.ForeignKey('Menu', on_delete=models.CASCADE) 
    # 1:N에서 N쪽이 관계(FK)선언
    # CASCADE: 1쪽이 삭제됐을때 모든 N쪽 데이터 삭제
    class Meta:
        db_table = 'categories'

class Images(models.Model):
    image_url = models.CharField(max_length=300)
    drink = models.ForeignKey('Drinks',on_delete=models.CASCADE)
    class Meta:
        db_table = 'images'

class Drinks(models.Model):
    korean_name = models.CharField(max_length=45)
    english_name = models.CharField(max_length=45)
    description = models.TextField(max_length=100)
    category = models.ForeignKey('Categories', on_delete=models.CASCADE)
    class Meta:
        db_table = 'drinks'

class Allergy_drink(models.Model):     #drinks와 allergy 의 중간테이블 
    allergy = models.ForeignKey('Allergy', on_delete=models.CASCADE)
    drink = models.ForeignKey('Drinks', on_delete=models.CASCADE)
    class Meta:
        db_table = 'allergy_drink'


class Allergy(models.Model):
    name = models.CharField(max_length=45)
    drinks = models.ManyToManyField(Drinks, through='Allergy_drink') #through에 중간테이블명 넣어줌
    class Meta:
        db_table = 'allergy'
   

class Nutritions(models.Model):  
    one_serving_kcal = models.DecimalField(max_digits=10, decimal_places=5)
    sodium_mg = models.DecimalField(max_digits=10, decimal_places=5)
    saturated_fat_g = models.DecimalField(max_digits=10, decimal_places=5)
    sugars_g = models.DecimalField(max_digits=10, decimal_places=5)
    protein_g = models.DecimalField(max_digits=10, decimal_places=5)
    drink = models.ForeignKey('Drinks', on_delete=models.CASCADE )
    size = models.ForeignKey('Sizes', on_delete=models.CASCADE)
    class Meta:
        db_table = 'nutritions'

class Sizes(models.Model):
    name = models.CharField(max_length=45)
    size_mi = models.CharField(max_length=45)
    size_fluid_ounce = models.CharField(max_length=45)
    class Meta:
        db_table = 'sizes'

# # categories(다) - (일)menu :  one-to-many : Menu 메뉴에 음료/푸드/상품 등 카테고리
# # drinks(다) - (일)categories : one-to-many
# # images (다) - drinks(일) : one-to-many
# # nutritions(다) - drinks(일) : one-to-many
# # nutritions(다) - sizes(일) : one-to-many
# drinks(다) - (다) allergy : many-to-many / allergy_drink 중간테이블
# NULL은 포인터가 가져올 값이 없는 상태
# 중간테이블 직접 정의 through model


이때, 서로 참조하고 있는 테이블들을 Read하는 방식에 대해 자세히 알아보려 합니다.

데이터는 임의로 넣어주었습니다.


먼저 다대일 (Categories - Menu)의 경우를 확인해보겠습니다.

카테고리가 메뉴를 정참조하고, 메뉴가 카테고리를 역참조하는 구조입니다.

menu변수에 객체 담아줍니다

menu = Menu.objects.get(id=2) menu <Menu: Menu object (2)>

_set 을 이용해 categories를 나타내는 매니저를 설정하고, get으로 역참조 객체(categories) 가져옵니다.

  • menu.categories_set.get(id=4)       # 역참조시 _set 사용 
  • <Categories: Categories object (4)>
  • menu.categories_set.get(id=4).name '브레드'

 

filter로 역참조 객체를 가져옵니다.

  • menu.categories_set.filter(id=4) 
  • <QuerySet [<Categories: Categories object (4)>]>
  • menu.categories_set.filter(id=4).name 
  • Traceback (most recent call last): File "<console>", line 1, in <module> AttributeError: 'QuerySet' object has no attribute 'name'

filter는 객체가 아닌 Queryset을 반환하기에 .name을 했을 시 값이 나오지 않는 모습입니다


따라서 [0]을 통해서 Queryset안의 객체를 꺼내주면 .name으로 카테고리의 이름을 확인할 수 있습니다.

  • menu.categories_set.filter(id=4)[0].name 
  • '브레드'


이번에는 category 변수에 객체를 담아주고, 정참조하는 Menu를 가져와봅니다. 

  • category = Categories.objects.get(id=1) 
  • category.menu 
  • <Menu: Menu object (1)> 
  • category.menu.name 
  • '음료'


이번에는 다대다 Drinks(다) - Allergy_drink(중간테이블) - Allergy(다) 의 경우를 확인해보겠습니다.

drink에 get으로 객체를 담고 .allergy를 찍으니 Drinks에 allergy란 속성이 없다고 합니다.

  • drink = Drinks.objects.get(id=4) 
  • drink.allergy 
  • Traceback (most recent call last): File "<console>", line 1, in <module> AttributeError: 'Drinks' object has no attribute 'allergy'


위의 역참조의 경우처럼 _set을 사용해야 drink와 연결된 값들을 확인할 수 있습니다

  • drink.allergy_set.all() 
  • <QuerySet [<Allergy: Allergy object (2)>, <Allergy: Allergy object (6)>]> drink.allergy_set.get(id=2).name 
  • '우유'


이번에는 이렇게 drink와 연결된 특정 알러지 값이 아닌,  allergy의 name 전체가 꺼내고 싶습니다. for 문을 사용해줍니다.

  • for i in drink.allergy_set.all(): print(i.name)
  • ... 
  • 우유 
  • 토마토

다대다 관계이기에 반대로 알러지를 담은 변수에서 연결된 Drinks 값을 꺼내는 것도 가능하겠죠?

  • allergy = Allergy.objects.get(id=2) 
  • allergy.drinks_set.get(id=1) 
  • Traceback (most recent call last): File "<console>", line 1, in <module> AttributeError: 'Allergy' object has no attribute 'drinks_set'

어라... drinks_set이란 어트리뷰트는 없다고 나옵니다.

_set을 사용하지 않고 정참조때와 같은 방식으로 drinks를 꺼내보았습니다.

allergy.drinks <django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager object at 0x7ffab86eafd0>

.drink가 many_to_manager로 동작하고 있음을 볼 수 있습니다

앞서 drink 변수의 경우는 _set을 붙여줘야만 했는데.. 뭐가 다른걸까요??

drink.allergy_set <django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager object at 0x7ffab86eacd0>


코드를 자세히 보니, 제가 Allergy 클래스에 ManyToManyField를 정의해주었습니다.
one-to-many에서 many인 쪽에 FK를 정의하고, many쪽의 클래스로 인스턴스를 만들어 정참조하는 테이블을 호출할 때, _set을 사용하지 않는 것과 같은 메커니즘인 듯 보입니다.
관계 키를 정의해준 쪽에서는 _set을 사용하지 않고 호출할 수 있는 것으로 이해했습니다.


추가

1. exclude(), values(), values_list()를 활용해 DB로부터 READ를 해보았습니다

value_list는 값만 있는 리스트가 반환되는 반면, values는 {'name':'값"} 의 형식으로 딕셔너리가 반환됩니다.

  •  Allergy.objects.exclude(id=1).values_list('name')
  • <QuerySet [('우유',), ('난류',), ('밀',), ('아황산류',), ('토마토',)]> 
  • Allergy.objects.exclude(id=1).values('name') 
  • <QuerySet [{'name': '우유'}, {'name': '난류'}, {'name': '밀'}, {'name': '아황산류'}, {'name': '토마토'}]>


2. filter메소드에 연관된 테이블의 id가 아닌 값을 사용하여 필터링을 할때 menu__name="음료" 이런식으로 _를 두번 사용해야됩니다( __ )

  • Categories.objects.filter(menu__name="음료") 

  • <QuerySet [<Categories: Categories object (1)>, <Categories: Categories object (2)>]>