오늘도 개발
[1차 프로젝트 록차] 상품 목록 GET api 제작기 3 - Django Q 오브젝트 사용하기 본문
앞서 정리했던 방식대로 코드를 짜기 위해 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
이제 페이지네이션을 구현해야 한다.
다음화에서 계속...
참고
'TIL & 프로젝트 회고' 카테고리의 다른 글
| [1차 프로젝트 록차] Postman으로 batch request 보내기 (0) | 2022.07.23 |
|---|---|
| [1차 프로젝트 록차] 상품 목록 GET api 제작기 4 - django paginator로 페이지네이션 구현하기 (0) | 2022.07.23 |
| [1차 프로젝트 록차] 상품 목록 GET api 제작기 2 - 어떻게 필터링을 구현할까 (0) | 2022.07.21 |
| [1차 프로젝트 록차] 상품 목록 GET api 제작기 1 - RESTful한 엔드포인트 만들기 (0) | 2022.07.21 |
| [Westagram] 2. 회원가입, 로그인, 팔로잉 구현으로 배운점 (0) | 2022.07.17 |