우리가 작성하는 대부분의 쿼리는 간단하다. Django의 ORM은 일반적으로 쿼리를 쉽게 작성할 수 있는 생산성 지름길을 제공한다. 이는 대부분의 상황에 꽤 잘 수행되지만, 몇가지 단점이 있고 Django를 잘 사용하기 위해선 이해해야 한다.
세부 정보 페이지와 같이 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()를 사용할 경우, 특정 레코드가 삭제될 때, 전체 사이트가 손상될 수 있다.
get_object_or_404()를 사용하는 경우, try-except를 래핑할 필요가 없지만, 대부분의 다른 상황에서는 try-except처리를 해야한다.
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)
쿼리가 둘 이상의 개체를 반환할 수 있는 경우 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)
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
또는 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
Django의 ORM은 강력하지만 잘 되지 않는 부분도 많다. 이때, 보통 사람들은 반환된 queryset을 Python에서 처리하곤 하는데 그보단 Advanced Query Tools를 활용하는것이 성능적으로도 뛰어나며 입증된 코드를 활용할 수도 있다.
데이터베이스에서 읽기를 수행할 때, 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/
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/
raw SQL 을 작성하는 것은 보안과 재사용성을 해친다. 특히, Django 앱 중 하나를 third-party package로 release하는 경우 portability에 좋지않다. 또한 드물게 데이터베이스를 다른 데이터베이스로 마이그레이션해야 할 경우, 마이그레이션이 복잡해진다.
그렇다면 언제 raw SQL을 사용하는가?
raw SQL을 사용함으로써 Python code나 ORM이 생성할 SQL이 대폭 간결해진다면 사용하라.
인덱스 없이 시작하여 필요에 따라 추가하라.
- 인덱스가 필요해지는 순간
- 실제로 데이터가 존재해서 인덱싱을 했을 때의 결과를 분석할 수 있을 때.
- 인덱싱을 통한 성능향상이 얼마나 일어나는지 테스트할 수 있을 때.
인덱싱은 전체 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.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)
명시적으로 트랜잭션을 지정하는 것은 개발 소요를 늘린다는 단점이 있지만 사이트 성능을 높일 수 있는 방법이다.
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으로 랩핑하라.
view가 django.http.StreamingHttpResponse 를 return하는 경우, response가 시작된 뒤엔 에러를 transaction이 처리할 수 없다. 만약, ATOMIC_REQUESTS를 사용하는 경우 두 가지 중 하나를 수행해야 한다.
- ATOMIC_REQUESTS 를 False로 설정하고 7.7.2: Explicit Transaction Declaration.의 방법을 적용한다.
- view를 django.db.transaction.non_atomic_requests decorator로 감싼다.
Streaming response와 ATOMIC_REQUESTS를 같이 사용할 순 있지만 transaction은 view 자체에만 적용된다. 만약 response stream이 추가적으로 SQL Query를 생성하는 경우, 그 이후의 처리는 roll back 되지 않고, 데이터베이스에 반영될 것이다.
MySQL을 사용하는 경우, 선택한 테이블 타입에 따라 Inno DB, MyISAM등을 선택했을 땐 transaction이 지원되지 않을 수 있다.
transaction docs : https://docs.djangoproject.com/en/3.2/topics/db/transactions/
Real Python에도 transaction을 주제로 한 좋은 튜토리얼이 있다.
참조 : https://realpython.com/transaction-management-with-django-1-6/