DB 모델 관계2
이전 장에서 우리는 Post 모델을 만들었습니다. 이번 장에서는 태그, 댓글 등의 기능들을 직접 추가해 보면서, 모델 관계를 직접 구현하고, 활용하는 방법을 배워보겠습니다.
1. 글쓴이(Author) 추가하기
게시물의 글쓴이를 나타낼 수 있게 author
필드를 추가하겠습니다. 글쓴이 한명은 여러 글을 쓸 수 있지만 글 하나가 여러 글쓴이를 가질 수는 없습니다. 그러니 1:n
관계에 사용하는 ForeingnKey
를 이용해 models.py
를 수정해봅시다.
#blog > models.py
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
head_image = models.ImageField(upload_to="blog/images/%Y/%m/%d/", blank=True)
file_upload = models.FileField(upload_to="blog/files/%Y/%m/%d/", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.title
#blog > models.py
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
head_image = models.ImageField(upload_to="blog/images/%Y/%m/%d/", blank=True)
file_upload = models.FileField(upload_to="blog/files/%Y/%m/%d/", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.title
이때 주의해야 할 점이 있습니다. 1 : N 관계에서 ForeignKey는 항상 N 쪽에 작성합니다. 예를 들어, 한 사용자가 여러 게시물을 작성할 수 있으므로, ForeignKey는 게시물 모델에 둡니다. 이렇게 하면 각 게시물은 자신의 작성자를 쉽게 참조할 수 있고, 한 사용자의 모든 게시물을 효율적으로 찾을 수 있습니다.
반대로 ForeignKey를 사용자 모델에 두면 여러 문제가 발생합니다. 예를 들어, 한 사용자가 100개의 게시물을 작성했다고 가정해 봅시다. 이 경우 사용자 모델에 100개의 게시물 참조를 저장해야 하며, 새 게시물을 작성할 때마다 사용자 정보를 수정해야 합니다. 또한 사용자가 작성할 수 있는 게시물 수에 제한이 생기고, 특정 게시물의 작성자를 찾으려면 모든 사용자 정보를 검색해야 하는 비효율이 발생합니다. 이런 이유로 ForeignKey는 항상 '다수(N)' 쪽인 게시물 모델에 두는 것이 바람직합니다.
모델을 수정했으니 makemigrations
과 migrate
를 해줍니다.
python manage.py makemigrations
python manage.py migrate
python manage.py makemigrations
python manage.py migrate
이때 makemigrations
을 하면, 아래와 같은 알림 사항이 뜹니다.
post
모델에 author
필드를 새로 넣으려고 하는데, 이 author
필드는 반드시 값이 있어야 한다는 조건이 있습니다. 그런데 이미 있던 게시물들은 author 정보가 없기 때문에 생긴 문제입니다. 이때 Django는 이 문제를 해결할 두 가지 방법을 제안합니다.
- 지금 모든 기존 게시물에 대해 임시로 같은 값을 넣는 방법
- 일단 멈추고 코드에서 기본값을 정해주는 방법 입니다.
1번을 입력하면 새로운 필드에 대한 기본값을 입력하라는 프롬프트가 나타납니다. 이때 >>>
프롬프트에 1
을 입력하면, 데이터베이스의 User 모델에서 ID가 1인 사용자가 모든 기존 게시물의 author로 설정됩니다. 아래 이미지 처럼 Author이 추가된 것을 볼 수 있습니다.
만약 추가해야 하는 값이 User가 아니라 날짜라면 프롬프트에서 import datetime
을 입력하고, datetime.datetime.now()
를 입력하면 됩니다. 이렇게 하면 모든 기존 게시물에 현재 시간이 들어갑니다. 또한 입력해야 하는 값이 숫자라면 숫자를, 텍스트라면 텍스트를 입력하면 됩니다.
해결하는 방법이 이것만 있는 것은 아닙니다. 2번을 선택해 models.py
에서 author
필드에 null=True
를 추가하면 됩니다. 이렇게 하면 author
필드에 값이 없어도 되기 때문에, 기존 게시물에도 author
필드를 추가할 수 있습니다.
1.1 CASCADE 실습
아래는 수정한 models.py 에서 추가된 코드입니다.
author = models.ForeignKey(User, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
이 코드는 유저를 삭제했을 때 이 유저와 연관된 게시물을 함께 삭제하겠다는 의미입니다. 1:N
관계에서 1을 삭제하면 N도 함께 삭제되는 것을 의미합니다. 이것을 CASCADE
라고 합니다. 만약 삭제가 되어도 남게 하고 싶다면 models.SET_NULL
을 넣으면 됩니다. 이제 이 코드를 다른 유저 계정을 생성해서, 실행이 되는지 테스트 해봅시다.
python manage.py runserver
python manage.py runserver
우선 서버를 키고, 관리자페이지 > 사용자 추가를 클릭해서 새로운 유저를 만들어 줍니다. 임의로 leehojun2
라는 유저를 생성했습니다. 사용자(들)
을 클릭해보면, 유저가 한명 더 생성된 것을 확인할 수 있습니다.
게시물을 3개 더 작성해 줍니다. 이때 Author은 이미지와 같이 새로 만든 유저로 설정해 주세요.
게시물을 3개 더 추가했다면, 이제 총 게시물은 6개가 되었습니다. 이제 유저를 삭제해봅시다. 사용자(들)
에서 삭제할 유저를 선택하고, 액션에서 삭제 선택, 실행을 누르면 유저가 삭제됩니다.
Post를 확인해보면, leejojun2
으로 만든 게시물이 삭제된 것을 확인 할 수 있습니다.
blog_list.html
파일을 수정해, blog 페이지에서 게시물이 잘 보이는지 확인해봅시다.
- templates > blog > blog_list.html
<!-- templates > blog > blog_list.html -->
{% for i in posts %}
<h1>{{ i.title }}</h1>
<p>{{ i.content }}</p>
<p>{{ i.author }}</p>
<hr>
{% endfor %}
<!-- templates > blog > blog_list.html -->
{% for i in posts %}
<h1>{{ i.title }}</h1>
<p>{{ i.content }}</p>
<p>{{ i.author }}</p>
<hr>
{% endfor %}
유저가 삭제되었을 때, 게시글을 보존하고 싶다면, 글쓴이를 빈칸으로 만들 수 있습니다. 아래 코드 처럼 on_delete을 models.CASCADE
대신 models.SET_NULL
로 설정하면 author가 빈칸이 됩니다.
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
on_delete=models.SET_NULL
을 사용하고 싶으면, null=True
과 같이 이 필드에 null값이 들어 갈 수 있게 설정해야합니다.
2. 태그, 댓글 기능 추가하기
이번에는 태그와 댓글을 추가해 봅시다. 태그
에 대해서 생각해보면, 한 게시물은 여러개의 태그를 가질 수 있고, 한 태그는 여러 게시물에 속할 수 있습니다. 그러니 게시물과 태그는 다대다(N:M)
관계를 이룹니다. 댓글
은 하나의 게시물에서 여러 댓글을 가질 수 있습니다. 하지만 댓글은 하나의 게시물에만 속합니다. 그러니 게시물과 댓글은 일대다(1:N)
관계를 가집니다. 일대다 관계에는 ForeingnKey
를 다대다 관계에는 ManyToManyField
를 사용합니다. ForeingnKey
는 1:N
에서 N에 정의하고, ManyToManyField
는 양쪽 모델 중 한 곳에 정의합니다.
2.1 models.py 수정하기
우선 models.py를 수정해 태그와 댓글 모델을 추가합니다.
- models.py
#blog > models.py
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
head_image = models.ImageField(
upload_to='blog/images/%Y/%m/%d/', blank=True)
file_upload = models.FileField(
upload_to='blog/files/%Y/%m/%d/', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
author = models.ForeignKey(
User, on_delete=models.CASCADE
)
# tags필드를 추가합니다.
tags = models.ManyToManyField('Tag', blank=True)
def __str__(self):
return self.title
#새로운 Comment모델을 추가합니다.
class Comment(models.Model):
message = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
post = models.ForeignKey(
Post, on_delete=models.CASCADE, related_name='comments'
)
author = models.ForeignKey(
User, on_delete=models.CASCADE
)
def __str__(self):
return self.message
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
#blog > models.py
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
head_image = models.ImageField(
upload_to='blog/images/%Y/%m/%d/', blank=True)
file_upload = models.FileField(
upload_to='blog/files/%Y/%m/%d/', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
author = models.ForeignKey(
User, on_delete=models.CASCADE
)
# tags필드를 추가합니다.
tags = models.ManyToManyField('Tag', blank=True)
def __str__(self):
return self.title
#새로운 Comment모델을 추가합니다.
class Comment(models.Model):
message = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
post = models.ForeignKey(
Post, on_delete=models.CASCADE, related_name='comments'
)
author = models.ForeignKey(
User, on_delete=models.CASCADE
)
def __str__(self):
return self.message
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
Post 모델에 tags 필드를 추가하여 Tag 모델과 다대다 관계를 설정합니다. Comment 모델을 새로 생성하고, post 필드를 통해 Post 모델과 일대다 관계를 설정했습니다.
이때 Comment 모델에서 related_name='comments'
를 사용하여 Post 모델에서 연결된 댓글들을 쉽게 접근할 수 있게 했습니다. 여기서 related_name
은 Post에서 Comment를 부를 때 사용할 이름입니다. 템플릿 문법에서 아래와 같이 호출됩니다.
{% for comment in post.Comment.all %}
{% for comment in post.Comment.all %}
모델을 변경했으므로 이를 데이터베이스에 반영해야 합니다.
python manage.py makemigrations
python manage.py migrate
python manage.py makemigrations
python manage.py migrate
2.2 admin.py 수정하기
새로 만든 모델들을 관리자 페이지에서 관리할 수 있도록 admin.py 파일을 수정합니다.
- admin.py
#blog > admin.py
from django.contrib import admin
from .models import Post, Comment, Tag
admin.site.register(Post)
admin.site.register(Comment)
admin.site.register(Tag)
#blog > admin.py
from django.contrib import admin
from .models import Post, Comment, Tag
admin.site.register(Post)
admin.site.register(Comment)
admin.site.register(Tag)
이제 서버를 키고, 관리자 페이지에서 변경사항을 확인해봅시다.
왼쪽 상단 위에 Comments와 Tags가 추가된 것을 볼 수 있습니다. 또 새 글을 작성할 때 태그를 선택할 수 있도록 추가되었습니다. 댓글과 태그을 달아보세요. 이제 관리자 페이지에서 작성한 댓글과 태그를 blog 사이트에서 볼수 있도록 만들어 봅시다.
2.3 템플릿 파일 수정하기
블로그 목록과 상세 페이지에서 태그와 댓글을 표시하도록 템플릿을 수정합니다.
- blog_list.html
<!-- blog_list.html -->
{% for post in posts %}
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<p>{{ post.author }}</p>
<p>{{ post.comments }}</p>
<p>{{ post.tags }}</p>
{% for comment in post.comments.all %}
<p>{{ comment.message }}</p>
{% endfor %}
{% for tag in post.tags.all %}
<p>{{ tag.name }}</p>
{% endfor %}
<hr>
{% endfor %}
<!-- blog_list.html -->
{% for post in posts %}
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<p>{{ post.author }}</p>
<p>{{ post.comments }}</p>
<p>{{ post.tags }}</p>
{% for comment in post.comments.all %}
<p>{{ comment.message }}</p>
{% endfor %}
{% for tag in post.tags.all %}
<p>{{ tag.name }}</p>
{% endfor %}
<hr>
{% endfor %}
댓글이나 태그를 가져올때 다음과 같은 코드는 사용하지 않습니다.
<p>{{ post.comments }}</p>
<p>{{ post.tags }}</p>
<p>{{ post.comments }}</p>
<p>{{ post.tags }}</p>
대신 아래의 코드를 사용해야 합니다.
{% for comment in post.comments.all %}
<p>{{ comment.message }}</p>
{% endfor %}
{% for tag in post.tags.all %}
<p>{{ tag.name }}</p>
{% endfor %}
{% for comment in post.comments.all %}
<p>{{ comment.message }}</p>
{% endfor %}
{% for tag in post.tags.all %}
<p>{{ tag.name }}</p>
{% endfor %}
Django에서 post.comments
나 post.tags
를 직접 사용하면 실제 데이터를 보여주지 않습니다. {% for comment in post.comments.all %}
와 같이 모든 객체를 불러와야만 읽을 수 있습니다. post.comments
는 해당 객체를 가리키기만 하는 것이며, 실제 데이터를 불러오지 않습니다.
또한 Post 모델에 comments와 tags 필드가 없는데 어떻게 Post에 .
을 찍어 comments와 tags를 사용할 수 있을까요? 이는 Django의 역참조(Reverse relationship) 기능 때문입니다. Post 모델에 직접적으로 comments와 tags 필드를 정의하지 않았지만, Comment 모델과 Tag 모델에서 ForeignKey로 Post를 참조하고 있기 때문에 자동으로 역참조가 가능해집니다. 기본적으로는 N으로 연결된 모델명을 소문자로 바꾸고 복수형으로 만들어 사용하지만 아래와 같이 related_name
을 사용하여 원하는 이름으로 바꿀 수 있습니다. 특별한 경우가 아니면 related_name
을 사용하지 않아도 됩니다.
class Comment(models.Model):
post = models.ForeignKey(
Post, on_delete=models.CASCADE, related_name='comments'
)
class Comment(models.Model):
post = models.ForeignKey(
Post, on_delete=models.CASCADE, related_name='comments'
)
또한 Post 모델에 tags와 comments가 연결은 되어있지만 실제 DB 쿼리를 실행하지 않습니다. 이는 성능 최적화를 위한 방법입니다. 만약 실제 데이터를 불러오고 싶다면 .all()
을 사용해야 합니다. 이 메서드를 사용하는 순간 데이터베이스 쿼리가 실행되어 실제 데이터를 불러옵니다. 다만 이렇게 할 것이라면 아래처럼 미리 로드해두는 것이 좋습니다. 이렇게 하면 템플릿에서 이 정보를 사용할 때 추가 쿼리를 실행하지 않아 성능이 향상됩니다. 이 수업에서는 성능 최적화에 대한 내용은 다루지 않습니다.
from django.shortcuts import render
from .models import Post
def post_list(request):
posts = Post.objects.all().prefetch_related('comments', 'tags')
return render(request, 'blog/post_list.html', {'posts': posts})
from django.shortcuts import render
from .models import Post
def post_list(request):
posts = Post.objects.all().prefetch_related('comments', 'tags')
return render(request, 'blog/post_list.html', {'posts': posts})
comments와 tags는 많은 쪽(N)
의 관계를 나타내기 때문에 이런 방식을 사용합니다. 하나의 게시물에 여러 댓글이나 태그가 연결될 수 있기 때문에, 이런 경우에는 성능 최적화를 위해 Django는 직접 모든 댓글과 태그를 가져오지 않고 "여기 댓글들이 있어요"라고 알려주기만 합니다.
만약 세부 내용이 필요하다면, .all()
을 사용해 실제 데이터를 불러옵니다. 이런 방식은 필요할 때만 데이터베이스 쿼리를 실행하여 효율성을 높힙니다. 반면에 하나
쪽 관계(예: 댓글에서 게시물로의 관계)는 직접 접근이 가능합니다.
추가적으로 {{post.comments}}
, {{post.tags}}
을 사용하면 아래와 같은 문구가 출력됩니다.
blog.Comment.None
blog.Tag.None
blog.Comment.None
blog.Tag.None
- blog_detail.html
<!-- blog_detail.html -->
<h1>{{ post.title }}</h1>
<p>{{ post.author }}</p>
<p>{{ post.content }}</p>
{% for tag in post.tags.all %}
<p>{{ tag.name }}</p>
{% endfor %}
{% for comment in post.comments.all %}
<p>{{ comment.message }}</p>
{% endfor %}
<!-- blog_detail.html -->
<h1>{{ post.title }}</h1>
<p>{{ post.author }}</p>
<p>{{ post.content }}</p>
{% for tag in post.tags.all %}
<p>{{ tag.name }}</p>
{% endfor %}
{% for comment in post.comments.all %}
<p>{{ comment.message }}</p>
{% endfor %}
3. 태그 모아보기 기능 추가하기
태그를 클릭하면, 태그가 달린 게시물을 모아서 볼 수 있도록 만들어봅시다.
3.1 urls.py 수정하기
우선 tag별로 모아서 볼 수 있도록, url패턴을 추가해 줍니다.
from django.urls import path
from . import views
urlpatterns = [
path("", views.blog_list, name="blog_list"),
path("<int:pk>/", views.blog_detail, name="blog_detail"),
path("tag/<str:tag>/", views.blog_tag, name="blog_tag"),
]
from django.urls import path
from . import views
urlpatterns = [
path("", views.blog_list, name="blog_list"),
path("<int:pk>/", views.blog_detail, name="blog_detail"),
path("tag/<str:tag>/", views.blog_tag, name="blog_tag"),
]
3.2 views.py 수정하기
tag url에 들어가면 실행될 view함수를 정의합니다.
# views.py
from django.shortcuts import render
from .models import Post, Comment, Tag
def blog_list(request):
posts = Post.objects.all()
return render(request, "blog/blog_list.html", {"posts": posts})
def blog_detail(request, pk):
post = Post.objects.get(pk=pk)
if request.method == "POST":
author = request.user
message = request.POST["message"]
c = Comment.objects.create(author=author, message=message, post=post)
c.save()
return render(request, "blog/blog_detail.html", {"post": post})
def blog_tag(request, tag):
posts = Post.objects.filter(tags__name__iexact=tag)
return render(request, "blog/blog_list.html", {"posts": posts})
# views.py
from django.shortcuts import render
from .models import Post, Comment, Tag
def blog_list(request):
posts = Post.objects.all()
return render(request, "blog/blog_list.html", {"posts": posts})
def blog_detail(request, pk):
post = Post.objects.get(pk=pk)
if request.method == "POST":
author = request.user
message = request.POST["message"]
c = Comment.objects.create(author=author, message=message, post=post)
c.save()
return render(request, "blog/blog_detail.html", {"post": post})
def blog_tag(request, tag):
posts = Post.objects.filter(tags__name__iexact=tag)
return render(request, "blog/blog_list.html", {"posts": posts})
blog_tag
함수는 ORM 쿼리의 필터 기능을 이용해서, 클릭한 태그와 일치하는 태그를 가진 모든 게시물을 찾아서 보여줍니다.
3.3 템플릿 수정하기
태그를 클릭하면 해당 태그의 게시물 목록으로 이동할 수 있도록 템플릿을 수정합니다.
<!-- blog_detail.html -->
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<p>{{ post.author }}</p>
{% for tag in post.tags.all %}
<a href="/blog/tag/{{ tag.name }}">#{{ tag.name }}</a>
{% endfor %}
{% for comment in post.comments.all %}
<p>{{ comment.message }}</p>
{% endfor %}
<!-- blog_detail.html -->
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<p>{{ post.author }}</p>
{% for tag in post.tags.all %}
<a href="/blog/tag/{{ tag.name }}">#{{ tag.name }}</a>
{% endfor %}
{% for comment in post.comments.all %}
<p>{{ comment.message }}</p>
{% endfor %}
3.4 실행 및 테스트
서버를 열고 블로그 상세페이지에서 태그를 클릭해 잘 작동하는지 테스트해봅시다. 태그를 클릭하면, 그 태그가 포함된 게시물만 보여주는 것을 확인할 수 있습니다. 아래는 실제 화면 입니다.
블로그 상세 페이지 | 태그 페이지 |
---|---|