Django ORM 모델 계층 완벽 가이드

모델 계층 구축

데이터베이스 연결 설정

Django는 기본적으로 SQLite3를 사용하지만, 시간 필드 처리에 민감하지 않아 일반적으로 다른 데이터베이스를 선택합니다.

가장 흔히 사용되는 MySQL 데이터베이스 연결을 위해 설정 파일을 수정해야 합니다:

# settings.py 설정 변경
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'my_database',
        'HOST': '127.0.0.1',
        'PORT': 3306,
        'USER': 'db_user',
        'PASSWORD': 'secure_password',
        'CHARSET': 'utf8'
    }
}

참고: MySQL 첫 연결시 오류가 발생할 수 있으며, mysqlclient 모듈 설치가 필요합니다.

모델 클래스 정의

ORM을 사용하면 클래스와 객체를 이용해 테이블과 레코드를 매핑할 수 있습니다. 이러한 클래스는 models.py 파일에 정의합니다:

from django.db import models

class Member(models.Model):
    # 필드명 = 필드타입 + 제약조건
    member_id = models.AutoField(primary_key=True)
    username = models.CharField(max_length=32)
    age = models.IntegerField()

이 클래스는 Member 테이블을 생성하며, 세 개의 필드(자동 증가 기본 키 포함)를 가집니다.

터미널에서 manage.py 명령어를 실행하여 마이그레이션을 수행합니다:

python manage.py makemigrations  # 데이터베이스 변경사항 기록
python manage.py migrate  # 변경사항을 데이터베이스에 적용

독립 테스트 환경 구성

Django는 기본적으로 단일 Python 파일의 독립 실행을 허용하지 않습니다. 특히 모델 계층 테스트 시 전체 프로젝트를 실행하는 것은 번거롭습니다.

PyCharm 콘솔을 이용한 즉시 테스트(코드 저장 불가) 또는 자체 환경을 구성할 수 있습니다:

import os

def setup_django():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
    import django
    django.setup()

if __name__ == '__main__':
    setup_django()
    # 테스트 코드 작성

ORM 기본 사용법

ORM SQL 확인 방법

  • ORM 쿼리는 보통 QuerySet 객체를 반환하며, 이 객체의 query 속성에 실제 SQL 문이 저장됩니다:
result = models.Member.objects.filter()
print(result.query)
# 출력: SELECT `app01_member`.`member_id`, `app01_member`.`username`, `app01_member`.`age` FROM `app01_member`

참고: 모든 ORM 메서드가 QuerySet을 반환하는 것은 아닙니다.

  • 설정 파일에 다음 코드를 추가하면 모든 데이터베이스 작업 시 SQL 문이 출력됩니다:
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console':{
            'level':'DEBUG',
            'class':'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'propagate': True,
            'level':'DEBUG',
        },
    }
}

기본 CRUD 메서드

메서드 반환값
create(필드명=값) 생성된 레코드 객체
filter(조건) QuerySet 리스트 객체
filter().update(수정내용) 영향 받은 행 수
filter().delete() 영향 받은 행 수 (딕셔너리 형태)

QuerySet 반환 메서드

메서드 설명 비고
all() 모든 레코드 조회
filter() 조건에 맞는 레코드 조회 괄호에 조건 명시
exclude() 조건에 맞지 않는 레코드 조회 괄호에 조건 명시
order_by() 특정 필드 기준 정렬 괄호에 필드명 명시
reverse() 정렬된 결과 역순 정렬 정렬된 QuerySet에만 적용 가능
distinct() 중복 제거 주로 values()와 함께 사용
raw(SQL) 원시 SQL 실행 실제 테이블명과 모델 클래스명 구분 주의

특수 QuerySet 반환 메서드

메서드 설명 반환 형태
values() 특정 필드만 조회 <QuerySet [{}, {}, {}]> (딕셔너리)
values_list() 특정 필드만 조회 <QuerySet [(), (), ()]> (튜플)

객체 반환 메서드

메서드 설명 반환값
create(**kwargs) 레코드 삽입 생성된 객체
QuerySet.first() 첫 번째 레코드 객체 또는 None
QuerySet.last() 마지막 레코드 객체 또는 None
QuerySet.get(조건) 조건에 맞는 레코드 객체 (없으면 오류)

조건 필터링

기본 필터

filter()에서 기본 조건은 = (동등)이며, 여러 조건은 AND로 처리됩니다.

Django에서는 필드명 뒤에 __조건을 붙여 다양한 조건을 표현합니다:

# 크기 비교
Member.objects.filter(age__gt=18)   # age > 18
Member.objects.filter(age__lt=18)   # age < 18
Member.objects.filter(age__gte=18)  # age >= 18
Member.objects.filter(age__lte=18)  # age <= 18

