掌握Django(七)

本文讲解DJango的RESTful API的创建。包括API相关的创建序列化、创建自定义序列化字段、序列化关系, 以及介绍模型序列化器的使用, 说明反序列化对象、数据验证、保存对象、删除对象等API的建创方法.


设置环境

为了与本文内容保持一致, 建议关注的朋友下载附件, 并重新设置环境.

附件地址:

https://box.zjenergy.com.cn/l/15Drht

用DataGrip新建数据库storefront2:

create database storefront2

解压附件, 看到有一个storefront2文件夹,  用 vscode 打开该文件夹.

修改 settings.py的数据库连接为刚建的 storefront2, 按实际配置用户名和密码.

再执行:

pipenv install

pipenv shell

python manage.py migrate

确认虚拟环境相关依赖包已建好, 数据表已迁移.

再用DataGrip在数据库 storefront2中运行附件解压后得到的seed.sql, 添加测试数据.

再创建超级用户用于管理:

python manage.py createsuperuser


创建RESTful APIs

标准的方法有几种:

  • get, 用于获取数据
  • post, 用于添加记录
  • put, 用于更新,所有字段属性
  • patch, 用于更新,特定的字段属性
  • delete, 用于删除记录

安装Django REST Framework

这是最流行的框架, 安装:

pipenv install djangorestframework

再添加settings.py下的INSTALLED_APPS:

rest_framework,


创建API views

先用原生的Django实现一个简单的页面展示, 在store—views.py键入:

from django.http import HttpResponse

def product_list(request):

    return HttpResponse(‘OK’)

再添加 store–urls.py:

urlpatterns = [

    path(‘products/’, views.product_list)

]

再在storefront—urls.py里添加一行:

urlpatterns = [

        …

        path(‘store/’, include(‘store.urls’)),

]

访问http://127.0.0.1/store/products, 能看到OK的页面.

现在修改用 REST Framework, 修改 store–views.py:

from rest_framework.decorators import api_view

from rest_framework.response import Response

@api_view

def product_list(request):

    return Response(‘OK’)


再访问http://127.0.0.1/store/products, 页面变漂亮了, 能清楚显示相关访问端点! 也可以通过点GET右侧的小箭头切换成其他格式,json.

我们看到:

Django原生的请求响应类: 

HttpRequest

HttpResponse

而REST Framework的请求响应类是:

Request

Response

REST框架更简单更强大!

我们再增加一个view, store–views.py里添加:

@api_view()

def product_detail(request, id):

    return Response(id)

同时在在urls里映射一下, store–urls.py添加:

path(‘products/<int:id>’, views.product_detail),

访问  http://127.0.0.1:8000/store/products/1 , 输出 1

urls映射里的 <int:id> 用于限定参数 id 为 数字, 用户提供的参数如果不是数字,如这样的: “http://127.0.0.1:8000/store/products/a“ 将提示找不到页面.


创建序列化

前面的product_detail是示例, 我们当然希望返回product的详细信息. 这就用到了序列化.

序列化Serializer的工作是:

将一个Python对象转变成字典dictionary. 

而:

REST Framework框架有一个类JSONRenderer, 其中的render方法将字典变成JSON对象返回给用户.

回到代码, 在store应用新建一个 serializers.py 文件:

from rest_framework import serializers

class ProductSerializer(serializers.Serializer):

    id = serializers.IntegerField()

    title = serializers.CharField(max_length=255)

    unit_price = serializers.DecimalField(max_digits=6, decimal_places=2)

我们看到, Serializer类的属性字段与模型字段很像,  这里设置的字段与模型字段名称不一定要相同, 但一般保持相同为好. 我们在序列化类中未必要设置全部的模型字段, 这里只设置id、title、unit_price 字段.

了解更多的序列化字段, 可以访问:

https://www.django-rest-framework.org/

