Skip to content

모델과 Django ORM 기초

FastAPI에서는 SQLAlchemy를 사용해 데이터베이스를 다뤘지만, Django는 자체 ORM(Object-Relational Mapping)을 제공합니다. Django ORM은 더 직관적이고 Django의 다른 기능들과 긴밀하게 통합되어 있습니다.

1. Django ORM vs SQLAlchemy

기본 개념 비교

SQLAlchemy (FastAPI):

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    email = Column(String, unique=True, index=True)

Django ORM:

from django.db import models

class User(models.Model):
    username = models.CharField(max_length=150, unique=True, db_index=True)
    email = models.EmailField(unique=True, db_index=True)
    # id는 자동 생성됨

2. Django 모델 필드 타입

주요 필드 타입

from django.db import models

class ExampleModel(models.Model):
    # 텍스트 필드
    char_field = models.CharField(max_length=100)  # 짧은 문자열
    text_field = models.TextField()  # 긴 텍스트
    email_field = models.EmailField()  # 이메일 검증 포함
    url_field = models.URLField()  # URL 검증 포함
    slug_field = models.SlugField()  # URL용 문자열

    # 숫자 필드
    integer_field = models.IntegerField()
    big_integer_field = models.BigIntegerField()
    small_integer_field = models.SmallIntegerField()
    float_field = models.FloatField()
    decimal_field = models.DecimalField(max_digits=10, decimal_places=2)

    # 불린 필드
    boolean_field = models.BooleanField(default=False)
    null_boolean_field = models.BooleanField(null=True)  # True/False/None

    # 날짜/시간 필드
    date_field = models.DateField()
    time_field = models.TimeField()
    datetime_field = models.DateTimeField()

    # 파일 필드
    file_field = models.FileField(upload_to='uploads/')
    image_field = models.ImageField(upload_to='images/')

    # 관계 필드는 아래에서 설명

필드 옵션

class Post(models.Model):
    title = models.CharField(
        max_length=200,
        unique=True,  # 유일한 값
        null=False,  # 데이터베이스 NULL 허용 여부
        blank=False,  # 폼 검증시 빈 값 허용 여부
        default='제목 없음',  # 기본값
        help_text='포스트 제목을 입력하세요',  # 도움말
        verbose_name='제목',  # 관리자 페이지 표시명
        db_index=True,  # 데이터베이스 인덱스 생성
    )

    created_at = models.DateTimeField(
        auto_now_add=True  # 생성시 자동 설정
    )

    updated_at = models.DateTimeField(
        auto_now=True  # 수정시 자동 업데이트
    )

3. 모델 관계 설정

1:N 관계 (ForeignKey)

from django.contrib.auth.models import User

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,  # 사용자 삭제시 포스트도 삭제
        related_name='posts'  # 역참조 이름
    )

class Comment(models.Model):
    post = models.ForeignKey(
        Post,
        on_delete=models.CASCADE,
        related_name='comments'
    )
    author = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,  # 사용자 삭제시 NULL 설정
        null=True
    )
    content = models.TextField()

1:1 관계 (OneToOneField)

class UserProfile(models.Model):
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        primary_key=True
    )
    bio = models.TextField(blank=True)
    birth_date = models.DateField(null=True, blank=True)

N:N 관계 (ManyToManyField)

class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField('Tag', related_name='posts')

    # 중간 테이블 커스터마이징
    likes = models.ManyToManyField(
        User,
        through='PostLike',
        related_name='liked_posts'
    )

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

