减少存储库以汇总根


83

目前,我几乎为数据库中的每个表都拥有一个存储库,并希望通过减少DDD使其仅聚合根来进一步使自己与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吗?还是说这违反了最佳实践。


2
确实,这是一个很好的问题,我一直在努力挣扎。似乎是那些没有“正确”解决方案的权衡点之一。虽然在我撰写本文时提供的答案不错,并且涵盖了大多数问题,但我认为它们没有提供任何“最终”解决方案。.
::

我听到你的声音,对“正确”解决方案的接近程度没有限制。我猜我们必须尽力而为,直到我们学习更好的方法为止:)
e36M3 2011年

+1-我也在为此苦苦挣扎。在我为每个表分别设置仓库和服务层之前。我开始在合理的地方将它们结合起来,但是最后我得到了一个包含超过一千行代码的存储库和服务层。在我的最新应用程序切片中,我备份了一些内容,以便仅将紧密相关的概念放在同一回购/服务层中,即使该项目是相关的。例如-对于一个博客,我正在将评论添加到发布回购汇总中,但是现在我将它们分离出来以单独添加评论回购/服务。
jpshook 2011年

Answers:


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

搜索通用存储库实体框架,您将实现许多不错的实现。使用其中之一或自己编写。


@amit_g,感谢您的信息。我已经利用了其他所有继承自的通用/基本存储库。我将“查找”表逻辑分组到一个存储库中的想法仅仅是为了节省时间并减少存储库的数量。因此,与其创建ColorCodeRepository和AnotherCodeRepository,不如直接创建CodesRepository.GetColorCodes()和CodesRepository.GetAnotherCodes()。但是我不确定将不相关实体逻辑组合到一个存储库中是否是不好的做法。
e36M3 2011年

另外,您还要确认通过DDD规则,与根相对应的存储库中的方法应返回根,而不是返回图形中的基础实体。因此,在我的示例中,UserRepository上的任何方法都只能返回User类型,而不管图的其余部分是什么样(或图上我真正感兴趣的部分,例如“地址”或“电话”)?
e36M3 2011年

CodesRepository很好,但是很难一致地维护其中的内容。只需通过GenericRepository <ColorCodes> GetAll()即可实现相同的目的。由于GenericRepository仅具有非常通用的方法(GetAll,GetByID等),因此对于Lookup表来说可以正常工作。
amit_g 2011年


2
不幸的是,这个答案是错误的。存储库应被视为内存中对象的集合,并且应避免延迟加载。这是一篇关于besnikgeek.blogspot.com/2010/07/…的
36

9

您在“聚合根”存储库上的示例非常好,即,任何在不依赖另一个实体的情况下无法合理存在的实体都不应拥有自己的存储库(在您的情况下为Phone)。无需考虑这一点,您就可以以1-1映射到db表的方式快速浏览存储库。

您应该考虑使用工作单元模式进行数据更改,而不是使用存储库本身,因为我认为它们在将更改持久保存回数据库时会引起您对意图的困惑。在EF解决方案中,工作单元本质上是围绕EF上下文的接口包装。

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


1
谢谢。我不确定工作单​​元如何替换存储库?从某种意义上说,我已经采用了UOW,即在每个业务事务的结尾(HTTP请求的结尾)都将有一个对实体框架上下文的SaveSaves()调用。但是,我仍然通过存储库(包含EF上下文)进行数据访问。如UserRepository.Delete(user)和UserRepository.Add(user)。
e36M3 2011年

5

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

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

如果您遵循域驱动的设计,则存储库应为每个聚合根为1:1。
没有理由。

我敢打赌,这些都是您面临的问题:

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

我不确定如何解决第一个问题,但是我注意到修复第二个问题可以很好地解决。要理解以行为为中心的含义,请尝试本文

Ps减少存储库以聚集根是没有意义的。
PPS避免"CodeRepositories"。这导致以数据为中心->程序代码。
Ppps避免使用工作单元模式。聚合根应定义事务边界。


1
由于到本文的链接不再有效,请改用此链接:web.archive.org/web/20141021055503/http
//www.objectmentor.com/…– JwJosefy

3

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

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

静态无效的updateNumber(int userId,字符串oldNumber,字符串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();
        }

    }

抽象不是强制性的,但建议使用。实体框架仍然只是提供者和基础架构的一部分。甚至不只是更改了提供者而发生的事情,而且在更大的系统中,您可能拥有多种类型的提供者,这些提供者将不同的域概念持久化到不同的持久性介质。这种抽象很容易在早期进行,但是很难在足够的时间和复杂性上进行重构。
约瑟夫·费里斯

1
当我尝试抽象到存储库接口时,我发现很难保留EF'S ORM的好处(例如,延迟加载,可查询)。
Chalky

可以肯定,这是一个有趣的讨论。由于延迟加载非常特定于实现,因此我发现它的价值仅限于基础架构(通过层边界转换进出域对象)。尝试泛型抽象时,我看到的许多实现都会遇到问题。我倾向于采用显式实现,因为通用方法几乎没有领域值。EF确实使可查询的对象高度可用,但是问题就变成了存储库的角色-即,控制器使用的存储库缺少抽象的好处。
约瑟夫·弗里斯

0

如果Phone实体仅与聚集的根User一起有意义,那么我也认为添加新Phone记录的操作是User域对象通过特定方法(DDD行为)的责任,并且出于多种原因完全有道理,紧迫的原因是我们应该检查User对象是否存在,因为Phone实体依赖于它,并且在进行更多验证检查以确保没有其他进程删除根聚合之前,请保留该对象的事务锁定,我们完成了验证操作。在其他情况下,如果使用其他类型的根聚合,您可能希望聚合或计算一些值并将其保留在根聚合的列属性中,以便以后通过其他操作进行更有效的处理。

另外,如果您要使用检索所有电话的方法,而不管拥有这些电话的用户是什么,尽管通过User存储库,您仍然可以通过一种方法将所有用户返回为IQueryable,然后可以映射它们以获取所有用户电话并进行精细处理查询。因此,在这种情况下,您甚至不需要PhoneRepository。另外,我想对IQueryable使用带有扩展方法的类,如果我想在方法后面进行抽象查询,则不仅可以在Repository类中使用它,还可以在任何地方使用。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.