【问题标题】:How to avoid masses of data loads in CQRS/DDD Commands/Events?如何避免 CQRS/DDD 命令/事件中的大量数据加载?
【发布时间】:2019-10-03 11:26:26
【问题描述】:

我们有一个包含研讨会、参与者和他们的膳食的 DDD AppDomain。每个研讨会最多可容纳 1000 名参与者,每位参与者最多可享用 50 餐。我们决定研讨会、参与者和膳食是聚合体,以保持这些聚合体较小。 用户可以重新安排所有参与者的整个研讨会或重新安排单个参与者。所以我们有命令“RescheduleSeminarCommand”和“RescheduleParticipantCommand”。

当您重新安排研讨会时会出现问题:“RescheduleSeminarCommand”会导致“SeminarRescheduledEvent”,而“SeminarRescheduledEvent”会导致每个参与者出现“RescheduleParticipantCommand”。这意味着从存储库中加载每个参与者 - 所以 1000 个数据库请求。每个“RescheduleParticipantCommand”都会导致一个“ParticipantRescheduledEvent”,它会触发“RescheduleMealsCommand”,它会为每个参与者加载膳食——因此还有 1000 个数据库请求。

我们怎样才能减少数据库请求的数量?

1) 我们考虑使用 SeminarId 扩展“RescheduleParticipantCommand”和“RescheduleMealsCommand”,这样我们不仅可以加载一个参与者/餐点,还可以加载整个研讨会的所有参与者/餐点。

2) 另一种方法是创建额外的事件/命令,例如“RescheduleParticipantsForSeminarCommand”、“ParticipantsForSeminarRescheduleEvent”和“RescheduleMealsForSeminarCommand”等。

你觉得哪个更好? 1)、2) 还是我们没有想到的不同?


好的,我将提供一些我在第一次描述中遗漏的细节:

如果有以下类

class Seminar
{
    UUID SeminarId,
    DateTime Begin,
    DateTime End
}

// Arrival/Departure of a participant may differ
// from Begin/End of the seminar
class Participant
{
    UUID ParticipantId
    UUID SeminarId,
    DateTime Arrival,
    DateTime Departure
}

// We have one Meal-Object for breakfast, one for lunch and 
// one for dinner (and additional for other meals) per day 
// of the stay of the participant
class Meal
{
    UUID MealId,
    UUID ParticipantId,
    DateTime Date,
    MealType MealType
}

用户可以

  • 使用“RescheduleParticipantCommand”更改单个参与者的到达/离开,这也会将他们的用餐更改为新的日期。

  • 使用“RescheduleSeminarCommand”更改研讨会的开始/结束,这会将所有参与者的到达/离开更改为新的开始/结束并相应地更改他们的膳食。

【问题讨论】:

  • 乍一看,您的 Schedule bounded context 中的聚合设计似乎是错误的,但我需要扩展信息,了解每个命令执行中的哪些变化以及为什么要确认它,也许会给您一个有用的响应。你能用一个工作流示例来扩展这个问题吗?
  • 感谢您的回复。我在描述中添加了一些细节。请参见上文。
  • 谢谢,但您忘记发布最重要的信息:规则。每个参与者都可以拒绝到达/离开更改吗?那时发生了什么?您重新安排了一半的参与者,或者只是取消了整个研讨会的重新安排?吃饭也是同样的问题。
  • 在等待回复时,我想向您指出一件事:命令是可能被您的域规则拒绝的东西。如果您因某个事件而发出命令(某事已经完成并且不能因为它通过所有域规则而被拒绝),请记住这一点;即使新命令因为被拒绝而什么也不做;您的系统必须处于一致的状态。基本规则:如果您引发事件是因为系统处于一致状态,即使该事件意味着系统中有更多命令可能被拒绝或不会改变系统中的任何内容。
  • 好的。一些规则: - 如果我在 2019 年 6 月 1 日至 2019 年 6 月 5 日举行研讨会并将其重新安排到 2019 年 6 月 2 日至 2019 年 6 月 6 日,那么所有参与者都应在 2019 年 6 月 2 日到达于 2019 年 6 月 6 日出发,他们每天的膳食应相应更改 - 当我有一个参与者从 2019 年 6 月 3 日到 06/06/2019 并且我重新安排到 2019 年 6 月 4 日到 2019 年 6 月 6 日时膳食应相应改变

标签: domain-driven-design cqrs ddd-repositories domain-model


【解决方案1】:

您可能缺少SeminarSchedule 的概念。首先让我们问几个会影响模型的问题

  • 如果你有一个研讨会,它是分为某种讲座、演示等,还是只是针对不同时间的不同人的同一件事的研讨会?

  • 你能先签一个人参加研讨会,然后再决定这个人什么时候参加吗?

我会用伪代码举例。

注意:我会跳过用餐,因为问题是关于日程安排的,但它们确实适合这个模型。我也会讨论与它们相关的逻辑,只需在代码中跳过它们