再点击API Guide–Serializer fields, 查看详细说明. 比如有Boolean fields、String feilds、Numeric fields等, 这些字段有一些共同的属性, 如 read_only、write_only、required等.

再来修改store–views.py的 product_detail:

from .models import Product

from .serializers import ProductSerializer

@api_view()

def product_detail(request, id):

    product = Product.objects.get(pk=id)

    serilizer = ProductSerializer(product)

    return Response(serilizer.data)

保存, 我们访问http://127.0.0.1:8000/store/products/1, 得到的响应是:

{
"id": 1,
"title": "Bread Ww Cluster",
"unit_price": "4.00"
}

我们看到 unit_price 的值为 字符串, 不是数字. 默认情况下, REST Fromework 将所有的字段都修改成字符串. 我们修改下settings.py, 在最后加上下面配置:

REST_FRAMEWORK = {

    ‘COERCE_DECIMAL_TO_STRING’: False,

}

再刷新网页, “unit_price”: 4.0 ,变成数字了.

前面我们讲到, rest_framework会通过JSONRenderer的render方法将序列化的字典变成json对象响应给客户端, 但我们在代码中未实现相关代码, 这是为什么呢? 实际上, 这一切都由rest_framework在后台自动帮我们转换了, 不用手动显示转换, 很方便和神奇!

我们再访问 http://127.0.0.1:8000/store/products/0 , 系统就崩溃了, 因为没有id 为 0 的product, 我们要捕捉这个错误. 我们将代码用 try包起来, 并c捕捉不存在的错误:

@api_view()

def product_detail(request, id):

    try:

        product = Product.objects.get(pk=id)

        serilizer = ProductSerializer(product)

        return Response(serilizer.data)

    except Product.DoesNotExist:

        return Response(status=404)

到遇到不存在的product时, 就返回404代码.  404代码对于程序员来说自然很熟悉, 但一般建议还是不要在代码中出现魔术数字, 我们来修改下增加可读性.

from rest_framework import status

    return Response(status=status.HTTP_404_NOT_FOUND)

每次捕捉错误明显很繁琐, rest_framework也考虑到了, 提供了 get_object_or_404的快捷方法, 代替 try 以及 返回404代码等. 我们将 product_detail简化成以下:

from django.shortcuts import get_object_or_404

@api_view()

def product_detail(request, id):

    product = get_object_or_404(Product, pk=id)

    serilizer = ProductSerializer(product)

    return Response(serilizer.data)

再将product_list视图也修改下:

@api_view()

def product_list(request):

    queryset = Product.objects.all()

    serializer = ProductSerializer(queryset, many=True)

    return Response(serializer.data)

Serializer类不仅可以序列化单个对象, 也可以序列化集合, 它将迭代集合中的每个对象进行序列化, 注意要提供参数 many=True.

访问 http://127.0.0.1:8000/store/products 就可以看到全部 product了


创建自定义序列化字段

序列化后的API展示的字段与后端的模型实现 不应该等同. 模型实现属于细节, 在后期的版本迭代中完全可能会修改, 比如增加、修改、删除等, 而前端的API一般需要保持稳定不能随意修改, 否则调用它的用户就会崩溃.  这跟遥控器可以类比, API好比按钮, 而模型好比里面的电路元器件实现, 在产品升级时, 内部的元器件设备可以使用更好更便宜的, 但外部的按钮一般需要保持不变.

我们需要序列化有自定义字段的能力, 在内部模型字段改变时, 保持对外的API一致. 当然 API也不是不能修改, 但要评估修改对外部和用户可能造成的影响.

回到代码, 我们在store–serializers.py添加一个自定义字段 price_with_tax.

from decimal import Decimal

from .models import Product


class ProductSerializer(serializers.Serializer):

    …

    price_with_tax = serializers.SerializerMethodField(

        method_name=’calculate_tax’)

    …

    def calculate_tax(self, product: Product):

        return product.unit_price*Decimal(1.1)