class PostLike(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ['post', 'user']

4. 모델 메타 옵션

class Post(models.Model):
    title = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
    is_published = models.BooleanField(default=False)

    class Meta:
        ordering = ['-created_at']  # 기본 정렬
        verbose_name = '포스트'  # 단수형 이름
        verbose_name_plural = '포스트 목록'  # 복수형 이름
        db_table = 'blog_posts'  # 테이블 이름 지정
        unique_together = [['title', 'created_at']]  # 복합 유니크
        indexes = [
            models.Index(fields=['created_at', 'is_published']),
        ]

    def __str__(self):
        return self.title

5. 마이그레이션

Django는 모델 변경사항을 추적하고 데이터베이스에 반영합니다.

마이그레이션 생성 및 적용

# 마이그레이션 파일 생성
python manage.py makemigrations

# 생성된 마이그레이션 SQL 확인
python manage.py sqlmigrate blog 0001

# 마이그레이션 적용
python manage.py migrate

# 마이그레이션 상태 확인
python manage.py showmigrations

마이그레이션 되돌리기

# 특정 마이그레이션으로 되돌리기
python manage.py migrate blog 0002

6. 데이터 조회 (QuerySet)

기본 조회

from blog.models import Post

# 모든 객체 조회
all_posts = Post.objects.all()

# 특정 객체 조회
post = Post.objects.get(id=1)  # 없으면 DoesNotExist 예외
post = Post.objects.get(title='Django 시작하기')

# 첫 번째/마지막 객체
first_post = Post.objects.first()
last_post = Post.objects.last()

# 개수 확인
count = Post.objects.count()

# 존재 여부 확인
exists = Post.objects.filter(title='Django').exists()

필터링

# 조건 필터링
published_posts = Post.objects.filter(is_published=True)
recent_posts = Post.objects.filter(created_at__gte=datetime.now() - timedelta(days=7))

# 여러 조건 (AND)
posts = Post.objects.filter(
    is_published=True,
    author__username='john'
)

# OR 조건
from django.db.models import Q
posts = Post.objects.filter(
    Q(title__contains='Django') | Q(content__contains='Django')
)

# 제외
unpublished = Post.objects.exclude(is_published=True)

조회 조건 (Lookups)

# 정확히 일치
Post.objects.filter(title__exact='Django')

# 대소문자 구분 없이
Post.objects.filter(title__iexact='django')

# 포함
Post.objects.filter(title__contains='Django')
Post.objects.filter(title__icontains='django')  # 대소문자 구분 없이

# 시작/끝
Post.objects.filter(title__startswith='Django')
Post.objects.filter(title__endswith='Tutorial')

# 범위
Post.objects.filter(id__in=[1, 2, 3])
Post.objects.filter(created_at__range=['2024-01-01', '2024-12-31'])

# 비교
Post.objects.filter(view_count__gt=100)  # greater than
Post.objects.filter(view_count__gte=100)  # greater than or equal
Post.objects.filter(view_count__lt=100)  # less than
Post.objects.filter(view_count__lte=100)  # less than or equal

# NULL 체크
Post.objects.filter(deleted_at__isnull=True)

정렬

# 오름차순
Post.objects.order_by('created_at')

# 내림차순
Post.objects.order_by('-created_at')

# 여러 필드
Post.objects.order_by('-is_published', '-created_at')

관계 조회

# Foreign Key 관계 조회
comments = Comment.objects.filter(post__title='Django 시작하기')
posts = Post.objects.filter(author__username='john')

# 역참조
user = User.objects.get(username='john')
user_posts = user.posts.all()  # related_name 사용

# select_related: JOIN으로 한 번에 가져오기 (1:1, N:1)
posts = Post.objects.select_related('author').all()

# prefetch_related: 별도 쿼리로 가져오기 (M:N, 역참조)
posts = Post.objects.prefetch_related('comments').all()

7. 데이터 생성, 수정, 삭제

생성

# 방법 1: create()
post = Post.objects.create(
    title='새 포스트',
    content='내용',
    author=request.user
)

# 방법 2: 인스턴스 생성 후 save()
post = Post(
    title='새 포스트',
    content='내용',
    author=request.user
)
post.save()

# 방법 3: get_or_create()
post, created = Post.objects.get_or_create(
    title='Django 튜토리얼',
    defaults={'content': '기본 내용', 'author': user}
)

수정

# 단일 객체 수정
post = Post.objects.get(id=1)
post.title = '수정된 제목'
post.save()

# 여러 객체 한번에 수정
Post.objects.filter(is_published=False).update(is_published=True)

# F 객체로 현재 값 기준 수정
from django.db.models import F
Post.objects.filter(id=1).update(view_count=F('view_count') + 1)

삭제

# 단일 객체 삭제
post = Post.objects.get(id=1)
post.delete()

# 여러 객체 삭제
Post.objects.filter(created_at__lt='2023-01-01').delete()

8. 집계 함수

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

# 집계
stats = Post.objects.aggregate(
    total_posts=Count('id'),
    total_views=Sum('view_count'),
    avg_views=Avg('view_count'),
    max_views=Max('view_count')
)

# 그룹화
from django.db.models import Count
author_stats = Post.objects.values('author').annotate(
    post_count=Count('id'),
    total_views=Sum('view_count')
).order_by('-post_count')

9. 실습: 블로그 모델 구현

# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(unique=True)

    class Meta:
        verbose_name_plural = 'Categories'

    def __str__(self):
        return self.name

class Post(models.Model):
    STATUS_CHOICES = [
        ('draft', '초안'),
        ('published', '공개'),
    ]

    title = models.CharField(max_length=200)
    slug = models.SlugField(unique_for_date='created_at')
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='posts')
    content = models.TextField()
    excerpt = models.TextField(max_length=500, blank=True)

    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published_at = models.DateTimeField(null=True, blank=True)

    view_count = models.PositiveIntegerField(default=0)

    tags = models.ManyToManyField('Tag', blank=True, related_name='posts')

    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['-created_at', 'status']),
        ]

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('blog:post_detail', kwargs={'slug': self.slug})

    @property
    def is_published(self):
        return self.status == 'published'

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.name

