Django ORM에서의 다중 테이블 관계 처리 및 고급 쿼리 활용

다중 테이블 모델 설계

Django ORM을 사용할 때, 데이터베이스 간의 관계는 주로 네 가지 유형으로 나뉜다: 일대일(OneToOne), 일대다(ForeignKey), 다대다(ManyToMany). 이러한 관계를 올바르게 정의하는 것은 애플리케이션 아키텍처의 핵심이다.

from django.db import models

class Publisher(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    address = models.CharField(max_length=64)
    phone_number = models.CharField(max_length=15)
    email = models.EmailField()

class Book(models.Model):
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=6, decimal_places=2)
    publication_date = models.DateTimeField(auto_now_add=True)
    
    # 일대다: 외래키는 '다'에 해당하는 테이블(Book)에 위치
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)

    # 다대다: 중간 테이블 자동 생성
    authors = models.ManyToManyField('Author')

class Author(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    age = models.PositiveSmallIntegerField()
    
    # 일대일: 작가 상세 정보 연결
    profile = models.OneToOneField('AuthorProfile', on_delete=models.CASCADE)

class AuthorProfile(models.Model):
    id = models.AutoField(primary_key=True)
    gender = models.SmallIntegerField()  # 0: female, 1: male
    address = models.CharField(max_length=64)
    phone = models.BigIntegerField()

Django 2.0 이상 버전에서는 ForeignKeyOneToOneField에 반드시 on_delete 인자를 명시해야 한다. 일반적으로 사용되는 값은 다음과 같다:

  • models.CASCADE: 참조된 객체 삭제 시 관련 레코드도 함께 삭제.
  • models.SET_NULL: 참조된 객체 삭제 시 외래키 필드를 NULL로 설정 (단, null=True 필요).
  • models.PROTECT: 관련 레코드 존재 시 삭제 방지.

데이터 삽입 연산

일대일 관계 삽입

# 프로필 먼저 생성
profile = AuthorProfile.objects.create(
    gender=1,
    address="서울 강남구",
    phone=1012345678
)

# 작가 생성 시 프로필 연결
Author.objects.create(
    name="김철수",
    age=35,
    profile=profile
)

# 또는 ID 기반 연결
author = Author.objects.get(name="김철수")
author.profile_id = profile.id
author.save()

일대다 관계 삽입

publisher = Publisher.objects.get(id=1)

Book.objects.create(
    title="파이썬 입문",
    price=25000,
    publisher=publisher  # 객체 전달
)

# 혹은 외래키 ID 직접 지정
Book.objects.create(
    title="장고 웹 개발",
    price=32000,
    publisher_id=1  # ID만으로도 가능
)

다대다 관계 조작

다대다 관계는 중간 테이블을 직접 다루지 않고, 매니저를 통해 조작한다.

# 책 선택
book = Book.objects.get(title="디자인 패턴")

# 여러 저자 추가 (객체 또는 ID 모두 가능)
author1 = Author.objects.get(name="이영희")
author2 = Author.objects.get(name="박민수")
book.authors.add(author1, author2)
# 또는 book.authors.add(1, 2)

# 특정 저자 제거
book.authors.remove(1)

# 전체 작성자 초기화 후 재설정
book.authors.clear()
book.authors.set([3, 4])  # ID 리스트로 설정

데이터 삭제

  • 일대일: 외래키를 가진 쪽(예: Author) 삭제 시 다른 테이블(AuthorProfile)에는 영향 없음. 반대로 Profile 삭제 시 연결된 Author도 삭제 여부는 on_delete 설정에 따라 달라짐.
  • 일대다: Publisher 삭제 시 관련된 모든 Book 레코드가 CASCADE 설정 하에 자동 삭제됨.
  • 다대다: 중간 테이블의 연결만 제거되며, 원본 레코드는 유지된다.

데이터 수정

# 특정 작가 정보 업데이트
Author.objects.filter(id=1).update(
    name="홍길동",
    age=28,
    profile_id=5  # 프로필 변경
)

# 책 출판사 변경
Book.objects.filter(title="장고 실전").update(publisher_id=2)

크로스 테이블 조회

객체 기반 탐색 (정방향/역방향)

관계 필드가 정의된 쪽에서 타겟 테이블로 접근하면 정방향, 반대 방향은 역방향이라 한다.

# 정방향 예시: 책 → 출판사
book = Book.objects.get(id=1)
print(book.publisher.name)

# 역방향 예시: 출판사 → 관련된 모든 책들
publisher = Publisher.objects.get(name="한빛미디어")
books = publisher.book_set.all()  # 테이블명 소문자 + _set
for b in books:
    print(b.title)

# 일대일 정방향: 작가 → 프로필
author = Author.objects.get(name="이영희")
print(author.profile.address)

# 역방향: 프로필 → 작가
profile = AuthorProfile.objects.get(id=1)
print(profile.author.name)

# 다대다 정방향: 책 → 저자 목록
book = Book.objects.get(title="알고리즘")
authors = book.authors.all()
for a in authors:
    print(a.name)

# 역방향: 저자 → 집필한 책들
author = Author.objects.get(name="박민수")
authored_books = author.book_set.all()

더블 언더스코어(__) 기반 조인 쿼리

단일 쿼리로 여러 테이블을 조인하여 결과를 가져올 수 있다.

# '한국출판사'에서 발행한 책 제목과 가격
result = Book.objects.filter(publisher__name='한국출판사').values('title', 'price')

# 이름이 '김철수'인 작가의 전화번호 조회
result = Author.objects.filter(name='김철수').values('profile__phone')

# 전화번호가 '010'으로 시작하는 작가가 쓴 책 제목과 출판사명
result = AuthorProfile.objects.filter(phone__startswith='010')\
    .values('author__book__title', 'author__book__publisher__name')

집계 및 그룹화

from django.db.models import Avg, Count, Max, Min, Sum

# 전체 책 평균 가격
avg_price = Book.objects.aggregate(avg_price=Avg('price'))

# 출판사별 평균 가격
publisher_avg = Book.objects.values('publisher__name')\
    .annotate(avg=Avg('price')).values('publisher__name', 'avg')

# 두 명 이상의 저자가 참여한 책 목록
multi_author_books = Book.objects.annotate(author_count=Count('authors'))\
    .filter(author_count__gt=1).values('title', 'author_count')

# 가격이 20,000원 초과이고 저자 수가 2명 이상인 책
filtered_books = Book.objects.filter(price__gt=20000)\
    .annotate(count=Count('authors'))\
    .filter(count__gt=1).values('title', 'count')

F 및 Q 객체 활용

F 객체: 필드 간 비교

from django.db.models import F

# 조회수(read_count)보다 댓글 수(comment_count)가 많은 책
popular_comment_books = Book.objects.filter(comment_count__gt=F('read_count'))

# 모든 책의 가격을 10% 인상
Book.objects.update(price=F('price') * 1.1)

Q 객체: 복합 조건 쿼리

from django.db.models import Q

# 이름이 '파이썬' 포함 또는 가격이 30,000원 이상
result = Book.objects.filter(Q(title__contains='파이썬') | Q(price__gte=30000))

# '자료구조' 제외하고 가격이 25,000원 이하
affordable = Book.objects.filter(~Q(title__contains='자료구조'), price__lte=25000)

# 중첩 조건: (제목에 '웹' 포함 AND 가격 낮음) OR ID가 5 미만
complex_query = Book.objects.filter(
    (Q(title__contains='웹') & Q(price__lt=20000)) | Q(id__lt=5)
)

원시 SQL 실행

복잡한 쿼리는 ORM으로 표현하기 어려울 수 있으므로, raw SQL을 사용할 수 있다.

# Raw Query 사용
books = Book.objects.raw('SELECT * FROM myapp_book WHERE price > %s', [25000])
for b in books:
    print(b.title)

# JOIN 포함 쿼리
authors_with_gender = Author.objects.raw('''
    SELECT a.*, ap.gender 
    FROM myapp_author a 
    JOIN myapp_authorprofile ap ON a.profile_id = ap.id
    WHERE ap.gender = 1
''')

성능 최적화 기법

  • select_related(): ForeignKey/OneToOne 관계 사전 로딩 (JOIN 사용).
  • prefetch_related(): ManyToMany 및 역방향 외래키 관계를 별도 쿼리로 가져와 메모리에서 조인.
  • only()/defer(): 필요한 필드만 선택하거나 제외하여 네트워크 부하 감소.
# select_related 예시 (일대다, 일대일)
books_with_publisher = Book.objects.select_related('publisher').all()

# prefetch_related 예시 (다대다)
books_with_authors = Book.objects.prefetch_related('authors').all()

# 특정 필드만 로드
titles_only = Book.objects.only('title')

# 특정 필드 제외
without_price = Book.objects.defer('price')

트랜잭션 처리

여러 DB 작업을 하나의 논리적 단위로 묶어 원자성을 보장한다.

from django.db import transaction

try:
    with transaction.atomic():
        profile = AuthorProfile.objects.create(address="부산", phone=1099998888, gender=0)
        Author.objects.create(name="정우성", age=40, profile=profile)
except Exception as e:
    # 자동 롤백
    print("트랜잭션 실패:", e)

다대다 관계 구현 방법 비교

방식 특징 장점 단점
자동 중간 테이블 ManyToManyField 사용 간편, add/remove/set 등 편의 메서드 제공 중간 테이블 확장 불가
직접 중간 테이블 정의 별도 모델 생성 추가 필드(ex: 작성일) 추가 가능 쿼리 복잡, ORM 편의 기능 사용 불가
中介 모드 (through) through + through_fields 확장성과 ORM 기능 동시 활용 가능 add()/remove() 제한적 사용

ORM 성능 최적화 요약

  • 불필요한 쿼리 반복 방지를 위해 select_relatedprefetch_related 활용.
  • 인덱스 전략: 자주 검색/정렬되는 컬럼, 외래키에 인덱스 생성.
  • 대량 데이터 삽입 시 bulk_create() 사용.
  • 결과 정렬은 꼭 필요한 경우에만 수행.
  • 실시간 분석이 아닌 경우 캐싱 도입을 고려.

태그: Django ORM Database ManyToMany ForeignKey

5월 20일 16:37에 게시됨