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가 붙어져서 컬럼으로 저장되있음을 확인 가능
⇒ 참조하는 클래스 이름의 소문자(단수형)로 작성하는것이 권장 되었던 이유
댓글 생성
- shell_plus 실행 및 게시글 작성
python manage.py shell_plus
# 게시글 생성
Article.objects.create(title='title', content='content')
- 댓글 생성
# Comment 클래스의 인스턴스 comment 생성
comment = Comment()
# 인스턴스 변수 저장
comment.content = 'first comment'
# DB에 댓글 저장
comment.save()
# 에러발생
NOT NULL constraint failed: articles_comment.article_id
# articles_comment 테이블의 ForeignKeyField, article_id 값이 저장 시 누락되었기 때문
- 댓글을 게시글과 연결
# 게시글 조회
article = Article.objects.get(pk=1)
# 외래 키 데이터 입력
comment.article = article
# 또는 comment.article_id = article.pk 처럼 pk 값을 직접 외래 키 컬럼에
넣어 줄 수도 있지만 권장 X
# 댓글 저장 및 확인
comment.save()
- 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)
- 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
- 사용자로 부터 댓글 데이터를 입력 받기 위한 CommentForm 정의
# articles/forms.py
from .models import Article, Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
# fields = '__all__'
- 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>
- Comment 클래스의 외래 키 필드 article 또한 데이터 입력이 필요한 필드이기 때문에 출력 되고 있는 것
But. 외래 키 필드 데이터는 사용자부터 입력받는 값이 아닌 view 함수 내에서 다른 방법으로 전달 받아 저장되어야 함
- CommentForm의 출력 필드 조정하여 외래 키 필드가 출력되지 않도록 함
# articles/forms.py
from .models import Article, Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ('content',)
- 출력에서 제외된 외래 키 데이터는 어디서 받아와야 할까?
- detail 페이지의 URL을 살펴보면 path('<int:pk>/',views.detail, name='detail')에서 해당 게시글의 pk 값이 사용 되고 있음
- 댓글의 외래 키 데이터에 필요한 정보가 바로 게시글의 pk 값
- 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">
- 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)
- 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.)
- 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)
- 댓글 작성 후 테이블 확인
READ
- 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)
- 전체 댓글 출력 및 확인
<!-- articles/detail.html -->
<h3>댓글 목록</h3>
<ul>
{% for comment in comments %}
<li>{{comment.content}}</li>
{% endfor %}
</ul>
DELETE
- 댓글 삭제url 작성
# articles/urls.py
path('<int:article_pk>/comments/<int:comment_pk>/delete/',
views.comments_delete,
name='comments_delete')
- 댓글 삭제 view 함수 정의
def comments_delete(request, article_pk, comment_pk):
# 어떤 댓글을 삭제하는 지 조회
comment = Comment.objects.get(pk=comment_pk)
comment.delete()
return redirect('articles:detail', article_pk)
- 댓글 삭제 버튼 작성
<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>
- 댓글 삭제 버튼 출력 확인 및 삭제 테스트
+) 추가적인 방법
# 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}}