如果沒有良好的分層,那麼一個Web專案最終會走向崩潰。
緣由
Django專案,一般是按照 APP 切分的,並且每一個 APP 有相似的結構,大家都是『各自管好自己份內的事情』,頗有點像微服務的味道。但是許多人寫Django 的程式碼,沒有一定的章法,一千個人一千種風格。甚至於,在Controller層出現直接裸呼叫UserModel.objects.filter
的情況也不少見。然而,我們發現,針對資料庫的操作,很多都是通用的,這時候,單獨抽取出一層,就顯得很有必要了。
參考的物件
如何組織、設計我們的這個層呢?我們沒有必要自己絞盡腦汁閉門造車,可以參考成熟專案的做法。Java Spring 是我參考的物件,一般的Spring 專案,有著很明確分層結構,雖然初期需要寫較多的程式碼,但是給後期的程式碼維護,著實帶來了很多便利。
一般會分為如下層級:
Controller
Service
Repository( DAO )
(Mapper,可選,如果使用了Mybatis的話)
Model
複製程式碼
結合Django的特性,我們發現Django的Manager層(即:XXModel.objects
),其實是對應著 DAO 層的,只不過大家的叫法不同。
我們不妨將抽取的單獨層,叫做DAO 好了,後面我們也會看到,它其實就是對 Manager 層的API進行組合,對上提供一些通用的操作。
如何寫
在正式寫之前,我們可以先根據實際經驗,思考:應該提供哪些通用的API?下面是我根據自己的經驗,得出的結論:
- save(obj)
- delete(obj)
- update(obj)
- findOne/findAll
那麼通過什麼手段實現呢?得益於 Python 強大的語言特性,讓我們的程式碼可以不必寫得像 Java 那樣冗長乏味。我的步驟如下:
- 首先封裝一個基礎父類,把所有通用操作,都放置到它裡面,就叫做
BaseDAO
吧。 - 其餘人,繼承這個父類。
- 在 Controller或者Service層使用
下面是程式碼片段:
# 基於 Python 3.5 的程式碼, 如果想要放到 Python 2 中的同學, 可以去掉 Type Hint
from .BaseModel import BaseModel # 一般的專案, 都會封裝一個基類Model
class BaseDAO:
# 子類必須覆蓋這個
MODEL_CLASS = BaseModel
SAVE_BATCH_SIZE = 1000
def save(self, obj):
"""insert one
:param obj:
:return:
"""
if not obj:
return False
obj.save()
return True
def save_batch(self, objs, *, batch_size=SAVE_BATCH_SIZE):
"""insert batch
:type objs: list[BaseModel]
:param objs:
:return:
"""
if not objs:
return False
self.MODEL_CLASS.objects.bulk_create(objs, batch_size=batch_size)
return True
def delete(self, obj):
if not obj:
return False
obj.delete()
return True
def delete_batch(self, objs):
if not objs:
return False
for obj in objs:
self.delete(obj)
return True
def delete_batch_by_query(self, filter_kw: dict, exclude_kw: dict):
"""批量刪除
"""
self.MODEL_CLASS.objects.filter(**filter_kw).exclude(**exclude_kw).delete()
return True
def delete_by_fake(self, obj):
"""假刪除/偽刪除
"""
if obj is None:
return False
obj.is_deleted = True
obj.save()
return True
def update(self, obj):
if not obj:
return False
obj.save()
return True
def update_batch(self, objs):
if not objs:
return False
for obj in objs:
self.update(obj)
return True
def update_batch_by_query(self, query_kwargs: dict, exclude_kw: dict, newattrs_kwargs: dict):
self.MODEL_CLASS.objects.filter(**query_kwargs).exclude(**exclude_kw).update(**newattrs_kwargs)
def find_one(self, filter_kw: dict, exclude_kw: dict, order_bys: list):
"""
:param query_kwargs:
:rtype: BaseModel | None
:return:
"""
qs = self.MODEL_CLASS.objects.filter(**filter_kw).exclude(**exclude_kw)
if order_bys:
qs = qs.order_by(*order_bys)
return qs.first()
def find_queryset(self, filter_kw: dict, exclude_kw: dict, order_bys: list):
"""
:param filter_kw:
:return:
"""
return self.MODEL_CLASS.objects.filter(**filter_kw).exclude(**exclude_kw)
def find_all_model_objs(self, filter_kw: dict, exclude_kw: dict, order_bys: list) -> list:
return self.find_queryset(filter_kw, exclude_kw, order_bys).all()
def is_exists(self, filter_kw:dict, exclude_kw:dict) -> bool:
return self.MODEL_CLASS.objects.filter(**filter_kw).exclude(**exclude_kw).exists()
def get_count(self, filter_kw:dict, exclude_kw:dict) -> int:
return self.MODEL_CLASS.objects.filter(**filter_kw).exclude(**exclude_kw).count()
複製程式碼
如何使用
比如在某個 Django APP 中使用:
某個Django APP, 這裡是 goods
goods/
views.py
tests.py
dao/ ( 也可以單獨放到一個 dao.py 中, 看自己喜好. 我比較喜歡弄一個目錄, 並且每一個py 檔案一個class, 這裡保持和java一樣的風格)
GoodsDao.py
models.py
GoodsDao.py內容
from ..models import Goods
from common_base import BaseDAO
class GoodsDao(BaseDAO):
MODEL_CLASS = Goods
複製程式碼
上層使用:基本可以很自由的使用。都是一些通用的CURD 操作,變化不大,並且再也不用寫冗長的XXModel.objects.filter
了
延伸
通過上面總結,我們可以看到,確實帶來了一個良好的封裝,雖然初期需要多寫一些程式碼,但是後期程式碼維護比較舒服。另外一個問題是:是不是就該摒棄Goods.objects.filter
這種寫法呢?
我覺得不是的,Goods.objects.filter
仍然可以自由使用,只不過在 DAO 無法應對的情況下(你又懶得再封裝了,因為是低頻操作),就該輪到它出場了。它們兩者應該是互為補充,互相融合,各自都有自己的使用場景。原始的寫法適用於『比較低頻、臨時的CURD操作』,DAO則適用於『比較高頻、通用的CURD操作』。
另外,Python 世界流行的 ORM ,不只有 Django ORM。比如SQLAlchemy等,你也可以封裝出同樣類似的 DAO 層,讓自己的程式碼越寫越舒服。