Django - ORM / Query

김기훈·2025년 12월 31일

Django

목록 보기
12/17

중첩 “표현”은 시리얼라이저 덕분이고, 중첩 “데이터 구조”는 ORM 관계 덕분이다


ORM - 1 🟥

구분정체
Question클래스
question객체 (한 행)
QuerySet객체들의 묶음
dict단순 데이터
JSON문자열 데이터

객체 🟧

  • DB의 한 행(row)을 파이썬 클래스로 감싼 인스턴스 (Instance (인스턴스) → 테이블의 “한 row”)
    • DB에서 한 행(row)을 읽어와서, 파이썬에서 다루기 좋게 만든 “실체”
      • DB의 한 줄 = Django 객체 하나


[1번 예시]

# AnswerComment
| id | content  | answer_id |
| -- | -------- | --------- |
| 7  | 좋은 답변이네요 | 3      |

# Django ORM으로 조회
comment = AnswerComment.objects.get(id=7) # 이 comment가 “객체”

# 파이썬 객체
comment.id        # 7
comment.content   # "좋은 답변이네요"
comment.answer_id # 3
[2번 예시]

# 클래스								|	| DB     | Django     |
class User:							|	| ------ | ---------- |
    def __init__(self, name):		|	| 테이블   | Model 클래스 |
        self.name = name			|	| 컬럼    |  필드        |
									|	|(Row) | 객체(Object)|
# 객체(인스턴스)
user = User("철수")

# User → 설계도(클래스) / user → 실제 만들어진 물건(객체) / user.name → 객체가 가진 값

Django에서 “객체”가 생기는 순간

[ 1. DB에 데이터가 있을 때 ]
id | title | content
1  | ORM?  | ORM 설명

[ 2. Django가 이걸 가져오면 ]
question = Question.objects.get(id=1)

[ 3. 이때 만들어지는 question이 Django 객체 ]
question.id        # 1
question.title     # "ORM?"
question.content   # "ORM 설명"

Django 객체의 중요성

[ 1. 이 객체는 단순한 dict 아님 / JSON 아님 / DB 그 자체 아님 ]
[ 2. DB와 연결된 파이썬 객체 ]
question = Question.objects.get(id=1)

[ 3. 따라서 이 작업 가능 ]
question.title = "새 제목"
question.save()   # DB UPDATE 발생

[ 4. 객체 조작 = DB 조작 ]

ForeignKey도 “객체로 연결됨”

class Answer(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)

# 사용 → 객체 안에 객체가 연결됨
answer = Answer.objects.get(id=1)

answer.question        # Question 객체 / question_id 숫자 아님
answer.question.title  # 질문 제목

Serializer에서 말하는 “객체”

class QuestionSerializer(serializers.Serializer):
    title = serializers.CharField()

# obj = Serializer의 obj = Django 모델 객체
obj = Question.objects.get(id=1)

obj.title
obj.author.nickname

ORM - 2 🟥

Instance (인스턴스) 🟧

  • 테이블의 “한 row”

# question = 인스턴스 → 실제 DB 데이터 1줄
question = Question.objects.first()

question.title / question.author → 이미 DB에서 가져온 값

Query (쿼리) 🟧

  • DB에게 “이런 데이터 줘”라고 요청하는 명령

    • SQL 예시 → SELECT * FROM question WHERE id = 1;
    • Django에서의 쿼리 → Question.objects.filter(id=1)
      • 아직 DB에 실행되진 않음 / “이런 조건의 데이터를 가져오겠다”는 요청서 수준

QuerySet (쿼리셋) 🟧

  • 쿼리들의 집합 + 아직 실행되지 않은 결과 후보

    • ex. qs = Question.objects.filter(category=1)
      • 이 qs: 리스트 ❌ / 실제 데이터 ❌ / DB에 날아간 결과 ❌
        • “나중에 실행될 수 있는 쿼리 묶음 객체”
  • Lazy Evaluation (지연 실행)

# 아직 DB쿼리 안나감
qs = Question.objects.filter(category=1)
print(qs)

# 아래 순간 DB 쿼리 실행
list(qs)
qs.first()
qs[0]

Query Parameter 🟧

  • URL 뒤에 붙어서 서버에 추가 조건이나 옵션을 전달하는 값

    • 조회(GET) 요청에서 필터링, 정렬, 페이지네이션 같은 “조회 옵션”을 표현할 때 사용
  • 예시

    • /questions?category=3&page=2&answered=true
      • ? : Query Parameter 시작 / key=value : 하나의 파라미터 / & : 여러 개 연결

