Django - Cookbook ORM(쿼리문) 연습하기 - 1
Web/Django

Django - Cookbook ORM(쿼리문) 연습하기 - 1

뉴비뉴 2020. 4. 15.

안녕하세요.

 

평소 ORM은 사용할 줄 알지만 내부적으로 쿼리문이 어떻게 짜여지고,

어떻게 사용해야 더 효율적이고, 최대한 쿼리문을 줄일 수 있는 방법을 찾아보면서 코드를 짜다가

휴일에 시간이 생겨 평소에 Github Star를 박아두었던 Django CookBook을 보면서 ORM 사용법을 복습하고

지금보다 더 효율적인 쿼리를 짜기 위해 Cookbook을 살펴보겠습니다.

 

Cookbook 문서 바로가기

 

장고 ORM 요리책 — Django ORM Cookbook 2.0 documentation

© Copyright 2018, Agiliq Revision 159ade05.

django-orm-cookbook-ko.readthedocs.io

"Cookbook이란 장고 ORM 요리책은 장고를 이용한 다양한 레시피(조리법)를 담은 책입니다. 장고 ORM/쿼리셋으로 ~을 하려면 어떻게 하나요? 하는 50여 개의 질문과 답을 담고 있습니다."

 

Model은 링크를 눌러 README 에 있는 Events와 Entities 를 생성해주시면 됩니다.

 

python manage.py startapp events
python manage.py startapp entities

 

App을 생성해주고, README에 있는 코드를 입력해주고, migrations, migrate 해주시면 테이블이 생성됩니다.

테이블 생성 코드의 궁금한 점

1. from django.utils.text import slugify 참고 URL

class Article(models.Model):
    headline = models.CharField(max_length=100)
    pub_date = models.DateField()
    reporter = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='reporter'
    )
    slug = models.SlugField()

    def save(self, *args, **kwargs):
        self.slug = slugify(self.headline)
        super(Article, self).save(*args, **kwargs)

    def __str__(self):
        return self.headline
  • slug 생성을 위한 모듈이다.
  • slugify("Python slug 생성", allow_unicode=True)
  • 한글을 넣고싶다면 allow_unicode를 True로 주어야 한다.
  • model의 save함수를 overriding하여 model이 저장될 때 마다 slug field에 값을 추가한다

2. editable=False

class Event(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False  # 수정 가능 유무
        # ex) DateTimeField의 auto_now_add 속성이 True로 설정되면 editable 속성이 자동으로
        # False 로 설정되기 때문에 editable 속성을 True로 변경해주면 디테일 화면에서 확인 및 수정이 가능해진다.
    )
    epic = models.ForeignKey(Epic, on_delete=models.CASCADE)
    details = models.TextField()
    years_ago = models.PositiveIntegerField()

3. managed = False

class TempUser(models.Model):
    first_name = models.CharField(max_length=100)

    class Meta:
        managed = False  # 해당 모델은 자동으로 테이블을 생성하지 않게 된다.
        db_table = "temp_user"

 

CookBook Docs

1. Django ORM이 실행하는 실제 SQL 쿼리문을 확인할 수 있나요?

>>> queryset = Event.objects.all()
>>> str(queryset.query)
SELECT "events_event"."id", "events_event"."epic_id",
    "events_event"."details", "events_event"."years_ago"
    FROM "events_event"

아래 예제는 filter -> where 을 사용하는 예제이다.

gt는 초과, gte 이상, lt는 미만, lte는 이하

>>> queryset = Event.objects.filter(years_ago__gt=5)
>>> str(queryset.query)
SELECT "events_event"."id", "events_event"."epic_id", "events_event"."details",
"events_event"."years_ago" FROM "events_event"
WHERE "events_event"."years_ago" > 5

 

2. OR 연산으로 일부 조건을 하나라도 만족하는 항목을 구하려면 어떻게 하나요?

문서에서는 django.contrib.auth의 User를 대상으로 쿼리문을 작성해 테스트하고 있습니다.

연산조건은 아래와 같습니다.

 

"OR 연산으로 여러 조건 중 하나라도 만족하는 행을 구해야 하는 경우가 많습니다. 이름이 ‘R’로 시작하거나 성이 ‘D’로 시작하는 모든 사용자를 구한다고 해 봅시다."

 

username이 'R'로 시작하거나 first_name이 'D' 로 시작하는 모든 사용자를 구해야 합니다.

비교에 쓰일 적당한 데이터를 생성해주겠습니다.

User.objects.create(
	username='Gilbong',
    first_name='Gil',
    last_name='bong',
    email='gilbong@gmail.com'
)

User.objects.create(
	username='Ricky',
    first_name='Ricky',
    last_name='Dayal',
    email='ricky@gmail.com'
)

# 1
queryset = User.objects.filter(
	first_name__startswith='R') | User.objects.filter(
    last_name__startswith='D')

# 2
from django.db.models import Q
queryset = User.objects.filter(Q(first_name__startswith='R') | Q(last_name__startswith='D'))

# 1과 2의 방법 둘 다 똑같은 쿼리문을 요청합니다.

육안으로 보기에 # 2가 더 깔끔해보이기 때문에 Q를 사용하는 것이 더 좋아보입니다.

 

queryset을 출력해보면 Ricky의 정보만 출력되는 것을 확인할 수 있습니다.

 

3. AND 연산으로 여러 조건을 모두 만족하는 항목을 구하려면 어떻게 하나요?

 

