ORM 심화 — QuerySet API
Django ORM은 Python 코드로 SQL을 생성합니다. QuerySet은 지연 평가(lazy evaluation)되어 실제 SQL은 필요한 시점에 실행됩니다.
모델 정의
# products/models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
class Meta:
verbose_name_plural = "categories"
ordering = ["name"]
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
related_name="products",
)
owner = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="products",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["category", "is_active"]),
]
def __str__(self):
return self.name
class Review(models.Model):
product = models.ForeignKey(
Product, on_delete=models.CASCADE, related_name="reviews"
)
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="reviews"
)
rating = models.PositiveSmallIntegerField()
comment = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
QuerySet 기초 CRUD
from products.models import Product, Category
# CREATE
product = Product.objects.create(
name="Python 책",
price=35000,
stock=100,
category=Category.objects.get(slug="books"),
owner_id=1,
)
# READ — 지연 평가
all_products = Product.objects.all() # SELECT * FROM products
active = Product.objects.filter(is_active=True) # WHERE is_active=1
# 단건 조회
try:
product = Product.objects.get(pk=1) # 없으면 DoesNotExist
except Product.DoesNotExist:
product = None
product = Product.objects.filter(pk=1).first() # 없으면 None
# UPDATE
Product.objects.filter(pk=1).update(stock=50) # SQL UPDATE (save() 미호출)
product.name = "파이썬 완전정복"
product.save() # 전체 필드 저장
# DELETE
Product.objects.filter(is_active=False).delete()
product.delete()
필터링 — 조회 연산자
from django.db.models import Q
from decimal import Decimal
# 비교 연산자
Product.objects.filter(price__gt=10000) # price > 10000
Product.objects.filter(price__gte=10000) # price >= 10000
Product.objects.filter(price__lt=50000) # price < 50000
Product.objects.filter(price__range=(10000, 50000)) # BETWEEN
# 문자열 검색
Product.objects.filter(name__icontains="파이썬") # LIKE '%파이썬%' (대소문자 무시)
Product.objects.filter(name__startswith="Python")
Product.objects.filter(name__regex=r"^Py.*$")
# NULL 조회
Product.objects.filter(category__isnull=True)
Product.objects.exclude(category__isnull=True)
# 관계 필터 (외래키)
Product.objects.filter(category__name="도서") # JOIN
Product.objects.filter(owner__email__endswith="@example.com")
# 복합 조건 (AND)
Product.objects.filter(is_active=True, stock__gt=0)
Product.objects.filter(is_active=True).filter(stock__gt=0)
# OR 조건 — Q 객체
Product.objects.filter(
Q(price__lt=10000) | Q(name__icontains="무료")
)
# NOT 조건
Product.objects.filter(~Q(is_active=False))
Product.objects.exclude(is_active=False)
# 정렬
Product.objects.order_by("price") # 오름차순
Product.objects.order_by("-price") # 내림차순
Product.objects.order_by("category__name", "-created_at") # 다중 정렬
select_related / prefetch_related — N+1 해결
# ❌ N+1 문제: 상품 100개 → SQL 101개 실행
products = Product.objects.all()
for product in products:
print(product.category.name) # 매번 추가 쿼리 발생
# ✅ select_related: ForeignKey/OneToOne — JOIN으로 한 번에 가져옴
products = Product.objects.select_related("category", "owner").all()
for product in products:
print(product.category.name) # 추가 쿼리 없음
# ✅ prefetch_related: ManyToMany/역참조 — 별도 쿼리 후 Python에서 합침
products = Product.objects.prefetch_related("reviews").all()
for product in products:
print(product.reviews.count()) # 추가 쿼리 없음
# 복합 최적화
from django.db.models import Prefetch
products = Product.objects.select_related(
"category", "owner"
).prefetch_related(
Prefetch(
"reviews",
queryset=Review.objects.select_related("author").order_by("-created_at"),
)
).filter(is_active=True)
# 쿼리 수 확인 (개발용)
from django.db import connection
def print_query_count():
print(f"실행된 쿼리 수: {len(connection.queries)}")
# settings.py에서: LOGGING 또는 django-debug-toolbar 사용
집계와 어노테이션
from django.db.models import Count, Avg, Sum, Max, Min, F, Value
from django.db.models.functions import Coalesce
# aggregate: 전체 집계
from products.models import Product
stats = Product.objects.filter(is_active=True).aggregate(
total=Count("id"),
avg_price=Avg("price"),
max_price=Max("price"),
total_stock=Sum("stock"),
)
print(stats)
# {'total': 50, 'avg_price': 25000.0, 'max_price': 99000, 'total_stock': 5000}
# annotate: 행별 집계 추가
from products.models import Category
categories = Category.objects.annotate(
product_count=Count("products"),
avg_price=Avg("products__price"),
).filter(product_count__gt=0).order_by("-product_count")
for cat in categories:
print(f"{cat.name}: {cat.product_count}개, 평균 {cat.avg_price:,.0f}원")
# F 표현식: 필드 간 비교·연산
from django.db.models import F
# stock이 reorder_point보다 낮은 상품
low_stock = Product.objects.filter(stock__lt=F("reorder_point"))
# 가격 10% 인상
Product.objects.filter(category__slug="books").update(
price=F("price") * 1.1
)
커스텀 Manager
from django.db import models
class ActiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
def in_stock(self):
return self.get_queryset().filter(stock__gt=0)
class Product(models.Model):
# ...
objects = models.Manager() # 기본 매니저
active = ActiveManager() # 커스텀 매니저
# 사용
Product.active.all() # is_active=True만
Product.active.in_stock() # is_active=True AND stock > 0
정리
| 기능 | 방법 |
|---|---|
| 단건 조회 | .get(pk=1) / .filter().first() |
| 복합 조건 OR | Q(a=1) | Q(b=2) |
| ForeignKey JOIN | select_related("category") |
| 역참조 배치 | prefetch_related("reviews") |
| 전체 집계 | .aggregate(total=Count("id")) |
| 행별 집계 | .annotate(cnt=Count("reviews")) |
| 필드 간 연산 | F("price") * 1.1 |
| 커스텀 조회 | Manager.get_queryset() 오버라이드 |
select_related와 prefetch_related만 잘 써도 Django 앱 성능의 80%가 해결됩니다.