Django进阶

多路数据库

配置

使用多个数据库的最简单方法是设置数据库路由方案。默认路由方案可确保对象对其原始数据库保持“粘性”(即,从foo数据库检索的对象将保存在同一数据库中)。默认路由方案可确保在未指定数据库的情况下,所有查询都会回退到default数据库。

在Django中支持多路数据库联合使用, ·详细内容可以查看官方文档

数据库配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'fangtx',
'USER': 'root',
'PASSWORD': '123456',
'HOST': '127.0.0.1',
'PORT': 3306,
'TIME_ZONE': 'Chongqing',
},
'hrs': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'hrs',
'USER': 'root',
'PASSWORD': '123456',
'HOST': '127.0.0.1',
'PORT': 3306,
'TIME_ZONE': 'Chongqing',
}
}

# 指定一个提供数据库路由的类
DATABASE_ROUTERS = [
'common.routers.BackendRouter'
]

数据库路由类

如上在common/routers.py中创建路由类BackendRouter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class BackendRouter:
"""
在已认证的app中,控制所有数据库模型操作的类
"""
def db_for_read(self, model, **hints):
"""
从对应数据库中读入
"""
if model._meta.app_label == 'hrs':
return 'hrs'
return 'default'

def db_for_write(self, model, **hints):
"""
写数据到对应数据库
"""
if model._meta.app_label == 'hrs':
return 'hrs'
return 'default'

def allow_relation(self, obj1, obj2, **hints):
"""
若app已注册,允许建立联系
"""
return True

def allow_migrate(self, db, app_label, model_name=None, **hints):
"""
允许迁移
"""
return None

迁移

反向生成和反向迁移

1
2
3
4
5
# 反向生成key为hrs的数据库模型到backend.models.py中
python manage.py inspectdb --database hrs > backend/models.py

# 正向迁移key为hrs的数据库中
python manage.py migrate --database=hrs

给模型文件指定数据库

如上, 在common.models.py中, 在需要的模型对象中

1
2
# 在meta中指定这个类对应的数据库, hrs是数据库配置中的key
app_label = 'hrs'

##正向迁移和反向生成

正向迁移

通过手动建立模型(models), 然后使用python manage.py makemigrations 进行生成迁移文件, 在使用python manage.py migrate执行迁移

反向生成

生成SQL文件

使用PowerDesigner反向生成逻辑模型, 将逻辑模型生成物理模型, 再将物理模型反向生成数据库建库建表sql文件

生成数据库表

1
2
# 登录mysql(以MySQL为例)
source <sql文件位置>

反向生成模型

1
2
3
# 在settings配置好数据库后, 默认反向生成key为default的数据库模型文件
# backend是app名
python manage.py inspectdb > backend/models.py

数据库性能优化

取消外键约束

如果要优化数据库性能, 就要减少不必要的外键约束, 通过代码层面的约束来保证参照完整性
使用正向工程时, 可以给ForeignKey添加一个db_contraint=False的设定来取消外键约束

1+N问题

如果查询对象的同时还是要查询它的关联对象, 就会引发1+N查询(或N+1查询), 程序的性能会下降的非常明显, 而且数据库的压力非常大。因此, 必须对自动生成的SQL语句进行优化.

如果有一对多关联, 需要用连接查询加载关联对象, 使用select_related()加载

如果有多对多关联, 需要用连接查询加载关联对象, 使用prefetch_related()加载

映射

通过映射(投影)的方式, 限制查询对象的字段

使用only()限制只要哪些字段

使用defer限制不要哪些字段

###原生SQL

Django中可以使用聚合函数aggregate()和分组函数annotate(), 但是真正使用对象来操作时, 非常麻烦, 可使用原生SQL代替ORM的方式

from django.db import connection, connections

通过引入connection来选择默认的数据库连接去建立游标对象; coonections可以用字典的形式取任意数据库的连接的游标, 它的键对应数据库配置的键

1
2
3
4
5
6
7
8
9
10
11
from django.db import connections
def chart_data(request):
names, totals = [], []
with connections['default'].cursor() as cursor:
cursor.execute('select name, total from ( '
' select agentid, count(agentid) as total from tb_agent_estate group by(agentid)) as t1 '
' inner join tb_agent t2 on t1.agentid=t2.agentid')
for row in cursor.fetchall():
names.append(row[0])
totals.append(row[1])
return JsonResponse({'names': names, 'totals': totals})

