集中封装 ORM 调用链以避免重复并提高可维护性的标准架构模式是什么?

sep*_*ehr 2 python database architecture design-patterns django-models

不久前我的一个好同事问了这个问题,现在我将自己的答案公开并分享出来,不仅供以后参考,也可以向社区的答案学习。

\n
\n

我想在应用程序中使用某种数据库层。大多数应用程序都使用 ORM,并且可以在那里构建复杂的查询。如果我不想\xe2\x80\x99 不想使用查询生成器并且更喜欢将其封装在函数或类中怎么办?例如代替:

\n\n
def get(category_id: int) -> HttpResponse:\n    posts = list(\n        Post.objects\n            .filter(category_id=category_id)\n            .filter(deleted_at__isnull=True)\n            .filter(published_at__gte=yesterday)\n    )\n    return HttpResponse(posts)\n
Run Code Online (Sandbox Code Playgroud)\n

我想做一些类似的事情:

\n
def recent_posts_from_category(category_id: int) -> List[Post]:\n    return list(\n        Post.objects\n            .filter(category_id=category_id)\n            .filter(deleted_at__isnull=True)\n            .filter(published_at__gte=yesterday)\n    )\n\ndef get(category_id: int) -> HttpResponse:\n    posts = recent_posts_from_category(category_id)\n    return HttpResponse(posts)\n
Run Code Online (Sandbox Code Playgroud)\n

您如何称呼这种方法/模式?你会把代码放在哪里?

\n

创建一个名为的模块或命名空间database听起来太宽泛了。我不想将这些函数放入utilshelpers命名空间,因为它们显然不是实用程序。

\n

这个词Repository用在这里合适吗?我不会封装所有内容(读取和写入),使用 ValueModel 代替 ORM 模型 (ActiveRecord),其抽象目标是能够在需要时替换 ORM,构建自定义units of work也超出了范围。

\n

但我正在寻找一些 DRY 助手,你知道,以避免搜索整个存储库,以便在我需要稍微改变行为时找到所有类似的用例。避免重复 ORM 调用链。

\n

sep*_*ehr 7

\n

你感兴趣的主题,用EAA 的 Fowler's P的话说,叫做“数据源架构模式”。那么,什么是数据源层,它的常见架构模式是什么?

\n

数据源层

\n
\n

我想在应用程序中使用某种数据库层。大多数应用程序都使用 ORM,并且可以在那里构建复杂的查询。如果我不想\xe2\x80\x99 不想使用查询生成器并更喜欢\n将其封装在函数或类中怎么办

\n
\n

从广义上讲,您可能已经知道,您正在寻求实现一个数据访问层 (DAL),应用程序的其余部分将与该层进行交互;作为间接层,不仅有助于与基础设施(特定数据库或 ORM 框架)解耦,还有助于消除重复。它\xe2\x80\x99s也称为“数据源层”

\n

数据源架构模式

\n

在实现方面,这样的层由一组对象组成,或者正如您所提到的,由函数组成。我们先来说说函数。

\n
\n

“分层可以发生在这些级别中的任何一个。一个小程序可能只是将各个层的单独函数放入不同的文件中。较大的\n系统可能具有与名称空间相对应的层,每个名称空间中有许多类\n 。” \xe2\x80\x94PresentationDomainDataLayering 福勒

\n
\n
\n

交易脚本

\n
\n

您如何称呼这种方法/模式?您会将\n代码放在​​哪里?

\n
\n

在您的具体示例中,函数用于包装 AR 对象的方法调用链,我能想到的最接近的模式是“事务脚本”。不是特定于数据源的模式,而是通用的业务逻辑封装方法;既不是函数式的,也不是面向对象的;而是一种程序性的。

\n

这些功能可能可以组织在以数据封装架构层命名的模块中;不管你怎么称呼它。命名候选者的范围可以是 到datasource.py以及dal.py其他主观上好听的名字。由于这种模式在 OO 代码库中并不常见,因此我认为您无法轻松找到社区接受的标准名称。

\n

该模式的简单性可能适用于小型代码库。然而,在处理许多查询的大型数据密集型代码库的情况下,最终出现维护和决策问题的可能性相对比更现代和可接受的模式要高。

\n

另外,我个人更喜欢保持一致的风格。如果 Django 带有面向对象模型,我会保持面向对象。面向对象设置中的此类孤立非 util 函数对于将来加入您的项目的大多数面向对象程序员来说是陌生的。