# IN 연산
Member.objects.filter(age__in=(18, 32, 34))

# 범위 (BETWEEN)
Member.objects.filter(age__range=(18, 38))

# LIKE 검색
Member.objects.filter(username__contains="lee")      # 대소문자 구분
Member.objects.filter(username__icontains="lee")     # 대소문자 미구분

# 날짜 필터
Member.objects.filter(join_date__year=2022)
Member.objects.filter(join_date__month=12)

F와 Q 객체

F 객체

두 필드를 비교하는 조건에 사용합니다.

from django.db.models import F

# 판매량이 재고보다 많은 책 조회
Book.objects.filter(sales__gt=F("stock")).values("title")

Q 객체

OR, NOT 조건을 구현할 때 사용합니다.

from django.db.models import Q

# 기본 키가 1이거나 가격이 2000 이상인 책
Book.objects.filter(Q(pk=1) | Q(price__gt=2000)).values("title")

# 기본 키가 1이 아닌 책
Book.objects.filter(~Q(pk=1)).values("title")
Q 객체 고급 활용
q_obj = Q()
q_obj.connector = 'or'  # 기본 AND -> OR로 변경
q_obj.children.append(("pk", 1))
q_obj.children.append(("price__gt", 50))

result = Book.objects.filter(q_obj)
print(result)

외래 키

모델에서 외래 키 정의

class Book(models.Model):
    title = models.CharField(max_length=32)
    price = models.FloatField()
    # 1:N 관계
    publisher = models.ForeignKey(to="Publisher", on_delete=models.CASCADE)
    # M:N 관계
    authors = models.ManyToManyField(to="Author")

class Publisher(models.Model):
    name = models.CharField(max_length=32)
    address = models.CharField(max_length=64)

class Author(models.Model):
    name = models.CharField(max_length=32)
    # 1:1 관계
    profile = models.OneToOneField(to="AuthorProfile", on_delete=models.CASCADE)

class AuthorProfile(models.Model):
    phone = models.CharField(max_length=16)
    address = models.CharField(max_length=64)

외래 키를 활용한 관계 설정

# 1:N 및 1:1 관계
AuthorProfile.objects.create(phone='1111', address="Seoul")
Author.objects.create(name="Kim", profile_id=1)

profile = AuthorProfile.objects.filter(pk=2).first()
Author.objects.filter(name="Kim").update(profile=profile)

# M:N 관계
book = Book.objects.filter(pk=1).first()
book.authors.add(1, 5)  # 기본 키로 관계 추가
author = Author.objects.filter(pk=7).first()
book.authors.add(author)  # 객체로 관계 추가

book.authors.set([6, 7, 8])  # 기존 관계 삭제 후 새 관계 설정
book.authors.remove(1)  # 관계 제거

크로스 테이블 쿼리

방향 객체 기반 필드 기반
정방향 book_obj.publisher.name Book.objects.filter(title="...").values("publisher__name")
역방향 publisher_obj.book_set.all() Publisher.objects.filter(name="...").values("book__title")

M:N 관계 심화

완전 자동, 수동, 그리고 최적화된 수동 방식이 있습니다:

# 수동 방식 (확장 가능)
class Book(models.Model):
    title = models.CharField(max_length=32)

class Author(models.Model):
    name = models.CharField(max_length=32)

class BookAuthor(models.Model):
    book = models.ForeignKey(to='Book', on_delete=models.CASCADE)
    author = models.ForeignKey(to='Author', on_delete=models.CASCADE)
    join_date = models.DateField(auto_now_add=True)

# 최적화된 수동 방식
class Book(models.Model):
    title = models.CharField(max_length=32)
    authors = models.ManyToManyField(to='Author',
                                     through='BookAuthor',
                                     through_fields=('book', 'author'))

class Author(models.Model):
    name = models.CharField(max_length=32)

class BookAuthor(models.Model):
    book = models.ForeignKey(to='Book', on_delete=models.CASCADE)
    author = models.ForeignKey(to='Author', on_delete=models.CASCADE)
    join_date = models.DateField(auto_now_add=True)

집계 함수와 그룹 쿼리

집계 함수

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

result = Book.objects.aggregate(
    max_price=Max('price'),
    min_price=Min('price'),
    total=Sum('price'),
    avg_price=Avg('price'),
    count=Count('pk')
)
print(result)
# {'max_price': 99.0, 'min_price': 99.0, 'total': 396.0, 'avg_price': 99.0, 'count': 4}

그룹 쿼리

# 필드 기준 그룹화
Book.objects.values("publisher_id").annotate(Min("price"))

# 테이블 기준 그룹화
Publisher.objects.annotate(min_price=Min("book__price")).values("name", "min_price")

# HAVING 조건 (filter 사용)
Book.objects.annotate(author_count=Count('authors__pk'))\
    .filter(author_count__gt=1)\
    .values("title", "author_count")

