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 <strong>lookup_field</strong>
, 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()
,
- metoda GET wywoła akcję
- 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()
.
- Metoda GET wywoła akcję
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 <strong>queryset</strong>
, 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 <strong>@action</strong>
, 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 parametrupk
,m<span class="has-inline-color has-black-color">ethods</span>
: 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ą ?!
Ciekawy artykuł. Wykorzystam kilka tricków w swoich projektach. 🙂
Poczytałbym nt. różnic pomiędzy APIView vs ViewSet
Cieszę się, że Ci się podoba ?.
Bardzo dobry pomysł z tym porównaniem, przyznam, że chodziło mi coś podobnego po głowie, więc chyba czas to zrealizować ?.
Dzięki za inspirację!
beda jakies nowe materialy?