正确的汇总设计和复杂的规范查询

发布于 2025-01-19 15:12:58 字数 3362 浏览 5 评论 0原文

我对 DDD 聚合主题缺乏了解。

我确实有一个 Offer 聚合,它具有指向其子集合 OfferProducts 的导航属性。 当我学习实体框架时,我认为我应该始终在关系的两侧定义导航属性,但 Ardalis(ef https://github.com/ardalis/Specification)在某处写下了我不正确理解的这些话:

您希望避免具有跨越聚合的导航属性。 因此,您需要决定导航属性应该放在哪里以及在哪里 应改为非导航键属性。

这就是我设计实体的方式:

public class Offer : BaseEntity, IAggregateRoot
{
   ...
   public ICollection<OfferProduct> OfferProducts { get; private set; } = new List<OfferProduct>();
   public Guid InquiryId { get; private set; }
   public virtual Inquiry Inquiry { get; private set; } = default!;
}
public class OfferProduct : BaseEntity, IAggregateRoot
{
    ...
    public Guid OfferId { get; private set; }
    public virtual Offer Offer { get; private set; } = default!;
    public Guid InquiryProductId { get; private set; }
    public virtual InquiryProduct InquiryProduct { get; private set; } = default!;
}
public class Inquiry : BaseEntity, IAggregateRoot
{
    ...
    public ICollection<Offer> Offers { get; private set; } = new List<Offer>();
    public ICollection<InquiryProduct> Products { get; private set; } = new List<InquiryProduct>();
}
public class InquiryProduct : BaseEntity, IAggregateRoot
{
    ...
    public Guid InquiryId { get; private set; }
    public virtual Inquiry Inquiry { get; private set; } = default!;
    public ICollection<OfferProduct> OfferProducts { get; private set; } = new List<OfferProduct>();
}

Ardalis 表示导航属性应该仅在一侧定义。 不知道是因为某些 DDD 原则还是因为它有一些性能缺陷?

Ardalis 规范包中的存储库仅适用于聚合根。

OfferProduct 实体仅使用 Offer 实体创建,并且永远不会更新。

InquiryProduct 实体仅使用 Inquiry 实体创建,并且永远不会更新。

我有一个业务用例,我需要获取不仅属于一个 OfferOfferProducts,而且还按 InquiryProductId 进行过滤,所以我认为最简单的方法是是使用 IAggregateRoot 接口标记 OfferProduct 实体并直接从存储库中查询它。但我认为这是作弊而且不正确,因为如果我理解正确,AggregateRoot 应该是唯一的,我应该始终从根查询。

我可以从 Inquiry 聚合根获取它,但我的规范必须如此复杂:

public class InquiryProductOffersSpec : Specification<Inquiry, InquiryDetailsDto>, ISingleResultSpecification
{
    public InquiryProductOffersSpec(Guid inquiryId, Guid productId) =>
        Query
            .Where(i => i.Id == inquiryId)
            .Include(i => i.Products.Where(ip => ip.Id == productId))
                .ThenInclude(ip => ip.OfferProducts);
}

从 DDD 角度来看,这可能更正确,但查询的性能将低于简单的 select * from OfferProducts wherequeryProductId = 'someId'

所以我的问题是:

我是否应该从 InquiryProductOfferProduct 实体中删除 IAggregateRoot 并仅从查询实体? 为什么最好仅将导航属性保留在关系的一侧? 也许我的实体和关系设计不正确,这就是为什么我正在努力处理这个复杂的查询?

我先介绍一下系统的操作: 系统可以使用其 InquiryProducts 创建查询,然后可以为每个查询创建报价,并且每个报价可以有一些与 InquiryProduct 相关的 OfferProduct。

在撰写本文时,我想到也许唯一的 AggregateRoot 应该是 Inquiry 实体,因为没有 Inquiry 任何其他实体都无法存在。但在系统中,我还需要独立于查询来获取(搜索)报价,如果我不使用 IAggregateRoot 接口标记报价,我就无法做到这一点。

I have a lack of understanding of the DDD aggregate topic.

I do have an Offer aggregate that has navigation property to its children's collection OfferProducts.
When I learned entity framework I thought I should always define navigation properties on both sides of the relation but Ardalis (maintainer of Specification package for ef https://github.com/ardalis/Specification) wrote somewhere these words which I do not understand correctly:

You want to avoid having navigation properties that span Aggregates.
So you need to decide where navigation properties should go, and where
non-navigation key properties should go instead.

This is how I designed my entities:

public class Offer : BaseEntity, IAggregateRoot
{
   ...
   public ICollection<OfferProduct> OfferProducts { get; private set; } = new List<OfferProduct>();
   public Guid InquiryId { get; private set; }
   public virtual Inquiry Inquiry { get; private set; } = default!;
}
public class OfferProduct : BaseEntity, IAggregateRoot
{
    ...
    public Guid OfferId { get; private set; }
    public virtual Offer Offer { get; private set; } = default!;
    public Guid InquiryProductId { get; private set; }
    public virtual InquiryProduct InquiryProduct { get; private set; } = default!;
}
public class Inquiry : BaseEntity, IAggregateRoot
{
    ...
    public ICollection<Offer> Offers { get; private set; } = new List<Offer>();
    public ICollection<InquiryProduct> Products { get; private set; } = new List<InquiryProduct>();
}
public class InquiryProduct : BaseEntity, IAggregateRoot
{
    ...
    public Guid InquiryId { get; private set; }
    public virtual Inquiry Inquiry { get; private set; } = default!;
    public ICollection<OfferProduct> OfferProducts { get; private set; } = new List<OfferProduct>();
}

Ardalis is saying that navigation properties should be defined only on one side.
I do not know if it is because of some DDD principles or maybe because it has some performance drawbacks?

Repository from your Ardalis specification package only works with aggregate root.

OfferProduct entities are created only with the Offer entity and are never updated.

InquiryProduct entities are created only with the Inquiry entity and are never updated.

I have a business use case where I need to fetch OfferProducts not only belonging to one Offer but filtered by InquiryProductId so I thought the easiest way will be to mark the OfferProduct entity with IAggregateRoot interface and query it from the repository directly. But I think it's cheating and it's not correct because if I understand correctly AggregateRoot should be the only one and I should always query from the root.

I could fetch it from the Inquiry aggregate root but then my specification would have to be that complex:

public class InquiryProductOffersSpec : Specification<Inquiry, InquiryDetailsDto>, ISingleResultSpecification
{
    public InquiryProductOffersSpec(Guid inquiryId, Guid productId) =>
        Query
            .Where(i => i.Id == inquiryId)
            .Include(i => i.Products.Where(ip => ip.Id == productId))
                .ThenInclude(ip => ip.OfferProducts);
}

This probably would be more correct from the DDD perspective but the query will be less performant than simple select * from OfferProducts where inquiryProductId = 'someId'

So my questions are:

should I remove IAggregateRoot from InquiryProduct and OfferProduct entities and fetch only from the Inquiry entity?
why it is better to keep navigation properties only on one side of the relation?
maybe my entities and relations are designed incorrectly and that's why I am struggling with that complex query?

I will introduce the operation of the system:
The system can create inquiries with its InquiryProducts, then there can be offers created for each inquiry and each offer can have some OfferProducts related to the InquiryProduct.

When writing it thought came to my mind that maybe the only AggregateRoot should be the Inquiry entity as any of the other entities can't exist without Inquiry. But In the system, I also need to fetch(search) offers independently of inquiry and I couldn't do it if I won't mark Offer with an IAggregateRoot interface.

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

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

发布评论

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

评论(1

杀手六號 2025-01-26 15:12:58

我是否应该从 InquiryProduct 和 OfferProduct 实体中删除 IAggregateRoot 并仅从 Inquiry 实体中获取?

是的。聚合根的全部要点是将实体组织成顶级实体,负责其依赖项,而这些实体对于自己的查询来说并没有真正的意义。

为什么最好仅将导航属性保留在关系的一侧?

只有当拥有双向引用有明显好处时才应使用双向引用。否则,它们只会导致有多种途径来获取信息,这可能会导致昂贵的、意外的延迟加载调用,或者在禁用延迟加载时导致“损坏”链接。

例如,客户和订单之类的事物之间的关系可以将两者视为聚合根。获取有关特定客户的订单的信息以及从给定订单获取有关客户的信息的价值将存在争议。与订单和用户(创建者/修改者)或订单/客户和地址等关系等场景相比。订单受益于能够访问有关创建或最后修改它的用户的信息或地址详细信息,但跟踪用户创建/修改的订单或给定地址可能是什么订单没有多大意义与.相关联。

如果需要,您仍然可以通过聚合根查询此信息,而无需依赖双向引用。例如,如果我确实关心特定用户修改了哪些订单,我不需要结构开销和“混乱”:

var orders = currentUser.OrdersICreated;
// or 
var orders = currentUser.OrdersIModified;

因为系统中的大多数实体可能会跟踪诸如 CreatedBy/ModifiedBy 引用回用户的内容,开始对 User 实体中的每个实体集合进行双向引用将是荒谬的。

如果这些不是双向引用...如果有需要,我可以使用聚合根:

var ordersQuery = _context.Orders.Where(x => x.CreatedBy.UserId == currentUserId);

依赖双向引用的问题是您最终会在内存中进行大量处理,从内存的角度来看,这是昂贵的,而且随着时间的推移,您最终会处理可能陈旧的数据。在上面的示例中,返回构建查询而不是依赖导航属性意味着我可以利用投影来获取我可能需要的详细信息,这可能像 .Any() 检查或.Count()

在充分利用 EF 时,我的建议是适应利用其查询和投影来构建高效的查询,然后仅在您实际需要处理理想情况下单个实体及其相关的完整图片时才处理聚合根细节。

should I remove IAggregateRoot from InquiryProduct and OfferProduct entities and fetch only from the Inquiry entity?

Yes. The whole point of an aggregate root is to organize entities into top-level entities responsible for their dependents which don't really make sense to query on their own.

why it is better to keep navigation properties only on one side of the relation?

Bi-directional references should only be used when there is a clear benefit to having them. Otherwise they just lead to having multiple pathways to get to information that can either result in expensive, unexpected lazy load calls or "broken" links if Lazy Loading is disabled.

For example, a relationship between something like a Customer and Order can make sense to treat both as aggregate roots. There will be a arguable value to get information about Orders for a particular customer, and value in getting information about a Customer from a given Order. Versus scenarios like relationships like Orders and Users (Created By/Modified By) or Orders/Customers and Addresses. An Order benefits from being able to access information about a User that created or last modified it, or Address details, but it doesn't make much sense to bother tracking what Orders a user Created/Modified, or what Order a given Address might be associated with.

You can still query this information if needed through the aggregate root without relying on bi-directional references. For instance if I do happen to care about what orders a particular user did modify, I don't need the structural overhead and "mess" of:

var orders = currentUser.OrdersICreated;
// or 
var orders = currentUser.OrdersIModified;

Since most entities in a system might track something like a CreatedBy/ModifiedBy reference back to a User, it would be ridiculous to start putting bi-directional references to every collection of entities in the User entity.

Where these aren't bi-directional references... I can instead use the aggregate root if and when there is a need:

var ordersQuery = _context.Orders.Where(x => x.CreatedBy.UserId == currentUserId);

The problem with relying on bi-directional references is that you end up doing a lot of processing in-memory, which is expensive from a memory standpoint, as well as you end up dealing with potentially stale data over time. In the above example, going back to build queries rather than relying on navigation properties means that I can leverage projection to get back just the details I might need, which could be something as simple as a .Any() check or a .Count().

My advice when it comes to getting the most out of EF is to adapt to leverage its querying and projection to build efficient queries, then deal with aggregate roots solely when you actually need to work with a complete picture of ideally a single entity and it's related details.

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