[Django]DRFのModelViewSetを使ってみる

## はじめに
自分用の個人開発のメモ/備忘録として記録していきます。


## やりたいこと
Django REST framework（DRF）のModelViewSetを使いたい


## 使い方

### viwes.pyで下記のように書く
※Serializerについては省略する
``` python: views.py
from rest_framework import viewsets

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

### urls.pyで下記のように書く
``` python: views.py
from rest_framework.routers import DefaultRouter
from .views import MyModelViewSet

router = DefaultRouter()
router.register(r'mymodels', MyModelViewSet, basename='mymodel')
urlpatterns = router.urls
```

## 一覧
これだけで下記のようなURL一覧が作成される

例：router.register(r'mymodels', MyModelViewSet) としてルーターに登録した場合のURLパターンと名前を表示

|カテゴリ|メソッド|URL|名前|説明|
|---|---|---|---|---|
|リスト(list)|GET|/mymodels/|mymodel-list|すべての MyModel インスタンスのリストを取得します。|
|詳細(retrieve)|GET|/mymodels/{id}/|mymodel-detail|指定された id の MyModel インスタンスの詳細を取得します。|
|作成(create)|POST|/mymodels/|mymodel-list|新しい MyModel インスタンスを作成します。|
|全体更新(update)|PUT|/mymodels/{id}/|mymodel-detail|指定された id の MyModel インスタンスを全体更新します。|
|部分更新（partial_update）|PATCH|/mymodels/{id}/|mymodel-detail|指定された id の MyModel インスタンスを部分的に更新します。|
|削除(destroy)|DELETE|/mymodels/{id}/|mymodel-detail|指定された id の MyModel インスタンスを削除します。|



## 中身について

ModelViewSetは下記のミックスインクラスと GenericViewSet を継承している
・ CreateModelMixin
・ RetrieveModelMixin
・ UpdateModelMixin
・ DestroyModelMixin
・ ListModelMixin

:::note info
特定のMixinだけ使うこともできる
``` python: views.py
class ModelViewSet(mixins.CreateModelMixin,
                    mixins.UpdateModelMixin,
                    mixins.DestroyModelMixin,
                    viewsets.GenericViewSet):
```
:::

## GenericAPIViewを確認
一緒に継承している```viewsets.GenericViewSet```を見ていくと、```GenericAPIView```を継承していた。
その中でよく使う```queryset``` と ```serializer_class```について確認。

冒頭の注釈にて下記のように書かれています。
```
`get_queryset()` / `get_serializer_class()` をオーバーライドする必要があります。　　
ビューメソッドをオーバーライドする場合は、 `queryset` プロパティに直接アクセスするのではなく、
`get_queryset()` を呼び出すことが重要です。　　
なぜなら、 `queryset` が評価されるのは一度だけであり、その結果は
それ以降のリクエストに対してキャッシュされるからである。
```

```get_queryset()```　と ```get_serializer_class()``` を抜粋

``` python:
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

    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_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
```

List や Retrieve は　下記のように ```get_queryset()``` / ```get_serializer_class()```　をオーバーライドして使うことが多い
<例>
querysetでフィルターを使ったり
retrieveとそれ以外で使うSerializerを分けたりできる

``` python:
def get_queryset(self):
    return MyModel.objects.filter(user=self.request.user)

def get_serializer_class(self):
    if self.action == 'retrieve':
        return MyModelDetailSerializer
    return MyModelSerializer
```



## mixins.pyを確認してみる
Create や Update や Destroy は一部をオーバーライドしてカスタマイズして使うことが多い
元のmixins.pyを確認

### インポートしているもの
``` python: mixins.py
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
```

### CreateModelMixin
オーバーライドする場所としてはsave前の```perform_create```
``` python:
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 {}
``` 

### ListModelMixin
``` python:
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)
``` 

### RetrieveModelMixin
``` python:
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)
``` 

### UpdateModelMixin
オーバーライドする場所としてはsave前の```perform_update```
``` python:
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)
``` 

### DestroyModelMixin
オーバーライドする場所としてはsave前の```perform_destroy```
``` python:
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()
```


## おわりに
・必要なところだけ、カスタマイズして使うと少ない記述量で最低限の機能を実装できるので便利


## 参考

https://www.django-rest-framework.org/api-guide/viewsets/

https://www.django-rest-framework.org/api-guide/routers/#defaultrouter