목차9개 항목
ORM(Object-Relational Mapping)은 객체지향 언어의 객체와 관계형 DB의 테이블 사이에서 호환되지 않는 데이터를 변환해 주는 기법이다. Django는 내장 ORM으로 DB를 객체처럼 조작하게 해 준다.
| 장점 | 단점 |
|---|---|
| SQL을 몰라도 DB 조작 가능 | 복잡한 쿼리는 ORM만으로 표현하기 어려울 수 있음 |
| 절차적 SQL이 아닌 객체지향적 접근 → 생산성 향상 | 생성되는 SQL을 의식하지 않으면 비효율적 쿼리(N+1 등)가 생기기 쉬움 |
생성되는 SQL은 print(queryset.query)로 언제든 확인할 수 있다. SQL 문법 자체는 SQL 기초에 있다.
ORM ↔ SQL 대응표
예시 모델은 User(first_name, last_name, age, country, phone, balance), 테이블은 users_user다.
| ORM | SQL | 비고 |
|---|---|---|
User.objects.create(first_name='길동', ...) | INSERT INTO users_user (...) VALUES (...) | User(...).save()와 같다 |
User.objects.all() | SELECT * FROM users_user | QuerySet 반환 |
User.objects.get(pk=101) | ... WHERE id = 101 | 정확히 1건. 아래 주의 참고 |
user.last_name = '김'; user.save() | UPDATE ... SET ... WHERE id = 101 | 가져와서, 바꾸고, 저장 |
user.delete() | DELETE FROM ... WHERE id = 101 | |
filter(age=30, last_name='김') | WHERE age = 30 AND last_name = '김' | 키워드 인자는 AND |
filter(age__gte=30) | WHERE age >= 30 | __gt·__lt·__lte도 같은 방식 |
filter(phone__startswith='02-') | WHERE phone LIKE '02-%' | __endswith, __contains는 '%x', '%x%' |
filter(id__in=[1, 2]), filter(phone__isnull=True) | IN (1, 2), IS NULL | |
filter(Q(age=30) | Q(last_name='김')) | WHERE age = 30 OR last_name = '김' | Q로 OR(|)·AND(&)·NOT(~) |
order_by('balance', '-age') | ORDER BY balance ASC, age DESC | -는 내림차순, 앞 기준이 우선 |
[:10], [4] | LIMIT 10, LIMIT 1 OFFSET 4 | 슬라이싱이 LIMIT/OFFSET이 된다 |
count() | SELECT COUNT(*) | |
aggregate(Avg('age'), Max('age')) | SELECT AVG(age), MAX(age) | QuerySet 전체를 dict 하나로 |
values('country').annotate(n=Count('id')) | SELECT country, COUNT(id) ... GROUP BY country | 그룹마다 값을 붙인다 |
from django.db.models import Avg, Count, Max, Q
User.objects.filter(last_name='김').aggregate(Avg('age'), Max('age'))
# {'age__avg': 28.2, 'age__max': 64}: 이름을 주려면 avg_value=Avg('age')
User.objects.values('country').annotate(num=Count('id'), avg_balance=Avg('balance'))주의할 점
get()은 정확히 한 건
- 없으면
DoesNotExist, 여러 건이면MultipleObjectsReturned예외가 난다. 그래서 PK처럼 고유한 값으로만 쓴다. - view에서는
get_object_or_404(Model, pk=pk)를 쓰면 없을 때 예외 대신 404 응답이 된다. - 목록은 0건이어도 정상 상태다.
get_list_or_404보다filter()로 빈 QuerySet을 받는 편이 자연스럽다.
values()는 dict를 돌려준다
.values('first_name')은 모델 인스턴스가 아니라 딕셔너리의 QuerySet이다. .values(...).first()는 dict라서 user.phone처럼 속성으로 접근할 수 없고 ['first_name']으로 꺼낸다. 인스턴스가 필요하면 .values() 없이 .first()를 쓴다.
count() vs len()
.count()는 DB에서 COUNT(*)로 센다. len(User.objects.all())은 모든 행을 메모리로 가져온 뒤 센다. 개수만 필요하면 count()다. 이미 QuerySet을 순회할 예정이라면 len()이 추가 쿼리를 아낀다.
annotate()는 조회 결과에 컬럼을 덧붙일 뿐이다
DB를 바꾸지 않는다. 1:N 관계에서 부모마다 자식 수를 붙일 때 특히 쓸모 있다.
Article.objects.annotate(
total_count=Count('comment'),
old_count=Count('comment', filter=Q(comment__created_at__lte='2000-01-01')),
)제약 위반
필수 필드를 빼고 생성하면 NOT NULL constraint failed, 이미 있는 id로 넣으면 UNIQUE constraint failed가 난다. ORM이 SQL로 번역할 뿐이라 오류도 DB의 것이 그대로 올라온다.
자주 쓰는 추가 기법
| 하고 싶은 것 | ORM | 요점 |
|---|---|---|
| 같은 행의 다른 필드와 비교·계산 | filter(balance__gt=F('age')), update(balance=F('balance') + 100) | F는 값을 파이썬으로 가져오지 않고 SQL 안에서 계산한다. 동시 갱신 경쟁도 피한다 |
| 조건 제외 | exclude(country='KR') | WHERE NOT (...) |
| 서브쿼리 | annotate(last=Subquery(Order.objects.filter(user=OuterRef('pk')).values('created')[:1])) | OuterRef로 바깥 쿼리의 행을 참조한다 |
| 쿼리셋 합치기 | qs1.union(qs2), qs1 | qs2 | union은 SQL UNION이라 같은 컬럼 구성이 필요하다 |
| 필요한 컬럼만 | values_list('id', flat=True), only('name') | 큰 텍스트 컬럼을 읽지 않는다 |
| 중복 제거 | values('country').distinct() | |
| 여러 행 한 번에 생성 | bulk_create([...]) | INSERT 한 번. save()와 시그널을 거치지 않는다 |
| 정방향 FK를 조인으로 함께 | select_related('team') | 1:1·N:1을 JOIN 한 번으로. N+1을 막는다 |
| 역방향·M:N을 묶어서 | prefetch_related('orders') | 별도 쿼리 한 번에 IN (...)으로 모아 붙인다 |
| 대소문자 무시 정렬 | order_by(Lower('name')) | DB 함수(django.db.models.functions)를 정렬·주석에 쓴다 |
| DB에서 다시 읽기 | obj.refresh_from_db() | F로 갱신한 뒤 실제 값을 볼 때 |
| 쿼리 횟수 고정 | self.assertNumQueries(2, func) | N+1이 다시 생기면 테스트가 실패한다 |
모델 필드의 null=True는 DB 컬럼이 NULL을 허용한다는 뜻이고, blank=True는 폼 검증에서 빈 값을 허용한다는 뜻이다. 문자열 필드는 NULL과 빈 문자열 두 가지 "없음"이 생기지 않도록 null 없이 blank=True만 쓰는 것이 관례다. 질문 단위의 레시피 모음은 「장고 ORM 요리책」에 있다.
핵심 정리
Model.objects(Manager) → QuerySet API(all,filter,get,order_by,values…).- 단건은
get(pk=), 다건·조건은filter(), OR은Q. - 전체 집계는
aggregate(dict 반환), 그룹별 계산은values().annotate(). - 의심스러우면
queryset.query로 SQL을 본다. - 연관은
select_related(정방향 JOIN)·prefetch_related(역방향 IN 묶음)로 N+1을 막고,assertNumQueries로 고정한다.
관련 노트: SQL 기초, Django Model과 Migration, Django 모델 관계 (1:1, 1:N, M:N), JPA와 영속성 컨텍스트