如何正确处理事件溯源中的聚合关系?

How to handle aggregates relationship in Event Sourcing properly?

当拥有某种"complex"领域模型时,不可避免地会有相关实体(这就是聚合根的意义)。但是我应该如何从事件中重建关系呢?在序列化事件中通过其他聚合 ID 进行搜索显然不是一种选择。

我有一个想法使用这样的数据库结构(例如)。它具有仅包含 id 和外键的聚合表,以简化从不同实体检索所有必要事件的过程。这不是违反了ES原则吗?

DDD says that "rich domain model" consists from entities which handles business logic, and we work with them as they model the domain objects

是的,当您使用事件溯源时仍然如此。

So I can imagine i.e. order.addItem(product) method, where OrderItem creating and making relationship with Order and Product.

啊哈,不——这两种方法都不是。如果 Order 和 Product 是不同的聚合,那么 order.addItem(product) 不是您要使用的拼写。订单和产品不对产品的状态承担责任。或者,换句话说,我们从不在彼此内部嵌套聚合根。

根据 DDD 的规则,通常的拼写是 order.addItem(product.id)。请注意这一重要区别:更改订单不能更改产品的任何详细信息。

这是要点的一部分:您域中的每个状态都有一个负责维护其一致性的机构。

注意:拼写 order.addItem(product) 对模型有意义,其中 Product 是一个不是聚合根的实体,而是从属于 Order(更准确地说,每个 Product 仅与一个订单关联).

But if Item has many-to-one relationship to Order, shouldn't it contain orderId instead? (Not Order containing list of ItemId). But that would mean it should be Item.AddToOrder(order.Id), which makes not so much sense.

简短的回答是,根据您决定如何对数据建模以及哪些项目负责维护聚合的完整性,您会得到不同的方法拼写。

向后工作 - 聚合的部分动机(而不是整个模型周围只有一个大的一致性边界)是同时修改模型的不同部分的想法。 OrderItem 是否是一个独立于 Order 的聚合将部分地取决于可以同时修改两个不同的 OrderItem 的重要性。

对于像在线购物车这样的情况,这可能不是特别重要。

对于多方试图同时修改同一订单的设置,例如

OrderItem.create(order.id, product.id)

不会不合理。

And still, aggregate root contains other aggregates, and OrderItem in that case is aggregate, not aggregate root or value object.

聚合根负责一个聚合。该聚合(基本上只是 "state")在概念上可以是从属于根的多个实体的当前状态,每个实体管理整体的特定部分。

And if I'm right, aggregate root contains business logic to control inner aggregates, so we still need to build aggregate root which contains other entities in it.

"Inner aggregate" 没有意义——聚合不嵌套。 实体 嵌套,最外层的实体扮演聚合根的角色。

那么,我们如何构建嵌套实体?

让我们退后一步,看看我们通常如何创建单个实体。我们 运行 一些查询(如 getById)来获取我们之前保存的 State

Factory {
    Entity fromState(currentState) {
        return new (entity);
    }

    State fromEntity(theEntity) {
        return theEntity.getState();
    }
}

嵌套实体的工作方式相同,从属实体接管部分工作。对于订单,它可能看起来像...

Factory {
    Order fromState(currentState) {
        List<OrderItem> items = ...

        for (State.Item itemState : currentState.items()) {
            OrderItem orderItem = OrderItem.from(itemState)
            items.add(orderItem)
        }

        return new Order(items);
    }
}

Order {
    State getState() {
        State currentState = State.EMPTY;

        for(OrderItem orderItem : this.items) {
            currentState = currentState.addItemState(orderItem.getState())

        return currentState
    }
} 

当我们使用事件源时,发生的一点变化是我们使用事件集合而不是状态。

Factory {
    Order fromEvents(history) {

        // The one tricky bit -- the history we will be looking
        // at is a mix of histories from all of the entities that
        // coordinate the changes to the aggregate, so we may need
        // to untangle that.

        Map<OrderItemId, Events> = itemHistories
        for (Event e : history )
            items.put(e.orderItemId, e)

        List<OrderItem> items = ...

        for (Events events: itemHistories.values) {
            OrderItem orderItem = OrderItem.from(events)
            items.add(orderItem)
        }
        return new Order(items);
    }
}

Order {
    List<Event> getEvents () {
        List<Event> events = new List();

        for(OrderItem orderItem : this.items) {
            events.addAll(orderItem.getEvents())
        }
        return events
    }
}

您会处理与键的任何关系,但它们不会像数据库中的外键约束那样被强制执行。事实上,您的商店活动中用户的密钥可能是人类可读的 ID。不需要通过 Guids link。

从一个到另一个的映射是在投影中完成的,可以帮助您在命令中显示正确的信息和正确的字段。