除此还有raw方法

1
Emp.objects.raw('select empno, ename, job from TbEmp where dno=10')

日志

日志相关

查看SQL执行情况

Django中可以配置日志, 通过日志查看数据库操作执行时间和执行次数

日志级别**

NOTSET < DEBUG < INFO < WARNING < ERROR < FATAL

日志级别越低,内容越详细

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
# 配置日志格式化器
'formatters': {
'simple': {
'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
},
'verbose': {
'format': '%(asctime)s %(levelname)s [%(process)d-%(threadName)s] '
'%(module)s.%(funcName)s line %(lineno)d: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
}
},
# 配置日志过滤器
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
# 配置日志处理器
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'filters': ['require_debug_true'],
'formatter': 'simple',
},
'file1': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'access.log',
'when': 'W0',
'backupCount': 12,
'formatter': 'simple',
'level': 'INFO',
},
'file2': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'error.log',
'when': 'D',
'backupCount': 31,
'formatter': 'verbose',
'level': 'WARNING',
},
},
# 配置日志器
'loggers': {
'django': {
'handlers': ['console', 'file1', 'file2'],
'propagate': True,
'level': 'DEBUG',
},
}
}

Debug工具

Django-Debug-Toolbar,非常好用的工具,可以查看当前django版本、设置、sql查询详情、请求、请求头、静态文件、模板、缓存、信号量、日志等(强烈建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app
INSTALLED_APPS = [
'debug_toolbar',
]

#config
DEBUG_TOOLBAR_CONFIG = {
# 引入jquery库
'JQUERY_URL': 'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js',
# 工具栏是否折叠
'SHOW_COLLAPSED': True,
# 是否显示工具栏
'SHOW_TOOLBAR_CALLBACK': lambda x: True,
}

# middleware
MIDDLEWARE = [
# 放在第一个
'debug_toolbar.middleware.DebugToolbarMiddleware',
]

短信服务

借助第三方短信服务提供商, 如云片,SendClound 螺丝帽, 秒赛, 互亿无线,云之讯等等这样的平台非常多, 我们选择的时候通常以它的性价比, 短信到达率,到达时间作为使用的参考点。下面以秒赛作为例子:

实现

进入秒赛官网,注册登录

进行签名配置

进行模板配置, 后续代码中的message内容就源于此

找到API或者实例中python部分

官方给的基础的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class SendCode:

def send(self, account, pswd, mobile, msg, ts, state):
url = 'http://139.196.108.241:8080' + {
1: '/Api/HttpSendSMYzm.ashx',
2: '/Api/HttpSendSMYx.ashx',
3: '/Api/HttpSendSMVoice.ashx'
}[state]
if ts != "":
m = hashlib.md5()
strs = account + pswd + str(ts)
m.update(strs.encode("utf8"))
pswd = m.hexdigest()
body = {"account": account, "pswd": pswd, "mobile": mobile, "msg": msg, "ts": str(ts)}

header_dict = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko',
"Content-Type": "application/x-www-form-urlencoded"}

response = requests.post(url, data=body, headers=header_dict)
return json.loads(response.text)
# account 用户账号
# pswd 必填参数。用户密码
# mobile 必填参数。合法的手机号码
# msg 必填参数。短信内容
# ts 可选参数,时间戳,格式yyyyMMddHHmmss
# state 必填参数 状态 1:验证码短信 2:营销短信 3:语音验证码

进行自己的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ACCOUNT = xxx
PASSWORD = xxx
def random_code(length=6):
items = StringIO()
for _ in range(length):
items.write(str(random.randint(0, 9)))
return items.getvalue()


def message(code):
return f'您的验证码是{code},如非本人操作,请忽略本短信!'

def send_msg(mobile, code):
sd = SendCode()
msg = message(code)
res = sd.send(ACCOUNT, PASSWORD, mobile, msg, (int(time.time())), 1)
return res

视图函数

1
2
3
4
5
6
7
8
9
def message_code(request, mobile):
"""发送短信验证码"""
code = random_code()
res = send_msg(mobile, code)
# 调用短信网关的函数返回字典对象就用JsonResponse处理
# 若返回字符串就用HttpResponse并制定MIME类型处理即可, ensure_ascii使在返回的数据能以真正的编码方式呈现(这里是utf-8,没有返回的数据就会以utf-8编码的字节形式呈现)
msg = json.dumps({'code': 200, 'msg': '短信已发出, 注意查收'}, ensure_ascii=False)
return HttpResponse(msg, content_type='application/json')
# return JsonResponse({'code': 200, 'msg': '短信已发出, 注意查收'})

