减less知识库以聚合根
我目前有一个数据库中的每个表的存储库,并希望进一步调整自己与DDD通过减less它们只聚合根。
假设我有以下表格, User
和Phone
。 每个用户可能有一个或多个电话。 没有聚合根的概念,我可能会这样做:
//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并将与Phone相关的方法合并到UserRepository中是否合理呢? 假设答案是肯定的,我将重写之前的代码示例。
我允许在UserRepository上有返回电话号码的方法吗? 或者它应该总是返回一个用户的引用,然后通过用户遍历关系来获得电话号码:
List<Phone> phones = UserRepository.GetPhoneNumbers(userId); // Or User user = UserRepository.GetUserWithPhoneNumbers(userId); //this method will join to Phone
不pipe我以哪种方式获得手机,假设我修改了其中一个,我该如何去更新它们? 我有限的理解是根源下的对象应该通过根来更新,这将引导我select下面的select#1。 尽pipeEntity Framework可以很好地工作,但是由于阅读代码,我不知道实际上是在更新什么,尽pipeEntity Framework在图中保留了更改对象的选项卡。
UserRepository.Update(user); // Or UserRepository.UpdatePhone(phone);
最后,假设我有几个查询表,并没有真正绑定任何东西,比如ColorsCodes
, ColorsCodes
, SomethingElseCodes
。 我可能会使用它们来填充下拉菜单或任何其他原因。 这些独立的存储库? 他们可以被组合成某种逻辑分组/存储库,如CodesRepository
? 或者是违背最佳做法。
您可以在存储库中使用任何方法:)在提到的两种情况下,返回带有电话列表的用户都是有意义的。 通常用户对象不会被所有的子信息(比如所有的地址,电话号码)完全填充,我们可能有不同的方法来获取填充了不同types信息的用户对象。 这被称为延迟加载。
User GetUserDetailsWithPhones() { // Populate User along with Phones }
为了更新,在这种情况下,用户正在被更新,而不是电话号码本身。 存储模式可能会将手机存储在不同的表格中,这种方式您可能认为手机正在更新,但如果您从DDDangular度来看,情况并非如此。 就可读性而言,而行
UserRepository.Update(user)
单独没有传达正在更新的内容,上面的代码会清楚地说明正在更新的内容。 此外,它很可能是前端方法调用的一部分,可能意味着正在更新的内容。
对于查找表,实际上甚至不然,有GenericRepository并使用它是有用的。 自定义存储库可以从GenericRepositoryinheritance。
public class UserRepository : GenericRepository<User> { IEnumerable<User> GetUserByCustomCriteria() { } User GetUserDetailsWithPhones() { // Populate User along with Phones } User GetUserDetailsWithAllSubInfo() { // Populate User along with all sub information eg phones, addresses etc. } }
search通用仓库entity framework,你会罚款很多很好的实现。 使用其中一个或写你自己的。
你在聚合根存储库上的例子是完全正确的,即任何不能合理地存在没有依赖另一个实体不应该有自己的存储库(在你的情况下电话)。 如果没有这个考虑,你可以很快发现自己在一个映射到数据库表的1-1映射中存在一个仓库。
你应该看看使用工作单元模式的数据变化,而不是存储库本身,因为我认为他们正在引起你一些混淆意图,当涉及到持久性变化回到数据库。 在EF解决scheme中,工作单元本质上是EF上下文的接口包装器。
关于您的存储库查找数据,我们只需创build一个ReferenceDataRepository来负责不专门属于一个域实体(国家,颜色等)的数据。
如果手机没有用户意义,它是一个实体(如果你关心它的身份)或价值对象,应该总是通过用户修改和一起检索/更新。
考虑聚合根作为上下文定义者 – 他们绘制本地上下文,但在全局上下文(你的应用程序)本身。
如果您遵循域驱动devise,那么存储库应该是每个聚合根1:1。
没有理由。
我敢打赌,这些都是你面临的问题:
- 技术难点 – 物体关系阻抗不匹配。 你正在努力保持整个对象图的轻松和entity frameworktypes没有帮助。
- 领域模型是以数据为中心的(而不是以行为为中心)。 因为这样 – 你就失去了关于对象层次结构的知识(前面提到的上下文),神奇地,所有东西都成为一个聚合根。
我不知道如何解决第一个问题,但我注意到修复第一个问题已经足够好了。 要理解我的意思是以行为为中心,请试试这篇论文 。
Ps将存储库还原为聚合根目录是没有意义的。
PPS避免"CodeRepositories"
。 这导致以数据为中心 – >程序代码。
Ppps避免工作单元模式。 聚合根应该定义事务边界。
这是一个古老的问题,但认为值得张贴一个简单的解决scheme。
- EF上下文已经为您提供了工作单元(跟踪更改)和存储库(内存中从DB引用的东西)。 进一步的抽象不是强制性的。
- 从您的上下文类中移除DBSet,因为Phone不是聚合根。
- 改为使用用户的“电话”导航属性。
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(); } }
如果一个电话实体只与一个聚合的根用户有联系,那么我也认为添加一个新电话logging的操作是通过一个特定的方法(DDD行为)对用户域对象的责任是有意义的,因为几个原因,我们应该检查User对象是否存在,因为Phone实体依赖于它的存在,并且可能保留一个事务锁,同时进行更多的validation检查,以确保没有其他进程已经删除根集合我们已经完成validation操作。 在其他情况下,对于其他types的根聚合,您可能需要聚合或计算某个值,并将其保留在根聚合的列属性上,以便稍后通过其他操作进行更高效的处理。 请注意,尽pipe我build议用户域对象有一个添加电话的方法,但这并不意味着它应该知道数据库或EF的存在,EM和Hibernate的一个重要特性就是它们可以跟踪对实体透明的类,也意味着通过导航集合属性添加新的相关实体。
另外,如果您想要使用检索所有电话的方法,而不pipe拥有它们的用户如何,您仍然可以通过用户存储库,只需要一种方法将所有用户返回为IQueryable,然后可以映射它们以获取所有用户电话并进行精制用这个查询。 所以在这种情况下你甚至不需要PhoneRepository。 除此之外,我宁愿使用带有扩展方法的类来处理IQueryable,如果我想在抽象方法后面抽象查询,那么我可以在任何地方使用,而不仅仅是从Repository类中使用。
只需要注意一点,即只能使用域对象而不是Phone库来删除Phone实体,以确保UserId是Phone主键的一部分,换句话说,Phonelogging的主键是一个组合键由UserId和Phone实体中的一些其他属性(我build议一个自动生成的标识)组成。 这是合理的,因为电话logging由用户logging“拥有”,并且从用户导航集合中删除将等于从数据库中完全删除。