Django(12) - 댓글 작성, 삭제

A one-to-many relationship

RDB(관계형 데이터베이스)

  • 데이터를 테이블, 행, 열 등으로 나누어 구조화 하는 방식
  • RDB의 모든 테이블에는 행에서 고유하게 식별 가능한 기본 키라는 속성이 있으며, 외래 키를 사용하여 각 행에서 서로 다른 테이블 간의 관계를 만드는 데 사용할 수 있음


RDB에서의 관계

  • 1:1 : 한 테이블의 레코드 하나가 다른 테이블의 레코드 단 한 개와 관련된 경우
    • 사용자의 프로필 페이지등의 관계
  • 1:N : 한 테이블의 0개 이상의 레코드가 다른 테이블의 레코드 한 개와 관련된 경우
    • 기준 테이블에 따라(1:N, One-to-many relationships)이라고도 한다.
    • 사용자의 글 / 댓글등의 관계
  • M:N : 한 테이블의 0개 이상의 레코드가 다른 테이블의 0개 이상의 레코드와 관련된 경우
    • 양쪽 모두에게서 1:N 관계를 가진다


Foreign Key

특징

  • 키를 사용하여 부모 테이블의 유일한 값을 참조(참조 무결성)
    • 참조 무결성 : 데이터베이스 관계 모델에서 관련된 2개의 테이블 간의 일관성
      • 외래 키가 선언된 테이블의 외래 키 속성(열)의 값은 해당 테이블의 기본 키 값으로 존재
  • 외래 키의 값이 반드시 부모 테이블의 기본 키 일 필요는 없지만 유일한 값이어야 함


1:N 관계로 게시판과 게시글 표현하기

모델 관계 설정

  • 게시판의 게시글과 1:N 관계를 나타낼 수 있는 댓글 구현
  • 1:N 관계에서 댓글을 담당할 Article 모델은 1, Comment 모델은 N이 될 것
    • 게시글은 댓글을 0개 이상 가진다.
      • 게시글(1)은 여러 개의 댓글(N)을 가진다.
      • 게시글(1)은 댓글을 가지지 않을 수도 있다.
    • 댓글은 반드시 하나의 게시글에 속한다.


Django Relationship fields 종류

  • OneToOneField()
  • ForeignKey()
    • one-to-many
  • ManyToManyField()


ForeignKey(to, on_delete, **options)

  • A one-to-many relationship을 담당하는 django의 모델 필드 클래스
  • Django 모델에서 관계형 데이터베이스의 외래 키 속성을 담당
  • 2개의 필수 위치 인자가 필요
    • 참조하는 model class
    • on_delete 옵션


ForeignKey arguments - on_delete

  • 외래 키가 참조하는 객체가 사라졌을 때, 외래 키를 가진 객체를 어떻게 처리할지를 정의
  • 데이터 무결성을 위해서 매우 중요한 설정
  • on_delete 옵션 값
    • CASCADE: 부모 객체(참조 된 객체)가 삭제 됐을 때 이를 참조하는 객체도 삭제
    • PROTECT, SET_NULL, SET_DEFAULT 등 여러 옵션 값들이 존재


Comment 모델 정의

  • 외래 키 필드는 ForeignKey 클래스를 작성하는 위치와 관계 없이 필드의 마지막에 작성됨
  • ForeignKey() 클래스의 인스턴스 이름은 참조하는 모델 클래스 이름의 단수형(소문자)으로 작성하는 것을 권장

models.py

class Comment(models.Model):
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    # integer가 저장되지만 integerField를 안쓰는이유?
    # 참조 대상은(직접 참조) comment.article
    # 역참조는 article.comment_set 하려고
    # A라고 하는 모델에 B모델의 FK를 정의했을때 B모델은 A모델을 어떻게 쓸까? -> b.a_set
    article = models.ForeignKey(Article, on_delete=models.CASCADE)


admin.py

from django.contrib import admin
from .models import Article, Comment

# Register your models here.
# https://docs.djangoproject.com/en/3.2/intro/tutorial07/
class ArticleAdmin(admin.ModelAdmin):
    list_display  = ('title', 'created_at', 'updated_at')
# Comment 추가
class CommentAdmin(admin.Model):
    list_display = ('content', 'created_at', 'article')

admin.site.register(Article, ArticleAdmin)
admin.site.register(Comment)


shell plus

$ python manage.py shell_plus