# Shell에서 사용 예제
"""
python manage.py shell

from blog.models import Post, Category, Tag
from django.contrib.auth.models import User

# 카테고리 생성
category = Category.objects.create(name='Django', slug='django')

# 포스트 생성
user = User.objects.first()
post = Post.objects.create(
    title='Django ORM 마스터하기',
    slug='django-orm-master',
    author=user,
    category=category,
    content='Django ORM은 정말 강력합니다...',
    status='published'
)

# 태그 추가
tag1 = Tag.objects.create(name='Python', slug='python')
tag2 = Tag.objects.create(name='웹개발', slug='web-dev')
post.tags.add(tag1, tag2)

# 조회
published_posts = Post.objects.filter(status='published').select_related('author', 'category')
django_posts = Post.objects.filter(category__slug='django')
"""

10. 성능 최적화 팁

쿼리 최적화

# 나쁜 예: N+1 문제
posts = Post.objects.all()
for post in posts:
    print(post.author.username)  # 각 포스트마다 쿼리 실행

# 좋은 예: select_related 사용
posts = Post.objects.select_related('author').all()
for post in posts:
    print(post.author.username)  # JOIN으로 한 번에 가져옴

# ManyToMany는 prefetch_related
posts = Post.objects.prefetch_related('tags').all()

필요한 필드만 가져오기

# values()
posts = Post.objects.values('id', 'title', 'created_at')

# only()
posts = Post.objects.only('id', 'title', 'created_at')

# defer() - 특정 필드 제외
posts = Post.objects.defer('content')

정리

Django ORM의 특징: - 직관적인 API: 파이썬스러운 문법으로 데이터베이스 조작 - 자동 마이그레이션: 모델 변경사항 자동 추적 - 풍부한 필드 타입: 다양한 데이터 타입 지원 - 강력한 쿼리셋: 체이닝 가능한 쿼리 작성 - 관계 처리: ForeignKey, ManyToMany 등 쉬운 관계 설정

FastAPI/SQLAlchemy와 비교: - Django ORM은 Django와 긴밀하게 통합 - 마이그레이션이 프레임워크에 내장 - Admin 인터페이스와 자동 연동 - 더 간결한 문법으로 복잡한 쿼리 작성 가능

다음 장에서는 Django의 킬러 기능인 Admin 인터페이스를 알아보겠습니다!

Comments