减少仓库到聚合根的数量

87

我目前为数据库中的每个表都有一个存储库,并希望通过将它们减少到仅聚合根来进一步遵循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

不管我以哪种方式获取电话,假设我修改了其中一个,我该如何更新它们? 我的了解有限,但我知道根目录下的对象应该通过根目录来更新,这会引导我选择下面的第一种方法。虽然这在实体框架中完全可行,但似乎非常不具描述性,因为读取代码时,我无法知道我实际上正在更新什么,尽管实体框架正在跟踪图形中已更改的对象。

UserRepository.Update(user);
// Or
UserRepository.UpdatePhone(phone);
最后,假设我有几个查找表并且它们与任何其他内容都没有真正联系,例如 CountryCodes、ColorsCodes、SomethingElseCodes。我可能会使用它们来填充下拉列表或出于其他任何原因。这些是独立的存储库吗?它们可以组合成某种逻辑分组/存储库,例如 CodesRepository 吗?还是说这违反了最佳实践?

2
确实是一个非常好的问题,我自己也一直在苦苦思索。似乎这是那种没有“正确”解决方案的权衡点之一。虽然我写这篇文章时提供的答案都很好并且涵盖了大多数问题,但我觉得它们并没有提供任何“最终”的解决方案.. :( - cwap
我明白你的意思,没有什么是完全正确的解决方案。我想我们只能尽力做到最好,直到我们学到更好的方法 :) - e36M3
+1 - 我也在这方面苦苦挣扎。以前我为每个表都有单独的存储库和服务层。我开始将它们合并,但最终得到了一个超过1k行代码的存储库和服务层。在我的最新应用程序切片中,我已经稍微退后,只将密切相关的概念放在同一个存储库/服务层中,即使该项是依赖的。例如-对于博客,我曾将评论添加到帖子存储库聚合中,但现在我已将它们分离到单独的评论存储库/服务中。 - jpshook
5个回答

12

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

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.
    }
}

搜索“通用仓储库Entity Framework”即可找到许多不错的实现。使用其中一个或编写自己的实现。


此外,您还确认根据DDD规则,与根相对应的存储库中的方法应返回根而不是图形中的底层实体。因此,在我的示例中,无论图形的其余部分或我真正感兴趣的部分(例如地址或电话)是什么样子,UserRepository上的任何方法都只能返回User类型? - e36M3
CodesRepository还不错,但要保持一致性并不容易。同样的功能可以通过GenericRepository<ColorCodes>GetAll()来实现。由于GenericRepository只有非常通用的方法(如GetAll、GetByID等),因此它对于查找表来说完全可以胜任。 - amit_g
1
@e36M3,是的。例如http://geekswithblogs.net/seanfao/archive/2009/12/03/136680.aspx。 - amit_g
懒加载的重点不就是你不必调用像 GetUserDetailsWithPhones 这样的方法吗?你的 ORM 会在第一次使用时填充它(例如,当你使用新号码更新它时)吗? - A. Murray
2
很遗憾,这个答案是错误的。仓库应该被视为内存对象的集合,并且应该避免使用延迟加载。这里有一篇关于此的好文章:http://besnikgeek.blogspot.com/2010/07/specification-pattern-and-complex.html - Rafał Łużyński
显示剩余3条评论

10

您在聚合根存储库方面的示例是完全正确的,即任何不能合理存在且依赖于另一个实体的实体不应该有自己的存储库(在您的情况下为电话)。如果没有这个考虑,您很快就会发现有大量的存储库与数据库表的1-1映射。

您应该考虑使用工作单元模式进行数据更改,而不是仅使用存储库本身,因为我认为它们在持久化更改返回到数据库时会造成一些混淆。在EF解决方案中,工作单元本质上是围绕您的EF上下文的接口包装器。

关于查找数据的存储库,我们只需创建一个ReferenceDataRepository,该存储库负责不特定属于领域实体的数据(Countries、Colours等)。


1
谢谢。我不确定 Unit of Work 如何替代仓储库?我已经在某种程度上使用了 UOW,即在每个业务交易(HTTP 请求结束时)的最后会有一个单独的 SaveChanges() 调用到 Entity Framework 上下文中。然而,我仍然通过仓储库(包含 EF 上下文)进行数据访问。例如 UserRepository.Delete(user) 和 UserRepository.Add(user)。 - e36M3