SerializerMethodField 表示自定义方法字段,方法为calculate_tax, 该方法的第2个参数product加了冒号Product, 是为了在能在IDE里自动提示. 引入的Decimal类用于类型转换, 否则1.1属于浮点类型与unit_price相乘会出错.

再来修改序列化字段的命名, 我们想对外展示product的价格时用 price 名字, 而不是 unit_price, 可以引入参数 source:

class ProductSerializer(serializers.Serializer):

    …

    price = serializers.DecimalField(

        max_digits=6, decimal_places=2, source=’unit_price’)

    …

如果不添加source参数, 将提示出错, 默认情况下序列化类ProductSerializer会自动去匹配模型类Product的同名字段, 找不到就抛出错误.


序列化关系

有时候需要序列化关联对象, 有几种方法, 我们一一来说明.

  • 第1种, 显示关系对象主键

from .models import Product, Collection

class ProductSerializer(serializers.Serializer):

    …

    collection = serializers.PrimaryKeyRelatedField(

        queryset=Collection.objects.all())

添加了一个关系字段 collection, 需要提供queryset信息, 这里是collection的查询集. 自然也需要导入Collection类. 显示的结果是:

[
{
"id": 648,
"title": "7up Diet, 355 Ml",
"price": 79.07,
"price_with_tax": 86.977,
"collection": 5
},    ...
]
  • 第2种, 返回关系对象的文本信息

只需前面的序列化字段collection改成StringRelatedField即可

collection = serializers.StringRelatedField()

StringRelatedField获取模型Collection的魔术方法 __str__() 返回的值, 我们之前已添加了. 刷新网页发现很慢, 原因是Django的延迟加载导致, 每显示一条product都需要向数据库发放一条SQL查询获取Collection信息, 所以我们要预加载. 

在store–views.py中修改 Product的查询集, 添加select_related:

@api_view()

def product_list(request): 

    queryset = Product.objects.select_related(‘collection’).all()

    …

显示结果是:

[
{
"id": 648,
"title": "7up Diet, 355 Ml",
"price": 79.07,
"price_with_tax": 86.977,
"collection": "Stationary"
},]
  • 第3种, 返回㠌套的关系对象

添加CollectionSerializer类, 然后在ProductSerializer类中添加该类型字段.

class CollectionSerializer(serializers.Serializer):

    id = serializers.IntegerField()

    title = serializers.CharField(max_length=255)

    …

class ProductSerializer(serializers.Serializer):

    …

    collection = CollectionSerializer()

显示的结果是:

[
{
"id": 648,
"title": "7up Diet, 355 Ml",
"price": 79.07,
"price_with_tax": 86.977,
"collection": {
"id": 5,
"title": "Stationary"
}
},]
  • 第4种, 返回超链接关系对象

超链接字段用 HyperlinkedRelatedField. 该类需要两个参数, 一个是关系查询集queryset, 另一个是view_name用于指定具体的视图. 

class ProductSerializer(serializers.Serializer):

    …

    collection = serializers.HyperlinkedRelatedField(

        queryset=Collection.objects.all(),

        view_name=’collection-detail’

    )

目前我们我们还没有collection-detail视图,我们添加下, 在 store–views.py里添加视图实现 collection_detail.

@api_view()

def collection_detail(request, pk):

    return Response(‘OK’)

Response返回只做一个简单的示例返回OK. 我们还要做 url映射, store–urls.py添加以下:

urlpatterns = [

path(‘collections/<int:pk>’, views.collection_detail, name=’collection-detail’),

]

注意这里url映射的 name 就是前面HyperlinkedRelatedField参数的view-name指定的名字. 视图及其映射的参数用了 pk ,不能用其他如id, 因为这是Django的约定, 否则可能出错了.

另外, 由于要在ProductSerializer中序列化成超链接, 还要传递上下文context过去, 修改 store–views.py, 构造ProductSerializer时传递当前的request.

