오늘은 Django의 ORM이 어떻게 동작하는지 궁금해서 이에 대해서 좀 찾아봤다. 배경 지식 삼아서 ORM에 대해서도 알아보려 했는데, 아직 블로그에 작성할 수 있을 정도로 정리가 되지 않아서 ORM은 조금 더 정리를 해야 할 것 같다. 대신 Django ORM에 대해 알아보던 중에 좋은 자료를 찾게 되었다. 그것은 Django ORM Cookbook으로, 한국어로 장고 ORM 요리책이라고 하는 것이었다. 이 책의 내용이 이 사이트에 무료로, 그걸로 무려 한국어로 실려 있었다.
『장고 ORM 요리책(Django ORM Cookbook)』은 장고의 ORM(객체 관계 매핑) 기능과 모델 기능을 활용하는 다양한 레시피(조리법)를 담은 책입니다. 장고는 모델-템플릿-뷰(MTV) 프레임워크입니다. 이 책은 그 가운데 ‘모델’에 대해 상세히 다룹니다.
이 책은 “장고 ORM/쿼리셋/모델으로 ~을 하는 방법은 무엇인가요?”와 같은 질문 50여 개와 그 답을 담고 있습니다.
위 사이트에서 이 책을 소개하는 내용이다. 책의 소개에서 알 수 있듯, Django의 모델에 대해 자세히 다룬다고 한다.
이 책의 각 장은 각각 질문을 하나씩 다룹니다. 비슷한 주제를 다루는 장들은 서로 모아 두었습니다. 이 책을 읽는 방법은 크게 두 가지가 있습니다.
- 특정한 질문에 관한 답을 찾는다면 그 질문을 다루는 장과 그와 연결된 장을 함께 읽으세요.
- 장고 ORM과 모델 계층에 대한 이해도를 높이고 싶다면 책을 처음부터 차례대로 읽으세요.
나는 2번 케이스에 해당하기 때문에, 앞으로 이 책을 처음부터 차근차근 읽으며 공식 문서의 내용도 조금 곁들여 정리해보고자 한다.
오늘은 '정보를 조회하고 필요한 항목을 선별하는 방법' 챕터의 1번부터 6번까지 정리할 것이다.
참고로 아래에 있는 예시들은 DRF 튜토리얼의 6번까지 완료했을 때의 소스 코드를 가지고 진행했다.
요즘 SQLD를 공부하고 있다. 그렇다 보니, Django의 모델이 DBMS에 어떤 쿼리를 날리는지 궁금했었다. 이 주제는 이 궁금증을 해결하기 아주 좋은 주제였다.
답은 간단하다. QuerySet 인스턴스에는 query 속성이 있는데, 이 속성을 str 함수에 넣으면 된다.
>>> qs = Snippet.objects.all()
>>> qs.query
<django.db.models.sql.query.Query object at 0x00000230EEA275C0>
>>> str(qs.query)
'SELECT "snippets_snippet"."id", "snippets_snippet"."created", "snippets_snippet"."title", "snippets_snippet"."code", "snippets_snippet"."linenos", "snippets_snippet"."language", "snippets_snippet"."style", "snippets_snippet"."owner_id", "snippets_snippet"."highlighted" FROM "snippets_snippet" ORDER BY "snippets_snippet"."created" ASC'
query 속성은 django.db.models.sql.query.Query라는 객체의 인스턴스로 나타난다. 공식 문서에 따르면 opaque한 객체로, 쿼리 구성의 내부 구조를 나타내며 퍼블릭 API의 한 부분이 아니라고 한다.WHERE)>>> qs = Snippet.objects.filter(id__lt=5)
>>> str(qs.query)
'SELECT "snippets_snippet"."id", "snippets_snippet"."created", "snippets_snippet"."title", "snippets_snippet"."code", "snippets_snippet"."linenos", "snippets_snippet"."language", "snippets_snippet"."style", "snippets_snippet"."owner_id", "snippets_snippet"."highlighted" FROM "snippets_snippet" WHERE "snippets_snippet"."id" < 5 ORDER BY "snippets_snippet"."created" ASC'
id가 5 미만인 Snippet들을 조회해봤다. filter 메소드에 지정한 필터링 조건이 SQL의 WHERE절에 작성(WHERE "snippets_snippet"."id" < 5)되어 있는 것을 볼 수 있다.원래 책에는 각각이 하나의 주제로 되어 있는데, 맥락이 비슷해서 그냥 묶어서 정리할 것이다.
Q 객체후술할 내용에는 모두 Q 객체가 등장한다. Q 객체는 SQL 쿼리의 조건을 표현하는 객체이다. 즉, SQL 쿼리에서 WHERE절에 해당한다고 보면 된다. 자주 사용하는 조건이 있다면, Q 인스턴스를 만들어 두고, 재사용하면 효율적이다.
Q 인스턴스를 생성할 때 Lookup Expression에 해당하는 내용을 인자로 주어 생성한다. 아래의 예시를 보면 감이 올 것이다.
그리고 Q 객체에 대해서 ~ (NOT), | (OR), & (AND), ^ (XOR) 연산자를 사용할 수 있다.
|queryset_1 | queryset_2QuerySet을 SQL의 OR(|) 연산을 이용해 합치는 방법이다.filter(Q(<condition_1>)|Q(<condition_2>))Q 객체에 대해 OR 연산자를 이용해서 각 조건을 합치고, 그 조건을 filter의 인자로 전달하는 방법이다.QuerySet 합치기 예시>>> qs1 = Snippet.objects.filter(id__lt=3)
>>> qs1
<QuerySet [<Snippet: Snippet object (1)>, <Snippet: Snippet object (2)>]>
>>> qs2 = Snippet.objects.filter(id__gt=9)
>>> qs2
<QuerySet [<Snippet: Snippet object (10)>, <Snippet: Snippet object (11)>]>
>>> q1 | q2
<QuerySet [<Snippet: Snippet object (1)>, <Snippet: Snippet object (2)>, <Snippet: Snippet object (10)>, <Snippet: Snippet object (11)>]>
id 3 미만 조건으로 조회한 QuerySet과 id 9 초과 조건으로 조회한 QuerySet을 OR 연산으로 합쳤다.Q 객체 예시>>> from django.db.models import Q
>>> q1 = Q(id__lt=3)
>>> q1
<Q: (AND: ('id__lt', 3))>
>>> q2 = Q(id__gt=9)
>>> q2
<Q: (AND: ('id__gt', 9))>
>>> Snippet.objects.filter(q1|q2)
<QuerySet [<Snippet: Snippet object (1)>, <Snippet: Snippet object (2)>, <Snippet: Snippet object (10)>, <Snippet: Snippet object (11)>]>
>>> qs = qs1 | qs2
>>> qobj = Snippet.objects.filter(q1|q2)
>>> str(qs.query) == str(qobj.query)
True
>>> str(qs.query)
'SELECT "snippets_snippet"."id", "snippets_snippet"."created", "snippets_snippet"."title", "snippets_snippet"."code", "snippets_snippet"."linenos", "snippets_snippet"."language", "snippets_snippet"."style", "snippets_snippet"."owner_id", "snippets_snippet"."highlighted" FROM "snippets_snippet" WHERE ("snippets_snippet"."id" < 3 OR "snippets_snippet"."id" > 9) ORDER BY "snippets_snippet"."created" ASC'
str(qs.query) == str(qobj.query)의 결과가 True이므로, 둘은 같은 SQL 쿼리를 구성한다는 것을 알 수 있다.|로 합친 각 조건이 WHERE절에서 OR로 연결되고 있는 것을 볼 수 있다.&filter(<condition_1>, <condition_2>)filter의 각 Lookup Expression들은 AND로 연결된다.queryset_1 & queryset_2filter(Q(<condition_1>) & Q(<condition_2>))QuerySet과 Q 객체는 이미 OR의 예시가 있으니, 여기서는 filter에 Lookup Expression들을 키워드 인자로 주는 예시에 대해서만 다루고자 한다.
filter 예시>>> qs = Snippet.objects.filter(id__lt=4, linenos__exact=True)
>>> qs
qs
<QuerySet [<Snippet: Snippet object (1)>]>
id가 4 초과 조건 및 linenos 속성이 True인 조건을 모두 만족해야 한다.>>> str(qs.query)
'SELECT "snippets_snippet"."id", "snippets_snippet"."created", "snippets_snippet"."title", "snippets_snippet"."code", "snippets_snippet"."linenos", "snippets_snippet"."language", "snippets_snippet"."style", "snippets_snippet"."owner_id", "snippets_snippet"."highlighted" FROM "snippets_snippet" WHERE ("snippets_snippet"."id" < 4 AND "snippets_snippet"."linenos") ORDER BY "snippets_snippet"."created" ASC'
WHERE절에서 각 조건이 AND로 연결된 것을 볼 수 있다.~exclude(<condition>)filter(~Q(<condition>))Q에 ~ 연산자를 사용해서 Q에 해당하지 않는~ 이라는 의미로 쓸 수 있다.⚠️ QuerySet에 쓸 수 있는 연산자들을 보면 알 수 있듯, QuerySet에는 ~를 쓸 수 없다!
filter는 앞에 예시가 있으니, exclude만 살펴보자.
exclude 예시>>> q = Snippet.objects.exclude(linenos__exact=True)
>>> q
<QuerySet [<Snippet: Snippet object (2)>, <Snippet: Snippet object (3)>]>
linenos 속성이 True가 아닌 인스턴스만을 조회한다.>>> str(q.query)
'SELECT "snippets_snippet"."id", "snippets_snippet"."created", "snippets_snippet"."title", "snippets_snippet"."code", "snippets_snippet"."linenos", "snippets_snippet"."language", "snippets_snippet"."style", "snippets_snippet"."owner_id", "snippets_snippet"."highlighted" FROM "snippets_snippet" WHERE NOT ("snippets_snippet"."linenos") ORDER BY "snippets_snippet"."created" ASC'
WHERE절에서 NOT으로 조건을 부정하고 있는 것을 볼 수 있다.UNION)QuerySet의 union 메소드를 이용한다. 이때, QuerySet의 필드와 데이터 유형이 서로 맞아야 한다. 책에 이렇게 적혀 있는데 SQL에서 UNION할 때의 제약 조건을 생각하면 될 것 같다. 또, 공식 문서에 따르면, union, intersection, difference와 같은 메소드들은 첫 번째 QuerySet 타입을 반환한다고 한다. 쉽게 말하면, A 모델과 B 모델의 QuerySet을 합친다면, A 모델을 기준으로 합쳐진다는 것이다.이 주제에 한해서 책에 있는 예시를 쓰겠다. DRF 튜토리얼의 코드는 메타 클래스에 ordering 속성이 있는데, 이 때문에 union 메소드를 사용하면 django.db.utils.DatabaseError: ORDER BY not allowed in subqueries of compound statements. 오류가 발생한다.
예시에서 사용하는 User 테이블의 데이터는 아래와 같다.

