DB업로더용 파이썬 파일을 만든 후, 제품 리스트 페이지와 제품 상세 페이지의 API를 만들었다.
처음에는 아무 생각 없이 페이지 단위로 View를 만들었다. 그래서 Main페이지, 모든 제품을 보여주는 페이지, 카테고리별 제품을 보여주는 페이지, 제품 상세 페이지 이렇게 모든 페이지마다 View를 만들었다. 진짜 멍청......
from django.http import JsonResponse
from django.views import View
from django.db.models import Sum
from products.models import Product
class ProductMainView(View):
def get(self, request):
products = Product.objects.all().annotate(sum=Sum('orderitem__quantity')).order_by('-sum')[:5]
product_list = []
for product in products:
product_list.append({
'id': product.id,
'korean_name' : product.korean_name,
'price' : product.price,
'thumbnail_image_url' : product.thumbnail_image_url,
'vegan_or_not' : product.vegan_or_not,
'category' :{
'name' : product.category.menu.name,
'category' : product.category.name
},
'created_at' : product.created_at
})
return JsonResponse({'product_list' : product_list}, status=200)
class ProductAllView(View):
def get(self,request):
products = Product.objects.all()
try:
product_list = []
for product in products:
a = product.orderitem_set.all().values('product').annotate(product_quantity=Sum('quantity'))
for i in a:
product_list.append({
'id' : product.id,
'korean_name' : product.korean_name,
'price' : product.price,
'thumbnail_image_url' : product.thumbnail_image_url,
'vegan_or_not' : product.vegan_or_not,
'category' :{
'name' : product.category.menu.name,
'category' : product.category.name
},
'order_quantity': i['product_quantity'],
'created_at' : product.created_at
})
return JsonResponse({'product_list': product_list}, status = 201)
except Product.DoesNotExist:
return JsonResponse({'message':'NOT_FOUND'}, status=401)
class ProductMenuView(View):
def get(self,request):
menu = request.GET.get('menu_name', None)
products = Product.objects.filter(category__menu__name=menu)
try:
product_list = []
if menu:
for product in products:
a = product.orderitem_set.all().values('product').annotate(product_quantity=Sum('quantity'))
for i in a:
product_list.append({
'id' : product.id,
'korean_name' : product.korean_name,
'price' : product.price,
'thumbnail_image_url' : product.thumbnail_image_url,
'vegan_or_not' : product.vegan_or_not,
'category' : product.category.id,
'menu' : product.category.menu.name,
'order_quantity' : i['product_quantity'],
'created_at' : product.created_at
})
return JsonResponse({'product_list': product_list}, status = 201)
except Product.DoesNotExist:
return JsonResponse({'message':'NOT_FOUND'}, status=401)
class ProductListView(View):
def get(self,request):
category = request.GET.get('category_name', None)
products = Product.objects.filter(category__name=category)
try:
product_list = []
if category:
for product in products:
a = product.orderitem_set.all().values('product').annotate(product_quantity=Sum('quantity'))
for i in a:
product_list.append({
'id' : product.id,
'korean_name' : product.korean_name,
'price' : product.price,
'thumbnail_image_url' : product.thumbnail_image_url,
'vegan_or_not' : product.vegan_or_not,
'category' : product.category.id,
'order_quantity' : i['product_quantity'],
'created_at' : product.created_at
})
return JsonResponse({'product_list': product_list}, status = 201)
except Product.DoesNotExist:
return JsonResponse({'message':'NOT_FOUND'}, status=401)
class ProductDetailView(View):
def get(self,request, product_id):
try:
product = Product.objects.get(id=product_id)
images = product.image_set.filter(product__id=product_id)
data = {
'korean_name' : product.korean_name,
'price' : product.price,
'thumbnail_image_url' : product.thumbnail_image_url,
'vegan_or_not' : product.vegan_or_not,
'sugar_level' : product.sugar_level,
'category' : product.category.name,
'description' : product.description,
'image_list' : [{
'id' : image.id,
'url': image.url
} for image in images]
}
return JsonResponse({'product_list':data}, status = 201)
except Product.DoesNotExist:
return JsonResponse({'message':'NOT_FOUND'}, status=401)
1차 멍청프로덕트뷰로 무지성 PR을 올린 후 멘토님의 피드백을 받았다.
from django.http import JsonResponse
from django.views import View
from django.db.models import Sum
from products.models import Product
class ProductView(View):
def get(self, request):
try:
product_list = []
main = request.GET.get('main', None)
all = request.GET.get('all', None)
menu = request.GET.get('menu', None)
category = request.GET.get('category', None)
products=[]
if main:
products = Product.objects.annotate(sum=Sum('orderitem__quantity'))\
.order_by('-sum')[:5]
if all:
products = Product.objects.all()\
.annotate(sum=Sum('orderitem__quantity')).order_by('-sum')
if menu:
products = Product.objects.select_related('category__menu')\
.filter(category__menu__name=menu)\
.annotate(sum=Sum('orderitem__quantity')).order_by('-sum')
if category:
products = Product.objects.select_related('category')\
.filter(category__name=category)\
.annotate(sum=Sum('orderitem__quantity')).order_by('-sum')
for product in products:
product_list.append({
'id': product.id,
'korean_name' : product.korean_name,
'price' : product.price,
'thumbnail_image_url' : product.thumbnail_image_url,
'vegan_or_not' : product.vegan_or_not,
'category' :{
'name' : product.category.menu.name,
'category' : product.category.name
},
'created_at' : product.created_at
})
return JsonResponse({'product_list': product_list}, status = 200)
except Product.DoesNotExist:
return JsonResponse({'message':'NOT_FOUND'}, status=401)
except AttributeError:
return JsonResponse({'message' : 'AttributeError'}, status=400)
except TypeError:
return JsonResponse({'message' : 'TypeError'}, status=400)
class ProductDetailView(View):
def get(self,request, product_id):
try:
product = Product.objects.get(id=product_id)
images = product.image_set.filter(product__id=product_id)
data = {
'korean_name' : product.korean_name,
'price' : product.price,
'thumbnail_image_url' : product.thumbnail_image_url,
'vegan_or_not' : product.vegan_or_not,
'sugar_level' : product.sugar_level,
'category' : product.category.name,
'description' : product.description,
'image_list' : [{
'id' : image.id,
'url': image.url
} for image in images]
}
return JsonResponse({'product_list':data}, status = 201)
except Product.DoesNotExist:
return JsonResponse({'message':'NOT_FOUND'}, status=401)
product 리스트 View들을 하나로 통합하여 같은 내용의 for문들이 불필요하게 중복되는 문제를 수정하였다. 그럼에도 불구하고 여전히 같은 내용의 if 조건문들이 중복된다는 문제가 있었다.
이후 멘토님의 라이브 코드 리뷰가 있었고, 코드가 대폭 수정되었다.
from django.http import JsonResponse
from django.views import View
from django.db.models import Sum, Q
from core.utils import AuthorizeProduct, authorization
from products.models import Product, Like
class ProductView(View):
@AuthorizeProduct
def get(self, request):
try:
menu = request.GET.get('menu', None)
category = request.GET.get('category', None)
limit = int(request.GET.get('limit', 100))
offset = int(request.GET.get('offset', 0))
q = Q()
category_mapping = None
mapping = {
"menu" : "category__menu",
"category" : "category"
}
if menu:
q &= Q(category__menu__name=menu)
category_mapping = mapping["menu"]
if category:
q &= Q(category__name=category)
category_mapping = mapping["category"]
products = Product.objects.select_related(category_mapping)\
.annotate(sum=Sum('orderitem__quantity')).filter(q).order_by('-sum')[offset:limit+offset]
product_list = [
{'id' : product.id,
'korean_name' : product.korean_name,
'price' : product.price,
'thumbnail_image_url' : product.thumbnail_image_url,
'vegan_or_not' : product.vegan_or_not,
'category' :{
'name' : product.category.menu.name,
'category' : product.category.name
},
'created_at' : product.created_at,
'like_num' : product.like_set.count(),
'is_like_True' : True if product.like_set.filter(user_id=request.user).exists() else False,
'order_quantity' : product.orderitem_set.all()[0].quantity if product.orderitem_set.all().exists() else 0,
} for product in products]
return JsonResponse({'product_list': product_list}, status = 200)
except Product.DoesNotExist:
return JsonResponse({'message':'NOT_FOUND'}, status=404)
except AttributeError:
return JsonResponse({'message' : 'AttributeError'}, status=400)
except TypeError:
return JsonResponse({'message' : 'TypeError'}, status=400)
class ProductDetailView(View):
@AuthorizeProduct
def get(self, request, product_id):
try:
product = Product.objects.get(id=product_id)
images = product.image_set.all()
data = {
'korean_name' : product.korean_name,
'price' : product.price,
'thumbnail_image_url' : product.thumbnail_image_url,
'vegan_or_not' : product.vegan_or_not,
'sugar_level' : product.sugar_level,
'category' : product.category.name,
'description' : product.description,
'like_num' : Product.objects.filter(like__product_id=product_id).count(),
'is_like_True' : True if product.like_set.filter(user_id=request.user).exists() else False,
'image_list' : [{
'id' : image.id,
'url': image.url
} for image in images]
}
return JsonResponse({'product_list':data}, status = 201)
except Product.DoesNotExist:
return JsonResponse({'message':'NOT_FOUND'}, status=401)
1. query parameter를 적용한 endpoint.
menu = request.GET.get('menu', None)
request.GET
은 GET으로 받는 인자들을 모두 포함하는 딕셔너리 객체이다.
get
메서드는 키 값이 딕셔너리 안에 있으면 밸류 값을 리턴해준다.
키 값이 존재하지 않으면 디폴트값 None을 리턴한다.
request.GET.get()
은 이 두 개념을 합친 것으로 GET요청이 접근할 수 있는 키와 밸류값을 이용한다.
2. annotate로 데이터 가공하여 임시 컬럼 만들기
Product.objects.select_related(category_mapping)\
.annotate(sum=Sum('orderitem__quantity')).filter(q).order_by('-sum')[offset:limit+offset]
order_items 테이블에 있는 데이터들 중 각각의 product들의 주문량들을 선별하여 합산한 값들이 필요했다. 장고ORM인 annotate와 Sum을 사용하여 DB에서 연산을 한 후에 order_by()로 내림 차순 정렬하여 값들을 가져왔다.
3. select_related()
select_related를 사용하지 않을 경우에는 참조관계에 있는 테이블의 값을 가져올 때마다 DB로 쿼리를 보내야 한다. select_related를 사용할 경우에는, 한번에 그 값들을 가져와서 캐싱한 후 캐시에 있는 데이터들을 사용하기 때문에 쿼리 최적화 면에서 좋다. (쿼리를 적게 날릴 수록 데이터베이스에 부담이 덜 가서 좋다.)
prefetch_related()
와의 차이점은 아직 더 공부가 필요.
4. Q객체
장고ORM에서 Q객체를 이용하여 쿼리문처럼 조건을 적용할 수 있다.
&를 사용하면 where 조건 and 조건이고, |를 사용하면 where 조건 or 조건이다.
5. offset과 limit (Pagination)
query parameter를 받아서 페이지 기능을 구현하였다. offset(시작 값)과 limit(끝 값)을 넣어주었고,offset의 디폴트는 0 으로, limit의 디폴트는 100으로 지정해주었다.
6. 두개의 인가 데코레이터
우리는 제품페이지에서 좋아요를 누르면 누가 좋아요를 눌렀는지와 (마이페이지에서 내가 좋아요 누른 제품들 조회 가능하게 하기 위해), 어떤 제품에 몇개의 좋아요가 달렸는지를 알 수 있게 만들어야했다.
그러려면, 해당 API에 '인가'데코레이터를 달아야했는데, 그러면 토큰이 없는 유저(로그인을 하지 않은 유저)는 제품정보를 볼 수 없게 된다.
그래서 토큰이 있는 유저, 없는 유저 모두 제품 정보를 볼 수 있게 하되, 유저 정보에 좋아요를 누른 제품이 포함되도록 하기 위해 새로운 인가 데코레이터를 만들었다.
해당 데코레이터는 아래와 같다. 시간이 촉박에 에러 예외처리는 아직 하지 못했으나 리팩토링을 하며 진행할 예정.
def AuthorizeProduct(func):
def wrapper(self, request, *args, **kwargs):
token = request.headers.get('Authorization')
if not token:
request.user = None
return func(self, request, *args, **kwargs)
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user = User.objects.get(id=payload['user'])
request.user = user
return func(self, request, *args, **kwargs)
return wrapper
제품 리스트 페이지에서, 좋아요를 누르면 숫자가 +1되지만, 페이지 새로고침을 하면 다시 좋아요 수가 원래대로 돌아가는 오류가 생겼었다.(DB에 저장되지 않는다는 뜻)
<before 코드>
class LikeView(View):
@authorization
def post(self, request):
try:
data = json.loads(request.body)
user = request.user
product_id = data['product_id']
like, is_created = Like.objects.get_or_create(user=user, product_id=product_id)
data_status = 201 if is_created else 200
if is_created:
return JsonResponse({"message" : "SUCCESS"}, status=data_status)
like.delete()
return JsonResponse({"message" : "SUCCESS"}, status=data_status)
except KeyError:
return JsonResponse({"message" : "KEY_ERROR"}, status=400)
class LikeView(View):
@authorization
def post(self, request, product_id):
try:
like, is_created = Like.objects.get_or_create(user=request.user, product_id=product_id)
data_status = 201 if is_created else 200
if is_created:
return JsonResponse({"message" : "CREATED"}, status=data_status)
like.delete()
return JsonResponse({"message" : "DELETED"}, status=data_status)
except KeyError:
return JsonResponse({"message" : "KEY_ERROR"}, status=400)
urlpatterns = [
path('', ProductView.as_view()),
path('/<int:product_id>', ProductDetailView.as_view()),
path('/<int:product_id>/like', LikeView.as_view()),
]