本文内容为工作交接时为同事所写的文档部分摘录,也适用于新接触django的童鞋。建议直接将本文拷贝下来放在Notepad++中以python语言打开使用;
文章尽量详细的介绍了流程与实现,更精彩的体验由你以后自己创造.
一、环境:
1、python的安装:
windows下安装: https://www.python.org/ftp/python/3.8.0/python-3.8.0-amd64.exe
linux(centos7)下安装: yum install python3
2、django及其相关库的安装:
pip install django==1.11.11 # 需要什么版本都随意,注意区别就行,最近出了django3了,支持了异步操作,性能进一步提升了
... PyMySQL
... django-crontab
... djangorestframework
... djangorestframework-jwt
有需要新的库就加
3、一键导入导出python环境中的第三方包:
导出: pip freeze > requirements.txt
导出: pip install -r requirements.txt
二、django基础
1、创建一个名为test_django的django项目:
· 在cmd中执行命令: django-admin startproject test_django
便会在当前文件夹中生成test_django项目。
打开这个项目会发现其根目录名为 test_django,其下有一个与之同名的文件夹和一个名为manage.py的文件
# 项目目录结构解释:
· test_django/ # 项目根目录【后文所述的根目录专指当前文件夹】
|
|___· manage.py # 用于整个项目管理的命令行工具,如启动项目、操作数据库等
|___· test_django/ # 整个项目的python包【后文所述的test_django专指该文件夹】
|
|___·__init__.py # 标志其上级文件夹为一个python包,其内部也将会用于写一些设置
|___·settings.py # 用于整个项目的配置
|___·urls.py # 根路由,用于不同模块(子应用)的路由分发
|___·wsgi.py # 项目与wsgi兼容的接口
· 为当前项目设置python的解释器:
File -> settings -> Projiet: test_django -> Project Interpreter 选择.../python.exe后点击ok即可
2、检查这个项目是否创建成功:
· 在根目录下执行命令:python manage.py runserver
然后打开http://127.0.0.1:8000/页面,出现It worked!字样表示项目创建成功。
· 根目录下会出现一个db.sqlite3,不用管,后面删除,因为不用这个数据库
3、为项目创建子应用(功能模块:如登录/注册、个人中心、视频等):
· 因为一个项目会有多个模块,若都放在根目录下会显得项目乱,因此在根目录下创建一个名为apps的Python Package文件夹统一管理所有的子应用
· 在apps目录下创建一个名为market的子应用: python ../manage.py startapp market # 若还需创建其他子应用,则按照该方式即可
然后在apps下面就会出现一个名为market的文件夹。
# 子应用market目录结构解释:
· market/
|
|___· migrations/ # 当前子应用的所有将要通过代码对数据表进行的操作都会被记录到这个文件夹中,在确认要操作数据表时会根据其中的记录文件进行数据表操作
|___· __init__.py # 标志其上级文件夹为一个python包,一般不用管
|___· admin.py # 用于后台管理时将数据表对应的模型类注册到django自带的后台管理系统
|___· apps.py # 内部自动生成设置类,表示其子应用的名字,默认和文件夹名字相同,用于将该子应用注册到django系统
|___· models.py # 当前子应用所涉及的全部的数据表都通过该文件中的类去关联,实现以面向对象的方式关联数据库,相当于MVC中的Model
|___· tests.py # 单元测试使用,不管它
|___· views.py # 当前子应用的功能都会通过其内部的代码实现,负责将前端连接前端的请求与处理业务逻辑并返回响应,相当于MVC中的Controller
以上是项目自动创建的目录结构,随着项目的进行,将来还会向里面新增其他的文件。
· 当前子应用创建完毕之后,django并不知道项目产生了一个名为market的子应用,所以需要将其注册给系统识别:
· 在setting.py文件中的添加如下代码,表示把apps添加到系统路径:
import sys
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
· 在setting.py的INSTALLED_APPS中添加如下代码,表示把子应用market注册给django系统:
'market.apps.MarketConfig',
· 鼠标放在apps上,右键选择Make Directory as -> Sources Root, 这个文件夹会变蓝。表示以后导入子应用的时候不需要通过apps这一层文件夹,就好像没有这个文件夹一样
子应用创建并注册完成之后,就要创建模型类实现生成对应的数据表
4、models与数据库的表:
· django通过使用类与表的关系映射实现用代码操作数据库,在modles.py中的class类对应数据库的表,
类属性对应数据表的字段,类属性类型对应数据表的字段类型,类属性的部分选项对应数据表字段的某些约束条件
· 首先生成一个名为db_test_django的空数据库:
在mysql中执行: create database db_test_django charset=utf8;
· django若要操作数据库,首先要将setting.py文件中的DATABASES配置改为mysql数据库, 如下:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '127.0.0.1', # 数据库主机
'PORT': 3306, # 端口
'USER': 'root', # 用户名
'PASSWORD': 'root', # 用户密码
'NAME': 'db_test_django' # 数据库名
}
}
顺便把db.sqlite3文件删除
· 在test_django的目录下找到__init__.py文件,加上:
# 表示当前项目要使用mysql数据库
import pymysql
pymysql.install_as_MySQLdb()
· 以上配置完成后,使用django的模型类在数据库db_test_django中创建一个名为cellphone_brand的数据表:
· 在子应用market的models.py文件中进行编写如下代码:
from django.db import models
'''手机品牌模型类'''
class CellphoneBrand(models.Model):
brand_name= models.CharField(max_length=15, verbose_name='品牌名称')
class Meta:
db_table = 'cellphone_brand'
verbose_name = '手机品牌'
verbose_name_plural = verbose_name
def __str__(self):
return self.brand_name
# 上述代码解释:
上述class类CellphoneBrand就是以代码的形式表示数据库中的一个表;
其类属性brand_name表示该数据表中的一个字段:brand_name;
字段类型models.CharField表示该字段的数据类型,对应着数据库的varchar类型;
字段选项max_length表示这个字段的长度最大为15,注意CharField必须要声明这个选项;
字段选项verbose_name表示该字段在admin后台中将要显示的字段名,可随意起;
内嵌类 class Meta, 主要目的是给上级类添加一些额外功能(当然这个内嵌类可以不需要),具体功能由其里面的属性决定,:
db_table可指定其上级类对应的数据表名称;
verbose_name可指定在admin中显示的数据表名字,注意区别于字段选项的verbose_name
verbose_name_plural表示其显示复数的名字,在中国不用理这个
__str__方法用来在shell中测试时,以该字段作为查询结果的直观信息展示(可不加)
* 其他字段选项和字段类型后面遇到再说
· 以上是编写数据表对应的模型类,此时,数据库mall仍然是个空库,现在让上面创建的模型类去数据库中生成名为cellphone_brand的数据表:
在主目录下执行:
python manage.py makemigrations # 生成数据库迁移文件,这会在子应用market的migrations文件夹下生成迁移记录文件0001_xxx.py,且数据库中会生成一个django_migrations的空表
python manage.py migrate # 生成数据表,这会在数据库中生成与class类对应的所有数据表,若是第一次执行该命令,还会生成系统默认的一些数据表,但目前只需要关心由class类生成的数据表即可
* 至此,通过python的类创建一个数据表的操作就完成了
5、url路由与视图:
django通过当前项目的test_django文件夹中的urls.py文件进行路由的分发,前端发送的url请求会先进入到这里,然后在urlpatterns列表中从上往下进行路由匹配,
根据前端的url的不同,将路由转给不同的子应用,在具体的子应用中,又会根据剩下的路由部分去匹配不同的视图函数,将请求转给具体的视图函数以实现具体功能。
· 在当前项目的test_django文件夹中的urls.py的urlpatterns中添加一个url的路由匹配:
url(r'^market/', include('market.urls'))
这表示当前端请求的url是以market开头的,则被转发到market.urls中去
· 此时的market子应用中还没有名为urls.py的文件,那就新建一个urls.py,其内部代码如下:
from django.conf.urls import url
urlpatterns = [
url(r'^cellphone_brand/$', 视图函数X),
]
# 上述代码解释:
r'^cellphone_brand/$': 表示由根路由转来的url中,以cellphone_brand开头且结尾的url会被匹配到视图函数X中,即视图函数X将会被调用 (视图函数X后面会说到)
· 创建一个名为CellphoneBrandAPIView的视图类,替代上述视图函数X (直接以类的方式实现)
当前数据表cellphone_brand里还没有数据,因此就让这个视图函数实现添加数据的功能:
· 在子应用market的views.py中创建一个名为CellphoneBrand的视图类,其内部将实现一个get()方法,如下:
from rest_framework.response import Response # rest_framework是基于django的一个第三方扩展,需要安装与注册
from rest_framework.views import APIView # APIView是基于rest_framework的视图类,是djangod的View的子类(django的view就不说了,因为远没有APIView及其子类好用)
from market.models import CellphoneBrand # 导入CellphoneBrand模型类
class CellphoneBrandAPIView(APIView): # 这个视图类从名字可以看出是用来处理与CellphoneBrand模型对应的数据表相关逻辑的,它继承自APIView
def get(self, request): # get方法会匹配请求对应的方式,必须是get请求才会触发get方法,并且视图函数的第一个参数永远是HTTP的请求对象;
name = CellphoneBrand.objects.create(brand_name='小米') # 模型类保存一条数据的方式,会返回这个数据的对象
return Response(name.brand_name) # 响应结果返回给前端,支持等多种数据类型
· rest_frameworkd的安装与注册:
安装:pip install djangorestframework
注册:在setting.py的INSTALLED_APPS中添加'rest_framework'
· 视图类CellphoneBrandAPIView创建好后,回到market子应用的urls.py中把视图函数X改为CellphoneBrandAPIView.as_view():
此时的urls.py文件的所有内容为:
from django.conf.urls import url
from market.views import CellphoneBrandAPIView
urlpatterns = [
# 手机品牌接口
url(r'^cellphone_brand/$', CellphoneBrandAPIView.as_view()),
]
至此,url路由和视图都有了,则测试一下这个CellphoneBrandAPIView视图类里的get方法是否能实现其逻辑,即在数据表cellphone_brand中增加一条数据,
访问http://127.0.0.1:8000/market/cellphone_brand/ 会发现一个响应页面,检查数据表,会多一条数据。
· 综合上述第4、第5步,一个简化的接收请求、处理逻辑与结果响应的流程如下所示:
前端 --> HTTP请求 --> 项目的根路由中(urls.py) --> 根据url的字符进行匹配,分发到子应用的路由中 --> 根据url后面的字符进行匹配,分发到不同的视图函数中--|
^ |
| |
|<------------------------------------------------------------返回响应结果 <-------- 视图函数根据其请求方法的不同触发对应名称的函数实现各自的逻辑 <--|
^ |
| |
| |
| |
|<------操作缓存/数据库 <-----|
6、视图的request参数:
全文所述的视图都是以视图类方式实现的,其内部可有多个方法,对应的get方法一般用来处理获取数据的操作;post用来新增;put用来修改;delete用来删除,不过这只是一种习惯不是必须的。
现在,前端想上传一条新手机品牌的数据,该品牌名称是由前端传来,对于这个需求,后端要在CellphoneBrandAPIView中进行如下操作:
class CellphoneBrandAPIView(APIView):
def post(self, request):
params = request.data
v_brand_name = params.get('brand_name', None)
if v_brand_name is None:
return Response({400: '请求参数不存在'})
name = CellphoneBrand.objects.create(brand_name=v_brand_name)
result = {
'brand_name': name.brand_name
}
return Response(result)
# 上述代码解释:
post方法: 为处理post请求而设置,其内部代码实现一个新增数据的功能
request参数: 是restframework提供的HTTP请求对象,优于django提供的HTTP对象
request.data: 会返回请求体解析后的数据(dict类型),适用于post、put、patch等请求方式
另,request.query_params: 适用于获取get请求方式的查询字符串参数 ?key1=value1&key2=value2
Response: 常用的构造方式Response(data, status=None, template_name=None)
其中data符合python的内建类型数据即可
status响应状态码
template_name是模板名,表示要返回哪个html页面
7、模型类与外键:
· 在market子应用的models.py中创建一个名为CellphoneType的模型类,对应数据表cellphone_type,用来描述手机型号:
'''
手机型号模型类
'''
class CellphoneType(models.Model):
# 外键,所属品牌
brand = models.ForeignKey(CellphoneBrand, on_delete=models.CASCADE, verbose_name='所属品牌')
type_name = models.CharField(max_length=15, verbose_name='型号名称')
price = models.IntegerField(default=0, verbose_name='价格')
imei_number = models.CharField(max_length=60, unique=True, verbose_name='序列号')
class Meta:
db_table = 'cellphone_type'
verbose_name = '手机型号'
verbose_name_plural = verbose_name
# 上述代码解释:
brand: 该字段名表示该手机型号所属的品牌对象,注意这是个CellphoneBrand的类对象,但是在生成数据表的时候,该字段会自动命名为brand_id,用以表示所属类对象的主键
ForeignKey: 该字段类型表示外键,即其字段名为当前数据的外键对象
CellphoneBrand: 表示这个外键是CellphoneBrand类型
on_delete: 表示删除其外键数据时,当前数据该作何处理,有多个值可选
models.CASCADE: 级联操作,表示其外键数据被删除时,当前数据也被删除(比如小米牌手机被删除了,那么小米9型号的手机数据也自动被删除)
IntegerField: 该字段类型为整型
unique: 该字段选项表示当前字段在整个表中的值应是唯一的,当有重复的数据传来时,会触发异常
!!主要是brand这个字段,该字段所表示的值就是外键的类对象,而在数据表中表示时会以brand_id表示该类对象的主键,因此,当你拿到了CellphoneType对象去调用brand时,会返回CellphoneBrand的对象;而调用brand_id时,会返回CellphoneBrand对象的主键值
· 添加了一个模型类后要执行数据库迁移操作, 以生成对应数据表:
python manage.py makemigrations
python manage.py migrate
· 在market子应用的views.py中创建一个名为CellphoneTypeAPIView的类视图:
'''
手机型号类视图
'''
class CellphoneTypeAPIView(APIView):
# 增加一个手机型号
def post(self, request):
params = request.data
# 获取请求的每一个参数
v_brand_id = params.get('brand_id', None) # 所属手机品牌,传来的是一个id
v_type_name = params.get('type_name', None)
v_price = params.get('price', None)
v_imei_number = params.get('imei_number', None)
if v_brand_id is None or v_type_name is None or v_price is None or v_imei_number is None:
return Response({'describe': '请求参数不存在'}, 400)
try:
cell_phone_type = CellphoneType.objects.create(brand_id=v_brand_id, type_name=v_type_name, price=v_price, imei_number=v_imei_number)
except Exception as e:
return Response({'describe': '服务器内部错误:' + str(e)}, 500)
# 组织返回的数据格式
result = {
'brand_id': cell_phone_type.brand_id,
'type_name': cell_phone_type.type_name,
'price': cell_phone_type.price,
'imei_number': cell_phone_type.imei_number
}
return Response(result, 200)
# 上述代码解释:
v_brand_id = params.get('brand_id', None): 表示获取前端传来的外键主键,即所属手机品牌的id,在保存手机型号数据时该字段也是以id的形式去保存
CellphoneType.objects.create(brand_id=v_brand_id, ...) 保存数据时,模型类中的brand属性,要改为brand_id,因为brand是一个对象
'brand_id': cell_phone_type.brand_id: 返回数据的时候,也是返回外键的id
· 现在想获取一个手机的型号信息,并且要查询其关联外键对象的品牌名称:
'''
手机型号类视图
'''
class CellphoneTypeAPIView(APIView):
# 获取一个手机型号的信息
def get(self, request):
params = request.query_params
v_id = params.get('id', None)
if v_id is None:
return Response({'describe': '请求参数不存在'}, 400)
try:
cell_phone_type = CellphoneType.objects.get(id=v_id)
except Exception as e:
return Response({'describe': '服务器内部错误:' + str(e)}, 500)
result = {
'brand_id': cell_phone_type.brand_id,
'type_name': cell_phone_type.type_name,
'price': cell_phone_type.price,
'imei_number': cell_phone_type.imei_number,
'brand_name': cell_phone_type.brand.brand_name # 所属品牌名称
}
return Response(result, 200)
# 上述代码解释:
'brand_name': cell_phone_type.brand.brand_name: 通过CellphoneType对象cell_phone_type去调用其brand属性便可获得关联的外键对象,即CellphoneBrand类对象,那么它的所有属性便可都获取到了
· 现在想获取一个手机品牌下所有的手机型号信息,如下:
'''
手机品牌类视图
'''
class CellphoneBrandAPIView(APIView):
# 获取所有品牌数据
def get(self, request):
params = request.query_params
v_id = params.get('id', None)
if v_id is None:
return Response({'describe': '请求参数不存在'}, 400)
try:
brand_obj = CellphoneBrand.objects.get(id=v_id)
except Exception as e:
return Response({'describe': '服务器内部错误:' + str(e)}, 500)
phone_type_queryset = brand_obj.cellphonetype_set.all() # 获取当前品牌下所有的手机类型信息
all_phone_type = []
for phone in phone_type_queryset:
result = {
'type_name': phone.type_name,
'price': phone.price,
'imei_number': phone.imei_number
}
all_phone_type.append(result)
result = {
'brand_name': brand_obj.brand_name,
'all_phone_type': all_phone_type
}
return Response(result)
# 上述代码解释:
phone_type_queryset = brand_obj.cellphonetype_set.all(): 手机品牌类对象brand_obj调用cellphonetype_set.all()就可以获取brand_obj这个品牌下所有的手机型号数据对象,
因为默认规定了用外键查询其所关联类的数据对象时,用与之关联模型的”类名小写_set“的形式进行查询。brand_obj.cellphonetype_set可以理解成CellphoneType.objects
· 根据本节的demo可发现,CellphoneBrand类和CellphoneType类之前是一种从属关系,即一个手机品牌可以对应多种手机型号,而一个手机型号只能对应一个手机品牌,这种对应关系称为一对多关系,以后碰到一对多的关系就按照这样处理
# 额外的
默认规定的一方访问多方数据所使用的cellphonetype_set这种方式可能会显得不够见名知义,为了能让一方更方便的访问多方的数据,
在定义多方类的时候,在其字段选项中添加一个选项related_name,这表示当一方对象访问多方数据时可以按照related_name的值去调用多方数据。
因此,上述的demo中,将CellphoneType类的属性brand修改为:brand = models.ForeignKey(CellphoneBrand, on_delete=models.CASCADE, related_name='phone_type', verbose_name='所属品牌')
对应的CellphoneBrandAPIView的get方法中的brand_obj.cellphonetype_set.all()改为:brand_obj.phone_type.all() 可以理解成phone_type就是CellphoneBrand类的一个“隐藏”的属性,
需要注意的是,既然修改了model的字段选项,别忘了进行数据库迁移操作。
8、序列化与反序列化:
在操作第7步的过程中可以发现,不管是获取前端传来的数据还是,返回数据的响应结果,都要写一堆代码用来组织数据,为了简化这些操作,而专注于代码本身的逻辑,可通过序列化器实现这些功能.
序列化器有两种,直接介绍这款序列化器:ModelSerializer (模型类序列化器),该序列化器是专门用于django模型类的序列化与反序列化操作的。
使用方式:
· 在market子应用中添加一个名为serializers.py的文件,专门用于编写不同模型类对应的序列化器
· 在该文件中,写一个基于CellphoneType类的序列化器CellPhoneTypeSerializer:
from rest_framework import serializers
from market.models import CellphoneType
'''CellphoneType类对应的序列化器'''
class CellPhoneTypeSerializer(serializers.ModelSerializer):
class Meta:
model = CellphoneType
fields = '__all__'
# 上述代码解释:
serializers: 是restframework提供的一个写有序列化器相关类的文件
ModelSerializer: 模型序列化器,本次写的CellPhoneTypeSerializer便继承自它
model = CellphoneType: 表示该序列化器是属于哪个模型类的序列化器
fields = '__all__': 指定序列化与反序列化时涉及到的字段为CellphoneType类的全部属性,也可以专门指定一部分字段
· 序列化器的序列化操作:
'''
手机型号类视图
'''
class CellphoneTypeAPIView(APIView):
# 获取一个手机型号的信息
def get(self, request):
params = request.query_params
v_id = params.get('id', None)
if v_id is None:
return Response({'describe': '请求参数不存在'}, 400)
try:
cell_phone_type = CellphoneType.objects.get(id=v_id)
except Exception as e:
return Response({'describe': '服务器内部错误:' + str(e)}, 500)
# result = {
# 'brand_id': cell_phone_type.brand_id,
# 'type_name': cell_phone_type.type_name,
# 'price': cell_phone_type.price,
# 'imei_number': cell_phone_type.imei_number,
# 'brand_name': cell_phone_type.brand.brand_name
# }
# 之前的代码中,使用的是上述被注释的result返回给前端,这需要自己手动组织数据,如果字段非常多就会很麻烦
# 使用CellphoneType类的序列化器去进行序列化操作就变得很方便,如下:
serializer = CellPhoneTypeSerializer(instance=cell_phone_type)
result = serializer.data
return Response(result, 200)
# 上述代码解释:
serializer = CellPhoneTypeSerializer(instance=cell_phone_type): 构造一个序列化器的对象,一般有两个构造参数,分别为instance和data,当前使用的是instance,表示给序列化器传入一个对象,以备其进行序列化操作。
serializer.data: 序列化器的序列化操作,将对象的属性及其值以key:value的形式返回
· 序列化器的反序列化操作:
按照上面的序列化操作的经验,尝试进行反序列化操作
'''
手机型号类视图
'''
class CellphoneTypeAPIView(APIView):
# 增加一个手机型号
def post(self, request):
params = request.data
# 这是之前一行行获取前端传来的参数然后使用模型类的create方法去新增一条数据的方式,这样看来获取参数也是很麻烦的事情
# v_brand_id = params.get('brand_id', None) # 所属手机品牌,传来的是一个id
# v_type_name = params.get('type_name', None)
# v_price = params.get('price', None)
# v_imei_number = params.get('imei_number', None)
# if (v_brand_id and v_type_name and v_price and v_imei_number) is None:
# return Response({'describe': '请求参数不存在'}, 400)
#
# try:
# cell_phone_type = CellphoneType.objects.create(brand_id=v_brand_id, type_name=v_type_name, price=v_price, imei_number=v_imei_number)
# except Exception as e:
# return Response({'describe': '服务器内部错误:' + str(e)}, 500)
# 之前手动组织响应的数据格式
# result = {
# 'brand_id': cell_phone_type.brand_id,
# 'type_name': cell_phone_type.type_name,
# 'price': cell_phone_type.price,
# 'imei_number': cell_phone_type.imei_number
# }
'''以上是原来的代码,下面用序列化器的方式去实现'''
# 存数据
serializer = CellPhoneTypeSerializer(data=params)
serializer.is_valid(raise_exception=True)
serializer.save()
# 返回响应数据
result = serializer.data
return Response(result, 200)
# 上述代码解释:
serializer = CellPhoneTypeSerializer(data=params): 构造一个序列化器的对象,这里使用了data参数,因为是要生成一条数据,因此要把前端传来的参数作为“原料”传给序列化器以备反序列化操作
serializer.is_valid(raise_exception=True): 反序列化操作必要的验证步骤,这省去了开发者进行的各种验证操作,raise_exception=True表示抛出异常信息,若验证失败则会向前端返回“HTTP 400 Bad Request”
serializer.save(): 序列化器对象调用save()方法,通过源码可以看出,会触发模型类的create方法或者是update方法,当前业务会触发create方法,因为当前序列化器是根据data参数构造的,表示新创建一条数据。
若想触发update方法去更新一条数据,则在构造序列化器对象时应该同时传入instance和data参数,不难理解传入instance会找到原来存在的那条数据,而data会对该数据进行重新赋值,综合下来就是更新,因此触应触发update方法
以上是序列化器的序列化与反序列化的最基本使用方式,还可以使用其他的方式进行灵活的操作,比如用户注册时并不需要把密码返回给前端,那么序列化器的fields就不能用“__all__”。更多的序列化器操作查看链接:https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
9、序列化器的嵌套:
根据步骤8,应该对使用ModelSerializer序列化器有了一些认识,现在需求变更为,当前端在获取某一种手机型号返回值时,还要返回其所属品牌的信息。
用序列化器实现这个功能:
· 对CellphoneType类来说,它有一个外键brand,在前端获取其数据信息的时候,会返回该字段,但是该字段在返回时只会返回其关联模型类的主键,其余属性无法直接现实出来,
之前说过,这个brand属性相当于是关联类CellphoneBrand的一个对象,既然是对象,那么就可以用序列化器将其序列化,这样在返回响应数据给前端的时候就不仅仅是个主键了。
· 在market子应用的serializers.py文件中新增一个序列化器CellphoneBrandSerializer,它是CellphoneBrand类的序列化器:
'''CellphoneBrand类对应的序列化器'''
class CellphoneBrandSerializer(serializers.ModelSerializer):
class Meta:
model = CellphoneBrand
fields = '__all__'
· 修改CellphoneType类的序列化器CellPhoneTypeSerializer为如下代码:
'''CellphoneType类对应的序列化器'''
class CellPhoneTypeSerializer(serializers.ModelSerializer):
brand = CellphoneBrandSerializer(read_only=True)
brand_id = serializers.IntegerField(write_only=True)
class Meta:
model = CellphoneType
fields = '__all__'
# 上述代码解释:
与之前的CellPhoneTypeSerializer相比,发现新增了:
brand = CellphoneBrandSerializer(read_only=True)
brand_id = serializers.IntegerField(write_only=True)
之前的brand字段被序列化器默认声明成了:brand = PrimaryKeyRelatedField(label='所属品牌', queryset=CellphoneBrand.objects.all()),表示要返回CellphoneBrand类的主键【这是默认的】
现在的brand字段被声明成了brand = CellphoneBrandSerializer(read_only=True),表示将返回CellphoneBrand的序列化器所序列化的内容.
read_only=True表示将只会在序列化的时候用于返回数据,而不会影响反序列化时写入数据
之前没有brand_id字段,现在是新增的,序列化器确实是可以新增字段的,但前提是如果该字段可以被写入数据表,那么没问题,若不能被写入数据表,就要在进行验证后返回的验证数据中将其去除,否则报错,而brand_id是可以被写入数据表的,因此可以新增。
如果换个字段xxxyyy = serializers.IntegerField(write_only=True) 就会报错了,因为数据表中没有xxxyyy这个字段
write_only=True表示将只会在反序列化的时候用于写入数据,而不会影响序列化时返回数据
label: 就是模型类中的verbose_name
如果想看模型类的序列化器规则,可以在终端中进入交互模式,命令: python manage.py shell
>>> from market.serializers import CellPhoneTypeSerializer
>>> CellPhoneTypeSerializer()
此时在前端获取CellphoneType类的某条数据时,便会一同返回其外键对象的所有数据信息,如下:
{
"id": 7,
# brand就是外键对象返回的数据
"brand": {
"id": 2,
"brand_name": "华为"
},
"type_name": "华为p9",
"price": 2000,
"imei_number": "ssdsddsdsfsdf"
}
# 而之前是这样的:
{
"id": 7,
"type_name": "华为p9",
"price": 2000,
"imei_number": "ssdsddsdsfsdf",
"brand": 2
}
10、序列化时返回多条数据:
· 序列化器的构造方法之前说过:
# 在进行序列化时进行如下方式构造
serializer = CellPhoneTypeSerializer(instance=cell_phone_type)
其中instance参数的值可以是一个对象,也可以是对象的集合,django称其为查询结果集,当要序列化多条数据时,该构造方法的参数要添加一个many=True,表示要对多条数据进行序列化。
如:
单个对象序列化: obj = CellphoneType.objects.get(id=1)
serializer = CellPhoneTypeSerializer(instance=obj)
serializer.data
多个对象序列化: obj_list= CellphoneType.objects.filter(id__gt=1, id__lt=10)
serializer = CellPhoneTypeSerializer(instance=obj, many=True)
serializer.data
11、模型类的多对多关系:
两个模型类之间存在多对多关系,可以直接在其中一个模型类中使用ManyToManyField进行关联,但是在后续的逻辑操作中会很麻烦,个人感觉很不好用。
· 建议使用手动添加第三方关系表的方式使用ManyToManyField进行两个模型类的关联,这样在处理两个模型类的关系时,只需要操作这张关系表就可以了,非常方便。
· 新建一个经销商模型类Dealer,该模型类将与手机品牌模型类CellphoneBrand发生一些关联:
想一下,一个经销商可以代理多个品牌的手机,而一个手机品牌也可以被多个经销商代理,所以Dealer模型类和CellphoneBrand模型类之间的关系是多对多的关系
· 新建经销商模型类Dealer:
'''
经销商模型类
'''
class Dealer(models.Model):
name = models.CharField(max_length=15, verbose_name='经销商名字')
class Meta:
db_table = 'dealer'
verbose_name = '经销商'
verbose_name_plural = verbose_name
· 新建第三方关系模型类DealerAgentCellphoneBrandRelation 用来表示Dealer与CellphoneBrand之间的代理关系:
当一个经销商代理了一个手机品牌时,这个模型类对应的数据表便会多一条数据,这条数据至少有两个字段,一个表示代理商的id,一个表示手机品牌的id,这样,当前端想查询某个代理商代理了哪些手机品牌时,直接查询这个代理关系表就行
'''
经销商与手机品牌之间的代理关系模型类
'''
class DealerAgentCellphoneBrandRelation(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, verbose_name='经销商')
brand = models.ForeignKey(CellphoneBrand, on_delete=models.CASCADE, verbose_name='品牌')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='代理关系创建时间')
class Meta:
db_table = 'dealer_cellphone_brand_agent_relation'
verbose_name = '经销商与手机品牌代理关系'
verbose_name_plural = verbose_name
# 上述代码解释:
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, verbose_name='经销商'): 当前关系所属的经销商
brand = models.ForeignKey(CellphoneBrand, on_delete=models.CASCADE, verbose_name='品牌'): 当前关系所属手机品牌
从上面的两行代码可以看出,用来描述两个模型类之间关系的模型类中,需要有关系双方的模型类作为外键,以此产生关联
create_time: 当前代理关系创建的时间
DateTimeField: 字段类型为:时间日期
auto_now_add: 当这个关系数据生成时,自定添加当前时间作为该字段的值
· Dealer与DealerAgentCellphoneBrandRelation都还没有各自的序列化器,开始创建他们:
在market子应用的serializers.py中:
'''Dealer类对应的序列化器'''
class DealerSerializer(serializers.ModelSerializer):
class Meta:
model = Dealer
fields = '__all__'
# 上述序列化器就不解释了
'''代理关系序列化器'''
class DealerAgentCellphoneBrandRelationSerializer(serializers.ModelSerializer):
dealer = DealerSerializer(read_only=True)
brand = CellphoneBrandSerializer(read_only=True)
dealer_id = serializers.IntegerField(required=True)
brand_id = serializers.IntegerField(required=True)
class Meta:
model = DealerAgentCellphoneBrandRelation
fields = '__all__'
# DealerAgentCellphoneBrandRelationSerializer序列化器的解释:
dealer = DealerSerializer(read_only=True): 嵌套了 DealerSerializer ,在对关系表DealerAgentCellphoneBrandRelation进行序列化时会将其所属的经销商对象的数据也序列化出来,而反序列化时与此字段没有任何关系,因为read_only=True
brand = CellphoneBrandSerializer(read_only=True): 同上
dealer_id = serializers.IntegerField(required=True): 新增的序列化器字段:关系模型所属的代理商id,required=True表示在生成一条关系数据时必须要有这个字段,否则报错。为何要加这个字段,因为外键在数据表中的保存方式就是 “外键_id”,缺少外键肯定写不进数据
brand_id = serializers.IntegerField(required=True): 同上
· 有了模型类与序列化器。就可以在views.py中编写与代理关系相关的视图函数,路由自己写吧,后面我都不写了:
'''代理关系类视图'''
class DealerAgentCellphoneBrandRelationAPIView(APIView):
# 添加代理关系
def post(self, request):
# 获取前端所有参数值
params = request.data
# 拿着所有参数值进行反序列化、验证、数据保存的操作
serializer = DealerAgentCellphoneBrandRelationSerializer(data=params)
serializer.is_valid(raise_exception=True)
serializer.save()
# 保存完数据后将该数据序列化的结果返回给前端
result = serializer.data
return Response(result)
# 检查代理关系
# 需求场景是:根据前端传来的经销商id获取其代理了哪些品牌,或根据手机品牌id获取被哪些经销商代理
def get(self, request):
params = request.query_params
v_dealer_id = params.get('dealer_id', None)
v_brand_id = params.get('brand_id', None)
if v_dealer_id is None and v_brand_id is None:
return Response({'describe': '请求参数不存在'}, 400)
elif v_dealer_id is not None and v_brand_id is not None:
return Response({'describe': '只允许一个请求参数'}, 400)
# 查询与经销商v_dealer_id有关的所有代理关系数据
if v_dealer_id is not None:
try:
relation_queryset = DealerAgentCellphoneBrandRelation.objects.filter(dealer_id=v_dealer_id)
except Exception as e:
return Response({'describe': '服务器内部错误:' + str(e)}, 500)
# 查询与手机品牌v_brand_id有关的所有代理关系数据
if v_brand_id is not None:
try:
relation_queryset = DealerAgentCellphoneBrandRelation.objects.filter(brand_id=v_brand_id)
except Exception as e:
return Response({'describe': '服务器内部错误:' + str(e)}, 500)
# 根据代理关系的查询结果集构造序列化器对象
serializer = DealerAgentCellphoneBrandRelationSerializer(instance=relation_queryset, many=True)
# 用序列化器对象去序列化列表数据返回前端
result = serializer.data
return Response(result)
写完之后,手动测试一下生成关系数据、查询关系数据的功能是否和预期一致
上述内容描述的是经销商和手机品牌之间多对多的代理关系,那么如果想了解各手机型号与消费者之间的销售情况,怎么分析和处理呢?
12、模板的使用(前后端不分离方式)
在本步骤之前的步骤中,测试都是使用postman进行,这样可以模拟前后端分离的情况进行测试。现在要在项目中增加前端代码,让浏览器运行前端代码对项目进行测试
· 在根目录下创建用于存放所有前端代码的模板文件夹templates, 这是一个Directory文件夹不是python包
· 在setting.py中的TEMPLATES里设置: 'DIRS': [os.path.join(BASE_DIR, 'templates')], 表示templates文件夹是模板是项目的查找路径,所有的模板查找,都可直接从其内部的文件路径开始查找
· 后端代码中有market子应用,为了对应后端,前端也添加一个market文件夹作为前端的模块,里面写业务的逻辑时就需要定义不同的html文件
· 在postman中,使用get方式请求http://127.0.0.1:8000/market/cellphone_brand/ 地址就可以获取所有的手机品牌数据,那么现在使用浏览器访问该地址时,前端的代码需要怎么写:
在market文件夹中定义一个goods_display.html文件(以h5形式定义),在标签中增加以下代码:
手机品牌展示
{% for brand in response %}
{{ brand.id }}: {{ brand.brand_name }}
{% endfor %}
# 上述代码解释:
{% for brand in response %}
{% endfor %}
在模板中,两层 {{}} 的中间可以变量,
{% %} 的中间可以存放for循环或if等代码,上述的response是由django响应的字典的key,在模板中会自动加载它的值
endfor表示for循环结束
· “for”的使用方式如下:
# 1、遍历列表list=[1,2,3,4]
{% for i in list %}
{{i}}
{% endfor %}
# 2、遍历字典dict={'a': 1, 'b': 2}
{% for k, v in dict %}
{{k}}, {{v}}
{% endfor %}
· “if”的使用方式如下:
{% if x == y %}
{# 你的代码 #}
{% elif brand.id == 1 %}
{# 你的代码 #}
{% else %}
{# 你的代码 #}
{% endif%}
· 模板页面的数据都来自于django后端的传递,现在去修改django中返回响应的代码:
找到类视图CellphoneBrandAPIView,将其get方法的return Response()改为:
return render(request, template_name='market/goods_display.html', context={'response': result})
表示返回的是一个html页面,request是HTTP请求对象,template_name是html文件的路径,context={'response': result}是传给html文件进行加载的数据,前端只要通过response就能拿到result
13、后台管理模块admin的使用:
django中的模型类对应着数据表,对于一个网站来说,如果管理员要对这些数据进行管理,就需要使用后台管理工具,django提供了后台管理模块admin。
admin和自己创建的子应用一样都需要注册到setting.py的INSTALLED_APPS中,不过项目在新建的时候就已经自动注册了。
· 通过模型类就能将其关联的数据表注册到admin进行管理:
在子应用market.py中的admin.py中对CellphoneBrand类进行注册,如下:
from django.contrib import admin
from market.models import CellphoneBrand
'''注册CellphoneBrand模型类到后台管理'''
@admin.register(CellphoneBrand)
class CellphoneBrandAdmin(admin.ModelAdmin):
list_display = ('id', 'brand_name') # 枚举在后台中要显示的数据表字段
search_fields = ('brand_name',) # 根据brand_name字段进行模糊搜索,若要精确搜索,该字段前面加 = 搜索条件可叠加,是与的关系
list_per_page = 20 # 后台一页显示的数据量
· 在setting.py中修改后台管理的本地语言与时区:
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai'
· 设置将子应用在后台显示的名称,不设置会默认显示子应用名:
在market子应用的apps.py中的MarketConfig类中添加属性:
verbose_name = '商城模块' # 当前子应用在后台中显示的名字
以上是后台管理的基本使用
· 在CellphoneBrandAdmin可以添加一些原数据表不存在的字段显示在后台管理页面中:
'''注册CellphoneBrand模型类到后台管理'''
@admin.register(CellphoneBrand)
class CellphoneBrandAdmin(admin.ModelAdmin):
list_display = ('id', 'brand_name', 'founder') # 在后台中要显示数据表字段, founder不是数据表的原有字段,是由下方的founder函数定义产生在后台页面中展示使用的
search_fields = ('brand_name',) # 根据brand_name字段进行模糊搜索,若要精确搜索,该字段前面加= 搜索条件可叠加,是与的关系
list_per_page = 20 # 后台一页显示的数据量
# 在后台管理页面中想显示创始人字段founder,而原数据表中又不存在,就可以通过这种方式扩展后台显示字段,该字段不支持搜索,所以别写入search_fields中
def founder(self, obj): # 参数obj为当前一条数据,即类对象
brand_name = obj.brand_name
if brand_name == '小米':
return '雷军' # return的结果就是字段的值
elif brand_name == '华为':
return '任正非'
else:
return '其他'
founder.short_description = '创始人' # 字段显示的名称
· 如果当前子应用使用的数据库不是setting.py中设置的default数据库,那么在写 CellphoneBrandAdmin时不能直接继承自admin.ModelAdmin,需要准备一个MultiDBModelAdmin类:
# 定义使用非default数据库中的表对应的模型类在注册到admin时需要继承的类
class MultiDBModelAdmin(admin.ModelAdmin):
using = 'logger' # 这表明当前使用logger数据库
def get_queryset(self, request):
return super(MultiDBModelAdmin, self).get_queryset(request).using(self.using)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
return super(MultiDBModelAdmin, self).formfield_for_foreignkey(db_field, request, using=self.using, **kwargs)
def formfield_for_manytomany(self, db_field, request, **kwargs):
return super(MultiDBModelAdmin, self).formfield_for_manytomany(db_field, request, using=self.using, **kwargs)
让CellphoneBrandAdmin继承自MultiDBModelAdmin即可,其他写法不变。
· 将模型类注册到admin之后就可以登录到后台进行查看,需要创建管理员账户:
在终端中执行命令:
python manage.py createsuperuser
设置账户名与密码之后登陆后台管理系统: http://127.0.0.1:8000/admin/
14、部署:
当前项目的目录结构首先要做一下调整:
原根目录test_django文件夹整体放入一个新的文件夹中,该文件夹表示整个项目,在该新的文件夹中增加一个front文件夹表示前端文件夹(名字随意),
然后将原根目录下的templates文件夹整体移动到front文件夹下,将原根目录test_django设置为 Sources Root即可,所以新的目录结构为:
·allProject/ 项目的总目录
|
|___·front/ 项目的前端目录,所有前端的代码/js/css文件,图片等静态文件都放在这个文件夹下面
| |
| |___·static/
| |___·templates/
|
|___·test_django/ 项目的后端目录,对于后端来说,也就是项目根目录
· django的部署方式有很多,这里提供一种简单的方式:
setting.py中:
DEBUG=True 需要改为False. 根据提示# SECURITY WARNING: don't run with debug turned on in production!
ALLOWED_HOSTS = ['*'] 表示允许所有的host进行请求(如果出于更安全的因素考虑,这个IP可以限制为固定的)
STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'front/static') 静态文件的存储目录,如图片、js、css文件放在这里,这个路径根据项目目录结构设置
TEMPLATES中的DIRS的值改为:
[os.path.join(os.path.dirname(BASE_DIR), 'front/templates')], 表示让系统去allProject/front/templates中去搜索html文件
根urls.py的urlpatterns添加:
url(r'^static/(?P.*)$', static.serve, {'document_root': settings.STATIC_ROOT}, name='static'),
在终端执行静态文件收集命令:
python manage.py collectstatic
然后会发现front/static文件夹下多了一些内容,这些就是系统的静态文件
· 主流的部署方式: 采用 Nginx + uwsgi 进行部署
(一)、Nginx:
1、安装:
yum install nginx
2、配置:
· centos7中,检查/etc/selinux/config文件中的#SELINUX值,将enforcing改为disabled
· 进入 /etc/nginx/nginx.conf文件:
~修改user的值,改为当前linux用户
~在http{ } 中添加:
# 静态请求处理
server {
listen 80;
server_name localhost; # 部署时设置为外网ip
location / {
root /media/sf_ShareDir/allProject/front; # 项目的静态文件根目录
index index.html; # 项目的首页,也是.../front下面的一个文件
}
}
# 通过uwsgi转发给django服务
upstream allproject {
server 192.168.12.11:8001; # 和uwsgi.ini文件中的socket值一致,一般为内网ip
}
# 动态请求处理
server {
listen 8000;
server_name 192.168.12.11; # 外网ip
location / {
include uwsgi_params;
uwsgi_pass allproject; # 请求转发给upstream allproject中
}
}
3、检查:
修改了nginx的配置之后就要检查配置是否正确: nginx -t
然后关闭后重启nginx.
启动nginx:
systemctl start nginx
关闭nginx:
systemctl stop nginx
访问localhost:80(本地测试)或者外网ip:80(外网测试)会出现index.html文件中的页面
(二)、uwsgi:
1、首先一顿安装依赖:
yum -y install gcc python36-devel bzip2-devel sqlite-devel openssl-devel readline-devel xz-devel xz-devel
2、安装uwsgi:
pip install uwsgi
3、在项目的python包目录下新建uwsgi.ini文件,内容如下:
[uwsgi]
#使用nginx连接时使用,Django程序所在服务器地址,所有请求后台动态数据的请求都会被转到这里
# 线上内网地址
socket=192.168.12.11:8001
#项目根目录
chdir = /media/sf_ShareDir/allProject/test_django
#项目中wsgi.py文件的目录,相对于项目目录
wsgi-file=test_django/wsgi.py
# 进程数(看服务器是几个核)
processes=1
# 线程数
threads=1
# uwsgi服务器的角色
master=True
# 存放进程编号的文件
pidfile=uwsgi.pid
# 日志文件,因为uwsgi可以脱离终端在后台运行,日志看不见。我们以前的runserver是依赖终端的
daemonize=uwsgi.log
# 指定依赖的虚拟环境
# virtualenv=/home/zhanghaiyang/.virtualenvs/django_1.11.11
4、使用uwsgi运行该项目:
【启动:uwsgi --ini uwsgi.ini】 (至此,项目就正式启动了)
【重启:uwsgi --reload uwsgi.pid】
【关闭:uwsgi --stop uwsgi.pid】
查看uwsgi的状态:
ps aux | grep uwsgi
注意,项目代码修改之后要重启uwsgi
(三)、注意事项:
1、setting.py文件中的debug设置为False
2、为保证项目的静态文件正常加载,在项目的根路由文件的 urlpatterns 列表外面添加:
# 部署时加载静态文件资源
if not settings.DEBUG:
urlpatterns += [url(r'^static/(?P.*)$', static.serve, {'document_root': settings.STATIC_ROOT}, name='static')]
15、User模块:
django拥有自带的用户模块AbstractUser,这个模块自带了许多好用的功能,它默认有一些字段,如username、password、email等,但是这些字段的数量未必能满足项目中用户表所需的字段,因此可以自己写
一个User类,然后继承自系统的AbstractUser。
· 新建一个名为user的子应用,在其中的model中添加自定义的用户模型类User:
from django.contrib.auth.models import AbstractUser
'''用户模型类'''
class User(AbstractUser):
# 在原有的用户模型类的字段基础上,新增下述三个字段
mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')
avatar = models.CharField(max_length=255, blank=True, default='', verbose_name='用户头像')
signature = models.CharField(max_length=50, blank=True, null=True, verbose_name='个人签名')
class Meta:
db_table = 'users'
verbose_name = '用户'
verbose_name_plural = verbose_name
# 上述代码解释:
AbstractUser: 系统自带的用户模块
blank=True: 表示当前字段,前端可以不传来
null=True: 表示当前字段在数据库中可为null
· 系统使用了自定义的用户模块需要告知系统,在setting.py中新增配置:
'''在django认证系统中配置自定义的模型类'''
AUTH_USER_MODEL = 'user.User' # 子应用名.模型类名
· 用户注册与登录:
用户在注册与登录之后,每次请求都需要认证用户的登录状态,建议使用使用Json Web Token认证而不是session。
· token的使用流程如下:
客户端d带上用户名密码第一次发送HTTP请求 ------> 服务器验证用户的信息 -------> 通过验证后
|
|
|<--------------------------------- 生成token返给客户端 <----------------------|
|
|
|--> 客户端保存token -------> 之后每次请求时在HTTP请求头中带上token -------> 服务器处理请求
|
|
响应的结果返回给客户端 <----------|
· 实现token的认证系统:
· 安装: pip install djangorestframework-jwt
· 在setting.py中配置:
# REST_FRAMEWORK的配置都放在这里,token也是用了djangorestframework-jwt,属于REST_FRAMEWORK
REST_FRAMEWORK = {
# 默认的认证方式,上面的有限即大于下面的优先级,当前配置表示采用token认证
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
# 对token认证的一些配置
import datetime
JWT_AUTH = {
# 设置有效期为30天,那么用户30天就要重新登录一次
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=30),
# 让系统调用复写的jwt_response_payload_handler方法,以自定义返回登陆数据,值即为自定义的复写方法
# # 用户登陆的时候会自动调用
'JWT_RESPONSE_PAYLOAD_HANDLER': 'utils.back_system_token.jwt_response_payload_handler',
}
· 如何在系统中使用token:
首先明确token是用于用户请求时进行身份认证的一种方式,所以其使用场景一般涉及3个地方:注册(未必)、登录、需要登录后才能访问的接口
· 用户注册:
默认一个用户注册成功后就让他直接登录,因此需要在注册成功后返回token给客户端。
· 用于实现注册接口的序列化器:
'''
用户注册接口的序列化器
'''
class RegisterUserSerializer(serializers.ModelSerializer):
# 用于验证的字段有: username、password、password_verify、mobile
password_verify = serializers.CharField(label='确认密码', write_only=True, allow_blank=False)
# 短信验证码
# sms_code = serializers.CharField(label='短信验证码', max_length=4, min_length=4, allow_null=False, allow_blank=False, write_only=True)
# 用于返回token
token = serializers.CharField(label='token', read_only=True)
class Meta:
model = User
fields = ('id', 'username', 'password', 'mobile', 'password_verify', 'sms_code', 'token', 'avatar', 'signature')
extra_kwargs = {
'id': {'read_only': True},
'username': {
'label': '用户名',
'min_length': 11,
'max_length': 11,
'error_messages': {
'min_length': '用户名注册时必须11个字符',
'max_length': '用户名注册时必须11个字符',
}
},
'password': {
'label': '密码',
'write_only': True,
'min_length': 5,
'max_length': 15,
'error_messages': {
'min_length': '密码5~15个字符',
'max_length': '密码5~15个字符',
}
}
}
# 验证用户名(使用手机号注册,默认用户名为手机号)(单个字段的验证)
def validate_username(self, value):
if not re.match(r'1[3-9]\d{9}$', value):
raise serializers.ValidationError('手机号格式不正确')
return value
# 验证手机号(单个字段的验证)
def validate_mobile(self, value):
if not re.match(r'1[3-9]\d{9}$', value):
raise serializers.ValidationError('手机号格式不正确')
return value
# 比较密码(多个字段字段的验证)
def validate(self, attrs):
password = attrs['password']
password_verify = attrs['password_verify']
if password != password_verify:
raise serializers.ValidationError('两次密码不一致')
return attrs
# 重写create方法
def create(self, validated_data):
# 删除多余字段
del validated_data['password_verify']
user = super().create(validated_data)
# 对密码加密
user.set_password(validated_data['password'])
user.save()
# 为该对象设置一个token,不会影响到模型类字段和数据库表结果
token = back_system_token(user)
user.token = token
# print('注册时返回token', token)
return user
自定义的back_system_token函数:
def back_system_token(user):
# 1、获取jwt提供的两个方法
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
# 2、将user传给jwt_payload_handler方法,生成payload
payload = jwt_payload_handler(user)
# 3、将payload编码以生成token
token = jwt_encode_handler(payload)
return token
· 注册的视图函数:
'''
用户注册接口
'''
class RegisterUserCreateView(APIView):
def post(self, request):
params = request.data
serializer = SignUpUserSerializer(data=params)
serializer.is_valid(raise_exception=True)
serializer.save()
result = serializer.data
return Response(result)
· 如何登陆:
django系统知道了登陆接口:
# 登录
# 系统默认的登陆接口路由
url(r'^login/', obtain_jwt_token),
直接访问上述路由即可登陆,登陆成功后默认返回token,一般只返回token给客户端是不够的,需要返回该登陆对象更多的信息才能满足需求:\
'''
在返回系统的token的基础上,再添加一些返回数据
用于登陆时返回数据使用
'''
def jwt_response_payload_handler(token, user=None, request=None):
result = {
'data': {
'token': token,
'id': user.id,
'username': user.username,
'mobile': user.mobile,
'avatar': user.avatar,
'signature': user.signature
}
}
return result
只要返回了token,客户端就拥有了登陆的钥匙,而客户端再次请求时,带着这个钥匙,只要在有效期内,服务器就认为客户端处于登陆状态
· 验证是否登陆
新建一个视图函数用于测试:
class TestLogin(APIView):
# 身份验证,登录用户可以访问该接口
permission_classes = [IsAuthenticated]
def get(self, request):
return Response('在登录状态')
三、django提高:
这部分主要介绍一下视图的使用,根据第二部分的内容介绍,在rest_framework中,APIView是整个框架中的基本视图,可以称为第一层视图。
关于APIView的使用,在第二部分与后台项目中已经使用了很多,下面介绍基于APIView进一步封装的第二层视图GenericAPIView。
1、第二层视图GenericAPIView:
GenericAPIView增加了一些类属性与方法:
· 对于要新增一条数据或返回多条数据的类视图:
· 属性:
queryset: 该属性可以提供视图的查询集
serializer_class: 该属性可以提供视图使用的序列化器
lookup_field: 根据该字段来查询数据,默认是pk
· 方法:
get_queryset(): 该方法获取要操作表的查询结果集
get_serializer(): 该方法获取类视图要使用的序列化器
get_object(): 根据该方法来获取lookup_field指定字段所筛选的对象
举例使用:
· 创建一个电脑品牌模型类ComputerBrand:
'''
电脑品牌模型类
'''
class ComputerBrand(models.Model):
brand_name = models.CharField(max_length=15, verbose_name='品牌名称')
class Meta:
db_table = 'computer_brand'
verbose_name = '电脑品牌'
verbose_name_plural = verbose_name
· 创建ComputerBrand的序列化器:
'''
ComputerBrand类对应的序列化器
'''
class ComputerBrandSerializer(serializers.ModelSerializer):
class Meta:
model = ComputerBrand
fields = '__all__'
· 创建电脑品牌类视图:
'''
电脑品牌类视图
'''
class ComputerBrandGenericAPIView(GenericAPIView):
# 这两个属性是重写了GenericAPIView中的这两个属性
queryset = ComputerBrand.objects.all() # 获取所有查询结果集
serializer_class = ComputerBrandSerializer # 获取序列化器类
# 新增一条电脑品牌数据
def post(self, request):
params = request.data
serializer = self.get_serializer(data=params)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
# 查询一个电脑品牌的详情信息
def get(self, request, pk):
computer_brand = self.get_object()
serializer = self.get_serializer(instance=computer_brand)
return Response(serializer.data)
# 上述代码解释:
GenericAPIView: 继承自APIView的一个视图
queryset: 重写了GenericAPIView的queryset,表明当前的类视图要操作的哪个数据表的查询结果集
serializer_class: 重写了GenericAPIView的serializer_class,表明当前的类视图要使用哪个序列化器
get_serializer: 实例化一个序列化器对象
pk: 客户端中请求url中的位置参数,get_object()可根据该值返回相应的对象
针对位置参数pk设置的路由是如下写法:
url(r'^computer/(?P\d+)/$', ComputerBrandGenericAPIView.as_view()),
上述的匹配方式为正则分组,其语法是(?Ppattern),其中name是组的名称,pattern是要匹配的内容
上述代码中对GenericAPIView的使用相对之前的APIView其实并没有显示出什么优势,甚至还要多写两个类属性,但是一般情况下GenericAPIView要与一个或多个的MiXin类联合使用可以更加简化代码。
2、MiXin类:
直接举例了,前面说过GenericAPIView要与一个或多个的MiXin类联合使用才能发挥更好的作用
'''
电脑品牌类视图
'''
class ComputerBrandGenericAPIViewWithMiXin(CreateModelMixin, RetrieveModelMixin, GenericAPIView):
# 这两个属性是重写了GenericAPIView中的这两个属性
queryset = ComputerBrand.objects.all() # 获取所有查询结果集
serializer_class = ComputerBrandSerializer # 获取序列化器类
# 新增一条电脑品牌数据
def post(self, request):
return self.create(request)
# 查询一个电脑品牌的详情信息
def get(self, request, pk):
return self.retrieve(request)
# 上述代码解释:
CreateModelMixin: rest-framework提供的封装了一些对数据进行反序列化操作的类
RetrieveModelMixin: rest-framework提供的封装了对对象进行序列化操作的类
create: CreateModelMixin中提供的具体实现反序列化的操作方法
retrieve: RetrieveModelMixin中提供的具体实现序列化的操作方法
类似的,如果你想返回多条数据给客户端时你要怎么做?删除时该怎么做?
用这些代码和步骤1中的代码比较一下可明显发现第二层视图的功能更好一些
3、第三层视图,这一层视图有多个类
第三层视图是在第二层视图与MiXin类上进行的封装,根据所继承的MiXin类不同拥有不同的功能:
举例:直接对GenericAPIView + MiXin类方式实现的功能进行修改如下:
'''
电脑品牌类视图(第三层视图)
'''
class ComputerBrandThirdView(CreateAPIView, RetrieveAPIView):
# 这两个属性是重写了GenericAPIView中的这两个属性
queryset = ComputerBrand.objects.all() # 获取所有查询结果集
serializer_class = ComputerBrandSerializer # 获取序列化器类
然后发现除了继承自CreateAPIView和RetrieveAPIView,然后重写了queryset与serializer_class的属性之外,再也没有其他的代码了,这样就可以完成添加数据和返回一个数据详情的功能。
如果你还想实现返回多条数据的功能,那么可以使用ListAPIView,但是这样就会有个问题,RetrieveAPIView和ListAPIView的底层都是实现了一个get()方法,当请求转发到该类视图时,这个类
到底是该调用哪个父类的get()方法?
假设现在这个类写成了这样:
class ComputerBrandThirdView(ListAPIView, CreateAPIView, UpdateAPIView, DestroyAPIView, RetrieveAPIView):
...
首先能明确的是CreateAPIView触发post方法,UpdateAPIView触发put/patch方法,DestroyAPIView触发delete方法,根据请求的方法不同,都可以转到各自的函数中,问题就是
ListAPIView和RetrieveAPIView都触发get()方法,那么当客户端发送了get()请求之后,谁的get()被触发了。根据测试结果显示:
当get方法请求http://127.0.0.1:8000/market/computer_create/2/ 或者 http://127.0.0.1:8000/market/computer_create/时,都会反会所有数据的信息,这说明触发的是
ListAPIView中的get()方法
现在修改ComputerBrandThirdView类如下:
class ComputerBrandThirdView(RetrieveAPIView, CreateAPIView, UpdateAPIView, DestroyAPIView, ListAPIView):
...
再次进行上述测试:
当客户端发送get()请求http://127.0.0.1:8000/market/computer_create/2/ 时, 会返回id为2的数据的详情信息,这表示RetrieveAPIView中的get()方法被触发
当客户端发送get()请求http://127.0.0.1:8000/market/computer_create/ 时, 会报错500
· 分析为什么会报错,首先如果这个请求的url会被能够被url(r'^computer_create/$', ComputerBrandThirdView.as_view()) 所匹配,然后被转到ComputerBrandThirdView类视图中,
根据请求方法应该被转到get()中,在ComputerBrandThirdView所继承的多个类中RetrieveAPIView和ListAPIView都拥有get()方法,按照继承的广度优先算法,会直接匹配到RetrieveAPIView
中的get()方法,但是RetrieveAPIView中的get()需要pk参数,而url中并没有给这个参数,因此直接报错。
而第一种继承的写法是会将请求匹配到ListAPIView的get()中,它的get()有无参数并不会影响方法的正确调用,只是会影响返回的结果而已。但是不论是以上哪一种写法都不合适,因为无法在同一个类视图中完成
所有需要的操作,因此建议根据分成两个类视图来写,这样就可以解决这些冲突。
那么到底优先匹配哪个父类的get()方法呢?是先写的先匹配吗,其实未必。可以打开shell,然后导入该这个类,然后调用它的__mro__方法:ComputerBrandThirdView.__mro__ 其结果会返回它的继承顺序,这样就可以得知
继承的父类中,谁的方法会被调用,另一个不会被调用。
4、视图集合:
视图集合也有多种,只说一个ModelViewSet,这个好用
直接举例实现了:
'''
电脑品牌类视图(ModelViewSet)
'''
class ComputerBrandModelViewSet(ModelViewSet):
queryset = ComputerBrand.objects.all() # 获取所有查询结果集
serializer_class = ComputerBrandSerializer # 获取序列化器类
· 要注意的是,使用视图集时,对路由的定义有些不一样:
之前是在urls.py中的urlpatterns列表中添加新的url(),现在是在这个列表外通过SimpleRouter的对象调用register函数,然后将route.urls添加到urlpatterns列表中即可:
# 使用视图集时,添加路由的方式
route = SimpleRouter()
# # 管理员操作记录路由
route.register(r'computer_brand_set', ComputerBrandModelViewSet, base_name='')
urlpatterns += route.urls
然后再进行步骤3中的那些测试,不管get()方法是带pk参数想获取单一数据还是不带参数获取所有数据,都能通过测试。
在步骤3和步骤4中,要明确的知道你需要给客户端提供的功能对应的接口地址和参数及其意义是什么,因为封装的层次越高就越显得有些抽象。
· 为何ModelViewSet可以实现这么多功能,查看源码可以发现:
其注释表示它就是支持这些操作:
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
其代码也是继承了多个MiXin类和GenericViewSet,而GenericViewSet还是继承了GenericAPIView,多个MiXin和GenericAPIView的联合使用决定了它可以支持多种操作,而GenericViewSet还继承了ViewSetMixin,
ViewSetMixin决定了请求时不再按照之前的get()、post()等方法来处理程序,而是根据动作来决定执行增删改查,查单一或是查列表,实际上是由路由中的 XXX.as_view({'get': 'list', 'get': 'retrieve'})来决定执行什么操作。
· 你可以把鼠标放在某个类上 --> 右键选择Diagrams --> show Diagram...查看这个类的所有继承关系图
不管第三层视图和视图集合看起来有多好用,在做项目的时候,有可能有各种条件的限制,也许需要对他们提供的方法进行重写才能满足需求,因此还是第三层视图(结合方法重写)或者是第一层视图APIView比较常用,在我们的项目中,基本都是
采用的APIView。但是对于一些非常规则的增删改查,使用第三层视图或者是视图集完全没有问题。
5、如何在项目中使用多数据库,不同的子应用对应不同的数据库:
· 在setting中的配置如下:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '127.0.0.1', # 数据库主机
'PORT': 3306, # 数据库端口
'USER': 'xxx', # 数据库用户名
'PASSWORD': 'xxx', # 数据库用户密码
'NAME': 'db_test_django' # 数据库名字
},
# 非默认数据库直接在这里进行配置即可
'record': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '127.0.0.1',
'PORT': 3306,
'USER': 'xxx',
'PASSWORD': 'xxx',
'NAME': 'record' # 数据库名字
}
}
· 在项目的python包中新建文件名为db_router.py,内容如下:
from django.conf import settings
DATABASE_MAPPING = settings.DATABASE_APPS_MAPPING
class DatabaseAppsRouter(object):
def db_for_read(self, model, **hints):
if model._meta.app_label in DATABASE_MAPPING:
return DATABASE_MAPPING[model._meta.app_label]
else:
return None
def db_for_write(self, model, **hints):
if model._meta.app_label in DATABASE_MAPPING:
return DATABASE_MAPPING[model._meta.app_label]
else:
return None
def allow_relation(self, obj1, obj2, **hints):
"""Allow any relation between apps that use the same database."""
db_obj1 = DATABASE_MAPPING.get(obj1._meta.app_label)
db_obj2 = DATABASE_MAPPING.get(obj2._meta.app_label)
if db_obj1 and db_obj2:
if db_obj1 == db_obj2:
return True
else:
return False
return None
def allow_syncdb(self, db, model):
"""Make sure that apps only appear in the related database."""
if db in DATABASE_MAPPING.values():
return DATABASE_MAPPING.get(model._meta.app_label) == db
elif model._meta.app_label in DATABASE_MAPPING:
return False
return None
def allow_migrate(self, db, app_label, model=None, **hints):
"""
Make sure the auth app only appears in the 'auth_db'
database.
"""
if db in DATABASE_MAPPING.values():
return DATABASE_MAPPING.get(app_label) == db
elif app_label in DATABASE_MAPPING:
return False
return None
· 在setting.py中配置数据库路由的位置:
DATABASE_ROUTERS = ['test_django.db_router.DatabaseAppsRouter'] # test_django就是项目的python包名
ATABASE_APPS_MAPPING = {
# '子应用名': '数据库名', 如果没有声明的子应用和数据库都会关联到默认的数据库
'other_db_models': 'logger'
}
· 在创建模型类的时候最好在class Meta 中添加 app_label = '子应用名' # 我没试过不添加app_label的情况,以后就都加上吧
· 以后在使用数据库迁移命令的时候,生成迁移文件的命令还是不变:
python manage.py makemigrations
在执行建表操作的时候如果保持原来的命令:
python manage.py migrate 则会找到非other_db_models子应用的模型去建表到默认数据库
如果想让数据库创建在logger数据库中,需要指定创建的数据库名称:
python manage.py migrate --database=logger
这表示other_db_models子应用中的Models都创建到logger数据库中
· 而数据的增删改查在使用非默认数据库时就不用再指明using('数据库名')了,采用正常的方式即可。