내일배움캠프 7주차 WIL
ManyToManyField를 통한 데이터 조작
1. 게시글에 태그 추가하기
post = Post.objects.create(title="Django 튜토리얼", content="Django는 강력합니다.")
tag1 = Tag.objects.create(name="Django")
tag2 = Tag.objects.create(name="Python")
# 태그 추가
post.tags.add(tag1, tag2)
2. 게시글의 태그 가져오기
post = Post.objects.get(id=1)
tags = post.tags.all() # 해당 게시글의 모든 태그 가져오기
for tag in tags:
print(tag.name)
3. 태그에서 게시글 검색 (역참조)
tag = Tag.objects.get(name="Django")
posts_with_tag = tag.posts.all()
for post in posts_with_tag:
print(post.title)
4. 태그 제거하기
post.tags.remove(tag1)
5. 모든 태그 삭제
post.tags.clear()
6. 특정 태그가 있는지 확인
if tag1 in post.tags.all():
print("이 태그가 포함되어 있습니다.")
폼에서 ManyToManyField 처리 방법
Django의 ModelForm에서는 ManyToManyField를 기본적으로 SelectMultiple 위젯으로 처리합니다.
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'tags', 'image']
템플릿 예시 (post_form.html)
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">저장</button>
</form>
태그 선택 UI 개선:
- forms.CheckboxSelectMultiple를 사용하여 태그를 체크박스로 표시할 수도 있습니다.
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'tags', 'image']
widgets = {
'tags': forms.CheckboxSelectMultiple(),
}
커스텀 중개 테이블 사용하기
기본적으로 Django는 자동으로 중간 테이블을 생성하지만, 추가 정보를 저장하려면 중개 모델을 직접 정의할 수 있습니다.
class PostTag(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('post', 'tag')
그 후 ManyToManyField를 정의할 때 through 옵션을 추가합니다.
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
tags = models.ManyToManyField(Tag, through='PostTag')
사용 예시:
post = Post.objects.create(title="새로운 글", content="내용")
tag = Tag.objects.create(name="Python")
PostTag.objects.create(post=post, tag=tag)
쿼리 최적화 (Prefetching)
ManyToMany 관계는 기본적으로 추가적인 쿼리를 발생시킵니다. 성능 최적화를 위해 prefetch_related를 사용할 수 있습니다.
posts = Post.objects.prefetch_related('tags').all()
for post in posts:
print(post.title, post.tags.all()) # 최소 쿼리 발생
ManyToManyField 사용 시 팁
- 최적화 필요:
- select_related()는 외래 키에 대해 유용하고, prefetch_related()는 다대다 관계에 효과적입니다.
- 데이터 일관성 유지:
- ManyToManyField를 사용하면 중복 관계가 방지되지만, 추가적인 제약 조건이 필요할 경우 커스텀 중개 모델을 사용하는 것이 좋습니다.
- 폼에서의 사용자 경험 개선:
- CheckboxSelectMultiple 등을 사용하여 태그 선택을 직관적으로 개선할 수 있습니다.
Django의 ManyToManyField와 ForeignKey는 데이터베이스의 관계를 정의하는 데 사용되지만, 각각의 용도가 다르고 동작 방식에도 차이가 있습니다. 두 필드를 비교하면서 언제 어떤 것을 사용해야 하는지 살펴보겠습니다.
1. 개요 및 차이점
특징 | ManyToManyField | ForeignKey |
관계 유형 | 다대다 (Many-to-Many) | 일대다 (One-to-Many) |
중간 테이블 생성 | 자동 생성되거나 커스텀 가능 | 추가적인 테이블 필요 없음 |
데이터 저장 방식 | 중간 테이블에 두 개의 외래 키 저장 | 외래 키(FK)를 직접 저장 |
관계 참조 방법 | model.related_name.all() | model.foreign_key |
성능 | 쿼리 최적화 필요 (prefetch_related) | 성능 우수 (select_related 사용 가능) |
예제 | Post.tags.add(tag1, tag2) | Comment.objects.filter(post=post1) |
2. ForeignKey (일대다 관계)
설명
ForeignKey는 한 개의 객체가 여러 개의 관련 객체를 가질 수 있는 "일대다(1:N)" 관계를 정의합니다.
예제
모델 정의 (Post와 Comment)
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
text = models.TextField()
설명
- 하나의 Post는 여러 개의 Comment를 가질 수 있습니다.
- post 필드가 외래 키 역할을 하며, 각 댓글은 특정 게시글과 연결됩니다.
- on_delete=models.CASCADE는 게시글이 삭제될 경우 연결된 댓글도 삭제됨을 의미합니다.
데이터 조작 예시
post = Post.objects.create(title="Django 관계 이해", content="ForeignKey vs ManyToManyField")
# 댓글 생성 (일대다 관계)
comment1 = Comment.objects.create(post=post, text="첫 번째 댓글")
comment2 = Comment.objects.create(post=post, text="두 번째 댓글")
# 해당 게시글의 모든 댓글 가져오기 (역참조)
comments = post.comments.all()
for comment in comments:
print(comment.text)
쿼리 최적화
posts_with_comments = Post.objects.select_related('comments').all()
- select_related는 외래 키 관계에서 SQL JOIN을 사용하여 쿼리 최적화를 수행합니다.
3. ManyToManyField (다대다 관계)
설명
ManyToManyField는 객체가 여러 개의 관련 객체를 가질 수 있으며, 반대로 관련 객체도 여러 개의 객체와 연결될 수 있는 "다대다(N:N)" 관계를 정의합니다.
예제
모델 정의 (Post와 Tag)
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class Post(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField(Tag, related_name='posts')
데이터 조작 예시
post = Post.objects.create(title="Django 튜토리얼")
tag1 = Tag.objects.create(name="Python")
tag2 = Tag.objects.create(name="Django")
# 태그 추가 (ManyToMany 관계)
post.tags.add(tag1, tag2)
# 특정 게시글의 태그 조회
tags = post.tags.all()
for tag in tags:
print(tag.name)
# 특정 태그가 달린 모든 게시글 조회 (역참조)
django_posts = tag2.posts.all()
쿼리 최적화
posts_with_tags = Post.objects.prefetch_related('tags').all()
- prefetch_related는 다대다 관계에서 쿼리를 미리 로드하여 최적화를 수행합니다.
4. 언제 ForeignKey와 ManyToManyField를 사용할까?
ForeignKey를 사용해야 할 때
- 관계가 일대다(1:N) 형태일 때.
- 예: 블로그 게시글과 댓글, 유저와 주문 내역, 카테고리와 제품 등.
- 성능이 중요한 경우 (SQL JOIN을 사용하여 쿼리 효율적).
ManyToManyField를 사용해야 할 때
- 관계가 다대다(N:N) 형태일 때.
- 예: 블로그 게시글과 태그, 학생과 수업, 영화와 장르 등.
- 관계의 양방향 검색이 필요한 경우.
5. 중간 테이블(through 모델) 활용
기본적으로 ManyToManyField는 자동으로 중간 테이블을 생성하지만, 관계에 추가적인 정보를 저장하려면 직접 중개 모델을 정의해야 합니다.
class Enrollment(models.Model):
student = models.ForeignKey('Student', on_delete=models.CASCADE)
course = models.ForeignKey('Course', on_delete=models.CASCADE)
enrolled_on = models.DateField(auto_now_add=True)
grade = models.CharField(max_length=10)
class Student(models.Model):
name = models.CharField(max_length=100)
courses = models.ManyToManyField('Course', through='Enrollment')
class Course(models.Model):
title = models.CharField(max_length=200)
데이터 조작 예시
student = Student.objects.create(name="Alice")
course = Course.objects.create(title="Python Basics")
# 중개 테이블을 통해 수강 정보 추가
Enrollment.objects.create(student=student, course=course, grade='A')
# 수강한 모든 코스 조회
student_courses = student.courses.all()
6. 주요 차이점 요약
특징 ForeignKey (일대다) ManyToManyField (다대다)
관계 유형 | 1:N | N:N |
예제 관계 | 게시글 - 댓글 | 게시글 - 태그 |
성능 | JOIN 최적화 가능 | 추가 쿼리 필요 |
관계 탐색 | 단순 | 복잡 |
커스텀 중간 테이블 | 필요 없음 | 가능 |
결론 및 선택 가이드
- ForeignKey는 관계가 **"일대다"**일 때 사용하며, 성능이 중요할 때 적합합니다.
- ManyToManyField는 두 모델 간 "다대다" 관계가 필요할 때 사용하며, 태그나 관심 목록 같은 경우 적합합니다.
- 관계에 추가 필드가 필요하다면 중간 모델(through)을 적극 활용하세요.