oz_externship - CRUD learning

김기훈·2025년 12월 19일

부트캠프 프로젝트

목록 보기
24/39

Create 🔴

질문등록api

  • 응답은 "question_id"만 내려주기
    • 응답은 무엇을 만들었는가 / 성공했는가 가 중요 등록api의 응답은 아이디값 하나로 충분
  • 응답을 풍부하게 줄 경우
    • 책임이 섞인다 (Single Responsibility 위반)
      • 현재 api명세서에 나온 응답 스키마는 사실상 상세 조회 응답급
      • Create API가 Read API의 책임까지 떠안는 구조
    • 중복 데이터 & 유지보수 비용 증가
      • 등록 응답 / 상세 조회 응답 둘 다에 같은 필드들이 중복된다.
      • 나중에 필드 하나 바뀌면?
        • 등록 API 수정 / 상세 조회 API 수정 / 테스트 2배 / 프론트 파싱 로직도 2군데
  • ID만 주는 게 왜 유용한 이유
    • API 역할이 명확해진다
      • POST /questions = 생성 | GET /questions/{id} = 조회
    • 프론트 흐름이 자연스럽다
질문 등록
 → question_id 수신
 → 상세 페이지로 이동
 → 상세 조회 API 호출

List 🔴


카테고리 🟢

  • 전체 흐름

    • query parameter (id) → DB 필터링(FK) → 응답에서는 문자열
  • 요청 단계 → 카테고리는 “숫자 ID”로 들어온다

    • GET /api/v1/qna/questions?category=3
      • category=3 → QuestionCategory 테이블의 PK → 문자열 "3"의 형태로 들어옴

  • 1. Query Serializer 단계

    • “정수인가?”만 검증 → 이 Serializer는 “요청 형태”만 검증하는 계층
    class QuestionListQuerySerializer(serializers.Serializer):
        category = serializers.IntegerField(required=False)
    • 여기에서 하는 일

      • 문자열 "3"의 형태 → 3 (int 변환) / category 없으면 → 필터 미적용
  • 2. Service 단계 → “FK 기준으로 필터링”

    • Question.category → category = models.ForeignKey(QuestionCategory, ...)
      • 현재 → 소분류 ID를 주면 → 그 소분류 질문만 / 중분류 ID를 주면 → 그 중분류에 직접 달린 질문만
if category is not None:
    qs = qs.filter(category_id=category)
  • 3. 응답 단계 → 카테고리는 “문자열”로 내려간다

class QuestionListSerializer(serializers.ModelSerializer):
    category = serializers.CharField()

1. Question.category는 FK 객체
2. 응답이 기대 하는 내용 → "category": "프론트엔드 > React > 상태관리"
3., 조회조건에서는 ID / 응답에서는 문자열로 출력

트리 구조

QuestionCategory
 ├─ parent (self FK)
 └─ children

1. 트리구조는 존재는 하지만 아직 사용은 X
2. 즉, “중분류를 선택하면 소분류 질문까지 같이 보여준다” → 아직 구현 안 됨
  • 카테고리 트리는 “고정 구조”로 취급

    • 대분류 / 중분류 / 소분류 개수 / 깊이 / 이름 규칙
    • API 코드에서는 “트리가 몇 단계인지” / “이게 소분류인지" 같은 걸 몰라도 됨
  • 카테고리 문자열을 책임지는 위치

    • Service

      • 카테고리 트리 규칙은 도메인 규칙 (Serializer는 문자열을 그냥 출력만)
        • 어드민이 정의한 구조를 / “어떻게 보여줄지”는 서버 책임

  • ⚠️ . 카테고리는 Soft 규칙, Hard 참조

    class Question(models.Model):
        category = models.ForeignKey(
            QuestionCategory,
            on_delete=models.PROTECT
        )
    
    ⭐️ PROTECT를 쓰는 이유
    - 이미 질문이 달린 카테고리는 삭제 ❌ / 데이터 무결성 보장 → 어드민 실수 방지 

