使用DDD模式实现简单API(8/1)

使用纯SQL语句和DDD模式实现简单的读写分离(转)

转载自(http://www.kamilgrzybek.com/design/simple-cqrs-implementation-with-raw-sql-and-ddd/

介绍
这篇文章将展示如何使用.NET CORE和CQRS,完成一个简单的REST API应用。这是最简单的实现版本-通过Write Model(写模型)更新数据,Read Model(读模型)的数据也立即更新。在大多数的情况下,将逻辑拆分为读和写并使用各自的模型是非常有效的,在这篇文章里的示例程序中就是如此。具体代码在github(https://github.com/kgrzybek/sample-dotnet-core-cqrs-api.git)。

目标
通过这个解决方案,希望完成下面几个目标:
1、清晰的拆分读模型和写模型。
2、使用读模型获取数据应该尽可能快。
3、写模型应该使用DDD模式实现;DDD实现的层级视领域的复杂度而定。
4、应用的逻辑应该与UI层解耦。
5、选择的第三方库应该是成熟的、知名的且有团队支持的。
设计
组件间高层级的流程如下:

如上图,读取数据的流程是相当简单直接的,因为需要尽可能快的获取数据。在这里不需要多级抽象和复杂的实现方式。从查询对象获取参数,执行SQL语句获取数据即可完成流程。
在写入数据的时候情况就不一样了,写操作通常需要更加进阶的技术,因为需要执行逻辑、计算数据或者进行验证。使用拥有跟踪改变功能的ORM工具和仓储模式,可以实现上述需求,并保持领域模型的完整。


解决方案
读模型
下面的图展示了完成读取操作时,组件之间的数据流转:

UI层负责创建查询对象:

 1 /// <summary>
 2 /// Get customer order details.
 3 /// </summary>
 4 /// <param name="orderId">Order ID.</param>
 5 [Route("{customerId}/orders/{orderId}")]
 6 [HttpGet]
 7 [ProducesResponseType(typeof(OrderDetailsDto), (int)HttpStatusCode.OK)]
 8 public async Task<IActionResult> GetCustomerOrderDetails(
 9     [FromRoute]Guid orderId)
10 {
11     var orderDetails = await _mediator.Send(new GetCustomerOrderDetailsQuery(orderId));
12 
13     return Ok(orderDetails);
14 }
1 internal class GetCustomerOrderDetailsQuery : IRequest<OrderDetailsDto>
2 {
3     public Guid OrderId { get; }
4 
5     public GetCustomerOrderDetailsQuery(Guid orderId)
6     {
7         this.OrderId = orderId;
8     }
9 }

然后,查询处理器执行查询操作:

 1 internal class GetCustomerOrderDetialsQueryHandler : IRequestHandler<GetCustomerOrderDetailsQuery, OrderDetailsDto>
 2 {
 3     private readonly ISqlConnectionFactory _sqlConnectionFactory;
 4 
 5     public GetCustomerOrderDetialsQueryHandler(ISqlConnectionFactory sqlConnectionFactory)
 6     {
 7         this._sqlConnectionFactory = sqlConnectionFactory;
 8     }
 9 
10     public async Task<OrderDetailsDto> Handle(GetCustomerOrderDetailsQuery request, CancellationToken cancellationToken)
11     {
12         using (var connection = this._sqlConnectionFactory.GetOpenConnection())
13         {
14             const string sql = "SELECT " +
15                                "[Order].[Id], " +
16                                "[Order].[IsRemoved], " +
17                                "[Order].[Value] " +
18                                "FROM orders.v_Orders AS [Order] " +
19                                "WHERE [Order].Id = @OrderId";
20             var order = await connection.QuerySingleOrDefaultAsync<OrderDetailsDto>(sql, new {request.OrderId});
21 
22             const string sqlProducts = "SELECT " +
23                                "[Order].[ProductId] AS [Id], " +
24                                "[Order].[Quantity], " +
25                                "[Order].[Name] " +
26                                "FROM orders.v_OrderProducts AS [Order] " +
27                                "WHERE [Order].OrderId = @OrderId";
28             var products = await connection.QueryAsync<ProductDto>(sqlProducts, new { request.OrderId });
29 
30             order.Products = products.AsList();
31 
32             return order;
33         }
34     }
35 }
 1 public class SqlConnectionFactory : ISqlConnectionFactory, IDisposable
 2 {
 3     private readonly string _connectionString;
 4     private IDbConnection _connection;
 5 
 6     public SqlConnectionFactory(string connectionString)
 7     {
 8         this._connectionString = connectionString;
 9     }
10 
11     public IDbConnection GetOpenConnection()
12     {
13         if (this._connection == null || this._connection.State != ConnectionState.Open)
14         {
15             this._connection = new SqlConnection(_connectionString);
16             this._connection.Open();
17         }
18 
19         return this._connection;
20     }
21 
22     public void Dispose()
23     {
24         if (this._connection != null && this._connection.State == ConnectionState.Open)
25         {
26             this._connection.Dispose();
27         }
28     }
29 }

首先,获取打开的数据库连接,这通过SqlConnectionFactory类实现,这个类从IOC容器的Http Request生命周期中获取,所以可以保证,在一次请求过程中使用相同的数据库连接。

第二步是准备和执行SQL,这里尝试不和数据库表关联而是与数据库视图关联。这使应用与数据库的表解耦,尽可能的隐藏数据库的内部结构。

对于SQL的执行,使用了微型ORM框架-Dapper,因为它差不多和本地的ADO.NET一样快。简而言之,它做了它该做的事情,而且做的很好。

写模型

下图展示了写操作时,组件之间的数据流转:

写操作的起点和读操作差不多,但是创建了Command对象代替了Query对象。

 1 /// <summary>
 2 /// Add customer order.
 3 /// </summary>
 4 /// <param name="customerId">Customer ID.</param>
 5 /// <param name="request">Products list.</param>
 6 [Route("{customerId}/orders")]
 7 [HttpPost]
 8 [ProducesResponseType((int)HttpStatusCode.Created)]
 9 public async Task<IActionResult> AddCustomerOrder(
10     [FromRoute]Guid customerId, 
11     [FromBody]CustomerOrderRequest request)
12 {
13    await _mediator.Send(new AddCustomerOrderCommand(customerId, request.Products));
14 
15    return Created(string.Empty, null);
16 }

接下来,命令处理器被调用。

 1 public class AddCustomerOrderCommandHandler : IRequestHandler<AddCustomerOrderCommand>
 2 {
 3     private readonly ICustomerRepository _customerRepository;
 4     private readonly IProductRepository _productRepository;
 5 
 6     public AddCustomerOrderCommandHandler(
 7         ICustomerRepository customerRepository, 
 8         IProductRepository productRepository)
 9     {
10         this._customerRepository = customerRepository;
11         this._productRepository = productRepository;
12     }
13 
14     public async Task<Unit> Handle(AddCustomerOrderCommand request, CancellationToken cancellationToken)
15     {
16         var customer = await this._customerRepository.GetByIdAsync(request.CustomerId);
17 
18         var selectedProducts = request.Products.Select(x => new OrderProduct(x.Id, x.Quantity)).ToList();
19         var allProducts = await this._productRepository.GetAllAsync();
20 
21         var order = new Order(selectedProducts, allProducts);
22         
23         customer.AddOrder(order);
24 
25         await this._customerRepository.UnitOfWork.CommitAsync(cancellationToken);
26 
27         return Unit.Value;
28     }
29 }

命令处理器不同于查询处理器,因为在写操作中通常会比读操作复杂,在这里使用了DDD方法的高度抽象,Aggregates和Entities。命令处理器创建Aggregates并调用其方法,最终保存数据到数据库。

客户Aggregate定义如下:

 1 public class Customer : Entity
 2 {
 3     public Guid Id { get; private set; }
 4 
 5     private readonly List<Order> _orders;
 6 
 7     private Customer()
 8     {
 9         this._orders = new List<Order>();
10     }
11 
12     public void AddOrder(Order order)
13     {
14         this._orders.Add(order);
15 
16         this.AddDomainEvent(new OrderAddedEvent(order));
17     }
18 
19     public void ChangeOrder(Guid orderId, List<OrderProduct> products, IReadOnlyCollection<Product> allProducts)
20     {
21         var order = this._orders.Single(x => x.Id == orderId);
22         order.Change(products, allProducts);
23 
24         this.AddDomainEvent(new OrderChangedEvent(order));
25     }
26 
27     public void RemoveOrder(Guid orderId)
28     {
29         var order = this._orders.Single(x => x.Id == orderId);
30         order.Remove();
31 
32         this.AddDomainEvent(new OrderRemovedEvent(order));
33     }
34 }
 1 public class Order : Entity
 2 {
 3     public Guid Id { get; private set; }
 4     private bool _isRemoved;
 5     private decimal _value;
 6     private List<OrderProduct> _orderProducts;
 7 
 8     private Order()
 9     {
10         this._orderProducts = new List<OrderProduct>();
11         this._isRemoved = false;
12     }
13 
14     public Order(List<OrderProduct> orderProducts, IReadOnlyCollection<Product> allProducts)
15     {
16         this.Id = Guid.NewGuid();
17         this._orderProducts = orderProducts;
18 
19         this.CalculateOrderValue(allProducts);
20     }
21 
22     internal void Change(List<OrderProduct> products, IReadOnlyCollection<Product> allProducts)
23     {
24         foreach (var product in products)
25         {
26             var orderProduct = this._orderProducts.SingleOrDefault(x => x.ProductId == product.ProductId);
27             if (orderProduct != null)
28             {
29                 orderProduct.ChangeQuantity(product.Quantity);
30             }
31             else
32             {
33                 this._orderProducts.Add(product);
34             }
35         }
36 
37         var existingProducts = this._orderProducts.ToList();
38         foreach (var existingProduct in existingProducts)
39         {
40             var product = products.SingleOrDefault(x => x.ProductId == existingProduct.ProductId);
41             if (product == null)
42             {
43                 this._orderProducts.Remove(existingProduct);
44             }
45         }
46 
47         this.CalculateOrderValue(allProducts);
48     }
49 
50     internal void Remove()
51     {
52         this._isRemoved = true;
53     }
54 
55     private void CalculateOrderValue(IReadOnlyCollection<Product> allProducts)
56     {
57         this._value = this._orderProducts.Sum(x => x.Quantity * allProducts.Single(y => y.Id == x.ProductId).Price);
58     }
59 }

架构

解决方案的架构基于知名的Onion Architecture定义,如下图:

如上图,定义了三个项目:

  • API项目包含程序端点和应用逻辑(命令和查询处理器)。
  • 领域项目和领域模型
  • 基础架构项目包含与数据库的集成

总结

在这篇文章中,尝试用最简单的方法来实现CQRS模式,使用原始sql脚本作为读模型端处理,使用DDD方法作为写模型端实现。这样就能够在不失去开发速度的情况下实现更多的关注点分离。引入这种解决方案的成本非常低,而且回报很快。

 

posted @ 2019-10-30 23:08  东方未  阅读(179)  评论(0)    收藏  举报