首先让我们说一下我们的要求。

  • 这是一个分为时间段的一件事(讲座、培训课程)的研讨会。相同的讲座将在不同的时间开始给不同的人。

  • 参与者可以在没有时间段的情况下签名。

  • 当参与者签名时,我们需要根据喜好为他/她做一顿饭(例如他/她可能是素食主义者或素食主义者)。

  • 计划将在系统用户的特定时间完成。他们会在做日程安排时获取参与者的信息。例如,我们可能希望在一个时间段或按照其他标准让年龄相同的人。

代码如下:

class Seminar {
    UUID ID;
    // other info for seminar like description, name etc.
}

class Participant {
    UUID ID;
    UUID SeminarID;
    // other data for participant, name, age, meal preferences etc.
}

class TimeSlot {

    Time StartTime;
    TimeInterval Duration;
    ReadonlyCollection<UUID> ParticipantIDs;

    void AddParticipant(UUID participantID) { }
}

class SeminarSchedule {

    UUID SeminarID;

    Date Date;
    Time StartTime;
    TimeInterval Duration;

    ReadOnlyCollection<TimeSlot> TimeSlots;

    void ChangeDate(Date newDate) { }
    void ChangeStartTime(Time startTime) { }
    void ChangeDuration(TimeInterval duration) { }
    void ScheduleParticipant(Participant p, Time timeSlotStartTime) { }
    void RemoveParticipantFromSchedule(Participant p) { }
    void RescheduleParticipant(Participant p, Time newTimeSlotStartTime) { }
}

这里我们有 3 个聚合:SeminarParticipantSeminarSchedule

如果您需要更改与 SeminarParticipant 相关的任何信息,您只需针对这些聚合。

另一方面,如果您需要执行与计划相关的任何操作,SeminarSchedule 聚合(作为围绕计划的事务边界)将处理这些命令以确保一致性。您还可以对计划实施并发控制。您可能不希望多人同时更改日程安排。例如,一个更改StartTime 而另一个更改Duration 或让两个用户将相同的参与者添加到计划中。您可以在 SeminarSchedule 聚合上使用 Optimistic Offline lock

例如,更改SeminarScheduleStartTimeDuration 将影响所有TimeSlots

如果您从Seminar 中删除Participant,那么您也必须将其从计划中删除。这可以通过最终一致性和处理ParticipantRemoved 事件来实现,或者您可以使用Saga

在对聚合进行建模时,我们需要考虑的另一件事是签署研讨会的逻辑是如何工作的。

假设参与者在安排研讨会之前应先签署研讨会。也许稍后将通过按某些标准定义人群来执行调度。上面的模型可以正常工作。它将允许用户将Participant 签名到Seminar。稍后在分配日程时,其他用户将能够通过查看签署了哪些类型的参与者来制定日程。

让我们以相反的情况为例,说未安排的参与者不能出席研讨会。

在这种情况下,我们可以将 Participant 实体添加到 SeminarSchedule 聚合中,但这会导致您加载整个聚合,即使您需要更改单个参与者的某些信息。这不是很实用。

所以为了保持良好的分离,我们可以使用 SagaProcessManager以确保一致性。我们还可以在SeminarSchedule 聚合中添加ReservedPlace 的概念。通过这种方式,您可以预订一个位置,然后将参与者添加到日程表中,然后通过将参与者分配到时间段来删除ReservedPlace。因为这是一个跨越多个聚合的复杂过程,所以Saga 肯定已经到位。

另一种方法是定义一个人可以制作的SeminarSignRequest 的概念。稍后,如果有饭菜和/或有空位,此请求可能会被批准。我们可能已经达到最大人数或没有足够的饭菜等。这可能也是一个过程,因此您可能也需要在这里Saga

如需了解更多信息,请查看this articlethis video

【讨论】:

    【解决方案2】:

    命令是您的域规则可能拒绝的东西。如果您因某个事件而发出命令(某事已经完成并且不能因为它通过所有域规则而被拒绝),请记住这一点;即使新命令因为被拒绝而什么也不做;您的系统必须处于一致的状态。基本规则:如果您引发事件是因为系统处于一致状态,即使该事件意味着系统中有更多命令可能被拒绝或不会改变系统中的任何内容。

    因此,根据您的 cmets,一旦研讨会聚合根据其规则接受新日期;您无需查看更多规则即可更改参与者日期。

    那么解决方案就是在持久性中更改所有内容,而不是针对您想要的每个更改发送垃圾邮件细粒度命令。

    关系数据库示例:

    Update Seminar ( Begin , End) Values ( '06/02/2019' ,06/06/2019 ) where SeminarID = @SeminarID;
    
    Update Participant ( Arrival , Departure ) Values ( '06/02/2019' ,06/06/2019 ) where SeminarId = @SeminarID
    

    PS:为什么不让 Seminar Begin/End 持久化并将这些数据放入参与者(到达/离开)聚合的 hidratation 中?这样一来,您的系统中的状态始终保持一致,而无需担心更改一些内容。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-10-21
      • 1970-01-01
      • 1970-01-01
      • 2012-08-12
      • 1970-01-01
      • 2015-11-21
      • 2015-12-25
      相关资源
      最近更新 更多