减少存储库以聚合根

Posted

技术标签:

【中文标题】减少存储库以聚合根【英文标题】:Reducing Repositories to Aggregate Roots 【发布时间】:2011-07-06 16:42:14 【问题描述】:

我目前几乎为数据库中的每个表都有一个存储库,并且希望通过将它们减少到仅聚合根来进一步使自己与 DDD 保持一致。

假设我有以下表格,UserPhone。每个用户可能拥有一部或多部手机。如果没有聚合根的概念,我可能会这样做:

//assuming I have the userId in session for example and I want to update a phone number
List<Phone> phones = PhoneRepository.GetPhoneNumberByUserId(userId);
phones[0].Number = “911”;
PhoneRepository.Update(phones[0]);

聚合根的概念在纸面上比在实践中更容易理解。我永远不会拥有不属于用户的电话号码,那么取消 PhoneRepository 并将与电话相关的方法合并到 UserRepository 是否有意义?假设答案是肯定的,我将重写之前的代码示例。

我是否允许在 UserRepository 上有一个返回电话号码的方法?或者它是否应该总是返回对用户的引用,然后通过用户遍历关系以获取电话号码:

List<Phone> phones = UserRepository.GetPhoneNumbers(userId);
// Or
User user = UserRepository.GetUserWithPhoneNumbers(userId); //this method will join to Phone

无论我以何种方式获取手机,假设我修改了其中一部,我该如何更新它们?我有限的理解是根目录下的对象应该通过根目录更新,这将引导我走向下面的选择#1。尽管这可以与 Entity Framework 完美配合,但这似乎非常不具描述性,因为阅读代码我不知道我实际上在更新什么,即使 Entity Framework 会密切关注图表中更改的对象。

UserRepository.Update(user);
// Or
UserRepository.UpdatePhone(phone);

最后,假设我有几个与任何东西没有真正关联的查找表,例如CountryCodesColorsCodesSomethingElseCodes。我可能会使用它们来填充下拉菜单或出于其他任何原因。这些是独立的存储库吗?它们可以组合成某种逻辑分组/存储库,例如CodesRepository?或者这违反了最佳实践。

【问题讨论】:

确实是一个非常好的问题,我一直在为自己苦苦挣扎。似乎是没有“正确”解决方案的权衡点之一。虽然在我写这篇文章时可用的答案很好并且涵盖了大多数问题,但我觉得它们没有提供任何“最终”解决方案.. :( 我听到你的声音,对于一个人可以获得的“正确”解决方案有多接近是没有限制的。我想我们必须尽力而为,直到我们学会更好的方法:) +1 - 我也在为此苦苦挣扎。在我为每个表设置单独的存储库和服务层之前。我开始在有意义的地方组合这些,但最终我得到了一个包含超过 1k 行代码的 repo 和服务层。在我最新的应用程序切片中,我已经备份了一点,只将密切相关的概念放在同一个 repo/service 层中,即使该项目是依赖的。例如 - 对于博客,我将 cmets 添加到 post repo 聚合中,但现在我已将它们分离为单独的评论 repo/service。 【参考方案1】:

您可以在存储库中使用任何您想要的方法:) 在您提到的两种情况下,返回填充电话列表的用户是有意义的。通常用户对象不会完全填充所有子信息(比如所有地址、电话号码),我们可能有不同的方法来让用户对象填充不同类型的信息。这称为延迟加载。

User GetUserDetailsWithPhones()

    // Populate User along with Phones

对于更新,在这种情况下,更新的是用户,而不是电话号码本身。存储模型可能会将手机存储在不同的表中,这样您可能会认为只有手机正在更新,但如果您从 DDD 的角度考虑,情况并非如此。就可读性而言,而行

UserRepository.Update(user)

单独并不能传达正在更新的内容,上面的代码会清楚地说明正在更新的内容。此外,它很可能是前端方法调用的一部分,可能表示正在更新的内容。

对于查找表,甚至在其他情况下,拥有 GenericRepository 并使用它是很有用的。自定义存储库可以继承自 GenericRepository。

