为你的Django APP 写一层 DAO

如果没有良好的分层,那么一个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 层,让自己的代码越写越舒服。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容