领域实体关系加载和性能问题

huangapple go评论62阅读模式
英文:

Domain entity relations loading and performance issue

问题

I'm sorry, but I cannot provide a translation of the code you've provided as it contains a significant amount of technical and programming-related content. If you have any specific questions or need assistance with understanding parts of the code, please feel free to ask, and I'll do my best to help.

英文:

I'm new in DDD previously I was working more with services and transaction scripts. Now I'm working with a team on a DDD project and from my point of view there is too much unnecessary data is loading and easy to forget about some data to load. I hope someone can help me to understand if there something wrong in this DDD project or it's me who just doesn't get it.

Assuming that we domain model and entity Order with relation. To change order status should I load all its relations and then all nested relations? (example 1)

If I load only the relation that I need isn't then my Order not fully created object that can cause some issue later?(example 2)

Also if I do not load every relation (because there are a lot of nested relations) operation may throw exception or just wrong result, but how I can possibly know which relation are need without checking method implementation? (example 3)

public class Application
{
    SomeDbContext _context;
    public Application(SomeDbContext context)
    {
        _context = context;
    }
    public void RunSomeLogic()
    {

        ///example 1
        var orders = _context.Orders
              .Include(x => x.Invoices).ThenInclude(x => x.Payments)
              .Include(x => x.Customer)
              .Include(x => x.VerifyByUser).Where(x => x.Status == OrderStatus.Paid).ToList();
        foreach (var order in orders)
            order.RefreshStatus();
        _context.SaveChanges();

        ///example 2
        var orders2 = _context.Orders
              .Include(x => x.Invoices).ThenInclude(x => x.Payments)
              .Include(x => x.VerifyByUser).Where(x => x.Status != OrderStatus.Paid).ToList();
        foreach (var order in orders)
            order.RefreshStatus();

        _context.SaveChanges();
        ///example 3
        var orders3 = _context.Orders
              .Include(x => x.Invoices)//.ThenInclude(x => x.Payments)
              .Include(x => x.Customer)
              .Include(x => x.VerifyByUser).Where(x => x.Status == OrderStatus.Paid).ToList();
        foreach (var order in orders)
            order.RefreshStatus();
        _context.SaveChanges();

    }

}

public enum OrderStatus
{
    New,
    Veryfied,
    Paid
}
public class User
{ }
public class Order
{

    public ICollection<Invoice> Invoices { get; set; }
    public OrderStatus Status { get; set; }
    public User VerifyByUser { get; set; }
    public Customer Customer { get; set; }
    public DateTime OrderDater { get; set; }
    public Guid Id { get; set; }
    public void RefreshStatus()
    {
        if (Status == OrderStatus.New && VerifyByUser != null)
            Status = OrderStatus.Veryfied;
        else if (Status == OrderStatus.Veryfied )
        {
            foreach (var invoice in Invoices)
                invoice.CheckPayments();
             if( Invoices.All(x => x.IsPaid))
                Status = OrderStatus.Paid;
        }
    }
}
public class Invoice
{
    public Order Order { get; set; }
    public int OrderId { get; set; }

    public ICollection<Payment> Payments { get; set; }
    public bool IsPaid { get; set; }
    public decimal Value { get; set; }
    public void CheckPayments()
    {
        if (Payments?.Sum(x => x.Value) >= Value)
            IsPaid = true;
        else
            IsPaid = false;
    }
}
public class Payment
{
    public Invoice Invoice { get;set; }
    public int InvoiceId { get; set; }
    public decimal Value { get; set; }
    ///code

}
public class Customer
{
    ///code
}

答案1

得分: 3

在DDD中,你(或你加入之前的团队)混淆了DDD实体和EF Core实体。

在DDD中,你不会对持久性层做任何假设。理想情况下,大多数领域对象甚至不应该有Id属性(除了聚合根),因为这主要是关系数据库的属性。其他类型的数据库(文档数据库)没有或不需要这些属性(只需用于文档本身)。

首先,在EF Core中,实体是任何未定义为“拥有类型”(EF Core中处理值类型的Microsoft的第一次尝试)且具有ID的复杂对象。

以订单和发票为例,从DDD的角度来看,除了Invoice.Order属性之外,没有任何东西是“实体”。InvoiceOrder都是聚合根,它们是事务边界。

保存在订单上的所有操作都需要在单个事务中执行,因此在加载聚合时需要加载所有属性。

对于发票,情况有点不同。发票(=聚合)引用了Order(=另一个聚合)。

这应该被建模为没有直接引用Order,因为发票不能加载另一个聚合。相反,它必须是public OrderId Order { get; },换句话说,只是一个引用。