public class UserRepository : GenericRepository<User>

    IEnumerable<User> GetUserByCustomCriteria()
    
    

    User GetUserDetailsWithPhones()
    
        // Populate User along with Phones
    

    User GetUserDetailsWithAllSubInfo()
    
        // Populate User along with all sub information e.g. phones, addresses etc.
    

搜索 Generic Repository Entity Framework,你会发现很多不错的实现。使用其中一种或自己编写。

【讨论】:

@amit_g,感谢您提供的信息。我已经使用了所有其他人继承的通用/基础存储库。我将“查找”表逻辑分组到一个存储库中的想法只是为了节省时间并减少存储库的数量。因此,我不会创建 ColorCodeRepository 和 AnotherCodeRepository,而是简单地创建 CodesRepository.GetColorCodes() 和 CodesRepository.GetAnotherCodes()。但我不确定将不相关的实体逻辑分组到一个存储库中是否是不好的做法。 此外,您随后确认通过 DDD 规则,与根对应的存储库中的方法应该返回根而不是图中的底层实体。因此,在我的示例中,UserRepository 上的任何方法都只能返回 User 类型,而不管图表的其余部分(或我真正感兴趣的图表部分,例如地址或电话)? CodesRepository 很好,但很难始终如一地维护其中的内容。同样可以通过 GenericRepositoryGetAll() 来实现。由于 GenericRepository 只有非常通用的方法(GetAll、GetByID 等),因此它适用于 Lookup 表。 @e36M3,是的。例如geekswithblogs.net/seanfao/archive/2009/12/03/136680.aspx 不幸的是,这个答案是错误的。存储库应被视为内存对象的集合,您应避免延迟加载。这是一篇关于 besnikgeek.blogspot.com/2010/07/… 的好文章【参考方案2】:

您在 Aggregate Root 存储库上的示例非常好,即任何不依赖另一个实体就无法合理存在的实体不应拥有自己的存储库(在您的情况下为 Phone)。如果没有这种考虑,您很快就会发现自己在 1-1 映射到 db 表中的存储库爆炸式增长。

您应该考虑使用工作单元模式来进行数据更改,而不是存储库本身,因为我认为在将更改持久化回数据库时,它们会导致您对意图产生一些混淆。在 EF 解决方案中,工作单元本质上是您的 EF 上下文的接口包装器。

关于您的查找数据存储库,我们只需创建一个 ReferenceDataRepository 来负责不专门属于域实体(国家、颜色等)的数据。

【讨论】:

谢谢。我不确定工作单​​元如何替换存储库?从某种意义上说,我已经使用了 UOW,即在每个业务事务结束时(HTTP 请求结束)都会对实体框架上下文进行一次 SaveChanges() 调用。但是,我仍然通过存储库(包含 EF 上下文)进行数据访问。如 UserRepository.Delete(user) 和 UserRepository.Add(user)。【参考方案3】:

如果电话在没有用户的情况下毫无意义,它是一个实体(如果您关心它的身份)或值对象,应始终通过用户进行修改并一起检索/更新。

将聚合根视为上下文定义器 - 它们绘制本地上下文,但它们本身位于全局上下文(您的应用程序)中。

如果您遵循域驱动设计,则存储库应该是每个聚合根的 1:1。 没有任何借口。

我敢打赌这些是你面临的问题:

技术难题 - 对象关系阻抗不匹配。您正在努力轻松地持久化整个对象图,而实体框架却无济于事。 域模型以数据为中心(与以行为为中心相反)。正因为如此 - 您失去了关于对象层次结构(前面提到的上下文)的知识,并且神奇地一切都变成了聚合根。

我不确定如何解决第一个问题,但我注意到解决第二个问题已经足够好。要理解我所说的以行为为中心的意思,请尝试this paper。

附:将存储库减少到聚合根是没有意义的。 附言避免"CodeRepositories"。这导致以数据为中心 -> 程序代码。 P.p.p.s 避免使用工作单元模式。聚合根应定义事务边界。

【讨论】:

