65、RESTful与django

Django框架 / 2021-03-25

参考文章:

阮一峰《RESTful API 设计指南》,原文链接:http://www.ruanyifeng.com/blog/2014/05/restful_api.html。

刘清政《RESTful规范》,原文链接:https://www.cnblogs.com/liuqingzheng/articles/9762022.html。

一、RESTful API

1.1、前言

据闻REST,最初只是英文单词rest即休憩的意思,后作者翻字典根据四个字母拼凑出所谓的Representational State Transfer,表征性状态转移,中文翻译我都不知道什么意思。。。Representational作者自己在查字典之前根本不认识这个词,ful也可能只是形容词后缀而已。

让我想起了在网上看到的关于交通大学校名的一则趣谈:有人将交通大学校名牵强附会称其取决于《易经》“天地交而万物通”,分明是国家交通部出钱办的。。。好像周星驰的大话西游系列电影,当年被骂成垃圾电影;多年后爆火,世人为其套上光环:后现代主义,怕不是连周星驰本人都懵圈了!

以上仅为笑谈,真假与否并不重要,重要的是这篇论文的重要性是实实在在影响到了全世界的Web开发领域!

1.2、REST简述

REST奉行资源至上原则,将一切数据视之为资源,资源即实体!它规定了一套web开发规范,才有了那句“如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构!”

1.3、API简述

此前我的Django学习过程一直是前后端不分离,比如模仿博客园写的网站,后端使用Django开发,前端使用Django模板语法DTL(Django Template Language)将后端与HTML、CSS、JavaScript等串联起来。

但据我了解与此前工作经历,前后端不分离的项目比较少它可称之为全栈开发,大多数应用于企业内部对前端页面要求不高的情况;而大多数项目都是前后端分离的,即后端专注于写后端接口,通过接口给前端返回JSON或XML数据,而前端拿到数据后尽情渲染丰富多彩的页面展示给用户!

因此API可以说是前后端数据交互的接口!API有一个特点就是返回的数据是跨语言的、通用的格式,比如:JSON、XML。

1.4、动静态简述

时常会听到动态页面与静态页面的说法,这里简单阐述一下!

1.4.1、动静态区别

简单来说,动态页面即数据经过后端渲染后再传给前端渲染并展示,根据输入的不同展示给用户的内容可能是不同的!比如Django开发中用render函数将数据处理好后通过locals传递给前端,在前端HTML中使用模板语法将其渲染展示!

静态就更简单了,就是直接写死了的东西,比如一个HTML页面里面就一行哈哈哈哈,这就是一个静态页面,因为它里面的内容是固定不变的,任凭雨打风吹,它始终是它!

1.4.2、高并发

很容易看出来,动态页面因为需要通过后端处理完数据后再传递给前端渲染数据,速度远比直接返回一个静态页面的固定内容要慢得多。

在访问量庞大、频率高的场景下,实时处理动态页面是制约网站承受力的主要因素之一!针对这种情况可以将动态页面静态化,什么意思呢?就是提前将页面渲染好,缓存下来供用户访问,比如电商平台首页等等!

1.5、RESTful规范

因为RESTful是一种Web API接口开发规范,因此本文通篇都是应该云云!

1.5.1、简述

RESTful规范了Web API接口的设计风格,尤其适合前后端分离的项目应用中!该理念人为后端就是提供数据的,对外则是暴露数据资源的访问接口,那么用户在访问时的键入的URL路径即要操作的数据资源!

它是一种规范,而不是某种编程语言的独有特性,因此我们可以使用任意一门语言任意一种Web框架来开发出符合该规范的API接口!

1.5.2、规范
1.协议规范

API接口与用户之间的通信协议,应该使用HTTPS协议来增强数据的安全性。

2.路径规范

应该将API接口部署在子域名之下,比如GitHub,这样可以见名知意!

https://api.domain.com

如API较为简单,且基本不会有扩展,可以考虑将其放在域名后面作为路径。

https://domain.com/api/
3.版本规范

应该保证数据的多版本共存,并将API版本号放入URL以作区分;比如:

https://api.domain.com/v3/
4.路径规范

RESTful中一切皆资源,因此URL中不应该存在动词,而是应该用名词,且该名词是同种数据的集合体,所以它们应该使用复数的名词!

比如此前写的博客网站有文章、文章标签、文章分类等,我们应该使用如articles这种复数来代表所有文章,如下所示:

https://api.domain.com/v3/articles
https://api.domain.com/v3/tags
https://api.domain.com/v3/classes
5.method规范

