如何在事件存储中 serialize/deserialize 事件,在其中存储聚合根是不是很糟糕?

How to serialize/deserialize events in event store and is it so bad to store aggregate roots in them?

我正在尝试在 PHP 中制作 CQRS 事件源应用程序。我想知道,是否可以将聚合根(以下示例中的 AbstractItem)放入在数据库中序列化的事件中? (我想没有,但有什么选择呢?)例如,我有 AddItemToCartCommand 的命令处理程序,使用此处理方法:

    public function handle(Command $command)
    {
        Assertion::isInstanceOf($command, AddItemToCartCommand::class);

        $cart = $this->loadCart($command->getCartId());
        $item = $this->loadItem($command->getItemId());

        $cart->addItem($item);

        $this->eventRepository->save($cart);
    }

CartAbstractItem 是聚合根。 我的 Cart AR 是这样实现的:

class Cart extends AggregateRoot
{

    /** @var UuidInterface */
    private $customerId;

    /** @var AbstractItem[] */
    private $items;

    public function __construct(UuidInterface $cartId, UuidInterface $customerId)
    {
        $this->apply(new EmptyCartCreated($cartId, $customerId));
    }

    public function addItem(AbstractCartItem $item)
    {
        $this->apply(new ItemToCartAdded($this->getId(), $item));
    }

    protected function applyEmptyCartCreated(EmptyCartCreated $event)
    {
        $this->setId($event->getCartId());
        $this->customerId = $event->getCustomerId();
    }

    protected function applyItemToCartAdded(ItemToCartAdded $event)
    {
        $item                                 = $event->getItem();
        $this->items[(string) $item->getId()] = $item;
    }
}

现在,问题出在具有以下结构的 ItemToCartAdded 事件中:

class ItemToCartAdded extends AbstractDomainEvent
{

    /** @var UuidInterface */
    protected $cartId;

    /** @var AbstractItem */
    protected $item;

    public function __construct(UuidInterface $cartId, AbstractItem $item)
    {
        $this->cartId = $cartId;
        $this->item   = $item;
    }

    public function getCartId(): UuidInterface
    {
        return $this->cartId;
    }

    public function getItem(): AbstractItem
    {
        return $this->item;
    }
}

也许我应该有一些 DTO 对象来保存 AbstractItem 数据而不是 AbstractItem AR 本身在 ItemToCartAdded 事件中。然后我会在 applyItemToCartAdded 方法中根据这些 DTO 数据实例化新的 AbstractItem 对象。

但是由于项目是抽象的class,我不知道我需要实例化什么实现。当然,我可以在那个 DTO 中有 class 名称,所以我知道然后以某种方式破解它。但这似乎有点矫枉过正,因为我在 AbstractItem 上有私有构造函数并在特定实现中使用工厂方法。

另一方面,序列化整个聚合根让我遇到了一个序列化问题:当我序列化 AR 时,我需要在某个时候反序列化,但我该怎么做呢?我知道有反射,但绕过我在反序列化上的 AR 验证是一种丑陋的 hack,因此我最终会得到某种无效的 AR,这可能会陷入噩梦。还是不行?

我应该如何序列化和反序列化我的事件才能很好地解决这个问题?也许我可以在 ItemToCartAdded 事件中只包含这两个聚合的 id,但那样我就无法将该事件应用于 Cart AR 以便它具有 AbstractItems 的列表(最终保护一些我的不变量)。我最终只会得到购物车 AR 中的项目 ID 列表,因为我当然无法访问我的 AR 中的存储库。

我的问题在哪里或者我哪里错了?

顺便说一下,我使用这个库的那个分支:https://github.com/beberlei/litecqrs-php

简单的答案 - 您不会将聚合放入事件中。

事件代表状态的变化。您不是存储聚合的当前状态,而是捕获更改的内容。这是你的活动。在您的情况下,添加的项目的 id 将是所需的最少信息。顺便说一句,为了方便起见,我喜欢在我的活动中添加更多信息。

为什么在事件中存储 AR 不好?

事件是不可变的。 AR 根据定义是可变的。还封装了一个AR,这使序列化充其量变得笨拙。

