• 项目完成小结 - 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,有些影响体验。

    数据例子如下

    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好像绕了一圈,不过当时比较急没有研究

    参考资料

    admin后台优化

    定制化的项目其实Django Admin后台用得也不多了,不过作为报表看看数据或者进行简单的筛选操作还是足够的。

    本项目的admin界面基于simpleUI库定制

    从上一篇文章可以看到我对admin后台的主页进行了重写替换,效果如下

    image

    这个界面是用Bootstrap和AdminLTE实现的,AdminLTE这个组件库确实不错,在Bootstrap的基础上增加了几个很好看的组件,很有用~

    然后图标用的font-awesome,图表用的是chart.js,都属于是看看文档就会用的组件,官网文档地址我都整理在下面了,自取~

    有一点要注意的是,在SimpleUI里,自定义的主页是以iframe的形式实现的!而SimpleUI本身是Vue+ElementUI,所以想要在主页里跳转到admin本身的其他页面是很难实现的!这点要了解,我暂时没想到什么好的办法,要不下次试试别的admin主题好了~

    参考资料

    继续说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,作用差不多,都是提取数据表里某一列的信息,(这俩都跟分组有关)

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

    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

    <QuerySet [{'country': '中国'}, {'country': '越南'}, {'country': '新加坡'}, {'country': '马来西亚'}, {'country': '中国'}, {'country': '中国'}]>
    

    后者返回的是包含元组的Queryset

    <QuerySet [('中国',), ('越南',), ('新加坡',), ('马来西亚',), ('中国',), ('中国',)]>
    

    然后values_list还能加一个flat=True参数,直接返回包含数组的Queryset

    <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
        }
    ]
    

    搞定~

    聚合查询这方面还有很多场景例子,本文只说了个大概,后续有时间再写篇新博客来细说一下~

    参考资料

    使用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
    

    关键的配置我写了注释,很好懂。

    参考资料

    关于缓存问题

    上一篇文章有提到缓存的用法,Redis搭配Django原生的缓存装饰器cache_page是没啥问题的,但用第三方的drf-extensions里的cache_response装饰器的时候,就有个问题,不能针对不同的query params请求参数缓存响应

    比如说下面这两个地址,虽然是指向同一个接口,但参数不同,按理说应该返回不同的数据。

    但加上cache_response装饰器之后,无论传什么参数都返回同一个结果,目前我还没搞清楚是我哪里写错了还是这个库的bug~

    性能优化

    老生常谈…

    上篇文章也说了一点,不过没有具体。都说DjangoORM性能差,其实瓶颈还是在数据库IO这块,在耗时最长的IO操作面前,那点性能劣势其实也不算什么了(特别是我们这种toB的系统,没有高并发的需求)

    经过profile性能分析,瓶颈基本都在哪些统计类的接口,这类接口的特征就是要关联多个表查询,经常一个接口内需要多次请求数据库,所以优化思路就很明确了,减少数据库请求次数。

    两种思路

    • 一种是一次性把数据全部取出到内存,然后用pandas这类数据分析库来做聚合处理;
    • 一种是做先做预计算,然后保存中间结果,下次请求接口的时候直接去读取中间结果,把中间结果拿来做聚合

    最终我选择使用第二种方式,并且选择把中间结果存在MongoDB数据库里

    小结

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

    团队的话,我们这的领导属于是不太了解技术那种,然后抗压能力比较差,平时任务不紧急的时候就不怎么干扰我们的进度,在项目比较急的情况下就乱套了,瞎指挥、乱提需求、乱干扰进度,总之就是添乱拖后腿…

    当然最大的问题还是出在政企部门,前期在和客户的沟通中出了很大的问题,当然这可能和国企的架构混乱也有关系,甚至在协议方面也出了大问题,根本没有把需求写清楚,导致了交付后客户无限制地增加需求。

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


    __EOF__

  • 本文作者: 程序设计实验室
  • 本文链接: https://www.cnblogs.com/deali/p/16258515.html
  • 关于博主: 公众号:程序设计实验室,欢迎交流~
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    【Java】注解 之 处理注解
    Arduino驱动MAX30102心率血氧传感器模块
    Apache Kafka与Spring整合应用详解
    科技云报道:车云协同,云计算下一个主战场?
    DeepStream--测试resnet50分类模型
    vue制作自己的组件库(仿ElementUI)
    等保: Mysql配置ssl链接
    Linux系统---nginx(4)负载均衡
    【面试宝典】Android篇
    【Unity数据交互】JsonUtility的“爱恨情仇“
  • 原文地址:https://www.cnblogs.com/deali/p/16258515.html