오늘도 개발
[2차 프로젝트 내일의 집] 포스트 목록 api - Django ORM 최적화하기 본문
포스트 목록을 보내주는 api는 1차 때도 만들었지만,
2차 때는 Django ORM 최적화까지 해보기로 했다.
포스트 목록 api는 클라이언트가 요청을 보내면 '포스트 대표 이미지, 작성자, 포스트 속 첫번째 사진의 설명'을 보내주어야 한다.
포스트 대표 이미지는 posts 테이블에, 작성자는 users 테이블에, 포스트 속 첫번째 사진의 설명은 photos 테이블에 들어있다.
posts 기준으로 세 테이블의 정보를 가져오면서 매번 DB를 호출하지 않도록 코드를 짜는 것이 목적이다.
0. 기본 세팅
<models.py>
User-Post
한 유저는 여러 포스트를 작성할 수 있으므로 유저와 포스트는 일대다 관계이다.
Post가 User를 정참조한다. (필드 user)
Post-Photo
한 포스트에는 여러 사진이 들어갈 수 있으므로 포스트와 사진은 일대다 관계이다.
Post가 Photo를 역참조한다. (필드 post)
class Post(TimeStampModel):
cover_image = models.URLField(max_length=300)
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Photo(models.Model):
description = models.TextField()
url = models.URLField(max_length=500)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
class User(models.Model):
kakao_id = models.CharField(max_length=200, unique=True)
nickname = models.CharField(max_length=50, null=True)
<views.py>
10평 미만, 홈스타일링 값을 갖는 포스트만 보내준다고 가정한다.
아래 쿼리를 만족하는 posts는 총 3개 존재한다.
posts = Post.objects.filter(Q(room_size="10평 미만")&Q(work_type="홈스타일링"))
for post in posts:
print(post.id)
print(post.cover_image)
# 일단은 모든 photo 오브젝트를 보여준다고 가정
print(post.photo_set.all())
print(post.user.id)
print(post.user.nickname)
1. 최적화를 하지 않는 경우
총 7번 DB를 호출한다.
이유는 다음과 같다.
for문 시작 : 1) 실행 (posts 테이블 선택해서 캐싱)
첫번째 post 오브젝트 : 2) 실행(photos 테이블 선택), 3) 실행(users 테이블 선택)
두번째 post 오브젝트 : 2) 실행(photos 테이블 선택), 3) 실행(users 테이블 선택)
세번째 post 오브젝트 : 2) 실행(photos 테이블 선택), 3) 실행(users 테이블 선택)
posts = Post.objects.filter(Q(room_size="10평 미만")&Q(work_type="홈스타일링"))
# DB 호출 없음, 1번 SQL 쿼리 준비
for post in posts:
# 1) DB 호출 후 1번 SQL 쿼리 실행
# posts 테이블 캐싱
print(post.id)
print(post.cover_image)
print(post.photo_set.all()[0].description)
# 2) DB 호출 후 2번 SQL 쿼리 생성해서 실행
# photos 테이블 캐싱
print(post.user.id)
# 3) DB 호출 후 3번 SQL 쿼리 생성해서 실행
# user 테이블 캐싱
print(post.user.nickname)
1) 1번 SQL 쿼리(posts 테이블 선택해서 캐싱)
SELECT `posts`.`id`, `posts`.`created_at`, `posts`.`updated_at`, `posts`.`title`, `posts`.`content`, `posts`.`cover_image`, `posts`.`living_type`, `posts`.`room_size`, `posts`.`family_type`, `posts`.`work_type`, `posts`.`worker_type`, `posts`.`user_id`
FROM `posts`
WHERE (`posts`.`room_size` = '10평 미만' AND `posts`.`work_type` = '홈스타일링');
2) 2번 SQL 쿼리(users 테이블 선택해서 캐싱)
SELECT `photos`.`id`, `photos`.`description`, `photos`.`url`, `photos`.`post_id`
FROM `photos`
WHERE `photos`.`post_id` = 1 ORDER BY `photos`.`id`;
3) 3번 SQL 쿼리(photos 테이블 선택해서 캐싱)
SELECT `users`.`id`, `users`.`created_at`, `users`.`updated_at`, `users`.`kakao_id`, `users`.`email`, `users`.`nickname`, `users`.`profile_image`
FROM `users`
WHERE `users`.`id` = 1;
2. select_related()만 사용하는 경우
posts 테이블은 users 테이블을 정참조하므로 select_related()를 사용할 수 있다.
select_related()를 사용했을 때 총 4번 DB를 호출한다.
for문 시작 : 1) 실행 (posts 테이블과 users 테이블 조인해서 캐싱)
첫번째 post 오브젝트 : 2) 실행(photos 테이블 쿼리)
두번째 post 오브젝트 : 2) 실행(photos 테이블 쿼리)
세번째 post 오브젝트 : 2) 실행(photos 테이블 쿼리)
posts = Post.objects.filter(Q(room_size="10평 미만")&Q(work_type="홈스타일링")).select_related('user')
# DB 호출 없음, 1번 SQL 쿼리 준비
for post in posts:
# 1) DB 호출 후 1번 SQL 쿼리 실행
# posts와 users를 조인한 테이블 캐싱
print(post.id)
print(post.cover_image)
print(post.photo_set.all()[0].description)
# 2) DB 호출 후 2번 SQL 쿼리 생성해서 실행
# photos 테이블 캐싱
print(post.user.id)
print(post.user.nickname)
1) 1번 SQL 쿼리(posts 테이블과 users 테이블을 join해서 가져온 후 캐싱)
SELECT `posts`.`id`, `posts`.`created_at`, `posts`.`updated_at`, `posts`.`title`, `posts`.`content`, `posts`.`cover_image`, `posts`.`living_type`, `posts`.`room_size`, `posts`.`family_type`, `posts`.`work_type`, `posts`.`worker_type`, `posts`.`user_id`, `users`.`id`, `users`.`created_at`, `users`.`updated_at`, `users`.`kakao_id`, `users`.`email`, `users`.`nickname`, `users`.`profile_image`
FROM `posts` INNER JOIN `users`
ON (`posts`.`user_id` = `users`.`id`)
WHERE (`posts`.`room_size` = '10평 미만' AND `posts`.`work_type` = '홈스타일링');
2) 2번 SQL 쿼리(photos 테이블 선택해서 캐싱)
SELECT `photos`.`id`, `photos`.`description`, `photos`.`url`, `photos`.`post_id`
FROM `photos` WHERE `photos`.`post_id` = 1
ORDER BY `photos`.`id` ASC LIMIT 1;
3. prefetch_related()도 사용하는 경우
posts 테이블은 photos 테이블을 역참조하므로 prefetch_related()를 사용할 수 있다.
select_related()와 prefetch_related()를 같이 사용하면 총 2번만 DB를 호출한다.
for문 시작 : 1) 실행(posts 테이블과 users 테이블 조인해서 캐싱), 2) 실행(photos 테이블 캐싱)
첫번째 post 오브젝트 : posts, users, phots 모두 캐싱된 데이터를 가져옴
두번째 post 오브젝트 : posts, users, phots 모두 캐싱된 데이터를 가져옴
세번째 post 오브젝트 : posts, users, phots 모두 캐싱된 데이터를 가져옴
posts = Post.objects.filter(Q(room_size="10평 미만")&Q(work_type="홈스타일링")).select_related('user').prefetch_related('photo_set')
# DB 호출 없음, 1번과 2번 SQL 쿼리 준비
for post in posts:
# 1) DB 호출 후 1번 SQL 쿼리 실행 => posts와 users를 조인한 테이블 캐싱
# 2) DB 호출 후 1번 SQL 쿼리 실행 => photos 테이블 캐싱
print(post.id)
print(post.cover_image)
print(post.photo_set.all()[0].description)
print(post.user.id)
print(post.user.nickname)
1) 1번 SQL 쿼리(posts 테이블과 users 테이블을 join해서 가져온 후 캐싱)
SELECT `posts`.`id`, `posts`.`created_at`, `posts`.`updated_at`, `posts`.`title`, `posts`.`content`, `posts`.`cover_image`, `posts`.`living_type`, `posts`.`room_size`, `posts`.`family_type`, `posts`.`work_type`, `posts`.`worker_type`, `posts`.`user_id`, `users`.`id`, `users`.`created_at`, `users`.`updated_at`, `users`.`kakao_id`, `users`.`email`, `users`.`nickname`, `users`.`profile_image`
FROM `posts` INNER JOIN `users`
ON (`posts`.`user_id` = `users`.`id`)
WHERE (`posts`.`room_size` = '10평 미만' AND `posts`.`work_type` = '홈스타일링');
2) 2번 SQL 쿼리(photos 테이블 캐싱)
SELECT `photos`.`id`, `photos`.`description`, `photos`.`url`, `photos`.`post_id`
FROM `photos`
WHERE `photos`.`post_id` IN (1, 11, 21);
'TIL & 프로젝트 회고' 카테고리의 다른 글
| [2차 프로젝트 내일의 집] AWS s3로 글쓰기 api 구현하기 - 1. AWS 세팅(IAM 설정, S3 설정) (0) | 2022.08.08 |
|---|---|
| [2차 프로젝트 내일의 집] 상품 상세 api - Django ORM 최적화하기 심화 (0) | 2022.08.08 |
| [2차 프로젝트 내일의 집] SNS 로그인/회원가입 api에 Unit Test 해보기 (0) | 2022.08.05 |
| [2차 프로젝트 내일의 집] get_or_create 사용하기 (0) | 2022.08.03 |
| [2차 프로젝트 내일의 집] 카카오 api로 SNS 로그인/회원가입 구현하기 (0) | 2022.08.03 |