(출처: 2. OR 연산으로 일부 조건을 하나라도 만족하는 항목을 구하려면 어떻게 하나요?)
>>> q1 = User.objects.filter(id__gte=5)
>>> q1
<QuerySet [<User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>, <User: rishab>]>
>>> q2 = User.objects.filter(id__lte=9)
>>> q2
<QuerySet [<User: yash>, <User: John>, <User: Ricky>, <User: sharukh>, <User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>]>
>>> q1.union(q2)
<QuerySet [<User: yash>, <User: John>, <User: Ricky>, <User: sharukh>, <User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>, <User: rishab>]>
>>> q2.union(q1)
<QuerySet [<User: yash>, <User: John>, <User: Ricky>, <User: sharukh>, <User: Ritesh>, <User: Billy>, <User: Radha>, <User: sohan>, <User: Raghu>, <User: rishab>]>
id가 5 이상인 User 인스턴스들의 쿼리셋과 id가 9 이하인 User 인스턴스들의 쿼리셋을 union 메소드로 합치는 예시이다.q1.union(q2)와 q2.union(q1)의 결과가 같음을 볼 수 있다.>>> q3 = EventVillain.objects.all()
>>> q3
<QuerySet [<EventVillain: EventVillain object (1)>]>
>>> q1.union(q3)
django.db.utils.OperationalError: SELECTs to the left and right of UNION do not have the same number of result columns
>>> Hero.objects.all().values_list(
"name", "gender"
).union(
Villain.objects.all().values_list(
"name", "gender"
))
values_list는 4번 주제에서도 설명하겠지만, positional arguemnts로 필드들을 받아 해당 필드의 값들을 튜플 형태로 뽑아내는 메소드이다.Hero와 Villain은 서로 다른 모델이지만 name과 gender라는 공통된 속성을 같은 순서로 뽑아서 union한다.SELECT절 컬럼 지정)QuerySet의 values 메소드, values_list 메소드를 이용한다.QuerySet을 반환하는데, 이 QuerySet을 순회할 때 각 요소가 어떤 타입인지가 다르다.values 메소드를 사용했을 때에는 딕셔너리 타입({필드1: 필드1의 값, 필드2: 필드2의 값, ...})이고, values_list 메소드를 사용했을 때에는 튜플 타입((필드1의 값, 필드2의 값, 필드3의 값, ...))이다.QuerySet의 only 메소드를 이용한다. 이 메소드 역시 QuerySet을 반환하는데, 위의 두 메소드와는 다르게 순회할 때 각 요소가 모델 인스턴스이다.only 메소드는 defer 메소드와 비슷한 맥락인데, only의 대상이 아닌 필드나, defer의 대상인 필드의 값은 필드에 접근할 때 데이터베이스에서 가져오게 된다.values 메소드와 values_list 메소드 예시>>> qs_values = Snippet.objects.values('title', 'language')
>>> qs_values
<QuerySet [{'title': 'test', 'language': 'python'}, {'title': 'Test 2', 'language': 'javascript'}, {'title': 'Test 3', 'language': 'javascript'}, {'title': 'Test 5', 'language': 'javascript'}, {'title': '', 'language': 'python'}, {'title': '', 'language': 'python'}, {'title': '', 'language': 'python'}, {'title': '', 'language': 'python'}, {'title': '', 'language': 'python'}, {'title': '', 'language': 'python'}, {'title': '', 'language': 'python'}]>
>>> qs_values_list = Snippet.objects.values_list('title', 'language')
>>> qs_values_list
<QuerySet [('test', 'python'), ('Test 2', 'javascript'), ('Test 3', 'javascript'), ('Test 5', 'javascript'), ('', 'python'), ('', 'python'), ('', 'python'), ('', 'python'), ('', 'python'), ('', 'python'), ('', 'python')]>
only 메소드>>> qs_only = Snippet.objects.only('title', 'language')
>>> qs_only
<QuerySet [<Snippet: Snippet object (1)>, <Snippet: Snippet object (2)>, <Snippet: Snippet object (3)>, <Snippet: Snippet object (4)>, <Snippet: Snippet object (5)>, <Snippet: Snippet object (6)>, <Snippet: Snippet object (7)>, <Snippet: Snippet object (8)>, <Snippet: Snippet object (9)>, <Snippet: Snippet object (10)>, <Snippet: Snippet object (11)>]>
전자의 방법과 후자의 방법은 차이가 있다.
>>> str(qs_values.query) == str(qs_values_list.query)
True
>>> str(qs_values.query) == str(qs_only.query)
False
>>> str(qs_values.query)
'SELECT "snippets_snippet"."title" AS "title", "snippets_snippet"."language" AS "language" FROM "snippets_snippet" ORDER BY "snippets_snippet"."created" ASC'
>>> str(qs_only.query)
'SELECT "snippets_snippet"."id", "snippets_snippet"."title", "snippets_snippet"."language" FROM "snippets_snippet" ORDER BY "snippets_snippet"."created" ASC'
str(qs_values.query) == str(qs_values_list.query)가 True이므로 전자의 두 메소드는 같은 SQL문을 만들어냄을 볼 수 있다.str(qs_values.query) == str(qs_only.query)는 False이다. 즉, 두 방법이 만들어 내는 SQL문에는 차이가 있다.id 필드가 포함되지 않지만, 후자의 경우 SQL문에 id 필드가 포함되어 있다는 것이다.왜 후자의 경우 SQL문에 id가 포함되어 있을까? 후자의 경우 QuerySet을 순회하면 모델 인스턴스에 대해 순회하기 때문에, 이를 위해 모델의 pk인 id를 같이 조회하는 것으로 보인다.
오늘은 장고 ORM 요리책 (Django ORM Cookbook)의 첫번째 챕터에서 1번 주제부터 6번 주제까지를 정리해 보았다. 조금 읽었음에도, 내가 몰랐던 메소드나 활용법이 보여서 흥미로웠다. 그런 점에서 앞으로 이 책은 확실히 나에게 Django의 모델을 이해함에 있어서 큰 도움을 줄 것 같다. 해커톤 일정 때문에 좀 바쁘긴 하지만, 꼭 끝까지 읽어보고 싶다.
원래 저녁 8시쯤 블로그 글을 쓰기 시작하는데, 1일 1TIL을 하고 싶지만 오늘은 도저히 글을 쓸 내용이 없어서 이것저것 찾다가 보니, 11시가 되어서야 정리가 끝났다. 그 후, 1시간 정도 쉬고 나서 글을 쓰니 어김없이 새벽 3시가 지나고 말았다. 시간은 왜 이리 빠른걸까?
그리고 글을 쓸 때, 조금 덜 심심하게끔 항상 헤더 2(##, 나는 헤더 1을 안 쓰고 헤더 2를 큰 제목 개념으로 쓴다)에 Win + .으로 이모티콘을 넣고 있다. 그런데 너무 이모티콘이 다양하지 못한 것 같다. 다른 좋은 방법이 없으려나..
참고로, 장고 ORM 요리책은 제목의 순서를 이어서 쓰고자 한다. 다시 말해, 다음 글에서는 1번이 아닌 5번부터 시작해서 이어나갈 것이다.