들어가며
이전 포스트에서 DRF에서 CBV 패턴으로 view를 정의하는 법을 정리해보았습니다. 이번에는 CBV 중에서도 “장고 APIView”를 사용해서 CRUD를 구현해보려도 합니다.
1:N 모델에서 CRUD를 하기 위해 게시글 – 댓글 모델에 대한 API를 생성해보겠습니다.
API 설계
Model 정의하기
1:N 모델을 정의하기 위해 ForeignKey를 N에 해당하는 모델에 정의합니다.
from django.db import models
class Articles(models.Model):
title = models.CharField(max_length=50)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Comments(models.Model):
article = models.ForeignKey(
Articles, on_delete=models.CASCADE, related_name="comments")
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
시리얼라이져 정의하기
serializers.ModelSerializer을 상속 받아서 각 모델에 해당하는 시리얼라이져를 작성합니다.
model
:
- Form과 마찬가지로 model에는 연결한 모델명을 입력합니다.
fields
:
- 시리얼라이져의 is_valid()나 save() 와 같은 메소드에 적용시킬 필드를 입력합니다.
__all__
은 모든 필드를 의미합니다.
read_only_fields
:
- is_valid()나 save() 와 같은 메소드에 제외되는 필드를 튜플로 입력합니다.
- save()에서 foreign key로 선언한 값은 따로 받을 수 있지만 is_valid()에서는 그게 안되때문에 read_only_field에 foreign key인 article를 입력해줍니다.
from rest_framework import serializers
from .models import Articles, Comments
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Articles
fields = "__all__"
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comments
fields = "__all__"
read_only_fields = ("article",)
URL 라우팅 정의하기
리스트 및 생성에서는 pk값을 받을 필요가 없지만 디테일, 수정, 삭제를 pk 값을 받아야 함으로 varible routing을 이용합니다.
“장고 APIView”는 CBV이기 때문에 view 클래스.as_view()
형식으로 라우팅 경로를 입력해줍니다.
from django.urls import path
from . import views
app_name = "articles"
urlpatterns = [
path("", views.ArticleListAPIView.as_view(), name="article_list"),
path("<int:pk>/", views.ArticleDetailAPIView.as_view(), name="article_detail"),
path(
"<int:article_pk>/comments/",
views.CommentListAPIView.as_view(),
name="comment_list",
),
path("/comment/<int:comment_id>",
views.CommentDetailAPIView.as_view(),
name="comment_detail",
)
]
View 정의하기
편의 상 Comment에 관한 APIView 코드만 가져왔습니다.
urls.py에서 urlpattern을 pk 값을 받는 것과 받지 않는 것으로 나누었었죠. view도 동일하게 pk를 받는지 여부를 기준으로 나누어 view를 정의합니다.
class CommentListAPIView(APIView)
APIView
형태로 진행하기 위해서CommentListAPIView
를 작성해줍니다.- 댓글 목록과 댓글 생성에서는 글의
pk
가 필요함으로get
함수와post
함수의 인자로 받습니다. get(self, request, pk)
article.comments.all()
- 역참조로 글에 해당하는 댓글 queryset을 모두 가져옵니다.
CommentSerializer(comments, many=True)
- 시리얼라이져에 역참조로 가져온 queryset을 첫 인자로 전달
- 하나의 queryset이 아니기 때문에
many=True
인자 전달
Response(serializer.data)
- 시리얼라이져 데이터를 API 응답으로 보냄
post(self, request, pk)
CommentSerializer(data=request.data)
- 위와 동일하게 시리얼라이져에 데이터를 담는데, get에서는 queryset을 넣었다면 post에서는
request
에서 요청 받은 데이터를 받습니다.
- 위와 동일하게 시리얼라이져에 데이터를 담는데, get에서는 queryset을 넣었다면 post에서는
serializer.is_valid(raise_exception=True)
reuqest
로 요청 받은 데이터가 유효한지 검사- 시리얼라이져에
read_only_field
로 작성된 필드는 제외하고 유효성 검사 raise_exception=True
는 유효성에 문제가 있으면 알아서 에러 상태코드을 반환해주는 인자
serializer.save(article=article)
- POST 요청에 받지 않은 내용은 따로 추가해서 저장할 수 있음
class CommentDetailAPIView(APIView)
CommentListAPIView
와 다르게 글의pk
가 아니라 코멘트의pk
를 받는 로직을 처리delete(self, request, comment_pk)
- delete에서는 댓글 삭제 로직을 구현
- 따로 직렬화한 데이터를 보낼 필요가 없으므로 모델에서 바로 삭제
- Response로 반환하면서 딕셔너리로 메시지 절달
put(self, request, comment_pk)
- 수정을 하는 시리얼라이져는 첫 인자로 모델 데이터를 두 번째 인자로 요청 받은 데이터를 입력
- 시리얼라이져 메소드에
partial=True
을 입력하여 부분 수정도 가능 - 수정 후, 유효성 검사 -> 저장 -> 반환
class CommentListAPIView(APIView):
def get(self, request, pk):
article = get_object_or_404(Articles, pk=pk)
comments = article.comments.all()
serializer = CommentSerializer(comments, many=True)
return Response(serializer.data)
def post(self, request, pk):
# 어떤 글인지 url로 받아옴
article = get_object_or_404(Articles, pk=pk)
serializer = CommentSerializer(data=request.data)
# is_valid()에서 article 정보는 빼고 검사하도록 CommentSerializer의 read_only_field 설정되야함
if serializer.is_valid(raise_exception=True):
# 댓글을 저장할 때 foreign key로 설정한 article을 save()의 인자로 넘김
serializer.save(article=article)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class CommentDetailAPIView(APIView):
def get_object(self, comment_pk):
return get_object_or_404(Comments, pk=comment_pk)
def delete(self, request, comment_pk):
comment = self.get_object(comment_pk)
comment.delete()
data = {"pk": f"{comment_pk} is deleted."}
return Response(data, status=status.HTTP_200_OK)
def put(self, request, comment_pk):
comment = self.get_object(comment_pk)
serializer = CommentSerializer(comment, data=request.data, partial=True)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.data)
API 테스트
테스트를 위한 댓글 seed 생성
django-seed를 사용한 테스트 데이터 생성하기
- article 앱의 모델에 seed를 추가하면 해당 앱의 모든 모델에 데이터가 추가됩니다.
python manage.py seed articles --number=20
- 특정 모델의 특정 값에 데이터 추가 가능
python manage.py seed your_app --number=20 --seeder "your_model" "value"
- articles 앱의 Comments 모델에 10개의 데이터를 추가하기
python manage.py seed articles --number=20 --seeder "Comments" 2
실제로 잘 생성 되었나 DB에서 확인해 봅니다.
댓글 API 테스트
댓글 생성
댓글 조회
새로운 댓글이 추가됨을 확인 할 수 있습니다.
댓글 수정
댓글 삭제
마치며
사실상 form을 배웠다면 다를 게 거의 없다는 걸 알 수 있었습니다.
다만 foreign key 처럼 다른 테이블과 연관관계를 가진 데이터를 입력하기 위해서는 주의해야 되는 점이 2개 있었습니다.
- is_valid() 에서 제외시키기 위해서 시리얼라이져 클래스에 read_only_field를 추가해야 된다는 점
- 시리얼라이져 save() 메서드를 사용할 때, 외래키 값을 전달하기 위해서 연관 테이블 queryset을 인자로 전달해줘야 한다는 점
연습 시 실수 한 사항
urlpatterns
- urlpattern에서 url에 trail slash을 붙이지 않거나 앞쪽 slash를 붙여서 오류난 경우가 종종 있었습니다.
- 어쩔 때는 바로 찾지만 이상하게 어쩔 때는 1시간을 찾아 헤매기도 했습니다…
- ViewSet의 DefaultRouter가 괜히 나온 게 아닌 것 같습니다…