客户端对于资源的请求的操作类型应该是以下几种:

常用方法:

  • GET:从服务器获取一个或多项资源。
  • POST:在服务器新建一项资源。
  • PUT:在服务器更新资源,用户需提供完整的改变后的数据资源。
    • 比如文章名是docker基础,分类docker;我想改文章名为docker入门,分类不动,提交时改后的文章名和分类全部要提交。
  • PATCH:同样是在服务器更新资源,但用户仅需提供需要改变的数据属性即可。
    • 还是上面的例子,我只改文章名,那么我只需要提交新文章名即可,分类等其它的没改就不传。
  • DELETE:从服务器删除数据资源。

不常用方法:

  • HEAD:获取资源的元数据,即meta。
  • OPTIONS:获取资源的信息,即资源的哪些属性是可修改的。
6.过滤信息规范

一般服务器数据都非常多,服务器不会都将它们返回给用户,而是应该提供参数,用参数作为条件将过滤后的结果返回。比如:

  • ?limit=6,指定返回的资源数量。
  • ?offset=10,指定返回的资源的开始位置。
  • ?page=2&per_page=100,指定第几页,及每页显示的数量。
  • ?sortby=name&order=asc,指定返回结果按照某种属性排序及其排序的顺序是正序还是反序。
  • ?animal_type_id=1,指定筛选条件。
7.服务器响应

比如以前我们是用get_book这种动词,现在用book,有多个就是books

5、资源操作由请求方式决定,重点

6、过滤,通过在url上传参的形式传递搜索条件,就是筛选条件

8.状态码规范

服务器在向用户返回结果时,应该带有提示信息与相应状态码,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。

200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
9.异常处理

如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

{
    "error": "Invalid API key"
}
10.Hypermedia

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

比如键入https://api.github.com,会返回所有可用API的目录及其对应链接,我们按照它的链接去访问对应资源即可。

二、drf实践

drf,即Django REST Framework,是Django对RESTful规范的一种具体实现,下面来开始学习它的简单使用!

2.1、drf的安装

pip3 install djangorestframework

2.2、序列化器

我们说过RESTful规范我们应该向前端返回JSON或XML数据,以往我们需要自己写templates和序列化数据,而现在只需要使用rest_framework提供的序列化器serializers即可!

我这次使用的是django 2.2.16,不再是之前的1.11.29了!且经过rest_framework后,原生django中的request等已经全部被rest_framework处理过。

2.2.1、查询单条数据
models定义
from django.db import models

# Create your models here.


class Article(models.Model):
    title = models.CharField(max_length=16, null=True)
    create_time = models.DateTimeField(auto_now_add=True)
    site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)  # django 2.X版本要手动指定级联删除

    def __str__(self):
        return self.title


class Site(models.Model):
    title = models.CharField(max_length=16, null=True)

    def __str__(self):
        return self.title
创建序列化器

在APP下创建任意名称的py文件,书写以下代码;想序列化models哪个字段就往这序列化器里一一对应即可,序列化器必须要继承Serializer!

注意:视图类必须继承View,而在rest_framework中APIView继承了View,因此我们不需要手动继承!

from rest_framework import serializers


class ArticleSeriallizer(serializers.Serializer):
    title = serializers.CharField()  # 和models字段对应
    create_time = serializers.DateTimeField()
    site = serializers.CharField()


class SiteSerializer(serializers.Serializer):
    title = serializers.CharField()
注册序列化器

在settings的APP中注册rest_framework

INSTALLED_APPS = [
    'rest_framework'
]
urls配置
urlpatterns = [
    path('admin/', admin.site.urls),  # 1.X用url,2.X用path,path不支持正则
    re_path('article/(?P<pk>\d+)', views.ArticleView.as_view())  # re_path支持正则
]
views自定义类

序列化的数据全部存放在.data字典中。

from django.shortcuts import render
from study.serializer import ArticleSeriallizer
from rest_framework.views import APIView
from rest_framework.response import Response
from study import models

# Create your views here.


class ArticleView(APIView):  # 必须继承APIView
    def get(self, request, pk):
        article = models.Article.objects.filter(pk=pk).first()  # 先获取数据
        article_obj = ArticleSeriallizer(instance=article)  # 将数据序列化得到一个Seriallizer对象

        return Response(article_obj.data)  # 返回对象中序列化好的数据
注意事项

本人用django 2.2.16实验时,在迁移数据库时报错如下:

packages/django/db/backends/mysql/operations.py", line 146, in last_executed_query

    query = query.decode(errors='replace')