Model (모델)

  • DB 테이블을 Python 클래스로 표현한 것


Manager (objects) 🟧

  • 쿼리 생성기

    • Question.objects
      • .filter() / .get() / .annotate() / .select_related()
        • 전부 QuerySet을 만들어내는 역할
  • filter / get / exclude 차이

# 결과: QuerySet | 0개여도 에러 ❌
Question.objects.filter(category=1)

# 결과: Instance | 0개 ❌ / 2개 이상 ❌ → 에러
Question.objects.get(id=1)

# 조건을 제외
Question.objects.exclude(is_deleted=True)

Django ORM 🟥

        생성 → `create()`	|	단건 조회 → `get()`  |	필터 → `filter()` / `exclude()` 

        정렬 → `order_by()` 	|	개수 →   `count()`	|	존재 여부 →`exists()` 

생성

  • create() / bulk_create() / get_or_create() / update_or_create()

조회

  • all() / get() / filter() / exclude() / first() / last() / values() / values_list()

수정 / 삭제

  • update() / delete()

집계 / 주석

  • annotate() → 행 단위 계산(QuerySet) / aggregate() → 전체 요약(dict)

ORM - 3 🟥

  • DB에 접근(hit)하는 횟수를 줄이고 더 빠르게 데이터를 조회할 수 있게 해줌


Django ORM 규칙

  • annotate, filter, exclude에서는 related_name을 필드처럼 사용 가능
구분select_relatedannotate / filter
방향정방향 FK정/역방향 모두
관계1:1 / N:11:N 가능
JOIN 결과row 1개 유지row 늘어날 수 있음
목적객체 접근 최적화조건 / 집계

qs 이해

  • qs는 “데이터”가 아니라 ‘어떤 데이터를 어떻게 가져올지 적어놓은 주문서’
# “질문 전체 가져와라 + 정렬 조건”
qs = Question.objects.annotate(...).order_by(...)

# “근데 답변 있는 것만 / 없는 것만”
qs = filter_by_answered(qs, answered)

# “그리고 카테고리 조건도 추가”
qs = filter_by_category(qs, category)

# “검색어도 포함”
qs = filter_by_search(qs, search)

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

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

# 결과
page_obj.object_list

# 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
- 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

annotate

# models.py

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)

# selector.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")
    )
  • select_related("author", "category")
    • author, category는 Question 쪽에 필드가 있음 → select_related("author") 가능
    • "author", "category"는 Question의 필드명
  • answer_count=Count("answers", distinct=True)
    • answers는 Question 모델에 필드가 없음 → Question FK의 related_name
      • Question.answers → Question에 매달린 Answer들의 QuerySet
  • When(answers__isnull=False, then=True)
    • 이 Question에 연결된 Answer row가 하나라도 있으면 True

  • 예시 2

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 ❌]

⭐️

기본 QuerySet

    qs = (
        Question.objects
        .select_related("author", "category")
        .annotate(answer_count=Count("ai_answers", distinct=True))
        .order_by("-created_at")
    )
- QuerySet 생성 시작 / 아직 DB 쿼리는 실행되지 않음 (Lazy Evaluation)
- ### .select_related("author", "category")
  - Question → author(User), category(FK)
  - 사용하지 않을 경우 
    - 질문 10개 조회 / 작성자/카테고리 접근 시 N+1 쿼리 발생
- ### .annotate(answer_count=Count("ai_answers", distinct=True))
  - 질문마다 답변 수를 계산 (카드 UI에 필요한 핵심 데이터)
    - ai_answers: Question ↔ QuestionAIAnswer related_name
    - distinct=True 이유: join 상황에서 중복 count 방지
- ### .order_by("-created_at")
  - 최신순 정렬 
  • 검색 필터

    • if search: → 빈 문자열 ""은 False → 즉, search="" → 검색 미적용
        qs = qs.filter(
            Q(title__icontains=search) |
            Q(content__icontains=search)
        )

