Day3 你必需要知道的drf最佳实践的十件事

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

1. ViewSets

viewsets的好处是使得你的代码保持一致,而且免于重复。若是你编写的views不止去作一件事,那么viewsets就是你想要的东西。数据库

举例来讲,若是你有一个model叫作Tag,你须要列表、建立和详情的功能,你能够定义一个viewset:django

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。json

ModelViewset能够为你提供如下方法:.list(),.retrieve(), .create(), .update(), .partial_update(), .destroy()api

此外,当你使用viewsets时,也会令你的路由配置更加的清晰。bash

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能够帮你实现如下功能:app

  • 获取Tag列表,发送GET请求给 v1/tag/
  • 建立Tag,发送POST请求给 v1/tag/
  • 获取特定Tag,发送GET请求给v1/tag/<tag_id>

你甚至能够在viewset里面经过@action装饰器添加一些自定义的路由。ide

2. 理解不一样类型的serializers

做为一个DRF的使用者,你没必要太去关心views或者路由配置,因此你可能会把绝大部分精力放在serializers上来。函数

serializers是充当Django的model及其表现形式(例如json)之间的翻译器。每个serializer可以既被用做读也可用做写,初始化的方式决定了它将执行的动做。咱们能够区分出三种不一样类型的serializer: create, update, retrieveui

若是你想要在序列化器外部传输数据,下面是一个例子:

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使用技巧或想法,欢迎分享!

相关文章
相关标签/搜索