문제점

아래의 함수는 emails가 많을수록 문제가 아주 많아진다. 왜냐? 서버 프로세스를 점유(Block)하기 때문이다.

만약 Gunicorn Worker가 3개 있다고 가정할때, 3명의 유저가 동시에 send_emails를 수행하면 모든 프로세스는 Block되기 때문에 더이상 서비스가 불가능해진다. 아주 문제가 심각하다.

이 문제를 async Celery task로 변경해 웹프로세스 Block을 막아보자

def send_emails(request):
    emails: list[str] = request.data.get('emails')
    for email in emails:
        requests.post(URL, data=email)        
    return Response(status=200)

 

WorkFlow

1. send_emails에서 async celery task 호출

2. 메시지 브로커가 방금 호출한 task를 받아서 celery worker로 넘긴다.

3. celery worker가 일을 처리하고 result backend에 결과를 넣어준다.

출처 : https://testdriven.io/

메시지브로커, result backend로 Redis, rabbitmq 등을 많이 사용한다.

 

Celery 사용

from typing import List

import requests
from celery import shared_task
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt


@shared_task()
def task(emails: List[str]) -> None:
    for _ in emails:
        requests.post('https://httpbin.org/delay/1')


@csrf_exempt
def send_emails(request):
    emails: List[str] = request.POST.get('emails')
    task_id = task.delay(['1.naver.com', '2.naver.com', '3.naver.com'])
    return JsonResponse({'task_id': task_id.id})
    
    
def task_status(request):
    task_id = request.GET.get('task_id')

    if task_id:
        task: AsyncResult = AsyncResult(task_id)
        if task.state == 'FAILURE':
            error = str(task.result)
            response = {
                'state': task.state,
                'error': error,
            }
        else:
            response = {
                'state': task.state,
            }
        return JsonResponse(response)

위와 같이 view에서 .delay 함수를 사용해서 호출해 주면 celery에 의해서 async하게 작동한다.

즉 email들이 다 처리되는걸 기다리지 않고(non-block) 일단 Response를 반환한다.

그럼 해당 Task가 성공적으로 처리 됐는지 어떻게 판단하냐? 

Result Backend에서 해당 Task가 지금 처리됐는지 아닌지 확인하면 된다. 그러므로 반환 시 해당 Task의 id를 던져주고 프론트단에서 해당 task_status를 지속적으로 호출해 해당 작업이 완료됐는지 확인한다.

 

결론

시간이 오래걸리거나, 네트워크 io 작업이 있는 작업은 view에서 바로 처리하지말자. 반환 속도도 느려지고, gunicorn worker도 block되기 때문에 좋지 않다.

 

코드 github : https://github.com/seunwoolee/celery_test 

Django ORM related_name, 역참조 삽질기 남긴다.. (피가 거꾸로 솟을뻔 했다)

 

 

아래와 같은 Model이 있다고 하자

차량(Car)이 있고 해당 차량에 적재할 수 있는 팔레트(Pallet)가 있다고 하자.

class Car(models.Model):
    car_number = models.CharField(max_length=50)
    type = models.CharField(max_length=50)

    def __str__(self):
        return f"{self.car_number}({self.type})_팔레트개수:{self.pallet.count()}"


class Pallet(models.Model):
    car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name='pallet')
    seq = models.PositiveIntegerField()

    def __str__(self):
        return f"{self.car}({self.seq})번째"
        
        
class OrderList(models.Model):
    codeName = models.CharField(max_length=255)
    pallet = models.ForeignKey(Pallet, on_delete=models.CASCADE, related_name='order_list', null=True, blank=True)

 

Car에서 자기에게 할당된 팔레트를 찾으려면 어떻게 하면 좋을까?

related_name을 활용한 역참조(reverse relation)를 쓰면된다.

Car.objects.first().pallet.all()

이런식으로 첫번째 차량의 모든 팔레트를 가져 오고 싶으면 여기서 pallet는 내가 related_name에서 pallet라고 명시해서 그렇다! love면 love로 참조하면 된다.

 

그렇다면 역참조 annotate는 어떻게 할까? 예를들어 모든 차량의 팔레트 수를 알고 싶을때 어떻게 할까?

Car.objects.all().annotate(pallet_count=Count(F('pallet')))

이런식으로 annotate를 쓰고 pallet로 참조해주면 된다.

 

 

이걸 쿼리로 풀면 아래와 같다. 진짜 간단한 쿼리다...

SELECT "release_car"."id", "release_car"."car_number", "release_car"."type", COUNT("release_pallet"."id") AS "pallet_count" FROM "release_car" LEFT OUTER JOIN "release_pallet" ON ("release_car"."id" = "release_pallet"."car_id") GROUP BY "release_car"."id"

 

 

마지막으로 역참조 filter + 역참조 annotate까지 붙이려면 어떻게 할까?

예를들면 해당 차량(car_id)의 모든 팔레트를 구하고싶은데 팔레트에 적재된 주문의 일자(주문 내역은 order_list로 역참조)로 filter하고 order_list 역참조의 주문 내역 Count를 알고싶을때는 아래와 같이 하면 된다.

 

상당히 복잡하게 말했는데 filter에서 order_list는 Pallet의 컬럼이 아니다 그런데 역참조로 인해서 컬럼처럼 쓰고있고(filter에서) annotate에서 또한 order_list를 마치 자신의 컬럼인양 사용하고 있다. 편하다 편해

pallets = Pallet.objects.filter(car__id=car_id).filter(order_list__ymd=ymd).annotate(counts=Count(F('order_list')))

 

 

결론 : 역참조는 좋다. 근데 헷갈린다.

'django' 카테고리의 다른 글