- 1. 제목 OR 내용 검색 - 2. icontains → 대소문자 무시 부분 검색
  • if not qs.exists()

    • 쿼리를 실제로 한 번 실행 / 결과가 0건이면 404 출력
  • thumbnail_subquery

    • 질문 하나당 이미지 여러 개 가능 / 그중 첫 번째 이미지 하나만 가져오기
    • OuterRef("pk")

      • 바깥 QuerySet의 Question.id 참조
  • qs = qs.annotate(thumbnail_image_url=Subquery(thumbnail_subquery))

    • 위 서브쿼리를 thumbnail_image_url 필드로 추가
      • 이제 QuerySet 각 row는 question.thumbnail_image_url
  • paginator = Paginator(qs, page_size)

    • 전체 QuerySet을 page_size 단위로 분할
  • page_obj = paginator.get_page(page)

    • 요청한 페이지 번호에 해당하는 객체 반환 / 범위를 벗어나면 마지막 페이지 반환 (Django 기본 동작)
  • return page_obj.object_list, {}

    • Service는 데이터 + 메타 정보를 함께 반환 / View에서 응답 구조를 만들기 위함
      • "page": page → 현재 페이지 번호 / "page_size": page_size → 페이지당 항목 수
      • "total_pages": paginator.num_pages → 전체 페이지 수
      • "total_count": paginator.count → 전체 질문 개수
  • 요약

    • “조회 조건을 신뢰할 수 있는 입력으로 받아 필터링·검색·집계·페이지네이션·가공을 모두 수행하고
      • ‘카드 UI에 바로 쓸 수 있는 질문 목록’을 만들어주는 핵심 로직”

ORM query 도구

  • from django.db.models import Count, OuterRef, Subquery, Q
    • ORM 쿼리를 더 똑똑하게 만들기 위한 도구들
      • Count → 연관된 데이터 개수 집계 (답변 수)
      • OuterRef → 서브쿼리에서 바깥 QuerySet의 필드를 참조
      • Subquery → “질문별 첫 번째 이미지” 같은 값 가져오기
      • Q → OR 조건, 복합 검색 조건 작성
    • 조회 API는 단순 CRUD가 아니라 “가공된 조회”라서 이런 도구들이 필요
from django.db.models import Count, OuterRef, Subquery, Case, When, BooleanField
# Count        : 답변 개수 집계
# OuterRef     : 서브쿼리에서 현재 Question row 참조
# Subquery     : 썸네일 이미지 1개 추출
# Case / When  : 조건에 따라 is_answered(Boolean) 생성
# BooleanField : is_answered 필드 타입 명시

  • .select_related("author", "category")

    • ForeignKey로 연결된 객체를 JOIN으로 한 번에 가져오기 (N+1 방지)
    • question.author / question.category 는 DB에서 별도 조회가 필요한 FK 관계
- 2. Question 모델
class Question(models.Model):
    author = models.ForeignKey(User, ...)
    category = models.ForeignKey(QuestionCategory, ...)

- 3. select_related가 없으면?
questions = Question.objects.all()

for q in questions:
    q.author.nickname     # ❌ 매번 DB 쿼리
    q.category.name       # ❌ 매번 DB 쿼리
						  # 질문 20개 조회 → author 접근 → 20번 쿼리 / category 접근 → 20번 쿼리
                          # 총 41번 쿼리 → N+1 문제

- 4. select_related 사용
Question.objects.select_related("author", "category")


# SQL적 의미
SELECT ...FROM questions
LEFT OUTER JOIN users ON ...
LEFT OUTER JOIN question_categories ON ...

# 질문 + 작성자 + 카테고리 한 번에 / 이후 serializer에서 접근해도 추가 쿼리 없음

qs(QuerySet)

  • 아직 DB에 실행되지 않은 질문 목록 설계도(QuerySet)

    • Django에서 QuerySet은
      • 실제 데이터 아님 / 리스트 아님 / DB에 보낼 SQL 쿼리를 Python 객체로 표현한 것
      • 즉, "이런 조건으로 questions 테이블에서 가져와라" 라는 요청서를 만들어 두는 단계
qs = (
    Question.objects
    .select_related(...)
    .annotate(...)
    .order_by(...)
)

  • Question.objects → 질문 테이블을 조회하겠다는 선언
  • .select_related("author", "category")
    • 질문 + 작성자 + 카테고리를 JOIN 해서 한 번에 가져오겠다
  • .annotate(answer_count=Count("answers", distinct=True),)
    • 각 질문마다 → answer_count라는 가짜 컬럼을 붙이겠다
    • DB에 컬럼 생기는 거 아님 / 조회 결과에만 존재

  • 아래 예시 두개 다 QuerySet

    • QuerySet은 계속 “체이닝”하면서 완성해나가는 것
- 1
qs = Question.objects.all()

- 2
qs = (
    Question.objects
    .filter(...)
    .annotate(...)
)

