項目使用ABP框架,最近有需求數據量會持續變大,需要分表存儲。 發現ShardinfCore可以快速實現EF分表操作,並且作者@薛家明還特別為ABP集成寫了教程,完美的選擇。 ShardinfCore作者教程很齊全,這次以ABP 8.*的用戶視角進行集成記錄,希望幫到需要的人。 開發環境: ABP ...
項目使用ABP框架,最近有需求數據量會持續變大,需要分表存儲。
發現ShardinfCore可以快速實現EF分表操作,並且作者@薛家明還特別為ABP集成寫了教程,完美的選擇。
ShardinfCore作者教程很齊全,這次以ABP 8.*的用戶視角進行集成記錄,希望幫到需要的人。
開發環境:
ABP VNext 8.1.5 + EF 8.0.4 + ShardinfCore 7.8.1.21 + Mysql 8.2.0
新同學註意區分ABP和ABP VNext,本文用的是這個:ABP.IO - Modern ASP.NET Core Web Application Platform | ABP.IO
參考資料:
Abp VNext分表分庫,拒絕手動,我們要happy coding - 薛家明 - 博客園 (cnblogs.com)
.Net下極限生產力之efcore分表分庫全自動化遷移CodeFirst - 薛家明 - 博客園 (cnblogs.com)
ABP EF CORE 7 集成ShardingCore實現分表 - cnblogsName - 博客園
集成操作
添加依賴包
// 只需要在YouProjectName.EntityFrameworkCore模塊中安裝依賴ShardinfCore 7.8.1.21 dotnet add package ShardingCore --version 7.8.1.21
定義分表類型介面
ABP使用Guid作為主鍵,但ShardinfCore分表鍵不能接受空值,需要做一些預處理,會在下麵AbstractShardingAbpDbContext的CheckAndSetShardingKeyThatSupportAutoCreate方法中編寫邏輯,可以根據分表類型反射屬性進行預處理。
在YouProjectName.Domain.Shared模塊中,添加2個Interface:
IShardingKeyIsCreationTime:根據創建時間分表
IShardingKeyIsGuId:根據ID分表
需要分表的Entity,按需繼承類型介面
/// <summary> /// 分表鍵是Guid類型,用於分表時繼承使用 /// </summary> public interface IShardingKeyIsGuId { } /// <summary> /// 分表鍵是CreationTime類型,用於分表時繼承使用 /// </summary> public interface IShardingKeyIsCreationTime { }
封裝抽象類集成IShardingDbContext介面實現分表能力
在YouProjectName.EntityFrameworkCore模塊中,添加一個抽象類,用於繼承AbpDbContext和IShardingDbContext
using Microsoft.EntityFrameworkCore; using ShardingCore.Core.VirtualRoutes.TableRoutes.RouteTails.Abstractions; using ShardingCore.Extensions; using ShardingCore.Sharding.Abstractions; using System; using System.ComponentModel.DataAnnotations.Schema; using System.Threading.Tasks; using Volo.Abp.Domain.Entities; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Reflection; namespace YourProjectName.EntityFrameworkCore; /// <summary> /// 繼承sharding-core介面 /// 封裝實現抽象類 /// </summary> /// <typeparam name="TDbContext"></typeparam> public abstract class AbstractShardingAbpDbContext<TDbContext> : AbpDbContext<TDbContext>, IShardingDbContext where TDbContext : DbContext { private bool _createExecutor = false; protected AbstractShardingAbpDbContext(DbContextOptions<TDbContext> options) : base(options) { } private IShardingDbContextExecutor _shardingDbContextExecutor; public IShardingDbContextExecutor GetShardingExecutor() { if (!_createExecutor) { _shardingDbContextExecutor = this.DoCreateShardingDbContextExecutor(); _createExecutor = true; } return _shardingDbContextExecutor; } private IShardingDbContextExecutor DoCreateShardingDbContextExecutor() { var shardingDbContextExecutor = this.CreateShardingDbContextExecutor(); if (shardingDbContextExecutor != null) { shardingDbContextExecutor.EntityCreateDbContextBefore += (sender, args) => { CheckAndSetShardingKeyThatSupportAutoCreate(args.Entity); }; shardingDbContextExecutor.CreateDbContextAfter += (sender, args) => { var dbContext = args.DbContext; if (dbContext is AbpDbContext<TDbContext> abpDbContext && abpDbContext.LazyServiceProvider == null) { abpDbContext.LazyServiceProvider = this.LazyServiceProvider; if (dbContext is IAbpEfCoreDbContext abpEfCoreDbContext && this.UnitOfWorkManager.Current != null) { abpEfCoreDbContext.Initialize( new AbpEfCoreDbContextInitializationContext( this.UnitOfWorkManager.Current ) ); } } }; } return shardingDbContextExecutor; } private void CheckAndSetShardingKeyThatSupportAutoCreate<TEntity>(TEntity entity) where TEntity : class { if (entity is IShardingKeyIsGuId) { if (entity is IEntity<Guid> guidEntity) { if (guidEntity.Id != default) { return; } var idProperty = entity.GetObjectProperty(nameof(IEntity<Guid>.Id)); var dbGeneratedAttr = ReflectionHelper .GetSingleAttributeOrDefault<DatabaseGeneratedAttribute>( idProperty ); if (dbGeneratedAttr != null && dbGeneratedAttr.DatabaseGeneratedOption != DatabaseGeneratedOption.None) { return; } EntityHelper.TrySetId( guidEntity, () => GuidGenerator.Create(), true ); } } else if (entity is IShardingKeyIsCreationTime) { AuditPropertySetter?.SetCreationProperties(entity); } } /// <summary> /// abp 5.x+ 如果存在併發欄位那麼需要添加這段代碼
/// 種子數據需要 /// </summary> protected override void HandlePropertiesBeforeSave() { if (GetShardingExecutor() == null) { base.HandlePropertiesBeforeSave(); } } public IRouteTail RouteTail { get; set; } public override void Dispose() { _shardingDbContextExecutor?.Dispose(); base.Dispose(); } public override async ValueTask DisposeAsync() { if (_shardingDbContextExecutor != null) { await _shardingDbContextExecutor.DisposeAsync(); } await base.DisposeAsync(); } }
改造原DbContext
打開YouProjectNameDbContext.cs文件,繼承剛剛添加的AbstractShardingAbpDbContext和IShardingTableDbContext
using Microsoft.EntityFrameworkCore; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.BackgroundJobs.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Identity; using Volo.Abp.Identity.EntityFrameworkCore; using Volo.Abp.OpenIddict.EntityFrameworkCore; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; using Volo.Abp.TenantManagement; using Volo.Abp.TenantManagement.EntityFrameworkCore; using ShardingCore.Sharding.Abstractions; namespace YourProjectName.EntityFrameworkCore; [ReplaceDbContext(typeof(IIdentityDbContext))] [ReplaceDbContext(typeof(ITenantManagementDbContext))] [ConnectionStringName("Default")] public class YourProjectNameDbContext : AbstractShardingAbpDbContext<YourProjectNameDbContext>, IIdentityDbContext, ITenantManagementDbContext, IShardingTableDbContext //如果dbcontext需要實現分表功能必須實現IShardingTableDbContext { // 原工程的代碼,內容不用變... }
添加ShardingMigrationsSqlGenerator類
作用是在執行Migrator程式自動遷移時,可以創建分表結構。
在YouProjectName.EntityFrameworkCore模塊中新建一個文件ShardingMigrationsSqlGenerator.cs
using System; using System.Linq; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Update; using Pomelo.EntityFrameworkCore.MySql.Infrastructure.Internal; using Pomelo.EntityFrameworkCore.MySql.Migrations; using ShardingCore.Core.RuntimeContexts; using ShardingCore.Helpers; namespace YouProjectName.EntityFrameworkCore; public class ShardingMigrationsSqlGenerator : MySqlMigrationsSqlGenerator { private readonly IShardingRuntimeContext _shardingRuntimeContext; public ShardingMigrationsSqlGenerator(IShardingRuntimeContext shardingRuntimeContext, MigrationsSqlGeneratorDependencies dependencies, ICommandBatchPreparer commandBatchPreparer, IMySqlOptions options) : base(dependencies, commandBatchPreparer, options) { _shardingRuntimeContext = shardingRuntimeContext; } protected override void Generate( MigrationOperation operation, IModel model, MigrationCommandListBuilder builder) { var oldCmds = builder.GetCommandList().ToList(); base.Generate(operation, model, builder); var newCmds = builder.GetCommandList().ToList(); var addCmds = newCmds.Where(x => !oldCmds.Contains(x)).ToList(); MigrationHelper.Generate(_shardingRuntimeContext, operation, builder, Dependencies.SqlGenerationHelper, addCmds); } }
編寫分表路由規則Routes
在YouProjectName.EntityFrameworkCore模塊中,新建Routes文件夾
新建路由規則類,告訴分表組件按照規則進行分表
按ID分表,需要繼承AbstractSimpleShardingModKeyStringVirtualTableRoute<T>
using YouProjectName.MsgAudits; using ShardingCore.Core.EntityMetadatas; using ShardingCore.VirtualRoutes.Mods; namespace YouProjectName.Routes; /// <summary> /// 根據ID分表 /// </summary> public class KeywordTableRoute : AbstractSimpleShardingModKeyStringVirtualTableRoute<Keyword> { /// <summary> /// 簡單說明就是表尾碼是2位,分3個表,例00,01,02 /// </summary> public KeywordTableRoute() : base(2, 3) { } public override void Configure(EntityMetadataTableBuilder<Keyword> builder) { //告訴框架通過Id欄位分表 builder.ShardingProperty(o => o.Id); } }
預期是這樣
按時間月份分表,需要繼承AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<T>
using System; using YouProjectName.MsgAudits; using ShardingCore.Core.EntityMetadatas; using ShardingCore.VirtualRoutes.Months; namespace YouProjectName.Routes; /// <summary> /// 設置ChatRecord表的分表路由規則 /// 根據時間月份分表 /// </summary> public class ChatRecordTableRoute : AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<ChatRecord> { public override bool AutoCreateTableByTime() => true; public override void Configure(EntityMetadataTableBuilder<ChatRecord> builder) { //告訴框架通過哪個欄位分表,消息最好是按消息時間分表 builder.ShardingProperty(o => o.MsgTime); //builder.ShardingProperty(o=>o.CreationTime); // 可以選擇按創建時間分表 } public override DateTime GetBeginTime() { //如果按消息時間分表,那這裡應該返回最早消息的時間 return new DateTime(2023, 01, 01); } }
預期是這樣
添加ShardinfCore 配置
在YouProjectName.EntityFrameworkCore模塊中,編輯YouProjectNameEntityFrameworkCoreModule.cs,修改ConfigureServices方法添加配置
using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.BackgroundJobs.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Identity.EntityFrameworkCore; using Volo.Abp.Modularity; using Volo.Abp.OpenIddict.EntityFrameworkCore; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; using Volo.Abp.TenantManagement.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.DependencyInjection; using Microsoft.EntityFrameworkCore;
using ShardingCore; using YouProjectName.Routes; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.Configuration;
----------------------⬆️引用參考⬆️----------------------
public override void ConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); context.Services.AddAbpDbContext<YouProjectNameDbContext>(options => { /* Remove "includeAllEntities: true" to create * default repositories only for aggregate roots */ options.AddDefaultRepositories(includeAllEntities: true); }); Configure<AbpDbContextOptions>(options => { /* The main point to change your DBMS. * See also YouProjectNameMigrationsDbContextFactory for EF Core tooling. */ options.UseMySQL(); //分表組件增加配置 options.Configure<YouProjectNameDbContext>(innerContext => { ShardingCoreExtension.UseDefaultSharding<YouProjectNameDbContext>(innerContext.ServiceProvider, innerContext.DbContextOptions); }); }); //分表組件單獨配置內容 context.Services.AddShardingConfigure<YouProjectNameDbContext>() .UseRouteConfig(op => { // op.AddShardingDataSourceRoute<TodoDataSourceRoute>(); //分庫規則,這次不包含 op.AddShardingTableRoute<ChatRecordTableRoute>(); //分表規則,單表添加 op.AddShardingTableRoute<KeywordTableRoute>(); }) .UseConfig((sp, op) => { //var loggerFactory = sp.GetRequiredService<ILoggerFactory>(); op.UseShardingQuery((conStr, builder) => { builder.UseMySql(conStr, MySqlServerVersion.LatestSupportedServerVersion); }); op.UseShardingTransaction((connection, builder) => { builder.UseMySql(connection, MySqlServerVersion.LatestSupportedServerVersion); }); op.UseShardingMigrationConfigure(builder => { builder.ReplaceService<IMigrationsSqlGenerator, ShardingMigrationsSqlGenerator>(); }); op.AddDefaultDataSource("ds0", configuration.GetConnectionString("Default")); }) .AddShardingCore(); // 你的其它代碼... }
使用DbMigrator創建資料庫結構
到此所有的配置都完成了,嘗試分表是否生效。
清空Migrations文件夾在YouProjectName.EntityFrameworkCore模塊中。
執行你的YouProjectName.DbMigrator,查看表結構是否符合預期。(DbMigrator會自動生成初始化遷移)
註意:不要使用“dotnet ef database update”這種方式更新資料庫,分表不會正確創建。
註意:分表不能作為其它表的外鍵表。
註意:不能使用Include方式操作數據,如果使用改造為Join方式查詢。
註意:如果DbMigrator執行報錯,查看日誌排查。提醒檢查是否有Entity包含Entity的屬性,EF會自動創建外鍵映射。