카테고리 핵심

| id | name   | parent_id |		백엔드 (id=1) 		# 백엔드.parent_id = NULL
| -- | ------ | --------- |		└─ Django (id=2)	# Django.parent_id = 1
| 1  | 백엔드   | NULL      |		   └─ ORM (id=3)	# ORM.parent_id = 2
| 2  | Django | 1         |
| 3  | ORM    | 2         |
  • 질문 등록 시에 대/중/소를 각각 필수로 선택 하는 것이 아닌 선택된 하나의 카테고리만 저장 하는 구조
  • 프론트에서는 단계적으로 선택하게 보이지만, 백엔드는 항상 “최종 선택된 카테고리 1개”만 받는다
1. 모델의 의미 
카테고리는 트리 구조 | 깊이는 고정 ❌ (2단계든 3단계든 가능) | “대//소”는 UI 개념, DB 제약 X

class QuestionCategory(TimeStampedModel):
    parent = models.ForeignKey(
        "self",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="children",
    )
    name = models.CharField(max_length=15)

2. related_name="children"
  - “부모 입장에서, 나를 parent로 참조하는 애들을 children이라고 부르겠다.-, parent를 참조하는 row들이 있어서, Django가 역참조 관계(children)를 만들어준 것

3. Question은 카테고리 하나만 참조한다

class Question(models.Model):
    category = models.ForeignKey(
        QuestionCategory,
        on_delete=models.PROTECT,
    )   

ORM

- 1. 자식 → 부모
orm = QuestionCategory.objects.get(name="ORM")
orm.parent  # Django 카테고리 객체

- 2. 부모 → 자식
django = QuestionCategory.objects.get(name="Django")
django.children.all()  # ORM 카테고리 queryset
  • 보통 사용법

if category.parent is None:
    # 루트
elif category.children.exists():
    # 중간 노드
else:
    # leaf
backend = QuestionCategory.objects.create(name="백엔드")
django = QuestionCategory.objects.create(name="Django", parent=backend)
orm = QuestionCategory.objects.create(name="ORM", parent=django)

# Django 내부적으로 하는 일
django.parent_id = backend.id
orm.parent_id = django.id

구현방식

  • 대/중/소 를 각각 선택?

{
  "main_category": 1,
  "sub_category": 3,
  "detail_category": 7
}

- 이 방식을 사용하면, DB 모델과 안 맞음 / 불필요한 중복 정보 / 정합성 검증 지옥
- “대분류와 중분류가 실제로 연결돼 있나?/ “중분류 없이 소분류가 가능한가?/ 테스트 폭발
  • 백엔드 - 프론트엔드

    • 프론트 UX
      • 대분류 선택 → 해당 대분류의 중분류 목록 표시
      • 중분류 선택 → 해당 중분류의 소분류 목록 표시
      • 어디서 멈추든 괜찮음
    • 백엔드 API
    {
      "title": "질문 제목",
      "content": "질문 내용",
      "category": 7
    }
    
    - 7이 대분류일 가능성 / 중분류일 가능성 / 소분류일 가능성 백엔드는 여기있는 ID 하나만 신뢰
  • 결론

    • 최종적으로 백엔드는 카테고리로 하나의 정수를 받음
    • 그게 대분류/중분류/소분류인지는 중요하지 않음
    • 존재 여부만 Service에서 체크
  • 오약

    • 카테고리는 프론트에서는 단계적으로 선택하지만,
      • 백엔드에서는 항상 “최종 선택된 카테고리 ID 하나”만 받는다.
      • 대/중/소는 UI 개념이지 API 계약이 아니다.

카테고리 기능 추가 ❗️

  • 중분류 선택 시 하위 포함 처리

    • 대/중/소는 UI 개념인데, 백엔드는 어떻게 구분하는가?
    • 백엔드는 ‘대/중/소’를 구분하지 않는다. 대신 “부모가 있느냐 / 자식이 있느냐”만 본다.
