專案完成小結 - Django3.x版本 - 開發部署小結 (2)

程式設計實驗室發表於2022-05-11

前言

好久沒更新部落格了,最近依然是在做之前部落格說的這個專案:專案完成 - 基於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資料庫裡

小結

本專案到這裡只是出了一個階段性的成果,還是未完結,從這個專案中也發現了很多問題,團隊的、自身的,都有。

團隊的話,我們這的領導屬於是不太瞭解技術那種,然後抗壓能力比較差,平時任務不緊急的時候就不怎麼干擾我們的進度,在專案比較急的情況下就亂套了,瞎指揮、亂提需求、亂干擾進度,總之就是添亂拖後腿…

當然最大的問題還是出在政企部門,前期在和客戶的溝通中出了很大的問題,當然這可能和國企的架構混亂也有關係,甚至在協議方面也出了大問題,根本沒有把需求寫清楚,導致了交付後客戶無限制地增加需求。

實際上一個政企專案涉及到太多非技術因素了,其實這本不是我們技術人員需要關心的,但現實就是這樣,唉。

相關文章