缓存

缓存配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
CACHES = {
# 默认缓存
# LOCATION中配置redis的实际位置, 如果有奴隶redis, 只需将他们放到主人的后面即可.
# 例如: 'LOCATION': [master, slave1, slave2 ...]
# KEY_PREFIX 键的前缀, 在实际开发中区分不同应用的关键
# redis默认下有16库, 索引号0-15, 端口号6379后接数据库索引号
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://xx.xxx.xxx.xx:6379/0',
],
'KEY_PREFIX': 'app_name',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 1000,
},
'PASSWORD': '123456',
}
},
# 页面缓存
'page': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://xx.xxx.xxx.xx:6379/1',
],
'KEY_PREFIX': 'app_name:page',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 500,
},
'PASSWORD': '123456',
}
},
# 会话缓存
# TIMEOUT 超时时间, 此处为14天
'session': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://xx.xxx.xxx.xx:6379/2',
],
'KEY_PREFIX': 'app_name:session',
'TIMEOUT': 1209600,
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 2000,
},
'PASSWORD': '123456',
}
},
# 接口数据缓存
'api': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://xx.xxx.xxx.xx:6379/3',
],
'KEY_PREFIX': 'app_name:api',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 500,
},
'PASSWORD': '123456',
}
},
}

session信息缓存到redis中

在已经配置好缓存的情况下, 将下面的配置添加到系统配置中

1
2
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'session'

添加session信息

1
2
# 在视图函数执行的时候, 添加session信息, 将会将session缓存到redis中
request.session['mobile_code'] = code

短信记录缓存到redis中

由于已经配置好了redis缓存, 我们利用缓存直接在发送短信时候, 将自定义内容缓存到redis中;当然, 也可以通过redis连接的方式,将数据存入到redis中

通过cache和caches

1
2
3
4
5
# cache使用缓存中的key为default的库, cache使用键值对的方式去找配置的对应一个redis库 
from django.core.cache import cache, caches

# 在发送短信时, 将短信记录存入redis, 设置超时时间120秒
caches['default'].set(f'mobile_code:{mobile}', code, timeout=120)

通过redis连接

1
2
3
4
5
6
7
# 通过django-redis获得原生redis连接, 功能过多, 更具有拓展性
# django-redis默认使用Pickle序列化来(反)序列化字符串(字符串与字节变换)
from django_redis import get_redis_connection

# 在发送短信, 将短信记录存入到redis, 设置超时时间120秒, alias可以指定是哪个redis库
redis_conn = get_redis_connection(alias='default')
redis_conn.set(f'mobile_code:{mobile}', code, ex=120)

短信发送频率限制

实际使用中, 我们不可能让用户发送短信后又可以马上发复发送短信, 这无疑大大增加了成本, 并且短信的处理也是需要时间的, 因此应该将发送短信限制一个有效时间, 如60秒或者120秒

上面已经实现了将发送短信的记录存入到redis中, 因此我们只需在发送短信给一个判断, 去取这个redis记录, 如果没有, 当然你可以继续发送短信, 若有记录, 你就必须等待这个记录的有效时间长度了

1
2
3
4
5
6
7
# 采用django框架封装的redis调用(简单弱小)
if caches['default'].get(f'mobile_code:{mobile}'):
return JsonResponse({'code': 1001, 'message': '120秒后可重新发送'})

# 采用原生redis连接, 可以使用所有redis的方法或者类型
if get_redis_connection().get(f'mobile_code:{mobile}'):
return JsonResponse({'code': 1001, 'message': '120秒后可重新发送'})

中间件控制

对应上面短信有效时间限制处理的另外一种有效的方式, 就是中间件。它更准确应该成为拦截过滤器, 属于代理模式

django最新关于中间件的写法, 已经变成了类似于装饰器的写法, 在函数中写函数的方式。

中间件函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import re

from django.http import JsonResponse
from django_redis import get_redis_connection

PATTERN = re.compile(r'/common/send_msg/(?P<mobile>1[3-9]\d{9})')

def block_sms_middleware(get_resp):