나눠서 쓰는 이유

  • QuerySet은 불변(immutable)에 가까움
    • filter()는 기존 qs를 바꾸지 않음 → 새 QuerySet을 반환
      • 그래서 qs = qs.filter(...) 이런 형태가 됨
qs = Question.objects.all()

qs = filter_by_answered(qs, answered)
qs = filter_by_category(qs, category)
qs = filter_by_search(qs, search)

진짜 DB를 조회 시기

# QuerySet evaluation(실행)
qs.exists()
list(qs) # 여기서 실행하는 듯 
for q in qs:
paginator = Paginator(qs, ...)

# 내 코드 기준
if not qs.exists():
paginator = Paginator(qs, page_size) # DB 조회 1번
page_obj = paginator.get_page(page) # 여기서 실제 SELECT 실행

내 코드 기반 해설

  • 질문 조회 시, answers 관계가 존재하는지를 기준으로
    • SELECT 결과에 is_answered라는 Boolean 값을 임시로 붙인다
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")
    )
  • answer_count와 is_answered의 차이

    항목answer_countis_answered
    타입IntegerBoolean
    의미몇 개 있는지하나라도 있는지
    SQLCOUNT()CASE WHEN
    용도숫자 표시탭 필터 / 상태

  • .annotate를 붙인 가짜 컬럼이 Serializer에 없으면 그냥 “무시되고 응답에 안 나온다”

  • 반대로 시리얼라이즈에는 있는데 qs에 없는경우

    • 에러 발생 / qs에는 있고 시리얼라이즈에는 없는경우는 에러 발생 X

      상황결과
      qs에 있음 + serializer에 있음✅ 정상 출력
      qs에 있음 + serializer에 없음❌ 무시
      qs에 없음 + serializer에 있음💥 에러
  • 즉, Serializer는 “출력하려는 값이 qs(QuerySet)에 존재해야만” 정상적으로 동작한다.

  • Serializer에 정의된 필드 값은 모델 필드이거나, annotate로 qs에 붙어 있거나,

    • Serializer가 직접 계산할 수 있어야 한다.

흐름

  • 1. 어떤 데이터를, 어떤 조건으로, 어떤 형태로 가져올지 정의한 QuerySet

qs = (
    Question.objects
    .select_related("author", "category")
    .annotate(...)
    .order_by("-created_at")
)
    1. qs는 계속 바뀌는 게 아니라 “누적”된다
qs = filter_by_answered(qs, answered)
qs = filter_by_category(qs, category)
qs = filter_by_search(qs, search)

if not qs.exists(): # 여기서 첫번 째 실행
  • 기본 질문 목록에

    • 답변 여부 조건을 붙이고 → 카테고리 조건을 붙이고 → 검색 조건을 붙여라”
    1. 진짜 데이터 가져오는 곳
    • page_obj = paginator.get_page(page)
      • LIMIT / OFFSET 이 적용된 SQL 실행 | 실제 질문 row들이 메모리로 로딩됨
        • return page_obj.object_list → 실질적으로 조회된 데이터
  1. Question.objects ... ← QuerySet 생성 (아직 안 읽음)
  2. filterby* 적용 ← 조건만 추가
  3. qs.exists() ← DB 조회 1회 (존재 여부)
  4. Paginator.get_page() ← DB 조회 1회 (실제 데이터)
  5. page_obj.object_list 반환 ← 조회 결과

qs 점검

# 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")
    )

# srializers.py
class QuestionListSerializer(serializers.ModelSerializer):
    category = serializers.SerializerMethodField()
    author = QuestionAuthorSerializer()

    content_preview = serializers.CharField()
    answer_count = serializers.IntegerField()
    is_answered = serializers.BooleanField()

    thumbnail_image_url = serializers.CharField(allow_null=True)

    class Meta:
        model = Question
        fields = [						# Question 모델 필드 (자동 OK)
            "id",						# id / title / view_count / created_at
            "category",					# select_related 로 해결되는 것
            "author",					# author / category 
            "title",					# annotate로 추가된 가짜 컬럼
            "content_preview",			# answer_count / is_answerd
            "answer_count",				# annotate로 추가된 가짜 컬럼(서비스에서 처리)
            "is_answered",				# content_preview / thumbnail_image_url
            "view_count",
            "created_at",
            "thumbnail_image_url",
        ]

  • 정리

    • ORM → Service → Serializer → Response
      • Question.objects를 기준으로 Question 모델의 필드를 조회하고
      • select_related로 FK로 연결된 다른 모델의 해당 row를 JOIN해서 미리 로딩하고
      • annotate로 카드 UI에 필요한 가짜 컬럼들을 조회 결과에 추가한 뒤
      • 최종적으로는 Serializer에 선언된 필드들만 Response에 포함되어 출력된다.

