F() 표현식으로 django 쿼리 업그레이드

10369 단어 djangowebdevpython

TL; DR



읽기/쓰기 작업을 위해 모델 필드를 참조할 때 F() 표현식을 사용합시다.
  • 데이터베이스에서 모델 필드를 직접 참조하도록 도와줍니다. Python 메모리에 로드할 필요 없이 -> 쿼리를 저장합니다.
  • 경합 상태를 방지하는 데 도움이 될 수 있습니다. 또는 dirty read .
  • Python은 실제 결과 대신 SQL 표현식에 대해서만 알고 있기 때문에 쿼리 후 refresh_from_db가 필요합니다.

  • 대량 업데이트



    귀하의 국가에서 정부가 세율을 5% 인상하여 귀하가 리스팅 제품 가격을 20% 인상해야 한다고 가정합니다. django 쿼리는 어떻게 생겼습니까?

    class Product(models.Model):
    
        name = models.TextField()
        price = models.DecimalField()
        in_stock = models.IntegerField(
            help_text="Number of items available in inventory"
        )
    


    여러 제품을 업데이트하는 순진한 구현은 다음과 같을 수 있습니다.

    products = Product.objects.all()
    for product in products:
        product.price *= 1.2
        product.save()
    


    이 경우 각 레코드를 SELECT price FROM product 다음에 UPDATE product SET price = new_value WHERE condition 수행합니다. 각 개체에 대해 2개의 쿼리(READ용 1개, WRITE용 1개)를 의미합니다.

    좀 더 신중하게 생각해보면 새로운 가격은 그것이 무엇이든 현재 가격에 상대적이라는 것을 알 수 있습니다. 직관적으로 업데이트 프로세스를 실행할 때 price 모델의 Product 필드를 참조하려고 합니다.

    이제 F() 식입니다. The Django official doc 상태:

    An F() object represents the value of a model field, transformed value of a model field, or annotated column.
    It makes it possible to refer to model field values and perform database operations using them without actually having to pull them out of the database into Python memory.


    F()update() queryset 메서드로 문제를 해결해 봅시다.

    from django.db.models import F
    
    Product.objects.update(price=F("price")*1.2)
    


    위의 쿼리는 인스턴스 속성에 값을 할당하는 일반적인 Python처럼 보이지만 실제로는 SQL 표현식입니다. 이 식은 데이터베이스에 데이터베이스의 price 필드에 120%를 곱하도록 지시합니다.

    새 가격 값은 현재 가격 값을 기반으로 하므로 Python 메모리에 로드할 필요가 없습니다. 이것이 F()가 작동하는 이유입니다.

    단일 개체 업데이트



    모든 주문 결제가 완료된 후 in_stock 필드를 업데이트한다고 가정해 보겠습니다.

    순진한 구현은 다음과 같을 수 있습니다.

    def process_payment(product: Product):
        with transaction.atomic():
            payment = Payment.objects.create(product=product)
            product.in_stock = product.in_stock - 1
            product.save(update_fields=["in_stock"])
    


    그래서 문제가 무엇입니까?
    제품 주문을 시도하는 여러 사용자가 있다고 가정해 보겠습니다. 시나리오는 다음과 같습니다.


    공정 1
    프로세스 2
    재고


    선택 in_stock -> 5

    5

    선택 in_stock -> 5
    5

    업데이트in_stock = 5-1

    4

    업데이트in_stock = 5-1
    4


    이 경우 두 프로세스product.in_stock가 동시에 업데이트되지만in_stock 값이 1씩 감소합니다. 이는 잘못된 것입니다.

    주요 문제는 가져온 항목을 기반으로 감소in_stock한다는 것입니다. 데이터베이스에 현재 저장된 항목을 기반으로 업데이트 명령in_stock을 제공하면 어떻게 됩니까?

    def process_payment(product: Product):
        with transaction.atomic():
            payment = Payment.objects.create(product=product)
            product.in_stock = F("in_stock") - 1
            product.save(update_fields=["in_stock"]])
    


    두 접근 방식의 차이는 매우 적지만 업데이트 명령으로 생성된 SQL을 살펴보겠습니다.

    순진한 접근 방식:

    UPDATE product_product
    SET in_stock = 4
    WHERE id = 262;
    


    이렇게 하면 데이터베이스의 in_stock 현재 값에 관계없이 수량 = 4가 감소합니다.

    F() 접근법:

    UPDATE product_product
    SET in_stock = in_stock + 1
    WHERE id = 262;
    


    ID가 262인 제품의 수량은 1개 감소하며 고정된 값으로 설정되지 않습니다. 경쟁 조건 문제를 해결하기 위해 F 표현식을 사용하는 방법입니다.

    메모



    모델 필드에 할당된 F() 개체는 모델 인스턴스를 저장한 후에도 지속되며 각각에 적용되므로save() 업데이트된 인스턴스를 가져오려면refresh_from_db 필요합니다.

    데이터베이스에서 새로 고치지 않고 인스턴스를 읽으려고 하면 예기치 않은 결과가 발생할 수 있습니다.

    In [12]: product = Product.objects.get(id=262)
    
    In [13]: product.in_stock = F("in_stock") - 1
    
    In [14]: product.save()
    
    In [15]: product.in_stock
    Out[15]: <CombinedExpression: F(in_stock) - Value(1)>
    
    In [16]: 
    


    요약



    기사 전체에서 우리는 F() 표현식의 두 가지 사용 사례를 지적했습니다.
  • 작업을 수행하기 위해 Python이 아닌 데이터베이스를 가져옴으로써 일부 작업에 필요한 쿼리 수를 줄입니다.
  • 두 프로세스가 동일한 인스턴스를 검색하고 업데이트할 때 경합 상태를 방지합니다.
  • 좋은 웹페이지 즐겨찾기