Django ⎮ query parameter, filter, sort, search

Chris-Yang·2021년 10월 17일
2

Django

목록 보기
2/7
post-thumbnail
post-custom-banner

> 전체 코드와 흐름

  1. FE로부터 query string으로 요청사항을 받습니다.
  2. 쿼리를 분석하고 해당하는 if문에 있는 Q객체를 실행시킵니다.
  3. 해당하는 조건을 모은 Q객체는 filter 메소드에 전달됩니다.
  4. filter 메소드는 조건에 해당하는 데이터를 DB에서 꺼내옵니다.
  5. 꺼낸 데이터를 FE와 합의된 데이터 형태로 만들어 보내줍니다.

from django.db.models.query_utils import Q
from django.http                  import JsonResponse
from django.views                 import View

from product.models import MainCategory, Product

class ProductFilter(View):
    def get(self, request):
        sub_category_products = request.GET.get('products_list')
        sort                  = request.GET.get('sort')
        search                = request.GET.get('search')

        sort_set = {
            'best'             : 'id',
            'price_ascending'  : 'price',
            'price_descending' : '-price',
            'name_ascending'   : 'korea_name',
            'name_descending'  : '-korea_name',
            'created_ascending': 'created_at',
            'created_decending': '-created_at',
        }

        product_list = Q()

        if sub_category_products:
            product_list.add(Q(sub_category=sub_category_products), Q.AND)

        if search:
            product_list.add(Q(korea_name__icontains=search)\
                |Q(sub_category__name__icontains=search), Q.AND)

        products = Product.objects.filter(product_list)\
            .order_by(sort_set.get(sort, 'id'))

        results = [{
            'product_id'  : product.id,
            'korea_name'  : product.korea_name,
            'foreign_name': product.foreign_name,
            'price'       : product.price,
            'information' : product.information,
            'is_deleted'  : product.is_deleted,
            'sizes' : [
                {
                    "id"    : size.id,
                    "width" : size.width,
                    "length": size.length
                } for size in product.product_sizes.all()
            ],
            'images': [
                {
                    "product_image": product_images.product_image,
                } for product_images in product.product_images.all()
            ] 
        } for product in products]
            
        return JsonResponse({'results': results }, status = 200)




> Code Divide and Conquer

▶︎ query parameter

FE에서 요청한 데이터를 보내주기 위해서는
먼저 프론트에서 어떤 요구사항을 보냈는지 확인해야 합니다.

프론트에서는 query string으로 아래와 같은 URI 형태로 요청을 보냅니다.

/products?products_list=1

이 요청은 다음과 같은 형태로 받을 수 있습니다.

sub_category_products = request.GET.get('products_list')

GET[]만을 이용해 request에서 QueryDict를 가져올 수 있지만
get()을 추가로 사용함에 따라 매칭되는 key가 없어도 에러가 나지 않게 됩니다.

GET[]은 request를 통해 전달되는 값을 dict 형태로 바꿔주고
그 안에서 [] 안에 있는 내용과 dict 안에 key들을 매칭하기 때문에
해당하는 key가 없으면 에러가 나므로 get()을 써줘야 합니다.

django.utils.datastructures.MultiValueDictKeyError: 'products_list'

get() 메소드는 해당하는 값이 없을 때 기본적으로 'None'을 반환하고
매칭값이 없어도 에러를 반환하지 않고 끝납니다.

# 이런 형태를
<WSGIRequest: GET '/products?products_list=1&sort=price_ascending'>

# 이렇런 식으로 바꿔줌
{
products_list: 1,
sort: "price_ascending"
}

# 정확히는 print()를 통해 이런 형태로 출력됨
<QueryDict: {
'products_list': ['1'], 
'sort': ['price_ascending']
}>


▶︎ Q객체

장고의 ORM인 Q객체는 SQL문의 WHERE, LIKE, OR, AND와 대응됩니다.

product_list = Q()

if sub_category_products:
    product_list.add(Q(sub_category=sub_category_products), Q.AND)

if search:
    product_list.add(Q(korea_name__icontains=search)\
        |Q(sub_category__name__icontains=search), Q.AND)

전달받은 query string과 일치하는 데이터들을 모아놓기 위해 빈 Q객체가
할당된 변수를 따로 만들어 놓습니다.

해당하는 query stirng을 받고싶은 경우 if문 등을 통해
Q(해당 테이블column(혹은 지정한 FK__column)=query담은 변수명)
위에서 지정한 빈 Q객체를 할당한 변수에 add시켜주도록 세팅합니다.

그리고 query string으로 다음과 같은 값을 받으면

/products?products_lista=1&search=침대

Q객체에는 아래와 같은 형태로 값을 받아놓습니다.

