跟着白月黑羽的视频以及网站:http://www.byhy.net/tut/webdev/django/01/学习了一下Python Web开发中的Django框架的使用,白月黑羽的 Django 教程 以一个 实际的项目 来 讲解如何使用Django 开发Web应用。简单易懂,并且记了一些笔记:
Web开发简介
Web应用技术
当今的互联网时代,大家经常使用电脑上的浏览器或者手机 进行购物、刷新闻,娱乐,学习。
这些丰富多彩应用,背后的软件系统是基于web技术开发的。
这些web系统,通常可以分为 : 客户端 和 服务端 。
比如,手机应用(比如微信)就是 客户端, 腾讯公司的微信服务程序(运行在腾讯的机房里面)就是服务端。
再比如,电脑打开淘宝, 浏览器里面运行的淘宝网页,就是 客户端, 而 阿里巴巴 的 淘宝服务程序(运行在阿里的机房里面)就是服务端。
也有人喜欢把 客户端 称之为 前端 ,服务端称之为 后端 。
那么 开发 客户端(前端)的工程师 就会被称之为 客户端(前端)工程师 ,
开发 服务端(后端)的工程师 就会被称之为 服务端(后端)工程师。
如果一个工程师,同时可以做前端和后端的开发,通常可以称之为 web系统全栈 工程师。
客户端和服务端 之间 是需要 进行数据信息的交流的。 想象一下,当你登录淘宝 想查看自己的购买记录, 你的购买记录存储在阿里的服务器上, 所以浏览器就需要从阿里的服务器上获取你的购买记录。
那么作为客户端的浏览器或者手机 是怎么获取信息呢?
通常是根据使用http协议(Hypertext Transfer Protocol)。
客户端通过http协议发送请求信息给服务端,并从服务端接收响应信息。
下面我们就 分别来看看 前端开发 和 后端开发 大体是做什么。
这里,我们先关注 浏览器前端, 也就是web前端。 对于手机前端的 开发,暂不涉及。
Web前端开发
Web 前端开发的重点是 : 提供用户界面给 用户进行观看和操作。
开发人员通常需要开发如下文件:
-
html 文件 用来显示界面给用户看,我们看到的 各种美观的web网页就是通过解释html实现的。 当然,要显示出各种美观的界面,并且让用户方便的操作,光是html是不够的,还需要下面这些文档。 -
CSS CSS 文档控制界面的显示样式和效果,比如字体、大小、前景色、背景色、间距、一些动画效果等等, 一句话:就是让你的界面更好看 -
资源文件 包括显示在界面上的 图片、视频等 -
javascript 脚本 html 文档里面还经常包括一些,javascript 脚本 ( 简称 js )。 js 和 python 一样是一种编程语言。 python脚本通过python解释器执行,js脚本通过浏览器内置的 js 引擎执行。 注意:html 和 css 文档 只是定义了一些静态的界面内容。前端的动态功能, 就是通过浏览器执行 这些 js脚本产生的。
上述的这些文件开发出来后, 最终都是通过 浏览器 来运行,展示出界面来给用户观看和操作的。
上述的文件(html,css,js)没法直接放到浏览器端,都是部署在后端服务器上(有些在cdn上)。
当我们浏览一个网站的时候,浏览器先通过http协议获取这些文档,然后读取解释它们的内容,生成对应的界面呈现给我们操作。
在大概十多年前,相对后端开发来说,前端的开发量相对较小。 主要就是设计界面。
前端开发一般没有太多的动态的逻辑控制功能。
最多就是用js 脚本做一些页面动态效果,以及一些数据校验的工作(比如注册时的用户名长度校验等)。
主要的数据处理 和 业务逻辑的实现,甚至界面html 文档的动态生成,都是后端做的事情。
由于 设计上的难度相对较小,前端工程师有时被称为 做界面的 ,略带一种轻视的意味。
而现在由于 浏览器 内嵌的js 解释器性能飞速提升,可以让大量的代码逻辑在前端实现。
由于前端浏览器运行在每个用户各自的电脑(或者手机)上,如果把一部分业务逻辑的实现放在前端,相当于有成千上万的前端设备分担后端的负荷, 可以大大分担后端的压力。
所以现在的web系统的设计,前端的重要性日益增加。
前端工程师经常需要通过javascript语言,实现数据处理和展示。
有的系统,前端也实现部分业务逻辑功能。 比如: 用户权限检查、用户显示数据过滤等。
有的架构师喜欢把大量的业务逻辑转移到前端,加上界面渲染的功能也完全由前端实现。
这样 后端就做单纯的数据存储 和 分析工作。
所以现在前端工程师, 不仅设计实现 用户界面,还要能使用js 语言 实现 数据获取、分析处理 和 业务相关的逻辑。
- 前端也越来越复杂,需要学习的东西越来越多,主流框架:Vue、React、Angular。
Web后端开发
刚才说了前端开发的工作,那么后端开发主要做什么?
简单说,后端要开发 服务进程,处理前端http请求,返回相应的数据。
通常 包括数据的 查询、增加、删除、修改。
这听起来似乎很简单,其实有的业务流程非常复杂 (想想淘宝购物),有时一个购买操作,要涉及到很多逻辑处理。
而且,如果设计用户量非常大,需要响应 百万级以上 的客户访问, 就需要精心的设计架构,做好多服务分布式、集群式的处理大量的用户请求。
通常,后端的开发涉及到:
就是前端开发出来的HTML、css、js文件存储在什么地方,使用什么的服务提供给前端浏览器访问。 通常一个比较大型的网站, 静态文件往往会使用单独的服务器专门提供服务,甚至一部分特别消耗带宽的数据(比如视频、图片)会使用第三方的云服务厂商(比如阿里云的cdn和oss服务)。
-
API 接口设计, 就是 定义 前端和后端交互接口规范。 目前流行的是REST API 风格的接口,但是需要我们设计具体的API请求和响应消息的组成细节。 这个通常应该是架构师设计的, 但是往往这工作经常会落到后端工程师头上。实际上 很多公司里面,系统架构师 也会做后端开发的工作。 -
数据库存储方案,比如:选择什么样的数据库,包括 关系型和非关系型的数据库。 -
数据库表结构设计, 要能合理、高效的存储业务数据,这样才能 高效查询、修改各种关联性的数据。 -
为了提高性能, 需要决定使用怎样的 缓存服务 和 异步任务服务 -
还有 其它种种特殊的考虑,比如 要熟悉目前日益流行的云存储技术,将有的数据,如图片,视频等 合理存储在云端 -
有的系统还需要有 大数据分析的需求 要后端高效实现。
使用 Django 开发后端服务
本教程重点放在后端服务的实现, 包括 API 接口设计 和 数据库设计和操作。
我们 使用 Python Web 开发 最流行的应用框架 Django 帮我们高效地实现 后端。
Django是一个 基于Python语言的 开源免费的 Web应用 开发框架。
它帮我们解决了Web应用开发的 常见问题。
使用它,我们可以把精力放在应用本身的逻辑处理上,而不用操心Web服务的基本问题。这样可以大大提高我们的开发效率。
这是Django的 官方网站 https://www.djangoproject.com
我们的Django 教程 以一个实际的案例 来 讲解如何使用Django 开发Web应用。
我们的案例是 实现一个 公司 的 销售管理系统。
要查看具体的系统需求 请点击这里
大家可以把自己想象成一个后端开发人员, 根据上面的系统需求, 一步步的实现后端系统。 而这个系统的前端开发 ,你们也假想有个团队在开发, 如果你们后端实现正确, 就可以正确 对接成功,完成一个完整系统的功能。
项目目标
白月医疗设备公司的销售管理系统 的需求文档
本系统简称 BYSMS (白月销售管理系统)
本系统的使用者有: 管理员 和 销售员
白月制药公司 生产各种药品,比如:来适可、立卫克、舒利迭 。
当销售员销售成功,客户(比如 上海华山医院)购买 白月公司的产品后,销售员自己 或者 管理员 需要把客户购买的 订单填入 BYSMS 系统,包括 日期、客户、药品、数量
管理员可以在 BYSMS 系统 查询、添加 、 修改、删除 所有 的订单, 而销售员只能 查询、添加 、 修改、删除 自己创建 的订单
管理员 查询订单可以根据 客户名、 日期、药品名 查询。
管理员操作
管理员操作界面如下
登录界面
登录界面示意图如下:
管理员输入自己的登录名、密码 登录。
同样的账号,输入3次错误密码,该管理员1小时内不能再登录。
客户管理界面
管理员登录后,点击左边的 客户 菜单项,显示界面如下
管理员可以查看当前系统中已经有的客户信息,包括:客户名、联系电话、地址
添加客户界面
在客户管理界面,点击添加按钮, 可以添加一名客户,示意图如下所示
药品管理界面
管理员登录后,点击左边的 药品 菜单项,显示界面如下
管理员可以查看当前系统中已经有的药品信息,包括:药品名、药品编号、药品描述
添加药品界面
在药品管理界面,点击添加按钮, 可以添加一种药品,示意图如下所示
订单管理界面
管理员登录后,点击左边的 订单 菜单项,显示界面如下
管理员可以查看当前系统中已经有的订单信息,包括:订单名、订单日期、下单客户、购买的药品和数量列表。
添加订单界面
在订单管理界面,点击添加按钮, 可以添加一份订单,示意图如下所示
接口文档
本接口用于 Bysms 系统 管理员用户 前后端系统 之间的数据 交互。
本接口中,所有请求 ( 除了登录请求之外 ),必须在cookie中携带有登录的成功后,服务端返回的sessionid。
本接口中,所有请求、响应的消息体 均采用 UTF8 编码
登录系统
请求消息
POST /api/mgr/signin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
请求参数
http 请求消息 body 中 参数以 格式 x-www-form-urlencoded 存储
需要携带如下参数,
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果登录成功,返回如下
{
"ret": 0
}
ret 为 0 表示登录成功
如果登录失败,返回失败的原因,示例如下
{
"ret": 1,
"msg": "用户名或者密码错误"
}
ret 不为 0 表示登录失败, msg字段描述登录失败的原因
客户数据
列出所有客户
请求消息
GET /api/mgr/customers?action=list_customer HTTP/1.1
请求参数
http 请求消息 url 中 需要携带如下参数,
-
action 填写值为 list_customer
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果获取信息成功,返回如下
{
"ret": 0,
"retlist": [
{
"address": "江苏省常州武进市白云街44号",
"id": 1,
"name": "武进市 袁腾飞",
"phonenumber": "13886666666"
},
{
"address": "北京海淀区",
"id": 4,
"name": "北京海淀区代理 蔡国庆",
"phonenumber": "13990123456"
}
]
}
ret 为 0 表示登录成功
retlist 里面包含了所有的客户信息列表。
每个客户信息以如下格式存储
{
"address": "江苏省常州武进市白云街44号",
"id": 1,
"name": "武进市 袁腾飞",
"phonenumber": "13886666666"
}
添加一个客户
请求消息
POST /api/mgr/customers HTTP/1.1
Content-Type: application/json
请求参数
http 请求消息 body 携带添加客户的信息
消息体的格式是json,如下示例:
{
"action":"add_customer",
"data":{
"name":"武汉市桥西医院",
"phonenumber":"13345679934",
"address":"武汉市桥西医院北路"
}
}
其中
action 字段固定填写 add_customer 表示添加一个客户
data 字段中存储了要添加的客户的信息
服务端接受到该请求后,应该在系统中增加一位这样的客户。
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果添加成功,返回如下
{
"ret": 0,
"id" : 677
}
ret 为 0 表示成功。
id 为 添加客户的id号。
如果添加失败,返回失败的原因,示例如下
{
"ret": 1,
"msg": "客户名已经存在"
}
ret 不为 0 表示失败, msg字段描述添加失败的原因
修改客户信息
请求消息
PUT /api/mgr/customers HTTP/1.1
Content-Type: application/json
请求参数
http 请求消息 body 携带修改客户的信息
消息体的格式是json,如下示例:
{
"action":"modify_customer",
"id": 6,
"newdata":{
"name":"武汉市桥北医院",
"phonenumber":"13345678888",
"address":"武汉市桥北医院北路"
}
}
其中
action 字段固定填写 modify_customer 表示修改一个客户的信息
id 字段为要修改的客户的id号
newdata 字段中存储了修改后的客户的信息
服务端接受到该请求后,应该在系统中增加一位这样的客户。
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果修改成功,返回如下
{
"ret": 0
}
ret 为 0 表示成功。
如果修改失败,返回失败的原因,示例如下
{
"ret": 1,
"msg": "客户名已经存在"
}
ret 不为 0 表示失败, msg字段描述添加失败的原因
删除客户信息
请求消息
DELETE /api/mgr/customers HTTP/1.1
Content-Type: application/json
请求参数
http 请求消息 body 携带要删除客户的id
消息体的格式是json,如下示例:
{
"action":"del_customer",
"id": 6
}
其中
action 字段固定填写 del_customer 表示删除一个客户
id 字段为要删除的客户的id号
服务端接受到该请求后,应该在系统中尝试删除该id对应的客户。
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果删除成功,返回如下
{
"ret": 0
}
ret 为 0 表示成功。
如果删除失败,返回失败的原因,示例如下
{
"ret": 1,
"msg": "id为 566 的客户不存在"
}
ret 不为 0 表示失败, msg字段描述添加失败的原因
药品
列出所有药品
请求消息
GET /api/mgr/medicines HTTP/1.1
请求参数
http 请求消息 url 中 需要携带如下参数,
-
action 填写值为 list_medicine
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果获取信息成功,返回如下
{
"ret": 0,
"retlist": [
{"id": 1, "name": "青霉素", "sn": "234324234234", "desc": "青霉素"},
{"id": 2, "name": "红霉素", "sn": "234545534234", "desc": "红霉素"}
]
}
ret 为 0 表示登录成功
retlist 里面包含了所有的药品信息列表。
每个药品信息以如下格式存储
{"id": 2, "name": "红霉素", "sn": "234545534234", "desc": "红霉素"}
添加一个药品
请求消息
POST /api/mgr/medicines HTTP/1.1
Content-Type: application/json
请求参数
http 请求消息 body 携带添加药品的信息
消息体的格式是json,如下示例:
{
"action":"add_medicine",
"data":{
"desc": "青霉素 国字号",
"name": "青霉素",
"sn": "099877883837"
}
}
其中
action 字段固定填写 add_medicine 表示添加一个药品
data 字段中存储了要添加的药品的信息
服务端接受到该请求后,应该在系统中增加这样的药品。
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果添加成功,返回如下
{
"ret": 0,
"id" : 677
}
ret 为 0 表示成功。
id 为 添加药品的id号。
如果添加失败,返回失败的原因,示例如下
{
"ret": 1,
"msg": "药品名已经存在"
}
ret 不为 0 表示失败, msg字段描述添加失败的原因
修改药品信息
请求消息
PUT /api/mgr/medicines HTTP/1.1
Content-Type: application/json
请求参数
http 请求消息 body 携带修改药品的信息
消息体的格式是json,如下示例:
{
"action":"modify_medicine",
"id": 6,
"newdata":{
"name":"武汉市桥北医院",
"phonenumber":"13345678888",
"address":"武汉市桥北医院北路"
}
}
其中
action 字段固定填写 modify_medicine 表示修改一个药品的信息
id 字段为要修改的药品的id号
newdata 字段中存储了修改后的药品的信息
服务端接受到该请求后,应该在系统中增加一位这样的药品。
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果修改成功,返回如下
{
"ret": 0
}
ret 为 0 表示成功。
如果修改失败,返回失败的原因,示例如下
{
"ret": 1,
"msg": "药品名已经存在"
}
ret 不为 0 表示失败, msg字段描述添加失败的原因
删除药品信息
请求消息
DELETE /api/mgr/medicines HTTP/1.1
Content-Type: application/json
请求参数
http 请求消息 body 携带要删除药品的id
消息体的格式是json,如下示例:
{
"action":"del_medicine",
"id": 6
}
其中
action 字段固定填写 del_medicine 表示删除一个药品
id 字段为要删除的药品的id号
服务端接受到该请求后,应该在系统中尝试删除该id对应的药品。
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果删除成功,返回如下
{
"ret": 0
}
ret 为 0 表示成功。
如果删除失败,返回失败的原因,示例如下
{
"ret": 1,
"msg": "id为 566 的药品不存在"
}
ret 不为 0 表示失败, msg字段描述添加失败的原因
订单
列出所有订单
请求消息
GET /api/mgr/orders HTTP/1.1
请求参数
http 请求消息 url 中 需要携带如下参数,
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果获取信息成功,返回如下
{
"ret": 0,
"retlist": [
{id: 1, name: "华山医院订单001", create_date: "2018-12-26T14:10:15.419Z", customer_name: "华山医院",medicines_name: "青霉素"},
{id: 2, name: "华山医院订单002", create_date: "2018-12-27T14:10:37.208Z", customer_name: "华山医院",medicines_name: "青霉素 | 红霉素 "}
]
}
ret 为 0 表示登录成功
retlist 里面包含了所有的订单信息列表。
每个订单信息以如下格式存储
{
id: 2,
name: "华山医院订单002",
create_date: "2018-12-27T14:10:37.208Z",
customer_name: "华山医院",
medicines_name: "青霉素 | 红霉素 "
}
其中 medicines_name 表示对应的药品,如果该订单有多个药品, 中间用 竖线隔开
添加一个订单
请求消息
POST /api/mgr/orders HTTP/1.1
Content-Type: application/json
请求参数
http 请求消息 body 携带添加订单的信息
消息体的格式是json,如下示例:
{
"action":"add_order",
"data":{
"name":"华山医院订单002",
"customerid":3,
"medicineids":[1,2]
}
}
其中
action 字段固定填写 add_order 表示添加一个订单
data 字段中存储了要添加的订单的信息
medicineids 是 该订单中药品的id 列表
服务端接受到该请求后,应该在系统中增加这样的订单。
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果添加成功,返回如下
{
"ret": 0,
"id" : 677
}
ret 为 0 表示成功。
id 为 添加订单的id号。
如果添加失败,返回失败的原因,示例如下
{
"ret": 1,
"msg": "订单名已经存在"
}
ret 不为 0 表示失败, msg字段描述添加失败的原因
安装Bysms进行学习
打开网盘准备下载
点击打开下面分享链接,准备下载
百度网盘分享
Windows 用户
下载 白月SMS系统 压缩包 bysms.zip
下载解压bysms.zip后,进入bysms目录,双击运行runserver.bat 即可启动 白月SMS系统。 出现下面这样的信息
\bysms\bysms>bysms.exe runserver 80
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 2 unapplied migration(s). Your project may not work properly
until you apply the migrations for app(s): auth.
Run 'python manage.py migrate' to apply them.
September 07, 2019 - 22:22:19
Django version 2.2.4, using settings 'bysms.settings'
Starting development server at http://127.0.0.1:80/
Quit the server with CTRL-BREAK.
注意:该窗口不能关闭,否则web 系统就会停止
苹果 Mac 用户
下载 白月SMS系统 压缩包 bysms_mac.zip
执行 unzip bysms_mac.zip 解压
然后,进入bysms目录,做如下事情
- 执行 pip3 install django 安装Django
- 执行 ./runserver.sh 启动服务
由于需要80端口权限,是sudo方式启动,需要输入当前用户密码
运行常见问题
如果启动运行 出现闪退,很可能 是因为web服务的80端口已经被使用了。
两种解决方法:
-
请检查关闭占用80端口的程序,可能是Apache、IIS等web服务。 -
文本编辑器编辑 runserver.bat (Mac 是 runserver.sh) python manage.py runserver 80 --noreload
修改网站服务端口为其他端口 比如 8047 python manage.py runserver 8047 --noreload
以后访问网址加上端口号,比如 http://127.0.0.1:8047/mgr/sign.html
登录
然后可以浏览器访问 登录页面 http://127.0.0.1/mgr/sign.html
输入如下管理员账号
用户名 :byhy 密码: 88888888
即可登录
HTTP 协议
web 前端系统 和 后端系统 之间就是通过HTTP协议进行通信的,要学习后端的开发,必须先了解HTTP协议的基础知识。最好能了解部分计算机网络的知识。
HTTP 协议简介
EB API接口 大都是基于 HTTP 协议的,所以,要进行接口测试 首先要了解 HTTP 协议 的 基础知识。
HTTP 协议 全称是 超文本传输协议, 英文是 Hypertext Transfer Protocol 。
HTTP 最初是用来 在 浏览器和 网站服务器(web服务)之间 传输超文本(网页、视频、图片等)信息的。
由于 HTTP 简洁易用,后来,不仅仅是浏览器 和 服务器之间 使用它, 服务器和服务器之间, 手机App 和 服务器之间, 都广泛的采用。 成了一个软件系统间 通信 的首选协议 之一。
HTTP 有好几个版本,包括: 0.9、1.0、1.1、2,当前最广泛使用的是 HTTP/1.1 版本。
HTTP 协议最大的特点是 通讯双方 分为 客户端 和 服务端 。
由于 目前 HTTP是基于 TCP 协议的, 所以要进行通讯,客户端 必须先 和服务端 创建 TCP 连接。
而且 HTTP 双方的信息交互,必须是这样一种方式:
- 客户端 先发送 http请求(request)给 服务端
- 然后服务端 发送 http响应(response)给 客户端
特别注意:HTTP协议中,服务端不能主动先发送信息给 客户端。
而且在1.1 以前的版本, 服务端 返回响应给客户端后,连接就会 断开 ,下一次双方要进行信息交流,必须重复上面的过程,重新建立连接,客户端发送请求,服务返回响应。
到了 1.1 版本, 建立连接后,这个连接可以保持一段时间(keep alive), 这段时间,双方可以多次进行 请求和响应, 无需重新建立连接。
如果客户端是浏览器,如何在chrome浏览器中查看 请求和响应的HTTP消息:
点击F12开network中的内容:
HTTP 请求消息
下面是2个http请求消息的示例:
GET /mgr/login.html HTTP/1.1
Host: www.baiyueheiyu.com
User-Agent: Mozilla/6.0 (compatible; MSIE5.01; Windows NT)
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
POST /api/medicine HTTP/1.1
Host: www.baiyueheiyu.com
User-Agent: Mozilla/6.0 (compatible; MSIE5.01; Windows NT)
Content-Type: application/x-www-form-urlencoded
Content-Length: 51
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
name=qingmeisu&sn=099877883837&desc=qingmeisuyaopin
http请求消息由下面几个部分组成:
请求行 request line
是http请求的第一行的内容,表示要操作什么资源,使用的 http协议版本是什么。
里面包含了3部分信息: 请求的方法,操作资源的地址, 协议的版本号
例如
GET /mgr/login.html HTTP/1.1
表示要 获取 资源, 资源的 地址 是 /mgr/login.html , 使用的 协议 是 HTTP/1.1
而
POST /api/medicine HTTP/1.1
表示 添加 资源信息, 添加资源 到 地址 /api/medicine , 使用的 协议 是 HTTP/1.1
GET、POST是请求的方法,表示这个动作的大体目的,是获取信息,还是提交信息,还是修改信息等等
常见的HTTP 请求方法包括:
-
GET 从服务器 获取 资源信息,这是一种最常见的请求。 比如 要 从服务器 获取 网页资源、获取图片资源、获取用户信息数据等等。 -
POST,请求方法就应该是 添加 资源信息 到 服务器进行处理(例如提交表单或者上传文件)。 比如 要 添加用户信息、上传图片数据 到服务器 等等 具体的数据信息,通常在 HTTP消息体中, 后面会讲到 -
PUT 请求服务器 更新 资源信息 。 比如 要 更新 用户姓名、地址 等等 具体的更新数据信息,通常在 HTTP消息体中, 后面会讲到 -
DELETE 请求服务器 删除 资源信息 。 比如 要 删除 某个用户、某个药品 等等
HTTP还有许多其他方法,比如 PATCH、HEAD 等,不是特别常用,暂且不讲。
请求头 request headers
请求头是http请求行下面的 的内容,里面存放 一些 信息。
比如,请求发送的服务端域名是什么, 希望接收的响应消息使用什么语言,请求消息体的长度等等。
通常请求头 都有好多个,一个请求头 占据一行
单个请求头的 格式是: 名字: 值
HTTP协议规定了一些标准的请求头,点击查看MDN的描述
开发者,也可以在HTTP消息中 添加自己定义的请求头
消息体 message body
请求的url、请求头中 可以存放 一些数据信息, 但是 有些数据信息,往往需要 存放在消息体中。
特别是 POST、PUT等请求,添加、修改的数据信息 通常都是 存放在 请求消息体 中的。
如果 HTTP 请求 有 消息体, 协议规定 需要在 消息头和消息体 之间 插入一个空行, 隔开 它们。
请求消息体中保存了要提交给服务端的数据信息。
比如:客户端要上传一个文件给服务端,就可以通过HTTP请求发送文件数据给服务端。
文件的数据 就应该在请求的消息体中。
再比如:上面示例中 客户端要添加药品,药品的名称、编码、描述,就存放在请求消息体中。
WEB API 请求消息体 通常是某种格式的文本,常见的有
- Json
- Xml
- www-form-urlencoded
HTTP 响应信息
下面是1个http响应消息的示例
HTTP/1.1 200 OK
Date: Thu, 19 Sep 2019 08:08:27 GMT
Server: WSGIServer/0.2 CPython/3.7.3
Content-Type: application/json
Content-Length: 37
X-Frame-Options: SAMEORIGIN
Vary: Cookie
{"ret": 0, "retlist": [], "total": 0}
HTTP响应消息包含如下几个部分:
状态行 status line
状态行在第一行,包含3个部分:
-
协议版本 上面的示例中,就是 HTTP/1.1 -
状态码 上面的示例中,就是 200 -
描述状态的短语 上面的示例中,就是 OK
我们重点来看一下状态码,它表示了 服务端对客户端请求的处理结果 。
状态码用3位的数字来表示,第一位 的 数字代表 处理结果的 大体类型,常见的有如下几种:
? 2xx
通常 表示请求消息 没有问题,而且 服务器 也正确处理了
最常见的就是 200
? 3xx
这是重定向响应,常见的值是 301,302, 表示客户端的这个请求的url地址已经改变了, 需要 客户端 重新发起一个 请求 到另外的一个url。
? 4xx
表示客户端请求有错误, 常见的值有:
400 Bad Request 表示客户端请求不符合接口要求,比如格式完全错误
401 Unauthorized 表示客户端需要先认证才能发送次请求
403 Forbidden 表示客户端没有权限要求服务器处理这样的请求, 比如普通用户请求删除别人账号等
404 Not Found 表示客户端请求的url 不存在
? 5xx
表示服务端在处理请求中,发生了未知的错误。
通常是服务端的代码设计问题,或者是服务端子系统出了故障(比如数据库服务宕机了)
响应头 response headers
响应头 是 响应状态行下面的 的内容,里面存放 一些 信息。 作用 和 格式 与请求头类似,不再赘述。
消息体 message body
有时候,http响应需要消息体。
同样, 如果 HTTP 响应 有 消息体, 协议规定 需要在 消息头和消息体 之间 插入一个空行, 隔开 它们。
比如,白月SMS系统 请求 列出 药品 信息,那么 药品 信息 就在HTTP响应 消息体中
再 比如,浏览器地址栏 输入 登录网址,浏览器 请求一个登录网页的内容,网站服务器,就在响应的消息体中存放登录网页的html内容。
和请求消息体一样,WEB API 响应消息体 通常也是某种格式的文本,常见的有:
- Json
- Xml
- www-form-urlencoded
关于这些格式,我们会在后续课程中进行讲解
安装与运行Django
安装Django
Django 框架是用Python语言开发的, 所以安装Django 就像安装其他的 Python库一样,执行如下命令即可
pip install django
你可以执行如下命令检查Django是否安装好, 并且查看安装的Django版本
> python -m django --version
2.1.3
如果像上面那样显示出一个版本数字(比如这里就是2.1.3) 表示已经安装好了。
创建项目
安装好以后, 我们需要创建我们 的 项目目录,项目目录里面保存了开发系统的所有文件。
我们可以创建 d:\projects 作为我们的项目所在的目录。
然后从命令行窗口中 进入到 d:\projects 目录,执行下面的命令创建项目目录
django-admin startproject bysms
注意最后的 bysms 就是项目的根目录名,执行上面命令后,就会创建 如下的目录结构:
bysms/
manage.py
bysms/
__init__.py
settings.py
urls.py
wsgi.py
-
最外层 bysms/ 就是项目根目录 d:\projects\bysms\ , 项目文件都放在里面。 -
manage.py 是一个工具脚本,用作项目管理的。以后我们会使用它执行管理操作。 -
里面的 bysms/ 目录是python包。 里面包含项目的重要配置文件。这个目录名字不能随便改,因为manage.py 要用到它。 -
bysms/settings.py 是 Django 项目的配置文件. 包含了非常重要的配置项,以后我们可能需要修改里面的配置。 -
bysms/urls.py 里面存放了 一张表, 声明了前端发过来的各种http请求,分别由哪些函数处理. 这个我们后面会重点的讲。 -
bysms/wsgi.py 要了解这个文件的作用, 我们必须明白wsgi 是什么意思 python 组织制定了 web 服务网关接口(Web Server Gateway Interface) 规范 ,简称wsgi。参考文档 https://www.python.org/dev/peps/pep-3333/ 遵循wsgi规范的 web后端系统, 我们可以理解为 由两个部分组成 wsgi web server 和 wsgi web application 它们通常是运行在一个python进程中的两个模块,或者说两个子系统。 wsgi web server 接受到前端的http请求后,会调用 wsgi web application 的接口( 比如函数或者类方法)方法,由wsgi web application 具体处理该请求。然后再把处理结果返回给 wsgi web server , wsgi web server 再返回给前端。 如下图所示 为什么要搞出两个子系统,这么麻烦呢? 因为这两个子系统有各自负责的重点。 wsgi web server 负责 提供高效的http请求处理环境,可以使用多线程、多进程或者协程的机制。 http 请求发送到 wsgi web server , wsgi web server 分配 线程或者进程或者 轻量级线程(协程),然后在 这些 线程、进程、或者协程里面,去调用执行 wsgi web application 的入口代码。 wsgi web application 被调用后,负责 处理 业务逻辑。 业务逻辑的处理可能非常复杂, wsgi web application 需要精心的设计来正确处理。 django是 wsgi web application 的框架,它只有一个简单的单线程 wsgi web server 。 供调试时使用。 产品正式上线运行的时候,通常我们需要高效的 wsgi web server 产品,比如 gunicorn,uwsgi,cherrypy等,结合Django ,组成一个高效的 后端服务。 所以这个 wsgi.py 就是 提供给wsgi web server 调用 的接口文件,里面的变量application对应对象实现了 wsgi入口,供wsgi web server 调用 。
补充
上面的方式创建的项目,项目配置目录和项目本身目录同名。
我个人觉得比较怪异。
可以这样创建目录,结构更合理一些:
mkdir bysms && cd bysms
django-admin startproject config .
运行 Django web服务
刚才我们说了, django虽然只是 wsgi web application 的框架,但是它也有一个简单的 wsgi web server 。 供调试时使用。
所以也构成一个完整的后端web服务。 本地调试代码的时候,完全可以运行起来。
运行开发web 服务只需要在命令行窗口里面,
首先进入到项目根目录 ,比如,我们这里就是 d:\projects\bysms\
然后执行如下命令
python manage.py runserver 0.0.0.0:80
这样服务就会被启动。 我们就可以在浏览器访问web服务了。
其中 0.0.0.0:80 是指定 web服务绑定的 IP 地址和端口。
0.0.0.0 表示绑定本机所有的IP地址, 就是可以通过任何一个本机的IP (包括 环回地址 127.0.0.1 ) 都可以访问我们的服务。
80 表示是服务启动在80端口上。
请打开浏览器,地址栏输入 ‘127.0.0.1’ ,就可以看到如下的界面,表示Django服务搭建成功,启动成功。
注意,启动web服务的命令行窗口不能关闭,如果关闭,web服务就停止了。
HTTP 请求的 url 路由
我们的Django 教程 以一个实际的案例 来 讲解如何使用Django 开发Web应用。
我们的案例是 实现一个 公司 的 销售管理系统。
具体的系统需求 请点击这里
在实现的过程中,我们将会了解 Django web 开发中的如下重要概念
url路由
http请求处理
ORM数据库操作
创建项目app
Django 中的一个app 就是项目里面的一个应用的意思。
一个项目包含多个app。
一个app 通常就是一个相对独立的模块 ,实现相对独立的功能。
比如,我们可以把 这个系统的 管理员管理的功能 做在一个名字为 mgr的app里面,把 销售人员的操作 实现在另外一个名字为 sales的app里面。
一个app 本质上 就是一个 Python 包, 里面包含了一些应用相关的代码文件。
当然,一个项目分成多少个app 这完全取决你的设计。 你把所有的功能都放入一个大app内也可以实现功能,只是这样做,这个app特别的臃肿。
Django 中创建app 可以 通过执行命令,创建一个app目录,并在里面自动创建app常用的文件。
比如,现在我们需要一个app 专门处理 白月医药系统中销售员的 添加、修改、查询、删除请求。
我们就进入项目根目录,执行下面的命令。
python manage.py startapp sales
这样就会创建一个目录名为 sales, 对应 一个名为 sales 的app,里面包含了如下自动生成的文件。
sales/
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py
这个目录其实就是一个python package
里面有很多文件,后面我们会分别讲到它们的作用。
返回 页面内容 给浏览器
当浏览器地址栏中输入网址访问的时候,服务端是如何让浏览器呈现出网页内容的?
是这样的: 当我们输入网址,比如百度网址,比较敲回车后, 浏览器会发出http请求给百度的服务器,百度服务器返回 HTML 文档内容给浏览器, 浏览器解析后,呈现出我们最终看到的网页内容。
服务器返回的 HTML 文档内容其实就是 符合 HTML 语法的 一段字符串 而已。
我们现在使用Django 开发后端服务, 就可以响应 浏览器的http请求, 返回一段HTML字符串, 浏览器就可以呈现在界面上了。
刚才我们创建的 sales 应用里面 有个 views.py 文件。 这个文件里面通常是写处理http 请求的代码的。
比如,我们 设计 凡是浏览器访问的http 请求的 url 地址 是 /sales/orders/ , 就由 views.py 里面的函数 listorders 来处理, 返回一段字符串给浏览器。
请大家 打开 views.py , 在里面加入如下内容
from django.http import HttpResponse
def listorders(request):
return HttpResponse("下面是系统中所有的订单信息。。。")
注意
-
这里面最终的返回结果是 HttpResponse 对象的参数字符串 ,也就是这句话 下面是系统中所有的订单信息。。。 -
listorders的参数 request 是Django中的 HttpRequest 对象,包含了HTTP请求中的信息。 后端程序处理请求,常常要根据请求中的数据进行相应的处理: 比如请求添加一个用户,那么HTTP请求消息中就会携带要添加用户的信息(姓名、登录账号等)。 我们写后端的代码,这些信息就在 HttpRequest 对象中获取。 所以这个参数非常重要。 HttpRequest 对象的属性和用法,具体可以参考官方文档这里 它的用法后面涉及到的地方会讲。这里暂时用不到该参数。
光是定义了这样一个函数不行的,我们需要 告诉 Django :
当前端发送过来的HTTP请求 url地址 是 /sales/orders/ , 就由 views.py 里面的函数 listorders 来处理
怎么告诉Django呢?
这就是 Django中的 url路由设置。
url路由
添加路由记录
前面在创建项目目录的时候,在项目的设置目录下,有如下的一个urls.py 文件
这个文件是 url路由设置的入口文件。
打开该文件,在 urlpatterns 列表变量中添加一条路由信息,结果如下
from django.contrib import admin
from django.urls import path
from sales.views import listorders
urlpatterns = [
path('admin/', admin.site.urls),
path('sales/orders/', listorders),
]
urlpatterns 列表 就是 Django 的 url 路由的入口。
里面是一条条的路由记录,我们添加的
path('sales/orders/', listorders)
就是告诉 当前端过来的请求 url地址 是 /sales/orders/ , 就由 views.py 里面的函数 listorders 来处理。
所以,所谓 路由 就是指 : 根据 HTTP请求的url路径, 设置 由哪个 函数来处理这个请求。
通常我们项目代码的修改, Django的测试服务可以自动检测到,并且重新加载,不需要我们重启 Django Web 服务。
如果大家想重新启动 Django web 服务, 大家可以在启动web服务的命令行窗口,按ctrl + break(也就是Pause按钮)先停止服务。 然后再次运行启动命令。
我们这时,就可以登录浏览器输入网址 http://127.0.0.1/sales/orders/
回车后,就会出现如下内容
这就是浏览器的请求经过 Django路由后, 选择执行我们定义的函数 listorders,该函数 返回的字符串, 被作为http响应的消息体中的内容返回给 浏览器了。
所以浏览器最终显示的就是 我们 listorders 函数返回的字符串。
注意:
只要修改了路由表配置,添加了我们自己的路由记录,再去浏览器访问 首页,这里就是 http://127.0.0.1 ,前面曾经出现的小火箭欢迎页就不见了! 会出现一个 404 Not Found 的报错页面。
这是正常的,小火箭欢迎页面 是Django在调试模式下,发现路由记录没有添加的时候,缺省作为首页的。 真正的产品是不会使用这个首页的。一旦路由记录发生变动, 就会消失。
路由子表
url 路由表就是可以像上面这样,一个请求对应一个处理函数。
但是有的时候,我们的项目比较大的时候, 请求的url 会特别多。
比如我们的系统提供给 客户、销售商、管理员 访问的url是不一样的,如下
customer/
customer/orders/
sales/
sales/orders/
mgr/
mgr/customers/
mgr/medicines/
mgr/orders/
复杂的系统url条目多达几百甚至上千个, 放在一个表中,查看时,要找一条路由记录就非常麻烦。
这时,我们通常可以将不同的路由记录 按照功能 分拆到不同的 url路由子表 文件中。
比如,这里我们可以把 访问 的 url 凡是 以 sales 开头的全部都 由 sales app目录下面的 子路由文件 urls.py 处理。
首先我们需要在 sales 目录下面创建一个新的文件 sales\urls.py 。
然后在这个 sales\urls.py 文件中输入如下内容
from django.urls import path
from . import views
urlpatterns = [
path('orders/', views.listorders),
]
然后,我们再修改主url路由文件 bysms/urls.py , 如下
from django.contrib import admin
from django.urls import path, include
from sales.views import listorders
urlpatterns = [
path('admin/', admin.site.urls),
path('sales/', include('sales.urls')),
]
当一个http请求过来时, Django检查 url,比如这里是sales/orders/ ,
先到主url路由文件 bysms/urls.py 中查看 是否有匹配的路由项。
如果有匹配 ( 这里匹配了 sales/ ), 并且匹配的对象 不是 函数, 而是 一个子路由设置 , 比如这里是 include('sales.urls')
就会去子路由文件中查看, 这里就是 sales.urls 对应的文件 sales\urls.py 。
注意这时,会从请求url中去掉 前面主路由文件 已经匹配上的部分(这里是 sales/ ), 将剩余的部分 (这里是 orders/ )去子路由文件中查看是否有匹配的路由项。
这里就匹配了 orders/ ,匹配的对象,这里是 views.listorders ,它是一个处理函数,就调用该函数处理 这个http请求, 将该函数的返回对象 构建 HTTP响应消息,返回给客户端。
创建数据库和表
后端开发离不开数据库
我们演示了一个获取订单信息的http请求到了服务端,请求的 url 地址 是 /sales/orders/, 那么服务端程序需要返回 订单信息,上次课程返回的只是简单的一句话, 演示一下url处理的流程。
真实的系统 就应该返回真实的 订单信息。 那么服务端从哪里获取真实的订单信息呢? 像 订单信息 这些数据通常就是保存在数据库里面的。
后端开发基本都需要操作数据,包括数据的 存储、查询、修改、删除。
通常,这些都是通过数据库来完成的。目前业界最广泛使用的数据库还是:关系型数据库。
关系型数据库系统,常用的开源数据库有 mysql 和 postgresql。
建议大家实际工作中使用的时候,使用上面这两种。
但是上面这些数据库,都需要我们安装数据库服务系统 和 客户端库,比较麻烦,现在我们先使用另一种更简单的 数据库 sqlite。
sqlite 没有 独立的数据库服务进程,数据操作被做成库直接供应用程序调用。 Django中可以直接使用,无须先搭建数据服务。
后面大家要使用mysql 等其他数据库 只需修改一些配置就可以了。
## 创建数据库
项目中数据库的配置在 bysms/settings.py 中,这里
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
大家可以发现,我们使用命令创建的项目, 缺省就是使用 sqlite。 而且对于的数据库文件,缺省的文件名是 db.sqlite3 , 就在项目的根目录下面
首先我们需要创建数据库,执行如下命令
python manage.py migrate
就会在 项目的根目录下面 生成一个配置文件中指定的数据库文件 db.sqlite3 。
并且 会在其中创建一些表。
我们可以点击下面链接,下载sqlite 数据库工具 sqlitestudio
下载后解压即可, 运行该工具, 然后打开上面的 文件 db.sqlite3
可以发现该库中有下面这些表已经创建好了。
这些表都是 Django缺省设置中, 需要的一些 基本表。
包括: 用户表 auth_user, 登录会话表 django_session 等等。
什么是 ORM
下图是某个数据库 里面 一张 medicine 表的 格式定义。 描述药品表的格式
下图是这张表里面存储的一条条数据
请你搜索一下你的大脑,这个 表的格式定义 和 表里面一条条数据 , 它们之间的关系就像什么?
是不是很像我们Python基础教程里面说的…. 再努力想想….
想起来了?
对了,是不是 很像 类定义 和 类的实例 之间的关系?
这个medicine表结构定义,就像定义了 一个medicine类, 定义好了之后,这个medicine表里面的一条条记录对应的就是一个个具体的药品。就是 medicine类的实例。
既然 数据库 表定义和表记录之间的关系 就像 类和实例 之间的关系,Django 就让开发者 通过 类 和 实例的操作 来对应 数据库 表 和记录的操作。
Django 里面, 数据库表的操作,包括 表的定义、表中数据的增删改查,都可以通过 Model 类型的对象进行的。
通常,在Django中
- 定义一张数据库的表 就是定义一个继承自 django.db.models.Model 的类
- 定义该表中的字段(列), 就是定义该类里面的一些属性
- 类的方法就是对该表中数据的处理方法,包括 数据的增删改查
这样,开发者对数据库的访问,从原来的使用底层的 sql 语句,变成 面向对象的开发,通过一系列对象的类定义 和方法调用就可以 操作数据库。
这样做:
首先 极大的简化了我们应用中的数据库开发,因为无需使用sql语句操作数据库了, 提高了开发的效率;
其次 屏蔽了 不同的数据库访问的底层细节,基本做到了 开发好代码后,如果要换数据库,几乎不需要改代码, 修改几个配置项就可以了。
这种 通过 对象 操作数据库 的方法 被称之为 ORM (object relational mapping),下面我们就来看怎样使用。
定义我们的 数据库表
我们开发系统,需要定义我们需要的数据库表。
首先,我们再创建一个名为common的应用目录, 里面存放我们项目需要的一些公共的表的定义。
大家还记得创建应用的命令吗?
对了, 进入项目根目录,执行下面的命令。
python manage.py startapp common
就会创建一个目录名为 common, 对应 一个名为 common 的app,里面包含了如下自动生成的文件。
common/
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py
前面已经说过,Django是通过定义类来定义数据库表的。
所以,我们要定义数据库的表,无需执行sql语句,只需要在app目录下面 定义特殊的类就可以了。
数据库表的定义,一般是放在app目录中的 models.py里面的。
打开 common/models.py,发现里面是空的,因为我们还没有定义我们的业务所需要的表。
我们修改它,加入如下内容
from django.db import models
class Customer(models.Model):
name = models.CharField(max_length=200)
phonenumber = models.CharField(max_length=200)
address = models.CharField(max_length=200)
这个 Customer 类继承自 django.db.models.Model, 就是用来定义数据库表的。
里面的 name、phonenumber、address 是该表的3个字段。
定义表中的字段 就是定义一些静态属性,这些属性是 django.db.models 里面的各种 Field 对象,对应不同类型的字段。
比如这里的3个字段 都是 CharField 对象,对应 varchar类型的数据库字段。
后面的参数 max_length 指明了该 varchar字段的 最大长度。
Djanog 有很多字段对象类型, 对应不同的类型的数据库字段。
大家可以参考官方文档
创建数据库表
定义好表以后,我们怎么真正去创建数据库表呢?
首先我们需要告诉Django: 我们的 common 应用中 需要你关注, 因为其中包含了 数据库Model的定义。
怎么告诉它? 在项目的配置文件 settings.py 中, INSTALLED_APPS 配置项 加入如下内容
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'common.apps.CommonConfig',
]
‘common.apps.CommonConfig’ 告诉 Django , CommonConfig 是 common/apps.py 文件中定义的一个应用配置的类。
是这样的
class CommonConfig(AppConfig):
name = 'common'
CommonConfig 是 AppConfig的 子类, 就表示这个是应用的配置类。
这里 name = ‘common’ , name 是用来定义 应用的python模块路径的。 这里就是说 应用 模块路径为 common 。
关于 其他的配置参数, 大家可以参考官方文档
https://docs.djangoproject.com/en/dev/ref/applications/#configurable-attributes
补充
在 INSTALLED_APPS中添加声明, 也可以直接写app的包名,比如
INSTALLED_APPS = [
'common',
]
这是老版本的做法。
两者的区别,具体可以参考这个帖子
现在Django知道了我们的 common 应用, 我们可以在项目根目录下执行命令
python manage.py makemigrations common
得到如下结果
Migrations for 'common':
common\migrations\0001_initial.py
- Create model Customer
这个命令,告诉Django , 去看看common这个app里面的models.py ,我们已经修改了数据定义, 你现在去产生相应的更新脚本。
执行一下,会发现在 common\migrations 目录下面出现了0001_initial.py, 这个脚本就是相应要进行的数据库操作代码。
随即,执行如下命令
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, common, contenttypes, sessions
Running migrations:
Applying common.0001_initial... OK
就真正去数据库创建表了。
用 sqlitestudio 查看数据库,发现创建了一张名为 common_customer的表,如下
其中 3 个字段就是我们上面定义的 Customer 类里面的 name、phonenumber、address 属性名。
多出来的 id 字段是该表的主键, 是Django自动帮我们添加的。
这个不需要我们在类中显式的定义。
注意
如果以后我们修改了Models.py 里面的库表的定义,都需要再次运行 python manage.py makemigrations common 和 python manage.py migrate 命令,使数据库同步该修改结果
Django Admin 管理数据
Django提供了一个管理员操作界面可以方便的 添加、修改、删除你定义的 model 表数据。
首先,我们需要创建 一个超级管理员账号。
进入到项目的根目录,执行如下命令,依次输入你要创建的管理员的 登录名、email、密码。
python manage.py createsuperuser
Username (leave blank to use 'byhy'): byhy
Email address: byhy@163.com
Password:
Password (again):
Superuser created successfully.
注意密码至少8个字符。 这里,我们设置密码为 88888888
然后我们需要修改应用里面的 管理员 配置文件 common/admin.py,注册我们定义的model类。这样Django才会知道
from django.contrib import admin
from .models import Customer
admin.site.register(Customer)
好了,现在就可以访问 http://127.0.0.1/admin/ ,输入刚才注册的用户密码登录。
登录后可以看到如下界面。这里面是目前系统中可以修改的表。
点击我们的定义的Customers表
点击下面的 ADD CUSTOMER 按钮来添加一条客户记录
在跳出的界面中输入客户信息后,点击SAVE按钮
完成上面操作后,我们使用数据库查看工具,就发现数据库中确实有了添加的数据信息。
如果你是中文的操作系统,想使用中文的admin界面,应该在配置文件 settings.py 中 MIDDLEWARE 最后加入如下配置
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware',
]
要注意上面的界面 Django 内置的给管理员使用的。只是实现了一些通用的功能,而且界面语言偏英语的。
在实际的工作项目中, 还是需要我们自己开发前端界面给他们使用。
读取数据库数据
获取全部记录
前面,我们已经创建了数据库和 Customer表。
现在我们来实现一个功能:浏览器访问 sales/customers/ ,我们的服务端就返回系统中所有的客户记录给浏览器。
我们先实现一个函数,来处理浏览器发出的URL为 sales/customers/ 的访问请求, 我们需要返回 数据库中 customer 表 所有记录。
Django 中 对数据库表的操作, 应该都通过 Model对象 实现对数据的读写,而不是通过SQL语句。
比如,这里我们要获取 customer 表 所有记录, 该表是和我们前面定义的 Customer 类管理的。
我们可以这样获取所有的表记录:
在文件sales/views.py 中,定义一个listcustomers 函数,内容如下:
from common.models import Customer
def listcustomers(request):
qs = Customer.objects.values()
retStr = ''
for customer in qs:
for name,value in customer.items():
retStr += f'{name} : {value} | '
retStr += '<br>'
return HttpResponse(retStr)
Customer.objects.values() 就会返回一个 QuerySet 对象,这个对象是Django 定义的,在这里它包含所有的Customer 表记录。
QuerySet 对象 可以使用 for 循环遍历取出里面所有的元素。每个元素 对应 一条表记录。
每条表记录元素都是一个dict对象,其中 每个元素的 key 是表字段名,value 是 该记录的字段值
上面的代码就可以将 每条记录的信息存储到字符串中 返回给 前端浏览器。
我们还需要修改路由表, 加上对 sales/customers/ url请求的 路由。
前面,我们在bysms\urls.py 主路由文件中,已经有如下的记录了
path('sales/', include('sales.urls')),
这条URL记录,指明 凡是 url 以 sales/ 开头的,都根据 sales.urls 里面的 子路由表进行路由。
我们只需修改 sales/urls.py 即可,添加如下记录
path('customers/', views.listcustomers),
大家可以使用 admin 登录, 再添加一些 客户记录。
然后可以在浏览器输入如下 网址: http://127.0.0.1/sales/customers/
回车后,浏览器显示结果类似如下
和我们数据库中的记录信息一致
过滤条件
有的时候,我们需要根据过滤条件查询部分客户信息。
比如,当用户在浏览器输入 /sales/customers/?phonenumber=13000000001 ,要求返回电话号码为 13000000001 客户记录。
我们可以通过 filter 方法加入过滤条件,修改view里面的代码,如下所示
def listcustomers(request):
qs = Customer.objects.values()
ph = request.GET.get('phonenumber', None)
if ph:
qs = qs.filter(phonenumber=ph)
retStr = ''
for customer in qs:
for name,value in customer.items():
retStr += f'{name} : {value} | '
retStr += '<br>'
return HttpResponse(retStr)
看到函数定义的参数 request了吗?
Django 框架在 url 路由匹配到函数后, 调用函数时,会传入 一个 HttpRequest 对象给参数变量 request,该对象里面 包含了请求的数据信息。
HTTP 的 Get 请求url里面的参数(术语叫 querystring 里面的参数), 可以通过 HttpRequest对象的 GET 属性获取。这是一个类似dict的对象。
比如要获取querystring里面的 phonenumber 参数 ,就可以像这样
ph = request.GET.get('phonenumber',None)
第二个参数传入 None 表示,如果没有 phonenumber 参数在 querystring中 ,就会返回 None。
然后通过调用 QuerySet 对象的filter方法,就可以把查询过滤条件加上去
qs = qs.filter(phonenumber=ph)
有了这个过滤条件,Django 会在底层执行数据库查询的SQL语句 加上相应的 where 从句,进行过滤查询。
注意,参数名 phonenumber 是和 定义的表 model 的属性名 phonenumber 一致的。
filter的过滤条件可以有多个,只要继续在后面的参数添加过滤条件即可。
比如
qs = qs.filter(phonenumber=ph,address='安徽芜湖')
这样就 除了 根据电话号码字段过滤,还有根据 地址字段过滤。
现在在浏览器输入如下 url
http://127.0.0.1/sales/customers/?phonenumber=13000000001
访问结果如下
可以发现过滤条件生效了。
前后端分离的架构
前面,我们的数据展示在界面上是这样的
很不好看。
因为我们返回的其实就是字符串,并不是HTML。
要好看一些,我们可以使用HTML来展示数据。
代码直接生成HTML
HTML本身其实也是字符串,只是这个字符串里面的内容是符合HTML语言规范的。
既然它也是字符串,我们可以使用Python直接构建出 HTML 字符串内容。
修改
html_template ='''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
table {
border-collapse: collapse;
}
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
</style>
</head>
<body>
<table>
<tr>
<th>id</th>
<th>姓名</th>
<th>电话号码</th>
<th>地址</th>
</tr>
%s
</table>
</body>
</html>
'''
def listcustomers(request):
qs = Customer.objects.values()
ph = request.GET.get('phonenumber',None)
if ph:
qs = qs.filter(phonenumber=ph)
tableContent = ''
for customer in qs:
tableContent += '<tr>'
for name,value in customer.items():
tableContent += f'<td>{value}</td>'
tableContent += '</tr>'
return HttpResponse(html_template%tableContent)
我们用一个变量 html_template 存储html模板, 然后 代码中生成html 里面需要插入的表格记录的内容,这个内容是html片段,也就是 html 表格的每行 。
最后填入到 html_template 模板里面,就产生了完整的HTML 字符串。
最后返回该 html 文档 字符串 即可。
修改后,再次访问 http://127.0.0.1/sales/customers/
得到如下内容
使用模板
上面我们是用Python代码直接拼接出html内容。
但是这种方式,我们代码处理比较麻烦。特别是,如果html里面有多处内容需要填入,使用Python代码直接拼接就显得很繁杂,不好维护。
很多后端框架都提供了一种 模板技术, 可以在html 中嵌入编程语言代码片段, 用模板引擎(就是一个专门处理HTML模板的库)来动态的生成HTML代码。
比如JavaEE 里面的JSP。
Python 中有很多这样的模板引擎 比如 jinja2 、Mako, Django也内置了一个这样的模板引擎。
我们修改一下代码,使用Django的模板引擎
html_template ='''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
table {
border-collapse: collapse;
}
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
</style>
</head>
<body>
<table>
<tr>
<th>id</th>
<th>姓名</th>
<th>电话号码</th>
<th>地址</th>
</tr>
{% for customer in customers %}
<tr>
{% for name, value in customer.items %}
<td>{{ value }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</body>
</html>
'''
from django.template import engines
django_engine = engines['django']
template = django_engine.from_string(html_template)
def listcustomers(request):
qs = Customer.objects.values()
ph = request.GET.get('phonenumber',None)
if ph:
qs = qs.filter(phonenumber=ph)
rendered = template.render({'customers':qs})
return HttpResponse(rendered)
然后,访问浏览器,可以得到一样的结果。
对比 Python直接产生 HTML,大家可以发现,使用模板引擎的好处,就是产生HTML的代码更简单方便了。
因为我们可以直接把要生成的 HTML片段 写在 HTML模板 里面。
然后,只需要传入渲染模板所需要的参数就可以了,模板引擎会自动化帮我们生成的HTML
上面只是一种模板用法的简单演示。
关于Django模板的详细用法,大家可以参考官方文档。
前后端分离架构
有了模板引擎,对我们后端开发来说,简化了程序员后端生成HTML的任务,提高了开发效率。
但是,通常后端开发人员的核心任务不是开发前端界面, 而且大部分后端开发人员对前端界面开发还是不熟悉的。
前端界面的开发还是得由前端人员去做。
如果动态的界面内容都是由后端模板生成, 就意味着前端开发人员要接触后端的模板。
比如这里,就需要他们了解Django的HTML模板。
或者需要前端人员提供他们做好的HTML, 交给后端人员,再由后端人员把它修改成Django模板。
这样有什么问题?
-
不利于前后端开发任务的分离,前后端开发人员要做额外的沟通。 -
如果前端除了web浏览器,还有手机APP的话, APP 不需要服务端返回HTML, 就得再开发一套数据接口 -
渲染任务在后端执行,大大的增加了后端的性能压力。 尤其是有的HTML页面很大, 当有大量的用户并发访问的时候, 后端渲染工作量很大,很耗费CPU 资源。
现在随着 浏览器中javascript 解释器性能的突飞猛进,以及一些前端模板库和框架的流行。很多架构师将 页面的html 内容生成 的任务放到前端。
这样 服务端就只负责提供数据, 界面的构成全部在前端(浏览器前端或者手机前端)进行,称之为前端渲染。
只是这个工作在前端执行, 使用前端的 框架库去完成,比如 Angular,React,Vue。
这样 界面完全交给前端开发人员去做, 后端开发只需要提供前端界面所需要的数据就行了。
前端 和 后端 之间的交互就完全是 业务数据了。
这样需要 定义好 前端和后端 交互数据 的接口。
目前通常这样的接口设计最普遍的就是使用 REST 风格的 API 接口。
前端通过 API 接口 从后端获取数据展示在界面上。
前端通过 API 接口 告诉后端需要更新的数据是什么。
通常 前后端的 API 接口 是由 架构师 设计的, 有时也可以由经验丰富的前端开发者、或者后端开发者设计。
接下来我们就聚焦在后端,我们的系统前端由另外的团队开发,我们只负责后端业务数据的维护
现在我们的系统,API接口 已经由架构师定义好了, 点击这里查看
我们只需要根据这个接口文档,实现后端系统的部分。
注意:需要Django返回的信息,通常都是所谓的 动态 数据信息。 比如:用户信息,药品信息,订单信息,等等。
这些信息通常都是存在数据库中,这些信息是会随着系统的使用发生变化的。
而 静态 信息,比如: 页面HTML文档、css文档、图片、视频等,是不应该由 Django 负责返回数据的。
这些数据通常都是由其他的 静态资源服务软件,比如 Nginx、Varnish等等,返回给前端。这些软件都会有效的对静态数据进行缓存,大大提高服务效率。在实际的项目中,往往还会直接使用 静态文件 云服务( OSS + CDN )提供静态数据的访问服务。
总之,Django处理并返回的应该是动态业务数据信息。
对资源的增查改删处理
前面已经说过,如果采用前后端分离的架构开发, 后端几乎不负责任何展现界面的工作,只负责对数据进行管理 。
数据的管理,主要就是:响应前端的请求, 对数据资源的 增加、修改、删除、列出 。
下面我们就以 BYSMS 系统中 customer 数据为例,看看如何进行 数据的增查改删 操作。
现在我们的系统,API接口 已经由架构师定义好了,翻看上面的项目目标下的接口文档就可以看到。
其中包括了客户数据的 增查改删 接口。
大家先仔细阅读一下这个接口文档,将来你们可能就需要根据这样的文档来实现后端的功能哦。
现在我们就根据这个接口文档,来实现后端。
创建 mgr应用目录
接口文档明确说明了,这是针对 管理员用户 的 请求。
前面我们已经为 销售员用户 专门创建了一个应用 sales 来处理相关的 请求。
所以,我们可以 再为 管理员用户 专门创建一个应用 mgr 来处理相关的 请求。 怎么创建还记得吗?
对了,执行
python manage.py startapp mgr
添加处理请求模块 和 url 路由
https://www.bilibili.com/video/av73284083/?p=13)
前面,我们都是在views.py 里面定义函数,处理 http请求的。
但是可以想象, 以后,这个mgr应用要处理很多类型的http请求。
都用这个views.py 就会让这个文件非常的庞大, 不好维护。所以,我们可以用不同的 py 文件处理不同类型的http请求。
比如,这里我们可以新增一个文件 customer.py, 专门处理 客户端对 customer 数据的操作。
将来如果客户端有对其他类型数据的操作, 比如 order 数据, 我们就可以添加 orders.py 来处理。
接下来,从接口文档,我们可以发现对资源的增删改查 操作, 都是同一个URL,都是 /api/mgr/medicine 。
而且我们发现,不同的操作请求,使用不同的 HTTP 请求方法 ,比如 添加是POST, 查询是 GET, 修改是 PUT, 删除是 DELETE。
而且请求的参数中都有 action 参数表明这次请求的操作具体是什么。
注意:Django 的 url路由功能 不支持 根据 HTTP 请求的方法 和请求体里面的参数 进行路由。
就是不能像下面这样,来根据请求 是 post 还是 get 来 路由
path('customers/', 'app.views.list_customer', method='get'),
path('customers/', 'app.views.add_customer', method='post'),
那么大家想想该怎么办?
一种方式是:自己编写一个函数, 来 根据 http请求的类型 和请求体里面的参数 分发(或者说路由)给 不同的函数进行处理。
我们可以 在 customer.py 中定义如下 dispatcher 函数
def dispatcher(request):
if request.method == 'GET':
request.params = request.GET
elif request.method in ['POST','PUT','DELETE']:
request.params = json.loads(request.body)
action = request.params['action']
if action == 'list_customer':
return listcustomers(request)
elif action == 'add_customer':
return addcustomer(request)
elif action == 'modify_customer':
return modifycustomer(request)
elif action == 'del_customer':
return deletecustomer(request)
else:
return JsonResponse({'ret': 1, 'msg': '不支持该类型http请求'})
该函数 把 请求消息中的参数统一放入到 request请求对象的params 属性中。
params 属性 被 做成一个 dict 类型 , 方便后面的处理函数来获取消息中的参数。
然后 dispatch函数再根据 请求的 类型 和 action 参数的值 决定由那个函数具体处理该请求消息。
比如 action 参数 为 ‘add_customer’ 的 请求 就由 addcustomer 函数 进行处理。
当然在文件的开头,我们需要 先导入 JsonResponse 和 json 的定义,像下面这样
from django.http import JsonResponse
import json
接下来,根据 API 接口 ,我们发现 凡是 API 请求url为 /api/mgr/customers 的,都属于 客户 相关的API, 都应该交由 我们上面定义的dispatch函数进行分派处理。
那么我们需要在Django的url路由文件中加入对应的路由。
所以,
第一步:我们应该在 总路由文件 bysms/urls.py 中定义了如下部分
path('api/mgr/', include('mgr.urls')),
第二步: 在 mgr 目录下面添加 urls.py 路由文件, 并 加入如下声明即可, 如下所示
from django.urls import path
from mgr import customer
urlpatterns = [
path('customers', customer.dispatcher),
]
这样,就表示 凡是 API 请求url为 /api/mgr/customers 的,都交由 我们上面定义的dispatch函数进行分派处理
列出客户
通常数据资源的 增查改删 里面的 查 就是 查看,对应的就是列出数据资源。
根据接口文档,列出客户数据接口,后端返回的数据格式如下
{
"ret": 0,
"retlist": [
{
"address": "江苏省常州武进市白云街44号",
"id": 1,
"name": "武进市 袁腾飞",
"phonenumber": "13886666666"
},
{
"address": "北京海淀区",
"id": 4,
"name": "北京海淀区代理 蔡国庆",
"phonenumber": "13990123456"
}
]
}
看到没有,这里我们无需 将数据库中获取的数据 转化为 供浏览器展示的HTML。
在前后端分离的开发架构中,如何展示数据,那是前端的事情。
我们后端只需要根据接口文档, 返回原始数据就行。
我们可以使用如下的函数来返回数据库的所有的 客户数据信息
def listcustomers(request):
qs = Customer.objects.values()
retlist = list(qs)
return JsonResponse({'ret': 0, 'retlist': retlist})
当然在文件的开头,我们需要 先导入 Customer 定义,像下面这样
from common.models import Customer
可以发现,无需转化数据为HTML, 后端的代码任务也大大减轻。
添加客户
通常数据资源的 增查改删 里面的 增 就是 添加,对应的就是添加数据资源。
根据接口文档,添加客户数据接口,前端提供的客户数据格式如下
{
"action":"add_customer",
"data":{
"name":"武汉市桥西医院",
"phonenumber":"13345679934",
"address":"武汉市桥西医院北路"
}
}
我们可以使用如下的函数来处理
def addcustomer(request):
info = request.params['data']
record = Customer.objects.create(name=info['name'] ,
phonenumber=info['phonenumber'] ,
address=info['address'])
return JsonResponse({'ret': 0, 'id':record.id})
Customer.objects.create 方法就可以添加一条Customer表里面的记录。
临时取消 CSRF 校验
根据接口文档,添加客户 请求是个Post请求
POST /网站名/api/mgr/signin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
注意,缺省创建的项目, Django 会启用一个 CSRF (跨站请求伪造) 安全防护机制。
在这种情况下, 所有的Post、PUT 类型的 请求都必须在HTTP请求头中携带用于校验的数据。
为了简单起见,我们先临时取消掉CSRF的 校验机制,等以后有需要再打开。
要临时取消掉CSRF的 校验机制,非常简单,只需要在 项目的配置文件 bysms/settings.py 中 MIDDLEWARE 配置项 里 注释掉 ‘django.middleware.csrf.CsrfViewMiddleware’ 即可。
如下所示
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
修改客户信息
数据资源的 增查改删 里面的 改 就是 改动,对应的就是修改数据资源。
根据接口文档,修改客户数据接口,前端提供的数据格式如下
{
"action":"modify_customer",
"id": 6,
"newdata":{
"name":"武汉市桥北医院",
"phonenumber":"13345678888",
"address":"武汉市桥北医院北路"
}
}
我们可以使用如下的函数来处理
def modifycustomer(request):
customerid = request.params['id']
newdata = request.params['newdata']
try:
customer = Customer.objects.get(id=customerid)
except Customer.DoesNotExist:
return {
'ret': 1,
'msg': f'id 为`{customerid}`的客户不存在'
}
if 'name' in newdata:
customer.name = newdata['name']
if 'phonenumber' in newdata:
customer.phonenumber = newdata['phonenumber']
if 'address' in newdata:
customer.address = newdata['address']
customer.save()
return JsonResponse({'ret': 0})
删除客户
数据资源的 增查改删 里面的 删 就是 删除,对应的就是删除数据资源。
根据接口文档,删除客户数据接口,前端只需要提供要删除的客户的ID。
数据格式如下
{
"action":"del_customer",
"id": 6
}
我们可以使用如下的函数来处理
def deletecustomer(request):
customerid = request.params['id']
try:
customer = Customer.objects.get(id=customerid)
except Customer.DoesNotExist:
return {
'ret': 1,
'msg': f'id 为`{customerid}`的客户不存在'
}
customer.delete()
return JsonResponse({'ret': 0})
测试后端代码
后端对 客户数据的增查改删已经实现了。
那么,怎么测试我们的代码是否正确呢?
有的同学说,可以等前端开发好了对接。
但是, 我们后端的开发和前端开发是并行的,要等前端开发好了,你们现在干什么?
喝茶看报纸吗?老板肯定不答应。
测试我们的代码,可以自己开发测试程序,模拟前端,发出http请求 对 后端进行测试。
这就是 Web API 接口测试了, Python做接口测试, 最合适的就是使用 requests 这个库。
点击这里参考一下白月黑羽教程requests库的使用
这里,我们只需要 使用 requests库构建 各种 接口规定的 http 请求消息, 并且检查响应。
比如,要检查 列出客户 的接口
import requests,pprint
response = requests.get('http://localhost/api/mgr/customers?action=list_customer')
pprint.pprint(response.json())
运行一下,大家看看,是不是可以返回这样的结果呢?
{'ret': 0,
'retlist': [{'address': '梁山行者武松大寨',
'id': 1,
'name': '武松',
'phonenumber': '14456789012',
'qq': '23434344'},
{'address': '梁山黑旋风大寨',
'id': 2,
'name': '李逵',
'phonenumber': '13000000001',
'qq': '234234234324'}]}
根据接口文档,ret 值为0,表示 接口调用成功。
如果retlist里面的格式符合接口规定,并且其中的数据和数据库内容一致,则测试通过
要检查添加客户的接口,如下
import requests,pprint
payload = {
"action":"add_customer",
"data":{
"name":"武汉市桥西医院",
"phonenumber":"13345679934",
"address":"武汉市桥西医院北路"
}
}
response = requests.post('http://localhost/api/mgr/customers',
json=payload)
pprint.pprint(response.json())
response = requests.get('http://localhost/api/mgr/customers?action=list_customer')
pprint.pprint(response.json())
和前端集成
最终我们的产品 前端和后端系统会集成在一起成为一个完整的系统。
部署到生产环境(生产环境就是正式的线上运营环境)运行的架构往往比较复杂。我们在后面有专门的章节讲述 一个比较完整的线上环境 如何搭建。
这里我们讲述开发环境下, 前后端分离的架构如何简单集成。
前端环境其实就是 一些前端的代码和资源文件,包括 js文件、html文件、css文件 还有 图片视频文件等。
我们模拟前端团队开发的 前端 系统 打包在这里 ,点击这里下载
下载好以后,可以解压该 z_dist.zip 文件到项目根目录下面,形成一个目录 z_dist。
该目录下面就是前端的 代码资源文件。
Django的开发环境也可以从浏览器访问这些前端的资源文件。
但是前端文件都是静态文件,需要我们配置一下Django的配置文件, 指定http请求如果访问静态文件,Django在哪个目录下查找。
注意,接下来我们配置 Django 静态文件服务, 是 开发时 使用的 一种 临时方案 ,性能很低,这是方便我们调试程序用的。
前面讲过,正式部署web服务的时候,不应该这样干,应该采用其它方法,比如Nginx等。后面的教程会有详细的讲解如何使用Nginx 和 Django 组合使用。
现在,请打开 bysms/urls.py 文件,在末尾 添加一个
+ static("/", document_root="./z_dist")
并添加如下声明
from django.conf.urls.static import static
最终,内容如下
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('sales/', include('sales.urls')),
path('api/mgr/', include('mgr.urls')),
] + static("/", document_root="./z_dist")
最后的
+ static("/", document_root="./z_dist")
就是在url 路由中加入 前端静态文件的查找路径。
这样如果 http请求的url 不是以 admin/ sales/ api/mgr/ 开头, Django 就会认为是要访问 z_dist目录下面的静态文件。
好了,现在我们 运行如下命令,启动Django 开发服务器
python manage.py runserver 80
然后我们打开浏览器,输入如下网址:
http://localhost/mgr/index.html
就会出现 管理员操作界面,如下
这是前端开发的 客户管理界面,可以在界面上进行客户的 增查改删操作, 这些操作会触发API 请求发送给我们的后端服务。
大家可以操作一下看看, 后端是否能够正确的响应。
常见问题
下载示例代码,运行后,发现页面是一片空白, 按F12发现如下错误
Refused to execute script from 'http://localhost/mgr/index.js?...'
because its MIME type ('text/plain') is not executable,
and strick MIME type checking is enabled.
那是因为你们的 Windows系统上不知道什么时候破坏了注册表,把扩展名为js的文件类型注册为 ‘text/plain’, 应该为’application/javascript’。
要解决这个问题,请在 Django项目 setting.py 中 末尾添加如下代码,强制Django使用’application/javascript’ 作为 js文件类型
import mimetypes
mimetypes.add_type('text/css', '.css')
mimetypes.add_type('application/javascript', '.js')
然后,刷新网页(按ctrl键刷新,确保不用缓存)即可。
实现登录
处理登录、登出请求
我们可以在mgr目录里面创建一个代码文件 sign_in_out.py
这个代码文件就是用来处理 管理员登录和登出 的API 请求的
大家可以思考一下, 我们的代码 该如何处理登录请求呢?
无非就把请求参数里面的用户名、密码取出来, 和数据库中记录的用户名密码进行比对。
如果和数据库中 记录的一致就认为是认证通过,否则就是不通过。
Django中有个内置app 名为 django.contrib.auth ,缺省包含在项目Installed App设置中。
这个app 的 models 定义中包含了一张 用户表,名为 auth_user 。
当我们执行 migrate 创建数据库表时,根据,就会为我们创建 用户表 auth_user,如下所示
django.contrib.auth 这个app 已经 为我们做好了登录验证功能。
我们只需要使用这个app库里面的方法就可以了。
Django的文档就给出了登录和登出代码范例,我们稍微修改一下。
我们在 mgr 目录下面, 创建一个 sign_in_out.py 文件。
在该文件中,输入如下内容
from django.http import JsonResponse
from django.contrib.auth import authenticate, login, logout
def signin( request):
userName = request.POST.get('username')
passWord = request.POST.get('password')
user = authenticate(username=userName, password=passWord)
if user is not None:
if user.is_active:
if user.is_superuser:
login(request, user)
request.session['usertype'] = 'mgr'
return JsonResponse({'ret': 0})
else:
return JsonResponse({'ret': 1, 'msg': '请使用管理员账户登录'})
else:
return JsonResponse({'ret': 0, 'msg': '用户已经被禁用'})
else:
return JsonResponse({'ret': 1, 'msg': '用户名或者密码错误'})
def signout( request):
logout(request)
return JsonResponse({'ret': 0})
创建 url路由
Bysms系统,浏览器登陆登录页面的url是 http://127.0.0.1/mgr/sign.html
但是这不是 登录 API接口的url路径。
根据接口,管理员登录的API 路径是 /api/mgr/signin
前面的课程, 我们已经在总路由文件 bysms/urls.py 中 添加了如下路由记录
path('api/mgr/', include('mgr.urls')),
现在,我们只需要 在mgr 目录下面 的子路由文件 urls.py 里添加如下内容
from django.urls import path
from mgr import sign_in_out
urlpatterns = [
path('signin', sign_in_out.signin),
path('signout', sign_in_out.signout),
]
这样就表示:
如果有HTTP请求 url是 /api/mgr/signin 就由 sign_in_out.py 里面的signin 函数处理,
如果有HTTP请求 url是 /api/mgr/signout 就由 sign_in_out.py 里面的signout 函数处理。
测试我们的代码
这样我们后端的登录请求处理的代码已经完成了。
那么,怎么测试是否正确呢?
还是可以 使用 requests库构建 登录请求http消息, 并且检查响应,看看是否能登录成功。
非常简单,如下代码即可
import requests,pprint
payload = {
'username': 'byhy',
'password': '88888888'
}
response = requests.post('http://localhost/api/mgr/signin',
data=payload)
pprint.pprint(response.json())
运行一下,大家看看,是不是可以返回这样的结果呢?
{'ret': 0}
根据接口文档,ret 值为0,表示登录接口调用成功。
Django内置用户表
前面说过,Django内置 django.contrib.auth 包含了内置用户表的定义 和 登录认证的功能。
如果我们要 扩展这个表的字段,或者实现用户记录的添加、修改功能,推荐的做法参考后面的教程这一章
session 和 token
前面我们已经根据接口文档,编写代码, 对前端发来的 Customer API请求进行处理了。
并且我们也编写了 用户登录处理的代码。
不知道大家有没有发现一个问题?
前端发来的 Customer API 请求, 我们后端代码就直接处理了, 并没有验证 这个请求是不是已经登录的管理员发出的。
如果是这样,客户端可以不需要登录,直接访问 登录后的主页, 我们编写的登录处理代码又 有什么用呢?
这就 需要我们在 处理 Customer API 请求前, 判断发出该请求的用户是否登录了。
对于请求消息的合法性验证, 通常有两种方案: session 和 token
session 方案
session 就是 会话 的意思。
session 方案 的原理如下:
-
服务端在数据库中保存一张session表。 这张表记录了一次用户登录的相关信息。 具体记录什么信息, 不同的系统各有差异, 通常 会记录 该用户的 ID 、姓名 、登录名 之类的。 Django 中 该表 名字就叫 django_session, 如下所示。 大家可以发现sessionid 通常就是 一串字符串 用来标记一个session的。 而session对应的数据在这里是加密的。 通过这张表,服务端 可以根据 session号(通常叫session ID) 查到 session 的信息数据。 -
在用户登录成功后, 服务端就在数据库session表中 中创建一条记录,记录这次会话。 也就是创建一个新的 sessionid 插入到 该表中。 同时也 放入一些 该session对应的数据到 记录的数据字段中,比如登录用户 的 信息。 然后在该登录请求的HTTP响应消息中, 的头字段 Set-Cookie 里填入 sessionid 数据。 类似这样 Set-Cookie: sessionid=6qu1cuk8cxvtf4w9rjxeppexh2izy0hh
根据http协议, 这个Set-Cookie字段的意思就是 要求前端将其中的数据存入 cookie中。 并且随后访问该服务端的时候, 在HTTP请求消息中必须带上 这些 cookie数据。 cookie 通常就是存储在客户端浏览器的一些数据。 服务端可以通过http响应消息 要求 浏览器存储 一些数据。 以后每次访问 同一个网站服务, 必须在HTTP请求中再带上 这些cookie里面的数据。 cookie数据由多个 键值对组成, 比如: sessionid=6qu1cuk8cxvtf4w9rjxeppexh2izy0hh
username=byhy
favorite=phone_laptop_watch
-
该用户的后续操作,触发的HTTP请求, 都会在请求头的Cookie字段带上前面说的sessionid 。 如下所示 Cookie: sessionid=6qu1cuk8cxvtf4w9rjxeppexh2izy0hh
服务端接受到该请求后,只需要到session表中查看是否有该 sessionid 对应的记录,这样就可以判断这个请求是否是前面已经登录的用户发出的。 如果不是,就可以拒绝服务,重定向http请求到登录页面让用户登录。
token机制
使用session机制验证用户请求的合法性 的主要缺点有两个
最近比较流行的一种token机制可以比较好的解决这些问题。
token 简单来说,就是包含了 数据信息 和 校验信息的 数据包。
Session 机制是把 数据信息(比如session表)放到 服务端,服务端数据是客户无法篡改的,从而保证验证的 可靠性。
而 token机制 数据信息 直接传给 客户端,客户每次请求再携带过来给服务端。服务端无需查找数据库,直接根据token里面的数据信息进行校验。
那么问题来了:客户数据直接发送给客户端,如果 客户端篡改了数据, 比如把自己改为 vip用户怎么办? 服务端怎么验证数据有没有被客户端篡改(术语叫完整性验证)呢?
token 机制的原理如下:
-
服务端配置一个密钥(secret key),该密钥是服务端私密保存的,不能外泄 -
在用户登录成功后, 服务端将 用户的信息数据 + 密钥 一起进行一个哈希计算, 得到一个哈希值。 注意:哈希算法保证了, 哈希值只能根据 同样的 源数据得到。 如果谁修改了用户信息, 除非他知道密钥,再次使用哈希算法才能得到 正确的新的 哈希值。 所以这个哈希值,就是用来校验数据是否被修改的. 然后将 用户数据信息 和 哈希值 一起 做成一个字节串 ,这个字节串就称之为 token 。 大家可以发现 token 里面 包含了用户数据信息 和 用于校验完整性的哈希值。 然后,服务端返回给客户的HTTP响应中 返回了这个token。 通常token是放在HTTP响应的头部中的。 具体哪个头部字段没有规定,开发者可以自行定义。 -
该用户的后续操作,触发的HTTP API请求, 会在请求消息里面 带上 token 。 具体在请求消息的什么地方 存放 token, 由开发者自己定义,通常也存放在http 请求 的头部中。 服务端接收到请求后,会根据 数据信息 和 密钥 使用哈希算法再次 生成 哈希值。 如果客户修改了数据信息, 因为他不知道密钥,没法得到正确的新的哈希值,那么 服务端根据 篡改后的数据+密钥 得到的新 哈希值一定和 保存在token中的老哈希值 不同。就知道数据被修改了。 如果客户没有修改数据,服务端 根据 原来的数据+密钥 得到的哈希值 和保存在token中原来的哈希值一致,就校验通过。 校验通过后,就确信了数据没有被修改,可以放心的使用token里面的数据 进行后续的业务逻辑处理了。 上述处理中,由于不需要服务端访问查找数据库,从而大大了提高了处理性能。
使用session验证客户端请求
Django 对session 的支持是 缺省就有的,下面我们来看看如何加入对API请求的合法性验证。
大家是否注意到,前面我们处理登录的函数中 有如下箭头所指的 一行代码
这行代码的作用 就是在登录认证后,将 用户类型保存到session数据中, 也就是存入前面数据库的那张图的 会话数据记录中。
Django 框架 会自动在 HTTP 响应消息头中 加入 类似下面的 sessionid cookie
Set-Cookie: sessionid=????????
后续的HTTP请求就会携带这个sessionid,
我们处理 URL 以 /api/mgr 开头的 API 请求 代码里面, 需要 加上一个验证逻辑。
验证请求的cookie里面是否有sessionid,并且检查session表,看看是否存在session_key为该sessionid 的一条记录,该记录的数据字典里面是否 包含了 usertype 为 mgr 的 数据。
前面实现的代码中, 这些请求都是在dispatcher入口函数处理的,我们就只需在该dispatch中进行验证。
修改 mgr/customer.py 的dispatcher 函数,在前面加上如下代码
if 'usertype' not in request.session:
return JsonResponse({
'ret': 302,
'msg': '未登录',
'redirect': '/mgr/sign.html'},
status=302)
if request.session['usertype'] != 'mgr' :
return JsonResponse({
'ret': 302,
'msg': '用户非mgr类型',
'redirect': '/mgr/sign.html'} ,
status=302)
注意request对象里面的session属性对应的就是 session记录里面的 数据。
该数据对象类似字典,所以检查是否有usertype类型为mgr的信息,就是这样写
if request.session['usertype'] != 'mgr'
数据库表的关联
后端系统开发中, 数据库设计是重中之重。
特别是前后端分离的系统, 后端的职责基本就是数据管理, 开发的代码几乎都是围绕数据操作的。
虽然,我们教程不是专门讲数据库的, 但也必须讲解常用的 数据库表 和 表之间的关系 的设计 。
目前 使用的数据库系统 主要还是 关系型数据库 。
什么是关系型数据库?就是建立在关系模型基础上的数据库。
大家耳熟能详的mysql、oracle、 sqlserver、SQLite 都是,而 mongodb、Cassandra不是
而关系型数据库,设计的一个难点就是 各种表之间的关联关系 。
常见的3种关联关系就是: 一对多 , 一对一 , 多对多
一对多
表之间 一对多 的关系,就是 外键 关联关系
比如,我们的 BYSMS 系统中, 已经定义了 客户(Customer) 这张表 。如下所示
class Customer(models.Model):
name = models.CharField(max_length=200)
phonenumber = models.CharField(max_length=200)
address = models.CharField(max_length=200)
现在我们还需要定义 药品(Medicine) 这张表,包括药品名称、编号和描述 这些信息。
这个也很简单,添加如下的类定义
class Medicine(models.Model):
name = models.CharField(max_length=200)
sn = models.CharField(max_length=200)
desc = models.CharField(max_length=200)
接下来我们要定义 订单(Order) 这张表,这个Order表 包括 创建日期、客户、药品、数量。
其中:
客户字段对应的客户 只能是 Customer 中的某个客户记录
可以说:
Order表里面 一条订单记录的客户 对应 Customer表里面的一条客户记录
而 多条 Order记录里面的客户 是可以对应 Customer 表里面 同一个客户记录的,
反过来说,就是:一个客户记录可以对应多条订单记录
这就是一对多的关系,可以用如下图片表示
像这种一对多的关系,数据库中是用 外键 来表示的。
如果一个表中 的 某个字段是外键,那就意味着 这个外键字段的记录的取值,只能是它关联表的某个记录的主键的值。
我们定义表的 Model类的时候,如果没有指定主键字段,migrate 的时候 Django 会为该Model对应的数据库表自动生成一个id字段,作为主键。
比如,我们这里,Customer、Medicine表均没有主键,但是在migrate之后,查看数据库记录就可以发现有一个id字段,且该字段是 主键 (primary key)
现在我们要定义 订单 表 Order,
其中客户字段就应该是一个外键,对应Customer表的主键,也就是id字段
Django中定义外键 的方法就是 Model类的该属性字段 值为 ForeignKey 对象,如下所示
import datetime
class Order(models.Model):
name = models.CharField(max_length=200,null=True,blank=True)
create_date = models.DateTimeField(default=datetime.datetime.now)
customer = models.ForeignKey(Customer,on_delete=models.PROTECT)
大家可以发现, customer 字段 是外键, 指向 Customer 类。 意思就是告诉Django: Order表的 customer 字段 指向 Customer表的主键 的一个外键。
另外一个参数 on_delete 指定了 当我们想 删除 外键指向的主键 记录时, 系统的行为。
比如 我们要删除客户记录, 那么 Order表中 对应这个客户的订单记录 该如何处理呢?
on_delete 不同取值对应不同的做法,常见的做法如下
-
CASCADE 删除主键记录和 相应的外键表记录。 比如,我们要删除客户张三,在删除了客户表中张三记录同时,也删除Order表中所有这个张三的订单记录 -
PROTECT 禁止删除记录。 比如,我们要删除客户张三,如果Order表中有张三的订单记录,Django系统 就会抛出ProtectedError类型的异常,当然也就禁止删除 客户记录和相关的订单记录了。 除非我们将Order表中所有张三的订单记录都先删除掉,才能删除该客户表中的张三记录。 -
SET_NULL 删除主键记录,并且将外键记录中外键字段的值置为null。 当然前提是外键字段要设置为值允许是null。 比如,我们要删除客户张三时,在删除了客户张三记录同时,会将Order表里面所有的 张三记录里面的customer字段值置为 null。 但是上面我们并没有设置 customer 字段有 null=True 的参数设置,所以,是不能取值为 SET_NULL的。
注意: 外键字段,实际在数据库表中的 字段名, 是 Django ForeignKey 定义 字段名加上后缀 _id 。
比如这里,在执行了 migrate 命令更新数据库后, customer 这个外键字段实际上在 数据库表中的字段名 是 customer_id
一对一
外键是 一对多 的关系, 也可以说是 多对一 的关系。
有的时候,表之间是 一对一 的关系。
比如,某个学校的学生 表 和学生的地址表,就形成一对一的关系,即 一条主键所在表的记录 只能对应一条 外键所在表的记录。
Django 中 用 OneToOneField 对象 实现 一对一 的关系,如下
class Student(models.Model):
name = models.CharField(max_length=200)
classname = models.CharField(max_length=200)
desc = models.CharField(max_length=200)
class ContactAddress(models.Model):
student = models.OneToOneField(Student, on_delete=models.PROTECT)
homeaddress = models.CharField(max_length=200)
phone = models.CharField(max_length=200)
Django发现这样一对一定定义,它会在migrate的时候,在数据库中定义该字段为外键的同时, 加上 unique=True 约束,表示在此表中,所有记录的该字段 取值必须唯一,不能重复。
多对多
数据库表还有一种 多对多 的关系。
在我们的 BYSMS系统中, 一个订单可以采购多种药品,就对应 Medicine表里面的多种药品;而一种药品也可以被多个订单采购, 那么Order表 和 Medicine表 之间就形成了多对多的关系。
其对应关系可以用下图来表示
Django是通过 ManyToManyField 对象 表示 多对多的关系的。
如下所示:
import datetime
class Order(models.Model):
name = models.CharField(max_length=200,null=True,blank=True)
create_date = models.DateTimeField(default=datetime.datetime.now)
customer = models.ForeignKey(Customer,on_delete=models.PROTECT)
medicines = models.ManyToManyField(Medicine, through='OrderMedicine')
class OrderMedicine(models.Model):
order = models.ForeignKey(Order, on_delete=models.PROTECT)
medicine = models.ForeignKey(Medicine, on_delete=models.PROTECT)
amount = models.PositiveIntegerField()
像这样
medicines = models.ManyToManyField(Medicine, through='OrderMedicine')
指定Order表和 Medicine 表 的多对多关系, 其实Order表中并不会产生一个 叫 medicines 的字段。
Order表和 Medicine 表 的多对多关系 是 通过另外一张表, 也就是 through 参数 指定的 OrderMedicine 表 来确定的。
migrate的时候,Django会自动产生一张新表 (这里就是 common_ordermedicine)来 实现 order 表 和 medicine 表之间的多对多的关系。
大家可以执行下面两行命令 migrate 试一下
python manage.py makemigrations common
python manage.py migrate
就会发现产生如下的一张新表 common_ordermedicine:
可以发现这张表中有 order_id 和 medicine_id 两个字段。
比如一个order表的订单id 为 1, 如果该订单中对应的药品有3种,它们的id分别 为 3,4,5。 那么就会有类似这样的这样3条记录在 common_order_medicine 表中。
order_id | medicine_id |
---|
1 | 3 | 1 | 4 | 1 | 5 |
实现代码
现在我们要实现 药品管理 和 订单管理 的服务端代码了。
药品管理
其中药品管理部分比较简单, 和前面的 customer.py 的代码 基本类似。
我们在 mgr 目录下面新建 medicine.py,处理 客户端发过来的 列出药品、添加药品、修改药品、删除药品 的请求。
如下所示
from django.http import JsonResponse
from common.models import Medicine
import json
def dispatcher(request):
if 'usertype' not in request.session:
return JsonResponse({
'ret': 302,
'msg': '未登录',
'redirect': '/mgr/sign.html'},
status=302)
if request.session['usertype'] != 'mgr':
return JsonResponse({
'ret': 302,
'msg': '用户非mgr类型',
'redirect': '/mgr/sign.html'},
status=302)
if request.method == 'GET':
request.params = request.GET
elif request.method in ['POST','PUT','DELETE']:
request.params = json.loads(request.body)
action = request.params['action']
if action == 'list_medicine':
return listmedicine(request)
elif action == 'add_medicine':
return addmedicine(request)
elif action == 'modify_medicine':
return modifymedicine(request)
elif action == 'del_medicine':
return deletemedicine(request)
else:
return JsonResponse({'ret': 1, 'msg': '不支持该类型http请求'})
def listmedicine(request):
qs = Medicine.objects.values()
retlist = list(qs)
return JsonResponse({'ret': 0, 'retlist': retlist})
def addmedicine(request):
info = request.params['data']
medicine = Medicine.objects.create(name=info['name'] ,
sn=info['sn'] ,
desc=info['desc'])
return JsonResponse({'ret': 0, 'id':medicine.id})
def modifymedicine(request):
medicineid = request.params['id']
newdata = request.params['newdata']
try:
medicine = Medicine.objects.get(id=medicineid)
except Medicine.DoesNotExist:
return {
'ret': 1,
'msg': f'id 为`{medicineid}`的药品不存在'
}
if 'name' in newdata:
medicine.name = newdata['name']
if 'sn' in newdata:
medicine.sn = newdata['sn']
if 'desc' in newdata:
medicine.desc = newdata['desc']
medicine.save()
return JsonResponse({'ret': 0})
def deletemedicine(request):
medicineid = request.params['id']
try:
medicine = Medicine.objects.get(id=medicineid)
except Medicine.DoesNotExist:
return {
'ret': 1,
'msg': f'id 为`{medicineid}`的客户不存在'
}
medicine.delete()
return JsonResponse({'ret': 0})
实现了请求处理的模块后,我们可以在 mgr\urls.py 里面加上 对 medicine 的 请求处理 路由设置
from django.urls import path
from mgr import customer,sign_in_out,medicine,order
urlpatterns = [
path('customers', customer.dispatcher),
path('medicines', medicine.dispatcher),
path('signin', sign_in_out.signin),
path('signout', sign_in_out.signout),
]
我的前端代码已经开发好了对药品的 增删改查处理, 所以可以和我们上面的代码进行集成测试了。
大家可以运行服务,在界面上操作测试一下。
ORM关联表、事务
ORM 对关联表的操作
前面我们学过 一对多,一对一,多对多,都是通过外键来实现。
接下来,我们通过一个实例演示,Django ORM 如何 操作 外键关联关系
请大家在 models.py 中定义这样的两个Model,对应两张表
class Country(models.Model):
name = models.CharField(max_length=100)
class Student(models.Model):
name = models.CharField(max_length=100)
grade = models.PositiveSmallIntegerField()
country = models.ForeignKey(Country,
on_delete=models.PROTECT)
然后,执行
python manage.py makemigrations common
python manage.py migrate
使定义生效到数据库中。
然后,命令行中执行 python manage.py shell ,直接启动Django命令行,输入代码。
先输入如下代码,创建一些数据
from common.models import *
c1 = Country.objects.create(name='中国')
c2 = Country.objects.create(name='美国')
c3 = Country.objects.create(name='法国')
Student.objects.create(name='白月', grade=1, country=c1)
Student.objects.create(name='黑羽', grade=2, country=c1)
Student.objects.create(name='大罗', grade=1, country=c1)
Student.objects.create(name='真佛', grade=2, country=c1)
Student.objects.create(name='Mike', grade=1, country=c2)
Student.objects.create(name='Gus', grade=1, country=c2)
Student.objects.create(name='White', grade=2, country=c2)
Student.objects.create(name='Napolen', grade=2, country=c3)
外键表字段访问
如果你已经获取了一个student对象,要得到他的国家名称只需这样
s1 = Student.objects.get(name='白月')
s1.country.name
外键表字段过滤
如果,我们要查找Student表中所有 一年级 学生,很简单
Student.objects.filter(grade=1).values()
如果现在,我们要查找Student表中所有 一年级中国 学生,该怎么写呢?
不能这么写:
Student.objects.filter(grade=1,country='中国')
因为,Student表中 country 并不是国家名称字符串字段,而是一个外键字段,其实是对应 Country 表中 id 字段 。
可能有的朋友会这样想:我可以先获取中国的国家id,然后再通过id去找,像这样
cn = Country.objects.get(name='中国')
Student.objects.filter(grade=1,country_id=cn.id).values()
注意外键字段的id是通过后缀 _id 获取的。
或者这样,也是可以的
cn = Country.objects.get(name='中国')
Student.objects.filter(grade=1,country=cn).values()
上面的方法,写起来麻烦一些,有两步操作。而且需要发送两次数据请求给数据库服务,性能不高。
其实,Django ORM 中,对外键关联,有更方便的语法。
可以这样写
Student.objects.filter(grade=1,country__name='中国').values()
写起来简单,一步到位,而且只需要发送一个数据库请求,性能更好。
如果返回结果只需要 学生姓名 和 国家名两个字段,可以这样指定values内容
Student.objects.filter(grade=1,country__name='中国')\
.values('name','country__name')
但是这样写有个问题:选择出来的记录中,国家名是 country__name 。 两个下划线比较怪。
有时候,前后端接口的设计者,定义好了接口格式,如果要求一定是 countryname 这样怎么办?
可以使用 annotate 方法将获取的字段值进行重命名,像下面这样
from django.db.models import F
Student.objects.annotate(
countryname=F('country__name'),
studentname=F('name')
)\
.filter(grade=1,countryname='中国').values('studentname','countryname')
外键表反向访问
如果你已经获取了一个Country对象,如何访问到所有属于这个国家的学生呢?
cn = Country.objects.get(name='中国')
cn.student_set.all()
通过 表Model名转化为小写 ,后面加上一个 _set 来获取所有的反向外键关联对象
Django还给出了一个方法,可以更直观的反映 关联关系。
在定义Model的时候,外键字段使用 related_name 参数,像这样
class Country(models.Model):
name = models.CharField(max_length=100)
class Student(models.Model):
name = models.CharField(max_length=100)
grade = models.PositiveSmallIntegerField()
country = models.ForeignKey(Country,
on_delete = models.PROTECT,
related_name='students')
就可以使用更直观的属性名,像这样
cn = Country.objects.get(name='中国')
cn.students.all()
外键表反向过滤
如果我们要获取所有 具有一年级学生 的国家名,该怎么写?
当然可以这样
country_ids = Student.objects.filter(grade=1).values_list('country', flat=True)
Country.objects.filter(id__in=country_ids).values()
但是这样同样存在 麻烦 和性能的问题。
Django ORM 可以这样写
Country.objects.filter(students__grade=1).values()
注意, 因为,我们定义表的时候,用 related_name='students' 指定了反向关联名称 students ,所以这里是 students__grade 。 使用了反向关联名字。
如果定义时,没有指定related_name, 则应该使用 表名转化为小写 ,就是这样
Country.objects.filter(student__grade=1).values()
但是,我们发现,这种方式,会有重复的记录产生,如下
<QuerySet [{'id': 1, 'name': '中国'}, {'id': 1, 'name': '中国'}, {'id': 2, 'name': '美国'}, {'id': 2, 'name': '美国'}]>
可以使用 .distinct() 去重
Country.objects.filter(students__grade=1).values().distinct()
注意:据说 .distinct() 对MySQL数据库无效,我没有来得及验证。实测 SQLite,Postgresql有效。
实现项目代码
现在,我们在 mgr 目录下面新建 order.py 处理 客户端发过来的 列出订单、添加订单 的请求。
同样,先写 dispatcher 函数,代码如下
from django.http import JsonResponse
from django.db.models import F
from django.db import IntegrityError, transaction
from common.models import Order,OrderMedicine
import json
def dispatcher(request):
if 'usertype' not in request.session:
return JsonResponse({
'ret': 302,
'msg': '未登录',
'redirect': '/mgr/sign.html'},
status=302)
if request.session['usertype'] != 'mgr':
return JsonResponse({
'ret': 302,
'msg': '用户非mgr类型',
'redirect': '/mgr/sign.html'},
status=302)
if request.method == 'GET':
request.params = request.GET
elif request.method in ['POST','PUT','DELETE']:
request.params = json.loads(request.body)
action = request.params['action']
if action == 'list_order':
return listorder(request)
elif action == 'add_order':
return addorder(request)
else:
return JsonResponse({'ret': 1, 'msg': '不支持该类型http请求'})
和以前差不多,没有什么好说的。
然后,我们在 mgr\urls.py 里面加上 对 orders 请求处理的路由
from django.urls import path
from mgr import customer,sign_in_out,medicine,order
urlpatterns = [
path('customers', customer.dispatcher),
path('medicines', medicine.dispatcher),
path('orders', order.dispatcher),
path('signin', sign_in_out.signin),
path('signout', sign_in_out.signout),
]
事务
接下来,我们添加函数 addorder,来处理 添加订单 请求。
我们添加一条订单记录,需要在2张表(Order 和 OrderMedicine )中添加记录。
这里就有个需要特别注意的地方, 两张表的插入,意味着我们要有两次数据库操作。
如果第一次插入成功, 而第二次插入失败, 就会出现 Order表中 把订单信息写了一部分,而OrderMedicine表中 该订单的信息 却没有写成功。
这是个大问题: 就会造成 这个处理 做了一半。
那么数据库中就会出现数据的不一致。术语叫 脏数据
熟悉数据库的同学就会知道, 我们应该用 数据库 的 事务 机制来解决这个问题。
把一批数据库操作放在 事务 中, 该事务中的任何一次数据库操作 失败了, 数据库系统就会让 整个事务就会发生回滚,撤销前面的操作, 数据库回滚到这事务操作之前的状态。
Django 怎么实现 事务操作呢?
这里我们可以使用 Django 的 with transaction.atomic()
代码如下
def addorder(request):
info = request.params['data']
with transaction.atomic():
new_order = Order.objects.create(name=info['name'] ,
customer_id=info['customerid'])
batch = [OrderMedicine(order_id=new_order.id,medicine_id=mid,amount=1)
for mid in info['medicineids']]
OrderMedicine.objects.bulk_create(batch)
return JsonResponse({'ret': 0,'id':new_order.id})
with transaction.atomic() 下面 缩进部分的代码,对数据库的操作,就都是在 一个事务 中进行了。
如果其中有任何一步数据操作失败了, 前面的操作都会回滚。
这就可以防止出现 前面的 Order表记录插入成功, 而后面的 订单药品 记录插入失败而导致的数据不一致现象。
大家可以发现 插入 OrderMedicine 表中的数据 可能有很多条, 如果我们循环用 py OrderMedicine.objects.create(order_id=new_order.id,medicine_id=mid,amount=1) 插入的话, 循环几次, 就会执行 几次SQL语句 插入的 数据库操作 这样性能不高。
我们可以把多条数据的插入,放在一个SQL语句中完成, 这样会大大提高性能。
方法就是使用 bulk_create, 参数是一个包含所有 该表的 Model 对象的 列表
写好后, 大家可以运行服务 , 用我们做好的前端系统添加几条 订单记录, 然后再查看一下数据库里面的数据是否正确。
ORM外键关联
接下来,我们来编写listorder 函数用来处理 列出订单请求。
根据接口文档,我们应该返回 订单记录格式,如下:
[
{
id: 1,
name: "华山医院订单001",
create_date: "2018-12-26T14:10:15.419Z",
customer_name: "华山医院",
medicines_name: "青霉素"
},
{
id: 2,
name: "华山医院订单002",
create_date: "2018-12-27T14:10:37.208Z",
customer_name: "华山医院",
medicines_name: "青霉素 | 红霉素 "
}
]
其中 ‘id’,‘name’,‘create_date’ 这些字段的内容获取很简单,order表中就有这些字段,
只需要这样写就可以了。
def listorder(request):
qs = Order.objects.values('id','name','create_date')
return JsonResponse({'ret': 0, 'retlist': newlist})
问题是:‘customer_name’ 和 ‘medicines_name’ 这两个字段的值怎么获取呢? 因为 订单对应的客户名字 和 药品的名字 都不在 Order 表中啊。
Order 这个Model 中 有 ‘customer’ 字段 , 它外键关联了 Customer 表中的一个 记录,这个记录里面 的 name字段 就是我们要取的字段。
取 外键关联的表记录的字段值,在Django中很简单,可以直接通过 外键字段 后面加 两个下划线 加 关联字段名的方式 来获取。
比如 这里我们就可以用 下面的代码来实现
def listorder(request):
qs = Order.objects\
.values(
'id','name','create_date',
'customer__name'
)
retlist = list(qs)
return JsonResponse({'ret': 0, 'retlist': retlist})
我们可以 浏览器访问一下 订单管理界面, F12 查看 浏览器抓包。
同样的道理 , 订单对应 的药品 名字段,是 多对多 关联, 也同样可以用 两个下划线 获取 关联字段的值, 如下所示:
def listorder(request):
qs = Order.objects\
.values(
'id','name','create_date',
'customer__name',
'medicines__name'
)
retlist = list(qs)
return JsonResponse({'ret': 0, 'retlist': retlist})
我们可以 浏览器访问一下 订单管理界面, F12 查看 浏览器抓包。
首先,第一个问题, 接口文档需要的名字是 ‘customer_name’ 和 ‘medicines_name’。 里面只有一个下划线, 而我们这里却产生了 两个下划线。
怎么办?
可以使用 annotate 方法将获取的字段值进行重命名,像下面这样
from django.db.models import F
def listorder(request):
qs = Order.objects\
.annotate(
customer_name=F('customer__name'),
medicines_name=F('medicines__name')
)\
.values(
'id','name','create_date',
'customer_name',
'medicines_name'
)
retlist = list(qs)
return JsonResponse({'ret': 0, 'retlist': retlist})
第二个问题,如果一个订单里面有多个药品,就会产生多条记录, 这不是我们要的。
根据接口,一个订单里面的多个药品, 用 竖线 隔开。
怎么办?
我们可以用python代码来处理,像下面这样
def listorder(request):
qs = Order.objects\
.annotate(
customer_name=F('customer__name'),
medicines_name=F('medicines__name')
)\
.values(
'id','name','create_date','customer_name','medicines_name'
)
retlist = list(qs)
newlist = []
id2order = {}
for one in retlist:
orderid = one['id']
if orderid not in id2order:
newlist.append(one)
id2order[orderid] = one
else:
id2order[orderid]['medicines_name'] += ' | ' + one['medicines_name']
return JsonResponse({'ret': 0, 'retlist': newlist})
代码优化
我们开发软件系统的时候,需要不断的反思我们代码里面是否有可以优化的地方。
优化的重点之一,就是把冗余的代码优化为可以复用的库。
大家有没有发现我们前面有很明显的冗余代码?
对了,就是那个 分发请求给不同函数处理的 dispatcher函数,
如下的3个文件中,
mgr\customer.py
mgr\medicine.py
mgr\order.py
我们可以用代码比对工具( 比如 BeyondCompare ) 比对一下,如下所示
该函数的大体代码基本类似,不同之处,只是分配给哪些函数处理。
冗余代码有什么坏处?
当你需要修改这些代码的时候,比如你需要把里面的 status=302 统一改为301,你需要每个地方都修改,非常的麻烦。
所以我们应该合并到一个库文件里面。
我们可以在项目根目录中新增lib目录,里面新建一个文件 名为 handler.py。
接下来我们要在 这个 handler.py 里面定义一个 dispatcherBase 函数,实现通用部分的代码。
我们发现 请求消息给哪个函数处理, 完全是由 请求消息里面的action参数决定的, 所以,我们可以修改下面这3个代码文件
mgr\customer.py
mgr\medicine.py
mgr\order.py
以 customer.py 为例, 我们删除原来的dispatcher函数, 在代码的最下面添加 如下的代码
from lib.handler import dispatcherBase
Action2Handler = {
'list_customer': listcustomers,
'add_customer': addcustomer,
'modify_customer': modifycustomer,
'del_customer': deletecustomer,
}
def dispatcher(request):
return dispatcherBase(request, Action2Handler)
我们定义一个 什么样的action 用什么函数 处理的一张表 Action2Handler 。
然后,dispatcher 函数可以简单到 直接调用 dispatcherBase, 并且把Action2Handler 作为参数传递给给它。
剩下的就交由 dispatcherBase 去处理了。
dispatcherBase 函数在 handler.py 里面 ,其代码如下:
def dispatcherBase(request,action2HandlerTable):
if 'usertype' not in request.session:
return JsonResponse({
'ret': 302,
'msg': '未登录',
'redirect': '/mgr/sign.html'},
status=302)
if request.session['usertype'] != 'mgr':
return JsonResponse({
'ret': 302,
'msg': '用户非mgr类型',
'redirect': '/mgr/sign.html'},
status=302)
if request.method == 'GET':
request.params = request.GET
elif request.method in ['POST','PUT','DELETE']:
request.params = json.loads(request.body)
action = request.params['action']
if action in action2HandlerTable:
handlerFunc = action2HandlerTable[action]
return handlerFunc(request)
else:
return JsonResponse({'ret': 1, 'msg': 'action参数错误'})
可以发现,大部分代码和以前相同,关键在最后
action = request.params['action']
if action in action2HandlerTable:
handlerFunc = action2HandlerTable[action]
return handlerFunc(request)
这段代码就是根据action参数的值,到 action2HandlerTable 查找出对应的 函数处理。
同样的,我们需要修改一下 medicine.py 和 order.py, 定义各自的 action2HandlerTable 表, 把原来的dispatch改为调用 dispatcherBase。
适当的数据库冗余
现在我们的 mgr/order.py 里面用 listorder 函数列出订单。
如果一个订单里面有多个药品,就会产生多条记录。
为了解决这个问题,我们不得不用python代码来处理冗余,像下面这样
def listorder(request):
qs = Order.objects\
.annotate(
customer_name=F('customer__name'),
medicines_name=F('medicines__name')
)\
.values(
'id','name','create_date','customer_name','medicines_name'
)
retlist = list(qs)
newlist = []
id2order = {}
for one in retlist:
orderid = one['id']
if orderid not in id2order:
newlist.append(one)
id2order[orderid] = one
else:
id2order[orderid]['medicines_name'] += ' | ' + one['medicines_name']
return JsonResponse({'ret': 0, 'retlist': newlist})
这样做其实有一些问题。
首先,它使得我们的代码增加了去除重复记录的处理,比较麻烦。
另外,还会带来性能问题:当用户大量登录,需要列出订单信息 的时候,服务程序从数据库获取到数据后,还得还要执行去除重复记录的任务。
那么怎样解决这个问题呢?
我们可以修改数据库表的设计, 就在 Order 表里面 直接存入 订单包含的药品信息。
这样,就不需要 从 OrderMedicine 表里面 去获取关联药品信息了,当然也不需要去除重复的代码了。
那么我们怎样在 Order 表 里面存储订单包含的药品信息呢?
这要包括 药品的ID,名称,和数量。而且订单中可能包含多种药品。
关键是:不同的订单记录 药品的数量 是 不同 的。
对于这种情况,我们通常可以使用一个字段, 里面存储 json格式的字符串,记录可变数量的数据信息。
修改 Order 表对应的类, 如下
class Order(models.Model):
name = models.CharField(max_length=200,null=True,blank=True)
create_date = models.DateTimeField(default=datetime.datetime.now)
customer = models.ForeignKey(Customer,on_delete=models.PROTECT)
medicines = models.ManyToManyField(Medicine, through='OrderMedicine')
medicinelist = models.CharField(max_length=2000,null=True,blank=True)
在最后 添加的 medicinelist 是一个字符串,里面用json格式来存储订单中的药品。像这样:
[
{"id": 1, "name": "青霉素", "amount": 20},
{"id": 2, "name": "来适可", "amount": 100}
]
其中:
id 表示药品的id,
name 表示 药品的名字,
amount 表示 药品的数量
上面的例子就表示该订单中有id 为 1 和 2 的两种药品 数量分别是 20 和 100
这个字段最大长度可达2000个字符,通常足以存储订单中的药品信息了。
修改了数据库类的定义, 别忘了migrate到数据库。 执行下面的命令
python manage.py makemigrations common
python manage.py migrate
我们发现,现在 Order表里面需要存储订单中药品的名字和数量, 而当前的接口的定义是没法满足这个需求的。
API v1.0 文档,添加消息体的格式如下:
{
"action":"add_order",
"data":{
"name":"华山医院订单002",
"customerid":3,
"medicineids":[1,2]
}
}
这里面只有药品的id,没有药品的 名称和数量。
我们在开发的时候,经常会遇到 当前的接口设计不能满足新的需求,需要修改 的情况。
这时候就要和 接口的设计者 , 以及接口对接的开发团队进行沟通, 说明为什么你需要修改接口。
讨论通过后,接口的设计者 就要修改接口文档。
我们这里,修改后就是的文档:API接口文档1.1, 点击查看
我们这样修改接口定义:
添加订单的格式为
{
"action":"add_order",
"data":{
"name":"华山医院订单002",
"customerid":3,
"medicinelist":[
{"id":16,"amount":5,"name":"环丙沙星"},
{"id":15,"amount":5,"name":"克林霉素"}
]
}
}
列出订单中,每个订单的格式为
{
"id": 2,
"name": "华山医院订单002",
"create_date": "2018-12-27T14:10:37.208Z",
"customer_name": "华山医院",
"medicinelist":[
{"id":16,"amount":5,"name":"环丙沙星"},
{"id":15,"amount":5,"name":"克林霉素"}
]
}
好的,既然接口变动了,前端的开发团队 要根据修改后的接口,修改他们的代码,保证按照新的接口实现消息格式。
想象一下,前端开发人员领命 重新修改代码去了。。。
当然,我们作为后端开发团队,也要立即动工。
修改 mgr/order.py 中 添加订单的代码,如下
def addorder(request):
info = request.params['data']
with transaction.atomic():
medicinelist = info['medicinelist']
new_order = Order.objects.create(name=info['name'],
customer_id=info['customerid'],
medicinelist=json.dumps(medicinelist,ensure_ascii=False),)
batch = [OrderMedicine(order_id=new_order.id,
medicine_id=medicine['id'],
amount=medicine['amount'])
for medicine in medicinelist]
OrderMedicine.objects.bulk_create(batch)
return JsonResponse({'ret': 0, 'id': new_order.id})
基本只有一处修改,把药品数据信息写入到 新增的medicinelist表字段中
还需要修改 列出订单函数,如下:
def listorder(request):
qs = Order.objects \
.annotate(
customer_name=F('customer__name')
)\
.values(
'id', 'name', 'create_date',
'customer_name',
'medicinelist'
)
retlist = list(qs)
return JsonResponse({'ret': 0, 'retlist': retlist})
哈哈,打完收工了。
好处很明显,列出订单的代码就比较简单了,不需要执行去重的任务。
性能也提高了, 只要查询一张表,并且不要执行去重的任务。
那么有没有什么坏处呢?大家自己先思考一下,再看后面的内容。
当然也有, 就是出现了 冗余数据 在数据库中。
订单里面订购了什么药品,我们本来记录在 OrderMedicine 表 中的, 现在我们还需要记录在 Order 表中。
这就意味着, 我们 修改 订单中关联的药品, 必须修改两张表。
可能你疑惑了, 既然 修改 的时候需要更新2张表,那么这也会造成性能的降低啊? 凭什么你说 这种方案比原来的性能好呢?
这就要看这张表 最频繁的操作是什么, 是查询还是更新订单中的药品。
我们订单数据要被大量的用户查看, 而更新订单中 关联的 药品 的情况相对较少, 所以权衡利弊后,采用冗余数据的方案。
可能你还有一个疑问, 我们能不能就只在 Order 表中记录 订单信息,不需要 OrderMedicine 表 中记录呢。
这样不是更简单了吗?
这样做,最大的问题是, 如果以后我们需要统计药品被采购的信息就非常麻烦了。
因为,现在我们在数据库中存储的订单中的药品信息,是以字符串类型存储的 json信息,不能直接使用SQL语句进行过滤查询,只能把所有的记录读取出来进行分析,那样性能会非常低。
新版本的 PostGresql 和 Mysql 提供了json格式的字段 ( 但是目前 Django 只支持 PostGresql 里面的json字段 ),可以部分解决这个问题,感兴趣的朋友可以自行研究一下。
好的,我们的代码完成了,大家可以先使用 requests构建前端请求,自己测试一下。
等待前端开发人员也修改好前端代码,就可以集成在一起发布了 :)
分页和过滤
现在我们的系统在列出,药品、客户、订单等数据的时候,都是全部用一张表 在一页里面显示全部内容 。
如下图所示,
聪明的读者,你来想想这样做有什么问题呢?
对了,如果我们有大量的数据, 比如系统中存储10万种药品,这个表将会非常的长,需要后端程序从数据库中读取大量的数据,并且传递给前端。
而用户通常只需要看其中的一点点数据。这是非常大的性能浪费。
怎么解决这个问题?大家只要看看淘宝、京东这些购物网站就知道了,
方案是 分页 和 过滤 。
分页 就是 每次只读取一页的信息,返回给前端。
过滤 就是 根据用户的提供的筛选条件,只读取符合条件的部分信息。
分页
先看分页的实现。
既然要分页,那么前端发送的请求中需要携带 两个信息: 每页包含多少条记录 和 需要获取第几页
我们定义列出数据请求中 添加 2个url 参数: pagesize 和 pagenum 分别对应这两个信息。
为了实现分页 和 过滤 ,接口也做了相应的修改,修改后就是的文档:API接口文档1.2, 点击查看
Django提供了对分页的支持,具体的信息,大家可以点击这里查看其官方文档
以列出药品的代码为例, 我们可以修改 listmedicine 函数,如下
from django.core.paginator import Paginator, EmptyPage
def listmedicine(request):
try:
qs = Medicine.objects.values()
pagenum = request.params['pagenum']
pagesize = request.params['pagesize']
qs = Medicine.objects.values()
pgnt = Paginator(qs, pagesize)
page = pgnt.page(pagenum)
retlist = list(page)
return JsonResponse({'ret': 0, 'retlist': retlist,'total': pgnt.count})
except EmptyPage:
return JsonResponse({'ret': 0, 'retlist': [], 'total': 0})
except:
return JsonResponse({'ret': 2, 'msg': f'未知错误\n{traceback.format_exc()}'})
注意,我们返回的信息,包括一页的数据, 还需要告诉前端, 符合条件的 总共有多少条记录 。为什么?
因为这样,前端可以计算出, 总共有多少页,从而正确的显示出分页界面,如下所示
这行代码 创建了 分页对象,在初始化参数里面设定每页多少条记录
pgnt = Paginator(qs, pagesize)
返回的 分页对象 赋值给变量 pgnt。
然后:
一页的数据 就可以通过 pgnt.page(pagenum) 获取。
而总共有多少页,通过 pgnt.count 得到。
好的,我们的代码完成了,大家可以先使用 requests构建前端请求,自己测试一下。
测试代码可以是这样
import requests,pprint
payload = {
'username': 'byhy',
'password': '88888888'
}
response = requests.post("http://localhost/api/mgr/signin",
data=payload)
retDict = response.json()
sessionid = response.cookies['sessionid']
payload = {
'action': 'list_medicine',
'pagenum': 1,
'pagesize' : 3
}
response = requests.get('http://localhost/api/mgr/medicines',
params=payload,
cookies={'sessionid': sessionid})
pprint.pprint(response.json())
我们指定了 每页3条记录,获取第一页
返回结果类似下面这样
{'ret': 0,
'retlist': [{'desc': '青霉素注射剂', 'id': 1, 'name': '青霉素', 'sn': 'sn345556235'},
{'desc': '来适可盒装', 'id': 2, 'name': '来适可', 'sn': 'sn886839452'},
{'desc': '盐酸盐片剂、胶囊', 'id': 3,'name': '环丙沙星','sn': 'Ciprofloxacin'}
],
'total': 15}
可以发现,药品总数有 15条记录,按照每页3条记录的话,返回的第一页内容就是retlist里面的信息。
大家可以修改测试代码,获取第2页的内容,对比查看一下。
过滤
我们再看过滤如何实现。
过滤 就是 根据用户的提供的筛选条件,只读取符合条件的部分信息。
比如,列出药品,需要根据 药品描述 中包含的关键字来 查询 。
而且用户可能会输入多个关键字, 比如 乳酸 和 注射液 。
这就有一个问题, 多个关键字查询 是 且 的关系 还是 或 的关系。 前者要求 药品描述 同时包含 多个关键字, 后者只需 包含其中任意一个关键字即可。
我们这里 先以 且 的关系 为例。
多条件 且关系
首先,我们需要在 列出药品的请求消息里面 添加一个参数 保存关键字信息。我们这里使用 keywords 参数。
里面包含的多个关键字之间用 空格 分开。
查询过滤条件,前面我们学过,可以通过 QuerySet 对象的 filter方法, 比如
qs.filter(name__contains='乳酸')
注意,上面的 name__contains='乳酸' 表示 name 字段包含乳酸这个关键字。
Django执行该代码是,会转换为下面的SQL条件从句到数据库进行查询
WHERE name LIKE '%乳酸%'
如果有 多个 过滤条件, 可以继续在后面调用filter方法,比如
qs.filter(name__contains='乳酸',name__contains='注射液')
就等价于下面的 SQL条件从句
WHERE name LIKE '%乳酸%' AND name LIKE '%注射液%'
多个 过滤条件, 也可以多个filter方法链在一起,比如
qs.filter(name__contains='乳酸').filter(name__contains='注射液')
通常在同一张表中过滤,这两种写法结果一样。
但是如果 过滤涉及到关联表,结果可能会不同,感兴趣的请点击这里,查看这篇文章
这个帖子的讲解也比较直观
多条件 或关系
上面两种写法,都是表示一种 AND 关系, 也就是要同时满足这些条件。
如果我们想表示的是 OR 的关系该怎么办呢?
这时候,可以使用 Django 里面提供 的 Q 对象 。
参考 官方文档 https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects
了解 Q对象的详细用法
Q 对象 的初始化参数里面 携带 和 filter 语法一致的 条件,比如
from django.db.models import Q
qs.filter(Q(name__contains='乳酸'))
如果我们查询的多个过滤条件是 或 的关系,就用 竖线 | 符号 连接多个Q对象,比如
qs.filter( Q(name__contains='乳酸') | Q(name__contains='注射液'))
等价于 下面的 SQL条件从句
WHERE name LIKE '%乳酸%' OR name LIKE '%注射液%'
如果我们查询的多个过滤条件是 且 的关系,也可以用 & 符号 连接多个Q对象,比如
qs.filter( Q(name__contains='乳酸') & Q(name__contains='注射液'))
等价于 下面的 SQL条件从句
WHERE name LIKE '%乳酸%' AND name LIKE '%注射液%'
和前面且的写法 效果相同。
利用 Q 的写法,有个独特的用途,就是过滤条件是运行时动态获取时,没法预先像下面这样写在代码中
qs.filter(name__contains='乳酸',name__contains='注射液')
这时,可以使用Q ,后面会讲到。
过滤类型
除了 等于、包含 这两种过滤类型, 还有 值大于、值小于、值在(列表中)等等。
还有对 过滤值的一些 特殊处理,比如 购买时间的 年、月、日 部分在某个范围内 等等,这些底层都是对应SQL的一些内置函数。
怎么实现这些过滤呢?
详情请参考官方文档这里
应用到项目代码
了解了上的知识,那么我们继续修改 listmedicine 函数,如下所示
def listmedicine(request):
try:
qs = Medicine.objects.values().order_by('-id')
keywords = request.params.get('keywords',None)
if keywords:
conditions = [Q(name__contains=one) for one in keywords.split(' ') if one]
query = Q()
for condition in conditions:
query &= condition
qs = qs.filter(query)
pagenum = request.params['pagenum']
pagesize = request.params['pagesize']
pgnt = Paginator(qs, pagesize)
page = pgnt.page(pagenum)
retlist = list(page)
return JsonResponse({'ret': 0, 'retlist': retlist,'total': pgnt.count})
except EmptyPage:
return JsonResponse({'ret': 0, 'retlist': [], 'total': 0})
except:
return JsonResponse({'ret': 2, 'msg': f'未知错误\n{traceback.format_exc()}'})
其中 下面这段代码
query = Q()
for condition in conditions:
query &= condition
qs = qs.filter(query)
我们先构建一个空Q对象 , 表示没有任何过滤条件, 然后 循环取出过滤关键字, 使用 & 叠加过滤条件。
最后 query 就是 多个 过滤条件 同时满足 的 Q 对象
如果我们想 构建 或 的关系, 就应该这样
query |= condition
好的,我们可以修改测试代码,加上过滤条件,再测试一下
import requests,pprint
payload = {
'username': 'byhy',
'password': '88888888'
}
response = requests.post("http://localhost/api/mgr/signin",
data=payload)
retDict = response.json()
sessionid = response.cookies['sessionid']
payload = {
'action': 'list_medicine',
'pagenum': 1,
'pagesize' : 3,
'keywords' : '乳酸 注射液'
}
response = requests.get('http://localhost/api/mgr/medicines',
params=payload,
cookies={'sessionid': sessionid})
pprint.pprint(response.json())
这里我们加上 删除订单的代码,如下
def deleteorder(request):
oid = request.params['id']
try:
one = Order.objects.get(id=oid)
with transaction.atomic():
OrderMedicine.objects.filter(order_id=oid).delete()
one.delete()
return JsonResponse({'ret': 0, 'id': oid})
except Order.DoesNotExist:
return JsonResponse({
'ret': 1,
'msg': f'id 为`{oid}`的订单不存在'
})
except:
err = traceback.format_exc()
return JsonResponse({'ret': 1, 'msg': err})
部署
我们前面的代码都是在我们自己的电脑(通常是Windows操作系统)上面运行的,因为我们还处于开发过程中。
当我们完成一个阶段的开发任务后,就需要把我们开发的网站服务,给真正的用户使用了。
那就需要我们的 网站 部署在公网机器上,而不是我们的个人电脑。 这个给真正用户使用的网站服务器我们通常称之为 生产环境
通常,我们的web服务是部署在云服务厂商的云主机上,比如阿里云的ECS云主机。当然如果你们公司有自己的机房和公网服务器,当然也可以。
现在的web服务,基本都是采用 Linux 操作系统。 而且生产环境不应该使用 SQLite 数据库,通常是 MySQL、Postgresql、Oracle等。
本章内容就是教大家,如何把基于Django的web系统部署到生产环境,也就是 如何 在Linux操作系统上安装 我们 网站系统,包括我们的代码和MySQL数据库服务。
首先,需要大家有 Linux 和MySQL的 基础知识。
架构说明
一个大型的网站系统,架构通常非常的复杂,包括很多的功能节点。大家可以以后慢慢学习。
当前,我们先从基础的架构学起。
可能你会说,我们前面已经把网站运行起来了呀,现在只需要把系统从 Windows 转移到Linux上了,把SQLite 改为 MySQL就行了吧?
不仅如此,认真学习课程的同学应该记得,我们前面曾经讲过如下两点:
-
Django 在生产环境不应该处理静态资源(比如网页、图片等)的请求 前面是开发阶段,为了是环境搭建容易,我们还是让Django来处理静态资源的请求了。 在生产环境不能这样做,这里我们使用Nginx来处理静态资源的请求。 -
Django 在生产环境 不能直接处理 HTTP请求 Django是 wsgi web application 的框架,它只有一个简单的单线程 wsgi web server。 供调试时使用。性能很低。 在生产环境必须提供 专业的 wsgi web server,比如 uWSGI 或者 Gunicorn。 我们这里使用 Gunicorn。 而且即使有了 uWSGI 或者 Gunicorn,我们最好还要在前面设置 Nginx 。所有的客户请求由它先接受,再进行相应的转发。 为什么要这样? Nginx 在整个后端的最前方, 可以实现 负载均衡、反向代理、请求缓存、响应缓存 、负荷控制等等一系列功能。可以大大的提高整个后端的性能和稳定性。
综上, 我们当前这个简单网站,其架构图如下
这里为了简单,把整个后端系统都部署在同一台Linux主机上,包括:Nginx、Gunicorn、Django(包括我们的代码)、MySQL服务。
在实际项目中,如果系统的负荷比较大,通常是部署在多台主机上。
这个架构的各个子系统是如何协同工作的?
-
Nginx Nginx 运行起来是多个进程,接收从客户端(通常是浏览器或者手机APP)发过来的请求。 它会 根据请求的URL 进行判断: 如果请求的 是 静态资源,比如HTML文档、图片等,它直接从配置的路径进行读取,返回内容给客户端。 如果请求的 是 动态数据, 转发给 Gunicorn+Django 进行处理 -
Gunicorn/Django Gunicorn 和 Django 是运行在同一个 Python进程里面的。 它们都是用Python代码写的程序。 启动Gunicorn的时候,它会根据配置加载Django的入口模块,这个入口模块里面提供了WSGI接口。 当 Gunicorn 接收到 Nginx 转发的 HTTP请求后,就会调用 Django的 WSGI入口函数,将请求给Django进行处理。 Django框架 再根据请求的URL 和 我们项目配置的 URL 路由表,找到我们编写的对应的消息处理函数进行处理。 我们编写的消息处理函数,就是前面章节大家学习到的,处理前端请求。如果需要读写数据库,就从MySQL数据库读写数据。
安装步骤
通常,我们的web服务是部署在云服务厂商的云主机上,比如阿里云的ECS云主机,或者企业的IT机房。
这里大家练习的时候,可以先用一台安装了Linux的虚拟机。
我们这里以一台安装了 Ubuntu 20 的虚拟机为例,给大家演示如何安装部署环境。
大家可以点击这里,根据此教程 先安装好 Ubuntu
安装好后, 为了方便后续apt安装其它软件时,能从国内的apt源高速下载,需要设置一下apt源,具体操作参考我们这篇教程有详细讲解
做好产品发布包
以后当你负责产品发布,你需要准备发布的产品包。
产品的发布包,是不是只是需要把你的代码 用zip打个包?
不是那样简单的。
产品往往会 涉及到好多子系统,前端通常包括 web前端、app前端, 后端 包括 业务处理系统、数据库系统、消息队列、异步任务系统、缓存系统等等。
为了保证这些子系统能在生产环境 友好配合 , 需要仔细的规划、配置、产生发布包。
我们先从基本的做起,我们现在的系统 包括 web前端系统(包括web前端的HTML、css、图片、js业务代码、js库等文件)、后端业务处理系统、数据库系统。
需要做到产品发布包里面的 包括 web前端系统 和 业务处理系统 的代码。
不同的运营架构,部署的方式不同,需要构建发布包的方式也不同。
这里,根据我们的架构图,可以把 前端系统代码 做在一个发布包中, 后端系统做在另一个发布包中。
我们完全可以 把 前后端系统 分别部署到 两台 Linux主机上。当有请求需要Django后端业务系统处理的时候,转发给Django所在的主机即可。 如果请求只是获取一些静态资源,比如HTML、图片等,在前端主机处理完即可。 这样 做到 部署的前后端分离。
目前我们先按照简单的来, 根据我们的架构图, 都部署在同一台机器上。
首先,我们需要为当前版本的发布,准备 web前端发布包,和web后端发布包。
现在我们假定发布的 版本号为 1.5
前端发布包,由前端开发人员提供,在如下百度网盘中的 bysms_front_v1.5.zip,大家先下载到本地
百度网盘链接:https://pan.baidu.com/s/1nUyxvq6IYykBNtPUf4Ho6w
提取码:9w2u
作为后端的开发人员,当然由你提供,后端的发布包。
基于Django开发的后端系统,要发布正式版本:
- 首先拷贝你的开发项目目录到一个新的目录中,可以改名为
bysms_back_v1.5
修改其中 bysms/settings.py ,把下面的 配置项DEBUG值为 False
DEBUG = True
前面我们为了开发简单,一直用的SQLite数据库,现在需要改为生产环境的MySQL数据库。
按照如下示例,修改 bysms/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'bysms',
'USER': 'byhy',
'PASSWORD': 'Mima123$',
'HOST': '127.0.0.1',
'PORT': '3306',
'CONN_MAX_AGE': 0
}
}
当然,上面配置的 MySQL连接的 用户名、密码、数据库名、数据库服务主机名、端口 都要和你的环境匹配。
生产环境,我们使用 Gunicorn 作为 Django的WSGI前端,首先我们需要创建一个 Gunicorn启动配置文件 ./bysms/gunicorn_conf.py ,内容如下
bind = '127.0.0.1:8000'
workers = 3
worker_class = "gevent"
errorlog = "/home/byhy/gunicorn.log"
loglevel = "info"
import sys,os
cwd = os.getcwd()
sys.path.append(cwd)
要保证我们的Django后端服务在linux上一个命令就能启动,需要开发一个 Linux 启动shell脚本 ./run.sh 。
可以参考下面的 shell脚本内容
#!/bin/bash
DIR="$( cd "$( dirname "$0" )" && pwd )"
echo $DIR
cd $DIR
nohup gunicorn --config=bysms/gunicorn_conf.py bysms.wsgi &> /dev/null &
VERSION = '1.5'
然后,删除 所有app 的 migrations 目录。
最好把整个Django后端的代码打包,包名为 bysms_back_v1.5.zip
安装、配置 Nginx
大家首先以root 用户 登录Ubuntu主机,
执行命令 apt install nginx 安装好 Nginx
接下来我们需要配置Nginx。
按照上面的安装方式,Nginx的配置文件路径是: /etc/nginx/nginx.conf
当然我们可以使用vim去编辑这个文件,但是建议大家使用 winscp 连接 Linux主机并且,配置用notepad++远程打开。因为这样看起来更清楚,特别是配置文件中如果有中文,vi看起来可能会比较乱。
打开 Nginx 配置文件 /etc/nginx/nginx.conf ,修改其中的配置项,以满足你的网站需求。
下面是一个Nginx配置示例,列出了其中核心的配置
user byhy;
worker_processes 2;
events {
worker_connections 1024;
}
worker_rlimit_nofile 2000;
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 30;
gzip on;
upstream apiserver {
keepalive 20;
server 127.0.0.1:8000;
}
server {
server_name www.byhy.com;
root /home/byhy/bysms_frontend/z_dist;
location /api/ {
proxy_pass http://apiserver;
proxy_set_header Host $host;
}
}
}
修改好配置后,必须重启Nginx,可以执行命令
systemctl restart nginx
如果启动报错, 可以打开 /var/log/nginx/error.log 查看nginx的错误日志文件。
为了使http服务的 80端口可以从外部访问,需要我们让防火墙开放80端口。
对于Ubuntu 20 来说,就是执行命令
ufw allow 80
或者 ,下面的命令也行
ufw allow 'Nginx HTTP'
安装 Django
比较简单,执行如下命令
apt install python3-pip
pip3 install Django -i https://pypi.douban.com/simple/
安装 Gunicorn
执行下面的命令安装 Gunicorn 和 它依赖的库 gevent 和 greenlet (异步模式需要)
pip3 install greenlet -i https://pypi.douban.com/simple/
pip3 install gevent
pip3 install gunicorn
安装 MySQL,创建数据库和用户
MySQL在Ubuntu上的安装和初始设置 点击这里参考我们的教程
注意:教程中有 修改 mysql 绑定所有网络接口的设置, 如果你的机器是生产环境,通常不要这样做。
安装 好MySQL 服务后,执行 mysql 启动客户端 :
- 先使用root用户创建数据库bysms,指定使用utf8的缺省字符编码,执行命令如下
CREATE DATABASE bysms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
- 再创建 bysms系统用来连接数据库的用户,保证该用户有访问 bysms数据库的权限,比如
CREATE USER 'byhy'@'localhost' IDENTIFIED BY 'Mima123$';
CREATE USER 'byhy'@'%' IDENTIFIED BY 'Mima123$';
随后输入如下命令,赋予byhy 用户所有权限,就是可以 该DBMS系统上 访问所有数据库里面所有的表
GRANT ALL ON *.* TO 'byhy'@'localhost';
GRANT ALL ON *.* TO 'byhy'@'%';
安装 MySQL 客户端库
执行 下面两个命令分别安装 MySQL 客户端开发库 和 Python 绑定库 mysqlclient
apt install libmysqlclient-dev
pip3 install mysqlclient -i https://pypi.douban.com/simple/
创建产品运行用户
通常我们需要为运行产品进程,比如 Nginx work进程、Gunicorn 等,创建一个专门的用户。
这里我们使用byhy用户
执行命令创建用户 adduser byhy 。
安装产品发布包
以root以后登录,执行如下命令安装工具:dos2unix 、unzip
apt install dos2unix
apt install unzip
然后再以 byhy用户 登录Linux 主机,下载拷贝 前端、后端 发布包 bysms_front_v1.5.zip 和 bysms_back_v1.5.zip 到 byhy 用户home目录下面,也就是 /home/byhy 。
实际项目中,发布包如果不能直接wget下载,可以使用 winscp 拷贝到 Linux 主机上。
然后执行下面的命令进行解压。
unzip bysms_front_v1.5.zip
unzip bysms_back_v1.5.zip
为了让Django认为你使用的虚拟机的IP地址或者域名是允许使用的, 需要修改settings.py 里面的配置项ALLOWED_HOSTS,加上一个你当前虚拟机的IP,也可以使用 '*' , 表示所有IP都可以。
ALLOWED_HOSTS = ['*','localhost','127.0.0.1']
然后进入到 目录 bysms_back_v1.5 中,
执行命令,让启动脚本符合linux文本格式,并且有可执行权限
dos2unix run.sh
chmod +x run.sh
创建数据库表
执行下面的命令, 让Django 在数据库中 创建 你的系统所需要的表
python3 manage.py makemigrations <your_app_label>
python3 manage.py migrate
注意, <your_app_label> 需要替换成你的 Django 项目中的 app (只需要写包含了数据库表定义的App)的名字,可以是多个app,中间用空格隔开
开始我们要创建数据库的业务管理员账号,进入到manage.py所在目录,执行如下命令,
python3 manage.py createsuperuser
依次输入你要创建的管理员的 登录名、email、密码。
Username (leave blank to use 'byhy'): byhy
Email address: byhy@163.com
Password:
Password (again):
Superuser created successfully.
启动 Gunicorn/Django
进入到 byhy 用户home目录,执行命令 run.sh
然后,执行命令 ps -ef | grep python |grep gunicorn_conf |grep -v grep 查看 是否启动成功。
自动化部署
上面的过程是不是很麻烦?
一个成熟的产品团队,通常可以开发自动化部署软件。可以一键部署,大大提高效率。
具备了Python开发能力的你,自己就可以尝试开发 自动化部署程序哦。
HTTPS 服务
这里,我们的服务运行在80端口上,是不加密的网站服务。
以后,为了安全考虑,需要运行在HTTPS协议上。
这就需要我们申请证书,并且配置Nginx 使用HTTPS。
|