Chapter 05. 장고의 Queryset 1편 (Create와 Retrieve 기초)
models.py에서 데이터 모델을 만들면 *데이터 객체를 만들고(create) 검색하고(retrieve) 수정하고(update) 삭제하는(delete) DB추상화 API를 자동적으로 사용할 수 있다. 데이터 객체의 CRUD(Create, Retrieve(Read), Update, Delete)는 Django ORM에서 제공하는 Queryset 자료형(Data Type)을 이용한다. Queryset은 DB에서 전달받은 객체의 목록으로, 구조는 list와 비슷하지만 파이썬의 기본 자료형이 아니므로 읽기 위해선 형변환이 필요하다.
* 데이터 객체는 DB의 레코드(record) 혹은 행(row)라고 생각하면 편하다.
데이터 객체 만들기(Creating objects)
데이터 객체를 만들기 위해서는 모델 클래스를 호출할 때 키워드 인수(keyword arguments, kwargs)를 이용해 인스턴스를 만들고 그 인스턴스를 호출하고 save()메소드로 저장한다.
django shell을 이용하면 다음과 같다
from users.models import User
new_user = User(
name="홍길동",
age=27,
course_credit=2.4,
introduce="안녕하세요. 저는 홍길동입니다.")
new_user.save()
User.objects.values().get(name="홍길동")
{'superduper_id': 2, 'name': '홍길동', 'age': 27, 'course_credit': 2.4, 'email_check': None, 'introduce': '안녕하세요. 저는 홍길동입니다.'}
이전 model글에서 만든 User모델을 그대로 import했다.
User모델 클래스를 호출하면서 키워드 인수에 모델을 만들 때 추가해줬던 속성들을 넣어준 new_user 인스턴스를 만들었다. save() 메소드로 저장 명령을 하기 전까지 아직 DB를 건드리지 않은 상태이며 save() 명령을 하면 그제서야 장고가 SQL의 INSERT명령어를 실행해 DB에 인스턴스의 내용을 저장하게 된다.
그러므로 save() 메소드를 실행하기 전에 인스턴스의 속성을 변경하고 저장할 수 있다.
아래와 같다
new_user.name = "김춘봉"
new_user.save()
User.objects.values().get(name="김춘봉")
{'superduper_id': 2, 'name': '김춘봉', 'age': 27, 'course_credit': 2.4, 'email_check': None, 'introduce': '안녕하세요. 저는 홍길동입니다.'}
위에서 생성한 new_user 인스턴스의 name 속성을 김춘봉으로 바꾸고 다시 save()했다. 결과적으로 위와 같이 DB의 레코드를 변경할 수도 있다.
데이터 객체 조회하기(Retrieving objects)
데이터 객체를 조회하기 위해선 Manager를 통해 QuerySet을 만들어야 한다.
장고에서 QuerySet Manager는 기본적으로 objects라는 이름을 갖고 있다. 하지만 objects라는 이름을 필드 이름으로 쓰고 싶거나 이름을 바꾸고 싶으면 바꿀 수 있다. 자세한 내용은 여기를 참조
Manager는 Table수준 작업과 Record수준 작업을 구분하기 위해 Model class를 통해서만 접근할 수 있다. Model의 인스턴스로는 접근할 수 없다. 아래의 예제를 보자
from users.models import User
User.objects
# 결과: <django.db.models.manager.Manager object at 0x00000272D863BA10>
user = User(name="Nancy")
user
# 결과: <User: Nancy>
user.objects
# 결과 : AttributeError: Manager isn't accessible via User instances
결과를 보면 User 클래스에 objects를 호출했을 때는 정상적으로 Manager객체가 출력됐다.
하지만 User클래스의 인스턴스인 user에 objects를 호출했을 때는 AttributeError가 발생한 것을 알 수 있다
모든 객체 조회하기
- all() : DB 내 모든 객체의 QuerySet을 리턴한다
User.objects.all()
# 결과 : <QuerySet [<User: Nancy>, <User: Ganet>]>
Filter를 이용해 특정 객체 조회하기
- filter(**kwargs) : 주어진 조회 매개변수를 만족하는 QuerySet을 리턴한다
User.objects.filter(name="Ganet")
# 결과 : <QuerySet [<User: Ganet>]>
위의 결과는 User.objects.all().filter(name="Ganet")과 동일하다
- exclude(**kwargs) : 주어진 조회 매개변수를 제외한 QuerySet을 리턴한다
User.objects.exclude(name="Ganet")
# 결과: <QuerySet [<User: Nancy>, <User: John>]>
*필터체인(Chaining filters)
filter한 결과는 QuerySet이므로 QuerySet에 추가적으로 반복해서 조회하는 것이 가능하다
User.objects.exclude(name="Ganet").filter(age__gte=20)
# 결과 : <QuerySet [<User: Nancy>]>
두번 이상 더 추가적으로 조회할 수도 있다
*QuerySet은 lazy하다
QuerySet은 lazy loading방식으로 실행된다. lazy loading은 실제로 필요하기 전까지는 아무런 행동을 하지 않는다. 즉, 실제로 필요하기 전까지는 filter를 몇번 걸든 실행(run)되지 않는다. 아래의 예제를 보자
user = User.objects.exclude(name="Ganet")
user = user.filter(age__gte=20)
print(user)
# 결과 : <QuerySet [<User: Nancy>]>
위의 필터체인과 같은 예제지만 user인스턴스에 반복적으로 filter를 하는 식으로 바꾸었다. 그렇다면 첫행과 두번째행은 실행되는 게 아니냐? 하겠지만, 사실 print(user)로 실제 QuerySet을 요청하기 전까지 django는 아무런 행동도 하지 않는다. print(user)까지 와서야 비로소 QuerySet이 필요하기 때문에 QuerySet을 생성한다
이는 불필요한 쿼리 비용이 낭비되는 것을 방지하기 위한 긍정적인 면을 가지고 있다. 하지만 이 특성이 장고ORM의 대표적인 단점이 되기도 한다.
N+1 Query문제
lazy-loading의 대표적인 성능 이슈로서 외래키(Foreign Key)를 참조해 데이터를 가져올 때 주로 발생한다.
아래를 보자
from articles.models import Article
all_article = Article.objects.all()
for article in all_article:
article.user.name
# 결과 : 'Ganet'
# 결과 : 'Ganet'
# 결과 : 'John'
# 결과 : 'Nancy'
# 결과 : 'John'
# 결과 : 'Nancy'
Article모델은 User모델을 ForeignKey로 참조하는 관계다.
위는 Article 모델의 모든 article들을 불러온 뒤 all_article에 넣어주고 for문을 통해 article을 쓴 user의 name을 반복적으로 가져오도록 했다. 결과는 총 6개의 article의 작성자 이름이 잘 리턴되었다.
이때 실행된 SQL 쿼리문을 보자
from django.db import connection
print(connection.queries)
[
{'sql': 'SELECT "articles_article"."id", "articles_article"."user_id", "articles_article"."title", "articles_article"."content", "articles_article"."created_at", "articles_article"."updated_at" FROM "articles_article"', 'time': '0.000'},
{'sql': 'SELECT "user"."superduper_id", "user"."name", "user"."age", "user"."course_credit", "user"."email_check", "user"."introduce" FROM "user" WHERE "user"."superduper_id" = 2 LIMIT 21', 'time': '0.000'},
{'sql': 'SELECT "user"."superduper_id", "user"."name", "user"."age", "user"."course_credit", "user"."email_check", "user"."introduce" FROM "user" WHERE "user"."superduper_id" = 2 LIMIT 21', 'time': '0.000'},
{'sql': 'SELECT "user"."superduper_id", "user"."name", "user"."age", "user"."course_credit", "user"."email_check", "user"."introduce" FROM "user" WHERE "user"."superduper_id" = 3 LIMIT 21', 'time': '0.000'},
{'sql': 'SELECT "user"."superduper_id", "user"."name", "user"."age", "user"."course_credit", "user"."email_check", "user"."introduce" FROM "user" WHERE "user"."superduper_id" = 1 LIMIT 21', 'time': '0.000'},
{'sql': 'SELECT "user"."superduper_id", "user"."name", "user"."age", "user"."course_credit", "user"."email_check", "user"."introduce" FROM "user" WHERE "user"."superduper_id" = 3 LIMIT 21', 'time': '0.000'},
{'sql': 'SELECT "user"."superduper_id", "user"."name", "user"."age", "user"."course_credit", "user"."email_check", "user"."introduce" FROM "user" WHERE "user"."superduper_id" = 1 LIMIT 21', 'time': '0.000'}
]
총 7번의 쿼리문이 실행되었다. 쿼리문을 자세히 보면 Article클래스의 articles_article 테이블로부터 한번 실행된 뒤 User클래스의 user테이블로부터 6번 실행되었다. SQL에서 JOIN을 하면 하나의 쿼리문으로 충분한 결과가 6 + 1번 반복됐다.
이것이 장고ORM의 N + 1 query 문제다.
지금처럼 호출하는 횟수가 적다면 큰 문제가 없겠지만 for문을 10000번, 100000번 반복한다면 데이터베이스에 부하를 줄 수 있다.
그렇다면 lazy loading의 해결책은 무엇일까
바로 select_related와 prepatch_related다
- select_related("table") : 셀렉트할 객체가 역참조하는 single object(one-to-one 혹은 many-to-one)이거나, 정참조 Foreign Key일 때 사용한다
위에서 작성한 코드를 다음과 같이 바꿔보자.
from articles.models import Article
all_article = Article.objects.select_related('user')
for article in all_article:
print(article.user.name)
# 결과 : 'Ganet'
# 결과 : 'Ganet'
# 결과 : 'John'
# 결과 : 'Nancy'
# 결과 : 'John'
# 결과 : 'Nancy'
결과를 보면 위에 작성한 Article.objects.all()의 결과와 같다. 그렇다면 호출된 SQL 쿼리문을 보자
from django.db import connection
print(connection.queries)
[
{'sql': 'SELECT "articles_article"."id", "articles_article"."user_id", "articles_article"."title", "articles_article"."content", "articles_article"."created_at", "articles_article"."updated_at", "user"."superduper_id", "user"."name", "user"."age", "user"."course_credit", "user"."email_check", "user"."introduce" FROM "articles_article" INNER JOIN "user" ON ("articles_article"."user_id" = "user"."superduper_id")', 'time': '0.000'}
]
보다시피 단 한번의 쿼리문이 호출됐으며 SQL 쿼리문을 자세히 보면 INNER JOIN을 실행했다.
즉, select_related는 각각의 lookup마다 SQL의 JOIN을 실행해 테이블의 일부를 가져오고 SELECT FROM으로 관련된 필드를 가져온다.
- prefetch_related("table") : 구하려는 객체가 정참조 multifle objects(many-to-many 혹은 one-to-many)거나, 역참조 Foreign Key일 때 사용한다
아까와 같이 위의 코드를 바꿔서 적용해보자
from articles.models import Article
all_article = Article.objects.prefetch_related('user')
for article in all_article:
print(article.user.name)
# 결과 : 'Ganet'
# 결과 : 'Ganet'
# 결과 : 'John'
# 결과 : 'Nancy'
# 결과 : 'John'
# 결과 : 'Nancy'
결과는 역시 같다. 그렇다면 SQL 쿼리문을 살펴보자
from django.db import connection
print(connection.queries)
[
{'sql': 'SELECT "articles_article"."id", "articles_article"."user_id", "articles_article"."title", "articles_article"."content", "articles_article"."created_at", "articles_article"."updated_at" FROM "articles_article"', 'time': '0.000'},
{'sql': 'SELECT "user"."superduper_id", "user"."name", "user"."age", "user"."course_credit", "user"."email_check", "user"."introduce" FROM "user" WHERE "user"."superduper_id" IN (1, 2, 3)', 'time': '0.000'}
]
prefetch_related를 적용한 결과는 JOIN이 발생하지 않고 각 테이블 별로 쿼리를 실행해 두번의 쿼리문이 호출된 것을 알 수 있다. 공식문서에 따르면 prefetch_related에서 join은 SQL level에서가 아닌 파이썬 level에서 이루어진다고 한다. 즉, 두번 호출된 쿼리를 파이썬에서 join해서 결과를 만들어낸다는 것이다.
그 외의 추가적인 차이나 자세한 사용법은 따로 정리하도록 하겠다.
'프로그래밍 > Python' 카테고리의 다른 글
[DRF] dj-rest-auth - email인증 및 User모델 필드 커스텀하기 (2) | 2023.05.16 |
---|---|
[Django] 장고 기초 - (5) Query Set(2) (0) | 2023.04.27 |
[Django] 장고 기초 - (3) Models (0) | 2023.04.20 |
[Django] 장고 기초 - (2) Template Language (0) | 2023.04.17 |
[Django] 장고 기초 - (1) 개요 및 개발 환경 구성 (0) | 2023.04.14 |