减少存储库以聚合根

发布于 2024-10-19 16:01:13 字数 1253 浏览 2 评论 0原文

我目前拥有数据库中几乎每个表的存储库,并且希望通过将它们减少为仅聚合根来进一步使自己与 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。尽管这将与实体框架完美配合,但这似乎非常缺乏描述性,因为阅读代码我不知道我实际上在更新什么,即使实体框架正在密切关注图表中已更改的对象。

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

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

I currently have a repository for just about every table in the database and would like to further align myself with DDD by reducing them to aggregate roots only.

Let’s assume that I have the following tables, User and Phone. Each user might have one or more phones. Without the notion of aggregate root I might do something like this:

//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]);

The concept of aggregate roots is easier to understand on paper than in practice. I will never have phone numbers that do not belong to a User, so would it make sense to do away with the PhoneRepository and incorporate phone related methods into the UserRepository? Assuming the answer is yes, I’m going to rewrite the prior code sample.

Am I allowed to have a method on the UserRepository that returns phone numbers? Or should it always return a reference to a User, and then traverse the relationship through the User to get to the phone numbers:

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

Regardless of which way I acquire the phones, assuming I modified one of them, how do I go about updating them? My limited understanding is that objects under the root should be updated through the root, which would steer me towards choice #1 below. Although this will work perfectly well with Entity Framework, this seems extremely un-descriptive, because reading the code I have no idea what I’m actually updating, even though Entity Framework is keeping tab on changed objects within the graph.

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

Lastly, assuming I have several lookup tables that are not really tied to anything, such as CountryCodes, ColorsCodes, SomethingElseCodes. I might use them to populate drop downs or for whatever other reason. Are these standalone repositories? Can they be combined into some sort of logical grouping/repository such as CodesRepository? Or is that against best practices.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

爱的十字路口 2024-10-26 16:01:13

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

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

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

You are allowed to have any method you want in your repository :) In both of the cases you mention, it makes sense to return the user with phone list populated. Normally user object would not be fully populated with all the sub information (say all addresses, phone numbers) and we may have different methods for getting the user object populated with different kind of information. This is referred to as lazy loading.

User GetUserDetailsWithPhones()
{
    // Populate User along with Phones
}

For updating, in this case, the user is being updated, not the phone number itself. Storage model may store the phones in different table and that way you may think that just the phones are being updated but that is not the case if you think from DDD perspective. As far as readability is concerned, while the line

UserRepository.Update(user)

alone doesn't convey what is being updated, the code above it would make it clear what is being updated. Also it would most likely be part of a front end method call that may signifiy what is being updated.

For the lookup tables, and actually even otherwise, it is useful to have GenericRepository and use that. The custom repository can inherit from the 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.
    }
}

Search for Generic Repository Entity Framework and you would fine many nice implementation. Use one of those or write your own.

寂寞清仓 2024-10-26 16:01:13

您在聚合根存储库上的示例非常好,即任何不依赖于另一个实体就无法合理存在的实体都不应该拥有自己的存储库(在您的情况下是电话)。如果没有考虑到这一点,您很快就会发现自己的存储库以 1-1 的方式映射到数据库表。

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

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

Your example on the Aggregate Root repository is perfectly fine i.e any entity that cannot reasonably exist without dependency on another shouldn't have its own repository (in your case Phone). Without this consideration you can quickly find yourself with an explosion of Repositories in a 1-1 mapping to db tables.

You should look at using the Unit of Work pattern for data changes rather than the repositories themselves as I think they're causing you some confusion around intent when it comes to persisting changes back to the db. In an EF solution the Unit of Work is essentially an interface wrapper around your EF Context.

With regards to your repository for lookup data we simply create a ReferenceDataRepository that becomes responsible for data that doesn't specifically belong to a domain entity (Countries, Colours etc).

小镇女孩 2024-10-26 16:01:13

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

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

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

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

  • 技术困难 - 对象关系阻抗不匹配。您正在努力轻松地持久保存整个对象图,而实体框架无法提供帮助。
  • 领域模型以数据为中心(而不是以行为为中心)。因此,您会失去有关对象层次结构(前面提到的上下文)的知识,并且神奇地一切都会变成聚合根。

我不确定如何解决第一个问题,但我注意到解决第二个问题就足以解决第一个问题。要理解我所说的以行为为中心的含义,请尝试一下本文

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

If phone makes no sense w/o user, it's an entity (if You care about it's identity) or value object and should always be modified through user and retrieved/updated together.

Think about aggregate roots as context definers - they draw local contexts but are in global context (Your application) themselves.

If You follow domain driven design, repositories are supposed to be 1:1 per aggregate roots.
No excuses.

I bet these are problems You are facing:

  • technical difficulties - object relation impedance mismatch. You are struggling with persisting whole object graphs with ease and entity framework kind a fails to help.
  • domain model is data centric (as opposed to behavior centric). because of that - You lose knowledge about object hierarchy (previously mentioned contexts) and magically everything becomes an aggregate root.

I'm not sure how to fix first problem, but I've noticed that fixing second one fixes first good enough. To understand what I mean with behavior centric, give this paper a try.

P.s. Reducing repository to aggregate root makes no sense.
P.p.s. Avoid "CodeRepositories". That leads to data centric -> procedural code.
P.p.p.s Avoid unit of work pattern. Aggregate roots should define transaction boundaries.

-残月青衣踏尘吟 2024-10-26 16:01:13

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

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

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

    }

This is an old question, but thought worth posting a simple solution.

  1. EF Context is already giving you both Unit of Work (tracks changes) and Repositories (in-memory reference to stuff from DB). Further abstraction is not mandatory.
  2. Remove the DBSet from your context class, as Phone is not an aggregate root.
  3. Use the 'Phones' navigation property on User instead.

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

    }
只等公子 2024-10-26 16:01:13

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

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

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

If a Phone entity only makes sense together with an aggregate root User, then I would also think it makes sense that the operation for adding a new Phone record is the responsibility of the User domain object throught a specific method (DDD behavior) and that could make perfectly sense for several reasons, the immidiate reason is we should check the User object exists since the Phone entity depends on it existence and perhaps keep a transaction lock on it while doing more validation checks to ensure no other process have deleted the root aggregate before we are done validating the operation. In other cases with other kinds of root aggregates you might want to aggregate or calculate some value and persist it on column properties of the root aggregate for more efficient processing by other operations later on. Note though I suggest the User domain object have a method that adds the Phone it doesn't mean it should know about the existence of the database or EF, one of the great feature of EM and Hibernate is that they can track changes made to entity classes transparently and that also means adding of new related entities by their navigation collection properties.

Also if you want to use methods that retrieve all phones regardless of the users owning them you could still though it through the User repository you only need one method returns all users as IQueryable then you can map them to get all user phones and do a refined query with that. So you don't even need a PhoneRepository in this case. Beside I would rather use a class with extensions method for IQueryable that I can use anywhere not just from a Repository class if I wanted to abstract queries behind methods.

Just one caveat for being able to delete Phone entities by only using the domain object and not a Phone repository you need to make sure the UserId is part of the Phone primary key or in other words the primary key of a Phone record is a composite key made up of UserId and some other property (I suggest an auto generated identity) in the Phone entity. This makes sense intuively as the Phone record is "owned" by the User record and it's removal from the User navigation collection would equal its complete removal from the database.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文