Jak wykorzystać wiele serializerów w jednym widoku DRF

Wydawać by się mogło, że stworzenie standardowego API do aplikacji CRUD nie powinno stanowić większego problemu…

Klasyczne widoki zwykle skumulowane w dwóch endopointach dzielą się następująco:

METODYPRZYKŁADOWY ENDPOINT
list - createapi/users/
retrieve - update - deleteapi/users/<:pk>/

I zgodnie z założeniami DRF jeden endpoint obsługujemy jednym widokiem klasowym, który domyślnie wykorzystuje jeden serializer.

Wyobraźmy sobie jednak sytuację, kiedy klient wymaga jednego formatu danych w przypadku pobierania listy użytkowników (wystarczą mu wówczas takie informacje jak imię, nazwisko i adres email), natomiast w momencie tworzenia nowego użytkownika potrzebuje dodatkowo numeru PESEL, hasła i adresu.

Okazuje się wówczas, że jeden serializer to za mało i wygodniej byłoby stworzyć dwa oddzielne, każdy odpowiedzialny jedynie za swoją metodę (zgodnie z zasadą pojedynczej odpowiedzialności SRP).

Znając podstawy tworzenia widoków wiemy jednak, że w polu serializer_class nie możemy podać więcej niż jednej klasy. Nie oznacza to jednak, że twórcy DRF usunęli ze swojego słownika pojęcie: “wiele serializerów w jednym widoku”.

A zatem w jaki sposób możemy osiagnąć nasz cel?

Dzisiaj postaram się przedstawić Wam kilka możliwych rozwiązań, w zależności od tego, jak zbudowaliście Wasze obecne widoki.

1. Klasa bazowa - APIView

Jeśli Twój aktualny widok oparty jest jedynie o klasę bazową APIView zaimportowaną z biblioteki rest_framework.views, to zapewne prezentuje się podobnie jak poniższy fragment kodu:

from .models import User
from .serializers import UserSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status


class UserAPIView(APIView):
  
    serializer_class = UserSerializer

    def get(self, request):
        users = User.objects.all()
        serializer = self.serializer_class(users, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

Metody get i post odwołują się do zdefiniowanego na poziomie klasy UserSerializer. Jeśli chcemy jednak wykorzystać dwa różne serializery, to nie pozostaje nam nic innego, jak wywołanie ich bezpośrednio w metodach:

class UserAPIView(APIView):
  
    def get(self, request):
        users = User.objects.all()
        serializer = UserListSerializer(users, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = UserCreateSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

Proste, prawda? 😉

2. Generyczna klasa bazowa

Dużo częściej jednak wykorzystywane są generyczne klasy DRF, dzięki którym nie trzeba duplikować kodu, a jedynie przekazać niezbędne informacje, takie jak model czy serializer, zaś resztę implementacji wykona za nas framework.

W takiej sytuacji kod prezentowałby się następująco:

class UserListCreateView(generics.ListCreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer 

I teraz pojawia się problem – nie mamy bowiem jawnie zadeklarowanych metod. Możemy je co prawda nadpisać, jednak w praktyce oznaczałoby to dodatkowe linie kodu, który finalnie sprowadziłby się do takiej postaci jak w poprzednim przykładzie, a chyba nie po to wykorzystujemy klasy generics

Na szczęście twórcy DRF nie zostawili nas z niczym i umożliwili dostęp do wielu ciekawych metod pomocniczych, a jedną z nich jest get_serializer_class(), która domyślnie zwraca klasę przypisaną do pola serializer_class danego widoku.

Wystarczy jednak nadpisać jej logikę i dodać jeden prosty warunek:

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return UserCreateSerializer
        return UserListSerializer 

W ten sposób widok będzie sprawdzał, czy wysłany został request typu POST i w zależności od tego zwracał odpowiedni serializer. Oczywiście możemy dodać kolejne warunki i zwracać tyle serializerów, ile tylko chcemy.

3. Klasa bazowa - ViewSet

class UserViewSet(viewsets.ModelViewSet):

    queryset = User.objects.all()
    serializer_class = UserSerializer 

W tym przypadku również możemy nadpisać metodę get_serializer_class(), jednak w nieco inny sposób:

    def get_serializer_class(self):
        if self.action == 'create':
            return UserCreateSerializer
        return UserListSerializer 

Tym razem zamiast metod sprawdzamy akcję, co ułatwia nam np. rozdzielenie sytuacji pobierania listy i pojedynczego obiektu – w obu przypadkach wywoływana jest metoda GET, jednak z punktu widzenia ViewSets mamy dwie różne akcje: list oraz retrieve i każdą z nich możemy “złapać” w nadpisywanej metodzie i odpowiednio obsłużyć.

Widoki DRF udostępniają też metodę get_serializer(), jednak jej użycie jest ryzykowne. Domyślnie zwraca ona też zawartość pola context, a więc jej nieumiejętne nadpisanie może skutkować utratą zawartości kontekstu serializera.

Wniosek? Chcesz nadpisać tylko instancję serializera? Wykorzystaj metodę, która robi TYLKO i WYŁĄCZNIE TO, a będziesz miał pewność, że nie stracisz przy okazji jakichś ważnych parametrów.

Podsumowanie

Jak widać Django Rest Framework okazuje się elastycznym narzędziem i w prosty sposób pozwala na wykorzystanie wielu serializerów w jednym widoku bez względu na to, jakich klas bazowych używamy. Mała zmiana kodu nie wymaga od nas przebudowywania połowy aplikacji, a zamierzony efekt osiągamy w mgnieniu oka.

I to jest właśnie magia naszego ulubionego frameworka i przykład “czystego kodu” w praktyce. 🙂

Ale mam jeszcze pytanie do Ciebie!

Może znasz jakieś inne sposoby na użycie kilku serializerów w jednym  widoku? Jeśli tak, proszę podziel się swoją wiedzą w komentarzu 😉 .

4.3 4 votes
Article Rating
guest
0 komentarzy
Inline Feedbacks
View all comments