5
如果电话没有用户就毫无意义,它是一个实体(如果您关心它的身份)或值对象,并且应始终通过用户进行修改并一起检索/更新。
将聚合根视为上下文定义者-它们绘制本地上下文,但本身处于全局上下文(您的应用程序)中。
如果您遵循领域驱动设计,存储库应该是每个聚合根的1:1。
没有借口。
我打赌这些是您正在面对的问题:
  • 技术困难-对象关系阻抗不匹配。您正在努力轻松持久化整个对象图,并且实体框架无法帮助。
  • 领域模型是数据中心(与行为中心相反)。由于此原因-您失去了有关对象层次结构(先前提到的上下文)的知识,并且神奇地变成了聚合根。

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

附注:将存储库缩减为聚合根毫无意义。
另外,避免使用"CodeRepositories"。这会导致数据中心化 -> 过程化代码。
再次强调,避免使用工作单元模式。聚合根应定义事务边界。


1
由于原始链接已失效,请使用此链接:http://web.archive.org/web/20141021055503/http://www.objectmentor.com/resources/articles/CoffeeMaker.pdf。 - JwJosefy

3

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

  1. EF上下文已经为您提供了工作单元(跟踪更改)和仓库(内存中对来自数据库的内容的引用)。进一步抽象并非必要。
  2. 从您的上下文类中删除DBSet,因为Phone不是聚合根。
  3. 使用User上的“Phones”导航属性。

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();
        }

    }

抽象化并非强制要求,但建议使用。Entity Framework仍然只是一个提供程序和基础设施的一部分。这不仅仅是因为如果提供程序发生变化会发生什么,而是在更大的系统中,您可能会有多种类型的提供程序,将不同的域概念持久化到不同的持久化介质中。这是一种极易在早期实现的抽象化,但随着时间和复杂性的增加,重构起来会很痛苦。 - Joseph Ferris
2
当我尝试将EF的ORM(例如延迟加载、可查询性)抽象到一个仓储接口时,我发现很难保留它的好处。 - Chalky
这是一个有趣的讨论,确实如此。由于惰性加载非常特定于实现,我发现它的价值仅限于基础设施(领域对象在层边界转换中进出)。我看到的很多实现都遇到了尝试使用通用抽象时的问题。我倾向于采用显式实现,因为通用方法几乎没有领域价值。EF确实使查询可用性高,但问题在于存储库的角色 - 即由控制器使用的存储库错过了抽象化的好处。 - Joseph Ferris

0
如果一个电话实体只有与聚合根用户一起使用才有意义,那么我认为添加新电话记录的操作也应该由用户领域对象负责通过特定方法(DDD行为),这在多个方面都可以完美地解释,其中最直接的原因是我们应该检查用户对象的存在,因为电话实体依赖于它的存在,并且在执行更多验证检查以确保没有其他进程在我们完成验证操作之前删除根聚合时,可能会在其上保持事务锁。在其他情况下,你可能需要对其他类型的根聚合进行聚合或计算一些值并将其持久化到根聚合的列属性上,以便稍后的其他操作能够更高效地处理。请注意,虽然我建议用户领域对象具有添加电话的方法,但这并不意味着它应该知道数据库或EF的存在,EM和Hibernate的一个伟大功能是它们可以透明地跟踪对实体类所做的更改,这也意味着通过导航集合属性添加了新的相关实体。
此外,如果您想使用检索所有电话的方法,而不管用户是否拥有它们,您仍然可以通过User存储库来完成。您只需要一个方法返回所有用户作为IQueryable,然后您可以映射它们以获取所有用户电话,并使用该电话进行精细查询。因此,在这种情况下,您甚至不需要PhoneRepository。此外,如果我想在方法后面抽象查询,我更愿意使用具有IQueryable扩展方法的类,而不仅仅是从Repository类中使用。
只有一个警告:要能够仅使用域对象而不使用Phone存储库删除Phone实体,您需要确保UserId是Phone主键的一部分,或者换句话说,Phone记录的主键是由UserId和Phone实体中的某些其他属性(我建议自动生成的标识)组成的复合键。直观地讲,这是有道理的,因为Phone记录是由User记录“拥有”的,从用户导航集合中删除它将等同于完全从数据库中删除它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接