def middleware(request, *args, **kwargs):
# 对应执行函数匹配执行的路由
if request.path.startswith('/common/send_msg'):
matcher = PATTERN.fullmatch(request.path)
# 手机号码格式正确
if matcher:
mobile = matcher.group('mobile')
conn = get_redis_connection(alias='default')
# 如果redis中已经存在短信记录, 就直接返回
if conn.get(f'mobile_code:{mobile}'):
return JsonResponse({'code': 1001, 'message': '120秒后可重新发送'})
else:
resp = get_resp(request, *args, **kwargs)
else:
return JsonResponse({'code': 1001, 'message': '手机号码不正确'})
else:
resp = get_resp(request, *args, **kwargs)

return resp

return middleware

配置 在系统配置文件中加入要使用中间件

1
'common.middlewares.block_sms_middleware',

异步化处理

如上, 调用第三方平台而且不需要马上出结果的应用, 我们都应该进行异步化处理。因为, 调用三方平台所需要的时间总是不可预估的, 不能使这个调用阻塞到其他功能,而解决这个问题的方式, 可以采用消息队列处理, 让执行函数延期执行

python三方库中, 提供一个异步化处理的库– celery

注意: 如果你使用的是windows系统并且celery的版本是4以上的时候, 因为celery对它已经不再支持, 你就需要考虑再安装一个eventlet的库.

配置

1
2
3
4
5
6
7
# 注册环境变量
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fangtx.settings')
# broker代表消息代理(从哪里获取队列服务), backend将所得结果存储起来
app = celery.Celery('common.utils',
broker='redis://:123456@xx.xxx.xxx.xx:6379/0',)
# 从项目的配置文件读取Celery配置信息
app.config_from_object('django.conf:settings')

使用

有了上面的配置, 就是使用`@app.task`作为你想要使用函数的装饰器

1
2
3
4
5
6
7
8
@app.task
def send_msg(mobile, code):
sd = SendCode()
msg = message(code)
res = sd.send(ACCOUNT, PASSWORD, mobile, msg, (int(time.time())), 1)
redis_conn = get_redis_connection(alias='default')
redis_conn.set(f'mobile_code:{mobile}', code, ex=120)
return res

在调用上面被装饰的函数时, 使用send_msg.delay()的方式, 使用异步化处理

1
2
3
4
5
6
7
8
def message_code(request, mobile):
"""发送短信验证码"""
code = random_code()
request.session['mobile_code'] = code
# delay延期处理
res = send_msg.delay(mobile, code)
msg = json.dumps({'code': 200, 'msg': '短信已发出, 注意查收'}, ensure_ascii=False)
return HttpResponse(msg, content_type='application/json')

此时, 我们发送的短信就会被缓存到redis的列表类型的数据中, 它会帮我们创建一个key为celery的列表数据, 并遵守队列即先入先出的原则。当我们使用下面的命令就会帮我们处理. 这也是典型的空间换取时间的做法。

1
celery -A common.utils worker -l info -P eventlet

Django-Rest-Framework

restrestful 知识点

官方文档地址

安装配置

安装

1
pip install djangorestframework

配置

1
2
3
INSTALLED_APPS = [
'rest-framework',
]

rest-framework配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
REST_FRAMEWORK = {
# 配置默认页面大小
'PAGE_SIZE': 5,
# 配置默认的分页类
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
# 配置默认的限流类
'DEFAULT_THROTTLE_CLASSES': ('rest_framework.throttling.AnonRateThrottle', ),
# 配置限流的策略
# 'DEFAULT_THROTTLE_RATES': {
# 'anon': '3/min',
# },
# 配置默认授权类
# 'DEFAULT_PERMISSION_CLASSES': (
# 'rest_framework.permissions.IsAuthenticated',
# ),
# 配置默认认证类
# 'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
# ),
}
  • rest-framework分页器有3种: 分别为PageNumberPagination / LimitOffsetPagination / CursorPagination
  • 限流策略, 即限制同一个ip访问接口的次数, 在FBV中使用@throttle_classes(())装饰器能够阻止限流

序列化器

为了得到自己想要的对象或者复杂对象的组合(即数据序列化),可以使用列表和字典类型取组装,但无疑效率低下,提倡的做法是利用rest_framework中的serializers编写序列化模块, 将数据序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from rest_framework import serializers
from common.models import District

class DistrictSerializer(serializers.ModelSerializer):

class Meta:
model = District
# fields = __all__ 就是使用所有字段
fields = ('distid', 'name')


class DistrictDetailSerializer(serializers.ModelSerializer):
# 定义一个使用序列化方法的字段
cities = serializers.SerializerMethodField()