class QuestionCategory(TimeStampedModel):	|	# 백엔드 구분법
    parent = models.ForeignKey(				|	parent == None     → 루트 카테고리 (UI상 대분류)
        "self",								|	parent != None
        null=True,							|	  ├─ children 있음  → 중간 노드 (UI상 중분류)
        blank=True,							|	  └─ children 없음  → leaf 노드 (UI상 소분류)
        on_delete=models.SET_NULL,			|
        related_name="children",
    )
    name = models.CharField(max_length=15)
  • 백엔드 판단법

    UI 개념백엔드 판단 기준
    대분류parent is None
    중분류parent != None AND children.exists()
    소분류children.exists() == False

  • 중분류 선택 시 하위 포함 기능 기본 초안

    • 선택된 카테고리 + 그 하위 모든 카테고리 ID를 구해서 IN 조건으로 필터링
def get_descendant_ids(category):
    ids = [category.id]
    for child in category.children.all():
        ids.extend(get_descendant_ids(child))
    return ids

# 조회 방법
category_ids = get_descendant_ids(selected_category)
qs = qs.filter(category_id__in=category_ids)

작동 예시 ❗️

    1. backend = QuestionCategory.objects.create(name="백엔드")

      idnameparent_id
      1백엔드NULL
    1. django = QuestionCategory.objects.create(name="Django", parent=backend)
    • parent=backend 는 객체를 넘긴 것

      • Django ORM 내부에서는

        • django.parent_id = backend.id 변환되어 INSERT

          idnameparent_id
          1백엔드NULL
          2Django1
  • 주의 ⚠️

backend = QuestionCategory(name="백엔드")
django = QuestionCategory.objects.create(name="Django", parent=backend)

- 여기서는 에러 발생
  - backend는 아직 DB에 저장되지 않아서 backend.id == None
  - QuestionCategory.objects.create(name="백엔드") 이렇게 사용 필요

QuerySet(qs) 🟢

  • QuerySet = Django 객체들의 집합

    • “데이터”가 아닌 ‘어떤 데이터를 어떻게 가져올지 적어놓은 주문서’
    • “아직 실행되지 않은 DB 조회 계획서이자, 실행되면 Django 객체들의 집합이 되는 것”
  • QuerySet이 생기는 순간

    • qs = Question.objects.all()

    • DB 조회 안 함 / 데이터 안 가져옴 / “Question 테이블을 전부 가져올 예정” 이라는 계획만 존재
# ex
- 1. “질문 전체 가져와라 + 정렬 조건” → qs = Question.objects.annotate(...).order_by(...)

- 2. “근데 답변 있는 것만 / 없는 것만” → qs = filter_by_answered(qs, answered)

- 3. “그리고 카테고리 조건도 추가” → qs = filter_by_category(qs, category)

- 4. “검색어도 포함” → qs = filter_by_search(qs, search)

- 5. 데이터 전체 / 있는지 없는지만 확인 / 그래서 가벼운 조회 1번 발생if not qs.exists():

- 6. 진짜 데이터가 나오는 순간 → page_obj = paginator.get_page(page)

- 7. 결과 → page_obj.object_list

QuerySet은 “DB 쿼리 조립기"

# “ORM이 들어간 제목 중에서 조회수가 0이 아닌 것들을 최신순으로 가져올 예정”
qs = (
    Question.objects
    .filter(title__icontains="ORM")
    .exclude(view_count=0)
    .order_by("-created_at")
)
  • DB에 진짜 쿼리를 날리는 시기

    • list(qs) / qs[0] / for q in qs: / len(qs) / bool(qs)
  • QuerySet의 결과물

qs = Question.objects.all()

# 실행시 구조
[
    <Question 객체>,
    <Question 객체>,
    <Question 객체>,
]

QuerySet vs 객체 차이

구분정체
Question.objects.get()객체 1개
Question.objects.filter()QuerySet
qs[0]객체
qs객체들의 묶음
question = Question.objects.get(id=1)   # 객체
qs = Question.objects.filter(id=1)      # QuerySet

