Django/DRF

[View] DRF CBV 뷰의 종류와 차이점 (APIView, Mixins, Generic APIView, ViewSet)

Jong_seoung 2024. 3. 19. 10:43
반응형

Django에서는 함수기반 뷰(FBV)와 클래스 기반 뷰(CBV) 두 가지 유형의 뷰를 제공합니다.

이중 CBV는 Class Based View의 줄임말로 클래스를 사용하여 뷰를 정의하고 구성하는 방식을 의미합니다.

 

CBV의 고도화 단계로 APIView > Mixins > Generic CBV > ViewSet 순으로 구성되어 있습니다.

모두 동일한 기능을 하는 view의 구현 방식이지만, 오른쪽으로 갈 수록 상속을 이용하여 view를 더욱 간단히 구현할 수 있습니다.

 

APIView

APIView는 원하는 HTTP를 커스터마이징할 수 있다는 장점이 있습니다.

반대로 코드가 더 길어질 수 있다는 단점도 있습니다.

 

Method List

1. Get

  • 주로 리소스를 검색하고 조회하는데 사용된다.
  • 데이터를 읽고 반환하며, 데이터베이스에서 데이터를 가져와 반환해주는 작업이 주로 수행된다.

2. Post

  • 새로운 리소스를 생성하거나 기존 리소스를 수정하는데 사용된다.
  • 데이터를 받아서 처리하고, 데이터베이스에 새로운 데이터를 작성하거나 업데이트하는 작업이 주로 수행된다.

3. Put

  • 클라이언트에서 제공된 데이터로 기존 리소스를 업데이트하는데 사용된다.
  • 전체 리소스를 업데이트 하므로 클라이언트는 모든 필수 데이터를 제공해야 한다.

4. Patch

  • 클라이언트에서 제공된 데이터를 기존 리소스를 일부 업데이트하는 데 사용된다.
  • 전체 리소스가 아닌 일부 필드만 업데이트하므로, 클라이언트는 업데이트 할 필드만 제공할 수 있다.

5. Delete

  • 지정된 리소스를 삭제하는데 사용된다.
  • 데이터베이스에서 해당 리소스를 삭제하는 작업이 주로 이루어진다.

6. Head

  • Get 요청과 유사하지만, 실제 데이터를 반환하지 않고 헤더 정보만 반환한다.

7. Options

  • 사용 가능한 HTTP 메소드 및 기타 엔드포인트에 대한 정보를 제공한다.
  • 클라이언트에게 서버에서 지원되는 기능 및 요청 가능한 작업에 대한 정보를 제공한다.

 

 

Mixins

APIView와 Mixins의 큰 차이점은 불필요한 코드의 중복을 얼마나 줄일 수 있는가 입니다. 

Mixins는 기능별로 작은 조각의 코드를 작성하고 이를 필요에 따라 조합하여 사용하는 것 입니다.

 

Django-rest-framework 공식 github 코드

더보기
"""
Basic building blocks for generic class based views.

We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways.
"""
from django.db.models.query import prefetch_related_objects

from rest_framework import status
from rest_framework.response import Response
from rest_framework.settings import api_settings


class CreateModelMixin:
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}


class ListModelMixin:
    """
    List a queryset.
    """
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)


class RetrieveModelMixin:
    """
    Retrieve a model instance.
    """
    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)


class UpdateModelMixin:
    """
    Update a model instance.
    """
    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        queryset = self.filter_queryset(self.get_queryset())
        if queryset._prefetch_related_lookups:
            # If 'prefetch_related' has been applied to a queryset, we need to
            # forcibly invalidate the prefetch cache on the instance,
            # and then re-prefetch related objects
            instance._prefetched_objects_cache = {}
            prefetch_related_objects([instance], *queryset._prefetch_related_lookups)

        return Response(serializer.data)

    def perform_update(self, serializer):
        serializer.save()

    def partial_update(self, request, *args, **kwargs):
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)


class DestroyModelMixin:
    """
    Destroy a model instance.
    """
    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)

    def perform_destroy(self, instance):
        instance.delete()

 

Generics APIView

Mixin 클래스와 함께 사용될 때 GenericAPIView는 Mixin 클래스에 구현된 특정 기능을 추가하여 CRUD 작업을 처리합니다.

 

Django-rest-framework 공식 github 코드

더보기
"""
Generic views that provide commonly needed behaviour.
"""
from django.core.exceptions import ValidationError
from django.db.models.query import QuerySet
from django.http import Http404
from django.shortcuts import get_object_or_404 as _get_object_or_404

from rest_framework import mixins, views
from rest_framework.settings import api_settings


def get_object_or_404(queryset, *filter_args, **filter_kwargs):
    """
    Same as Django's standard shortcut, but make sure to also raise 404
    if the filter_kwargs don't match the required types.
    """
    try:
        return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
    except (TypeError, ValueError, ValidationError):
        raise Http404