@api_view()

def product_list(request):

    …

    serializer = ProductSerializer(queryset, many=True, context={

        ‘request’: request

        })

    …

显示的结果是:

[
{
"id": 648,
"title": "7up Diet, 355 Ml",
"price": 79.07,
"price_with_tax": 86.977,
"collection": "http://127.0.0.1:8000/store/collections/5"
},]

点击collection后的链接, 可以链接到具体的collection对象.


模型序列化器

用Serializer, 我们需要分别定义序列化字段, 而这与模型的字段又是一样的, 这是一个重复的工作, 每序列化一个模型, 都要复复定义一下, 碰到模型修改也是如此. 我们可以简化这项工作, 这就用到模型序列化器 ModelSerializer.

class ProductSerializer(serializers.ModelSerializer):

    class Meta:

        model = Product

        fields = [‘id’, ‘title’, ‘unit_price’, ‘collection’]

内联类提供 model 和 fields 就好了. 这里提供了关系字段collection, 实际显示是collection的主键即 id .

我们如果想修改字段名字, 比如 unit_price 改成 price; 再或增加一个附加字段, 如之前的price_with_tax; 或将collection改成HyperlinkedRelatedField. 这个跟之前都是一样的.

class ProductSerializer(serializers.ModelSerializer):

    class Meta:

        model = Product

        fields = [‘id’, ‘title’, ‘price’, ‘price_with_tax’, ‘collection’]

    price = serializers.DecimalField(

        max_digits=6, decimal_places=2, source=’unit_price’)

    price_with_tax = serializers.SerializerMethodField(

        method_name=’calculate_tax’)   

    collection = serializers.HyperlinkedRelatedField(

        queryset=Collection.objects.all(),

        view_name=’collection-detail’

        )

    def calculate_tax(self, product: Product):

        return product.unit_price*Decimal(1.1)

fields 还可以一个魔术方法: fields = ‘__all__’, 表示添加全部字段, 但这是不建议的, 后面模型的改变将直接影响序列化对外展现的WEB API . 一般禁止这么做.

我们将CollectionSerializer也修改成ModelSerializer.

class CollectionSerializer(serializers.ModelSerializer):

    class Meta:

        model = Collection

        fields = [‘id’, ‘title’]


反序列化对象

将用户传过来的JSON变成对象, 如POST, PUT等. 这就是反序列化.

@api_view([‘GET’, ‘POST’])

def product_list(request):

    if request.method == ‘GET’:

        …

    elif request.method == ‘POST’:

        serializer = ProductSerializer(data=request.data)

        # serializer.validated_data

        return Response(‘OK’)

我们在 @api_view加上方法 @api_view([‘GET’, ‘POST’]), 再判断方法, 如果是POST的话, 就反序列化, ProductSerializer 如果提供Product或查询集queryset就是序列化, 如果提供参数 data, 则是反序列化. 反序列化后一般需要进行数据验证(下一节讲), 然后进行相关处理, 比如写入数据库等.

我们刷新网页测试 http://127.0.0.1:8000/store/products , 拉到最后, 在输入框 键入 空的json ,  {} , 然后点 POST , 就返回OK.


数据验证

我们将上一节注释掉的serializer.validated_data带回来, 再刷新网页POST空对象, 就会提示需要首先验证数据, 我们修改下.

@api_view([‘GET’, ‘POST’])

def product_list(request):

    …

    elif request.method == ‘POST’:

        serializer = ProductSerializer(data=request.data)

        if serializer.is_valid():

            serializer.validated_data

            return Response(‘OK’)

        else:

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

is_valid()的判断if…else 明显有点丑, Django也提供了raise_exception字健字参数的简化方法.

        …

        elif request.method == ‘POST’:

            serializer = ProductSerializer(data=request.data)

            serializer.is_valid(raise_exception=True)

            print(serializer.validated_data)

            return Response(‘OK’)

