오늘도 개발

[1차 프로젝트 록차] 상품 목록 GET api 제작기 3 - Django Q 오브젝트 사용하기 본문

TIL & 프로젝트 회고

[1차 프로젝트 록차] 상품 목록 GET api 제작기 3 - Django Q 오브젝트 사용하기

Sueeeeeee 2022. 7. 22. 01:04

앞서 정리했던 방식대로 코드를 짜기 위해 Q 오브젝트에 대해 알아보았다.

Q 오브젝트란?

쿼리를 담을 수 있는 오브젝트.

예를 들어 이런 쿼리는 

WHERE title LIKE '제주%'

이렇게 담을 수 있다.

Q(title__startswith='제주')

 

Q 오브젝트는 filter(), exclude(), get()등의 인수로 넣어 복잡한 쿼리를 쉽게 작성할 수 있다.

예를 들어 2차 카테고리 아이디가 1이고 가격이 20000원 이하인 제품을 필터링 하려면 이렇게 해야 한다.

Product.object.filter(second_category_id=1) & Product.objects.filter(price__lte=20000)

하지만 Q 오브젝트를 사용하면 다음처럼 간결하게 쓸 수 있다.

Product.objects.filter(Q(second_category='1') & Q(price__lte=20000))

 

Q 오브젝트 연결하기

Q 오브젝트는 아래의 연산자로 연결할 수 있다.

 

~ : NOT

& : AND

| : OR

 

연결된 Q 오브젝트는 하나의 새로운 Q 오브젝트가 된다.

아래 코드는 하나의 Q오브젝트이다.

Q(title__startswith='제주') | Q(second_category__id=1)

~와 |, &을 섞어서 사용할 수도 있다.

Q(price__lte=10000) & ~Q(stock=0)

 

lookup 함수에 넣을 때 연산자 없이 두 개 이상의 Q 오브젝트를 넣으면 자동으로 &으로 처리된다.

Product.objects.filter(Q(price__lte=10000), Q(stock=10))

Q 오브젝트를 일반 쿼리와 함께 사용해도 자동으로 & 처리된다.

단, 이 때는 Q 오브젝트를 먼저 써주어야 오류가 나지 않는다.

Product.objects.filter(Q(price__lte=10000), stock=10)

 

동적으로 Q 오브젝트 생성하기

빈 Q 오브젝트를 생성한 후

&=나 |=로 쿼리를 더할 수 있다.

 

동적으로 생성한 Q 오브젝트는 filter(), get() 등에 인수로 넣을 수 있다.

(order_by()에는 Q 오브젝트를 넣을 수 없다. order_by()는 인수로 문자열을 받기 때문이다.)

# 빈 Q 오브젝트 생성
q = Q()
print(q)
# <Q: (AND: )>

# Q 오브젝트에 쿼리 추가
# and으로 추가
q &= Q(price__lte=10000)

# or로 추가
q |= Q(title__startswith='제주')

print(q)
# <Q: (OR: ('price__lte', 10000), ('title__startswith', '제주'))>

Product.objects.filter(q)
# Product.objects.filter(Q(price__lte=10000)|Q(title__startswith='제주'))와 같음

 

프로젝트에 적용하기

Q 오브젝트에 대해 공부하고 나니 if문을 사용해서 경우의 수마다 쿼리를 만들 수 있을 것 같았다.

그래서 앞서 글로 정리한 내용을 코드로 바꾸었더니 정말 작동했다!

 

한 api로 어떻게 복잡한 필터링을 수행할 지 처음에는 막막했지만,

이 함수로 무슨 일을 해야 하는지 시간을 들여 글로 정리해보았더니 코드 구현 자체는 어렵지 않았다.

앞으로 복잡한 기능을 수행하는 api를 만들 때는 글로 한 번 정리해보고 시작하는 습관을 들여야겠다.

 

리팩토링을 여러 번 거쳐야겠지만 일단 나온 코드는 이렇다. 

class ProductListView(View):
    def get(self, request):
        first_category_id  = request.GET.get('first-category', None)
        second_category_id = request.GET.get('second-category', None)
        sort               = request.GET.get('sort', None)
        types              = request.GET.get('type', None)

        # filter()에 넣을 인수
        filter_queries = Q()
        # order_by()에 넣을 인수
        order_string = ''

        # 쿼리 파라미터가 없는 경우 - 1차 카테고리 아이디 1번에 속한 모든 제품을 신상품 순으로 보여줌
        if not request.GET:
            filter_queries &= Q(second_category__first_category_id=1)

        # 1차 카테고리를 선택한 경우
        if first_category_id:
            filter_queries &= Q(second_category__first_category_id = first_category_id)
        
        # 2차 카테고리를 선택한 경우
        if second_category_id:
            filter_queries &= Q(second_category = second_category_id)

        # sort를 선택한 경우(new-arrival, price-desc, price-asc)
        if not sort or sort == 'new-arrival':
            order_string = '-created_at'
        elif sort == 'price-desc':
            order_string = 'price'
        else:
            order_string = '-price'
        

        result = []
        products = Product.objects.filter(filter_queries).order_by(order_string)
    
        for product in products:
            result.append({
                'id'              : product.id,
                'title'           : product.title,
                'price'           : product.price,
                'stock'           : product.stock,
                'thumbnail_images': [image.url for image in product.thumbnail_images.all()],
                'types'           : [type.name for type in product.types.all()]
            })

        return JsonResponse({'result': result}, status=200)

 

* 타입별 필터링

타입별 필터링은 조금 더 어려웠다.

한 프로덕트는 tealeaf, teabag, pyramid, powder 타입을 0개 이상 가질 수 있는데,

타입별로 프로덕트를 필터링해야 했다.

 

처음에는 2차 카테고리가 n번인 상품 중 타입에 tealeaf나 powder가 들어가는 모든 상품을 필터링하려고 했는데

모든 쿼리를 &으로 엮었더니 제대로 작동하지 않았다. 

 

다시 잘 생각해보니 나는 2차 카테고리가 1번 AND (타입이 tealeaf OR 타입이 powder)인 쿼리를 실행해야 했다.

그래서 or 로 연결되는 두 Q 오브젝트는 괄호로 묶어서 처리했더니 결과가 잘 나오는 듯 했다.

Product.objects.filter(Q(second_category=1)&(Q(types__name='tealeaf')|Q(types__name='powder')))
# 4, 5, 4(1번 카테고리 상품 중 타입이 tealeaf이거나 powder인 것)

하지만 types는 ManyToManyField라서 중복되는 Product가 걸러지지 않고 나왔다.

그래서 검색 후 distinct() 메서드를 써서 중복되는 row는 걸러내기로 했다.

Product.objects.filter(Q(second_category=1)&(Q(types__name='tealeaf')|Q(types__name='powder'))).distinct()
# 4, 5

 

이제 페이지네이션을 구현해야 한다.

다음화에서 계속...

 

 

참고

Whare are the benefits of using Q objects?