코드 리뷰

  • 썸네일 1장을 “질문마다” 붙이는 Subquery

thumbnail_subquery = (
        QuestionImage.objects
        .filter(question=OuterRef("pk")) # question: QuestionImage의 필드명
        .order_by("created_at")
        .values("img_url")[:1]
    )
    qs = qs.annotate(thumbnail_image_url=Subquery(thumbnail_subquery))
  • OuterRef("pk")

    • 바깥 쿼리(=Question 목록 쿼리)의 현재 row의 pk를 의미
      • “지금 처리 중인 Question의 pk”를 QuestionImage 쿼리 안에서 참조
      • “현재 질문(바깥 QuerySet)의 pk를 가진 질문에 연결된 이미지들만 골라라”
  • created_at

    • 오름차순이면 → 가장 먼저 등록된 이미지 1장
    • “가장 최신 이미지”를 썸네일 → -created_at
  • .values("img_url")[:1]

    • values("img_url") : 이미지 row 전체가 아니라 img_url 컬럼만 뽑아오겠다
      • [:1] : 그 중 딱 1개만 (LIMIT 1)
    • “현재 질문에 달린 이미지들 중, 정렬 기준으로 1개 img_url만 가져와라”
  • Subquery(...)로 “가짜 컬럼”을 질문에 붙인다

    • qs의 각 Question 객체에는 DB에 없는 속성이 생김
      • question.thumbnail_image_url

  • 페이지네이션 처리

paginator = Paginator(qs, page_size)
    page_obj = paginator.get_page(page)

    return page_obj.object_list, {
        "page": page,
        "page_size": page_size,
        "total_pages": paginator.num_pages,
        "total_count": paginator.count,
    }
  • Paginator(qs, page_size)

    • Django의 기본 페이지네이터 / qs를 page_size 단위로 나눌 준비를 한다.
    • page_size=10이면 / 1페이지: 1~10 / 2페이지: 11~20
  • page_obj = paginator.get_page(page)

    • page가 정상이면 해당 페이지를 줌 / page가 이상해도(문자열/범위초과) 에러 대신 안전하게 처리
    • 너무 큰 페이지면 마지막 페이지로 / 잘못된 값이면 1페이지로 (이게 page()와의 차이점 중 하나)
  • page_obj.object_list (응답 데이터 만들기)

    • 현재 페이지에 해당하는 Question 목록(QuerySet/리스트 성격)
    • serializer에 넣을 “results”가 되는 애들
  • paginator.count

    • 필터 조건을 모두 적용한 후
    • 전체 질문 개수 (페이지와 무관)
# page info 메타
"page": page | → 요청한 페이지 번호

"page_size": page_size | → 한 페이지에 몇 개씩

"total_pages": paginator.num_pages | → 전체 페이지 수
(내부적으로 count 기반으로 계산됨)

"total_count": paginator.count | → 전체 질문 개수 (필터 적용 이후 기준)
paginator.count는 보통 DB에 SELECT COUNT(*) ... 같은 쿼리를 한 번 날려서 전체 개수를 알아냄

  • 바깥 쿼리(outer query)

    • 지금 DB가 한 줄씩 만들고 있는 대상 = Question 한 row = 바깥 쿼리의 현재 row
qs = (
    Question.objects		# 바깥 쿼리(outer query) ⭐️
    .select_related("author", "category")
    .annotate(
        thumbnail_image_url=Subquery(thumbnail_subquery)
    )
)
  • 안쪽 쿼리(subquery)

# QuestionImage 테이블을 조회하는 쿼리
thumbnail_subquery = (
    QuestionImage.objects
    .filter(question=OuterRef("pk"))
    ...
)
  • 구조

[바깥] Question  ────────────────▶ 한 row씩 처리
        │
        └── [안쪽] QuestionImage ─▶ 해당 Question의 이미지 1
  • OuterRef("pk")

    • 안쪽쿼리에서 바깥 쿼리의 현재 row의 pk 값을 참조
      • 기본설정일 경우 obj.id 도 가능은 하지만 obj.pk 이렇게 하면 항상 동작 함(안전)
바깥 쿼리: Question
-------------------
row 1: pk=10
row 2: pk=11
row 3: pk=12

