軟體架構
關於CQRS
最近筆者在研究CQRS Pattern,在整潔架構或是DDD都會看到CQRS部分…
在過往筆者都是三層式架構開發,都會透過Repository Pattern來做資料異動和查詢,那都是會用的是相同實體,在一般業務系統當中,問題不大,隨業務需求增長,系統可能會逐步浮現性能問題,在DB當中可以做到讀寫分離,但在系統開發來說,讀寫方面混合在一起,會浮現出問題。
在CQRS當中,系統分為兩大部分,Command和Query的設計思維。
- 在寫資料庫的部分叫做Command:改變某一個物件或整個系統狀態。
- 讀取資料庫的叫做Query:回傳值但不會改變物件的狀態。
在實際操作中很容易把這兩個區分出來。
主要是可以讓命令和查詢分開處理,讓系統獨立優化讀取和寫,對於血的部分,可以引入Event Sourcing和複雜業務邏輯來保持資料一致性與完整性。
讀取部分可以透過設計出高效能查詢,同時也可以使用改善效能的技術,例如快取,提高系統性能和快速回應。
CQRS實戰場景
不是每個系統都能用CQRS,稍微評估需求,資料庫一致性,通常是用場景會是讀寫操作非常頻繁時,效能要求高的分散式架構,需要有職責釐清提高安全性和可擴展性,CQRS就能派上用場,同時CQRS也可以魂和其他模式和技術一起使用,例如Event Sourcing、DDD、微服務架構,都跟CQRS連結再一起,可以進一步提升系統效能、伸縮性,可維護性。
CQRS與三層架構差別
三層架構是放在同一個層次處理讀取和寫入,CQRS這是分開來,這樣好處可以優化讀取和寫入的操作,提升效能和擴展性。
單一資料庫CQRS設計
單一資料庫的CQRS,是在Command中做執行使用案例,修改實體狀態,透過ORM框架將實體保存到資料庫,例如:Entity Framework。
雙資料庫CQRS
兩個資料庫一個用讀取一個用寫,命令部分針對寫Insert Update Delete做優化資料庫,Query針對Query的DB操作,並進行同步。
Event Sourcing CQRS
透過Event Sourcing來進行,實體發生的狀態,做為快照來進行儲存,透過事件的時間來保存儲存,這一款是比較複雜的CQRS。
個人看完CQRS,它要求的技術門檻其實還滿高的….
CQRS實做初體驗
EF Core指令轉移這邊就省略…開始進行CQRS初體驗!
在Model建立
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
在專案當中建置Data資料夾並建立LocalDBContext
public partial class LocalDBContext:DbContext
{
public LocalDBContext(DbContextOptions<LocalDBContext> options) : base(options)
{
}
public DbSet<Product> Products { get; set; }
}
分別建立Features和底下的Command和Queries和Repository資料夾
public interface IProductRepository
{
Task<IEnumerable<Product>> GetProducts();
Task<Product> GetById(int id);
Task<Product> CreateProduct(Product product);
Task<int> UpdateProduct(Product player);
Task<int> DeleteProduct(Product player);
}
public class ProductRepository:IProductRepository
{
private readonly LocalDBContext _context;
public ProductRepository(LocalDBContext context)
{
_context = context;
}
public async Task<IEnumerable<Product>> GetProducts()
{
return await _context.Product.ToListAsync();
}
public async Task<Product> GetById(int id)
{
return await _context.Product.FirstOrDefaultAsync(x => x.Id == id);
}
public async Task<Product> CreateProduct(Product product)
{
_context.Product.Add(product);
await _context.SaveChangesAsync();
return product;
}
public async Task<int> UpdateProduct(Product product)
{
_context.Product.Update(product);
return await _context.SaveChangesAsync();
}
public async Task<int> DeleteProduct(Product product)
{
_context.Product.Remove(product);
return await _context.SaveChangesAsync();
}
}
先安裝 MediatR並在Command分別三支程式
public class CreateProductCommand: IRequest<Product>
{
public string Name { get; set; }
public string Description { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Product>
{
private readonly IProductRepository _productRepository;
public CreateProductCommandHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<Product> Handle(CreateProductCommand command, CancellationToken cancellationToken)
{
var product = new Product()
{
Name = command.Name,
Description = command.Description,
Quantity = command.Quantity,
Price = command.Price
};
return await _productRepository.CreateProduct(product);
}
}
}
public class DeleteProductCommand: IRequest<int>
{
public int Id { get; set; }
public class DeleteProductCommandHandler : IRequestHandler<DeleteProductCommand, int>
{
private readonly IProductRepository _productRepository;
public DeleteProductCommandHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<int> Handle(DeleteProductCommand command, CancellationToken cancellationToken)
{
var product = await _productRepository.GetById(command.Id);
if (product == null)
{
return default;
}
return await _productRepository.DeleteProduct(product);
}
}
}
public class UpdateProductCommand: IRequest<int>
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public class UpdateProductCommandHandler : IRequestHandler<UpdateProductCommand, int>
{
private readonly IProductRepository _productRepository;
public UpdateProductCommandHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<int> Handle(UpdateProductCommand command, CancellationToken cancellationToken)
{
var product = await _productRepository.GetById(command.Id);
if (product == null)
return default;
product.Name = command.Name;
product.Description = command.Description;
product.Quantity = command.Quantity;
product.Price = command.Price;
return await _productRepository.UpdateProduct(product);
}
}
}
在Queries分別新增GetAllProductQuery和GetProductByIdQuery
public class GetAllProductQuery: IRequest<IEnumerable<Product>>
{
public class GetAllPlayersQueryHandler : IRequestHandler<GetAllProductQuery, IEnumerable<Product>>
{
private readonly IProductRepository _productRepository;
public GetAllPlayersQueryHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<IEnumerable<Product>> Handle(GetAllProductQuery query, CancellationToken cancellationToken)
{
return await _productRepository.GetProducts();
}
}
}
public class GetProductByIdQuery:IRequest<Product>
{
public int Id { get; set; }
public class GetPlayerByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Product>
{
private readonly IProductRepository _productRepository;
public GetPlayerByIdQueryHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<Product> Handle(GetProductByIdQuery query, CancellationToken cancellationToken)
{
return await _productRepository.GetById(query.Id);
}
}
}
最後新增一個ProductController
public class ProductController : Controller
{
private readonly IMediator _mediator;
public ProductController(IMediator mediator)
{
_mediator = mediator;
}
public async Task <IActionResult> Index()
{
return View(await _mediator.Send(new GetAllProductQuery()));
}
public async Task<IActionResult> Detail(int id)
{
return View(await _mediator.Send(new GetProductByIdQuery() { Id = id }));
}
public async Task<IActionResult> Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreateProductCommand command)
{
try
{
if (ModelState.IsValid)
{
await _mediator.Send(command);
return RedirectToAction(nameof(Index));
}
}
catch (Exception ex)
{
ModelState.AddModelError("", "Unable to save changes.");
}
return View(command);
}
public async Task<IActionResult> Edit(int id)
{
return View(await _mediator.Send(new GetProductByIdQuery() { Id = id }));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, UpdateProductCommand command)
{
if (id != command.Id)
{
return BadRequest();
}
try
{
if (ModelState.IsValid)
{
await _mediator.Send(command);
return RedirectToAction(nameof(Index));
}
}
catch (Exception ex)
{
ModelState.AddModelError("", "Unable to save changes.");
}
return View(command);
}
[HttpGet]
public async Task<IActionResult> Delete(int id)
{
try
{
await _mediator.Send(new DeleteProductCommand() { Id = id });
}
catch (Exception ex)
{
ModelState.AddModelError("", "Unable to delete. ");
}
return RedirectToAction(nameof(Index));
}
}
分別加入檢視
@model CQRSMVCProductDemo.Models.Product
@{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>Create Player</h1>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create" method="post">
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<input asp-for="Description" class="form-control" />
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Quantity" class="control-label"></label>
<input asp-for="Quantity" class="form-control" />
<span asp-validation-for="Quantity" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>
</form>
</div>
</div>
@model CQRSMVCProductDemo.Models.Product
@{
ViewData["Title"] = "Details";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>@Model.Name</h1>
<hr />
<div>
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Id)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Id)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Description)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Description)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Quantity)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Quantity)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Price)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Price)
</dd>
</dl>
</div>
<div>
@Html.ActionLink("Edit", "Edit", new { id = Model.Id }, new { @class = "btn btn-primary" })
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>
@model CQRSMVCProductDemo.Models.Product
@{
ViewData["Title"] = "Edit";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>Edit Player</h1>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit" method="post">
<input asp-for="Id" class="form-control" type="hidden" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<input asp-for="Description" class="form-control" />
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Quantity" class="control-label"></label>
<input asp-for="Quantity" class="form-control" />
<span asp-validation-for="Quantity" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>
</form>
</div>
</div>
@model IEnumerable<CQRSMVCProductDemo.Models.Product>
@{
ViewData["Title"] = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
<div class="col">
<h1>Players</h1>
</div>
<div class="col text-right">
<a asp-action="Create" class="btn btn-success">Create New</a>
</div>
</div>
<br />
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Id)
</th>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Description)
</th>
<th>
@Html.DisplayNameFor(model => model.Quantity)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Id)
</td>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</td>
<td>
@Html.DisplayFor(modelItem => item.Quantity)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id = item.Id }, new { @class = "btn btn-primary" })
@Html.ActionLink("Details", "Detail", new { id = item.Id }, new { @class = "btn btn-secondary" })
@Html.ActionLink("Delete", "Delete", new { id = item.Id }, new { @class = "btn btn-danger" })
</td>
</tr>
}
</tbody>
</table>
//注入連線字串與Repository
string connString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<LocalDBContext>(options => options.UseSqlServer(connString));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
後續筆者實際開發的時候,搭配CQRS結合Clean Architecture,做到符合SRP的原則之後,有發現到開發速度上和維護上真的一致性,因為過往在開發階層架構,還沒辦法拆到這麼細,因為階層架構的領域邏輯部分,還是會把CRUD擠在一塊,但採用Clean Architecture+CQRS之後,發現閱讀程式碼和維護與開發上,一致性的效益浮現出來,但是初期開發這個架構是很龜毛…會依據領域邏輯的使用案例來做開發。
日後我們再談談關於Clean Architecture部分。
元哥的筆記