使用 DDD 和 CQRS 从读取模型获取数据与从写入模型接收数据

问题描述 投票:0回答:1

我第一次使用 DDD 做一个项目。该项目与汽车租赁有关,有一些事情让我很困惑,我找不到答案。如果有人能给我详细而清晰的答案,我会非常高兴。

我理解,在简单的情况下,例如经理想要添加/更新新的/现有的车辆,他最终会转向将数据写入数据库的写入模型,并且在他想要查看现有车辆的情况下,比如他对每一个读写请求都会转向读取模型等等。

我的问题是这样的,我将给出一个来自我的项目的直接例子,我收到一个租车请求,其中包含以下数据:接送地点、租赁日期、驾驶员年龄等,我需要在这些日期将可用车辆返还给客户 + 优惠价格 所有这些,根据我的理解,应该是从几个不同的聚合计算出来的,所以我正在制作一个将计算数据的域服务,问题是在这种情况下我应该在哪里将数据返回给客户端,毕竟我不会将计算结果保存在数据库中只是为了从读模型发出另一个请求,那么当我需要从写模型立即得到答案并且不需要通过数据库的情况下我该怎么办?

在这种情况下,域服务中函数的对象是否直接返回给客户端?如果是的话,是哪些实体?

c# domain-driven-design cqrs clean-architecture
1个回答
0
投票

例如,经理想要添加/更新新的/现有的车辆,他最终转向将数据写入数据库的写入模型

首先也是最重要的一点,经理不会转向任何类型的模型。管理者是用户,不关心实现,他们关心业务流程,不涉及“数据库”、“模型”、“存储库”等技术术语

读取模型写入模型是抽象术语。根据您域的实现,这可能意味着

  1. 两个不同的数据库或表
  2. 一个共享数据库或表

一些架构选择会影响您是否选择其中一种。使用不同表/数据库的一个原因可能是当您想要使用事件源或当您的域非常纯粹时(域模型中没有与数据库/基础设施相关的属性或当ORM阻抗不匹配特别大时)并且与您读取的模型不同。

将读写模型拆分为多个表/数据库的另一个标准是,当您希望它们以不同的方式扩展时,例如,如果汽车租赁公司很大或有很多访客,您需要许多服务器来服务查询,虽然实际保留的很少,那么你也可以分摊。这样可以轻松地分别扩展读取模型和写入模型。此外,在预订实际租赁时,查询端的非常高的负载不会减慢您的业务。

当领域更简单且差异足够小时或者您对 DDD 不是很务实/教条并且甚至不需要采购时,您使用共享表。

如果有两个不同的数据库,您将拥有写入模型,在保存数据时,已发出由“读取模型”使用的消息来创建只读(或者更确切地说:可查询,因为通过默认事件源模型根本无法查询)模型。

到目前为止是基础知识。

CQRS本身更关心这些的技术实现如何。通常,您可以使用进程内请求/查询框架来执行此操作,例如 .NET 的“MediatR”,其中您将消息拆分为“查询”和“命令”,其中“命令处理程序”包含您的写入逻辑或(不太常见)一个只读存储库和一个进程管理器(又名 saga)。

我收到租车请求,其中包含以下数据:接送地点、租赁日期、驾驶员年龄等,我需要在这些日期将可用车辆归还给客户+价格优惠

首先不清楚为什么需要司机年龄来查询,因为这些数据与想要租用它的人有点无关。

无论如何,您想要创建一个

AvailableCarsQuery
或类似的东西作为“读取模型”/查询

public class AvailableCarRequest : IRequest<RentableCar[]>
{
    public string? PickUpLocation { get; set; }
    public string? DropOffLocation { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
}

public class RentableCar
{
    public int Id { get; set; }
    public string Brand { get; set; }
    public string Model { get; set; }
    public decimal Price { get; set; }
    public string Currency { get; set; }
}

AvailableCarsQuery
定义了您的查询参数以及返回的预期返回值(
IRequest<RentableCar[]>
)。然后实现处理程序

public AvailableCarHandler : IRequestHandler<AvailableCarRequest, RentableCars[]>
{
    public async Task<RentableCar[]> Handle(AvailableCarRequest request, CancellationToken cancellationToken)
    {
        // do your query here
        RentableCar[] rentableCars = await ...;
        return rentableCars;
    }
}

具体查询的外观取决于您是否拥有共享或单独的数据库/表。如果您发现自己的写入模型很难查询,即您需要执行数十次调用才能获取数据或者必须在内存中搜索,请考虑将持久性拆分到不同的表中,这些表对查询性能进行了更优化。

然后调用它

[HttpPost("search/cars")]
public Task<ActionResult<RentableCar[]>> GetAvailableCars(AvailableCarRequest request)
{
    RentableCar[] rentableCars = await mediator.Send(request);
    return Ok(rentableCars);
}

写入端看起来类似,但你会有一个

RentCarCommand
,并且(通常)没有返回值。

public class RentCarCommand : IRequest
{
    public int CarId { get; set; }
    public string PickUpLocation { get; set; }
    public string DropOffLocation { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
    public int CustomerId { get; set; }
}

public class RentCarHandler : IRequestHandler<RentCarcommand>
{
    public async Task Handle(Jing request, CancellationToken cancellationToken)
    {
        Car car = await unitOfWork.Cars.GetById(request.CarId);
        Customer customer = await unitOfWork.Customers.GetById(request.CustomerId);
        car.RentTo(customer, request.From, request.To, request.PickUpLocation, request.DropOffLocation);
        await unitOfWork.SaveChangesAsync();
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.