Django workers process 메모리 공유  (0) 2021.02.19
Django queryset to json  (0) 2019.12.31
Django Docker 배포(production)  (0) 2019.10.25
헷가리는 Django Static 간략 정리  (0) 2019.10.18
DRF ForeignKey Update  (0) 2019.06.14

엑셀파일

위와같은 엑셀파일을 Django ORM을 이용해서 insert 하고자한다.

 

소스코드


import openpyxl
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "subul.settings")
django.setup()

from order.models import Order
from core.models import Location
from product.models import ProductCode

import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "subul.settings")
django.setup()

 

위의 3코드는 Django의 설정파일을 set해주고 django.setup()으로 django를 실행한다고 생각하면 편하겠다

 


wb = openpyxl.load_workbook('productOrder.xlsx') # productOrder.xlsx 파일을 열어서 wb 변수에 할당
sheet1 = wb['Sheet1'] # 엑셀의 Sheet1을 open
rows = sheet1['A3':'L310'] # sheet1의 A3부터 L310까지 rows 변수에 할당

for row in rows:

    # rows는 여러줄이니 row는 한줄

    # row[0]은 가로 한줄의 제일 첫번째 세로칸 즉 A3,B3.... L3


     dict = {}
     dict['id'] = row[0].value
     dict['type'] = row[1].value
     dict['specialTag'] = row[2].value
     dict['ymd'] = row[3].value
     dict['orderLocationName'] = row[5].value
     dict['codeName'] = row[6].value
     dict['amount'] = row[7].value
     dict['count'] = row[8].value
     dict['price'] = row[10].value
     dict['memo'] = row[11].value

     product_Instance = ProductCode.objects.get(codeName=dict['codeName'])
     toLocation_instance = Location.objects.filter(codeName=dict['orderLocationName']).filter(type='05').first()
     dict['code'] = product_Instance.code
     dict['productCode'] = product_Instance
     dict['orderLocationCode'] = toLocation_instance
     Order(**dict).save() # 킬링포인트 **딕셔너리를 활용해서 쉽게 insert


'python' 카테고리의 다른 글

파이썬 데코레이터 활용(로깅)  (1) 2021.09.02
python oracle 데이터 json serialize(django)  (0) 2020.02.25
python super  (0) 2019.12.26
python 메모리 관리(memory allocation)  (4) 2019.11.15
클래스 관계 IS-A 상속  (0) 2019.11.07

 

model Code

 

class Detail(models.Model):
    """
    Order, Release의 부모 클래스
    """
    ymd = models.CharField(max_length=8)
    code = models.CharField(max_length=255)
    codeName = models.CharField(max_length=255)
    count = models.IntegerField()
    amount = models.DecimalField(decimal_places=2, max_digits=19, default=0)
    amount_kg = models.DecimalField(decimal_places=2, max_digits=19, default=0, blank=True, null=True)
    memo = models.TextField(blank=True, null=True)

    class Meta:
        abstract = True

 

 

class Release(Detail):

    """

    출고에 관한 데이터 출고는 주문을 기반으로 생성되거나 독립적으로 생성될 수 있음

    """
    price = models.IntegerField()

    releaseVat = models.IntegerField()
    releaseOrder = models.ForeignKey('order.Order', on_delete=models.CASCADE, null=True, blank=True)

 

class Order(Detail):

    """

    주문 데이터

    """
    orderLocationCode = models.ForeignKey(Location, on_delete=models.CASCADE)
    orderLocationName = models.CharField(max_length=255)
    price = models.DecimalField(decimal_places=1, max_digits=19)

 

django rest framework를 이용해서 Update, Delete를 하고있는데 ForeignKey가 걸려있는 데이터를 Update해달라는 요청이 왔다. 즉 Release Class - releaseOrder의 memo값을 수정해야 한다(주문메모를 출고 데이터 update 시 같이 변경)

 

Serializer Code

 

class ReleaseSerializer(serializers.ModelSerializer):

    class Meta:
        model = Release
        fields = '__all__'

 

이렇게 아름답고 깔끔하게 되어있는 Serializer를 변경하기 싫지만 수정은 해야하니....

프론트 엔드쪽에서는 patch를 이용해 Update를 실시한다 그러므로 update 메소드를 오버라이딩해서 기본 update에 ForeignKey가 걸려있는 Order(주문) 데이터도 함께 변경해보자

 

 

class ReleaseSerializer(serializers.ModelSerializer):
    orderMemo = serializers.CharField(required=False) # 프론트에서 넘어올때 orderMemo로 넘어옴

    class Meta:
        model = Release
        fields = '__all__'

    def update(self, instance, validated_data):
        """
        출고조회 - 수정 시 ForeignKey Order의 메모 수정으로 overRide 실시(orderMemo)

         출고 데이터 중 price(가격), releaseVat(부가세), 주문메모만을 변경함.
        """
        instance.price = validated_data.get('price')
        instance.releaseVat = validated_data.get('releaseVat')
        instance.releaseOrder.memo = validated_data.get('orderMemo')
        instance.releaseOrder.save()
        instance.save()
        return instance

 

ModelSerializer의 update 메소드를 오버라이딩해서 instance.releaseOrder.memo와 같이 접근해서 Order(주문) 데이터를 Release(출고) 데이터와 함께 Update를 실시하고 있다!

'django' 카테고리의 다른 글

Django workers process 메모리 공유  (0) 2021.02.19
Django queryset to json  (0) 2019.12.31
Django related_name(reverse 역참조 )  (0) 2019.11.13
Django Docker 배포(production)  (0) 2019.10.25
헷가리는 Django Static 간략 정리  (0) 2019.10.18

+ Recent posts