Day3 你必須要知道的drf最佳實踐的十件事

知魚君發表於2020-04-04

翻譯文章,原文地址:medium.com/profil-soft…

1. ViewSets

viewsets的好處是使得你的程式碼保持一致,並且免於重複。如果你編寫的views不止去做一件事,那麼viewsets就是你想要的東西。

舉例來說,如果你有一個model叫做Tag,你需要列表、建立和詳情的功能,你可以定義一個viewset:

from rest_framework import mixins, permissions
from rest_framework.viewsets import GenericViewSet


class TagViewSet(mixins.ListModelMixin,
                 mixins.CreateModelMixin,
                 mixins.RetrieveModelMixin,
                 GenericViewSet):
    """
    The following endpoints are fully provided by mixins:
    * List view
    * Create view
    """
    queryset = Tag.objects.all()
    serializer_class = TagSerializer
    permission_classes = (permissions.IsAuthenticated,)
複製程式碼

viewset的mixins可以被自由組合,你可以定義自己的mixins或者使用ModelViewSet。

ModelViewset可以為你提供以下方法:.list(),.retrieve(), .create(), .update(), .partial_update(), .destroy()

此外,當你使用viewsets時,也會令你的路由配置更加的清晰。

from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter


api_router = DefaultRouter()
api_router.register(r'tag', TagViewSet, 'tag')

urlpatterns = [
   url(r'^v1/', include(api_router.urls, namespace='v1'))
]

複製程式碼

現在,你的viewset可以幫你實現以下功能:

  • 獲取Tag列表,傳送GET請求給 v1/tag/
  • 建立Tag,傳送POST請求給 v1/tag/
  • 獲取特定Tag,傳送GET請求給v1/tag/<tag_id>

你甚至可以在viewset裡面通過@action裝飾器新增一些自定義的路由。

2. 理解不同型別的serializers

作為一個DRF的使用者,你不必太去關心views或者路由配置,所以你可能會把絕大部分精力放在serializers上來。

serializers是充當Django的model及其表現形式(例如json)之間的翻譯器。每一個serializer能夠既被用作讀也可用作寫,初始化的方式決定了它將執行的動作。我們可以區分出三種不同型別的serializer: create, update, retrieve

如果你想要在序列化器外部傳輸資料,下面是一個例子:

def retrieve(self, request, *args, **kwargs):
    instance = self.get_object()
    serializer = ProfileSerializer(instance=instance)
    return Response(serializer.data)
複製程式碼

但是建立時,你需要另一種寫法:

def create(self, request, *args, **kwargs):
    serializer = ProfileSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save()
    return Response(serializer.data)
複製程式碼

最後,當你更新一個例項,你不但要提供instance,也要提供date:

def update(self, request, *args, **kwargs):
    instance = self.get_object()
    serializer = ProfileSerializer(
        instance=instance,
        data=request.data
    )
    serializer.is_valid(raise_exception=True)
    serializer.save()
    return Response(serializer.data)
複製程式碼

serializer.save()會基於初始化時的引數傳遞呼叫適當的內部方法。

3. 使用SerializerMethodField

SerializerMethodField是一個只讀的欄位,通過在其附加到的serializer classs上呼叫相應的方法,在請求處理時計算其值。

舉例來說,你有一個model,裡面有一個欄位datetime儲存的是models.DateTimeField型別,但是你想在序列化時,獲得timestamp型別的資料:

from rest_framework import serializers


class TagSerializer(serializers.ModelSerializer):
    created = serializers.SerializerMethodField()
    
    class Meta:
        model = Tag
        fields = ('label', 'created')
        
    def get_created(self, obj):
        return round(obj.created.timestamp())
複製程式碼

SerializerMethodField接收method_name,但是通常使用預設的命名方法會更為便捷,比如get_<field_name>。另外你要確保,不會為任何繁重的操作增加方法欄位的負擔。

4. 使用source引數

很多情況下,你的model裡面定義的欄位,與你想要序列化的欄位不一樣。你可以使用source引數,輕鬆解決這個問題。

舉個例子:

from rest_framework import serializers
class TaskSerializer(serializers.ModelSerializer):
    job_type = serializers.CharField(source='task_type')

    class Meta:
        model = Task
        fields = ('job_type',)
複製程式碼

模型中的task_type會被轉換成job_type。這個操作不光適用於讀,還適用於寫。

另外,你還可以藉助點語法去從關聯的模型中獲取欄位。

owner_email = serializers.CharField(source='owner.email')
複製程式碼

5. 序列化欄位的驗證

除了在初始化serializer欄位和serializer.validate()hook可以傳遞的validators引數之外,此外還有一種欄位級別的驗證,可以幫你為單獨的每個欄位定義它們自己的驗證方法。

我發現它有用的原因有兩個:首先,它可以對特別的欄位進行校驗,進行解耦。其次,它可以產生結構化的錯誤響應。

這種驗證方式的使用,和SerializerMethodField特別相似,只是這時候的函式名字形如def validate_<field_name>。舉個例子:

from rest_framework import serializers

class TransactionSerializer(serializers.ModelSerializer):
    bid = serializers.IntegerField()

    def validate_bid(self, bid: int) -> int:
        if bid > self.context['request'].user.available_balance:
            raise serializers.ValidationError(
                _('Bid is greater than your balance')
            )
        return bid
複製程式碼

如果驗證錯誤,會得到下面這樣的輸出:

{
   "bid": ["Bid is greater than your balance"]
}
複製程式碼

