TIl

N:1 1

아크몽 2024. 4. 22. 01:35

Many to one relationships

한 테이블의 0개 이상의 레코드가 다른 테이블의 레코드 한 개와 관련된 관계

N:1 OR 1:N

Comment(N) - Article(1) 0개 이상의 댓글은 1개의 게시글에 작성될 수 있다.

N대 1관계에서는 N에 외래키가 들어가게 된다.

댓글 모델

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

ForeignKey(to, on_delete)

article = models.ForeignKey(Article,on_delete=models.CASCADE)

to : 참조하는 모델 class이름

on_delete : 외래 키가 참조하는 객체(1)가 사라졌을 때, 외래 키를 가진 객체(N)를 어떻게 처리할 지를 정의하는 설정 (데이터 무결성)

on_delete의 ‘CASCADE’

  • 부모 객체(참조 된 객체)가 삭제 됐을 때 이를 참조하는 객체도 삭제

migration 이후 Comment 테이블 확인 하면 _id가 붙어져서 컬럼으로 저장되있음을 확인 가능

⇒ 참조하는 클래스 이름의 소문자(단수형)로 작성하는것이 권장 되었던 이유

댓글 생성

  1. shell_plus 실행 및 게시글 작성
python manage.py shell_plus

# 게시글 생성
Article.objects.create(title='title', content='content')
  1. 댓글 생성
# Comment 클래스의 인스턴스 comment 생성
comment = Comment()

# 인스턴스 변수 저장
comment.content = 'first comment'

# DB에 댓글 저장
comment.save()

# 에러발생
NOT NULL constraint failed: articles_comment.article_id
# articles_comment 테이블의 ForeignKeyField, article_id 값이 저장 시 누락되었기 때문
  1. 댓글을 게시글과 연결
# 게시글 조회
article = Article.objects.get(pk=1)
# 외래 키 데이터 입력
comment.article = article
# 또는 comment.article_id = article.pk 처럼 pk 값을 직접 외래 키 컬럼에 
넣어 줄 수도 있지만 권장 X

# 댓글 저장 및 확인
comment.save()
  1. comment 인스턴스를 통한 article 값 참조하기
comment.pk => 1

# 클래스 변수 명인 article로 조회 시 해당 참조하는 게시물 객체를 조회할 수 있음
comment.article => <Article: Article object (1)>

# article_pk는 존재하지 않는 필드이기 때문에 사용 불가
comment.article_id => 1

# 한번에 만들기
Comment.objects.create(content='second comment',article=articles)
  1. comment 인스턴스를 통한 aritcle 값 참조하기
# 1번 댓글이 작성된 게시글의 pk 조회
comment.article.pk => 1

# 1번 댓글이 작성된 게시물의 content 조회
comment.article.content => 'content'

역참조 1→ N

N:1 관계에서 1에서 N을 참조하거나 조회하는 것

⇒ N은 외래 키를 가지고 있어서 물리적으로 참조가 가능하지만

1은 N에 대한 참조 방법이 존재하지 않아 별도의 역참조 기능이 필요

comment_Set : related manager (역참조 이름)

   article   .     comment_set            .    all()
모델 인스턴스  related manager(역참조 이름)   QuerySET API

⇒ 특정 게시글에 작성된 댓글 전체를 조회하는 명령

# 응용
comments = articles.comment_set.all()

for comment in comments:
    print(comment.content)

related manager

N:1 혹은 M:N 관계에서 역참조 시에 사용하는 매니저

⇒ ‘objects’ 매니저를 통해 QuerySet API를 사용했던 것처럼 related manager를 통해 QuerySet API를 사용할 수 있게 됨

related manager 이름 규칙

  • N:1관계에서 생성되는 Related manager의 이름은 참조하는 모델명_set 이름 규칙으로 만들어짐
  • 특정 댓글의 게시글 참조 ( Comment → Article)
    • comment.article
  • 특정 게시글의 댓글 목록 참조(Article → Comment)
    • article.comment_set.all()

댓글 구현

Create

  1. 사용자로 부터 댓글 데이터를 입력 받기 위한 CommentForm 정의
# articles/forms.py
from .models import Article, Comment

class CommentForm(forms.ModelForm):
	class Meta:
			model = Comment
			# fields = '__all__' 
  1. detail view 함수에서 CommentForm을 사용하여 detail 페이지에 렌더링
# articles/views.py

from .forms import ArticleForm, CommentForm

def detail(request, pk):
	article = Article.objects.get(pk=pk)
	
	# 사용자로부터 댓글 데이터 입력을 받기 위한 form
	comment_form = CommentForm()
	context = {
		'article':article,
		'comment_form':comment_form,
	}
	return render(request, 'articles/detail,html', context)
<!-- articles/detail.html -->
<form action='#" method="POST">
	{% csrf_token %}
	{{comment_form.as_p }}
	<input type="submit">
</form>
  1. Comment 클래스의 외래 키 필드 article 또한 데이터 입력이 필요한 필드이기 때문에 출력 되고 있는 것

But. 외래 키 필드 데이터는 사용자부터 입력받는 값이 아닌 view 함수 내에서 다른 방법으로 전달 받아 저장되어야 함

  1. CommentForm의 출력 필드 조정하여 외래 키 필드가 출력되지 않도록 함
# articles/forms.py
from .models import Article, Comment