# 方法名要与上面的字段对应
@staticmethod
def get_cities(district):
# 使用自关联字段去找distid
queryset = District.objects.filter(parent__distid=district.distid).only('distid', 'name')
# results = []
# for city in queryset:
# results.append(DistrictSerializer(city).data)
# return results
return DistrictSerializer(queryset, many=True).data

class Meta:
model = District
fields = ('distid', 'name', 'intro', 'cities')
class AgentSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = ('agentid', 'name', 'tel')


class EstateSerializer(serializers.ModelSerializer):
district = serializers.SerializerMethodField()
agents = serializers.SerializerMethodField()

@staticmethod
def get_district(estate):
return DistrictSerializer(estate.district).data

@staticmethod
def get_agents(estate):
return AgentSerializer(estate.agents, many=True).data

class Meta:
model = Estate
fields = '__all__'


class HouseTypeSerializer(serializers.ModelSerializer):

class Meta:
model = HouseType
fields = '__all__'

装饰器实现缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from django.views.decorators.cache import cache_page
from rest_framework.decorators import api_view
from rest_framework.response import Response
from api.serializers import DistrictSerializer
from api.serializers import DistrictDetailSerializer
# 指定http只使用GET操作
@api_view(['GET'])
# 对于经常使用又不修改的数据, 进行缓存, 下次使用的时候直接读缓存
@cache_page(timeout=None, cache='api')
# 通过cache_page装饰器, 配置缓存和超时时间
# @throttle_classes(())
# 阻止限流
def provinces(request):
# 通过自关联字段去找其有没有distid值, 若没有就是省级区域
queryset = District.objects.filter(parent__isnull=True)
# 通过自定义的序列化器序列化数据 many=True表示查询多个对象
serializer = DistrictSerializer(queryset, many=True)
# 通过序列化器获得序列化之后的数据(字典或列表)生成响应
# 方式1
# msg = json.dumps(serializer.data, ensure_ascii=False)
# return HttpResponse(msg, content_type='application/json')
# 方式2
# return JsonResponse(serializer.data, safe=False)
# 方式3
return Response(serializer.data)

@api_view(['GET'])
@cache_page(timeout=None, cache='api')
def cities(request, distid):
district = District.objects.filter(distid=distid).first()
serializer = DistrictDetailSerializer(district)
return Response(serializer.data)

使用id和pid的方式, pid为id的上一级id, 并且将pid指定为自关联字段, 通过上面的cities方法, 就能逐层显示数据(查询省显示市一级, 查询市显示县一级)。主要应用于能够自关联的模型结构。

通过省查看市一级 通过市查看县一级
district district

@cache_page用于基于函数的视图函数提供缓存的装饰器

rest-framework配置中已经提供了分页功能, 但是注意, 这个功能只对CBV(基于类的视图函数)生效, 而不对上面的FBV(基于函数的视图函数)生效。

基于类使用装饰器缓存

1
2
3
4
5
6
@method_decorator(cache_page(timeout=120, cache='api'), name='get')
class EstatesView(ListAPIView):
# 指定查询集
queryset = Estate.objects.all().select_related('district').prefetch_related('agents')
# 指定序列化器
serializer_class = EstateSerializer

对于使用类的视图函数, 要使用@method_decorator装饰器, name='get'参数表示装饰器要装饰的函数使用, 它也表示HTTP要操作的动词

使用装饰器非常灵活, 这样我们能够自定义

ListAPIView

1
2
3
4
5
6
# 通过drf-extension配置缓存, 使用CacheResponseMixin混入类
class EstatesView(CacheResponseMixin, ListAPIView):
# 指定查询集
queryset = Estate.objects.all().select_related('district').prefetch_related('agents')
# 指定序列化器
serializer_class = EstateSerializer

路由

1
2
3
urlpatterns = [
path('estates/', views.EstatesView.as_view(), name='estates'),
]

ListAPIView是继承于mixins.ListModelMixin, GenericAPIView, 所以有列出所有记录的功能

drf-extensions的使用需要安装pip install drf-extensions, 并且配置

1
2
3
4
5
6
7
8
9
# 配置DRF扩展来支持缓存API接口调用结果
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 300,
# 使用哪个缓存库的key
'DEFAULT_USE_CACHE': 'api',
# 配置默认缓存单个对象的key函数
'DEFAULT_OBJECT_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_object_cache_key_func',
# 配置默认缓存对象列表的key函数
'DEFAULT_LIST_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_list_cache_key_func',

