Skip to content

Latest commit

 

History

History
345 lines (243 loc) · 13.4 KB

07.md

File metadata and controls

345 lines (243 loc) · 13.4 KB

7. Queries and the Database Layer

우리가 작성하는 대부분의 쿼리는 간단하다. Django의 ORM은 일반적으로 쿼리를 쉽게 작성할 수 있는 생산성 지름길을 제공한다. 이는 대부분의 상황에 꽤 잘 수행되지만, 몇가지 단점이 있고 Django를 잘 사용하기 위해선 이해해야 한다.

7.1 Use get_object_or_404() for Single Objects

세부 정보 페이지와 같이 single object를 retrieve하고 관련 작업을 수행하는 경우, get() 대신 get_object_or_404()를 사용하라.

Warning

get_object_or_404()는 views에서만 사용해야 한다.

views에서만 사용하고, helper functions, forms, model methods 등 view가 아니거나 view와 directly related 되지 않았을 때 사용하지 마라.

  • view 외의 곳에서 get_object_of_404()를 사용할 경우, 특정 레코드가 삭제될 때, 전체 사이트가 손상될 수 있다.

7.2 Be Careful With Queries That Might Throw Exceptions

get_object_or_404()를 사용하는 경우, try-except를 래핑할 필요가 없지만, 대부분의 다른 상황에서는 try-except처리를 해야한다.

7.2.1 ObjectDoesNotExist vs. DoesNotExist

ObjectDoesNotExist는 모든 모델 개체에 적용할 수 있지만 DoesNotExist는 특정 모델에 적용된다.

예제 7.1: ObjectDoesNotExist 사용예시

from django.core.exceptions import ObjectDoesNotExist

from flavors.models import Flavor
from store.exceptions import OutOfStock

def list_flavor_line_item(sku):
  try:
    return Flavor.objects.get(sku=sku, quantity__gt=0)
  except Flavor.DoesNotExist:
    msg = 'We are out of {0}'.format(sku)
    raise OutOfStock(msg)

def list_any_line_item(model, sku):
  try:
    return model.objects.get(sku=sku, quantity__gt=0)
  except ObjectDoesNotExist:
    msg = 'We are out of {0}'.format(sku)
    raise OutOfStock(msg)

7.2.2 When You Just Want One Object but Get Three Back

쿼리가 둘 이상의 개체를 반환할 수 있는 경우 MultipleObjectsReturned 예외를 확인하라.

그런 다음 except 절에서 special exception를 발생시키거나 error를 logging하는 등 어떠한 처리도 가능하다.

예제 7.2: MultipleObjectsReturned 사용예시

from flavors.models import Flavor
from store.exceptions import OutOfStock, CorruptedDatabase

def list_flavor_line_item(sku):
  try:
    return Flavor.objects.get(sku=sku, quantity__gt=0)
  except Flavor.DoesNotExist:
    msg = 'We are out of {}'.format(sku)
    raise OutOfStock(msg)
  except Flavor.MultipleObjectsReturned:
    msg = 'Multiple items have SKU {}. Please fix!'.format(sku)
    raise CorruptedDatabase(msg)

7.3 Use Lazy Evaluation to Make Queries Legible

Django의 ORM이 강력한 만큼 유지보수를 위해 코드를 읽기 쉽게 만드는 것은 중요하다.

복잡한 쿼리를 사용하는 경우, 너무 많은 기능이 작은 line set에 chaining 되지 않도록 주의하라.

예제 7.3: Illegible Queries

# Don't do this!
from django.db.models import Q

from promos.models import Promo