Article.objects.all()
Article.objects.create(title='제목1', content='내용1')
article = Article.objects.create(title='제목1', content='내용1')
# 게시글 13번에 내용이 111인 댓글을 작성하는 코드?
comment = Comment.objects.create(content='111', article=article)
comment.article # 결과가 뭘까?
# <Article: Article object (13)>
comment.article_id # 결과는?
# 13
comment = Comment.objects.create(contnet='111', article_id=13)
# 13번 게시글의 모든 댓글을 알고자 한다면 어떻게 해야할까?
Comment.objects.filter(article_id=13)
article.comment_set.all()
# 같은 결과가 나옴


게시글 상세페이지에서 댓글들을 출력

views.py

def detail(request, pk):
    # 특정 글을 가져온다.
    article = Article.objects.get(pk=pk)
    # template에 객체 전달
    context = {
        'article': article,
        'comment': article.comment_set.all(),
    }
    return render(request, 'articles/detail.html', context)


detail.html

{% extends 'base.html' %}

{% block body %}
  <h1>{{ article.pk }} 게시글</h1>
  <p>{{ article.created_at|date:"SHORT_DATETIME_FORMAT" }}
    |
    {{ article.updated_at|date:"y-m-d D" }}</p>
  <p>{{ article.content }}
  </p>
  {% if article.image %}
    <img src="{{ article.image.url }}" alt="{{ article.image }}" width="400" height="300">
  {% endif %}
  {% if article.image_thumbnail %}
    <img src="{{ article.image_thumbnail.url }}" alt="{{ article.image_thumbnail }}" width="400" height="300">

  {% endif %}
  <a href="{% url 'articles:update' article.pk %}">수정하기</a>
  <a href="{% url 'articles:delete' article.pk %}">삭제하기</a>
  # 댓글 출력
  <h4 class="my-3">댓글</h4>
  {% for comment in article.comment_set.all %}
    <p>{{ comment.content }}</p>
    <hr>
  {% endfor %}
{% endblock %}


게시글 상세페이지에서 댓글 입력창 만들기

forms.py

from django import forms
from .models import Article, Comment

class ArticleForm(forms.ModelForm):

    class Meta:
        model = Article
        fields = ['title', 'content', 'image', 'image_thumbnail']
# commentForm 
class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['content']

views.py

def detail(request, pk):
    # 특정 글을 가져온다.
    article = Article.objects.get(pk=pk)
    comment_form = CommentForm()
    # template에 객체 전달
    context = {
        'article': article,
        'comments': article.comment_set.all(),
      	# comment_form 추가
        'comment_form': comment_form
    }
    return render(request, 'articles/detail.html', context)

detail.html

{% extends 'base.html' %}

{% block body %}
  <h1>{{ article.pk }}번 게시글</h1>
  <p>{{ article.created_at|date:"SHORT_DATETIME_FORMAT" }}
    |
    {{ article.updated_at|date:"y-m-d D" }}</p>
  <p>{{ article.content }}
  </p>
  {% if article.image %}
    <img src="{{ article.image.url }}" alt="{{ article.image }}" width="400" height="300">
  {% endif %}
  {% if article.image_thumbnail %}
    <img src="{{ article.image_thumbnail.url }}" alt="{{ article.image_thumbnail }}" width="400" height="300">

  {% endif %}
  <a href="{% url 'articles:update' article.pk %}">수정하기</a>
  <a href="{% url 'articles:delete' article.pk %}">삭제하기</a>
  <h4 class="my-3">댓글</h4>
  <form action="">
    {% bootstrap_form comment_form %}
    {% bootstrap_button button_type="submit" content="OK" %}
    {% comment %}  {% endcomment %}
  </form>
  <hr>
  {% for comment in comments %}
    <p>{{ comment.content }}</p>
    <hr>
    {% empty %}
    <p>댓글이 없어요 ㅠ_ㅠ</p>
  {% endfor %}
{% endblock %}


댓글 기능 구현하기

urls.py

from django.urls import path 
from . import views

app_name = 'articles'

urlpatterns = [
  path('', views.index, name='index'),
  path('create/', views.create, name='create'),
  path('<int:pk>/', views.detail, name='detail'),
  path('<int:pk>/update/', views.update, name='update'),
  path('<int:pk>/delete/', views.delete, name='delete'),
  # comment_create 생성
  path('<int:pk>/comments/', views.comment_create, name='comment_create'),
]


views.py