안쪽 쿼리: QuestionImage
-----------------------
question_id = OuterRef("pk")

# DB처리
row 1 → question_id = 10
row 2 → question_id = 11
row 3 → question_id = 12

각 Question마다 자기 이미지 1개를 찾아서 붙임
  • 정리

    • 바깥쿼리의 각 row(Question 1개)를 기준으로 서브쿼리가 “해당 Question의 pk를 조건으로”
      • 실행되어 그 결과값(img_url 1개)을 컬럼 값으로 붙인다.
  • 조회 시 DB는 Question을 기준으로 한 row씩 처리하면서,

    • 각 row의 pk를 조건으로 사용하는 서브쿼리를 통해 해당 Question에 연결된 이미지 중
      • 하나의 img_url을 계산하고, 그 값을 thumbnail_image_url이라는 가짜 컬럼으로 붙인다.
    • Question | id = 1 → 이미지 있음 / id = 5 → 이미지 없음
    • QuestionImage | question_id = 1 → row 존재 / question_id = 5 → row 없음

실전 예시


참조 문법

# --- [ Question ] ---
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")

    title = models.CharField(max_length=50)
    content = models.TextField()

    view_count = models.BigIntegerField(default=0)
    
# --- [ User ] ---
class User(AbstractBaseUser, PermissionsMixin, TimeStampedModel):
    email = models.EmailField(unique=True)
    name = models.CharField(max_length=30)
    nickname = models.CharField(max_length=15, unique=True)
    phone_number = models.CharField(max_length=20)
    gender = models.CharField(choices=GenderChoices.choices, max_length=1)
    birthday = models.DateField()
    profile_image_url = models.URLField(null=True, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    role = models.CharField(choices=RoleChoices.choices, max_length=2,
	default=RoleChoices.USER)

# --- [ Category ] ---
class QuestionCategory(TimeStampedModel):
    CATEGORY_TYPES = (
        ("large", "대분류"),
        ("medium", "중분류"),
        ("small", "소분류"),
    )

    name = models.CharField(max_length=50, verbose_name="카테고리 이름")
    type = models.CharField(max_length=10, choices=CATEGORY_TYPES, default="large", verbose_name="카테고리 종류")
    parent = models.ForeignKey(
        "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children", verbose_name="부모 카테고리"
    )

1. author__nickname

  • 구조 분해
    • author: Question 모델에 정의된 ForeignKey 필드 (작성자)
    • __: Django에서 모델 간의 관계를 "따라가기(follow)" 위해 사용하는 약속된 구분자
    • nickname: 연결된 User 모델이 실제로 가지고 있는 속성(필드) 이름
  • 역할
    • 질문(Question)의 작성자(author) 객체로 건너가서, 그 안의 닉네임(nickname) 값을 가져와라.

2. category__name

  • 구조 분해
    • category: Question 모델의 ForeignKey 필드
    • __: 관계 연결 구분자
    • name: 연결된 QuestionCategory 모델의 필드 (카테고리명)
  • 역할
    • 이 질문이 속한 카테고리 객체를 찾고, 그 안의 이름(name)을 가져와라.

3. category__parent__name

  • 구조 분해
    • category: Question 모델의 카테고리 필드
    • __: 첫 번째 연결
    • parent: QuestionCategory 모델 내부의 부모 카테고리 필드
    • __: 두 번째 연결
    • name: 부모 카테고리의 이름 필드
  • 역할
    • 질문의 카테고리로 가서 -> 그 카테고리의 부모(parent)를 찾고 -> 그 부모의 이름(name)을 가져와라. (계층 구조 타고 올라가기)

4. questions__title (역참조)

  • 구조 분해
    • questions: Question 모델의 author 필드에 정의된 related_name="questions"
    • __: 관계 연결 구분자
    • title: Question 모델의 제목 필드
  • 역할
    • 유저(User) 입장에서, 본인이 작성한 모든 질문들(questions)을 찾아서 그 제목(title)을 조건으로 사용해라.
    • 예: User.objects.filter(questions__title__contains="오류") -> 제목에 '오류'가 들어간 질문을 쓴 유저 찾기

5. created_at__year (날짜 필드 조회)

  • 구조 분해
    • created_at: 생성일시(DateTimeField) 필드
    • __: 필드 속성 접근 구분자
    • year: 날짜 데이터의 내장 속성 (연도)
  • 역할
    • 작성일시 데이터에서 연도(year) 정보만 추출해서 비교하거나 조회해라.

profile
안녕하세요.

0개의 댓글