def fun_function(name=None):
  """Find working ice cream promo"""

    # Too much query chaining makes code go off the screen or page. Notgood.
  return
    Promo.objects.active().filter(Q(name__startswith=name)|Q(description__icontain)

위와 같은 코드는 스크린을 넘어갈 정도로 길어 좋지않다. 이러한 불편함은 lazy evaluation을 이용해 ORM code를 깨끗하게 유지할 수 있다.

Lazy evaluation이란 데이터가 실제로 필요할 때까지 SQL 호출을 하지 않는것을 뜻한다.

이를 통해 안좋은 예제7.3에서 아래와 같은 읽기 좋은 코드로 바꿀 수 있다.

예제 7.4: Legible Queries

# Don't do this!
from django.db.models import Q

from promos.models import Promo

def fun_function(name=None):
  """Find working ice cream promo"""
  results = Promo.objects.active()
  results = results.filter(
                   Q(name__startswith=name) |
                   Q(description__icontains=name)
               )
  results = results.exclude(status='melted')
  results = results.select_related('flavors')
  return results

7.3.1 Chaining Queries for Legibility

또는 Lazy evaluation을 사용하는 대신 다음과 같이 쿼리를 연결할 수도 있다.

예제 7.5: Chaining Queries

# Do this!
from django.db.models import Q

from promos.models import Promo

def fun_function(name=None):
  """Find working ice cream promo"""
  qs = (Promo
      .objects
      .active()
      .filter(
        Q(name__startswith=name) |
        Q(description__icontains=name)
      )
      .exclude(status='melted')
      .select_related('flavors')
    )
  return qs

이러한 코드의 단점은 디버깅이 lazy evaluation을 이용했을 때 보다 어렵다는 것이다. 이 방식에선 쿼리 중간에 PDB 또는 IPDB 호출을 고정시킬 수 없다.

이를 위해선 약간의 주석 처리를 해야한다.

예제 7.6: Debugging with Chained Queries

# Do this!
from django.db.models import Q

from promos.models import Promo

def fun_function(name=None):
  """Find working ice cream promo"""
  qs = (Promo
      .objects
      .active()
      #.filter(
      #  Q(name__startswith=name) |
      #  Q(description__icontains=name)
      #)
      #.exclude(status='melted')
      #.select_related('flavors')
    )
  return qs

7.4 Lean on Advanced Query Tools

Django의 ORM은 강력하지만 잘 되지 않는 부분도 많다. 이때, 보통 사람들은 반환된 queryset을 Python에서 처리하곤 하는데 그보단 Advanced Query Tools를 활용하는것이 성능적으로도 뛰어나며 입증된 코드를 활용할 수도 있다.

7.4.1 Query Expressions

데이터베이스에서 읽기를 수행할 때, query expression 을 활용하여 읽는 도중에 값이나 계산을 만들 수 있다.

이는 이해하기 쉽지 않다. 아래의 예시를 보자.

예시는 아이스크림 가게를 방문할 때마다 평균적으로 한 스쿱 이상을 주문한 모든 고객을 나열하고 있다.

예제 7.7: Query Expressions 을 활용하지 않는 경우

# Don't do this!
from models.customers import Customer

customers = []

for customer in Customer.objects.iterator():
  if customer.scoops_ordered > customer.store_visits:
    customers.append(customer)
  • 위 코드의 단점
    • Python 반복문으로 데이터베이스의 모든 고객 레코드를 iterate 하는것은 느리고 메모리를 낭비한다.
    • 사용량에 관계없이 race condition이 발생한다. 이는 고객이 데이터와 상호 작용하는 동안 스크립트를 실행할 때 발생한다. UPDATE를 수행하는 경우 치명적일 수 있다.

아래와 같이 query expression 을 활용하면 효율적으로 race condition 에서 안전하게 같은 동작을 수행할 수 있다.

예제 7.8: Yes Query Expressions

from django.db.models import F

from models.customers import Customer

customers = Customer.objects.filter(scoops_ordered__gt=F('store_visits'))

이는 데이터 베이스가 직접 비교를 수행하게 한다. 이때, Django는 아마 아래 동작을 수행한다.

예제 7.9: Query Expression Rendered as SQL

SELECT * from customers_customer where scoops_ordered > store_visits

query expressions docs: https://docs.djangoproject.com/en/3.2/ref/models/expressions/

7.4.2 Database Functions

Django 1.8부터 UPPER(), LOWER(), COALESCE(), CONCAT(), LENGTH(), 그리고 SUBSTR() 와 같은 데이터베이스 함수들을 손쉽게 사용할 수 있게됐다.

  • 데이터베이스 함수의 장점
    • 사용하기 쉽다.
    • 일부 논리를 Python에서 데이터베이스로 옮길 수 있다.
    • Python에서의 처리보다 빠르다.
    • 데이터를 데이터베이스에서 처리한다.
    • 데이터베이스 함수는 데이터베이스마다 다르게 구현되지만, Django의 추상화 덕분에 PostgreSQL에 대해 작성하더라도 MySQL, SQLite3에서 잘 작동한다.
    • 데이터베이스 함수는 query expression이기도 하다. 이미 배운 좋은 패턴

database functions docs: https://docs.djangoproject.com/en/3.2/ref/models/database-functions/

7.5 Don’t Drop Down to Raw SQL Until It’s Necessary

raw SQL 을 작성하는 것은 보안과 재사용성을 해친다. 특히, Django 앱 중 하나를 third-party package로 release하는 경우 portability에 좋지않다. 또한 드물게 데이터베이스를 다른 데이터베이스로 마이그레이션해야 할 경우, 마이그레이션이 복잡해진다.

그렇다면 언제 raw SQL을 사용하는가?

raw SQL을 사용함으로써 Python code나 ORM이 생성할 SQL이 대폭 간결해진다면 사용하라.

7.6 Add Indexes as Needed

인덱스 없이 시작하여 필요에 따라 추가하라.

  • 인덱스가 필요해지는 순간
    • 실제로 데이터가 존재해서 인덱싱을 했을 때의 결과를 분석할 수 있을 때.
    • 인덱싱을 통한 성능향상이 얼마나 일어나는지 테스트할 수 있을 때.

인덱싱은 전체 query의 10-25% 정도로 자주 사용된다.

indexes docs: https://docs.djangoproject.com/en/3.2/ref/models/indexes/

option index docs: https://docs.djangoproject.com/en/3.2/ref/models/options/#indexes

7.7 Transactions

7.7.1 Wrapping Each HTTP Request in a Transaction

예제 7.10: Wrapping Each HTTP Request in a Transaction

# settings/base.py
DATABASES = {
  'default': {
    # ...
    'ATOMIC_REQUESTS': True,
  },
}

장점: 간단한 세팅 하나로 모든 request를 transaction으로 감쌀 수 있다.

단점: 성능에 영향을 미칠 수 있다.

쓰기 작업이 많은 프로젝트를 시작할 때, 쉽게 데이터베이스 무결성을 유지하는데 좋은 방법이다. 트래픽이 많아진다면 다른 접근법을 적용해야 할 것이다.

고려해야 할 점: 이 방법은 데이터베이스에 대한 변경점만 roll back 시켜준다. 그러므로 확인 이메일을 보내는 작업이 섞여있는 등의 상황에서 난감할 수 있다. 이러한 데이터베이스에 create/update/delete�하면서 동시에 데이터베이스가 아닌 것과 상호작용하는 경우, transaction.non_atomic_requests()를 이용해 아래 예제와 같이 작성할 수 있다.

예제 7.11: Simple Non-Atomic View

# flavors/views.py
from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone

from .models import Flavor

@transaction.non_atomic_requests
def posting_flavor_status(request, pk, status):
  flavor = get_object_or_404(Flavor, pk=pk)

  # This will execute in autocommit mode (Django's default).
  flavor.latest_status_change_attempt = timezone.now()
  flavor.save()

  with transaction.atomic():
    # This code executes inside a transaction.
    flavor.status = status
    flavor.latest_status_change_success = timezone.now()
    flavor.save()
    return HttpResponse('Hooray')

  # If the transaction fails, return the appropriate status
  return HttpResponse('Sadness', status_code=400)

7.7.2 Explicit Transaction Declaration

명시적으로 트랜잭션을 지정하는 것은 개발 소요를 늘린다는 단점이 있지만 사이트 성능을 높일 수 있는 방법이다.

Aymeric Augustin : 성능 오버헤드를 견딜 수 있는 한 ATOMIC_REQUESTS를 사용하십시오. 그것은 대부분의 사이트에서 "영원히"를 의미합니다.

transaction 사용 가이드라인

Purpose ORM 함수 일반적으로 transaction 사용?
Create Data .create(), .bulk_create(), .get_or_create(), Yes
Retrieve Data .get(), .filter(), .count(), .it- erate(), .exists(), .exclude(), .in_bulk, etc.
Modify Data .update() Yes
Delete Data .delete() Yes

Note

TIP: 개별적인 ORM 함수 호출을 랩핑하지 마세요

Django는 내부적으로 개별적인 ORM 함수 호출에 대해선 transaction이 적용되고 있기 때문에 따로 개별적으로 ORM 함수 호출을 랩핑할 필요가 없다. 대신 view, function, 메소드 등에서 여러 함수를 호출할 때, transaction으로 랩핑하라.

7.7.3 django.http.StreamingHttpResponse and Transactions

view가 django.http.StreamingHttpResponse 를 return하는 경우, response가 시작된 뒤엔 에러를 transaction이 처리할 수 없다. 만약, ATOMIC_REQUESTS를 사용하는 경우 두 가지 중 하나를 수행해야 한다.

  1. ATOMIC_REQUESTS 를 False로 설정하고 7.7.2: Explicit Transaction Declaration.의 방법을 적용한다.
  2. view를 django.db.transaction.non_atomic_requests decorator로 감싼다.

Streaming response와 ATOMIC_REQUESTS를 같이 사용할 순 있지만 transaction은 view 자체에만 적용된다. 만약 response stream이 추가적으로 SQL Query를 생성하는 경우, 그 이후의 처리는 roll back 되지 않고, 데이터베이스에 반영될 것이다.

7.7.4 Transactions in MySQL

MySQL을 사용하는 경우, 선택한 테이블 타입에 따라 Inno DB, MyISAM등을 선택했을 땐 transaction이 지원되지 않을 수 있다.

7.7.5 Django ORM Transaction Resources

transaction docs : https://docs.djangoproject.com/en/3.2/topics/db/transactions/

Real Python에도 transaction을 주제로 한 좋은 튜토리얼이 있다.

참조 : https://realpython.com/transaction-management-with-django-1-6/