IT/WEB

[Django] 테이블(모델)의 JOIN(3) ; prefetch_related()

개발자 두더지 2020. 11. 18. 00:00
728x90

간단 사용법


 QueryAPI에서도 QuerySet을 돌려주는 메소드이다. select_related()와 마찬가지로 한 번의 쿼리로 관계 테이블의 객체를 얻을 수 있다 (즉 JOIN가능하다). 차이점이라면 prefetch_related()는 M2M 관계일 때 사용한다는 것이다. 이와 관련해서 조금 더 설명을 덧붙이자면 다음과 같다.

 

● select_relation()의 경우

 select_relation()은 관계 테이블의 객체도 포함해서 SQL로 SELECT를 실행해 객체를 얻고 있다. 따라서 관계 테이블의 객체도 포함해서 한 번의 쿼리로 모든 대상 객체를 얻는다. 따라서 M2M의 관계의 객체를 대상으로 실행했을 경우 엄청난 수의 객체를 얻게 된다. 따라서 select_related는 ForeignKey나 one-to-one의 관계에만 사용하도록 장려하고 잇다.

 

● prefetch_related()의 경우

 한편, prefetch_related()을 살펴보면 관계 테이블의 객체를 취득하기 위해 별도의 쿼리는 사용한다. 그리고 취득한 객체와 관계된 테이블을 SQL이 아닌 Python을 사용하여 JOIN을 한다.

 그리고 이 메소드는 GenericRelationGenericForeignKey도 서포트하고 있다. 그러나 select_related()가 지원하고 있는 ForeignKey나 one-to-one은 사용할 수 없다.

 

 

조금 더 자세히 살펴보기


● 캐시가 클리어된 경우

Django의 문서로 부터 코드를 가져왔다. 이 코드는 문제가 있는 코드이다.

>>> pizzas = Pizza.objects.prefetch_related('toppings')
>>> [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]

 잠깐 보면 제대로 prefetch_related()를 사용하여 낭비되는 쿼리없는 것 처럼 보이지만, 실제로 2행의 for문에서 DB에 몇 번이고 접근하고 있다. 그 이유는 QuerySet의 특징에 의해 새로운 체인 메소드를 기존과 다른 쿼리가 실행되기 때문이다.  여기서 filter()를 사용하고 있으므로 새롭게 prefetch되지 않은 pizza 객체가 for문으로 처리 되어 버리고 있다.

 

● 의도적으로 캐시를 클리어하고 싶은 경우

select_related()와 동일하게 키워드 인수에 None을 전달한다. 

>>> non_prefetched = qs.prefetch_related(None)

 

● Prefetch() 객체를 사용하는 경우

Prefetch()의 기본 파라미터는 다음과 같다.

Prefetch(lookup, queryset=None, to_attr=None)

1) 사용법

Prefetch() 객체는 prefetch_related()를 컨트롤할 때 사용한다. lookup이라는 키워드 인수는 prefetch_related()의 lookup과 같다. 이번에도 공식 문서의 코드를 가져왔다.

>>> from django.db.models import Prefetch

>>> Question.objects.prefetch_related(Prefetch('choice_set')).get().choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

2) queryset 설정할 경우

계속해서 query인수를 설정해보자. queryset을 설정하는 것으로 관계 테이블로써 참고하고 있는 객체를 엮는 것이 가능하다. 

>>> voted_choices = Choice.objects.filter(votes__gt=0)
>>> voted_choices
<QuerySet [<Choice: The sky>]>
>>> prefetch = Prefetch('choice_set', queryset=voted_choices)
>>> Question.objects.prefetch_related(prefetch).get().choice_set.all()
<QuerySet [<Choice: The sky>]>

3) to_attr을 설정할 경우

to_attr인수를 이용해 prefetch에 독자적인 속성을 부여할 수 있다.

>>> prefetch = Prefetch('choice_set', queryset=voted_choices, to_attr='voted_choices')
>>> Question.objects.prefetch_related(prefetch).get().voted_choices
<QuerySet [<Choice: The sky>]>
>>> Question.objects.prefetch_related(prefetch).get().choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

voted_choices를 설정하는 것으로 객체에 대해서 어떠한 관계의 관계 테이블인지를 보다 명확히 할 수 있다. 그리고 to_attr를 이용해 얻은 prefetched의 결과는 리스트에 보존된다. 이것은 기존의 prefetch_relation이 QuerySet인스턴스에 결과를 캐시했을 때 보다 더 빠른 속도로 처리할 수 있으므로 to_attr의 사용을 추천한다.  


참고자료

mkai.hateblo.jp/entry/2018/11/05/234611

728x90