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