Django의 QuerySet은 Lazy 한 특성을 지닌다.
코드 상에서 QuerySet을 만드는 동안에는 실제 DB에 접근을 하지 않고, 실제로 데이터가 필요한 시점에서야 접근을 한다.
따라서 이러한 특성을 잘 이해하여 쿼리 표현식을 쓴다면 DB에 접근하는 횟수를 줄일 수 있고, 쿼리를 최적화하여 퍼포먼스를 증가시킬 수 있다.
( 데이터가 필요한 시점 )
◽ queryset
◽ print(queryset)
◽ list(queryset)
◽ for instance in queryset: print(instance)
# Entry를 선언하는 시점에 q는 단순 쿼리셋 지나지 않았다.
>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q = q.exclude(body_text__icontains="food")
# print(q) 로 쿼리셋을 불렀을 때 DB에 접근한다.
>>> print(q)
(1) 조건을 추가한 QuerySet
◽ queryset.filter(...)
›› queryset
◽ queryset.exclude(...)
›› queryset
(2) 특정 모델 객체 1개 조회하기
◽ queryset[숫자인덱스]
›› 모델객체 or IndexError 반환
◽ queryset.get(...)
›› 모델객체 or DoesNotExist, MultipleObjectsReturned 반환
◽ queryset.first()
›› 모델객체 or None 반환
◽ queryset.last()
›› 모델객체 or None 반환
⚡ (TIP) 특정 모델 객체 1개를 흭득할때에는, first()
와 last()
를 많이 이용한다.
객체가 없을 때 None
을 반환하므로, 로직을 판별하기 쉽다.
(1) 모델 클래스의 Meta
속성으로 ordering 설정하기 : list로 지정
›› queryset 코드에서 직접 order_by()를 지정하면 이는 무시된다.
›› (2)번 방법보다 선호된다.
(2) 모든 queryset에 order_by()
에 지정하기
from django.conf import settings
from django.db import models
class POST(models.Model):
message = models.TextField(blank=True)
photo = models.ImageField(blank=True, upload_to='instagram/post/%Y%m%d')
is_public = models.BooleanField(default=False, verbose_name='공개여부')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
# return f"Custom Post object ({self.id})"
return self.message
# (1) 모델 클래스의 `Meta` 속성으로 ordering 설정하기
class Meta:
ordering = ['-id']
(1) QuerySet을 통해 알아보는 ORM 특징
◽ LazyLoading / Caching / EagerLoading
(2) QuerySet 상세요소
◽ 쿼리셋은 1개의 쿼리와 N개의 추가 쿼리로 구성된다.
◽select_related
와prefetch_related
쿼리를 최적화하려면 데이터 set을 "필요한 만큼만" "웬만하면 한 번에" 가져와야한다.
한 번에 가져오기 위해 Fokeignkey 는select_related
를 이용하고 ManyToManyFiled 는prefetch_related
를 이용한다.
⚡ 쿼리를 좀 더 명확하게 하기 위해 LazyLoading 이용하기
앞 서 언급한 LazyLoading의 특성으로 우리가 결과를 실행하기 전까지 장고는 실제 데이터베이스에 연동되지 않는다. 따라서 한 줄에 여러 메서드와 데이터베이스의 각종 기능을 엮어 넣는 대신에, 이를 여러 줄에 걸쳐 나눠 쓸 수 있다.
# 나쁜 예제
from django.models import Q
from promos.models import Promo
# 유효한 아이스크림 프로모션을 찾는 함수
def fun_function(**kwargs):
return Promo.objects.active().filter(Q(name__startswith=name|
Q(ddescription__icontains=name))
나쁜 예제의 경우 너무 길게 작성된 쿼리 체인이 화면을 넘겨버려 가독성이 좋지 못하다.
# 좋은 예제
from django.models import Q
from promos.models import Promo
# 유효한 아이스크림 프로모션을 찾는 함수
def fun_function(**kwargs):
results = Promo.objects.active()
results = results.filter(
Q(name__startswith=name) |
Q(description__icontains=name)
)
results = results.exclude(status='melted')
results = results.select_related('flavors')
좋은 예제처럼 코드를 여러 줄로 분리하면 가독성이 좋아진다.
⚡ 고급 Query 도구 이용하기
파이썬을 이용하여 데이터를 가공하기 이전에, 장고의 고급 쿼리도구들을 이용하여 데이터베이스를 통한 데이터 가공을 이용하자.
이렇게 하면 성능이 향상될 뿐 아니라 파이썬 기반 데이터 가공보다 더 잘 테스트되어 나온 코드를 이용할 수 있다.
예제를 통해 쿼리 표현식을 알아보자. 단일 모델에서의 쿼리이다.
아이스크림 상점을 방문한 모든 고객 중, 한 번 방문할 때마다 평균 한 주걱 이상의 아이스크림을 주문한 모든 고객 목록을 가져오는 샘플
""" avoid : 쿼리 표현식을 이용하지 않음 """
from models.customers import Customer
customers = []
for customer in Customer.objects.iterate():
if customer.scoops_ordered > customer.sotre_visits:
customers.append(customer)
몇 가지 문제점이 있는데,
""" good : 쿼리 표현식을 이용함 """
from django.db.models import F
from models.customers import Customer
customers = Customer.objects.filter(scoops_ordered__gt=F('store_visits'))
하지만 위처럼 쿼리 표현식을 이용하면 경합 상황에 대비할 수 있다. 모든 고객 레코드를 조회하지않고, 데이터베이스 자체 내에서 해당 조건을 만족하는 오브젝트만을 가져오도록 조건을 검으로써 "필요한 만큼만 가져오기" 를 잘 수행한 것이다.
단일모델에서의 쿼리를 알아보았으니, 이번엔 patched_related
와 select_related
를 이용하여 하나 이상의 모델과 얽힌 모델 설계에서의 쿼리를 최적하기 위한 방법을 알아보자
여기에 author 를 ForeignKey 로, tag 를 ManyToManyField 로 가진 Post 모델이 있다.
class Post(models.Model)
author = models.ForeignKey(Author)
tage = models.ManyToManyField(Tag)
""" avoid : DB를 두번이나 순회한다. """
post = Post.objects.get(id=1) # post를 가져올 때 한 번 post를 순회하고
author = post.author # author를 가져 올 때 한번 더 post를 순회한다.
""" good : select_realted로 author 값도 한번에 가져온다. """
post = Post.objects.select_related('author').get(id=1)
author = post.author
""" avoid : 100번 게시글의 태그를 가져오기 위해 100번이나 순회한다 """
post = Post.objects.all()
for post in posts: # post를 한 번 돌때마다
for tag in post.tag_set.all(): # tag_set을 가져온다
print(tag)
""" good : prefetch_relaed 로 post를 가져온 다음 tag_set도 가져오게 만든다. """
post = Post.objects.all().prefetch_related('tag_set)
for post in posts:
for tag in post.tag_set.all():
print(tag)