모델과 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
마이그레이션 되돌리기
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 인터페이스를 알아보겠습니다!