개념
특징
키를 사용하여 부모 테이블의 유일한 값을 참조 (참조 무결성)
참조 무결성
데이터 무결성 원칙 3가지 중 한가지 - 데이터의 정확성과 일관성을 유지하는 것
개체 무결성 - 모든 테이블이 기본 키(primary key)를 가져야 하며 기본 키로 선택된 열은 고유하여야 하며 빈 값은 허용치 않음을 규정
참조 무결성 - 기본 테이블에서 일치하는 필드가 '기본 키'이거나, 고유 인덱스를 갖고 있거나, 관련 필드의 데이터 형식이 같아야 합니다.
참조되는 테이블의 행을 이를 참조하는 참조키가 존재하는 한 삭제될 수 없고, 기본키도 변경될 수 없습니다.
개체 무결성 - 테이블에 있는 모든 행들이 유일한 식별자를 가질 것을 요구
외래 키의 값이 부모 테이블의 기본 키 일 필요는 없지만 유일해야 함
ForeignKey()
Django 에서 A many-to-one relationship(1:N)을 표현하기 위한 model field
2개의 필수 위치 인자가 필요
작성 예시
class Comment(models.Model) :
article = models.ForeignKey(Article, on_delete=models.CASCADE)
# articles/models.py
class Comment(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE)
content = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.content
ForeignKey의 필수 인자이며, ForeignKey가 참조하고 있는 부모(Article) 객체가 사라졌을 때 달려 있는 댓글들을 어떻게 처리할 지 정의
Database Integrity(데이터 무결성)을 위해서 매우 중요한 설정이다.
on_delete에 쓸 수 있는 매개변수들(https://docs.djangoproject.com/en/3.1/ref/models/fields/#arguments)
CASCADE
: 부모 객체(참조 된 객체)가 삭제 됐을 때 이를 참조하는 객체도 삭제PROTECT
: 참조가 되어 있는 경우 오류 발생SET_NULL
: 부모 객체가 삭제 됐을 때 모든 값을 NULL로 치환(NOT NULL 조건 시 불가능)SET_DEFAULT
: 모든 값이 DEFAULT 값으로 치환SET()
: 특정 함수 호출DO_NOTHING
: 아무것도 하지 않음RESTRICT
: 3.1 에 새로나온 기능 article.comment_set.all()
<QuerySet [<comment: 댓글1>, <comment:댓글2>]>
모델이름_set
형식의 manager를 생성article.comment
형태로는 가져올 수 없다. comment.article
comment.article
와 같이 접근할 수 있음https://docs.djangoproject.com/en/3.1/ref/models/fields/#django.db.models.ForeignKey.related_name
위에서 확인한 것처럼 부모 테이블에서 역으로 참조할 때(the relation from the related object back to this one.) 모델이름_set
이라는 형식으로 참조한다. (역참조)
related_name
값은 django 가 기본적으로 만들어 주는 _set
manager를 임의로 변경할 수 있다.
# articles/models.py
class Comment(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')
...
위와 같이 변경하면 article.comment_set
은 더이상 사용할 수 없고 article.comments
로 대체된다.
article.comment_set.all()
>> article.comments.all()
로 변화comment_set
은 사용할 수 없게 됨1:N 관계에서는 거의 사용하지 않지만 M:N 관계에서는 반드시 사용해야 할 경우가 발생한다.
articles/models.py
에서 댓글 model 만들기
article = models.ForeignKey(Article, on_delete=models.CASCADE)
abcd = models.ForeignKey(..)
형태로 생성 했다면 abcd_id
로 만들어진다. 내용 - 200 자 제한
생성일
수정일
class Comment(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE)
content = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self): # 확인 더 편하게 보기 위해서 출력 형태 설정
return self.content
데이터 베이스에 적용하기
articles/admin.py
에 Comment 등록하기
from .models import Comment
admin.site.register(Comment)
articles/forms.py
- comment form 만들기
exclude = ('article', )
from .models import Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
exclude = ('article', )
articles/views.py > detail
에서 댓글도 출력할 수 있게 수정comment_form = CommentForm()
comments = article.comment_set.all()
from .forms import CommentForm
def detail(request, pk):
article = get_object_or_404(Article, pk=pk)
comment_form = CommentForm()
comments = article.comment_set.all()
context = {
'article': article,
'comment_form': comment_form,
'comments' : comments,
}
return render(request, 'articles/detail.html', context)
articles/urls.py
에 댓글 작성 주소 만들기path('<int:pk>/comments/', views.comments_create, name='comments_create'),
articles/templates/articles/detail.html
에 댓글 작성 form 추가<form action="{% url 'articles:comments_create' article.pk %}" method="POST">
{{ comments|length }}
{{ article.comment_set.all|length }}
{{ comments.count }}
{% extends 'base.html' %}
{% block content %}
...
<a href="{% url 'articles:index' %}">back</a>
<hr>
<h4>댓글 목록 ({{ comments|length }} 개)</h4>
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>
<form action="{% url 'articles:comments_create' article.pk %}" method="POST">
{% csrf_token %}
{{ comment_form }}
<input type="submit">
</form>
{% endblock %}
articles/views.py
에 comments_create
만들기comment_form.save()
하면 에러가 남 comment = comment_form.save(commit=False)
comment.article = article
에 값을 넣어주고, comment.save()
하면 됨HttpResponse(status=401)
: 로그인 안한 상태에서의 접근을 처리하기from django.http import HttpResponse
@require_POST
def comments_create(request, pk):
if request.user.is_authenticated : # @require_POST 랑 @login_required랑 같이 있으면 로그인 된 순간 GET방식으로 여기에 돌아와서 논리적 모순이 일어나니 이렇게
article = get_object_or_404(Article, pk=pk)
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 = {
'comment_form': comment_form,
'article': article,
}
return render(request, 'articles/detail.html', context)
#return redirect('accounts:login') # 로그인 하지 않은 상황에서 쓰려하면 로그인페이지로 이동
return HttpResponse(status=401) # 401 에러 : 잘못된 로그인 권한 에러페이지로 보내주기
articles/urls.py
path('<int:article_pk>/comments/<int:comment_pk>/delete/', views.comments_delete, name='comments_delete'),
articles/views.py
comment.article.pk
로 접근해서 받아도 되지만, RestAPI 설계 구조에 맞게 짜기 위해서는 url에 article_pk 를 명시하는게 훨씬 좋은 짜임이 된다.@require_POST
def comments_delete(request, article_pk, comment_pk):
if request.user.is_authenticated : # 로그인 확인
comment = get_object_or_404(Comment , pk=comment_pk)
comment.delete()
return redirect('articles:detail', article_pk)
return redirect('accounts:login') # 로그인 하지 않은 상황에서 지우려하면 로그인페이지로 이동
articles/detail.html
{% extends 'base.html' %}
{% block content %}
...
<a href="{% url 'articles:index' %}">back</a>
<hr>
<h4>댓글 목록</h4>
<ul>
{% for comment in comments %}
<li>`
{{ comment }}
<form action="{% url 'articles:comments_delete' article.pk comment.pk %}" method="POST">
{% csrf_token %}
<input type="submit" value="DELETE">
</form>
</li>
{% endfor %}
{% empty %} <!-- 댓글 없는 경우의 처리 -->
<p>아직 댓글이 없습니다.</p>
{% endfor %}
</ul>
<form action="{% url 'articles:comments_create' article.pk %}" method="POST">
{% csrf_token %}
{{ comment_form }}
<input type="submit">
</form>
{% endblock %}
AUTH_USER_MODEL
설정을 제공하여 기본 user model을 재정의(override) 할 수 있도록 함auth.User
django.contrib.auth
내부에 들어있음 - settings.py를 보면 INSTALLED_APPS에 있는걸 볼 수 있음https://github.com/django/django/blob/main/django/contrib/auth/base_user.py#L47
https://github.com/django/django/blob/main/django/contrib/auth/models.py#L321
https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#referencing-the-user-model
settings.AUTH_USER_MODEL
get_user_model()
account/models.py
에 커스텀 계정 모델 만들기AbstractUser
를 상속 받아 만듬from django.contrib.auth.models import AbstractUser
class User(AbstractUser) :
pass # 아직은 커스텀하지는 않았지만 대체만
# 지금까지와 builtin과의 차이는 나중에 커스텀을 할 수 있냐 없냐의 차이임.
프로젝트.settings.py
AUTH_USER_MODEL='accounts.User'
추가accounts/forms.py
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.contrib.auth import get_user_model
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = get_user_model()
fields = ('email', 'first_name', 'last_name',) #너무 많은 작성form이 생겨서 조금 선택해서 나오게 하기
class CustomUserCreationForm(UserCreationForm) :
class Meta(UserCreationForm.Meta) : # 사실 UserCreationForm.Meta를 상속 받지 않아도 작동에는 지장이 없으나, 이렇게 하는 것이 명시적으로 더 좋음
model = get_user_model() # user를 직접참조하기보다 이렇게 간접적으로 참조하는 것이 바람직하다.
fields = UserCreationForm.Meta.fields + ('추가필드들', )
# UserCreationForm의 Meta정보에서 field만 가져와서 모든 field들을 쓰지 않고 편하게 만들기
accounts.view.py
에서 UserCreationForm과 UserChangeForm들을 우리가 재정의한 Form으로 바꿔주기
articles/models.py
가서 User와 article의 1:N 관계를 세팅해주기
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
# from accounts.models import User # 직접참조는 바람직한 방법이 아님
from django.conf import setttings
class Article(models.Model) :
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
데이터베이스에 적용하기
articles/forms.py
에서 글작성할 때 작성자도 선택하게 현재는 되기 때문에 Meta 수정해주기
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
# fields = '__all__'
exclude = ('article',)
혹시 User 커스텀을 까먹고 작업을 하다가 커스텀User를 적용해야하는 경우
articles/models.py
수정 - User와 연결
class Article(models.Model) :
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete = models.CASCADE)
...
article/forms.py
- form에서 아이디 고를 수 없도록 -> title, content만 쓸 수 있도록 설정
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ('title', 'content',)
# exclude = ('title',)
article/views.py > create
수정
@login_required
@require_http_methods(['GET', 'POST'])
def create(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False)
article.user = request.user
article = form.save()
return redirect('articles:detail', article.pk)
else:
form = ArticleForm()
context = {
'form': form,
}
return render(request, 'articles/create.html', context)
article/views.py > update
수정
if request.user == article.user :
추가로 글쓴이만 수정 할 수 있도록 로직 수정from django.http import HttpResponseForbidden
@login_required
@require_http_methods(['GET', 'POST'])
def update(request, pk):
article = get_object_or_404(Article, pk=pk)
if request.user == article.user :
if request.method == 'POST':
form = ArticleForm(request.POST, instance=article)
if form.is_valid():
form.save()
return redirect('articles:detail', article.pk)
else:
form = ArticleForm(instance=article)
else :
return HttpResponseForbidden() # 글 쓴 유저가 아닐 경우 권한 없음 에러코드 반환
context = {
'form': form,
'article': article,
}
return render(request, 'articles/update.html', context)
article/views.py > delete
수정
@require_POST
def delete(request, pk):
article = get_object_or_404(Article, pk=pk)
if request.user.is_authenticated:
if request.user == article.user :
article.delete()
return redirect('articles:index')
return redirect('articles:detail', article.pk)
articles/templates/articles/detail.html
{% if article.user == request.user %}
<a href="{% url 'articles:update' article.pk %}" class="btn btn-primary">[UPDATE]</a>
<form action="{% url 'articles:delete' article.pk %}" method="POST">
{% csrf_token %}
<button class="btn btn-danger">DELETE</button>
</form>
{% endif %}
articles/models.py
수정 - Comment를 user, article과 연결
class Comment(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE)
content = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.content
article/forms.py
- form에서 아이디 고를 수 없도록 -> content만 쓸 수 있도록 설정
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
exclude = ('article', 'user')
articles/views.py > comment_create
수정
@require_POST
def comments_create(request, pk):
if request.user.is_authenticated:
article = get_object_or_404(Article, pk=pk)
comment_form = CommentForm(request.POST)
if comment_form.is_valid():
comment = comment_form.save(commit=False)
comment.article = article
comment.user = request.user
comment.save()
return redirect('articles:detail', article.pk)
context = {
'comment_form': comment_form,
'article': article,
}
return render(request, 'articles/detail.html', context)
return redirect('accounts:login')
articles/templates/articles/detail.html
{% if request.user.is_authenticated %}
<form action="{% url 'articles:comments_create' article.pk %}" method="POST">
{% csrf_token %}
{{ comment_form }}
<input type="submit">
</form>
{% else %}
<a href="{% url 'accounts:login' %}"> 로그인 하세요 </a>
{% endif %}
articles/views.py > comment_delete
수정
@require_POST
def comments_delete(request, article_pk, comment_pk):
if request.user.is_authenticated:
comment = get_object_or_404(Comment, pk=comment_pk)
if request.user == comment.user :
comment.delete()
else :
return HttpResponseForbidden()
return redirect('articles:detail', article_pk)
articles/templates/articles/detail.html
{% if request.user == comment.user %}
<form action="{% url 'articles:comments_delete' article.pk comment.pk %}" method="POST" class="d-inline">
{% csrf_token %}
<input type="submit" value="DELETE">
</form>
{% endif %}
,
를 넣어줌 1000000
becomes 1.0 million
처럼 바꿔주기settings.py > INSTALLED_APPS
에 추가
django.contrib.humanize
사용하고자 하는 template에서
{% load humanize %}
articles/detail.html
{% load humanize %}
<p>작성시각 : {{ article.created_at|naturalday }}</p>
<p>수정시각 : {{ article.updated_at|naturaltime }}</p>