[Django] QuerySet

jaylight·2020년 12월 13일
0

Intro

  • 파이썬 기초 공부를 위한 내용은 온라인에 많은 반면, 조금이라도 심화된 깊은 내용을 찾으려면 자료가 급격하게 줄어듬. 특히 프레임워크 공부를 하기 위한 자료는 별로 없다.
  • 하이버네이트를 통해 ORM을 접했는데, 설명이 잘되고 원론적인 내용을 쉽게 접할 수 있었다. 배우는 것은 어려워도 자료를 얻는 것은 어렵지 않았다.
  • Django는 웹프로그래밍, MTV 패턴 이런 키워드만 벗어나도 자료가 급격하게 줄어들었다.
  • 공식문서도 있고 좋은 내용이지만, ORM의 큰 그림을 그리는데는 아쉬웠다.
  • 따라서 Django 소스코드를 뜯어보고 공부하고 내용 공유를 위해 이번 자료를 준비

QuerySet을 통해 알아보는 ORM 특징

  • Lazy Loading
    - ORM은 정말 필요한 시점에만 SQL을 호출한다. (즉, 실제로 쓰일 때!)
    - 선언된 시점에는 단지 QuerySet에 지나지 않는다.
users: QuerySet = User.objects.all()
  • 위 코드를 통해 User의 Model이 넘어온다고 착각하는 경우가 많은데, 선언한 시점에는 여전히 QuerySet으로 남아있다.

  • list()로 쿼리셋을 묶는 로직이 수행될 때, 쿼리셋이 동작하여 SQL이 호출되고 데이터가 불러와진다.

  • 즉, 꼭 필요하지 않다면 QuerySet은 SQL을 호출하지 않는다.
    - 위 예제에서는 세 개의 QuerySet이 선언됨

users: QuerySet = User.objects.all()
orders: QuerySet = Order.objects.all()
companies: QuerySet = Company.objects.all()
  • 하지만 실제 호출되는 QuerySet은 Users QuerySet 뿐이므로, 나머지 두 개의 쿼리셋(orders, companies)은 사용되지 않는다.

  • 위 특성으로 인해 비효율적으로 ORM이 동작하기도 함
    - 위 예제에서는 쿼리셋을 통해 1. 첫 번째 유저 호출 2. 모든 유저를 호출할 때,
    두 번째에서 기존 쿼리셋을 통해 호출한 결과는 무시하고 다시 또 SQL문을 호출함
    - 일반적으로 위와 같은 경우, SQL을 한번만 호출해서 재사용하는 것이 가장 효율적
    쿼리셋은 위를 인지하지 못하고, 지금 당장 필요한 만큼만 SQL을 호출하고자 함

  • 위와 같은 문제 해결을 위해 SQL 수행 결과를 캐싱하는 점을 알고 작업해야 한다.
    - 쿼리셋 → SQL 호출 시, 그 호출 결과를 저장해둔다.
    따라서 쿼리셋 캐싱을 재사용하면 효율성을 증대시킬 수 있다.
    - 위의 2-3의 문제는 전체 쿼리셋을 먼저 호출하고 일부 쿼리셋을 호출하는 순서로 바꿈으로써 쿼리셋을 두 번 호출하는 비효율성 문제를 해결할 수 있다.
    쿼리셋은 호출하는 순서가 바뀌는 것 만으로도, 쿼리셋 캐싱을 활용함으로써 발생하는 SQL 호출이 달라질 수 있다.

  • Eager Loading

    • SQL로 한번에 많은 데이터를 끌어오고 싶을 때, 이를 ORM에서는 Eager Loading이라고 부른다.

    • QuerySet은 이를 지원하기 위해, select_related(), prefetch_related() 메서드를 제공

    • 위 예제에서는 ORM에서 Eager Loading과 Lazy Loading을 이야기 할 때, 가장 흔히 이야기하는 예제

      • users 쿼리셋을 선언 후 이를 활용해 for문을 통해 userinfo를 조회할 때,
        for문을 돌 때마다 매번 SQL이 호출되는 문제가 발생한다.

        `for`문을 선언할 때, users QuerySet을 불러오지만, `userinfo` 변수가 필요한 것은 아니기 때문에 가져오지 않고, 이후 `for`문 내에서 `userinfo`가 필요할 때마다 매번 SQL을 호출
      • 위의 경우 user 데이터가 100건이기 때문에 user를 호출하는 SQL이 100+1번의 SQL이 발생

    • (N+1 Problem) 위 예제는 N+1 이라는 ORM에서 대표적인 문제

      • userinfo라는 정보가 이 시점에서 당장 필요하지 않아 호출이 지연되었고,
        user의 수 만큼 QuerySet이 호출되는 비효율이 발생
      • 위를 해결하기 위해서는 Eager Loading 전략을 택하도록 옵션을 주어야 함
        ( → 3장에서 다룸)

QuerySet 상세

  • 왼쪽은 실제 Django의 쿼리셋 구현체: 적혀진 내용르 굳이 다 알 필요는 없음
  • QuerySet은 1개의 쿼리와 0~N개의 추가쿼리(셋)으로 구성되어 있다.
class QuerySet:
	query: Query = Query()
	_result_cache: List[Dict[Any, Any]] = dict()
	_prefetch_related_lookups: Tuple[str] = ()
	_ iterable_class = ModelIterable
  • _result_cache: 쿼리셋 호출 결과를 여기에 저장해두고 재사용
    (QuerySet을 재호출 시, 이 프로퍼티에 저장된 데이터가 없으면 SQL을 재호출해서 데이터를 가져옴)
  • _prefetch_related_lookups: 추가 쿼리셋이 될 타겟들을 저장해둠
  • _iterable_class: 이 쿼리셋의 반환 타입을 결정하는 프로퍼티
    추가 옵션을 주지 않는다면, 장고의 모델을 반환
  • select_related()
    • JOIN을 통해 데이터를 즉시 로딩하는 방식
  • prefetch_related()
    • 추가 쿼리를 수행하여 데이터를 즉시 가져오는 방식
  • (모델 구조) 주문: 회원 → N:1 // 주문: 상품 → N:N
  • (쿼리셋 구조) order 정보를 아래와 같은 쿼리셋 구조로 가져옴
order_list = (
	Order.objects
	.select_related('order_owner')
	.filter(order_owner__username='username4')
	.prefetch_related('product_set_included_order')
)
  • select_related: user의 정보를 JOIN
    - 역참조 모델은 select_related()에 줄 수 없다.
  • prefetch_related: 상품의 정보를 모두 끌어옴
  • prefetch_related()는 추가 쿼리셋으로 새로운 쿼리셋으로써 실행된다.
    - 새로운 쿼리셋으로 수행함
queryset = (
	AModel.objects.
	.prefetch_related(
	"b_model_set",
	"c_models",
	)
)
  • 역참조하고 싶은 필드를 각각 지정하면 매번 쿼리셋으로써 각각 실행된다.
    (메인모델에 역참조된 모델을 전부 조회하고 싶다면, 단순히 역참조 필드만 선언해주면 됨)
queryset = (
	AModel.objects.
	.prefetch_related(
		prefetch(to_attr="b_model_set", queryset=BModel.objects.all()),
		prefetch(to_attr="c_models") queryset=BModel.objects.all()),
	)
)

queryset=BModel.objects.filter(is_deleted=False)
  • 위와 같은 쿼리셋을 수행하면 각각의 쿼리셋에 대해 조건절들을 추가하여 수행한다.

0개의 댓글