如果加载一个订单并需要对订单进行任何修改,您需要创建一个领域服务,该服务加载Invoice聚合,然后根据ID加载Order聚合。

例如,如果您希望订单的状态在发票付款时更改,您将创建一个领域服务(或者如果您使用消息传递,则为命令处理程序/通知),如下所示:

public class PaymentService
{
    public Task InvoicePaid(InvoiceId invoiceId, IEnumerable<Payments> payments)
    {
        Invoice invoice = await unitOfWork.Invoices.GetById(invoiceId);
        Order order = await unitOfWork.Orders.GetById(invoice.OrderId);

        invoice.MakePayments(payments);
        order.Completed();

        await unitOfWork.SaveChangesAsync();
    }
}

这就是领域服务的用途,协调多个聚合之间的交互并将其包装到一个事务中。

尽管如此,DDD在文档数据库中效果更好,因为它更好地表示“聚合”作为文档的概念。

此外,您的聚合的命名和封装至少有些问题。您在某个地方设置了IsPaid,然后调用了CheckPayment()。这是非常容易出错的,因为开发人员很容易修改您的领域模型或忘记调用CheckPayment()

与其这样:

public class Invoice
{
    public Order Order { get; set; }
    public int OrderId { get; set; }

    public ICollection<Payment> Payments { get; set; }
    public bool IsPaid { get; set; }
    public decimal Value { get; set; }
    public void CheckPayments()
    {
        if (Payments?.Sum(x => x.Value) >= Value)
            IsPaid = true;
        else
            IsPaid = false;
    }
}

您可能更喜欢像这样的东西:

public class Invoice
{
    public OrderId OrderId { get; }

    public IEnumerable<Payment> Payments { get; }
    public bool IsPaid { get; }
    public MonetaryAmount TotalSum { get; }

    public void MakePayment(IEnumerable<Payment> paymentsDone)
    {
        // 在支付时进行一些基本验证

        if(IsPaid) 
        {
            throw new InvalidOperationException("Invoice is already paid!");
        }

        payments.AddRange(paymentsDone);
        IsPaid = Payments.Sum(x => x.Value) >= TotalSum;
        Status = OrderStatus.Paid;
    }
}

现在,您的领域模型受到保护。没有人可以从类的外部设置IsPaidPayments。他们必须使用MakePayment方法并传递支付。该方法将验证输入,将支付添加到订单并更新IsPaidStatus属性。

这个模型非常容易进行单元测试。由于您不能修改Invoice,因此在服务中需要较少的测试(因为它们只协调)。

英文:

You (or your team before you joined) are confusing DDD Entities and EF Core entities.

In DDD you don't make any assumptions of the persistence layer at all. Ideally there shouldn't even be Id properties (except on the aggregate) on most domain objects, because that's mostly a property of relational databases. Other type of databases (document databases) don't have or require these (only for the document itself).

First, in EF Core entities are any complex objects which are not defined as "owned types" (Microsofts first attempt on addressing value types in EF Core) and have an id.

Taking an your order and Invoice as example, nothing there is an "entity" in DDD sense, except for Invoice.Order property. Invoice and Order are aggregates, they are the transactional boundary.

All operations saved on an order, need to be done in a single transaction, hence all of the properties need to be loaded at the time the aggregate is loaded.

On the invoice, its a bit different. The Invoice (=aggregate) is referencing Order (=another aggregate).

This should and must be modeled in a way that there is no direct reference to the Order, because an invoice can't be loading another aggregate. Instead, it would have to be public OrderId Order { get; }, in other words: Just a reference.

If you load an order and need to do any modifications on the order, you would need to create a domain service, that loads the Invoice aggregate and then load the Order aggregate from the id).

For example, if you want the status of the order to change, when an invoice is paid you would create an domain service (or a command handler/notification if you are using messaging) like

public class PaymentService
{
    public Task InvoicePaid(InvoiceId invoiceId, IEnumerable&lt;Payments&gt; payments)
    {
        Invoice invoice = await unitOfWork.Invoices.GetById(invoiceId);
        Order order = await unitOfWork.Orders.GetById(invoice.OrderId);

        invoice.MakePayments(payments);
        order.Completed();

        await unitOfWork.SaveChangesAsync();
    }
}

That's what domain services are for, to coordinate interaction between multiple aggregates and wrap it into one transaction.

That being said, DDD works better with document databases, as it better represents the "aggregate" as document.

Also the naming and encapsulation of your aggregates is questionable at best. You are setting IsPaid somewhere then calling CheckPayment(). This is very error prone, as its easy for a developer to modify your domain model or forget to call CheckPayment().

