장고 시작하기 25. 장고 APIView 로 CRUD

들어가며

이전 포스트에서 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에서 요청 받은 데이터를 받습니다.
    • 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개 있었습니다.

  1. is_valid() 에서 제외시키기 위해서 시리얼라이져 클래스에 read_only_field를 추가해야 된다는 점
  2. 시리얼라이져 save() 메서드를 사용할 때, 외래키 값을 전달하기 위해서 연관 테이블 queryset을 인자로 전달해줘야 한다는 점

연습 시 실수 한 사항

urlpatterns

  • urlpattern에서 url에 trail slash을 붙이지 않거나 앞쪽 slash를 붙여서 오류난 경우가 종종 있었습니다.
  • 어쩔 때는 바로 찾지만 이상하게 어쩔 때는 1시간을 찾아 헤매기도 했습니다…
  • ViewSet의 DefaultRouter가 괜히 나온 게 아닌 것 같습니다…

참고하면 좋은 글

APIView 관련 DRF 공식 문서

Leave a Comment

목차