為你的Django APP 寫一層 DAO

浮生若夢的程式設計發表於2019-03-04

如果沒有良好的分層,那麼一個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?下面是我根據自己的經驗,得出的結論:

  1. save(obj)
  2. delete(obj)
  3. update(obj)
  4. findOne/findAll

那麼通過什麼手段實現呢?得益於 Python 強大的語言特性,讓我們的程式碼可以不必寫得像 Java 那樣冗長乏味。我的步驟如下:

  1. 首先封裝一個基礎父類,把所有通用操作,都放置到它裡面,就叫做 BaseDAO 吧。
  2. 其餘人,繼承這個父類。
  3. 在 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 層,讓自己的程式碼越寫越舒服。

相關文章