Bài viết này tiếp nối phần trước, trong đó đã thực hiện phân bảng theo thời gian. Ở đây, chúng ta sẽ triển khai phân bảng dựa trên số đuôi của khóa chính — ví dụ như 00, 01, 02. Ngoài ra, bài viết cũng bổ sung kiểm thử việc migration dữ liệu.
Trong mô hình dữ liệu, ngoài entity Order từ bài trước, ta thêm mới entity Product. Một đơn hàng có thể chứa nhiều sản phẩm và một sản phẩm có thể xuất hiện trong nhiều đơn hàng — tạo thành quan hệ nhiều-nhiều. Tuy nhiên, khi áp dụng sharding (phân mảnh cơ sở dữ liệu), mối quan hệ này không thể được duy trì thông qua navigation property truyền thống như:
public ICollection<OrderProduct> OrderProducts { get; set; }
Do đó, ta sử dụng một bảng trung gian OrderProduct để xử lý mối quan hệ này.
Định nghĩa các entity
public class Order
{
[Key]
public string Id { get; set; }
public string Payer { get; set; }
public long Amount { get; set; }
public string Region { get; set; }
public OrderStatus Status { get; set; }
public string CustomerId { get; set; }
public DateTime CreatedAt { get; set; }
}
public enum OrderStatus
{
Pending = 1,
Processing = 2,
Completed = 3,
Failed = 4
}
public class OrderItem
{
[Key]
public string Id { get; set; }
public string ProductId { get; set; }
public string OrderId { get; set; }
}
public class Product
{
[Key]
public string Id { get; set; }
public string Title { get; set; }
}
Cấu hình phân bảng theo số đuôi
Với bảng OrderItem và Product, ta sử dụng chiến lược phân bảng dựa trên modulo của số đuôi chuỗi ID. Ví dụ: lấy 2 ký tự cuối cùng của Id và chia cho 3 để xác định bảng đích.
public class OrderItemShardingRoute : AbstractSimpleShardingModKeyStringVirtualTableRoute<OrderItem>
{
public OrderItemShardingRoute() : base(tailLength: 2, tableCount: 3) { }
public override void Configure(EntityMetadataTableBuilder<OrderItem> builder)
{
builder.ShardingProperty(x => x.Id);
builder.AutoCreateTable(create: null);
builder.TableSeparator("_");
}
}
public class ProductShardingRoute : AbstractSimpleShardingModKeyStringVirtualTableRoute<Product>
{
public ProductShardingRoute() : base(tailLength: 2, tableCount: 3) { }
public override void Configure(EntityMetadataTableBuilder<Product> builder)
{
builder.ShardingProperty(x => x.Id);
builder.AutoCreateTable(create: null);
builder.TableSeparator("_");
}
}
Phân bảng theo tháng cho Order
Bảng Order vẫn giữ nguyên logic phân theo tháng như bài trước:
public class OrderShardingRoute : AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>
{
public override DateTime GetBeginTime() => new DateTime(2023, 5, 1);
public override void Configure(EntityMetadataTableBuilder<Order> builder)
{
builder.ShardingProperty(x => x.CreatedAt);
}
public override bool AutoCreateTableByTime() => true;
}
Cấu hình DI và ShardingCore
builder.Services.AddShardingDbContext<AppDbContext>()
.UseRouteConfig(options =>
{
options.AddShardingTableRoute<OrderItemShardingRoute>();
options.AddShardingTableRoute<OrderShardingRoute>();
options.AddShardingTableRoute<ProductShardingRoute>();
})
.UseConfig(options =>
{
var loggerFactory = LoggerFactory.Create(cfg => cfg.AddConsole());
options.UseShardingMigrationConfigure(migrationBuilder =>
{
migrationBuilder.ReplaceService<IMigrationsSqlGenerator, ShardingMySqlMigrationsSqlGenerator>();
});
options.ThrowIfQueryRouteNotMatch = false;
options.UseShardingQuery((connectionString, dbContextOptions) =>
{
dbContextOptions.UseMySql(connectionString, MySqlServerVersion.AutoDetect(connectionString))
.UseLoggerFactory(loggerFactory);
});
options.UseShardingTransaction((dbConnection, dbContextOptions) =>
{
dbContextOptions.UseMySql(dbConnection, MySqlServerVersion.AutoDetect(dbConnection.ConnectionString))
.UseLoggerFactory(loggerFactory);
});
options.AddDefaultDataSource("main", "Data Source=192.168.0.192;Initial Catalog=test;uid=root;pwd=123456;");
options.AddReadWriteSeparation(_ => new Dictionary<string, IEnumerable<string>>
{
["main"] = new[] { "Data Source=192.168.0.192;Initial Catalog=test;uid=root;pwd=123456;" }
}, ReadStrategyEnum.Loop, defaultEnable: true);
})
.AddShardingCore();
Khởi tạo và migration
using var scope = app.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
context.Database.Migrate();
// Tự động tạo bảng nếu thiếu
app.Services.UseAutoTryCompensateTable();
// Thêm dữ liệu mẫu (nếu cần)
app.SeedInitialData();