장고 쿼리 표현식 (Django Query Expressions)은 업데이트, 생성, 필터링, 순서 기준, 주석 또는 집계에서 사용할 수 있는 값 또는 계산을 설명한다. (update, create, filter, order by, annotation, or aggregate) 해당 부분을 살펴보자!
F() 객체는 모델의 필드 혹은 어노테이트된 열의 값을 나타낸다. 실제로 데이터베이스에서 Python 메모리로 가져오지 않고, 모델 필드 값을 참조하고 이를 데이터베이스에서 사용하여 작업할 수 있다.
즉, 우리가 python 코드로 작성해 python 런타임 환경에서 연산을 해서 DB에 적용하는 것이 아니라, F() object를 통해 해당 연산을 하는 Query를 만들 수 있다는 것이다.
# [CASE1]
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed += 1
reporter.save()
---
# [CASE2]
from django.db.models import F
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()
reporter = Reporters.objects.get(pk=reporter.pk)
# 또는 간결하게 아래처럼
reporter.refresh_from_db()
case1, case2 모두 stories_filed 값을 +1 하고 싶어한다. 연산을 처리하는 주체에 집중해서 차이점을 살펴보자.
case1
case2
연산자를 오버라이딩하여 캡슐화된 SQL문을 생성한다.
refresh_from_db
를 통해 다시 불러와야 한다. # filter로 특정 모델객체들 다 들고와서 값 +1 하기
reporter = Reporters.objects.filter(name='Tintin')
reporter.update(stories_filed=F('stories_filed') + 1)
# 모든 모델 객체 값들 +1 로 업데이트 하기
Reporter.objects.all().update(stories_filed=F('stories_filed') + 1)
Queryset For문 멈춰!!
기본적으로 데이터베이스에서 해당 연산을 처리, 그에 따라 Query 튜닝 가능하며 그렇기 때문에 경쟁 조건 (race condition)을 피할 수 있음을 기억하고 있어야한다.
Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks') * 2)
예제와 같이, 핵심은 'DB에게 연산을 처리할 수 있는 SQL 만들어 준다' 에 초점이 맞춰져 있다. company = Company.objects.annotate(chairs_needed=F('num_employees') - F('num_chairs'))
예제와 같이 annotate을 활용해 Company모델이 가지고 있는 필드 num_employees, num_chairs를 연산해서 chairs_needed라는 필드로 만들어서 (create dynamic fields) 가져올 수 있다.
즉 annotate이 as
와 비슷한 역할을 할 때 F를 통해 연산된 동적필드를 만들 수 있다는 것이다.
만약 필드가 다른 타입이라면, F() 객체에서는 output_field를 지정할 수 없으니 아래처럼 ExpressionWrapper
로 해당 표현식을 감싸줘야 한다.
from django.db.models import DateTimeField, ExpressionWrapper, F
Ticket.objects.annotate(
expires=ExpressionWrapper(
F('active_at') + F('duration'),
output_field=DateTimeField()
)
)
Company.objects.order_by(F('last_contacted').desc(nulls_last=True))
와 같이 Company 모델 인스턴스들을 last_contacted
필드를 기준으로 내림차순 정렬하되, 해당 필드가 null 값을 가졌을 경우에 제일 뒤로 보내는 쿼리를 아쌉하게 만들 수 있다. "Func() 식은 MERGE 및 LOWER와 같은 데이터베이스 함수 또는 SUM과 같은 Aggregate를 포함하는 모든 식들의 기본 유형이다." 라고 공식문에서 말한다. 즉 데이터베이스의 함수를 표현하는 것의 Base가 Func object라는 것이다.
즉, Djnago가 지원하지 않는 DB 함수를 쓸 땐, DB에서 데이터를 받아온 다음 파이썬 코드를 작성해야 하며, Func()를 사용해 장고에서 지원하지 않는 DB 함수를 구현하면 된다는 것이다.
class Lower(Func):
function = 'LOWER'
queryset.annotate(field_lower=Lower('field'))
# 위 python 코드는 아래 SQL query를 만들어 낸다.
SELECT
...
LOWER("db_table"."field") as "field_lower"
from django.db.models import Func
# 단발성, 1회성인 경우에는 인라인으로
temp = Test.objects.annotate(te1=Func('test_data', function='UNNEST'))
# 또는 class를 만들어서 재사용이 가능하게
class UNNEST(Func):
function = 'UNNEST'
temp = Test.objects.annotate(test_data=UNNEST('test_data'))
class Func(*expressions, **extra)
를 좀 더 살펴보자. 아래 실제로 Func 함수가 어떻게 정의되어 있는지 단편적 코드를 가져왔다. class Func(SQLiteNumericMixin, Expression):
"""An SQL function call."""
function = None
template = '%(function)s(%(expressions)s)'
arg_joiner = ', '
arity = None # The number of arguments the function accepts.
# ... 중략 ...
def as_sql(self, compiler, connection, function=None, template=None, arg_joiner=None, **extra_context):
# ... 생략 ...
function과 expression을 결합하여 DB에 질의할 query를 만드는 템플릿이다. 기본값은 '%(function)s(%(expressions)s)'
이다.
만약 DB 쿼리 시, strftime('%W', 'date')
와 같이 %가 필요한 경우에는 %가 두 번 입력되기 때문에 템플릿 속성에서 % 문자를 (%%%%)로 4배 늘려야 합니다.
DBSM 마다 같은 기능을 하는 DB 함수의 명칭이 다를 수 있다. 그래서 as_dbms이름와 같은 형태로 다양한 형태의 DBMS에 대해 정의 해 줄 수 있다.
as_DB명()은 function, template, arg_joiner와 추가적인 **extra_context 파라미터를 선언해야 한다.
class ConcatPair(Func):
...
function = 'CONCAT'
...
def as_mysql(self, compiler, connection, **extra_context):
return super().as_sql(
compiler, connection,
function='CONCAT_WS',
template="%(function)s('', %(expressions)s)",
**extra_context
)
**extra_context
는 key=value
쌍으로 표현할 수 있는데 이는 "문자열 그대로 전달"하여 사용하므로 잘못 사용하면 SQL 인젝션에 취약할 수 있다.
위 케이스의 예시는 postgresql를 위해 만드는 django 미지원 함수 글에서 확인이 가능하다.
Q object는 from django.db.models import Q
에 속해 있는 Django built in object이다. Where에서 AND OR NOT 등과 같은 조금 복잡한 질의에 활용되는 object이다.
쉽게 생각하면 filter, get등의 ORM 질의 함수에 들어가는 Q object다. 아래 예제들을 살펴보자!
# SQL 쿼리문
SELECT * FROM product WHERE category='A' AND sub_category='AB'
# Django ORM, 아래 2개가 동일하다.
Product.objects.filter(Q(category='A') & Q(sub_category='AB'))
Product.objects.filter(category='A', sub_category='AB')
# SQL 쿼리문
SELECT * FROM product WHERE category='A' OR sub_category='AB'
# Django ORM
Product.objects.filter(Q(category='A') | Q(sub_category='AB'))
# SQL 쿼리문
SELECT * FROM product WHERE category='A' AND sub_category != 'AB' # 더 정확하게 '<>' 임
# Django ORM
Product.objects.filter(Q(category='A') & ~Q(sub_category='AB'))
# User의 first_name이 R로 시작하면서 last_name이 Z로 시작하지 않는 것
User.objects.filter(
Q(first_name__startswith='R') & ~Q(last_name__startswith='Z')
)
# Product의 category에 A가 포함되거나, sub_category가 AA, AB, AC 중 하나 인 것
Product.objects.filter(
Q(category__contains='A') | Q(sub_category__in=['AA', 'AB', 'AC'])
)
# 추가로 icontains는 contains와 같지만 대소구분을 하지않는 쿼리를 던진다.
# 그리고 대소구분은 DB(or table, or column)의 charset, BINARY 설정과 관련되어 있다.
# 아래 코드의 답은 읽으시는 분의 해석에 맡기겠다. 댓글 기대하겠습니다. :)
Product.objects.filter(
Q(category__contains='A') &
Q(sub_category__in=['AA', 'AB', 'AC']) &
Q(option__isnull=False) &
Q(price__gte=5000)
)
Queryset을 가져와 재가공하거나 직접 list를 가공하는 것 보다, 한 줄의 Query, ORM 코드로 표현하는 것이 훨씬 빠르다. (물론 절대적이라는 의미가 아니라, 결과가 1~2개 object인 경우 인 등의 특수 케이스를 제외하고)
특히 하기 쉬운 타협점이 queryset을 들고와 또 해당 queryset으로 filter 등의 질의 함수를 호출한다. 아마 직접 확인해보면 알겠지만, 아무리 캐싱을 하더라도 해당 작업은 쿼리를 2개 이상 날리게 될 것이다. 그래서 가능하면 하나의 쿼리로 묶어서 질의를 던지는게 DB에 부담도 훨씬 덜 가고, 최적화를 하는 것이 올바른 방향이다.
실제 query는 어떻게 되는지, queryset.query 의 str 을 확인하면 된다. 계속 실제 쿼리를 확인하면서 SQL을 최적화(쿼리 튜닝)을 하는 것이 BE의 가장 기본적인 소명이라고 생각한다.
Aggregate(), Value(), Subquery()
를 추가적으로 다룬다. 한 글에 다 쓰고 싶었으나 예제가 까지 포함하면 분량이 너무 많아서 2개로 나누었다.