Viewsety w DRF, czyli jak stworzyć API w 5 minut


Ten artykuł zakłada, że znasz podstawy Django i Django Rest Framework.

Fragmenty kodu pochodzą z oficjalnej dokumentacji DRF.


Jeśli zdarzyło Ci się budować typowe aplikacje obsługujące operacje CRUD, mogłeś dostrzec powtarzalność kodu w tworzonych widokach.

Twórcy Django Rest Framework udostępniają jednak ciekawe rozwiązanie, które pozwala obsłużyć standardowego CRUDA minimalnym nakładem pracy i maksymalnie ograniczoną ilością kodu.

Są to tzw. viewsety.

Można je potraktować jako swoiste „kontrolery”, które definiują jednolitą struktrurę routingu i widoków. Nie oznacza to jednak, że nie mamy możliwości rozszerzeń i modyfikacji (ale o tym za chwilę 😊).


Podstawowe akcje

Bazowa klasa Viewset implementuje 6 podstawowych metod, zwanych akcjami (actions), które są odpowiednikami standardowych metod, jakie zwykle wykonujemy na obiektach.

def list(self, request):
    pass
def create(self, request):
    pass
def retrieve(self, request, pk=None):
    pass
def update(self, request, pk=None):
    pass
def partial_update(self, request, pk=None):
    pass
def destroy(self, request, pk=None):
    pass

Jak widać, akcje list i create dotyczą całej kolekcji obiektów i nie wymagają żadnych dodatkowych parametrów. Wszystkie pozostałe akcje operują na pojedynczym obiekcie, stąd też wymagają dodatkowego argumentu, zwyczajowo oznaczanego pk (skrót od primary key), który pozwala jednoznacznie zindetyfikować obiekt.


💡 W zależności od struktury danych, zamiast parametru pk można wykorzystać dowolne inne pole, którego wartość jest unikalna dla każdego obiektu w ramach całej kolekcji. DRF udostępnia pole lookup_field, które zdefiniowane na poziomie widoku lub akcji nadpisuje domyślną wartość pk. Przykładowo zapis lookup_field = name sprawi, że w danym modelu jako identyfikator obiektu użyte zostanie pole name.



Skąd bierze się parametr <pk>?

Cała magia kryje się w definicji routingu, czyli ścieżek url, które wywołują powyższe akcje.

Przykładowa definicja routingu w pliku urls.py mogłaby wyglądać następująco:

from .views import UserViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
urlpatterns = router.urls

Jedna linia kodu definiuje wszystkie możliwe ścieżki i połączone z nimi metody http, które wyglądają następująco:

  • URL: ^users/$ , nazwa: 'user-list':
    • metoda GET wywoła akcję list(),
    • metoda POST wywoła akcję create(),
  • URL  ^users/{pk}/$ nazwa: 'user-detail'
    • Metoda GET wywoła akcję retrieve(),
    • Metoda PUT wywoła akcję update(),
    • Metoda PATCH wywoła akcję partial_update(),
    • Metoda DELETE wywoła akcję destroy().

W ten sposób zaledwie 2 ścieżki url obsługują 6 możliwych widoków. Czas więc zaimplementować naszego CRUDA. Zacznijmy od akcji list i retrieve:

from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from myapps.serializers import UserSerializer
from rest_framework import viewsets
from rest_framework.response import Response