ModelViewSet

1
2
3
4
5
class HouseTypeViewSet(CacheResponseMixin, ModelViewSet):
queryset = HouseType.objects.all()
serializer_class = HouseTypeSerializer
# 指定不分页
pagination_class = None

ModelViewSet这个类其实继承于多个操作模型的类, 因此它能够实现查看一条或多条记录, 增加, 修改, 删除的功能

1
mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet

除此还有一个ReadOnlyModelViewSet 它只支持查看一条和多条的功能

路由

1
2
3
4
router = DefaultRouter()
# 注册路由集, 可使用多个路由path
router.register('housetypes', HouseTypeViewSet)
urlpatterns += router.urls

分页

DRF中提供的分页共有三种,分别为

  • PageNumberPagination

根据Page_sizepage来进行分页

  • LimitOffsetPagination

根据限制查询数目和偏移量来分页

  • CursorPagination

该分页只显示前进和后退两个控件,不允许用户直接任意导航位置;基于游标的分页要结果集顺序唯一不变,因此通常将排序即ordering设置为时间戳字段(必须为queryset中的一个字段)

注:三种方式都提供了限制当前页总数的功能,防止被恶意调用

配置

1
2
3
4
5
# 使用基于PageNumber的分页器
REST_FRAMEWORK = {
'PAGE_SIZE': 5,
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination'
}

使用

只能CBV

1
2
3
4
5
6
7
8
# views.py
class HouseTypeViewSet(ModelViewSet):
"""视图集"""
queryset = Estate.objects.all().select_related('district').prefetch_related('agents')
serializer_class = EstateSerializer
# 使用默认的分页器
# pagination_class = None
pagination_class = CustomCursorPagination

自定义分页器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# paginations.py
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination


class CustomPageNumberPagination(PageNumberPagination):
max_page_size = 6
page_size = 4
page_size_query_param = 'page_size'


class CustomLimitOffsetPagination(LimitOffsetPagination):
default_limit = 4
max_limit = 6


class CustomCursorPagination(CursorPagination):
# 对当前页即时修改不生效,只有使用下次调用api才生效page_size
page_size = 5
max_page_size = 8
# 根据排序的字段默认升序,使用-name为降序
ordering = 'name'
page_size_query_param = 'page_size'

缓存

缓存主要是将热数据或者长时间不改变的数据缓存到数据库/文件系统/缓存服务器/本地内存缓存中,减轻数据库服务器的工作压力。

下面以redis缓存为例,采用django-redis三方库处理缓存(中文文档

配置

1
2
3
4
5
6
7
8
9
10
11
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PASSWORD": "mysecret"
}
}
}
# 也可以配置多个缓存,照着模板做如配置一个机遇session的缓存(默认是缓存在数据库中-->上面短信认服务-->缓存)

FBV

function base view基于函数实现视图显示

在FBV方式中可以采用三种方式处理缓存

  1. django提供cache_page
1
2
3
4
5
6
7
8
9
10
11
from rest_framework.decorators import api_view
from rest_framework.response import Response

from django.views.decorators.cache import cache_page

@api_view(['GET', 'POST'])
@cache_page(timeout=86400, key_prefix='rzlong')
def cache_foo(request):
queryset = District.objects.all()
serializer = DistrictSerializer(queryset, many=True)
return Response(serializer.data)

会建立两个缓存项(page和head),并且会拼接前缀和setting中配置组成新的前缀

  1. django提供的cache和caches
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.core.cache import caches, cache

from rest_framework.decorators import api_view
from rest_framework.response import Response

@api_view(['GET', 'POST'])
def cache_foo2(request):
data = caches['default'].get('api:foo2')
if data is None:
queryset = District.objects.all()
serializer = DistrictSerializer(queryset, many=True)
caches['default'].set('api:foo2', serializer.data, timeout=500)
return Response(serializer.data)
else:
return Response(data)