class GenericAPIView(views.APIView):
    """
    Base class for all other generic views.
    """
    # You'll need to either set these attributes,
    # or override `get_queryset()`/`get_serializer_class()`.
    # If you are overriding a view method, it is important that you call
    # `get_queryset()` instead of accessing the `queryset` property directly,
    # as `queryset` will get evaluated only once, and those results are cached
    # for all subsequent requests.
    queryset = None
    serializer_class = None

    # If you want to use object lookups other than pk, set 'lookup_field'.
    # For more complex lookup requirements override `get_object()`.
    lookup_field = 'pk'
    lookup_url_kwarg = None

    # The filter backend classes to use for queryset filtering
    filter_backends = api_settings.DEFAULT_FILTER_BACKENDS

    # The style to use for queryset pagination.
    pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

    # Allow generic typing checking for generic views.
    def __class_getitem__(cls, *args, **kwargs):
        return cls

    def get_queryset(self):
        """
        Get the list of items for this view.
        This must be an iterable, and may be a queryset.
        Defaults to using `self.queryset`.

        This method should always be used rather than accessing `self.queryset`
        directly, as `self.queryset` gets evaluated only once, and those results
        are cached for all subsequent requests.

        You may want to override this if you need to provide different
        querysets depending on the incoming request.

        (Eg. return a list of items that is specific to the user)
        """
        assert self.queryset is not None, (
            "'%s' should either include a `queryset` attribute, "
            "or override the `get_queryset()` method."
            % self.__class__.__name__
        )

        queryset = self.queryset
        if isinstance(queryset, QuerySet):
            # Ensure queryset is re-evaluated on each request.
            queryset = queryset.all()
        return queryset

    def get_object(self):
        """
        Returns the object the view is displaying.

        You may want to override this if you need to provide non-standard
        queryset lookups.  Eg if objects are referenced using multiple
        keyword arguments in the url conf.
        """
        queryset = self.filter_queryset(self.get_queryset())

        # Perform the lookup filtering.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

        assert lookup_url_kwarg in self.kwargs, (
            'Expected view %s to be called with a URL keyword argument '
            'named "%s". Fix your URL conf, or set the `.lookup_field` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, lookup_url_kwarg)
        )

        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        # May raise a permission denied
        self.check_object_permissions(self.request, obj)

        return obj

    def get_serializer(self, *args, **kwargs):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs.setdefault('context', self.get_serializer_context())
        return serializer_class(*args, **kwargs)

    def get_serializer_class(self):
        """
        Return the class to use for the serializer.
        Defaults to using `self.serializer_class`.

        You may want to override this if you need to provide different
        serializations depending on the incoming request.

        (Eg. admins get full serialization, others get basic serialization)
        """
        assert self.serializer_class is not None, (
            "'%s' should either include a `serializer_class` attribute, "
            "or override the `get_serializer_class()` method."
            % self.__class__.__name__
        )

        return self.serializer_class

    def get_serializer_context(self):
        """
        Extra context provided to the serializer class.
        """
        return {
            'request': self.request,
            'format': self.format_kwarg,
            'view': self
        }

    def filter_queryset(self, queryset):
        """
        Given a queryset, filter it with whichever filter backend is in use.

        You are unlikely to want to override this method, although you may need
        to call it either from a list view, or from a custom `get_object`
        method if you want to apply the configured filtering backend to the
        default queryset.
        """
        for backend in list(self.filter_backends):
            queryset = backend().filter_queryset(self.request, queryset, self)
        return queryset

    @property
    def paginator(self):
        """
        The paginator instance associated with the view, or `None`.
        """
        if not hasattr(self, '_paginator'):
            if self.pagination_class is None:
                self._paginator = None
            else:
                self._paginator = self.pagination_class()
        return self._paginator

    def paginate_queryset(self, queryset):
        """
        Return a single page of results, or `None` if pagination is disabled.
        """
        if self.paginator is None:
            return None
        return self.paginator.paginate_queryset(queryset, self.request, view=self)

    def get_paginated_response(self, data):
        """
        Return a paginated style `Response` object for the given output data.
        """
        assert self.paginator is not None
        return self.paginator.get_paginated_response(data)


# Concrete view classes that provide method handlers
# by composing the mixin classes with the base view.

class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):
    """
    Concrete view for creating a model instance.
    """
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)


class ListAPIView(mixins.ListModelMixin,
                  GenericAPIView):
    """
    Concrete view for listing a queryset.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)


class RetrieveAPIView(mixins.RetrieveModelMixin,
                      GenericAPIView):
    """
    Concrete view for retrieving a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)


class DestroyAPIView(mixins.DestroyModelMixin,
                     GenericAPIView):
    """
    Concrete view for deleting a model instance.
    """
    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)


class UpdateAPIView(mixins.UpdateModelMixin,
                    GenericAPIView):
    """
    Concrete view for updating a model instance.
    """
    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)