class UserViewSet(viewsets.ViewSet):
    """
    A simple ViewSet for listing or retrieving users.
    """
    def list(self, request):
        queryset = User.objects.all()
        serializer = UserSerializer(queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        queryset = User.objects.all()
        user = get_object_or_404(queryset, pk=pk)
        serializer = UserSerializer(user)
        return Response(serializer.data)

No dobra, chyba obiecałam Wam minimalną ilość kodu, a tutaj widać wyraźnie, że kolejne akcje wymagają 3-4 linii kodu, z czego 90% to duplikaty. Czy możemy coś z tym zrobić?
No pewnie!

ModelViewSet


Twórcy DRF udostępnili secjalną klasę ModelViewSet, która tworzy wszystkie niezbędne akcje za nas. Jedyne, co musimy jej podać, to queryset, czyli zbiór danych, na których ma operować oraz serializer_class:

class UserViewSet(viewsets.ModelViewSet):
    """
    A viewset for viewing and editing user instances.
    """
    serializer_class = UserSerializer
    queryset = User.objects.all()

Jeśli w kodzie korzystamy z obiektów opisanych modelami (a dzieje się tak w zdecydowanej większości przypadków), to warto wykorzystać tę klasę.

W ten sposób zaledwie 3 linie kodu obsługują 6 standardowych akcji CRUDa. Piękne, prawda😀?

Dodatkowe akcje

Wbudowana obsługa podstawowych akcji to tylko początek możliwości viewsetów. Nic nie ogranicza nas przed dodaniem dowolnej ilości własnych metod. Wystarczy, że przed każdą z nich dodamy dekorator @action, dzięki któremu wybrane metody zostaną potraktowane tak samo jak wbudowane akcje i dodane do routingu.

from django.contrib.auth.models import User
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from myapp.serializers import UserSerializer, PasswordSerializer

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    
    @action(detail=True, methods=['post'])
    def set_password(self, request, pk=None):
        user = self.get_object()
        serializer = PasswordSerializer(data=request.data)
        if serializer.is_valid():
            user.set_password(serializer.validated_data['password'])
            user.save()
            return Response({'status': 'password set'})
         else:
            return Response(serializer.errors,
            status=status.HTTP_400_BAD_REQUEST)

Opakowanie metody set_password dekoratorem @action(detail=True, methods=['post']) sprawi, że nowa metoda będzie dostępna pod adresem: ^users/{pk}/set-password/$ dla metody POST.


Jakie parametry udostępnia dekorator @action?

  • detail: true / false – określa, czy akcja będzie wymagała parametru pk,
  • methods: string[] – w liście należy podać wszystkie metody http, które mają być obsługiwane przez daną akcję,
  • name: string – nadpisanie nazwy urla nadawanej domyślnie przez Django.
  • wszystkie inne pola konfiguracyjne definiowane z poziomu Viewsetu, takie jak: permission_classes, serializer_class. Dodanie ich do dekaratora nadpisze wartość pola zdefiniowanego na poziomie klasy w ramach danej akcji (więcej szczegółów znajdziecie w oficjalnej dokumentacji).

Ale to jeszcze nie koniec możliwości Viewsetów. Skoro możemy dowolnie rozszerzać ModelViewSet, czemu by nie zawężyć zakresu dostępnych metod?

ReadOnlyModelViewSet

DRF udostępnia klasę ReadOnlyModelViewSet, która implementuje jedynie metody read-only (tylko do odczytu), czyli list i retrieve. Dzięki temu możemy zabezpieczyć widoki, w których nie chcemy udostępniać użytkownikom pozostałych metod.

Jej przykładowe użycie może wyglądać tak:

class AccountViewSet(viewsets.ReadOnlyModelViewSet):
    """
    A simple ViewSet for viewing accounts.
    """
    queryset = Account.objects.all()
    serializer_class = AccountSerializer

A co w przypadku, gdy potrzebujemy innego zestawu metod? Wtedy możemy zdefiniować własny spersonalizowany ViewSet i wykorzystywać go w projekcie bez ograniczeń.

Własne klasy ViewSet

Każda ze standardowych akcji jest udostępniana przez tzw. mixiny, które można wykorzystywać we własnych klasach niezależnie od siebie. Wystarczy, że tworzona przez nas klasa będzie dziedziczyła po wybranym mixinie ORAZ po generycznej klasie Viewsetów: GenericViewSet . Należy zachować odpowiednią kolejność klas nadrzędnych, z których dziedziczymy: najpierw podajemy wszystkie mixiny, a następnie klasę generyczną.

from rest_framework import mixins

class CreateListRetrieveViewSet(mixins.CreateModelMixin,
                                mixins.ListModelMixin,
                                mixins.RetrieveModelMixin,
                                viewsets.GenericViewSet):
    """
    A viewset that provides `retrieve`, `create`, and `list` actions.

    To use it, override the class and set the `.queryset` and
    `.serializer_class` attributes.
    """
    pass

W ten sposób mamy możliwość stworzenia dowolnej kombinacji mixinów i zaimplementowania indywidualnej logiki do każdej metody.

I co dalej?

Ten artykuł to tylko mały wycinek wszystkich możliwości viewsetów. Jeśli spodobała Ci się prostota i minimalizm tego podejścia, gorąco zachęcam do uruchomienia edytora kodu i stworzenie pierwszego API z wykorzystaniem Viewsetów. Nie bój się eksperymentować 🦾!

I pamiętaj, że oficjalna dokumentacja to najważniejsze źródło informacji. Zaglądaj do niej jak najczęściej.

A w razie pytań lub wątpliwości pisz śmiało w komentarzu lub wiadomości prywatnej.

Niech Django będzie z Tobą 🎉!

5 1 vote
Article Rating
guest
2 komentarzy
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
anonim
28 dni temu

Ciekawy artykuł. Wykorzystam kilka tricków w swoich projektach. 🙂

Poczytałbym nt. różnic pomiędzy APIView vs ViewSet