cache默认使用default库,caches必须指定使用的库;key是字符串,value是任何可以pickle序列化(转字节串)的python对象;除此其set方法也不能指定前缀;建立一个缓存项会使用settings中前缀名与自己设立的前缀进行组合

  1. django-redis提供的get_redis_connection(建议使用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django_redis import get_redis_connection

from rest_framework.decorators import api_view
from rest_framework.response import Response

@api_view(['GET',])
def estates(request):
conn = get_redis_connection('default')
data = conn.get('api:estates')
if data is None:
queryset = Estate.objects.all().select_related('district').prefetch_related('agents')
serializer = EstateSerializer(queryset, many=True)
conn.set('api:estates', json.dumps(serializer.data), timeout=400)
return Response(serializer.data)
else:
return Response(json.loads(data))

该方法是基于django.core.cache.caches实现的;测试的时候序列化后的对象是list类型,必须使用json进行序列化;并且此时不会使用settings中默认前缀进行拼接

CBV

  1. django提供的method_decorator – 将作用于函数的装饰器变成装饰类中方法的装饰器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 对View使用
    @method_decorator(cache_page(timeout=300), 'get')
    class CBVView(ListAPIView):
    queryset = District.objects.all()
    serializer_class = DistrictSerializer


    # 对ViewSet使用
    # @method_decorator(cache_page(timeout=3600), 'list')
    # @method_decorator(cache_page(timeout=3600), 'retrieve')
    class CBVViewSet(ModelViewSet):
    queryset = District.objects.all()
    serializer_class = DistrictSerializer
  1. drf-extensions三方库提供,使用混入类进行设定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # drf-extensions的配置
    REST_FRAMEWORK_EXTENSIONS = {
    # # 默认的缓存超时时间
    'DEFAULT_CACHE_RESPONSE_TIMEOUT': 300,
    # # 默认使用哪一组缓存
    'DEFAULT_USE_CACHE': 'default',
    # # 配置默认缓存单个对象的key函数
    'DEFAULT_OBJECT_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_object_cache_key_func',
    # # 配置默认缓存对象列表的key函数
    'DEFAULT_LIST_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_list_cache_key_func',
    }

    # 使用CacheResponseMixin
    class CBVViewSet(CacheResponseMixin, ModelViewSet):
    queryset = District.objects.all()
    serializer_class = DistrictSerializer
  1. 通过自定义中间件,使用decorator_from_middleware方法将中间件变成装饰器打在对应的类上

过滤

原生

通过重载get_queryset()方法,然后再方法中定义所需的筛选数据(或者排序其他的都行)的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# urls.py
urlpatterns = [
path('estates/', EstatesView.as_view()),
# {pk:pk_value}的字典数据会传到request中的kwargs参数中
path('estates/<int:pk>/', EstatesView.as_view()),
]
# views.py
class EstatesView(RetrieveAPIView, ListAPIView):

def get(self, request, *args, **kwargs):
if 'pk' not in kwargs:
self.action = 'list'
return ListAPIView.get(self, request, *args, **kwargs)
else:
self.action = 'retrieve'
return RetrieveAPIView.get(self, request, *args, **kwargs)

def get_serializer_class(self):
if self.action == 'list':
return EstateSimpleSerialzier
return EstateSerializer

def get_queryset(self):
"""Q对象实现或关系过滤"""
queryset = Estate.objects.all().select_related('district').prefetch_related('agents')
q = Q()
name = self.request.GET.get('name', None)
if name:
q |= Q(name__icontains=name)
distid = self.request.GET.get('distid', None)
if distid:
q |= Q(district__distid=distid)
queryset = queryset.filter(q)
return queryset

如果想要XxxView使用Retrieve和List两种APIView的组合(没有在通用模块generic定义),需要重新定义get()方法,如果要使两种方式返回的序列化数据结构也不同,需要重新定义序列化器和重写get_serializer_class()方法;除此Q()对象可以实现或的过滤

django-filter

  • 配置

使用三方库django-filter能够使处理对象的序列化非常容易,首先需要安装它

pip install django-filter

然后进入setting中配置

1
2
3
4
5
6
7
8
INSTALLED_APPS = [
'django-filters',
]

# 对全局有效,也可以直接View或ViewSet中单独定义
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}
  • django-filter**提供的过滤器(文档)有三种,分别为DjangoFilterBackend 容易定制化的过滤 / SearchFilter 用于搜索单个参数关键字过滤 / OrderingFilter 用于数据排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class EstatesView(RetrieveAPIView, ListAPIView):
queryset = Estate.objects.all().select_related('district').prefetch_related('agents')
filter_backends = [DjangoFilterBackend, OrderingFilter]
# 用于DjangoFilterBackend
filterset_fields = ['district', 'name']
# 用于搜索过滤
# search_fields = ['district', 'name']
# 用于数据排序
ordering = 'district'
ordering_fields = ['district', 'name']
def get(self, request, *args, **kwargs):
pass