class CommentForm(forms.ModelForm):
	class Meta:
			model = Comment
			fields = ('content',)
  1. 출력에서 제외된 외래 키 데이터는 어디서 받아와야 할까?
  • detail 페이지의 URL을 살펴보면 path('<int:pk>/',views.detail, name='detail')에서 해당 게시글의 pk 값이 사용 되고 있음
  • 댓글의 외래 키 데이터에 필요한 정보가 바로 게시글의 pk 값
  1. url 작성 및 action 작성
# articles/url.py

urlpatterns = [
    path('<int:pk>/comments/', views.comments_create, name='comments_create'),
]
<!-- articles/detail.html -->

<form action="{% url "articles:comment_create" article.pk%} " method ="POST">
  1. comments_create view함수 정의

⇒ url에서 넘겨받은 pk 인자를 게시글에 조회하는 데 사용

# articles/views.py

def comments_create(request, pk):
    # 게시글 조회
    article = Article.objects.get(pk=pk)
    # 사용자 입력 데이터를 받아서 Comment 저장 (+유효성 검사)
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid()
        comment_form.save()
		return redirect('articles:detail', article.pk)
		
    
  1. article 객체는 어떻게/언제 저장할 수 있을까?
# articles/views.py
def comments_create(request, pk):
    # 게시글 조회
    article = Article.objects.get(pk=pk)
    # 사용자 입력 데이터를 받아서 Comment 저장 (+유효성 검사)
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid():
        comment = comment_form.save(commit=False)
        comment.article = article
        comment.save()
    return redirect('articles:detail', article.pk)
    

save(commit=False)

DB에 저장하지 않고 인스턴스만 반환

(Create, but don’t save the new instance.)

  1. save의 commit 인자를 활용해 외래 키 데이터 추가 입력
# articles/views.py
def comments_create(request, pk):
    # 게시글 조회
    article = Article.objects.get(pk=pk)
    comments = article.comment_set.all()
    
    # 사용자 입력 데이터를 받아서 Comment 저장 (+유효성 검사)
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid():
        comment = comment_form.save(commit=False)
        comment.article = article
        comment.save()
        return redirect('articles:detail', article.pk)
    context = {
        'article':article,
        'comment_form':comment_form,
        'comments': comments,
    }
    return render(request, 'articles/detail.html', context)
  1. 댓글 작성 후 테이블 확인

READ

  1. detail view 함수에서 전체 댓글 데이터를 조회
# articles/views.py
def detail(request, pk):
    article = Article.objects.get(pk=pk)
    
    # 특정 게시글에 작성된 모든 댓글 조회 (Article -> Comment, 역참조)
    comments = article.comment_set.all()
    
    # DB에 모든 댓글을 조회(특정 게시글에 작성된 모든 댓글 조회)
    # Comment.objects.all()
    
    context = {
        'article': article,
        'comments': comments,
    }
    return render(request, 'articles/detail.html', context)
  1. 전체 댓글 출력 및 확인
<!-- articles/detail.html -->
  <h3>댓글 목록</h3>
  <ul>
    {% for comment in comments %}
      <li>{{comment.content}}</li>
    {% endfor %}
  </ul>

DELETE

  1. 댓글 삭제url 작성
# articles/urls.py
path('<int:article_pk>/comments/<int:comment_pk>/delete/',
			 views.comments_delete,
			 name='comments_delete')
  1. 댓글 삭제 view 함수 정의
def comments_delete(request, article_pk, comment_pk):
    # 어떤 댓글을 삭제하는 지 조회
    comment = Comment.objects.get(pk=comment_pk)
    comment.delete()
    return redirect('articles:detail', article_pk)
  1. 댓글 삭제 버튼 작성
  <h3>댓글 목록</h3>
  <ul>
    {% for comment in comments %}
      <li>{{comment.content}}</li>
      <form action="{% url "articles:comments_delete" article.pk comment.pk %}" method="POST">
        {% csrf_token %}
        <input type="submit" value="댓글 삭제">
      </form>
    {% endfor %}
  </ul>
  1. 댓글 삭제 버튼 출력 확인 및 삭제 테스트

+) 추가적인 방법

# articles/url.py
path('<int:pk>/update/', ...),
path('<int:pk>/comments/', ...),
path('<int:comment_pk>/comments/delete/', ...),
def comments_delete(request, comment_pk):
    # 어떤 댓글을 삭제하는 지 조회
    comment = Comment.objects.get(pk=comment_pk)
    # 아래 코드처럼 작성 가능
    # 단, 이렇게 작성할 경우 url에서 article_pk가 제거되고 url 구성을 변경해야 함
    # 지금까지의 url 전체 구성 및 통일성을 유지하기 위해 아래코드 방식을 지양함
	  article_pk = comment.article.pk
    comment.delete()
    return redirect('articles:detail', article_pk)

참고

admin site 등록

Comment 모델을 admin site에 등록해 CRUD 동작 확인하기

# articles/admin.py

from .models import Article, Comment

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

댓글이 없는 경우 대체 콘텐츠 출력

DTL 의 for empty 태그 활용

{% for comment in comments %}
  <li>{{comment.content}}</li>
  
  {%empty%}
  <p> 댓글이 없어요..</p>

{% endfor %}

댓글 개수 출력하기

  • DTL filter - length 사용
{{ comments|length}}
{{article.comment_set.all|length}}
  • QuerySet API - count() 사용
{{article.comment_set.count}}

디버깅 TIP

NoReverseMatch 에러 : 현재 페이지의 url 태그만 보면 된다.