单个的字段验证一般没什么问题, 如果多个字段相互验证的话, 就需要自定义验证方法了,  比如有个用户注册系统, 我们需要验证两次密码输入是否一致. 我们就可以覆盖validate方法.

class UserSerializer(serializers.ModelSerializer):

    …

    def validate(self, data):

        if data[‘password’] != data[‘password_confirm’]:

            return serializers.ValidationError(‘Passwords do not match.’)

        return data

我们也看到, validate方法的data参数是一个字典.


保存对象

前面的ProductSerializer是继承于ModelSerializer, 该类有一个save 方法, 用于保存和更新对象.

先添加ProductSerializer的字段, 因为inventory, slug 在模型中都不能为空.

class ProductSerializer(serializers.ModelSerializer):

    class Meta:

        model = Product

        fields = [‘id’, ‘title’, ‘description’, ‘slug’, ‘inventory’,

            ‘unit_price’, ‘collection’]

再在store–views.py的product_list中添加 save()代码用于保存.

serializer.is_valid(raise_exception=True)

serializer.save()

return Response(serializer.data, status=status.HTTP_201_CREATED)

然后再刷新网页, 并post一个json, 如:

{

“title”:”a”,

“slug”:”a”,

“inventory”:1,

“collection”:1,

“unit_price”:1

}

 能看到输出OK表示保存成功.

ModelSerializer的save()方法实际会调用 create或update方法. 在我们需要增加额外字段, 或处理与其他对象的关联等情形时, 就需要重写这两个方法实现.

def create(self, validated_data):

    product = Product(**validated_data)

    product.other = 1

    product.save()

    return product

def update(self, instance, validated_data):

    instance.unit_price = validated_data.get(‘unit_price’)

    instance.save()

    return instance

当然在这里我们不需要手动定义, 在save时 Django REST Framework 会自动根据不同情况选择create 或 update, 我们可以暂时删除这些代码. 

再修改product_detail , 使其可以更新

@api_view([‘GET’, ‘PUT’])

def product_detail(request, id):

    product = get_object_or_404(Product, pk=id)

    if request.method == ‘GET’:

        serializer = ProductSerializer(product)

        return Response(serializer.data)

    elif request.method == ‘PUT’:

        serializer = ProductSerializer(product, data=request.data)

        serializer.is_valid(raise_exception=True)

        serializer.save()

        return Response(serializer.data, status=status.HTTP_200_OK)


删除对象

删除对象也在product_detail里处理.

@api_view([‘GET’, ‘PUT’, ‘DELETE’])

def product_detail(request, id):

    product = get_object_or_404(Product, pk=id)

    …

    elif request.method == ‘DELETE’:

        if product.orderitems.count() > 0:

            return Response({‘error’: ‘Product cannot be delete because it is associated with an order item.’}, status=status.HTTP_405_METHOD_NOT_ALLOWED)

        product.delete()

        return Response(status=status.HTTP_204_NO_CONTENT)

我们在装饰符api_view里添加DELETE, 然后在方法体里添加DELETE的代码, 我们先判断是否已有订单, 然后进行删除. 注意product.orderitems, 默认应该是product.orderitem_set, 为了好看一点, 我们在OrderItem里的外键 product里添加 related_name为orderitems.

这里用到很多HTTP的状态, 可以访问 httpstatuses.com 查看相关介绍.


练习:建立Collections API

给collection添加API, 可以获取所有collection列表, 列表行包括collection的product数; 可以添加, 可以修改、删除collection.

实现与product类似, 具体代码就不列了. 


小结

本文讲解了DJango的RESTful API的创建。包括API相关的创建序列化、创建自定义序列化字段、序列化关系, 以及介绍模型序列化器的使用, 说明反序列化对象、数据验证、保存对象、删除对象等API的建创方法.

下一篇计划讲解高级API.

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注