本文讲解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.