AttributeError: 'str' object has no attribute 'decode'

需要去报错里显示的operations.py里面,将query = query.decode(errors='replace')里面的decode改为encode

query = query.encode(errors='replace')
测试结果

浏览器输入:http://127.0.0.1:8000/article/1/,得到结果如下所示:

image-20210324143730802

2.2.2、查询所有数据

序列化多条数据时,需要在视图类里指定many参数为True

urls代码
urlpatterns = [
    path('articles/', views.ArticlesView.as_view())
]

views代码
class ArticlesView(APIView):
    def get(self, request):
        article = models.Article.objects.all()
        article_obj = ArticleSeriallizer(instance=article, many=True)  # 序列化多条数据时必须指定many为True

        return Response(article_obj.data)

当朝:127.0.0.1:8000/articles/ 发起GET请求后,得到结果:

[
    {
        "title": "测试文章",
        "create_time": "2013-03-03T03:03:03",
        "site": "小破站"
    },
    {
        "title": "瞎写一通",
        "create_time": "2014-04-04T00:00:00",
        "site": "小破站"
    }
]
2.2.3、通用参数
read_only

表明该字段仅用于序列化输出,true即get可以看到,修改时不需要也看不到

当发起PUT请求时,

{
    "title": "测试文章",
    "create_time": "2013-03-03 03:03:03",
    "site": "1"
}

结果为

{
    "status": 201,
    "msg": "sucess",
    "data": {
        "title": null,
        "create_time": "2013-03-03T03:03:03"
    }
}
write_only

表明该字段仅用于序列化输入,GETq请求时是看不到的,PUT请求提交时必须带有该字段。

class ArticleSeriallizer(serializers.Serializer):
    site = serializers.CharField(write_only=True)

当朝着:127.0.0.1:8000/article/1/发起GET请求时,得到以下结果:

{
    "title": "喝茶",
    "create_time": "2013-03-03T03:03:03"  // 说明字段用了write_only后在GET请求时看不到该字段的
}

2.3、反序列化

2.3.1、数据修改与校验

rest_framework也支持forms组件功能,使用方法也基本一致。

序列化器接口定义子类的行为,没有这个行为就报错,所以要重写update方法

views代码

在上面的例子的基础上,添加一个put方法。

    def put(self, request, pk):
        callback_dict = {'status': 0, 'msg': 'sucess'}

        article = models.Article.objects.filter(pk=pk).first()  # 获取修改的那个对象
        article_obj = ArticleSeriallizer(instance=article, data=request.data)  # 把原来的数据改成请求里的数据
        if article_obj.is_valid():  # 先判断请求修改的数据是否合法
            article_obj.save()  # 合法则保存数据,直接这样修改会报错,必须要在序列化器里重写update方法!
            callback_dict['data'] = article_obj.data  # 返回数据
        else:
            callback_dict['status'] = 1
            callback_dict['msg'] = 'invaild data'
            callback_dict['data'] = article_obj.errors
        return Response(callback_dict)
序列化器代码
from study.models import Site
from rest_framework import serializers


class ArticleSeriallizer(serializers.Serializer):
    title = serializers.CharField(label='文章标题', min_length=2, max_length=16)  # 跟forms类似的校验规则
    create_time = serializers.DateTimeField()
    site = serializers.CharField()

    def update(self, instance, validated_data):  # 必须在序列化器中重写update方法前端才能修改数据
        instance.title = validated_data.get('title')  # 一一获取对应的数据
        instance.create_time = validated_data.get('create_time')
        instance.site = Site.objects.filter(pk=validated_data.get('site')).first()  # 外键字段必须传入对象
        instance.save()  # 写入数据

        return instance  # 返回数据
        


class SiteSerializer(serializers.Serializer):
    title = serializers.CharField()
2.3.2、钩子函数

rest_framework提供的校验规则较少,只有以下几种:

max_length最大长度
min_lenght最小长度
allow_blank是否允许为空
trim_whitespace是否截断空白字符
max_value最小值
min_value最大值

如果字段的校验规则不够用,可以自定义钩子函数!

局部钩子
    def validate_title(self, attrs):
        taboo_list = ['开车', '修车', '喝茶']
        for key in taboo_list:
            if not attrs.find(key):
                return attrs
            else:
                raise ValidationError('查水表警告!')

我在postman发起PUT请求

{
    "title": "喝茶",
    "create_time": "2013-03-03 03:03:03",
    "site": "1"
}

返回结果