驗證方法必須要返回一個值,之後會傳給model例項。

另外要記住,欄位級別的驗證將會在serializer.validate()之前被serializer.to_internal_value()呼叫。

6. 把值直接傳給save方法

某些情況下,將值從序列化器外部直接傳遞到其save()方法很方便。

此方法將採用可以等同於序列化物件的引數。以這種方式傳遞的值將不會得到驗證。它可用於強制覆蓋初始資料。

serializer = EmailSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(owner_id=request.user.id)
複製程式碼

7. 使用CurrentUserDefault

如果需要設定使用者,比上面的例子更好的是,使用CurrentUserDefault,這時候不必去重寫view了。

from rest_framework import serializers

class EmailSerializer(serializers.ModelSerializer):
    owner = serializers.HiddenField(
        default=serializers.CurrentUserDefault()
    )
複製程式碼

這將會做兩件事。首先,將在請求物件中認證的使用者設定為預設使用者。其次,因為使用了HiddenField,因此不會考慮任何傳入的資料,所以不可能會設定成別的使用者。

8. serializers的初始資料

有時候你需要去獲取serializer的最原始的資料。這是因為資料已經通過執行serializer.is_valid()進行了修改,或者需要在validated_data尚不可用時比較驗證方法中另一個欄位的值。

資料能夠通過serializer.initial_data被獲取到,格式是dict,舉個例子:

from rest_framework import serializers


class SignupSerializer(serializers.ModelSerializer):
    password1 = serializers.CharField()
    password2 = serializers.CharField()

    def validate_password1(self, password1):
        if password1 != self.initial_data['password2']:
            raise serializers.ValidationError(
                'Passwords do not match'
            )
複製程式碼

9. 在巢狀序列化程式中處理多個建立/更新/刪除

大多數時候,序列化器是完全簡單的,並且有一定的經驗,沒有什麼可能出錯。但是,有一些限制。當您必須在一個高階序列化程式中支援巢狀序列化程式中的多個建立,更新和刪除操作時,事情可能會有些棘手。

這需要權衡:要選擇處理較多的請求數量,還是在一個請求裡處理較長的時間。

預設情況下,DRF根本不支援多個更新。很難想象它如何支援所有可能的巢狀插入和刪除型別。這就是DRF的建立者選擇靈活性而非現成的“萬能”解決方案的原因,並把特權留給了我們。

在這種情況下,可以遵循兩種路徑:

我建議至少選擇一次第二個選項,這樣您就會知道其中的含義。

在分析傳入資料之後,在大多數情況下,我們可以做出以下假設:

  • 所有應更新的例項都有ID,
  • 所有應建立的例項都沒有ID,
  • 所有應刪除的例項都都存在於資料儲存(例如資料庫)中,但不會出現在傳入的request.data中。

基於此,我們知道如何處理列表中的特定例項。以下是詳細顯示此過程的程式碼段:

class CUDNestedMixin(object):
    @staticmethod
    def cud_nested(queryset: QuerySet,
                   data: List[Dict],
                   serializer: Type[Serializer],
                   context: Dict):
        """
        Logic for handling multiple updates, creates and deletes
        on nested resources.
        :param queryset: queryset for objects existing in DB
        :param data: initial data to validate passed from higher
                level serializer to nested serializer
        :param serializer: nested serializer to use
        :param context: context passed from higher level
                serializer
        :return: N/A
        """
        updated_ids = list()
        for_create = list()
        for item in data:
            item_id = item.get('id')
            if item_id:
                instance = queryset.get(id=item_id)
                update_serializer = serializer(
                    instance=instance,
                    data=item,
                    context=context
                )
                update_serializer.is_valid(raise_exception=True)
                update_serializer.save()
                updated_ids.append(instance.id)
            else:
                for_create.append(item)

        delete_queryset = queryset.exclude(id__in=updated_ids)
        delete_queryset.delete()

        create_serializer = serializer(
            data=for_create,
            many=True,
            context=context
        )
        create_serializer.is_valid(raise_exception=True)
        create_serializer.save()
複製程式碼

這是高階序列化程式如何利用此mixin的簡化版本:

from rest_framework import serializers

class AccountSerializer(serializers.ModelSerializer,
                        CUDNestedMixin):
    phone_numbers = PhoneSerializer(
        many=True,
        source='phone_set',
    )

    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'phone_numbers')

    def update(self, instance, validated_data):
        self.cud_nested(
            queryset=instance.phone_set.all(),
            data=self.initial_data['phone_numbers'],
            serializer=PhoneSerializer,
            context=self.context
        )
        ...
        return instance
複製程式碼

請記住,巢狀物件應使用initial_data而不是validated_data

那是因為執行驗證會在序列化器的每個欄位上呼叫field.to_internal_value(),這可能會修改特定欄位儲存的資料(例如,通過將主鍵更改為模型例項)。

10. 覆蓋資料以強制排序

通過在view上的queryset新增排序,可以輕鬆地實現對列表檢視的排序,但是在還應該對巢狀資源進行排序的情況下,並不是那麼簡單。

對於只讀欄位,可以在SerializerMethodField中完成,但是在必須寫欄位的情況下該怎麼辦?

在這種情況下,可以覆蓋序列化程式的data屬性,如以下示例所示:

@property
def data(self):
    data = super().data
    data['phone_numbers'].sort(key=lambda p: p['id'])
    return data
複製程式碼

結論:

希望您在本文中找到了一些有趣的新技術。有新的drf使用技巧或想法,歡迎分享!

相關文章