쿼리 최적화

지연 로딩 (Lazy Loading)

result = Book.objects.filter()  # 아직 쿼리 실행 안 됨
# print(result)  # 이 시점에서 쿼리 실행

only()와 defer()

# only: 특정 필드만 로드
result = Book.objects.only("title", "price")
for book in result:
    print(book.title)  # 추가 쿼리 없음
    # print(book.author)  # 추가 쿼리 발생

# defer: 특정 필드 제외하고 로드
result = Book.objects.defer("price")
for book in result:
    print(book.title)  # 추가 쿼리 없음
    # print(book.price)  # 추가 쿼리 발생

select_related와 prefetch_related

# select_related: JOIN 사용
result = Book.objects.select_related("publisher")
for book in result:
    print(book.publisher.name)  # 추가 쿼리 없음

# prefetch_related: 서브쿼리 사용
result = Book.objects.prefetch_related("publisher")
for book in result:
    print(book.publisher.name)  # 추가 쿼리 없음

벌크 연산

from apps import models

def bulk_create_view(request):
    data_list = [{"title": f"Book {i}", "price": i * 10} for i in range(100000)]
    # 비효율적 방법
    # for data in data_list:
    #     models.Book.objects.create(**data)

    # 효율적 방법
    book_objects = [models.Book(**data) for data in data_list]
    models.Book.objects.bulk_create(book_objects)

트랜잭션

전역 트랜잭션

DATABASES = {
    'default': {
        'ATOMIC_REQUESTS': True,  # 모든 요청이 하나의 트랜잭션
    }
}

데코레이터

from django.db import transaction
from django.db.models import F

@transaction.atomic
def transfer_view(request):
    models.User.objects.filter(pk=1).update(account=F("account") + 30)
    # 오류 발생 시 자동 롤백
    models.User.objects.filter(pk=2).update(account=F("account") - 30)
    return HttpResponse("이체 완료")

컨텍스트 매니저

with transaction.atomic():
    models.Book.objects.filter(pk=1).update(price=F("price") + 30)
    models.Book.objects.filter(pk=2).update(price=F("price") - 30)

주요 필드 타입

필드 타입 파라미터 설명
AutoField primary_key 자동 증가 기본 키
CharField max_length 고정 길이 문자열
IntegerField/BigIntegerField 정수형
DecimalField max_digits, decimal_places 소수점 포함 숫자
DateField/DateTimeField auto_now, auto_now_add 날짜/시간
BooleanField True/False 저장 (DB: 1/0)
TextField 대용량 텍스트
EmailField 이메일 형식 검증
FileField upload_to 파일 업로드 (DB: 경로 저장)
ForeignKey/OneToOneField to, to_field, on_delete 외래 키 관계
ManyToManyField to, to_field 다대다 관계

사용자 정의 필드

class CharFieldCustom(models.Field):
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        super().__init__(max_length=max_length, *args, **kwargs)

    def db_type(self, connection):
        return 'char(%s)' % self.max_length

class User(models.Model):
    name = models.CharField(max_length=32)
    info = CharFieldCustom(max_length=64)

주요 필드 파라미터

공통 파라미터

파라미터 설명
primary_key 기본 키 설정
verbose_name 필드 설명 (관리자 페이지 등)
null NULL 허용 여부
default 기본값
unique 고유성 제약
db_index 데이터베이스 인덱스 추가

특수 파라미터

  • choices: 열거 가능한 필드에 사용 (성별, 학력 등)
class User(models.Model):
    GENDER = (
        (1, 'Male'),
        (2, 'Female'),
    )
    gender = models.IntegerField(choices=GENDER, null=True)

user = User.objects.filter(pk=1).first()
user.gender  # 실제 저장값 (1)
user.get_gender_display()  # 'Male'

외래 키 관련 파라미터

파라미터 설명
to 연결할 모델
to_field 연결할 필드 (기본: 기본 키)
on_delete 연결된 데이터 삭제 시 동작
related_name 역방향 쿼리명 (기본: 모델명_set)
related_query_name 역방향 필드명
db_constraint 데이터베이스 외래 키 제약 생성 여부 (기본: True)

on_delete 옵션

  1. CASCADE: 참조하는 데이터도 함께 삭제
  2. SET_NULL: NULL로 설정 (필드는 null=True여야 함)
  3. PROTECT: 삭제 불가 (ProtectedError 발생)
  4. SET_DEFAULT: 기본값으로 설정 (필드에 default 필요)
  5. SET(value): 지정된 값으로 설정
  6. DO_NOTHING: 아무 작업 안 함 (데이터베이스 수준 제약에 따름)

태그: Django ORM models python MySQL

8월 1일 14:24에 게시됨