由于该论文的链接不再有效,请改用此链接:web.archive.org/web/20141021055503/http://www.objectmentor.com/…【参考方案4】:

这是一个老问题,但值得发布一个简单的解决方案。

    EF Context 已经为您提供了工作单元(跟踪更改)和存储库(对数据库中内容的内存引用)。进一步的抽象不是强制性的。 从上下文类中删除 DBSet,因为 Phone 不是聚合根。 请改用 User 的“电话”导航属性。

static void updateNumber(int userId, string oldNumber, string newNumber)

static void updateNumber(int userId, string oldNumber, string newNumber)
    
        using (MyContext uow = new MyContext()) // Unit of Work
        
            DbSet<User> repo = uow.Users; // Repository
            User user = repo.Find(userId); 
            Phone oldPhone = user.Phones.Where(x => x.Number.Trim() == oldNumber).SingleOrDefault();
            oldPhone.Number = newNumber;
            uow.SaveChanges();
        

    

【讨论】:

抽象不是强制性的,但建议使用。实体框架仍然只是一个提供者和基础设施的一部分。如果提供者发生变化,这甚至不仅仅是会发生什么的问题,而是在更大的系统中,您可能有多种类型的提供者将不同的域概念持久化到不同的持久性介质。这种抽象在早期非常容易制作,但在足够的时间和复杂性上进行重构是很痛苦的。 当我尝试抽象到存储库接口时,我发现很难保留 EF 的 ORM 的好处(例如延迟加载、可查询)。 当然,这是一个有趣的讨论。由于延迟加载非常特定于实现,我发现它的价值仅限于基础设施(域对象进出层有界翻译)。当尝试通用抽象时,我看到的许多实现都会遇到问题。我倾向于使用显式实现,因为泛型方法的领域价值很小。 EF 确实使可查询对象高度可用,但问题变成了存储库的角色——即控制器使用的存储库错过了抽象的好处。【参考方案5】:

如果电话实体仅与聚合根用户一起有意义,那么我也认为添加新电话记录的操作是用户域对象通过特定方法(DDD行为)的责任是有意义的出于几个原因,这可能是完全合理的,最直接的原因是我们应该检查 User 对象是否存在,因为 Phone 实体依赖于它的存在,并且可能在进行更多验证检查时保持事务锁定,以确保没有其他进程删除了在我们完成验证操作之前的根聚合。在其他类型的根聚合的其他情况下,您可能希望聚合或计算一些值并将其保存在根聚合的列属性中,以便稍后由其他操作进行更有效的处理。请注意,虽然我建议用户域对象有一个添加电话的方法,但这并不意味着它应该知道数据库或 EF 的存在,EM 和 Hibernate 的一大特点是它们可以跟踪对实体所做的更改类透明,这也意味着通过其导航集合属性添加新的相关实体。

此外,如果您想使用检索所有手机的方法,而不管用户拥有它们,您仍然可以通过用户存储库,您只需要一种方法将所有用户返回为 IQueryable,然后您可以映射它们以获取所有用户手机和用它做一个细化的查询。因此,在这种情况下,您甚至不需要 PhoneRepository。除此之外,如果我想在方法后面抽象查询,我宁愿使用带有 IQueryable 扩展方法的类,我可以在任何地方使用,而不仅仅是来自 Repository 类。

仅使用域对象而不是电话存储库来删除电话实体只有一个警告,您需要确保 UserId 是电话主键的一部分,或者换句话说,电话记录的主键是Phone 实体中由 UserId 和其他一些属性(我建议使用自动生成的身份)组成的复合键。这在直觉上是有道理的,因为电话记录由用户记录“拥有”,并且从用户导航集合中删除它等于将其从数据库中完全删除。

【讨论】:

以上是关于减少存储库以聚合根的主要内容,如果未能解决你的问题,请参考以下文章

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

什么是聚合根?

聚合根中覆盖的实体如何保存在 DDD 中?

如何扩展mercurial存储库以包含父文件夹?

更新 github 存储库以匹配本地 [重复]

sh 设置本地存储库以跟踪所有远程分支