Django에서 queryset 사용하기

Posted by on November 24, 2019

Recently by the same author:


Python에서 Singleton 구현

You may find interesting:


Celery를 사용한 Django에서 분산 비동기 처리


Django Serializer

Queryset의 특징


queryset

  • django에서 ORM을 사용하기 위한 DB의 레코드


1. Queryset은 마지막 까지 지연(lazy)된다

  • 필터링을 하는 경우 .filter() 를 사용
  • .filter() 를 사용한다 하더라도, django는 DB에 아무 쿼리도 전달하지 않는다.
    • 실제로 데이터에 접근하는 로직에만 작동
    • 이는 DB에 접근하는 횟수를 줄여 웹 앱이 느려지게 하는 것을 방지한다
  • 최종적으로 queryset을 순회(iterate)하는 경우에만 레코드를 가져온다


filter & iteration
post_set = Posts.obejcts.filter(title="branch")

for post in post_set:
    print(post.title)
  • 글 제목이 title인 모든 글을 나타낸다.
  • 최종적으로 iteration 하는 경우에만 query 실행


2. queryset은 캐시된다

evaluation

  • queryset을 순회하는 시점에 쿼리셋에 해당하는 DB의 레코드를 가져온다(fetch)
  • 또한, 이는 모두 django model로 변환되며 이를 평가(evaluation)이라 한다.


cashing

  • 평가된 모델은 queryset 내장 캐시에 캐시된다.
  • 또다시 queryset을 순회해도 같은 query를 DB에 전달하지 않는다.


두번의 순회를 하더라도 한번만 query를 전달

for post in post_set:
    print(post.title)
    
for post in post_set:
    print(post.title)


3. If 문에서는 queryset evaluation 발생

  • queryset에 레코드가 있는지 검사한 후 있을 때만 순회하도록 할 수 있다.


사용 예시
post_set = Posts.objects.filter(title="branch")

# if문에서 queryset evaluation 실행

if post_set:
  # 순회시에는 캐시된 queryset 사용
  
  	for post in post_set:
    		print(post.title)


4. 결과 전체가 필요하지 않은 경우 queryset 캐시는 비효율적

post_set = Posts.objects.filter(title="branch")

# queryset eavalution 발생

if post_set:
  	print("For only one query, whole size of queryset is fetched")
  • if문이 queryset evaluation을 발생시키므로 하나의 결과를 위해 전체 query를 가져오게 되어 비효율적이다


exist()

  • 최소한 하나의 레코드가 존재하는지 판단
post_set = Posts.objects.filter(title="branch")

if post_set.exists():
  	print("Exist method is useful for checking there is queryset has a record")
  • .exists() 메소드는 최소한 하나의 레코드가 존재하는지 판별한다.


5. queryset이 엄청 큰 경우 queryset 캐시는 문제가 된다

  • 수많은 레코드를 다루는 경우 한번에 메모리에 올리는 것은 비효율적
  • 거대한 쿼리가 서버의 프로세스에 lock을 걸어 app이 죽을 수도 있다.


iterator()

  • queryset 캐시를 방지하며 전체 레코드를 순회해야 할 때 사용
  • 데이터를 작은 덩어리로 쪼개서 가져오며, 사용한 레코드는 메모리에서 지운다.
post_set = Posts.objects.all()

for post in post_set.iterator():
  	print(post.title())
  • 전체 레코드의 일부만 가져오브로 메모리를 절약할 수 있다.
  • 그러나 같은 queryset을 순회하면 다시 evaluation이 발생하므로 조심해야 한다.


6. queryset이 엄청 큰 경우 if문도 문제가 된다

  • queryset 캐시와 if/for문을 사용하여 상황에 따라 queryset을 순회하는 것을 정할 수 있다.
  • 그러나, queryset이 큰 경우 queryset 캐시는 고려할 수 없으므로 다른 방법을 사용


exists()와 iterator()를 함께 사용하는 방법

post_set = Posts.objects.all()

# exists()메서드로 레코드가 존재하는지 검사

if post_set.exists():
  	# 다음 query로 레코드를 조금씩 가져온다
  
  	for post in post_set.exists():
      	print(post.title)


고급 순회 메서드

  • 순회 진행을 정하기 전에 iterator() 메서드의 첫번째 레코드를 감지(peek)
post_set = Posts.objects.all()

# 이 때부터 첫 번째 query로 레코드를 가져오기 시작한다

post_iterator = post_set.iterator()

# 첫 번째 레코드를 감지(peek)

try:
  	first_post = next(post_iterator)
except StopIteration:
  	# 레코드가 없으면 아무일도 하지 않는다.
		pass
else:
  	# 레코드가 하나라도 존재하면, 모든 레코드를 순회
    from itertools import chain
    for post in chain([first_post], post_set):
      	print(post.mass)