cqrs - 在 api 平台中实现事件源/CQRS 方法

标签 cqrs symfony4 event-sourcing api-platform.com command-query-separation

在官方 Api 平台网站上有一个 General Design Considerations页面。

Last but not least, to create Event Sourcing-based systems, a convenient approach is:

  • to persist data in an event store using a custom data persister
  • to create projections in standard RDBMS (Postgres, MariaDB...) tables or views
  • to map those projections with read-only Doctrine entity classes and to mark those classes with @ApiResource

You can then benefit from the built-in Doctrine filters, sorting, pagination, auto-joins, etc provided by API Platform.

因此,我尝试通过一种简化来实现这种方法(使用一个数据库,但读写分离)。

但是失败了...有一个问题,我不知道如何解决,所以恳请您的帮助!

我创建了一个 User Doctrine 实体和注释字段,我想用 @Serializer\Groups({"Read"}) 公开。我将在这里省略它,因为它非常通用。

用于 api 平台的 yaml 格式的

User 资源:

# config/api_platform/entities/user.yaml

App\Entity\User\User:
    attributes:
        normalization_context:
            groups: ["Read"]
    itemOperations:
        get: ~
    collectionOperations:
        get:
            access_control: "is_granted('ROLE_ADMIN')"

因此,如上所示,User Doctrine 实体是只读的,因为只定义了 GET 方法。

然后我创建了一个CreateUser DTO:

# src/Dto/User/CreateUser.php

namespace App\Dto\User;

use App\Validator as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;

final class CreateUser
{
    /**
     * @var string
     * @Assert\NotBlank()
     * @Assert\Email()
     * @AppAssert\FakeEmailChecker()
     */
    public $email;
    /**
     * @var string
     * @Assert\NotBlank()
     * @AppAssert\PlainPassword()
     */
    public $plainPassword;
}
用于 api 平台的 yaml 格式的

CreateUser 资源:

# config/api_platform/dtos/create_user.yaml

App\Dto\User\CreateUser:
    itemOperations: {}
    collectionOperations:
        post:
            access_control: "is_anonymous()"
            path: "/users"
            swagger_context:
                tags: ["User"]
                summary: "Create new User resource"

所以,在这里您可以看到只定义了一个 POST 方法,正好用于创建新用户。

这里是路由器显示的内容:

$ bin/console debug:router
---------------------------------- -------- -------- ------ -----------------------
Name                               Method   Scheme   Host   Path
---------------------------------- -------- -------- ------ -----------------------
api_create_users_post_collection   POST     ANY      ANY    /users
api_users_get_collection           GET      ANY      ANY    /users.{_format}
api_users_get_item                 GET      ANY      ANY    /users/{id}.{_format}

我还添加了自定义 DataPersister 来处理 POST/users。在 CreateUserDataPersister::persist 中,我使用了 Doctrine 实体来写入数据,但对于这种情况并不重要,因为 Api 平台不知道 DataPersister 将如何写入它。 所以,从概念上来说——它是读和写的分离。

读取由 Api 平台附带的 Doctrine 的 DataProvider 执行,写入由自定义 DataPersister 执行。

# src/DataPersister/CreateUserDataPersister.php

namespace App\DataPersister;

use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Dto\User\CreateUser;
use App\Entity\User\User;
use Doctrine\ORM\EntityManagerInterface;

class CreateUserDataPersister implements DataPersisterInterface
{
    private $manager;

    public function __construct(EntityManagerInterface $manager)
    {
        $this->manager = $manager;
    }

    public function supports($data): bool
    {
        return $data instanceof CreateUser;
    }

    public function persist($data)
    {
        $user = new User();
        $user
            ->setEmail($data->email)
            ->setPlainPassword($data->plainPassword);

        $this->manager->persist($user);
        $this->flush();

        return $user;
    }

    public function remove($data)
    {

    }
}

当我执行创建新用户的请求时:

