项目完成小结 - Django3.x版本 - 开发部署小结 (2)
前言
好久没更新博客了,最近依然是在做之前博客说的这个项目:项目完成 - 基于Django3.x版本 - 开发部署小结
这项目因为前期工作出了问题,需求没确定好,导致了现在要做很多麻烦的工作,搞得大家都身心疲惫。唉,只能说技术团队,有里一个靠谱有能力的领导是非常重要的。
进入正题
本文继续记录Django项目开发的一些经验。
本次的项目依然基于我定制的「DjangoStarter」项目模板来开发,该项目模板(脚手架)整合了一些常用的第三方库以及配置,内置代码生成器,只要专注业务逻辑实现即可。
数据批量导入
上篇文章说到我写了脚本导入大量数据的时候很慢,然后有网友评论可以使用bulk_create
,所以在第二期的新增需求中,我处理完数据就使用bulk_create
来导入,速度确实有了可观的提升,应该是能达到原生SQL的性能。
先把Model
的实例全都添加到列表里面,然后再批量导入,就很快了。
写了个伪代码例子
result = data_proc()
data = []
for item in result:
print(f"处理:{item['name']}")
data.append(ModelObj(name=item['name']))
print('正在批量导入')
ModelObj.objects.bulk_create(data)
print('完成')
还有除了这个批量新增的API,DjangoORM还支持批量更新,bulk_update
,用法同这个批量新增。
数据处理
上次需求很急的情况下,拿到了几百M的Excel数据之后,我直接用Python的openpyxl
库来预处理成JSON格式,然后再一条条导入数据库
而且这些数据还涉及到多个表,这就导致了数据处理和导入速度异常缓慢
当时是DB manager直接把Excel导入到数据库临时表处理的,但是后面发现SQL处理的数据,清洗过后还是出了很多错误
所以后面来的新一批数据,我选择自己来搞,SQL还是不太适合做这些数据清洗~
直接用openpyxl
来处理Excel也太外行了,Python做数据分析有很多工具,都可以利用起来,比如pandas
。
这次新需求给的Excel很恶心,里面一堆合并的单元格,虽然是好看,但要导入数据库很麻烦啊!
不过还好pandas的数据处理功能足够强大,可以应付这种情况,然后为了整个数据处理的过程更直观,我安排上了jupyter
,pycharm现在已经集成了,体验比网页版的好一点,不过实际使用的时候发现有一些bug,有些影响体验。
数据例子如下
第一列序号是没用的,不管,然后看到这里面姓名的人和家庭成员直接应该是一对多的关系,为了好看合并了单元格,这样处理的时候就很恶心了,合并后的单元格,pandas读取进来只有第一行是有数据的。
不过我们可以用pandas的数据补全功能来处理。
简单的处理单元格合并的代码参考:
import pandas as pd
xlsx1 = pd.ExcelFile('文件名.xlsx')
# 参数0代表第一个工作表,header=0代表第一行作为表头,uescols表示读取的列范围,我们不要第一列那个序号
df = pd.read_excel(xlsx1, 0, header=0, usecols='B:G')
df['姓名'].fillna(method='pad', inplace=True)
df['性别'].fillna(method='pad', inplace=True)
df['出生年月'].fillna(method='pad', inplace=True)
df['联系人'].fillna(method='pad', inplace=True)
df['联系电话'].fillna(method='pad', inplace=True)
代码里有注释,用fillna
填充缺失的字段即可
然后再把DateFrame
转换成比较容易处理的JSON格式(其实在Python里是dict)
json_str = df.to_json(orient='records')
parsed = json.loads(json_str)
这样出来就是键值对的数据了
PS:好像可以直接遍历df来获取数据,转JSON好像绕了一圈,不过当时比较急没有研究
参考资料
使用Pandas读取结构不良 Excel 的2个方法:https://www.shouxicto.com/article/1642.html
PANDAS合并单元格的表格读取后处理:http://zhangqijun.com/2733-2/
admin后台优化
定制化的项目其实Django Admin后台用得也不多了,不过作为报表看看数据或者进行简单的筛选操作还是足够的。
本项目的admin界面基于simpleUI库定制
从上一篇文章可以看到我对admin后台的主页进行了重写替换,效果如下
这个界面是用Bootstrap和AdminLTE实现的,AdminLTE这个组件库确实不错,在Bootstrap的基础上增加了几个很好看的组件,很有用~
然后图标用的font-awesome,图表用的是chart.js,都属于是看看文档就会用的组件,官网文档地址我都整理在下面了,自取~
有一点要注意的是,在SimpleUI里,自定义的主页是以iframe的形式实现的!而SimpleUI本身是Vue+ElementUI,所以想要在主页里跳转到admin本身的其他页面是很难实现的!这点要了解,我暂时没想到什么好的办法,要不下次试试别的admin主题好了~
参考资料
AdminLTE:https://adminlte.io/docs/3.2/components/boxes.html FontAwesome:http://www.fontawesome.com.cn/faicons/ Chart.js:https://www.chartjs.org/docs/latest/ Django进阶(1): admin后台高级玩法(多图) Django实战: 手把手教你配置Django SimpleUI打造美丽后台(多图):https://zhuanlan.zhihu.com/p/372185998
继续说Django的聚合查询
上一篇文章有提到聚合查询,但是没有细说,本文主要介绍这几个:
aggregate annotate values values_list
根据我目前的理解,aggregate
和annotate
的第一个区别是,前者返回dict,后者返回queryset,可以继续执行其他查询操作。
aggregate
然后就是使用场景的区别,aggregate
一般用于整体数据的统计,比如说
统计用户的男女数量
from django.db.models import Count
result1 = User.objects.filter(gender='男').aggregate(male_count=Count('pk', distinct=True))
result2 = User.objects.filter(gender='女').aggregate(female_count=Count('pk', distinct=True))
PS:其实这里的
Count
函数里,可以不加distinct
参数的,毕竟主键(pk
)应该是不会重复的
这样返回的数据是
# result1
{
"male_count": 100
}
# result2
{
"female_count": 100
}
应该很容易理解
annotate
annotate
的话,一般是搭配values
这种分组操作使用,例子:
from django.db.models import Count
result1 = User.objects.values('gender').annotate(count=Count('pk'))
返回结果
[
{
"gender": "男",
"count": 100
},
{
"gender": "女",
"count": 100
}
]
简而言之,就是在values
分组之后,annotate
对数据进行聚合运算之后把自定义的字段插入每一组内~ 有点拗口,反正看上面的代码就好理解了。
values / values_list
最后是values
和values_list
,作用差不多,都是提取数据表里某一列的信息,*(这俩都跟分组有关)*
比如说我们的用户表长这样
id | name | gender | country |
---|---|---|---|
1 | 人1 | 男 | 中国 |
2 | 人2 | 女 | 越南 |
3 | 人3 | 男 | 新加坡 |
4 | 人4 | 女 | 马来西亚 |
5 | 人5 | 男 | 中国 |
6 | 人6 | 男 | 中国 |
我们可以用这段代码提取所有国家
User.objects.values("country")
# 或者
User.objects.values_list("country")
前者根据指定的字段分组后返回包含字典的Queryset
'country': '中国'}, {'country': '越南'}, {'country': '新加坡'}, {'country': '马来西亚'}, {'country': '中国'}, {'country': '中国'}]>
后者返回的是包含元组的Queryset
'中国',), ('越南',), ('新加坡',), ('马来西亚',), ('中国',), ('中国',)]>
然后values_list
还能加一个flat=True
参数,直接返回包含数组的Queryset
'中国', '越南', '新加坡', '马来西亚', '中国', '中国']>
这就可以很直观的看出来这俩函数的作用了。
然后结合上面的annotate
再说一下,假如我们要计算每个国家有多少人,可以用这个代码
User.objects.values("country").annotate(people_count=Count('pk'))
结果大概是这样
[
{
"country": "中国",
"people_count": 3
},
{
"country": "越南",
"people_count": 1
},
{
"country": "新加坡",
"people_count": 3
},
{
"country": "马来西亚",
"people_count": 3
}
]
搞定~
聚合查询这方面还有很多场景例子,本文只说了个大概,后续有时间再写篇新博客来细说一下~
参考资料
Python 教程之如何在 Django 中实现分组查询:https://chinese.freecodecamp.org/news/introduction-to-django-group/ aggregate和annotate的区别:https://www.cnblogs.com/Young-shi/p/15174328.html values / values_list:https://www.jianshu.com/p/e92ab45075d5 django_filter的values / values_list:https://blog.csdn.net/weixin_40475396/article/details/79529256
使用docker部署MySQL数据库
虽然之前看到有人说MySQL不适合用docker来部署,不过docker实在方便,优点掩盖了缺点,所以本项目还是继续使用docker。
继续用docker-compose来编排容器。
首先如果在本地启动一个测试用的MySQL,可以找个空目录,单独创建一个docker-compose.yml
文件,配置内容在下面,然后运行docker-compose up
。
下面的配置里我做了volumes映射,MySQL数据库的文件会保存在本地这个目录下的mysql-data
文件夹里
version: "3"
services:
mysql:
image: daocloud.io/mysql
restart: always
volumes:
- ./mysql-data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=mysql-admin
- MYSQL_USER=test
- MYSQL_PASS=yourpassword
ports:
- "3306:3306"
使用ports
开启端口,方便我们使用Navicat等工具连接数据库操作。
下面是整合在web项目中的配置(简化的配置,详细配置可以看我的DjangoStarter项目模板)
version: "3"
services:
mysql:
image: daocloud.io/mysql
restart: always
volumes:
- ./mysql-data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=mysql-admin
# 注意这里使用expose而不是ports里,这是暴露端口给其他容器使用,但docker外部就无法访问了
expose:
- 3306
web:
restart: always # 除正常工作外,容器会在任何时候重启,比如遭遇 bug、进程崩溃、docker 重启等情况。
build: .
command: uwsgi uwsgi.ini
volumes:
- .:/code
ports:
- "80:8000"
# 在依赖这里指定mysql容器,然后才能连接到数据库
depends_on:
- mysql
关键的配置我写了注释,很好懂。
参考资料
Docker-compose封装mysql并初始化数据以及redis:https://www.cnblogs.com/xiao987334176/p/12669080.html 你必须知道的Docker数据卷(Volume):https://www.cnblogs.com/edisonchou/p/docker_volumes_introduction.html
关于缓存问题
上一篇文章有提到缓存的用法,Redis搭配Django原生的缓存装饰器cache_page
是没啥问题的,但用第三方的drf-extensions
里的cache_response
装饰器的时候,就有个问题,不能针对不同的query params请求参数缓存响应
比如说下面这两个地址,虽然是指向同一个接口,但参数不同,按理说应该返回不同的数据。
http://example.api/?type=1 http://example.api/?type=2
但加上cache_response
装饰器之后,无论传什么参数都返回同一个结果,目前我还没搞清楚是我哪里写错了还是这个库的bug~
性能优化
老生常谈…
上篇文章也说了一点,不过没有具体。都说DjangoORM性能差,其实瓶颈还是在数据库IO这块,在耗时最长的IO操作面前,那点性能劣势其实也不算什么了(特别是我们这种toB的系统,没有高并发的需求)
经过profile性能分析,瓶颈基本都在哪些统计类的接口,这类接口的特征就是要关联多个表查询,经常一个接口内需要多次请求数据库,所以优化思路就很明确了,减少数据库请求次数。
两种思路
一种是一次性把数据全部取出到内存,然后用pandas这类数据分析库来做聚合处理; 一种是做先做预计算,然后保存中间结果,下次请求接口的时候直接去读取中间结果,把中间结果拿来做聚合
最终我选择使用第二种方式,并且选择把中间结果存在MongoDB数据库里
小结
本项目到这里只是出了一个阶段性的成果,还是未完结,从这个项目中也发现了很多问题,团队的、自身的,都有。
(不好说太多,删了)
实际上一个政企项目涉及到太多非技术因素了,其实这本不是咱技术人员需要关心的,但现实就是这样,唉。