【问题标题】:Strategy to select correct Aggregate root, so transaction won't span multiple aggregates选择正确聚合根的策略,因此事务不会跨越多个聚合
【发布时间】:2019-04-27 21:30:37
【问题描述】:

我正在对类别/菜单域上下文进行建模,并决定为此上下文设置 2 个聚合根。

 public class MenuItem : Aggregate<Guid>
{
    public List<string> ImageUrls { get; set; }
    public decimal Price { get; set; }
    public IList<ExtraProperty> Extras { get; set; }
    public ITranslationList<MenuItemTranslation> Translations { get; set; }
    public bool Active { get; set; }
}




 public class Category : Aggregate<Guid>
 {
    public ITranslationList<CategoryTranslation> Translations { get; set;}
     public SortedList<int,Guid> Children { get; set; }
    public List<string> ImageUrls { get; set; }

    internal Category() { }


}

在 Category 模型中,Children 属性是子 Category 和 MenuItems id 的排序列表。

现在假设我要创建类别。为此我有一个命令:

  public class CreateCategoryCommand:ICommand
  {
    public Guid Id { get; set; }
    public List<string> ImageUrls { get; set; }
    public ITranslationList<CategoryTranslation> Translations  { get; set; }
    public Guid UserId { get; set; }
    public Guid? ParentId { get; set; }
    public int ParentSortIndex { get; set; }
  }

所以这里发生的事情是,我创建类别,如果设置了 ParentId 属性,我从存储库中获取具有该 ID 的类别,将记录添加到子排序列表并保存父类别。

在这种情况下,问题是事务跨越 2 个聚合(新创建的聚合和父级)。

因此,我感觉我对聚合边界的建模不正确。一方面,我尝试按照 Vaughn Vernon 的建议使聚合尽可能小(这就是为什么 Category 包含 Id 引用,而不是实际对象的原因),另一方面,在保存一个聚合时,事务跨越多个聚合,这是设计缺陷恕我直言。

您对这种环境建模有什么策略/建议/意见?

【问题讨论】:

    标签: transactions domain-driven-design aggregate


    【解决方案1】:

    您的类别处于分层结构中。您通过添加包含子类别 ID 的 Children 属性对它们进行建模有什么特别的原因吗?

    如果您通过删除 Children 属性并添加 ParentID 属性将参考方向从子级转向父级,这将解决您的一致性边界问题。添加新的类别不会影响父级。

    您可以将方法 GetChildren(parentID)GetChildrenIDs(parentID) 添加到 CategoryRepository 以获取子项或其 ID 类别(如果需要)。

    编辑:

    获得有关应用程序及其要求的更多信息在实施中很重要。不同的需求会导致不同的不变量,也会导致聚合的一致性边界不同。

    我将针对特定要求给出一个示例实现。它们并不完整,因为编写所有案例的所有代码将需要大量文本。

    让我们问几个关于类别排序的问题。

    • 问题 1:ParentSortIndex 是如何从 Command 发送方计算出来的,以便设置为 命令?

    • 问题 2: 如果一个 Category 没有子级,那么接收带有 Command 是否有效ParentSortIndex = 10?

    • 问题 3:ParentSortIndex 重要还是类别的顺序唯一重要?

    假设 Categories 的顺序是唯一重要的事情,它是如何实现的,或者 SortIndex 的值并不重要。

    首先让我们介绍一下SortingIndex的概念。现在,让我们考虑一下这个概念的实现。我们可以使用 float 作为 SortingIndex 的值而不是 int(如果我们期望有很多 Categories,则可以使用 double)。花车有一个很好的属性,你可以(几乎)总能找到一个适合其他两个花车的。例如,如果您有 1 和 2,则 1.5 在它们之间,1.2 在 1 和 1.5 之间等等。

    接下来让我们添加 CategoryRepository.GetSortingIndicesForChilren(parentId) 方法。此方法将为父级的所有子级获取具有 CategoryGuid 和 SortingIndex 属性的对象,以便我们可以计算紧邻请求的 SortingIndex类别

    这将避免加载所有子项。从 Repositories 返回特殊值是一种不错的技术。在DDD book 中,Eric Evans 对此进行了解释,并表示 Repositories 返回此类包含一些信息或数据的特殊对象是很正常的。

    接下来让我们指定要将新子类别放置到哪个子类别,而不是指定具体的索引值。 (我们可能希望将它放在类别的上方,但为了简单起见,我将跳过这种情况。它可以通过添加到 Command 的 enum { placeAbove, placeBellow } 来解决)

    public class SortingIndex : ValueObject {
    
      public static readonly MinValue = new SotringIndex(float.MinValue);
      public static readonly MidValue = new SotringIndex(float.MaxValue);
      public static readonly MaxValue = new SotringIndex(float.MaxValue);
    
      public float Value { get; private set; }
    
      public SortingIndex(float value) { .... }
    
      public SortingIndex GetBtween(SortingIndex other) { ... }
    
      public static operator > (OrderingPriority other) { .. }
      public static operator >= (OrderingPriority other) { .. }
      // other operators <=, ==, != etc.
    }
    
    public class Category : Aggregate<Guid> {
       public Guid ParentGuid { get; private set; }
       public SortingIndex SortingIndex { get; private set; }
       // constructor and other stuff......
    }
    
    public class CreateCategoryCommand : ICommand
    {
        public Guid? ParentId { get; set; }
        public Guid? CategoryGuidToPlaceNextTo { get; set; }
        // other stuff...
    }
    
    public class CreateCategoryCommandHandler {
    
      public void Handle(CreateCategoryCommand cmd) {
    
        var sortingIndex = SortingIndex.MidValue;
    
        // start with mid value. If there aren't any children, this will be the 
           first. Later when we add other children we can calculate an index 
           before of after this one.
    
        if(cmd.ParentID != null && cmd.CategoryGuidToPlaceNextTo != null) {
    
              var childrenSortingIndices = CategoryRepository
                               .GetSortingIndicesForChilren(cmd.ParentID);
    
               sortingIndex = PlaceChildNextTo(
                                childrenSortingIndices,
                                cmd.CategoryGuidToPlaceNextTo);
         }
    
        var category = new Category(cmd.ID, cmd.ParentID, sortingIndex, ...);
    
        CategoryRepository.Save(category);
      }
    }
    

    在上述情况下,由于没有任何规则可以为索引指定特定值,因此我们可以通过避免子节点之间的冲突和必须改变任何状态的方式来实现它们。

    拥有一个包含孩子的集合会导致该集合的状态突变。

    使用整数会导致索引之间发生冲突的可能性很高,并且会导致重新计算子索引。这将跨越多个聚合。

    添加新的Category很简单,因为我们只需要找到在指定类别之后(或两个类别之间)的索引,而不需要修改集合或其他子类别。 p>

    如果上述情况不成立,并且 SortingIndex 的值有规则,则意味着需要满足额外的不变量,并且会导致不同的一致性边界。

    您仍然可以通过具有最终一致性或使用Saga 来实现这一点,该Saga 将管理父类别和新类别之间的分布式事务。在这种情况下,您无法逃避最终一致性,并且会担心其他事情。

    如果您认为最终一致性是一个问题并且您不想处理复杂性,那么如果您的应用程序允许,您可以在同一个事务中修改两个聚合。您不能在分布式应用程序中执行此操作。

    【讨论】:

    • 当然,但是当我需要对类别内的项目进行排序时会出现问题。在建议的情况下,我需要加载父类别的所有子类别来更改排序索引,这是真正的性能问题。
    猜你喜欢
    • 2018-07-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-25
    相关资源
    最近更新 更多