為你的Django APP 寫一層 DAO
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
下面是程式碼片段:
# 基於 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 層,讓自己的程式碼越寫越舒服。