如何以 DDD 方式部分更新聚合

How to partially update aggregate in a DDD manner

我正在以 DDD 方式使用 C# .NET 开发应用程序。我也查了eshopcontainers,但是没有说明我想知道的,所以让我在这里提出一个问题。我的问题是

详情
我正在开发一个 RSVP 应用程序。现在我想做的是向尚未投票的人发送提醒邮件。我的领域模型和基础设施层是这样的。

//Domain Model
class RSVP //RootAggreagate
{
    public long Id {get; private set;}
    public List<TimeSlot> TimeSlots {get; private set;}  

    public AutoRemindRule AutoRemindRule {get; private set;}
}

class AutoRemindRule 
{
    public long Id {get; private set;}
    public int IntervalHour { get; private set; }
    public DateTimeOffset NextTriggerDate { get; private set; }
    public DateTimeOffset RemindBeginDate { get; private set; }

    //Foreign Key for Plan
    public long RSVPId


    void SetNextTriggerDate() 
    {
        //Compute NewNextTriggerDate based on IntervalHour and RemindBeginDate field.
        NextTriggerDate = NewNextTriggerDate;
    }
}

//Infrastructure Layer (EF Core)
public class MyDbContext : DbContext
{
    //Plan Aggregate
    public DbSet<RSVP> RSVPs { get; set; }
    public DbSet<TimeSlot> TimeSlots { get; set; }
    public DbSet<AutoRemindRule> AutoRemindRules { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        ...
    }
}

在这个模型中,我想 运行 定期检查 AutoRemindRules 中的 NextTriggerDate 的批次,如果 NextTriggerDate 比现在旧,则批次发送邮件给没有投票 RSVP 的用户。最终,批处理通过调用 SetNextTriggerDate 更新 NextTriggerDate。如果我在批处理中在Usecase(Application)层编写如下代码,我可以实现我想要的。但是,我认为这些代码不遵循 DDD 规则,因为它会部分更新聚合根。在应用层编写这样的代码是否可以,如果不行,谁能告诉我更好的编码方式?

//Usecase(Application) layer in a batch
using (var context = new MyDbContext) 
{
    var rules = context.AutoRemindRules.Where(i => i.NextTiggerdate < DateTimeOffset.Now);
    foreach (var rule in rule)
    {
        SendRemindMail();
        rule.SetNextTriggerDate();

        context.SaveChanges();
    }
}

更新
另一种方法是创建一个方法来更新聚合根中的 AutoRemindRule,然后批处理使用该方法。但是,我担心的是性能和数据库的负载。在那种情况下,除了 AutoRemindRule 记录之外,批处理还必须读取一堆不必要的 RSVP 记录。我想知道是否有另一种方法可以在保持 DDD 方式的同时减少数据库的负载。

Is it OK to partially update an root aggregate directly from batch operation?

简短的回答是:否。

聚合的要点是封装数据和公开业务操作,以便检查和执行验证和不变量。如果您让外部进程修改聚合的数据,那么它就完全违背了它们的目的。

可能的解决方案:

  1. 不要使用聚合。如果您只有一个 table 和一些数据以及一个批处理作业,但没有或几乎没有要执行的业务逻辑,那么就这样做:一个 table 和一个批处理作业。乍一看,您似乎唯一需要强制执行的是 NextTriggerDate 是未来的某个时间,您可以将其编码到批处理作业中。但我可以想象你可以有更多的规则,比如不要触发超过 X 次,不要早于 1 天触发等等。或者甚至让聚合决定下一个触发日期,基于一些内部逻辑(首先1 天后触发,2 天后触发第二次,依此类推)。聚合对于这些事情非常方便,因为每个聚合将存储它们计算下一个状态变化所需的状态。

  2. 评估加载完整RSVP聚合是否真的有问题。加载一些额外的列对于大多数应用程序来说应该不会对性能造成很大的影响,并且是为了获得聚合封装其业务逻辑的好处而付出的必要代价,以防你需要做我在前面提到的那些事情观点。相反,如果您正在构建一个规模庞大且业务逻辑很少的功能,那么请考虑采用不同的方法。

  3. 如果问题是加载完整聚合意味着加载像 TimeSlots 这样的大集合,而这恰好对于该业务操作不是必需的,您可以通过在您的存储库加载聚合而不加载该集合。您应该编写需要该集合的聚合根操作来验证集合是否已加载,否则失败,以避免错误。