CQRS pattern là gì?
CQRS là viết tắt của Command and Query Responsibility Segregation, là một mẫu giúp việc phân tách các việc đọc và cập nhật dữ liệu. Triển khai CQRS giúp ứng dụng của bạn có thể tối đa performance, scalability, và security. Sự linh hoạt khi chuyển sang CQRS giúp hệ thống của bạn phát triển tốt hơn theo thời gian và ngăn sự xung đột giữa các domain level.
Đặt vấn đề
Trong các kiến trúc truyền thống, cùng một data model sẽ được query và update vào database. Điều đó hoạt động tốt với CRUD cơ bản. Trong các ứng dụng phức tạp hơn, thì cách tiếp cận này nó sẽ trở nên khó sử dụng. Ví dụ ở read side, ứng dụng sẽ thực hiện nhiều query, trả về DTO (data transfer objects) với các cấu trúc khác nhau, nên việc mapping object có thể trở nên phức tạp. Ở write side, model sẽ triển khải những validation phức tạp và các business logic. Theo thời gian, việc làm này dần sẽ trở nên phức tạp.
Khối lượng của việc đọc và ghi thường không giống nhau về performance và scale.
- Thường có sự không phù hợp giữa các biểu diễn đọc và ghi của dữ liệu, chẳng hạn như các cột hoặc thuộc tính bổ sung phải được cập nhật chính xác mặc dù chúng không bắt buộc như một phần của hoạt động.
- Tranh chấp dữ liệu có thể xảy ra khi các hoạt động được thực hiện song song trên cùng một tập hợp dữ liệu.
- Cách tiếp cận truyền thống có thể có tác động tiêu cực đến hiệu suất do tải trên kho dữ liệu và lớp truy cập dữ liệu và sự phức tạp của các truy vấn cần thiết để lấy thông tin.
- Quản lý security và permissions có thể trở nên phức tạp, bởi vì mỗi thực thể phải tuân theo cả hoạt động đọc và ghi, có thể làm lộ dữ liệu trong bối cảnh sai.
Giải pháp
CQRS phân tách các lần đọc và ghi thành các mô hình khác nhau, sử dụng commands để cập nhật dữ liệu và các truy vấn để đọc dữ liệu.
- Commands nên dựa trên nhiệm vụ, thay vì tập trung vào dữ liệu.
- Commands có thể được đặt trên một hàng đợi để xử lý không đồng bộ, thay vì được xử lý đồng bộ.
- Truy vấn không bao giờ sửa đổi cơ sở dữ liệu. Truy vấn trả về DTO không gói gọn bất kỳ domain nào.
Các model sẽ tách biệt nhau, bạn có thể tham khảo diagram sau.
Việc tách biệt query và update model giúp đơn giản hoá thiết kế và triển khai. Tuy nhiên, một nhược điểm của CQRS là code không thể động generated từ database bằng cách sử dụng scaffolding của các ORM.
Để cô lập hơn, bạn có thể tách dữ liệu đã đọc khỏi dữ liệu ghi. Trong trường hợp này, đọc dữ liệu từ database sẽ sử dụng data schema được tối ưu cho query. Ví dụ, nó có thể lưu trữ một cái nhìn trực quan của dữ liệu, để tránh các phép nối phức tạp hoặc các ánh xạ ORM phức tạp. Còn ghi dự liệu, có thể là dữ liệu có các mối quan hệ với những dữ liệu khác, trong khi đọc dữ liệu là một document database.
Nếu việc phân tách read và write database được sử dụng, chúng phải được đồng bộ. Thông thường, để thực hiện ghi một model bằng cách publish một event mỗi khi update database, update database và publish event sẽ chỉ diễn ra trên một transaction duy nhất.
Dữ liệu dùng để read và write có thể giống nhau, hoặc khác nhau hoàn toàn về cấu trúc. Phân tách việc đọc và ghi dữ liệu, cho phép mỗi phần sẽ dễ hơn trong việc mở rộng và tăng hiệu suất.
Lợi ích của CQRS mang lại:
- Independent scaling: CQRS cho phép xử lý read và write có thể scale độc lập, hạn chế lock contentions.
- Optimized data schemas: Read side có thể sử dụng schema tối ưu cho query, trong khi write side sử dụng schema tối ưu cho update.
- Separation of concerns: Phân tách read side và write side giúp cho model dễ dàng bảo trì và linh hoạt.
- Simpler queries: Ứng dụng tránh việc thực hiện các query phức tap.
Example of CQRS pattern
Đoạn code ví dụ việc triển khai CQRS, sử dụng mô hình read side và write side.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// Query interface namespace ReadModel { public interface ProductsDao { ProductDisplay FindById(int productId); ICollection<ProductDisplay> FindByName(string name); ICollection<ProductInventory> FindOutOfStockProducts(); ICollection<ProductDisplay> FindRelatedProducts(int productId); } public class ProductDisplay { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal UnitPrice { get; set; } public bool IsOutOfStock { get; set; } public double UserRating { get; set; } } public class ProductInventory { public int Id { get; set; } public string Name { get; set; } public int CurrentStock { get; set; } } } |
RateProduct
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public interface ICommand { Guid Id { get; } } public class RateProduct : ICommand { public RateProduct() { this.Id = Guid.NewGuid(); } public Guid Id { get; set; } public int ProductId { get; set; } public int Rating { get; set; } public int UserId {get; set; } } |
ProductsCommandHandler
để xử lý lệnh gửi bởi ứng dụng. Thông thường client sẽ gửi lệnh vào queue. Các CommandHandler
sẽ tiến hành xử lý các lệnh đó.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public class ProductsCommandHandler : ICommandHandler<AddNewProduct>, ICommandHandler<RateProduct>, ICommandHandler<AddToInventory>, ICommandHandler<ConfirmItemShipped>, ICommandHandler<UpdateStockFromInventoryRecount> { private readonly IRepository<Product> repository; public ProductsCommandHandler (IRepository<Product> repository) { this.repository = repository; } void Handle (AddNewProduct command) { ... } void Handle (RateProduct command) { var product = repository.Find(command.ProductId); if (product != null) { product.RateProduct(command.UserId, command.Rating); repository.Save(product); } } void Handle (AddToInventory command) { ... } void Handle (ConfirmItemsShipped command) { ... } void Handle (UpdateStockFromInventoryRecount command) { ... } } |