def get_serializer_class(self):
pass

ordering指定默认排序方式,ordering_fields可用于排序的可选参数集

  • 通过自定义过滤器类实现过滤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import django_filters as filters
from django.db.models import Q

class HouseInfoFilter(filters.FilterSet):
title = filters.CharFilter(field_name='title', lookup_expr='icontains')
hmin = filters.NumberFilter(field_name='price', lookup_expr='gte')
hmax = filters.NumberFilter(field_name='price', lookup_expr='lte')
district = filters.NumberFilter(method='filter_by_district')

@staticmethod
def filter_by_district(queryset, param_name, param_value):
queryset = queryset.filter(Q(district_level2__distid=param_value) |
Q(district_level3__distid=param_value)
)
return queryset

class Meta:
model = HouseInfo
fields = ('title', 'hmin', 'hmax', 'district')

class EstatesView(RetrieveAPIView, ListAPIView):
queryset = 'xxx'
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_class = HouseInfoFilter

定义过滤器类,method()指定复杂的过滤方法,field_name指定模型字段名,lookup_expr指定过滤规则,filterset_class指定过滤器类

认证

JWT与令牌

我们知道目前使用的CS结构是基于HTTP协议的,而HTTP协议是一个无状态的协议,也就是说在两次请求之间不能保存任何会话状态,也就不能跟踪用户。但是我们有必须保存用户状态,目前有三种方式可选:

  • URL重写,就是访问服务端时通过修改URL,加入参数以达到保存用户状态的作用,典型的就是百度搜索关键词进行URL重写
  • 隐式表单域,即埋点,就是在表单标签中加入类型为hidden的标签,以达到故武器进行识别,典型的就是django中CSRF(跨站请求伪造问题)
  • 本地存储,通过cookie、localStorage或sessionStorage进行存储服务器上表示用户身份的信息

然后我们本地存储开始谈,它主要包含三种思路:

  • 第一种使用cookie+session的方式,用户向服务器发出请求(登陆),服务器会为改用户创建一个session对象保存用户状态,同时将sessionid作为cookie的一个字段的值保存在浏览器上,下一次就可以通过session的方式进行身份认证;(问题:扩展性不好,在服务器集群情况下,需要做session数据共享;同时内存消耗也大)
  • 第二种使用session持久化,基于上面的方案,当时是将session数据保存到数据库中,而不是在内存中(问题:数据库出错,直接就验证不了用户会崩溃)
  • 第三种直接不使用session,通过JWT(JSON Web Token)的方式,将代表用户身份的标识(token)传到浏览器上保存下来,下次访问时带上token一起访问(问题:用户验证信息保存在浏览器中,一旦指定有效时长,服务器端不能修改)

目前来看使用JWT是最有利用服务器的方式,python实现它的方式是三方库PyJWT,JWT能够通过签名和加密,有效的防止数据被篡改;同时,也可以指定有效时长,使token失效。

身份令牌有了,就可以通过认证和JWT结合使用,为用户生成身份令牌,认证类再进行识别令牌达到认证的目的

自定义认证类

1
2
3
4
5
6
7
8
9
10
11
12
13
class CustomAuthentication(BaseAuthentication):

def authenticate(self, request):
token = request.META.get('HTTP_TOKEN', None)
if token:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms='HS256')
user = User()
user.id = payload['data']['userid']
return user, token
except:
raise AuthenticationFailed()
raise AuthenticationFailed()

官方文档

授权

限流

Django-Debug-Toolbar

用于Django调试的工具栏, 提供了查看SQL执行记录 / 查看缓存记录 / 查看执行时间 / 查看日志 / 查看模板 / 查看使用静态文件记录等功能, 非常强大。

安装

1
pip install django-debug-toolbar

配置

系统配置

1
2
3
INSTALLED_APPS = [
'debug_toolbar',
]
1
2
3
4
5
DEBUG_TOOLBAR_CONFIG = {
'JQUERY_URL': 'https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js',
'SHOW_COLLAPSED': True,
'SHOW_TOOLBAR_CALLBACK': lambda x: True,
}
1
2
3
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
]

路由

1
2
3
4
from django.conf import settings
if settings.DEBUG:
import debug_toolbar
urlpatterns.insert(0, path("__debug__/", include(debug_toolbar.urls)))

运行

DRF

图形验证码

-------------end-------------
0%