前言
好久沒更新部落格了,最近依然是在做之前部落格說的這個專案:專案完成 - 基於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
<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
}
]
搞定~
聚合查詢這方面還有很多場景例子,本文只說了個大概,後續有時間再寫篇新部落格來細說一下~
參考資料
- 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請求引數快取響應
比如說下面這兩個地址,雖然是指向同一個介面,但引數不同,按理說應該返回不同的資料。
但加上cache_response
裝飾器之後,無論傳什麼引數都返回同一個結果,目前我還沒搞清楚是我哪裡寫錯了還是這個庫的bug~
效能優化
老生常談…
上篇文章也說了一點,不過沒有具體。都說DjangoORM效能差,其實瓶頸還是在資料庫IO這塊,在耗時最長的IO操作面前,那點效能劣勢其實也不算什麼了(特別是我們這種toB的系統,沒有高併發的需求)
經過profile效能分析,瓶頸基本都在哪些統計類的介面,這類介面的特徵就是要關聯多個表查詢,經常一個介面內需要多次請求資料庫,所以優化思路就很明確了,減少資料庫請求次數。
兩種思路
- 一種是一次性把資料全部取出到記憶體,然後用pandas這類資料分析庫來做聚合處理;
- 一種是做先做預計算,然後儲存中間結果,下次請求介面的時候直接去讀取中間結果,把中間結果拿來做聚合
最終我選擇使用第二種方式,並且選擇把中間結果存在MongoDB資料庫裡
小結
本專案到這裡只是出了一個階段性的成果,還是未完結,從這個專案中也發現了很多問題,團隊的、自身的,都有。
團隊的話,我們這的領導屬於是不太瞭解技術那種,然後抗壓能力比較差,平時任務不緊急的時候就不怎麼干擾我們的進度,在專案比較急的情況下就亂套了,瞎指揮、亂提需求、亂干擾進度,總之就是添亂拖後腿…
當然最大的問題還是出在政企部門,前期在和客戶的溝通中出了很大的問題,當然這可能和國企的架構混亂也有關係,甚至在協議方面也出了大問題,根本沒有把需求寫清楚,導致了交付後客戶無限制地增加需求。
實際上一個政企專案涉及到太多非技術因素了,其實這本不是我們技術人員需要關心的,但現實就是這樣,唉。