Model Relationship - 1:N
목차
- Foreign Key
- Comment CREATE
- Comment READ
- Comment DELETE
- Customizing authentication in Django
- Substituting a custom User model
- Custom user & Built-in auth forms
- 1:N relationship
Foreign Key
Forein Key
개념
- 외래 키 (외부 키)
- 관계형 데이터베이스에서 한 테이블의 필드 중 다른 테이블의 행을 식별할 수 있는 키
- 참조하는 테이블에서 1개의 키 (속성 또는 속성의 집합)에 해당하고, 이는 참조되는 측 테이블의 기본 키 (Primary Key)를 가리킴.
- 참조하는 테이블의 행 1개의 값은, 참조되는 측 테이블의 행 값에 대응됨.
- 이 때문에 참조하는 테이블의 행에는, 참조되는 테이블에 나타나지 않는 값을 포함할 수 없음.
- 참조하는 테이블의 행 여러 개가, 참조되는 테이블의 동일한 행을 참조할 수 있음.
예시
게시글(Article)과 댓글(Comment) 간의 모델 관계 설정
Article
id | title | content |
---|---|---|
1 | 제목1 | 내용1 |
2 | 제목2 | 내용2 |
3 | 제목3 | 내용3 |
Comment
참조하는 모델(Comment)에서 외래 키는 참조되는 측 모델(Article)의 기본 키(Primary Key)를 가리킴
id | content | foreign key |
---|---|---|
1 | 댓글1 | 3 |
2 | 댓글2 | 1 |
3 | 댓글3 | 1 |
4 | 댓글4 | 2 |
특징
- 키를 사용하여 부모 테이블의 유일한 값을 참조 (참조 무결성)
- 외래 키의 값이 반드시 부모 테이블의 기본 키일 필요는 없지만 유일한 값이어야 함.
[참고] 참조 무결성
- 데이터베이스 관계 모델에서 관련된 2개의 테이블 간의 일관성을 말함.
- 외래 키가 선언된 테이블의 외래 키 속성(열)의 값은 그 테이블의 부모가 되는 테이블의 기본 키 값으로 존재해야 함.
Foreign Key field
- A many-to-one relationship
- 2개의 위치 인자가 반드시 필요
- 참조하는 model class
on_delete
옵션
- migrate 작업 시 필드 이름에
_id
를 추가하여 데이터베이스 열 이름을 만듦.
[참고] 재귀 관계 (자신과 1:N)
models.ForeignKey('self', on_delete=models.CASCADE)
- comment 모델 정의하기
# 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
on_delete
- 외래 키가 참조하는 객체가 사라졌을 때 외래 키를 가진 객체를 어떻게 처리할지를 정의
- Database Integrity(데이터 무결성)을 위해서 매우 중요한 설정
on_delete
옵션에 사용 가능한 값들- CASCADE : 부모 객체 (참조된 객체)가 삭제 됐을 때 이를 참조하는 객체도 삭제
- PRODUCT
- SET_NULL
- SET_DEFAULT
- SET()
- DO_NOTHING
- RESTRICT
[참고] 데이터 무결성
- 데이터의 정확성과 일관성을 유지하고 보증하는 것을 가리키며, 데이터베이스나 RDBMS 시스템의 중요한 기능임.
- 무결성 제한의 유형
- 개체 무결성 (Entity integrity)
- PK의 개념과 관련
- 모든 테이블의 PK를 가져야 하며 PK로 선택된 열은 고유한 값이어야 하고 빈 값은 허용치 않음을 규정
- 참조 무결성 (Referential integrity)
- FK(외래 키) 개념과 관련
- FK 값이 데이터베이스의 특정 테이블의 PK 값을 참조하는 것
- 범위 (도메인) 무결성 (Domain integrity)
- 정의된 형식(범위)에서 관계형 데이터베이스의 모든 칼럼이 선언되도록 규정
- 개체 무결성 (Entity integrity)
Migration
- migrations
$ python manage.py makemigraions
$ python manage.py migrate
- articles_comment 테이블의 외래 키 칼럼 확인 (필드 이름에
_id
가 추가됨)article_id
데이터베이스의 ForeignKeyField 표현
- 만약 ForeignKey 인스턴스를 abcd로 생성했다면
abcd_id
로 만들어짐. - 하지만 명시적인 모델 관계 파악을 위해 참조하는 클래스 이름의 소문자(단수형)로 작성하는 것이 바람직함 (1:N)
articles_comment
id | content | article_id |
---|---|---|
1 | 댓글1 | 3 |
2 | 댓글2 | 1 |
3 | 댓글3 | 1 |
4 | 댓글4 | 2 |
댓글 생성 연습하기
- 댓글 생성 시도
$ python manage.py shell_plus
In [1]: comment = Comment()
In [2]: comment.content = 'first comment'
In [3]: comment.save()
----------------------------------------------------------------------------------
IntrgrityError: NOT NULL constraint failed: articles_comment.article_id
- 에러 확인
- articles_comment 테이블의 ForeignKeyField, article_id 값이 누락되었기 때문
- 게시글 생성 후 댓글 생성 재시도
In [4]: article = Article.objects.create(title='title', content='content')
In [5]: article = Article.objects.get(pk=1)
In [6]: article
Out[6]: <Article: title>
In [7]: comment.article = article
In [8]: comment.save()
In [9]: comment
Out[9]: <Comment: first comment>
In [10]: comment.pk
Out[10]: 1
- 댓글 속성 값 확인
- 실제로 작성된 외래 키 칼럼명
article_id
이기 때문에article_pk
로는 값에 접근할 수 없음
- 실제로 작성된 외래 키 칼럼명
In [11]: comment.content
Out[11]: 'first comment'
In [12]: comment.article_id
Out[12]: 1
In [13]: comment.article
Out[13]: <Article: title>
- comment 인스턴스를 통한 article 값 접근
In [14]: comment.article.pk
Out[14]: 1
In [15]: comment.article.content
Out[15]: 'content'
- 두번째 댓글 작성 해보기
In [16]: comment = Comment(content='second comment', article=article)
In [17]: comment.save()
In [18]: comment.pk
Out[18]: 2
In [19]: comment.article_id
Out[19]: 1
- admin site 에서 작성된 댓글 확인
# articles/admin.py
from .models import Article, Comment
admin.site.register(Comment)
$ python manage.py createsuperuser
1:N 관계 related manager
- 역참조(
comment_set
)- Article(1) → Comment(N)
article.comment
형태로는 사용할 수 없고,article.comment_set
manager가 생성됨- 게시글에 몇 개의 댓글이 작성되었는지 Django ORM이 보장할 수 없기 때문
- article은 comment가 있을 수도 있고, 없을 수도 있음
- 실제로 Article 클래스에는 Comment와의 어떠한 관계도 작성되어 있지 않음.
- 참조 (
article
)- Comment(N) → Article(1)
- 댓글의 경우 어떠한 댓글이든 반드시 자신이 참조하고 있는 게시글이 있으므로,
comment.article
과 같이 접근할 수 있음. - 실제 ForeignKeyField 또한 Comment 클래스에서 작성됨.
- article의 입장에서 모든 댓글 조회하기 (역참조, 1 → N)
In [20]: article.comment_set.all()
Out[20]: <QuerySet [<Comment: first comment>, <Comment: second comment>]>
- 조회한 모든 댓글 출력하기
In [21]: comments = article.comment_set.all()
In [22]: for comment in comments:
...: print(comment.content)
first comment
second comment
- comment의 입장에서 참조하는 게시글 조회하기 (참조, N → 1)
In [23]: comment = Comment.objects.get(pk=1)
In [24]: comment.article
Out[24]: <Article: title>
In [25]: comment.article.content
Out[25]: 'content'
In [26]: comment.article_id
Out[26]: 1
ForeignKey arguments - related_name
- 역참조 시 사용할 이름(
model_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
로 대체됨
주의
역참조 시 사용할 이름 수정 후, migration 과정 필요
Comment CREATE
CommentForm 작성
# articles/forms.py
from django import forms
from .models import Article, Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = '__all__'
detail 페이지에서 CommentForm 출력
# articles/views.py
from .forms import ArticleForm, CommentForm
def detail(request, pk):
article = get_object_or_404(Article, pk=pk)
comment_form = CommentForm()
context = {
'article': article,
'comment_form': comment_form,
}
return render(request, 'articles/detail.html', context)
<!-- articles/detail.html -->
{% extends 'base.html' %}
{% block content %}
...
<a href="{% url 'articles:index' %}">[back]</a>
<hr>
<form action="" method="POST">
{% csrf_token %}
{{ comment_form }}
<input type="submit">
</form>
{% endblock content %}
- ForeignKeyField를 작성자가 직접 입력하는 상황 발생
- CommentForm에서 외래 키 필드 출력 제외
# articles/forms.py
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
# fields = '__all__'
exclude = ('article',)
댓글 작성 로직
# articles/urls.py
from django.urls import path
from . import views
app_name = 'articles'
urlpatterns = [
...
path('<int:pk>/comments/', views.comments_create, name='comments_create'),
]
# articles/views.py
@require_POST
def comments_create(request, pk):
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)
<!-- articles/detail.html -->
<form action="{% url 'articles:comments_create' article.pk %}" method="POST">
{% csrf_token %}
{{ comment_form }}
<input type="submit">
</form>
The save()
method
save(commit=False)
- Create, but don't save the new instance.
- 아직 데이터베이스에 저장되지 않은 인스턴스를 반환
- 저장하기 전에 객체에 대한 사용자 지정 처리를 수행할 때 유용하게 사용.
Comment READ
댓글 출력
- 특정 article에 있는 모든 댓글을 가져온 후 context에 추가
# articles/views.py
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/detail.html -->
{% extends 'base.html' %}
{% block content %}
...
<a href="{% url 'articles:index' %}">[back]</a>
<hr>
<h4>댓글 목록</h4>
<ul>
{% for comment in comments %}
<li>
{{ comment.user }} - {{ comment.content }}
</li>
{% endfor %}
</ul>
...
{% endblock content %}
Comment DELETE
# articles/urls.py
app_name = 'articles'
urlpatterns = [
...
path('<int:article_pk>/comments/<int:comment_pk>/delete/', views.comments_delete, name='comments_delete'),
]
# articles/views.py
@require_POST
def comments_delete(request, article_pk, comment_pk):
comment = get_object_or_404(Comment, pk=comment_pk)
comment.delete()
return redirect('articles:detail', article_pk)
<!-- articles/detail.html -->
{% extends 'base.html' %}
{% block content %}
...
<h4>댓글 목록</h4>
<ul>
{% for comment in comments %}
<li>
{{ comment.user }} - {{ comment.content }}
{% if 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 %}
</li>
{% endfor %}
</ul>
<hr>
...
{% endblock content %}
인증된 사용자의 경우만 댓글 작성 및 삭제
# articles/views.py
@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.save()
return redirect('articles:detail', article.pk)
return redirect('accounts:login')
@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)
Comment 추가 사항
댓글 개수 출력하기
<!-- articles/detail.html -->
<h4>댓글 목록</h4>
{% if comments %}
<p><b>{{ comments|length }}개의 댓글이 있습니다.</b></p>
{% endif %}
댓글이 없는 경우 대체 컨텐츠 출력
- DTL의 for-emtpy 태그 활용
<!-- articles/detail.html -->
<ul>
{% for comment in comments %}
<li>
{{ comment.user }} - {{ comment.content }}
{% if 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 %}
</li>
{% empty %}
<p>댓글이 없어요..</p>
{% endfor %}
</ul>
Customizing authentication in Django
User 모델 대체하기
- 일부 프로젝트에서는 Django의 내장 User 모델이 제공하는 인증 요구사항이 적절하지 않을 수 있음
- ex. username 대신 email을 식별 토큰으로 사용하는 것이 더 적합한 사이트
- Django는 User를 참조하는데 사용하는
AUTH_USER_MODEL
값을 제공하여, default user model을 재정의(override) 할 수 있도록 함. - Django는 새 프로젝트를 시작하는 경우 기본 사용자 모델이 충분하더라도, 커스텀 유저 모델을 설정하는 것을 강력하게 권장 (highly recommended)
- 단, 프로젝트의 모든 migrations 혹은 첫 migrate를 실행하기 전에 이 작업을 마쳐야 함.
AUTH_USER_MODEL
- User를 나타내는 데 사용하는 모델
- 프로젝트가 진행되는 동안 변경할 수 없음
- 프로젝트 시작 시 설정하기 위한 것이며, 참조하는 모델은 첫번째 마이그레이션에서 사용할 수 있어야 함
- 기본 값 :
auth.User
(auth
앱의User
모델)
[참고] 프로젝트 중간(mid-project)에 AUTH_USER_MODEL
변경하기
- 모델 관계에 영향을 미치기 때문에 훨씬 더 어려운 작업이 필요
- 즉, 중간 변경은 권장하지 않으므로 초기에 설정하는 것을 권장
Custom User 모델 정의하기
- 관리자 권한과 함께 완전한 기능을 갖춘 User 모델을 구현하는 기본 클래스인 AbstractUser를 상속받아 새로운 User 모델 작성
# accounts/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
- 기존에 django가 사용하는 User 모델이었던 auth앱의 User 모델을 accounts 앱의 User 모델을 사용하도록 변경
# settings.py
AUTH_USER_MODEL = 'accounts.User'
- admin site에 Custom User 모델 등록
# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
admin.site.register(User, UserAdmin)
- 프로젝트 중간에 진행했기 때문에 데이터베이스를 초기화 한 후 마이그레이션 진행
- 초기화 방법
- db.sqlite3 삭제
- migrations 파일 모두 삭제 (파일명에 숫자가 붙은 파일만 삭제)
$ python manage.py makemigrations
$ python manage.py migrate
Custom user & Built-in auth forms
- 회원가입 시도 후 ModelForm 관련 에러 발생
UserCreationForm
과UserChangeForm
은 기존 내장 User 모델을 사용한 ModelForm이기 때문에 커스텀 User 모델로 대체해야 함.- 커스텀 User 모델이 AbstractUser의 하위 클래스인 경우 다음과 같은 방식으로 form을 확장
# accounts/forms.py
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.contrib.auth import get_user_model
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = get_user_model()
fields = UserCreationForm.Meta.fields + ('email',)
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = get_user_model()
fields = ('email', 'first_name', 'last_name',)
# accounts/views.py
from .forms import CustomUserChangeForm, CustomUserCreationForm
def signup(request):
if request.user.is_authenticated:
return redirect('articles:index')
if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
auth_login(request, user)
return redirect('articles:index')
else:
form = CustomUserCreationForm()
context = {
'form': form,
}
return render(request, 'accounts/signup.html', context)
get_user_model()
- 현재 프로젝트에서 활성화된 사용자 모델(active user model)을 반환
- User 모델을 커스터마이징한 상황에서는 Custom User 모델을 반환
- 이 때문에 django는 User 클래스를 직접 참조하는 대신
django.contrib.auth.get_user_model()
을 사용하여 참조해야 한다고 강조
1:N Relationship
User - Article (1:N)
User 모델 참조하기
settings.AUTH_USER_MODEL
- User 모델에 대한 외래 키 또는 다대다 관계를 정의할 때 사용해야 함.
- models.py에서 User 모델을 참조할 때 사용
get_user_model()
- 현재 활성화(active)된 User 모델을 반환
- 커스터마이징한 User 모델이 있을 경우는 Custom User 모델, 그렇지 않으면 User를 반환
- User를 직접 참조하지 않는 이유
- models.py가 아닌 다른 모든 곳에서 유저 모델을 참조할 때 사용
- 현재 활성화(active)된 User 모델을 반환
User와 Article 간 모델 관계 정의 후 migration
# articles/models.py
from django.conf import settings
class Article(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
...
$ python manage.py makemigrations
- null 값이 허용되지 않는 user_id 필드가 별도의 값 없이 article에 추가되려 할 때
- 1 입력 후 enter : 현재 화면에서 기본 값을 설정하겠다는 의미
- 1 입력 후 enter : 기존 필드에 추가되는 user_id 필드의 값을 1로 설정하겠다는 의미
$ python manage.py migrate
게시글 출력 필드 수정
# articles/forms.py
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ('title', 'content',)
CREATE : NOT NULL constraint failed ERROR 수정
- 게시글 작성 시 작성자 정보 (article.user) 누락
# articles/views.py
@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.save()
return redirect('articles:detail', article.pk)
else:
form = ArticleForm()
context = {
'form': form,
}
return render(request, 'articles/create.html', context)
DELETE 수정
- 자신이 작성한 게시글만 삭제 가능하도록 설정
# articles/views.py
@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)
UPDATE 수정
- 자신이 작성한 게시글만 수정 가능하도록 설정
# articles/views.py
@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 redirect('articles:index')
context = {
'article': article,
'form': form,
}
return render(request, 'articles/update.html', context)
READ 수정
- 게시글 작성 user 정보 출력
<!-- articles/index.html -->
{% extends 'base.html' %}
{% block content %}
...
{% for article in articles %}
<p>작성자 : {{ article.user }}</p>
<p>글 번호 : {{ article.pk }}</p>
<p>글 제목 : {{ article.title }}</p>
<p>글 내용 : {{ article.content }}</p>
<a href="{% url 'articles:detail' article.pk %}">[DETAIL]</a>
<hr>
{% endfor %}
{% endblock content %}
- 해당 게시글의 작성자가 아니라면, 수정/삭제 버튼을 출력하지 않도록 처리
<!-- articles/detail.html -->
{% extends 'base.html' %}
{% block content %}
...
{% if user == article.user %}
<a href="{% url 'articles:update' article.pk %}">[UPDATE]</a>
<form action="{% url 'articles:delete' article.pk %}" method="POST">
{% csrf_token %}
<input type="submit" value="DELETE">
</form>
{% endif %}
{% endblock content %}
User - Comment (1:N)
User와 Comment 간 모델 관계 정의 후 migration
# articles/models.py
class Comment(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
...
댓글 출력 필드 수정
- 게시글 작성 페이지에서 불필요한 필드 제외
# articles/forms.py
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
exclude = ('article', 'user',)
CREATE: NOT NULL constraint failed ERROR 수정
- 댓글 작성 시 작성자 정보 (comment.user) 누락
# articles/views.py
@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)
return redirect('accounts:login')
READ 수정
- 비로그인 유저에게는 댓글 form 출력 숨기기
<!-- 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/detail.html -->
{% for comment in comments %}
<li>
{{ comment.user }} - {{ comment.content }}
<form action="{% url 'articles:comments_delete' article.pk comment.pk %}" method="POST" class="d-inline">
{% csrf_token %}
<input type="submit" value="DELETE">
</form>
</li>
{% empty %}
<p>댓글이 없어요..</p>
{% endfor %}
DELETE 수정
- 자신이 작성한 댓글만 삭제 버튼을 볼 수 있도록 수정
<!-- articles/detail.html -->
{% for comment in comments %}
<li>
{{ comment.user }} - {{ comment.content }}
{% if 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 %}
</li>
{% empty %}
<p>댓글이 없어요..</p>
{% endfor %}
- 자신이 작성한 댓글만 삭제할 수 있도록 수정
# articles/views.py
@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()
return redirect('articles:detail', article_pk)
마무리
- Foreign Key
- Comment CREATE
- Comment READ
- Comment DELETE
- Customizing authentication in Django
- Substituting a custom User model
- Custom user & Built-in auth forms