type(question)  # Question
type(qs)        # QuerySet

QuerySet 특성

  • QuerySet은 “체이닝”이 가능

# 새로운 쿼리마다 쿼리 실행X / 조건만 누적 → 최종 실행 시점에 한번에 실행
qs = Question.objects.all()
qs = qs.filter(category=1)
qs = qs.exclude(is_deleted=True)
  • QuerySet은 리스트X

    • qs.append() / qs.pop() 불가능 | QuerySet은 읽기 전용 결과 뷰(view)에 가깝다
    • QuerySet은 DB 조회 결과 / 리스트는 메모리 데이터
  • QuerySet이 Serializer로 넘어갈 때

    • serializer = QuestionSerializer(qs, many=True)
      • Serializer는 QuerySet을 순회
      • 내부의 Question 객체 하나씩 처리 / dict로 변환 / 최종적으로 JSON 응답

# models.py
class Question(TimeStampedModel):
    author = models.ForeignKey(settings.AUTH_USER_MODEL,
    on_delete=models.CASCADE, related_name="questions")

    category = models.ForeignKey(QuestionCategory, 
    on_delete=models.PROTECT, related_name="questions")

# services.py
qs = (
    Question.objects
    .select_related("author", "category")
    .annotate(
        answer_count=Count("answers", distinct=True),
        is_answered=Case(
            When(answers__isnull=False, then=True),
            default=False,
            output_field=BooleanField(),
        ),
    )
    .order_by("-created_at")
)
  • 위의 qs의 의도 → 질문 목록 카드 UI에 필요한 데이터를 한 번에 가져오자

    • Question.objects

      • FROM questions 테이블 / 즉, Question 모델의 row들이 기준
      • 하지만, 카드 UI에서는 Question 필드만으로는 부족
      • 작성자 닉네임 / 작성자 프로필 이미지 / 카테고리 이름 → 전부 다른 테이블에 위치
    • select_related("author", "category")

      • "author", "category" 는 모델의 필드명 (related_name X)
      • ForeignKey / OneToOne 관계를 JOIN으로 한 번에 가져오는 것
      • author → User 테이블 FK / category → QuestionCategory 테이블 FK
SELECT *
FROM questions
JOIN users ON questions.author_id = users.id
JOIN question_categories ON questions.category_id = question_categories.id
ORDER BY questions.created_at DESC;

- 1. select_related를 사용하지 않은 경우		# 질문 목록 쿼리 1번
										# 질문 개수만큼 author 조회 쿼리 N번
questions = Question.objects.all()		# 질문 개수만큼 category 조회 쿼리 N번
										# N + 1 문제 발생
for q in questions:
    print(q.author.nickname)

- 2. select_related 쓴 경우
Question.objects.select_related("author", "category") # 질문 + 작성자 + 카테고리 → 쿼리 1번

  • 코드 기준 상세 설명

qs = Question.objects.select_related("author", "category")
question = qs.first()

# 아래 전부 가능
question.title                 # Question 테이블
question.view_count             # Question 테이블

question.author.id              # User 테이블
question.author.nickname        # User 테이블
question.author.profile_image_url

question.category.id            # QuestionCategory 테이블
question.category.name

Django ORM 규칙

  • annotate, filter, exclude 에서는 related_name 을 필드처럼 사용 가능

    구분select_relatedannotate / filter
    방향정방향 FK정/역방향 모두
    관계1:1 / N:11:N 가능
    JOIN 결과row 1개 유지row 늘어날 수 있음
    목적객체 접근 최적화조건 / 집계

annotate

  • qs = Question.objects.annotate(answer_count=Count("answers"))

    • 해석: “Question 객체를 가져오는데 answer_count라는 가짜 컬럼을 붙여서 가져와라”
