(Django) 쿼리셋과 ORM

duo2208·2021년 9월 3일
1

Django

목록 보기
5/23
post-thumbnail

QuerySet is Lazy (LazyLoading)


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)	

🚀 (Django) QuerySets are Lazy
🚀 (Django) QuerySet이 평가 될 때

다양한 조회요청 방법


(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을 반환하므로, 로직을 판별하기 쉽다.


🚀 (Django) Retrieving a single object with get()
🚀 (Django) Other QuerySet methods

정렬 조건 추가


(1) 모델 클래스의 Meta 속성으로 ordering 설정하기 : list로 지정
›› queryset 코드에서 직접 order_by()를 지정하면 이는 무시된다.
›› (2)번 방법보다 선호된다.

(2) 모든 queryset에 order_by() 에 지정하기

  • models.py
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']

QuerySet 최적화 전략


+ [PyCon Korea 2020] Django ORM(QuerySet) 구조와 원리 그리고 최적화 전략 강의 메모

(1) QuerySet을 통해 알아보는 ORM 특징
◽ LazyLoading / Caching / EagerLoading
(2) QuerySet 상세요소
◽ 쿼리셋은 1개의 쿼리와 N개의 추가 쿼리로 구성된다.
select_relatedprefetch_related

+ Django의 배신(주니어 개발자의 Django 삽질기) 메모

쿼리를 최적화하려면 데이터 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)

몇 가지 문제점이 있는데,

  • 데이터베이스 안의 모든 고객 레코드에 대해 하나하나 파이썬을 이용한 루프가 돌고 있다. 이는 매우 느리며 메모리도 많이 이용하게 된다.
  • 코드가 얼마나 이용되는지에 상관없이, 코드 자체가 경합 상황(race con-dition) 에 직면하게 된다. 이 코드는 사용자가 데이터와 상호 교류하는 동시에 실행되는 코드다. 여기서 단순히 READ 역할만을 하는 상황에서는 문제가 없을지 몰라도, 실행 중에 UPDATE가 처리되는 환경이라면 데이터 분실의 여지가 생긴다.

""" good : 쿼리 표현식을 이용함 """
from django.db.models import F
from models.customers import Customer

customers = Customer.objects.filter(scoops_ordered__gt=F('store_visits'))

하지만 위처럼 쿼리 표현식을 이용하면 경합 상황에 대비할 수 있다. 모든 고객 레코드를 조회하지않고, 데이터베이스 자체 내에서 해당 조건을 만족하는 오브젝트만을 가져오도록 조건을 검으로써 "필요한 만큼만 가져오기" 를 잘 수행한 것이다.


단일모델에서의 쿼리를 알아보았으니, 이번엔 patched_relatedselect_related 를 이용하여 하나 이상의 모델과 얽힌 모델 설계에서의 쿼리를 최적하기 위한 방법을 알아보자

여기에 author 를 ForeignKey 로, tag 를 ManyToManyField 로 가진 Post 모델이 있다.


class Post(models.Model)
	author = models.ForeignKey(Author)
    tage = models.ManyToManyField(Tag)
  • ForeignKey : select_related
""" 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
  • ManyToManyField : prefatch_related
""" 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)

🚀 (Django) QuerySet API reference


📌 참고 출처

0개의 댓글