"AND 연산으로 여러 조건을 모두 만족하는 행을 구해야 하는 경우가 많습니다. last_name이 ‘D’로 시작하고 first_name이 ‘R’로 시작하는 모든 사용자를 구한다고 해 봅시다."

# 1
# filter는 조건에 맞다면 출력을 하는 조건문이기 때문에 AND 연산의 &를 굳이 붙일 필요가 없습니다.

queryset = User.objects.filter(first_name__startswith='R', last_name__startswith='D')

# 2
# Q를 사용하는 방안
queryset = User.objects.filter(Q(first_name__startswith='R') & Q(last_name__startswith='D'))

4. NOT 연산으로 조건을 부정하려면 어떻게 하나요?

 

"id < 5 라는 조건을 만족하지 않는 모든 사용자를 구해 봅시다. 이를 수행하려면 NOT 연산이 필요합니다."

 

exclude(<condition>)와 filter(~Q(<condition>)) 으로 구할 수 있습니다.

~Q 방법은 처음보는 방법이네요 아직 갈 길이 먼거 같습니다. ㅎㅅㅎ

 

# 1
# include 포함하다, exclude 포함하지 않다.
# id가 5미만이지 않은 것을 가져와라
>>> queryset = User.objects.exclude(id__lt=5)
>>> queryset 

# 2
# 위와 결과는 같다. 
>>> queryset = User.objects.filter(~Q(id__lt=5))

5. 동일한 모델 또는 서로 다른 모델에서 구한 쿼리셋들을 합할 수 있나요?

 

SQL 에서는 여러 개의 결과 집합을 합할 때 UNION 연산을 이용합니다.

Django ORM에서 union 메서드를 이용해 쿼리셋을 합할 수 있습니다.

합하려는 쿼리셋의 모델이 서로 다른 경우, 각 쿼리셋에 포함도니 필드와 데이터 유형이 서로 맞아야 합니다.

 

UNION Join은 중복을 제거하여 조인한다.

 

# 1 UNION JOIN SUCCESS
q1 = User.objects.filter(id__lt=5)
# <QuerySet [<User: Gilbong>]>

q2 = User.objects.exclude(id__lt=5)
# <QuerySet [<User: Ricky>]>

q1.union(q2)
<QuerySet [<User: Gilbong>, <User: Ricky>]>

q2.union(q1)
<QuerySet [<User: Gilbong>, <User: Ricky>]>

# 2 UNION JOIN FAIL
q3 = EventVillain.objects.all()
q1.union(q3)

union 메서드는 합하려는 쿼리셋의 필드와 데이터 유형이 서로 일치할 때만 실행할 수 있습니다.

그래서 # 2의 명령이 실패하였습니다.

 

결국 필드가 같다면 UNION JOIN이 실행된다는 것이고,

Hero 모델과 Villanin 모델은 둘 다 name, gender 필드를 갖고 있습니다.

values_list로 해당 데이터만 가져온 뒤 UNION JOIN을 수행할 수 있습니다.

# 1
Hero.objects.all().values_list(
    "name", "gender"
).union(
Villain.objects.all().values_list(
    "name", "gender"
))

# 1을 수행한다면 중복 된 데이터는 제거되고 name과 gender의 정보가 출력 될 것 입니다.

 

6. 필요한 열만 골라 조회하려면 어떻게 하나요?

 

쿼리셋의 values 메서드와 values_list 메서드, only 메서드

 

"이름이 R로 시작하는 모든 사용자의 이름(first_name)과 성(last_name)을 구해 봅시다. 데이터베이스 시스템의 부하를 줄이기 위해 그 외의 열은 가져오지 않겠습니다."

 

# 1 values 사용
queryset = User.objects.filter(first_name__startswith='R').values('first_name', 'last_name') 

queryset
<QuerySet [{'first_name': 'Ricky', 'last_name': 'Dayal'}]>  # 사전의 리스트 형식

# 2 only 사용
# values 메서드와 다른 점은 id 필드를 함께 가져온다는 점이다.
queryset = User.objects.filter(first_name__startswith='R').only('first_name', 'last_name')

queryset
<QuerySet [<User: Ricky>]>

# 1 value 의 경우 사전의 리스트 형식으로 데이터가 출력되므로 안의 데이터를 사용하려면

queryset[0] 으로 list를 벗기고, dict 호출하는 방법으로 호출을 해주면 됩니다.

ex) queryset[0].get('first_name')  -> 'Ricky'

 

# 2 only 의 경우 values 와 다른 점은 id 필드를 함께 가져온다는 것 입니다.

실제 데이터가 나오는 형식은 인스턴스 변수를 호출하는 것 처럼 사용됩니다.

ex) queryset[0].first_name

ex) queryset.values('') 쿼리셋에서 사용할 수 있는 것들을 다 사용할 수 있습니다.

 

7. Django 에서 서브쿼리 식을 사용할 수 있나요?

 

서브쿼리를 사용할 수 있습니다.

ex) from django.db.models import Subquery

 

# 1 Subquery 사용
from django.db.models import Subquery

users = User.objects.all()
UserParent.objects.filter(user_id__in=Subquery(users.valeus('id'))) # 1-1 
UserParent.objects.filter(user_id__in=users.values('id')) # 1-2

그렇다면 # 1-1 과 # 1-2 의 차이는 무엇일까..출력해보니 결과는 똑같이 출력된다.

 

2020-04-15

댓글

💲 추천 글