项目完成小结 - Django3.x版本 - 开发部署小结 (2)

程序设计实验室

共 6553字,需浏览 14分钟

 ·

2022-05-14 20:09

前言

好久没更新博客了,最近依然是在做之前博客说的这个项目:项目完成 - 基于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,有些影响体验。

数据例子如下

image

第一列序号是没用的,不管,然后看到这里面姓名的人和家庭成员直接应该是一对多的关系,为了好看合并了单元格,这样处理的时候就很恶心了,合并后的单元格,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后台的主页进行了重写替换,效果如下

image

这个界面是用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

根据我目前的理解,aggregateannotate的第一个区别是,前者返回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

最后是valuesvalues_list,作用差不多,都是提取数据表里某一列的信息,*(这俩都跟分组有关)*

比如说我们的用户表长这样

idnamegendercountry
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数据库里

小结

本项目到这里只是出了一个阶段性的成果,还是未完结,从这个项目中也发现了很多问题,团队的、自身的,都有。

(不好说太多,删了)

实际上一个政企项目涉及到太多非技术因素了,其实这本不是咱技术人员需要关心的,但现实就是这样,唉。


浏览 23
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报