class ListCreateAPIView(mixins.ListModelMixin,
                        mixins.CreateModelMixin,
                        GenericAPIView):
    """
    Concrete view for listing a queryset or creating a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)


class RetrieveUpdateAPIView(mixins.RetrieveModelMixin,
                            mixins.UpdateModelMixin,
                            GenericAPIView):
    """
    Concrete view for retrieving, updating a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)


class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
                             mixins.DestroyModelMixin,
                             GenericAPIView):
    """
    Concrete view for retrieving or deleting a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)


class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
                                   mixins.UpdateModelMixin,
                                   mixins.DestroyModelMixin,
                                   GenericAPIView):
    """
    Concrete view for retrieving, updating or deleting a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

 

 

ViewSets

ViewSet은 상속을 여러번 진행한 만큼 코드가 매우 간결해진다.

viewset을 하나만 상속함으로써 CRUD를 모드 상속받을 수 있다.

 

from rest_framework import viewsets
from .models import MyModel
from .serializers import MyModelSerializer

class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer

 

ViewSet Path 묶기

ViewSet의 경우 하나의 Class에 객체의 목록을 보여주는 기능과 하나의 객체의 detail을 보여주는 기능이 모두 존재하지만 전자의 경우 pk값이 필요하지않지만 후자의 경우 필수이기 때문에 서로 다른 path를 묶어주는 기능이 필요합니다.

 

Path를 묶어주는 방법으로는 as_view함수를 이용하는 것과 router를 이용하는 방법이 있습니다. 

 

as_view를 이용한 Path 연결

더보기
from django.urls import path
from .views import BlogViewSet

# Blog 목록 보여주기
blog_list = BlogViewSet.as_view({
    'get': 'list',
    'post': 'create'
})

# Blog detail 보여주기 + 수정 + 삭제
blog_detail = BlogViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'delete': 'destroy'
})

urlpatterns =[
    path('blog/', blog_list),
    path('blog/<int:pk>/', blog_detail),
]

 

router를 이요한 Path묶기

더보기
from django.urls import path, include
from .views import BlogViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
# 첫 번째 인자는 url의 prefix
# 두 번째 인자는 ViewSet
router.register('blog', BlogViewSet)

urlpatterns =[
    path('', include(router.urls))
]

 

 

총 정리

# APIView
from rest_framework.views import APIView
class SnippetsAPIView(APIVIEW):
	def get(self, request):
		pass
	def post(self, request):
		pass

# Mixins
from rest_framework import mixins
RetrieveModelMixin : Get
ListModelMixin : Gets
CreateModelMixin : Post
UpdateModelMixin : Put
DestroyModelMixin : delete

# Generics APIView
from rest_framework import generics
generics.CreateAPIView : 생성
generics.ListAPIView : 목록
generics.RetrieveAPIView : 조회
generics.DestroyAPIView : 삭제
generics.UpdateAPIView : 수정
generics.RetrieveUpdateAPIView : 조회/수정
generics.RetrieveDestroyAPIView : 조회/삭제
generics.ListCreateAPIView : 목록/생성
generics.RetrieveUpdateDestroyAPIView : 조회/수정/삭제

# ViewSets - Router 사용해야함
from rest_framework import viewsets
viewsets.ReadOnlyModelViewSet : 목록 조회, 특정 레코드 조회 (get만 가능)
viewsets.ModelViewSet : 목록 조회, 특정 레코드 생성/조회/수정/삭제 전부 자동으로 만들어줌.
Viewsets.ViewSet : 자동으로 만들지마! 내가 다 만들께 

urls.py
from rest_framework.routers import DefaultRouter
post_router = DefaultRouter()
post_router.register('viewset',views.PostViewSet)

 

 

출처

 

[D.R.F] ViewSets으로 CRUD 구현하기

안녕하세요 ๑(•‿•)๑ 오늘은 CRUD 구현의 마지막 방식인 ViewSet을 사용해보도록 하겠습니다. APIView에서부터 Mixins, 그리고 Generic CBV를 거쳐 ViewSet까지 왔네요 ㅎㅎ ViewSet은 상속을 여러 번 거친

wisdom-990629.tistory.com

 

Home - Django REST framework

 

www.django-rest-framework.org

 

[Django] APIView, Mixins, generics APIView, ViewSet을 알아보자

django 에서는 view 를 통해서 HTTP 요청을 처리합니다. view에서 이를 처리하는 방법은 다양합니다. FBV(함수기반뷰), CBV(클래스기반뷰) 를 통해서도 API 를 만들 수 있지만 rest_framework 는 보다 쉽게, 효

ssungkang.tistory.com

 

django Rest framework APIView / Mixins / Viewset

- 아래의 코드는 python2.7 , restframework 3.6.4 의 내용입니다. 최신버전의 내용과 다른 부분이 있습니다. 1.APIView APIView -> 클래스 기반 (CBV : class base view) @api_view -> 함수기반 (FBV : function base view) APIView

uiandwe.tistory.com

 

반응형