(AND: (OR: ('korea_name__icontains', '침대'), ('sub_category__name__icontains', '침대')))

이들은 곧 filter()에 전달될 것입니다.



▶︎ filter

Q객체를 모아놓은 변수를 filter의 매개변수로 넣어줍니다.

products = Product.objects.filter(product_list)\
    .order_by(sort_set.get(sort, 'id'))

그럼 filter는 데이터베이스에 접근하여 Product테이블에서(그리고
지정한 연결된 테이블에서도) 일치하는 데이터를 수집해 옵니다.

# print(products)
<QuerySet [<Product: 노르들리>, <Product: 말름>... <Product: 횟스트베드>]>

# print(products.values().first())
{
'id': 1, 
'created_at': datetime.datetime(2021, 10, 9, 17, 21, 39),
'updated_at': datetime.datetime(2021, 10, 9, 17, 21, 39), 
'foreign_name': 'NORDLI', 
'korea_name': '노르들리', 
'price': Decimal('299000.00'), 
'information': '수납침대프레임+수납상자2', 
'description': '이 제품은 어쩌구~', 
'is_deleted': False, 
'sub_category_id': 1
}

이제 products 변수에는 수집한 데이터들이 담겨져있습니다.



▶︎ search와 sort

search:
search 기능은 sort = request.GET.get('sort')로 query를 받아오고
if문으로 Q(korea_name__icontains=search)을 빈 Q객체에 넣으면 됩니다.

korea_name__icontainskorea_name이라는 column에
__icontains라는 장고 ORM을 적용한 것으로 SQL문에서 LIKE와 같은 의미입니다.


sort:
sort는 .order_by()라는 django의 QuerySet API로 컨트롤 합니다.

sort = request.GET.get('sort')
...

sort_set = {
    'best'             : 'id',
    'price_ascending'  : 'price',
    'price_descending' : '-price',
    'name_ascending'   : 'korea_name',
    'name_descending'  : '-korea_name',
    'created_ascending': 'created_at',
    'created_decending': '-created_at',
}
...

...filter(product_list).order_by(sort_set.get(sort, 'id'))

sorting할 종류를 dict 자료형으로 만들어놓고 그 안에서 FE로부터 받은
query stirng과 일치하는 대상을 찾아오면 .order_by()는 그에 따라
오름차순과 내림차순으로 데이터를 정리해줍니다.

sort_set.get()을 쓴 이유는 get()의 두번째 인자는 첫번째 인자가
존재하지 않을 경우 반환하는 값으로 그 자리에 'id'를 넣음으로써
입력값이 없을 때 'id'를 기준으로한 오름차순, 즉 아무런 sorting값이 전달되지
않을 때를 대비하기 위함입니다.



▶︎ 준비한 데이터 요리해서 FE로 보내기

이제 FE에서 원하는 데이터는 products라는 모두 변수에 모여있습니다.

데이터를 그대로 보내면 안되고 FE와 합의된 형태로 데이터를 보내야 합니다.

results = [{
    'product_id'  : product.id,
    'korea_name'  : product.korea_name,
    'foreign_name': product.foreign_name,
    'price'       : product.price,
    'information' : product.information,
    'is_deleted'  : product.is_deleted,
    'sizes' : [
        {
            "id"    : size.id,
            "width" : size.width,
            "length": size.length
        } for size in product.product_sizes.all()
    ],
    'images': [
        {
            "product_image": product_images.product_image,
        } for product_images in product.product_images.all()
    ] 
} for product in products]

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

python의 list comprehension 문법을 하면 간단히 for문을 적용할 수 있으며
일반적인 for문에 비해 가독성이 좋아보입니다.

일반적인 for문을 쓰면 대략 다음과 같습니다.

results = []

for product in products:
    sizes  = []
    images = []
    for size in product.product_sizes.all():
        sizes.append({
            'id'   : size.id,
            'width': size.width,
            })
    for image in product.product_images.all():
        images.append({
            'image': image.product_image,
        })
    results.append({
    'product_id'  : product.id,
    'sizes'       : sizes,
    'images'      : images,
    })

데이터를 많이 줄였는데도 코드가 길고 가독성이 좋지 못합니다.


for size in product.product_sizes.all():에서
product.product_sizes는 product테이블이 ProductSize테이블을
역참조하기 위해 model에서 FK를 지정할 때 지정한 related_name을 쓴 것입니다.

related_name을 지정하지 않았다면 productsize_set문법으로 접근하면 됩니다.

이제 FE에서 원하는대로 요리된 데이터를 보내줄 수 있습니다!^^


함께 Django의 세계를 여행하시는 모든 분들 파이팅!!

profile
sharing all the world
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 3월 6일

감사합니다! 덕분에 프로젝트에 큰 도움이 되었습니다!

답글 달기