{
    "status": 1,
    "msg": "invaild data",
    "data": {
        "title": [
            "查水表警告!"
        ]
    }
}
全局钩子

在序列化器中需要同时对多个字段进行比较验证时,可以定义validate方法来验证;比较少用,容易乱套。

    def validate(self, attrs):
        article = attrs.get('title')
        tag = attrs.get('tag')
        if article == tag:
            raise ValidationError('文章标题与标签不能一致')
        else:
            return attrs
第三种校验方式

可以使用字段的validators=[],列表里面写函数名,函数里配置校验。

def title_form(field):
    if not field.find('python'):
        raise serializers.ValidationError('不是Python差评!')


class ArticleSeriallizer(serializers.Serializer):
    title = serializers.CharField(label='文章标题', min_length=2, max_length=16, validators=[title_form,])
    create_time = serializers.DateTimeField()
    site = serializers.CharField()
2.3.3、新增数据

新增跟修改一样,必须要重写方法,新增是重写create保存数据,要return

views代码
    def post(self, request):
        callback_dict = {'status': 201, 'msg': 'sucess'}

        article = ArticleSeriallizer(data=request.data)
        if article.is_valid():
            article.save()
            callback_dict['data'] = article.data
        else:
            callback_dict['status'] = 400
            callback_dict['msg'] = 'invaild data'
            callback_dict['data'] = article.errors
        return Response(callback_dict)
Seriallizer代码
    def create(self, validated_data):
        instance = Article.objects.create(**validated_data)
        return instance
postman测试POST请求

127.0.0.1:8000/article/

{
    "status": 201,
    "msg": "sucess",
    "data": {
        "title": "docker",
        "create_time": "2021-03-25T13:47:34.553944",
        "author": "三溪"
    }
}
2.2.4删除

直接删除即可

    def delete(self, request, pk):
        delete_obj = models.Article.objects.filter(pk=pk).delete()
        return Response({'status': 201, 'msg': 'deleted sucess'})

2.4、自定义Response对象

在前面我们自定义字典,每个函数每次判断都要手写代码,造成代码冗余,因此可以考虑将其封装成对象来调用。

按照之前所说,此文件应该放在utils里面,然后views导入该文件直接return即可

from rest_framework import status
from rest_framework.response import Response


class CallBackDict(Response):
    def __init__(self, status_code=200, msg='sucess', data=None, status=None, headers=None, **kwargs):
        dict = {'status_code': status_code, 'msg': msg}
        dict.update(kwargs)
        if data:
            dict = {'status_code': status_code, 'msg': msg, 'data': data}

        super().__init__(data=dict, status=status, headers=headers)

2.5、模型序列化器

上面的演示中,我们需要手写序列化器代码,其实rest_framework给我们提供了模型序列化器可以自动序列化表,且无需再重写update和create方法。

2.X版本后write_only_fields不能用了,得使用extra_kwargs!

class ArticleModelSeriallizer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = '__all__'
        # fields = ('title', 'tag', 'create_time', 'author')  # 仅序列化指定字段
        # exclude = ('name')  # 与上相反,写谁就排除谁,且不能与fields同时存在。
        # read_only_fields = ('title')
        # extra_kwargs = {
        #     'price': {'write_onlu': True}
        # }

2.6、serializer参数补充

source

source有点像起别名,它可以是属性也可以是方法;如果是字段则显示字段;如果是方法则执行且不用加括号;它还可以用点实现跨表,一起来看看。

models代码
class Article(models.Model):
    title = models.CharField(max_length=16, null=True)
    tag = models.CharField(max_length=8, null=True)
    create_time = models.DateTimeField(auto_now_add=True)
    site = models.ForeignKey(to='Site', on_delete=models.CASCADE())
    
    def __str__(self):
        return self.title


class Site(models.Model):
    title = models.CharField(max_length=16)

    def __str__(self):
        return self.title
序列化器代码
class ArticleSeriallizer(serializers.Serializer):
    title = serializers.CharField(max_length=16)
    create_time = serializers.DateTimeField()
    site = serializers.CharField(source='Site.title', default='三溪')

postman测试GET请求:127.0.0.1:8000/article/1/,结果如下:

{
    "title": "测试文章",
    "create_time": "2013-03-03T03:03:03",
    "site": "三溪"
}
SerializerMethodField

因为不想让显示出来的字段名跟数据库的一样,隐藏数据库真实字段信息;我们可以使用SerializerMethodField字段,它有个配套方法,get_字段名,return什么就显示什么。

世间微尘里 独爱茶酒中