POST https://{{host}}/users
Content-Type: application/json

{
  "email": "test@custom.domain",
  "plainPassword": "123qweQWE"
}

问题! 我收到 400 响应 ... ...

但是,一条新记录被添加到数据库中,因此自定义 DataPersister 可以工作;)

根据 General Design Considerations实现了写入和读取分离,但未按预期工作。

我很确定,我可能缺少一些需要配置或实现的东西。所以,这就是它不起作用的原因。

很乐意得到任何帮助!

更新 1:

问题出在 \ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameResolver::getRouteName() 中。在第 48-59 行,它遍历所有路线,试图找到适合的路线:

  • $operationType = 'item'
  • $resourceClass = 'App\Dto\User\CreateUser'

但是 $operationType = 'item' 仅为 $resourceClass = 'App\Entity\User\User'定义,所以它找不到路由并抛出异常。

更新 2:

所以,这个问题听起来可能是这样的:

如何使用 Doctrine 实体进行读取并使用 DTO 进行写入来实现读取和写入分离(CQS?),两者都驻留在同一路由上,但使用不同的方法?

更新 3:

Data Persisters

  • store data to other persistence layers (ElasticSearch, MongoDB, external web services...)
  • not publicly expose the internal model mapped with the database through the API
  • use a separate model for read operations and for updates by implementing patterns such as CQRS

是的!我想要那个...但是如何在我的示例中实现它?

最佳答案

简答

问题是 Dto\User\CreateUser 对象正在为响应序列化,而实际上,您实际上希望返回并序列化 Entity\User。

长答案

当 API 平台序列化一个资源时,它们会为该资源生成一个 IRI。 IRI 生成是代码呕吐的地方。默认的 IRI 生成器使用 Symfony 路由器实际构建基于 API 平台创建的 API 路由的路由。

因此,为了在实体上生成 IRI,需要定义一个 GET 项目操作,因为这是将成为资源 IRI 的路由。

在您的情况下,DTO 没有 GET 项目操作(也不应该有),但是当 API 平台尝试序列化您的 DTO 时,它会抛出该错误。

修复步骤

从您的代码示例来看,似乎正在返回用户,但是,从错误中可以清楚地看出用户实体不是被序列化的实体。

要做的一件事是安装调试包,使用 bin/console server:dump 启动转储服务器,并在 API Platform WriteListener 中添加一些转储语句:ApiPlatform\Core\EventListener\WriteListener 在第 53 行附近:

dump(["Controller Result: ", $controllerResult]);
$persistResult = $this->dataPersister->persist($controllerResult);
dump(["Persist Result: ", $persistResult]);

Controller Result 应该是你的 DTO 的一个实例,Persist Result 应该是你的 User 实体的一个实例,但我猜它会返回你的 DTO。

如果它返回您的 DTO,您只需调试并弄清楚为什么 DTO 从 dataPersister->persist 而不是 User 实体返回。也许您的系统中有其他数据持久性或事物可能会导致冲突。

希望这对您有所帮助!

关于cqrs - 在 api 平台中实现事件源/CQRS 方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51954752/

相关文章:

mysql - Symfony4/Doctrine - 无法通过 SSL 连接(DBAL 配置)

php - 在 symfony 4 上允许 CORS

spring-data-jpa - 轴突框架 : how to configure mutiple databases?

c# - CQRS,仍然没有完全理解,数据库中保存了什么;(

c# - 事件溯源基础设施实现

architecture - 更新 CQRS 中的查询部分

java - 为什么 Axon Framework 中的 RetryScheduler 在出现 NoHandlerForCommandException 后不重试?

reactjs - 使用 Jest 和 Enzyme(在 Symfony 中)进行 react 测试得到 "Syntax Error: Unexpected token import"

java - 是否有 Java 端口或 NEventStore 库的等效项?

cqrs - 什么触发了 CQRS 客户端应用程序中的 UI 刷新?