减少存储库以聚合根

时间:2011-03-01 17:29:44

标签: c# asp.net entity-framework entity-framework-4 domain-driven-design

我目前拥有数据库中几乎每个表的存储库,并希望通过将它们简化为只聚合根来进一步使自己与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

无论我采用哪种方式购买手机,假设我修改了其中一种,我该如何更新它们?我有限的理解是,根目录下的对象应该通过root更新,这将引导我选择下面的#1。虽然这对Entity Framework非常有效,但这似乎非常缺乏描述性,因为阅读代码时我不知道我实际更新了什么,即使实体框架在图表中保留了更改对象的标签。

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

最后,假设我有几个与任何事物没有关联的查找表,例如CountryCodesColorsCodesSomethingElseCodes。我可能会用它们来填充下拉或其他任何原因。这些独立的存储库吗?它们可以组合成某种逻辑分组/存储库,例如CodesRepository吗?或者这是最佳做法。

5 个答案:

答案 0 :(得分:11)

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

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

搜索通用存储库实体框架,你会很好的实现。使用其中之一或自己编写。

答案 1 :(得分:9)

您在Aggregate Root存储库中的示例是完全正确的,即任何不能合理地存在而不依赖于另一个的实体都不应该拥有自己的存储库(在您的情况下是Phone)。如果不考虑这一点,您可以在1-1映射到数据库表中快速找到自己的存储库。

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

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

答案 2 :(得分:5)

如果手机对用户毫无意义,那么它就是一个实体(如果您关心它的身份)或价值对象,应始终通过用户进行修改并一起检索/更新。

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

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

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

  • 技术难点 - 物体关系阻抗不匹配。您正在努力维护整个对象图,而实体框架则无法提供帮助。
  • 域模型是以数据为中心的(与行为为中心相对)。因为那样 - 你失去了关于对象层次结构的知识(之前提到的上下文),并且神奇地一切都变成了聚合根。

我不确定如何解决第一个问题,但我注意到修复第二个问题首先修好了。要了解行为中心的含义,请尝试this paper

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

答案 3 :(得分:3)

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

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

答案 4 :(得分:0)

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

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

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