

개념
특징
키를 사용하여 부모 테이블의 유일한 값을 참조 (참조 무결성)
참조 무결성
데이터 무결성 원칙 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.contentForeignKey의 필수 인자이며, 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.articlecomment.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.pypath('<int:article_pk>/comments/<int:comment_pk>/delete/', views.comments_delete, name='comments_delete'),articles/views.pycomment.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.Userdjango.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_MODELget_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.contentarticle/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>