목차
1. Mixins
2. Generics APIView
3. Viewset & router
4. action-decorator
5. Nested router
위 사이트를 참고하면서 읽자.
1. Mixins
APIView는 이전 포스팅에서 간략히 다뤘었는데 request마다 하나하나 상속받을 view를 지정하고 serializer와 연결해주어야 했기 때문에 코드의 낭비가 심했었다.
그래서 rest_framework.mixins는 이런 기능들을 만들어 다중 상속을 받을 수 있게끔 지원하고 있다.
어렵게 받아들일 것 없이 그대로 이해하면 된다.
queryset과 serializer_class를 지정해주면 필요한 Mixin을 다중 상속받아 연결해주면 된다.
urlpatterns = [
path('api/example/', views.ExampleListMixins.as_view()),
path('api/exapme/<int:pk>/', views.ExampleDetailMixins.as_view()),
]
# views.py
from rest_framework.response import Response
from rest_framework import generics
from rest_framework import mixins
from .models import Post
from .serializers import PostSerializer
class ExampleListMixins(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
def get(self, request, *args, **kwargs):
return self.list(request)
def post(self, request, *args, **kwargs):
return self.create(request)
class ExampleDetailMixins(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.delete(request, *args, **kwargs)
이렇게 되면 Detail 정보의 필요여부에 따라 하나의 클래스에 모든 요청을 묶어 버릴 수 있다.
🤔 post? create? perform_create?
drf를 어느정도 여기까지 내용을 이해한 사람은 이런 의문이 들 수 있다.
어떤 정보를 drf에 추가하기 위해서는 body에 data를 담아서 post 요청을 보내야 하는데,
Mixin을 상속받더니 갑자기 def post가 아니라 def create라는 메서드를 오버라이딩 하고 있다.
그렇다면 둘은 어떤 차이가 있고, perform_create는 무엇일까?
CreateAPIView를 예시로 들어보자.
해당 뷰는 CreateModelMixin과 GenericAPIView, APIView를 상속받고 있다.
그리고 각각의 메서드 코드는 다음과 같다.
class CreateAPIView(mixins.CreateModelMixin, GenericAPIView):
(...)
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 post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
(...)
create와 perform_create는 CreateModelMixin에서 상속받아왔고, post 메서드는 CreateAPIView에 정의된 함수이다.
클라이언트가 보낸 데이터를 create 메서드에게 넘기면, create 메서드가 실행되면서 작업을 수행한다.
이때, perform_create 메서드를 이용하면 serializer 이전에 좀 더 세부적인 작업이 가능한 것.
generic을 상속받으면 queryset과 serializer_class만 정해줘도 알아서 잘 돌아가는 이유가 바로 이런 이유 때문이다.
(APIView나 GenericAPIView에는 post나 get 메서드가 정의되어 있지 않다.)
2. Generics APIView
Mixin을 상속하면 코드의 반복을 통해 가독성을 높일 수 있긴 하지만 아무래도 상속 받는 부모 클래스가 많아지다보면 자식 클래스가 정확히 어떤 작업을 수행하는 View인지 한 눈에 들어오지 않을 수 있다.
그래서 rest_framework는 상속받은 Mixin 종류에 따라 또 한 번 클래스를 정의해놓았다.
예를 들어 RetrieveDestroyAPIView의 경우에는 조회/삭제가 가능한 뷰이므로 RetrieveModelMixiin과 DestroyModelMixin을 상속받고 있다.
상속을 받을 때, 좀 더 깔끔하고 직관적으로 할 수 있다는 차이가 있을 뿐이지 실행 결과는 위와 동일하다.
3. Viewset & router
귀찮음의 끝판왕이다.
만약 detail이 True가 됐건, False가 됐건 같은 queryset과 serializer_class를 사용한다면 굳이 클래스를 양분할 필요가 있을까?
이를 해결하기 위해 나온 것이 바로 ViewSet이다.
ViewSet은 CBV가 아니다. CBV가 아니기 때문에 하나의 뷰가 아닌, 여러 뷰를 만들 수 있는 확장된 CBV이기 때문에 .as_view()로 라우트하지 않고 router를 따로 사용해야 하는 것이다.
(만약 정 as_view 함수를 써야한다면 각각의 뷰를 모두 설정해주어야 한다. 코드가 순조롭게 더러워진다.)
ViewSet은 헬퍼클래스로써 두 가지 종류가 존재한다.
- viewsets.ReadOnlyModelViewSet (리스트 / 특정 레코드 조회)
- 목록
- mixins.ListModelMixin : list
- 특정 레코드
- mixins.RetrieveModelMixin : retrieve
- 목록
- viewsets.ModelViewSet (조회 / 생성 / 삭제 / 수정)
- 목록
- mixins.ListModelMixin : list
- 특정 레코드
- mixins.RetrieveModelMixin : retrieve
- 레코드 생성
- mixins.CreateModelMixin : create
- 레코드 수정
- mixins.UpdateModelMixin : update, partial_update
- 부분적인 필드값만 받아 수정이 가능, request method 중 Fetch 와 대응된다.
- 레코드 삭제
- mixins.DestroyModelMixin : destroy() 함수
- 목록
# urls.py
urlpatterns = [
path('api/example/', views.example_list),
path('api/example/<int:pk>/', views.example_detail),
]
# views.py
class ExampleViewSet(ModelViewSet):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
example_list = ExampleViewSet.as_view({
'get': 'list',
'post': 'create',
})
example_detail = ExampleViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy',
})
하지만 as_view로 정의하는 건 아무래도 코드가 너무 길어지는 감이 있다.
그래서 보통 이렇게 하지 않고 router을 import하여 사용한다.
router는 사용자가 하나하나 매칭시키게 하지 않고, 자동으로 연결시켜 작동하도록 한다.
- list route
- url : /prefix/
- name : {model name}-list (단, model name 은 소문자.)
- 'get': 'list' 'post': 'create'
- detail route
- url : /prefix/pk/
- name : {model name}-detail
- 'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy'
# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'post',views.PostViewSet)
urlpatterns = [
path('',include(router.urls)),
]
# views.py
class ExampleViewSet(ModelViewSet):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
고작 위의 코드로 Example 모델에 대한 CRUD를 완성하게 되었다.
💡 SimpleRouter()와 DefaultRouter()
내용을 읽기 귀찮다면 하나만 알면 된다. 차이를 모르겠으면 그냥 DefaultRouter를 사용한다.
router란 결국 사용자가 직접 매칭시켰어야 하는 내용들을 알잘딱깔센 정리해주는 건데,
거기에 몇 가지 url을 더 추가해서 사용할 수 있게 만들어놓기까지 했다.
SimpleRouter는 매칭만하고 끝났다면, DefaultRouter는 API root 페이지와 format suffix라는 응답 포맷의 접미사를 붙임으로써 api/example.json이나 api/example/1.api 라는 방식으로 확인할 수 있다.
4. action-decorator
하지만 어디까지나 router와 modelViewSet을 활용하면 default mapping의 경우밖에 처리하지 못 한다.
list router 2가지와 detail router 4가지 경우 외에 추가적인 경우가 필요하면 어떻게 할까?
바로 ModelViewSet을 내팽개치고 generic을 상속받으러 험난한 여정을 거쳐야 할까?
그렇지 않다. 그냥 ViewSet을 상속받고 있는 클래스 내부에 메서드명을 정하고 decorator만 잘 붙여주면, url은 Router가 알아서 잘 정해준다. 원한다면 이 url 경로도 변경할 수 있다.
from rest_framework.decorators import action
action의 인자로는 detail, methods, url_path, url_name이 있다.
물론 전부 default 값이 정해져 있으며, 보통 detail과 methods만 재정의하는 경우가 많다.
- detail=True
- url : /prefix/{pk}/{function name}/
- name : {model name}-{function name}
- detail=False
- url : /prefix/{function name}/
- name : {model name}-{function name}
class ExampleViewSet(ModelViewSet):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
# url : example/public_list/
@action(detail=False)
def public_list(self, request):
qs = self.queryset.filter(is_public=True)
serializer = self.get_serializer(qs, many=True)
return Response(serializer.data)
# url : example/{pk}/set_public/
@action(detail=True, methods=['patch'])
def set_public(self, request, pk):
instance = self.get_object()
instance.is_public = True
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data)
5. Nested router
보통 소규모 프로젝트의 경우 ModelViewSet과 router까지 이해할 정도면 거의 모든 작업이 처리된다.
하지만 router의 치명적인 단점 중 하나로 다음과 같은 케이스가 존재한다.
해당 관계형 모델에서 내가 원하는 것은 '특정' 펫 모델을 참조하는 '특정' 싸이클이라고 가정하자.
그렇다면 url은 아래처럼 작성될 것이다.
/api/pets/{pet_id}/cycles/{cycle_id}/
이게 왜 문제일까? 위에서 배운대로 urls.py를 작성해보자.
from rest_framework import router
router = routers.SimpleRouter()
router.register('pets', views.LibraryViewSet)
router.register('cylces',views.BookViewSet)
urlpatterns = [
path('', include(router.urls)),
]
이렇게 하면 pets를 router에 등록한 다음에 cycles의 url을 등록했으니 원하는 대로 동작하지 않을까?
애석하게도 그렇지 않다.
Django의 router는 아래와 같은 url pattern을 생성한다.
URL pattern : ^pets/$, name : 'pet-list'
URL pattern : ^pets/{pk}/$, name : 'pet-detail'
URL pattern : ^cycles/$, name : 'pet-list'
URL pattern : ^cycles/{pk}/$, name : 'pet-list'
pet과 cycle이 분리된 URL 패턴을 생성하기 때문에 목적에 부합하지 않는다.
router만 이용해서 이 문제를 해결하고 싶다면 다음의 방법을 사용해야 한다.
# Get list of cycles in a Pet
url(r'^pets/(?P<pet_pk>\d+)/cycles/?$',
views.CycleViewSet.as_view({'get':'list'}), name='pet-cycle-list')
# Get deatil of a cycle in a Pet
url(r'^pets/(?P<pet_pk>\d+)/cycles/(?P<pk>\d+)/?$',
views.CycleViewSet.as_view({'get':'retrieve'}), name='pet-cycle-detail')
class CycleViewSet(viewsets.ModelViewSet):
"""Viewset of cylce"""
queryset = Cycle.objects.all().select_related(
'pet'
).prefetch_related(
'regular_cycle'
)
serializer_class = CycleSerializer
def get_queryset(self, *args, **kwargs):
pet_id = self.kwargs.get("pet_pk")
try:
pet = Pet.objects.get(id=pet_id)
except Pet.DoesNotExist:
raise NotFound('A pet with this id does not exist')
return self.queryset.filter(pet=pet)
여기까지 온 것을 축하하며, 여러분들은 드디어 중첩된 라우터를 컨트롤할 수 있게 된다.
하지만 안심해도 좋은 것이 나라도 이딴 방법으로 코딩하라고 했으면 사용하지 않았을 것이다.
다행히 이 문제를 훨씬 쉽게 해결할 수 있는 방법이 존재한다.
✨ drf-nested-routers
from rest_framework_nested import routers
router = SimpleRouter()
router.register('pets', views.PetViewSet)
cycle_router = routers.NestedSimpleRouter(
router,
r'pets',
lookup='pet'
)
cycle_router.register(
r'books',
views.CycleViewSet,
basename='pet-cycle'
)
app_name = 'pet'
urlpatterns = [
path('', include(router.urls)),
path('', include(cycle_router.urls)),
]
bash에서 drf-nested-routers를 install하고 url을 위에 처럼 구성해주면 놀랍게도 ViewSet에서 별다른 정의 없이 사용가능하다.
심지어 가독성까지 높아지는 일석이조의 효과를 거둘 수 있다.
저를 구원해준 한 줄기 빛 같은 블로그에서 참조해왔습니다.