
CQRS Là Gì?
CQRS (Command Query Responsibility Segregation) là một design pattern tách biệt các thao tác đọc (Queries) khỏi các thao tác ghi (Commands) thành các model riêng biệt. Thay vì dùng một model cho cả đọc và ghi dữ liệu, CQRS chia ứng dụng thành hai phần riêng biệt.
Nguyên Tắc Cốt Lõi:
- **Commands**: Thay đổi trạng thái của ứng dụng (Tạo, Sửa, Xóa)
- **Queries**: Lấy dữ liệu mà không thay đổi gì (Thao tác đọc)
Nghĩ về nó như một nhà hàng:
- **Command side** = Bếp (chuẩn bị món ăn, thay đổi kho)
- **Query side** = Menu hiển thị (hiển thị món có sẵn, không thay đổi gì)
Tại Sao Cần CQRS?
Vấn Đề Với Kiến Trúc Truyền Thống
Trong ứng dụng truyền thống, chúng ta dùng cùng một model cho cả đọc và ghi:
// Cách tiếp cận truyền thống - cùng model cho mọi thứ
public class ProductService
{
public void CreateProduct(Product product)
{
// Thao tác ghi
_dbContext.Products.Add(product);
_dbContext.SaveChanges();
}
public Product GetProduct(int id)
{
// Thao tác đọc - cùng model
return _dbContext.Products
.Include(p => p.Category)
.Include(p => p.Reviews)
.FirstOrDefault(p => p.Id == id);
}
}
Các Vấn Đề:
1. Vấn đề hiệu suất - Join phức tạp làm chậm việc đọc
2. Khó scale - Không thể scale đọc và ghi riêng biệt
3. Xung đột khóa - Nhiều thao tác trên cùng dữ liệu gây conflict
4. Query phức tạp - Một model phục vụ nhiều mục đích trở nên phức tạp
Kiến Trúc CQRS
Kiến Trúc Cơ Bản
┌─────────────┐
│ Client │
└──────┬──────┘
│
├─────────► Commands (Ghi)
│ ├─ TạoĐơnHàng
│ ├─ CậpNhậtSảnPhẩm
│ └─ XóaNgườiDùng
│
└─────────► Queries (Đọc)
├─ LấyĐơnHàngTheoId
├─ TìmKiếmSảnPhẩm
└─ LấyThôngTinUser
Hai Cách Tiếp Cận Chính
1. Tách Biệt Logic (Cùng Database)
┌──────────────────────────────────┐
│ Cùng Database │
│ ┌────────────┐ ┌────────────┐ │
│ │ Commands │ │ Queries │ │
│ │ Logic │ │ Logic │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────┘
2. Tách Biệt Vật Lý (Database Khác Nhau)
┌─────────────┐ ┌─────────────┐
│ Database │ Events │ Database │
│ Command │ ───────────────►│ Query │
│ (Ghi) │ │ (Đọc) │
└─────────────┘ └─────────────┘
Ví Dụ Thực Tế: Hệ Thống Đơn Hàng E-Commerce
Cùng xây dựng một ví dụ đầy đủ sử dụng C# và MediatR.
Bước 1: Cài Đặt Packages Cần Thiết
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Bước 2: Định Nghĩa Commands
Commands đại diện cho các hành động thay đổi trạng thái.
using MediatR;
// Command để tạo đơn hàng
public class CreateOrderCommand : IRequest<CreateOrderResult>
{
public string CustomerId { get; set; }
public List<OrderItem> Items { get; set; }
public string ShippingAddress { get; set; }
}
public class OrderItem
{
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
public class CreateOrderResult
{
public bool Success { get; set; }
public int OrderId { get; set; }
public string Message { get; set; }
}
Bước 3: Tạo Command Handler
Handler chứa business logic cho command.
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, CreateOrderResult>
{
private readonly IOrderRepository _orderRepository;
private readonly IInventoryService _inventoryService;
private readonly IEventPublisher _eventPublisher;
public CreateOrderCommandHandler(
IOrderRepository orderRepository,
IInventoryService inventoryService,
IEventPublisher eventPublisher)
{
_orderRepository = orderRepository;
_inventoryService = inventoryService;
_eventPublisher = eventPublisher;
}
public async Task<CreateOrderResult> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
try
{
// 1. Kiểm tra hàng tồn kho
foreach (var item in request.Items)
{
var available = await _inventoryService
.CheckAvailability(item.ProductId, item.Quantity);
if (!available)
{
return new CreateOrderResult
{
Success = false,
Message = $"Sản phẩm {item.ProductId} đã hết hàng"
};
}
}
// 2. Tính tổng tiền
var total = request.Items.Sum(i => i.Price * i.Quantity);
// 3. Tạo entity đơn hàng
var order = new Order
{
CustomerId = request.CustomerId,
OrderDate = DateTime.UtcNow,
TotalAmount = total,
ShippingAddress = request.ShippingAddress,
Status = OrderStatus.Pending,
Items = request.Items
};
// 4. Lưu vào database
await _orderRepository.CreateAsync(order);
// 5. Publish event để cập nhật query side
await _eventPublisher.PublishAsync(new OrderCreatedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
TotalAmount = order.TotalAmount,
CreatedAt = order.OrderDate
});
return new CreateOrderResult
{
Success = true,
OrderId = order.Id,
Message = "Tạo đơn hàng thành công"
};
}
catch (Exception ex)
{
// Log lỗi
return new CreateOrderResult
{
Success = false,
Message = $"Lỗi khi tạo đơn hàng: {ex.Message}"
};
}
}
}
Bước 4: Định Nghĩa Queries
Queries lấy dữ liệu mà không thay đổi gì.
// Query để lấy chi tiết đơn hàng
public class GetOrderByIdQuery : IRequest<OrderDetailsDto>
{
public int OrderId { get; set; }
}
// DTO cho kết quả query - tối ưu cho hiển thị
public class OrderDetailsDto
{
public int OrderId { get; set; }
public string CustomerName { get; set; }
public string CustomerEmail { get; set; }
public DateTime OrderDate { get; set; }
public string Status { get; set; }
public decimal TotalAmount { get; set; }
public string ShippingAddress { get; set; }
public List<OrderItemDto> Items { get; set; }
}
public class OrderItemDto
{
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal Subtotal { get; set; }
}
Bước 5: Tạo Query Handler
Query handlers lấy dữ liệu hiệu quả mà không có business logic.
public class GetOrderByIdQueryHandler
: IRequestHandler<GetOrderByIdQuery, OrderDetailsDto>
{
private readonly IReadOnlyDbContext _readDbContext;
// Hoặc dùng Dapper cho raw SQL queries
public GetOrderByIdQueryHandler(IReadOnlyDbContext readDbContext)
{
_readDbContext = readDbContext;
}
public async Task<OrderDetailsDto> Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
// Câu SQL trực tiếp để hiệu suất tốt hơn
var query = @"
SELECT
o.OrderId,
o.OrderDate,
o.Status,
o.TotalAmount,
o.ShippingAddress,
c.Name as CustomerName,
c.Email as CustomerEmail,
oi.ProductName,
oi.Quantity,
oi.Price,
(oi.Quantity * oi.Price) as Subtotal
FROM Orders o
INNER JOIN Customers c ON o.CustomerId = c.CustomerId
INNER JOIN OrderItems oi ON o.OrderId = oi.OrderId
WHERE o.OrderId = @OrderId";
// Dùng Dapper để lấy dữ liệu hiệu quả
using var connection = _readDbContext.CreateConnection();
var orderDictionary = new Dictionary<int, OrderDetailsDto>();
var result = await connection.QueryAsync<OrderDetailsDto, OrderItemDto, OrderDetailsDto>(
query,
(order, item) =>
{
if (!orderDictionary.TryGetValue(order.OrderId, out var orderEntry))
{
orderEntry = order;
orderEntry.Items = new List<OrderItemDto>();
orderDictionary.Add(order.OrderId, orderEntry);
}
orderEntry.Items.Add(item);
return orderEntry;
},
new { OrderId = request.OrderId },
splitOn: "ProductName"
);
return orderDictionary.Values.FirstOrDefault();
}
}
Bước 6: Setup Trong API Controller
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
// Command endpoint - Thao tác ghi
[HttpPost]
public async Task<IActionResult> CreateOrder(
[FromBody] CreateOrderCommand command)
{
var result = await _mediator.Send(command);
if (result.Success)
{
return Ok(result);
}
return BadRequest(result.Message);
}
// Query endpoint - Thao tác đọc
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{
var query = new GetOrderByIdQuery { OrderId = id };
var result = await _mediator.Send(query);
if (result == null)
{
return NotFound();
}
return Ok(result);
}
}
Bước 7: Cấu Hình Services
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Thêm MediatR
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Startup).Assembly));
// Thêm repositories
services.AddScoped<IOrderRepository, OrderRepository>();
// Thêm database contexts
services.AddDbContext<WriteDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("WriteDb")));
services.AddDbContext<ReadDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("ReadDb")));
// Thêm các services khác
services.AddScoped<IInventoryService, InventoryService>();
services.AddScoped<IEventPublisher, EventPublisher>();
}
}
Đồng Bộ Read và Write Models
Sử Dụng Event-Driven Communication
// Định nghĩa event
public class OrderCreatedEvent
{
public int OrderId { get; set; }
public string CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public DateTime CreatedAt { get; set; }
}
// Event handler để cập nhật read model
public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
private readonly IReadModelRepository _readModelRepository;
public OrderCreatedEventHandler(IReadModelRepository readModelRepository)
{
_readModelRepository = readModelRepository;
}
public async Task Handle(
OrderCreatedEvent notification,
CancellationToken cancellationToken)
{
// Cập nhật read model (denormalized view)
var orderView = new OrderReadModel
{
OrderId = notification.OrderId,
CustomerId = notification.CustomerId,
TotalAmount = notification.TotalAmount,
CreatedAt = notification.CreatedAt,
LastUpdated = DateTime.UtcNow
};
await _readModelRepository.UpsertAsync(orderView);
}
}
Các Use Case Thực Tế
1. Nền Tảng E-Commerce
Command Side:
- Xử lý đơn hàng
- Cập nhật kho
- Xử lý thanh toán
Query Side:
- Hiển thị catalog sản phẩm
- Hiển thị lịch sử đơn hàng
- Tạo báo cáo
2. Hệ Thống Ngân Hàng
Command Side:
- Chuyển tiền
- Tạo tài khoản
- Xử lý giao dịch
Query Side:
- Xem số dư tài khoản
- Hiển thị lịch sử giao dịch
- Tạo sao kê
3. Nền Tảng Mạng Xã Hội
Command Side:
- Tạo bài viết
- Thêm comment
- Like/share nội dung
Query Side:
- Hiển thị news feed
- Tìm kiếm bài viết
- Hiển thị profile người dùng
Best Practices
✅ Nên Làm
1. Giữ Commands Đơn Giản
// Tốt - Ý định rõ ràng
public class ActivateUserCommand : IRequest<bool>
{
public int UserId { get; set; }
}
// Tránh - Quá chung chung
public class UpdateUserCommand : IRequest<bool>
{
public int UserId { get; set; }
public Dictionary<string, object> Changes { get; set; }
}
2. Tối Ưu Query Models
// Tốt - Denormalized để đọc nhanh
public class ProductListDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string CategoryName { get; set; } // Denormalized
public int ReviewCount { get; set; } // Pre-calculated
public double AverageRating { get; set; } // Pre-calculated
}
3. Dùng Async Communication
// Tốt - Non-blocking
public async Task<Result> Handle(
UpdateInventoryCommand request,
CancellationToken cancellationToken)
{
await _repository.UpdateAsync(request);
// Publish event bất đồng bộ
await _eventBus.PublishAsync(new InventoryUpdatedEvent
{
ProductId = request.ProductId,
NewQuantity = request.Quantity
});
return Result.Success();
}
❌ Không Nên Làm
1. Không Trộn Lẫn Responsibilities
// Không tốt - Query không nên sửa dữ liệu
public class GetUserQuery : IRequest<UserDto>
{
public int UserId { get; set; }
public bool UpdateLastAccess { get; set; } // Sai!
}
2. Không Over-Normalize Read Models
// Không tốt - Cần quá nhiều joins
public class OrderDto
{
public int OrderId { get; set; }
public int CustomerId { get; set; } // Cần join
public int ProductId { get; set; } // Cần join
// Nên include denormalized data thay vì
}
3. Không Dùng CQRS Cho CRUD Đơn Giản
// Quá phức tạp cho thao tác đơn giản
public class GetUserByIdQuery : IRequest<User>
{
public int Id { get; set; }
}
// Chỉ cần dùng:
var user = await _dbContext.Users.FindAsync(id);
Khi Nào Nên Dùng CQRS
✅ Phù Hợp
1. Domain Logic Phức Tạp
- Nhiều business rules
- Validation khác nhau cho đọc và ghi
2. Tỷ Lệ Đọc/Ghi Cao
- Đọc nhiều hơn ghi rất nhiều
- Cần scale độc lập
3. Performance Quan Trọng
- Cần queries tối ưu
- Yêu cầu scaling khác nhau
4. Hệ Thống Cộng Tác
- Nhiều users làm việc trên cùng dữ liệu
- Cần ngăn conflicts
❌ Không Phù Hợp
1. Ứng Dụng CRUD Đơn Giản
- Chỉ cơ bản create/read/update/delete
- Không có business logic phức tạp
2. Ứng Dụng Traffic Thấp
- User base nhỏ
- Không cần scaling
3. Yêu Cầu Consistency Nghiêm Ngặt
- Giao dịch ngân hàng (một số trường hợp)
- Hệ thống real-time quan trọng
Các Lỗi Thường Gặp Và Giải Pháp
Lỗi 1: Eventual Consistency Issues
Vấn Đề:
// User tạo đơn hàng
await _mediator.Send(new CreateOrderCommand { ... });
// Ngay lập tức đọc nó - có thể chưa có!
var order = await _mediator.Send(new GetOrderByIdQuery { OrderId = newId });
// order có thể null do replication lag
Giải Pháp:
// Trả về data đã tạo trực tiếp từ command
public class CreateOrderResult
{
public bool Success { get; set; }
public int OrderId { get; set; }
public OrderDetailsDto Order { get; set; } // Include chi tiết đầy đủ
}
// Hoặc dùng correlation IDs để track sync status
public class CreateOrderCommand : IRequest<CreateOrderResult>
{
public Guid CorrelationId { get; set; } = Guid.NewGuid();
}
Lỗi 2: Over-Engineering
Vấn Đề: Dùng CQRS cho mọi thứ, kể cả thao tác đơn giản.
Giải Pháp: Chỉ dùng CQRS khi nó mang lại giá trị:
// Thao tác đơn giản - bỏ qua CQRS
public class UserService
{
public async Task<User> GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
}
// Thao tác phức tạp - dùng CQRS
public class GetUserDashboardQuery : IRequest<DashboardDto>
{
public int UserId { get; set; }
}
Lỗi 3: Bỏ Qua Error Handling
Vấn Đề: Không xử lý lỗi đồng bộ.
Giải Pháp:
public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
private readonly ILogger<OrderCreatedEventHandler> _logger;
private readonly IReadModelRepository _repository;
public async Task Handle(OrderCreatedEvent notification, CancellationToken ct)
{
try
{
await _repository.UpsertAsync(notification);
}
catch (Exception ex)
{
_logger.LogError(ex, "Không cập nhật được read model cho đơn {OrderId}",
notification.OrderId);
// Implement retry logic hoặc dead letter queue
throw; // Re-throw để trigger retry mechanism
}
}
}
Lợi Ích Và Thách Thức
Lợi Ích
✅ Scale Độc Lập - Scale đọc và ghi riêng biệt
✅ Hiệu Suất Tối Ưu - Models khác nhau cho nhu cầu khác nhau
✅ Tách Biệt Rõ Ràng - Dễ hiểu và maintain hơn
✅ Data Models Linh Hoạt - Dùng database tốt nhất cho mỗi side
✅ Security Tốt Hơn - Kiểm soát access vào write operations
Thách Thức
❌ Tăng Độ Phức Tạp - Nhiều code và infrastructure hơn
❌ Eventual Consistency - Dữ liệu có thể tạm thời out of sync
❌ Learning Curve - Team cần hiểu pattern
❌ Nhiều Infrastructure Hơn - Có thể cần message queues, multiple databases
Tổng Kết
CQRS là pattern mạnh mẽ để xây dựng ứng dụng scalable, high-performance. Các điểm chính:
1. Tách đọc khỏi ghi để tối ưu tốt hơn
2. Dùng commands cho thay đổi state với business logic
3. Dùng queries cho lấy dữ liệu không có side effects
4. Áp dụng chọn lọc - không phải app nào cũng cần CQRS
5. Xử lý eventual consistency với UI/UX design phù hợp
6. Bắt đầu đơn giản với logical separation trước khi physical separation
Nhớ rằng: CQRS là công cụ, không phải giải pháp vạn năng. Chỉ dùng khi lợi ích vượt trội độ phức tạp thêm vào.
Tài Nguyên Thêm
- Microsoft Docs: CQRS Pattern
- Greg Young: CQRS Documents
- Martin Fowler: CQRS Pattern
- MediatR Documentation