def comment_create(request, pk):
    article = Article.objects.get(pk=pk)
    comment_form = CommentForm(request.POST) # CommentForm의 인스턴스(모델폼)
    if comment_form.is_valid(): # comment_form : 어떤 클래스의 인스턴스
      	# 직접 DB에 save하지 않고 객체를 주면 
        # 내가 필요한 값을 직접 넣고 
        # save함
        comment = comment_form.save(commit=False) # comment : 어떤 클래스의 인스턴스
        comment.article = article # 모델폼의 save메서드는 리턴값이 그 모델의 인스턴스
        comment_form.save()
    return redirect('articles:detail', article.pk)


detail.html

{% extends 'base.html' %}

{% block body %}
  <h1>{{ article.pk }}번 게시글</h1>
  <p>{{ article.created_at|date:"SHORT_DATETIME_FORMAT" }}
    |
    {{ article.updated_at|date:"y-m-d D" }}</p>
  <p>{{ article.content }}
  </p>
  {% if article.image %}
    <img src="{{ article.image.url }}" alt="{{ article.image }}" width="400" height="300">
  {% endif %}
  {% if article.image_thumbnail %}
    <img src="{{ article.image_thumbnail.url }}" alt="{{ article.image_thumbnail }}" width="400" height="300">

  {% endif %}
  <a href="{% url 'articles:update' article.pk %}">수정하기</a>
  <a href="{% url 'articles:delete' article.pk %}">삭제하기</a>
  <h4 class="my-3">댓글</h4>
  <!-- comment_create 수정 -->
  <form action="{% url 'articles:comment_create' %}" method="POST">
    {% csrf_token %}
    {% bootstrap_form comment_form layout='inline' %}
    {% bootstrap_button button_type="submit" content="OK" %}
    {% comment %}  {% endcomment %}
  </form>
  <hr>
  {% for comment in comments %}
    <p>{{ comment.content }}</p>
    <hr>
    {% empty %}
    <p>댓글이 없어요 ㅠ_ㅠ</p>
  {% endfor %}
{% endblock %}


댓글 삭제

urls.py

from django.urls import path 
from . import views

app_name = 'articles'

urlpatterns = [
  path('', views.index, name='index'),
  path('create/', views.create, name='create'),
  path('<int:pk>/', views.detail, name='detail'),
  path('<int:pk>/update/', views.update, name='update'),
  path('<int:pk>/delete/', views.delete, name='delete'),
  path('<int:pk>/comments/', views.comment_create, name='comment_create'),
  # 삭제 url 생성
  path('<int:article_pk>/comments/<int:comment_pk>/delete/', views.comment_delete, name='comment_delete'),
]


views.py

from .models import Article, Comment

def comment_delete(request, article_pk, comment_pk):
    comment = Comment.objects.get(pk=comment_pk)
    comment.delete()
    return redirect('articles:detail', article_pk)


detail.html

{% extends 'base.html' %}
{% load django_bootstrap5 %}


{% block body %}
  <h1>{{ article.pk }}번 게시글</h1>
  <p>{{ article.created_at|date:"SHORT_DATETIME_FORMAT" }}
    |
    {{ article.updated_at|date:"y-m-d D" }}</p>
  <p>{{ article.content }}
  </p>
  {% if article.image %}
    <img src="{{ article.image.url }}" alt="{{ article.image }}" width="400" height="300">
  {% endif %}
  <div>
  <a class="btn btn-primary my-3" href="{% url 'articles:update' article.pk %}">수정하기</a>
  <a class="btn btn-danger my-3" href="{% url 'articles:delete' article.pk %}">삭제하기</a>
  </div>
  <h4 class="my-3">댓글</h4>
  <form action="{% url 'articles:comment_create' article.pk %}" method="POST">
    {% csrf_token %}
    {% bootstrap_form comment_form layout='inline' %}
    {% bootstrap_button button_type="submit" content="OK" %}
    {% comment %}  {% endcomment %}
  </form>
  <hr>
  {% for comment in comments %}
    <p>{{ comment.content }}</p>
		<!-- delete form 추가 -->
    <form action="{% url 'articles:comment_delete' article.pk comment.pk %}" method="POST">
      {% csrf_token %}
      <input type="submit" value="DELETE">
    </form>
    <hr>
    {% empty %}
    <p>댓글이 없어요 ㅠ_ㅠ</p>
  {% endfor %}
{% endblock %}


댓글 개수 카운트

  <p>총 개의 댓글이 있습니다.</p>

관심있을 포스팅