存储库模式:如何延迟加载?或者,我应该拆分这个聚合吗?

Posted

技术标签:

【中文标题】存储库模式:如何延迟加载?或者,我应该拆分这个聚合吗?【英文标题】:Repository Pattern: how to Lazy Load? or, Should I split this Aggregate? 【发布时间】:2010-10-02 06:29:12 【问题描述】:

我有一个具有编辑器和项目概念的域模型。

一个编辑器拥有多个项目,一个项目不仅有一个编辑器所有者,还有多个编辑器成员。因此,一个编辑器也有许多“加入”的项目。

我正在采用 DDD 方法对此进行建模并使用存储库模式进行持久化。但是,我对模式的理解还不够好,无法确定我应该如何做到这一点。

我正在假设 Editor 和 Project 可能在同一个聚合中,根是 Editor。因此,我可以获得一个编辑器,然后枚举其项目,并可以从那里枚举项目的成员编辑器。

但是,如果只允许我从存储库中检索编辑器,这是否意味着当我获得拥有它们的编辑器时,我必须从存储库中加载所有项目?如果我想延迟加载成员编辑器,项目还需要对存储库的引用吗?

或者,如果我拆分聚合并拥有一个编辑器存储库和一个项目存储库,我应该如何处理这两者之间的事务,例如将新项目添加到编辑器时?例如:

Editor e = new Editor("Editor Name");
editorRepository.Add(e);

Project p = e.CreateProject("Project Name");
projectRepository.Add(p);    // These two lines
editorRepository.Save(e);    // should be atomic

我是否误解了存储库模式的意图?

【问题讨论】:

你可能想看看我的相关问题:***.com/q/20820302/253098 【参考方案1】:

我是否误解了存储库模式的意图?

我会说“是的”,但要知道我和我共事过的每个人都出于同样的原因问过同样的事情……“你不是在第四维度思考,马蒂"。

让我们稍微简化一下,先坚持使用构造函数而不是 Create 方法:

Editor e = new Editor("Editor Name");
e = editorRepository.Add(e);

Project p = new Project("Project Name", e);
p = projectRepository.Add(p);

在下面,您的项目存储库始终在创建项目数据时将有效所有者 (p.EditorId) 存储到项目数据中,无论您重新填充编辑器的项目,它都会在那里。这就是为什么将所有必需的属性放入构造函数是一种很好的做法。如果您不想传递整个对象,只需 e.Id 即可。

如果我想延迟加载成员编辑器,项目还需要对存储库的引用吗?

现在,关于如何按需重新填充编辑器的项目,您有几个选择,具体取决于您的目标。 Straight Repository 说你想要:

IEnumerable<Project> list = projectRepository.GetAllProjects()
                                .Where(x => x.editorId == e.Id);

但是放在哪里呢?不是在 Project 或 Editor 中,你是对的,否则他们将不得不访问存储库,这是不好的。上面的 sn-p 是松耦合的,但不能单独重用。您刚刚达到了存储库模式的极限。

接下来是您的应用程序的适配器层,具有共享的存储库源 (StaticServiceWrapper) 和某种 EditorAdapter 对象(或聚合或您所称的任何东西),或者现在您可以混合扩展方法可以流利地与任何和所有必要的存储库对话。我没有在生产系统中完全这样做过,但给你看一个简洁的例子:

public static class Aggregators

    // one to one, easy
    public static Editor GetOwner(this Project p)
    
        return StaticServiceWrapper.editorRep.GetEditorById(p.editorId);
    

    // one to many, medium
    public static IEnumerable<Project> GetProjects(this Editor e) 
     
        return StaticServiceWrapper.projectRep.GetAllProjects()
                .Where(x => x.editorId == e.Id);
    

    // many to many, harder
    public static IEnumerable<Editor> GetMembers(this Project p)
    
        var list = StaticServiceWrapper.projectMemberMap.GetAllMemberMaps()
                        .Where(x => x.projectId == p.projectId);

        foreach ( var item in list )
            yield return StaticServiceWrapper.editorRep.GetEditorById(item.editorId);
    

基本上,一旦您的 GetAll、GetById、Add、Update、Remove 对象存储库完成后,您就必须不理会关联,并在对象/层层次结构上向上移动到有趣的部分,例如适配器和缓存以及业务逻辑(“哦,我的!”)。

【讨论】:

如果 IRepository 的底层实现是一个数据库,并且你有非常多的 X 实体。在存储库上使用 GetAll() 是否仍然可以,然后对整个集合执行 LINQ where 查询?如果这太慢了怎么办,而您宁愿在数据库级别进行查询,您将如何使用存储库来执行此操作? 使用 FindAll() 不会导致 SQL 查询立即被触发。这只发生在您将其转换为列表、单个元素时(例如使用.FirstOrDefault() 或在其上执行 foreach 循环。因此您可以在执行查询之前使用它执行各种 Linq 操作,结果是从您的数据库中获取。【参考方案2】:

这里有 2 种不同的关系,一种是所有权关系,一种是成员关系。

所有权关系是简单的一对多(每个项目一个所有者)。成员关系是多对多的(项目有很多编辑,编辑有很多项目)。

您可以在 Project 类上提供 Owner 属性,并在 ProjectRepository 上提供一个方法来获取特定编辑器拥有的所有项目。

对于多关系,在 Project 类上提供一个 Members 属性,在 ProjectRepository 上提供一个方法来获取包含指定 Editor 作为成员的所有项目。

Editors 和 Projects 似乎也是实体,我可能会拆分聚合,但也许这些术语在您的上下文中具有特定含义,使其成为聚合的子实体。

【讨论】:

【参考方案3】:

如何将职责拆分为 EditorOwner 和 EditorMember?

在不了解您的域的情况下,我想他们会承担不同的职责 - 例如,EditorOwner 可能非常丰富(并且可能是聚合根),但项目可能只需要了解其有限的信息成员,所以 EditorMember 对象可能很轻。

这些域对象也可能与用户相关,但那将是在另一个上下文中。

这对事情有帮助,还是只是让事情变得更复杂?

【讨论】:

【参考方案4】:

这取决于您的应用程序的需求。如果为给定的编辑器加载所有项目是个大问题,那么尝试像Virtual Proxy 这样的延迟加载模式。

关于延迟加载项目的成员编辑器,如果您使用虚拟代理,我认为使用 EditorRepository 注入代理没有问题,因为我不认为代理是域的一部分。

如果您拆分聚合,您可以研究 Unit of Work 模式作为解决原子性的一种方法。不过,这个问题并不是 DDD 独有的,我相信还有其他针对事务行为的解决方案。

【讨论】:

以上是关于存储库模式:如何延迟加载?或者,我应该拆分这个聚合吗?的主要内容,如果未能解决你的问题,请参考以下文章

数据库和聚合根的存储库模式

Nestjs路由器中控制器如何拆分路由

延迟加载 Spring Data JPA 存储库

存储库模式和聚合根模式和实体框架

如何使用 neuraxle 实现延迟数据加载的存储库?

什么是聚合根?