I am trying to make CQRS event sourced app in PHP. And I wonder, is it ok to put aggregate root (AbstractItem in following example) into event that is serialized in db? (I suppose no, but what is alternative?)

不,你不能。我同意@Codescribler。 我认为你的设计有问题。

Whereas Cart and AbstractItem are aggregate roots.

为什么要将 AbstractItem 设为 Aggregate rootAbstractItem 保护什么不变量?它处理什么 Commands 以及 Events 产生什么?这是您的主要设计问题。你应该根据你的 Ubiquitous language 来命名它,比如 Product 或者如果你需要一个更抽象的名字那么 CartItem

Product 是聚合根,但在不同的有界上下文中,如 Inventory。或者如果不需要保护任何不变量,它可以是一个 CRUD 实体。在该 BC 中,它处理您商店中产品的创建、修改和删除。

您现在位于 Ordering bounded context,您应该查看此 BC 中的域规则。那么,你的规则是什么?您是否允许 Inventory BC 中的价格修改传播到 Cart 中的项目?我不这么认为。您的客户会非常生气,至少不会因为他们将商品添加到购物车而没有收到价格更改的任何通知。因此,让我们假设一旦将产品添加到购物车,价格就会冻结。所以,它将是不可变的。

现在介绍 Cart。您需要有关添加到 CartProducts 的哪些信息以保护您的 不变性 Cart 不变量是什么?好吧,您可以有一个最大 Order 价值,比方说 10000 美元。所以你需要标识一个ProductProduct price。 因此,CartItem 是不可变的 Value Object,包含 ProductIdProductPrice。 因此,您的代码应如下所示:

namespace Domain\Ordering;

class Cart
{
    /** @var CartItem[] */
    private $items = [];

    const MAXIMUM_CART_VALUE = 10000;

    public function handleAddItemToCart(AddItemToCart $command)
    {
        if ($this->getTotalCartValue() + $command->getItem()->getTotalItemPrice() > self::MAXIMUM_CART_VALUE) {
            throw new \Exception(sprintf("A cart value cannot be more than %d USD", self::MAXIMUM_CART_VALUE));
        }

        yield new AnItemWasAddedToCart($command->getCartId(), $command->getItem());
    }

    public function applyAnItemWasAddedToCart(AnItemWasAddedToCart $event)
    {
        $this->items[] = $event->getItem();
    }

    private function getTotalCartValue()
    {
        return array_reduce($this->items, function (float $acc, CartItem $item) {
            return $acc + $item->getTotalItemPrice();
        }, 0.0);
    }
}

class AddItemToCart implements Command
{

    /**
     * @var CartId
     */
    private $cartId;
    /**
     * @var CartItem
     */
    private $item;

    public function __construct(
        CartId $cartId,
        CartItem $item
    )
    {
        $this->cartId = $cartId;
        $this->item = $item;
    }

    public function getCartId(): CartId
    {
        return $this->cartId;
    }

    public function getItem(): CartItem
    {
        return $this->item;
    }
}

class AnItemWasAddedToCart implements Event
{
    /**
     * @var CartId
     */
    private $cartId;
    /**
     * @var CartItem
     */
    private $item;

    public function __construct(
        CartId $cartId,
        CartItem $item
    )
    {
        $this->cartId = $cartId;
        $this->item = $item;
    }

    public function getCartId(): CartId
    {
        return $this->cartId;
    }

    public function getItem(): CartItem
    {
        return $this->item;
    }
}

class CartId
{
    //...
}

class ProductId
{
    //...
}

class CartItem
{
    /**
     * @var ProductId
     */
    private $productId;
    /**
     * @var float
     */
    private $pricePerUnit;
    /**
     * @var int
     */
    private $quantity;

    public function __construct(
        ProductId $productId,
        float $pricePerUnit,
        int $quantity
    )
    {
        $this->productId = $productId;
        $this->pricePerUnit = $pricePerUnit;
        $this->quantity = $quantity;
    }

    public function getProductId(): ProductId
    {
        return $this->productId;
    }

    public function getTotalItemPrice()
    {
        return $this->pricePerUnit * $this->quantity;
    }
}