qs = qs.annotate(
        content_preview=Substr("content", 1, 100)
    )
  • content 컬럼을 1~100자만 잘라서 결과 컬럼으로 내려준다.
    • Question.objects.annotate(content_preview=Substr("content", 1, 100))
      • 실제형태에 따라 원래는 Question 모델 전체 필드도 같이 조회됨
      • 즉, content (원문)도 조회됨 / content_preview (잘린 값)도 조회됨
      • 하지만 시리얼라이즈에서 목록에서 원문이 보이지 않게 하면 됨
      • fields = ["title","content_preview",# content ❌]

코드 분석

qs = (
        Question.objects
        .select_related("author", "category")
        .annotate(
            answer_count=Count("answers", distinct=True),
        )
        .order_by("-created_at")
    )
  • select_related("author", "category") : "author", "category" → Question의 필드명
  • answer_count=Count("answers", distinct=True)
    • "answers" : Answer(답변) 모델의 ForeignKey에 정의된 related_name
class Question(models.Model):
    author = models.ForeignKey(User, ...)
    category = models.ForeignKey(QuestionCategory, ...)

class Answer(models.Model):
    question = models.ForeignKey(Question,related_name="answers",on_delete=models.CASCADE)
  • author, category: Question 쪽에 필드 존재 → select_related("author") ⭕️
  • answers: Question 모델에 필드가 없음 → Question FK의 related_name
    • Question.answers → Question에 매달린 Answer들의 QuerySet

Detail 🔴


기본 세팅 🟢

  • 모델 구조

Student ───▶ School
   │
   └───▶ Assignment (여러 개)
            │
            └───▶ Comment (여러 개)
  • 모델 코드 (단순화)

class School(models.Model):
    name = models.CharField(max_length=50)

class Student(models.Model):
    name = models.CharField(max_length=50)
    school = models.ForeignKey(School, on_delete=models.CASCADE)

class Assignment(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)

class Comment(models.Model):
    assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
    content = models.TextField()

최적화 전

# 학생 10명을 조회해서 각 학생의 학교 이름 + 과제 제목들 + 과제의 댓글들을 출력

students = Student.objects.all()		|	| 단계       | 쿼리       |	
										|	| --------- | --------- |	
for s in students:						|	| 학생 목록    | 1        |	
    print(s.school.name)				|	| 학생마다 학교 | 10       |	
										|	| 학생마다 과제 | 10       |	
    for a in s.assignment_set.all():	|	| 과제마다 댓글 | 30       |	
        print(a.title)					|	| **총합**    |51|	
										|
        for c in a.comment_set.all():
            print(c.content)

  • “1명당 1개” 관계를 JOIN / 한 객체당 딱 하나만 연결된 관계 / FK

# 적용
students = Student.objects.select_related("school")

# 결과
for s in students:
    print(s.school.name)  # 추가 쿼리 ❌
  • JOIN으로 Student + School을 한 번에 가져옴 / school 접근할 때 DB 안 감

    단계쿼리
    학생 + 학교1
    과제10
    댓글30
    총합❌ 41

  • “여러 개” 관계를 따로 모아서 묶기 / 1명이 여러 개를 가질 때
# Student → Assignment (1:N) / Assignment → Comment (1:N)
students = Student.objects.select_related("school").prefetch_related("assignment_set")

# 결과
for s in students:
    for a in s.assignment_set.all():  # 추가 쿼리 ❌
        print(a.title)
  • Django가 내부적으로

    • 학생 id 목록 → WHERE student_id IN (...) / 파이썬에서 자동으로 학생별로 묶어줌

      단계쿼리
      학생 + 학교1
      모든 과제 (IN 쿼리)1
      댓글30
      총합❌ 32

Prefetch

# 문제 : comment_set에서 다시 N+1 발생
for a in s.assignment_set.all():
    for c in a.comment_set.all():
        print(c.content)

# 해결 : “학생의 과제를 미리 가져오되, 과제를 가져올 때 댓글도 같이 미리 가져와라”
from django.db.models import Prefetch

students = Student.objects.select_related("school").prefetch_related(
    Prefetch(
        "assignment_set",
        queryset=Assignment.objects.prefetch_related("comment_set")
    )
)
단계쿼리
학생 + 학교1
모든 과제1
모든 댓글1
총합3번

정리

도구한 문장 설명
select_related“딱 1개 연결된 건 JOIN으로 한 번에”
prefetch_related“여러 개는 따로 모아서 파이썬에서 묶기”
Prefetch“prefetch할 때 가져오는 방식까지 직접 정하기”

내 코드 🟢

def get_question_detail_queryset(question_id: int) -> Question | None:
    return (
        Question.objects
        .select_related("author", "category")
        .prefetch_related(
            "images",
            Prefetch(
                "answers",
                queryset=Answer.objects
                .select_related("author")
                .prefetch_related(
                    "answer_comments__author"
                )
            )
        )
        .filter(id=question_id)
        .first()
    )

  • 이 코드의 현재 목표


                Question (1)					# 질문 1개를 가져오면서
                 ├─ images (N)					# 질문의 이미지들
                 └─ answers (N)					# 질문의 답변들
                      ├─ author (1)				# 각 답변의 작성자
                      └─ answer_comments (N)	# 각 답변의 댓글들
                            └─ author (1)		# 각 댓글의 작성자
                                                # 를 추가 쿼리 없이 한 번에 메모리에 올리기  

관계방법
FK / OneToOneselect_related
1:N / M:Nprefetch_related
1:N 안에서 FKPrefetch + select_related
깊은 중첩Prefetch 중첩
Question
 ├─ author (FK)
 ├─ category (FK)
 ├─ images (1:N)
 └─ answers (1:N)
      ├─ author (FK)
      └─ answer_comments (1:N)
           └─ author (FK)
    • .select_related("author", "category")
      • FK / OneToOne
      • SQL JOIN
      • “한 행당 하나”일 때
    • JOIN이 아니라 별도 쿼리로 미리 로딩하고 싶을 때
    • .prefetch_related("answers", "images")
      • 1:N / M:N
      • 별도 쿼리 + Python에서 매칭
      • “여러 개일 수 있을 때”
  • Prefetch

    • prefetch_related("answers")는 “그냥 다 가져오기”밖에 못 함
    • 답변 최신순 정렬 / 특정 조건만 가져오기
    • 답변에 또 select_related / prefetch_related 적용 가능
from django.db.models import Prefetch

Prefetch(
    "관계명",
    queryset=커스텀_queryset,
    to_attr="저장할_속성명"  # 선택
)
상황선택
그냥 관계 미리 불러오기prefetch_related("xxx")
필터 / 정렬 필요Prefetch
내부에 select_related 필요Prefetch
댓글 → 작성자 같이 필요Prefetch
성능 튜닝Prefetch

from django.db.models import Prefetch

question = (
    Question.objects
    .select_related(
        "author",
        "category",
    )
    .prefetch_related(
        "images",
        Prefetch(
            "answers",
            queryset=Answer.objects
            .select_related("author")
            .prefetch_related(
                Prefetch(
                    "answer_comments",
                    queryset=AnswerComment.objects.select_related("author"),
                )
            ),
        ),
    )
    .get(id=question_id)
)
- 1. 질문 1개당 작성자 1, 카테고리 1(Question.author, Question.category → FK)
Question.objects.select_related("author", "category") 

- 2. 이미지 개수는 0~N개 / JOIN 하면 row 수가 폭발함
Question.objects.prefetch_related("images")

- 3. answers는 1:N(prefetch) 하지만 answer.author는 FK(select_related)
Prefetch("answers",queryset=Answer.objects.select_related("author")...)

- 4. 댓글은 답변마다 여러 개 / 댓글 작성자도 FK
.prefetch_related(Prefetch
("answer_comments",queryset=AnswerComment.objects.select_related("author"),))

N+1 문제

# ex. 질문 10개를 조회해서 각 질문의 작성자 닉네임을 보여주기 

questions = Question.objects.all()  # 1번 쿼리
for q in questions:
    print(q.author.nickname)        # 질문마다 author 조회 쿼리 발생 가능

# result
1. 질문 목록 가져오는 쿼리 1(N개 질문)
2. 각 질문마다 author를 따로 가져오면 추가로 N번
3.1 + N → 이게 N+1 문제
DRF serializer에서 중첩된 관계(작성자/이미지/답변/댓글)를 펼칠수록 N+1이 더 심해짐.

.prefetch_related(
    "images",
    Prefetch(
        "answers",
        queryset=Answer.objects
            .select_related("author")
            .prefetch_related("answer_comments__author")
    )
)
  • 해석

    • Question을 가져올 때 Question.images 는 prefetch 하고
    • Question.answers 는 특별한 조건이 적용된 쿼리셋으로 prefetch 하겠다
      • 그 answers 안에서는
        • answer.author 는 select_related
        • answer.answer_comments 와 그 author 는 prefetch 하겠다

  • .prefetch_related("images")

    • Question.images 는 역참조 + 다대일/다대다
      • 단순히 “전부 다 가져오기”만 하면 됨 / 정렬, 필터, select_related 필요 없음
    • 별도의 쿼리로 QuestionImage row들을 가져와서, 파이썬 객체로 Question.images에 붙인다
    question_image 테이블
    -------------------------
    id
    question_id   ← FK (related_name = "images")
    img_url
    created_at
    updated_at
    → question_id + img_url 는 한 row에 같이 있음
    • .prefetch_related("images") 의 의미

      • Question 객체를 가져온 뒤 그 Question의 PK들을 모아서 QuestionImage 테이블에서
        • question_id IN (...) 인 row들을 전부 가져와라
      • 즉, QuestionImage 모델에서 해당 question에 속한 모든 row 를 필터 없이 가져온다
question.images.all()

# 위의 결과가 될 수 있는 모든 이미지row를 한번에 미리 로딩해 두는 것
[
    QuestionImage(question_id=1, img_url="a.jpg"),
    QuestionImage(question_id=1, img_url="b.jpg"),
    QuestionImage(question_id=1, img_url="c.jpg"),
]

# 장점
- prefetch 없을 때
question = Question.objects.get(id=1)
question.images.all()  # ❗ 여기서 쿼리 발생

- prefetch 있을 때
question = Question.objects.prefetch_related("images").get(id=1)
question.images.all()  # ⭕️ 쿼리 안 나감

- prefetch는 이 역참조 이름을 기준으로 동작 related_name이 없었다면 
question.questionimage_set
  • images는 Prefetch 객체 안쓴 이유

    이유설명
    필터 없음이미지 전부 필요
    정렬 필요 없음생성순 그대로 OK
    추가 FK 접근 없음serializer에서 img_url만 씀
  • 요약

    • related_name="images" 로 인해 Question.pk == QuestionImage.question_id 인
      • QuestionImage 객체들(row들) 을 .prefetch_related("images") 가 미리
      • 로딩해서 question.images 에 붙여준다

Prefetch

  • Question.answers 를 불러올 때 기본 Answer 쿼리 말고 내가 정의한 이 쿼리셋으로 가져와라
Prefetch(
    "answers",
    queryset=Answer.objects
        .select_related("author")
        .prefetch_related("answer_comments__author")
)
| 항목          | 의미                        |
| ----------- | -----------------------     |
| `"answers"` | Question → answers 관계      |
| `queryset=` | answers를 가져올 **커스텀 쿼리** |
  • .prefetch_related("answer_comments__author")
    • answer → answer_comments (N)
    • answer_comment → author (FK)
  • 전체 요약

    • 이 prefetch_related 블록은 Question → Answers → AnswerComments → Author 까지
      • 모든 중첩 관계를 N+1 없이 한 번에 로딩하기 위한 최적화 쿼리 구성이다

profile
안녕하세요.

0개의 댓글