Instead you'd want to encapsulate your model

Instead of

public class Invoice
{
    public Order Order { get; set; }
    public int OrderId { get; set; }

    public ICollection&lt;Payment&gt; Payments { get; set; }
    public bool IsPaid { get; set; }
    public decimal Value { get; set; }
    public void CheckPayments()
    {
        if (Payments?.Sum(x =&gt; x.Value) &gt;= Value)
            IsPaid = true;
        else
            IsPaid = false;
    }
}

you would prefer something like

public class Invoice
{
    public OrderId OrderId { get; }

    public IEnumerable&lt;Payment&gt; Payments { get; }
    public bool IsPaid { get; }
    public MonetaryAmount TotalSum { get; }

    public void MakePayment(IEnumerable&lt;Payment&gt; paymentsDone)
    {
        // do some basic validation on payments

        if(IsPaid) 
        {
            throw new InvalidOperationException(&quot;Invoice is already paid!&quot;);
        }

        payments.AddRange(paymentsDone);
        IsPaid = Payments.Sum(x =&gt; x.Value) &gt;= TotalSum;
        Status = OrderStatus.Paid;
    }
}

Now, your domain model is protected. No one can set IsPaid or Payments from outside the class. They MUST use MakePayment method and pass the payments. The method will validate the input, add the payments to the order and update the IsPaid and Status properties.

And this model is very easy to unit test. And since you can't modify the Invoice, there is less testing required in the services (as they only coordinate)

答案2

得分: 1

Polling loops to do something like update a status /w a DDD approach is bound to get expensive. One option to help cut back the cost of eager loading all of that data would be to leverage a Split Query.

使用轮询循环来执行类似于使用DDD方法更新状态的操作很容易变得昂贵。帮助减少急切加载所有数据成本的一个选择是利用分割查询。

A DDD approach for something like this would be instead to ensure that an Invoice has a method to "MakePayment" which the only way that the system allows a Payment entity to be added to the invoice, refreshing the invoice's status, and then signaling to its order if its status has changed. Even then you will need to know what entity relationships a DDD operation (MakePayment) is going to need and either have those eager loaded or accept the cost of lazy loading them. Ultimately this is best done at the time you are making changes to a single aggregate root rather than polling through a set of aggregate roots.

对于类似这样的情况,DDD方法可以确保发票有一个“MakePayment”方法,这是系统允许将付款实体添加到发票的唯一方式,刷新发票的状态,然后如果其状态已更改,则向其订单发出信号。即使如此,您仍需要知道DDD操作(MakePayment)需要哪些实体关系,要么急切加载它们,要么接受惰性加载它们的成本。最终,最好在对单个聚合根进行更改时执行此操作,而不是轮询一组聚合根。

If the system needs to accommodate other services etc. adding payment records to the database, then you need to consider an approach and a supporting data structure that can easily identify these new payment records, load them with their respective invoice & order they apply to, and proceed to use the same logic to refresh those statuses. This would involve looking at something like a CreatedAt DateTime and likely an indicator of some kind on the payment to mark rows that were inserted by an alternate system and need additional processing.

如果系统需要适应其他服务等将付款记录添加到数据库中,那么您需要考虑一种方法和支持的数据结构,可以轻松识别这些新的付款记录,将它们与相应的发票和订单一起加载,并继续使用相同的逻辑来刷新这些状态。这将涉及查看类似于CreatedAt DateTime的内容,可能还包括在付款中的某种指示器,用于标记由替代系统插入的行,需要额外处理。

英文:

Polling loops to do something like update a status /w a DDD approach is bound to get expensive. One option to help cut back the cost of eager loading all of that data would be to leverage a Split Query.

A DDD approach for something like this would be instead to ensure that an Invoice has a method to "MakePayment" which the the only way that the system allows a Payment entity to to be added to the invoice, refreshing the invoice's status, and then signalling to it's order if it's status has changed. Even then you will need to know what entity relationships a DDD operation (MakePayment) is going to need and either have those eager loaded or accept the cost of lazy loading them. Ultimately this is best done at the time you are making changes to a single aggregate root rather than polling through a set of aggregate roots.

If the system needs to accommodate other services etc. adding payment records to the database, then you need to consider an approach and a supporting data structure that can easily identify these new payment records, load them with their respective invoice & order they apply to, and proceed to use the same logic to refresh those statuses. This would involve looking at something like a CreatedAt DateTime and likely a indicator of some kind on the payment to mark rows that were inserted by a alternate system and need additional processing.

huangapple
  • 本文由 发表于 2023年5月11日 03:50:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/76222124.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定