\n

现在让我们谈谈对象。在面向对象的代码库中,此类对象在内存中形成按需的“虚拟对象数据库”。这些对象通常按照属于“对象关系映射”和更通用的“ DAO ”模式(简单地说:单独的数据访问关注点)类别的数据源架构模式来实现。让我们尝试了解一下它们的表面。

\n
\n

ActiveRecord数据映射器

\n

他们席卷了所有社区。你知道这一切。所以,这里没什么好说的。

\n

我想进一步阐述的唯一一点是,正如您所经历的那样;他们只是给我们带来了“一些解耦”,因为他们通常富有表现力的FluentInterface几乎总是需要一个“家”来驻留,这样我们就可以避免重复并提高可维护性。

\n

这个家还可以托管任何罕见的性能敏感查询构建逻辑,这些逻辑可能不太适合标准 ORM。这样的家将帮助我们划定一个清晰的数据访问边界。

\n

如果您更好奇,请参阅:
\n Flask SQLAlchemy 数据映射器与 Active Record 模式

\n
\n

存储库

\n
\n

术语\xc2\xa0Repository\xc2\xa0 适合这里吗?

\n
\n

它很好地做到了每个 Django 模型的存储库对象都可以作为我们的“家”。

\n

一组存储库充当在域层和数据源逻辑之间进行中介的间接层。它通常是 DAL 的正面,主要实现 DAO 模式并“包装”底层 ORM 模式(如 AR 或 DM),有时甚至是普通的传统查询。

\n
\n

“具有复杂域模型的系统通常受益于一层,例如Data Mapper (165) 提供的层,该层将域对象与数据库访问代码的细节隔离开来在此类系统中,它可以是值得在查询构造代码集中的映射层上构建另一层抽象。当存在大量域类或大量查询时,这一点变得更加重要。特别是在这些情况下,添加这一层\ n n有助于最大限度地减少重复的查询逻辑。存储库在域和数据映射层之间进行中介就像内存中的域对象集合一样。” \xe2\x80\x94 PoEAA,福勒

\n
\n

在支持本机接口的语言中,每个存储库实现通常以其源命名(例如UserDjangoRepositoryUserLdapRepositoryUserMongoRepository),并驻留在UserRepositoryInterface应用程序其余部分使用的泛型后面(接口驻留在领域层,而具体内容则推送到基础设施层) 。

\n

就像我们下面要探讨的“表数据网关”对象一样,存储库也是多个记录的表示。不同之处在于,存储库往往是对数据的更通用、类似集合的抽象,没有数据库操作的概念(例如latest()vs. order_by_date_desc())。

\n实施\n
## app/repositories.py\n\nfrom app.models import Post\n\nclass PostRepository(AbstractRepository): \n\xc2\xa0 @staticmethod\n\xc2\xa0 def recent_in_category(category_id):\n\xc2\xa0 \xc2\xa0 return Post.objects.filter(...)\n\n# the optional parent abstract can expose generic repo methods like count()\n
Run Code Online (Sandbox Code Playgroud)\n
\n
## client code\n\nfrom app.repositories import PostRepository\n\ndef get(id):\xc2\xa0\n\xc2\xa0 return list(\n    PostRepository.recent_in_category(id)\n  )\n
Run Code Online (Sandbox Code Playgroud)\n

在该模式的狂热忠实实现中,存储库方法应该提供“标准”对象而不是原始值。Django Q 类可能是创建此类标准对象的良好候选者。

\n

但是,如果我尝试推送特定的 Django ORM impl。在存储库的引擎盖下的详细信息,我不会泄漏和耦合我的存储库实现。具有 Django 特定的概念,例如Q客户端代码需要了解的概念。我只是传递普通的数据结构。

\n缺点\n

在巨大的数据密集型应用程序中,存储库很容易成为难以维护的上帝对象;其中每个方法代表针对目标数据源的一个命名查询。想象一个具有 42 个不同方法的类。它不可扩展。

\n

Bogard 有一些反对存储库的 案例,通常支持查询对象

\n
\n

表数据网关

\n

这是一个对象,它将数据库表上的所有合法操作封装为网关实现。此类对象的一个​​实例代表整个表以及针对该表的所有合法操作。

\n

这就是objectsPost.objects.filter(...)示例中的那个对象。广泛地(在我看来很糟糕)在 Django 中被命名为“模型管理器”,它位于模型“下方”,与“顶部存储库”形成鲜明对比。如果我们想将问题视为 Django 特定的问题,那么基于现有的 Django 模式构建可能是一个不错的选择。

\n实施\n

我们为自定义管理器实现自定义方法,并将确保模型附带我们的自定义管理器而不是默认的实现。来自 Django.

\n
## app/models.py\n\nfrom django.db import models\n\nclass PostManager(models.Manager):\n\xc2\xa0 def recent_in_category(self, category_id):\n\xc2\xa0 \xc2\xa0 return self.filter(\n      category_id=category_id, \n      deleted_at__isnull=True, \n      published_at__gte=yesterday\n    )\n\xc2\xa0 # other repository methods follow...\n\n\nclass Post(models.Model):\n\xc2\xa0 title = models.CharField(maxlength=50)\n\xc2\xa0 # other fields declarations follow...\n\n\xc2\xa0 # set the custom model manager instance\n\xc2\xa0 # or even better: give it a custom name that reflects the pattern\n\xc2\xa0 objects = PostManager()\xc2\xa0\n\n\xc2\xa0 # custom model methods follow...\n
Run Code Online (Sandbox Code Playgroud)\n
\n
## client code\nfrom app.models import Post\n\ndef get(category_id):\xc2\xa0\n\xc2\xa0 return list(\n    Post.objects.recent_in_category(category_id)\n  )\n
Run Code Online (Sandbox Code Playgroud)\n

尽管差异很小,但它是一个不精确但足够公平的声明,可以将其视为“特定于 Django 的存储库模式实现”。以及。如果是这样,人们可以命名经理,以便它反映这种模式:

\n
## client code\n\n# without criterias:\nPost.repository.recent_in_category(cid)\n\n# or with criteria:\nPost.repository.recent(Q(category_id=cid))\n
Run Code Online (Sandbox Code Playgroud)\n

显然,它只适用于与框架分离不是标准的代码库。

\n
\n

查询对象

\n

想象每个数据库查询都由它自己的对象表示。这是一种流行的模式,尤其是在反存储库阵营中。

\n

它基本上是存储库模式,但不是每个查询都是存储库方法,而是一个自包含的独立类。因此,大型代码库中的可维护性更好。

\n
## app/queries.py\n\nfrom app.models import Post\n\n# AbstractQuery simply enforces derived classes to implement execute()\n\nclass RecentInCategoryPostsQuery(AbstractQuery):\n  @staticmethod\n  def execute(category_id):     \n    return Post.objects.filter(...)\n    # could also accept paging params or criteria objects maybe.\n   \n    # or it could be non-static & accept the category_id as a constructor \n    # param.\n
Run Code Online (Sandbox Code Playgroud)\n
\n
## client code\n\nfrom app.queries import RecentInCategoryPostsQuery\n\ndef get(category_id):\n  return HttpResponse(list(\n    RecentInCategoryPostsQuery.execute(category_id)\n  ))\n
Run Code Online (Sandbox Code Playgroud)\n
\n

其他图案

\n

Fowler 列出的其他数据源架构模式包括行数据网关(无域逻辑AR)、表模块(全域逻辑TDG)和工作单元(原子事务)。它们仅与您的问题相关,因此让我们跳过它们。

\n
\n

进一步阅读

\n

Fowler 的“企业应用程序架构模式”虽然现在已经很老了,但它仍然是该类型中改变生活的经典,它将让任何开发人员走上成为架构师的道路。我只能推荐它。不要害怕跳过旧的生锈的图案。

\n

更轻、更新、更特定于 Python/Django 的读物是“ Architecture Patterns with Python ”。我只是粗略地浏览了一下;不过看起来不错。

\n
\n

结论

\n
\n

一些 DRY 助手,你知道,以避免搜索整个存储库\n以便在我需要稍微更改行为\n时找到所有类似的用例

\n
\n

您知道,架构通常是考虑到所需的输出和情况而做出良好的权衡。如果这里的目标是像您所说的那样开发一些干助手,那么使用“表数据网关”并扩展每个模型的默认 django“模型管理器”听起来像是一种良好的一致的 OO 和 django 友好方法。

\n

在一个与框架无关的项目中,如果它不是数据密集型应用程序,我个人会选择“存储库”,否则,“查询对象”的扩展性会更好。

\n