前言
本文是多租戶系列文章的附加操作文章,如果想查看系列中的其他文章請查看下列文章
主線文章
Asp.net core下利用EF core實現從數據實現多租戶(1)
Asp.net core下利用EF core實現從數據實現多租戶(2) : 按表分離
Asp.net core下利用EF core實現從數據實現多租戶(3): 按Schema分離 附加:EF Migration 操作 (強關聯文章,建議先閱讀)
附加文章
EF core (code first) 通過自定義 Migration History 實現多租戶使用同一數據庫時更新數據庫結構
EF core (code first) 通過自動遷移實現多租戶數據分離 :按Schema分離數據 (本文)
實施
項目介紹
本項目是用系列文章的主分支代碼進行修改的。目前項目主要支持使用MySql,通過分庫,分表實現多租戶。
本文需要實現分Schema,MySql不能實現,所以引入了MSSqlServer。
項目依賴
1. .net core app 3.1。在機器上安裝好.net core SDK, 版本3.1
2. Mysql. 使用 Pomelo.EntityFrameworkCore.MySql 包, 版本3.1.1
3. MS Sql Server. 使用 Microsoft.EntityFrameworkCore.SqlServer 包,版本3.1.1
4. EF core,Microsoft.EntityFrameworkCore, 版本3.1.1。這里必須要用3.1的,因為ef core3.0是面向.net standard 2.1.
5. EF core design, Microsoft.EntityFrameworkCore.Design, 版本 3.1.1
6. dotnet-ef tool, 版本 3.1.1
關鍵要點
其實如果讀過我之前的EF core 自動遷移的文章,就會發現其實有幾個關鍵點
1. 通過ef core CLI 生成Migration文件,並且在所有版本的Migration文件中添加一個帶參數的構造函數
2. 自定義 MigrationByTenantAssembly 類,通過重寫 CreateMigration 實現對修改后的Migration文件進行實例化
3. 自定義 __EFMigrationsHistory 的命名和存放位置
實施步驟
1. 運行dotnet-ef命令,生成Migration files
命令:
1 dotnet-ef migrations add init_schema
執行后,會在項目中的Migrations文件夾下生成多個*.cs文件,其實他們也是可執行C#對象
這3個文件中,主要起作用的是*_init_schema.cs這個文件
打開之后我們需要對他進行修改(所有修改內容已進行高亮)
這里修改的主要是:
1.1 新增構造函數,並且在里面添加一個 schema 參數。
1.2 在Up方法中,對調用 EnsureSchema 進行修改,把 schema 變量加在name參數(第16行)
1.3 在Up方法中,對調用 CreateTable 的 schema 參數中添加自定義變量schema (第20行)
1.4 在Down方法中,對調用 DropTable 的 schema 參數中添加自定義變量schema(第39行)
1 using Microsoft.EntityFrameworkCore.Migrations; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Migrations 4 { 5 public partial class init_schema : Migration 6 { 7 private readonly string schema; 8 public init_schema(string schema) 9 { 10 this.schema = schema; 11 12 } 13 protected override void Up(MigrationBuilder migrationBuilder) 14 { 15 migrationBuilder.EnsureSchema( 16 name: "dbo." + schema); 17 18 migrationBuilder.CreateTable( 19 name: "Products", 20 schema: "dbo." + schema, 21 columns: table => new 22 { 23 Id = table.Column<int>(nullable: false) 24 .Annotation("SqlServer:Identity", "1, 1"), 25 Name = table.Column<string>(maxLength: 50, nullable: false), 26 Category = table.Column<string>(maxLength: 50, nullable: true), 27 Price = table.Column<double>(nullable: true) 28 }, 29 constraints: table => 30 { 31 table.PrimaryKey("PK_Products", x => x.Id); 32 }); 33 } 34 35 protected override void Down(MigrationBuilder migrationBuilder) 36 { 37 migrationBuilder.DropTable( 38 name: "Products", 39 schema: "dbo." + schema); 40 } 41 } 42 }
2. 添加 MigrationByTenantAssembly 類,同時需要實現 MigrationsAssembly 類和重寫 CreateMigration。

1 using System; 2 using System.Reflection; 3 using Microsoft.EntityFrameworkCore; 4 using Microsoft.EntityFrameworkCore.Diagnostics; 5 using Microsoft.EntityFrameworkCore.Infrastructure; 6 using Microsoft.EntityFrameworkCore.Migrations; 7 using Microsoft.EntityFrameworkCore.Migrations.Internal; 8 9 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 10 { 11 public class MigrationByTenantAssembly : MigrationsAssembly 12 { 13 private readonly DbContext context; 14 15 public MigrationByTenantAssembly(ICurrentDbContext currentContext, 16 IDbContextOptions options, IMigrationsIdGenerator idGenerator, 17 IDiagnosticsLogger<DbLoggerCategory.Migrations> logger) 18 : base(currentContext, options, idGenerator, logger) 19 { 20 context = currentContext.Context; 21 } 22 23 public override Migration CreateMigration(TypeInfo migrationClass, 24 string activeProvider) 25 { 26 if (activeProvider == null) 27 throw new ArgumentNullException($"{nameof(activeProvider)} argument is null"); 28 29 var hasCtorWithSchema = migrationClass 30 .GetConstructor(new[] { typeof(string) }) != null; 31 32 if (hasCtorWithSchema && context is ITenantDbContext tenantDbContext) 33 { 34 var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), tenantDbContext?.TenantInfo?.Name); 35 instance.ActiveProvider = activeProvider; 36 return instance; 37 } 38 39 return base.CreateMigration(migrationClass, activeProvider); 40 } 41 } 42 }
這個類中沒有什么特別的,關鍵在於29~37行。首先需要判斷目標 Migration 對象的是否有一個構造函數的參數有且僅有一個string 類型
判斷DbContext是否有實現ITenantDbContext接口。
利用 Activator 創建 Migration 實例(把tenant Name傳進構造函數)
3. 修改在 MultipleTenancyExtension 類的AddDatabase方法。(所有修改部分已經高亮) (這是非常關鍵的步驟)
關鍵點:
必須為 UseMySql 和 UseSqlServer 添加第二個參數,同時定義 __EFMigrationsHistory 的命名和存放位置。注意SqlServer中的schema命名必須跟dbContext中的schema的名字完全相同。由於MySql沒有schema的概念,所以MySql中不能加入對應的schema參數。
在最后一行高亮的代碼,通過 ReplaceService 替換 dbContext 中默認的 MigrationAssembly 實現類
1 internal static IServiceCollection AddDatabase<TDbContext>(this IServiceCollection services, 2 ConnectionResolverOption option) 3 where TDbContext : DbContext, ITenantDbContext 4 { 5 services.AddSingleton(option); 6 7 services.AddScoped<TenantInfo>(); 8 services.AddScoped<ISqlConnectionResolver, TenantSqlConnectionResolver>(); 9 10 services.AddDbContext<TDbContext>((serviceProvider, options) => 11 { 12 var dbContextManager = serviceProvider.GetService<IDbContextManager>(); 13 var resolver = serviceProvider.GetRequiredService<ISqlConnectionResolver>(); 14 var tenant = serviceProvider.GetService<TenantInfo>(); 15 16 DbContextOptionsBuilder dbOptionBuilder = null; 17 switch (option.DBType) 18 { 19 case DatabaseIntegration.MySql: 20 dbOptionBuilder = options.UseMySql(resolver.GetConnection(), 21 optionBuilder => 22 { 23 if (option.Type == ConnectionResolverType.ByTabel) 24 { 25 optionBuilder.MigrationsHistoryTable($"{tenant.Name}__EFMigrationsHistory"); 26 } 27 }); 28 break; 29 case DatabaseIntegration.SqlServer: 30 dbOptionBuilder = options.UseSqlServer(resolver.GetConnection(), 31 optionBuilder => 32 { 33 if (option.Type == ConnectionResolverType.ByTabel) 34 { 35 optionBuilder.MigrationsHistoryTable($"{tenant.Name}__EFMigrationsHistory"); 36 } 37 if (option.Type == ConnectionResolverType.BySchema) 38 { 39 optionBuilder.MigrationsHistoryTable("__EFMigrationsHistory", $"dbo.{tenant.Name}"); 40 } 41 }); 42 break; 43 default: 44 throw new System.NotSupportedException("db type not supported"); 45 } 46 if (option.Type == ConnectionResolverType.ByTabel || option.Type == ConnectionResolverType.BySchema) 47 { 48 dbOptionBuilder.ReplaceService<IModelCacheKeyFactory, TenantModelCacheKeyFactory<TDbContext>>(); 49 dbOptionBuilder.ReplaceService<Microsoft.EntityFrameworkCore.Migrations.IMigrationsAssembly, MigrationByTenantAssembly>(); 50 } 51 }); 52 53 return services; 54 }
4. 修改StoreDbContext 中的 OnModelCreating 方法
1 protected override void OnModelCreating(ModelBuilder modelBuilder) 2 { 3 // seperate by table 4 // modelBuilder.Entity<Product>().ToTable(this.tenantInfo.Name + "_Products"); 5 // seperate by Schema 6 modelBuilder.Entity<Product>().ToTable(nameof(this.Products), "dbo."+this.tenantInfo.Name); 7 }
5. 修改ProductController的構造函數
1 public ProductController(StoreDbContext storeDbContext) 2 { 3 this.storeDbContext = storeDbContext; 4 // this.storeDbContext.Database.EnsureCreated(); 5 this.storeDbContext.Database.Migrate(); 6 }
查看效果
1. 我們還是跟本系列的其他文章一樣,分別在store1和store2中添加數據。
其中怎么添加的就不再重復貼圖了,簡單來說就是調用controller的post方法在數據庫中添加數據
查詢 store1 的數據
查詢 store2 的數據
2. 查看數據庫的機構和數據。
這是數據庫的結構, 可以看到有4個schema,其中 dbo.store1 和 dbo.store2 是存放我們自己的數據的。
dbo.store1 和 dbo.store2 里面分別有一個__EFMigrationsHistory 表,這個就是EF core自動遷移的版本記錄。
store1 中的數據
store2 中的數據
添加遷移版本
讀到這里的朋友可能還會有個疑問,覺得我這個自動遷移只做了一個版本,似乎不足以證明這個方案是可以行的。
1. 那我們就簡單在Product里面再加一個 Discount 的字段。注意了,新加的字段我建議使用可空類型
1 using System.ComponentModel.DataAnnotations; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.DAL 4 { 5 public class Product 6 { 7 [Key] 8 public int Id { get; set; } 9 10 [StringLength(50), Required] 11 public string Name { get; set; } 12 13 [StringLength(50)] 14 public string Category { get; set; } 15 16 public double? Price { get; set; } 17 18 public double? Discount { get; set; } 19 } 20 }
2. 通過dotnet-ef CLI運行命令,添加Migration版本
dotnet-ef migrations add disount_support
可以看到在Migrations 目錄下多了2個*.cs文件
我們根據本文前面的步驟依樣畫瓢,分別添加帶參數的構造函數和修改 Up 和 Down 方法
1 using Microsoft.EntityFrameworkCore.Migrations; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Migrations 4 { 5 public partial class disount_support : Migration 6 { 7 private readonly string schema; 8 public disount_support(string schema) 9 { 10 this.schema = schema; 11 } 12 13 protected override void Up(MigrationBuilder migrationBuilder) 14 { 15 migrationBuilder.AddColumn<double>( 16 name: "Discount", 17 schema: "dbo." + schema, 18 table: "Products", 19 nullable: true); 20 } 21 22 protected override void Down(MigrationBuilder migrationBuilder) 23 { 24 migrationBuilder.DropColumn( 25 name: "Discount", 26 schema: "dbo." + schema, 27 table: "Products"); 28 } 29 } 30 }
我們啟動項目,調用查詢接口。然后在修改 store1 中的Coffee的Discount (這里注意的是,需要先調用接口,再到數據庫中修改數據)
下面就是 store1 的查詢結果,可以看到Coffee的Discount是0.6 ,就會說咖啡是打六折的
下面我們對比 store1 和 store2 的表結構和數據。發現 store2 中並沒有Discount的字段。並且 __EFMigrationsHistory 中只有一條記錄
原因是我們還沒執行接口,導致EF core的自動遷移並沒有生效
我們重新調用 store2 的接口,然后在看數據庫表結構。發現 store2 中的Discount已經加上去了
總結
EF core的自動遷移的具體實操步驟就是上文所述了。
我再次重申,EF core的自動遷移並不是必備的,而是選配。 我非常不建議在運行多年並且有多租戶結構的項目中使用 EF core 的 code first 模式。
不過假設你的項目已經分離得很不錯,或是一個全新項目,我建議大家嘗試。
關於代碼
本系列的所有文章代碼都會上傳到github中,目前master是主分支,由於自動遷移部分只是附加內容,所以本文所有代碼,請查看分支 EF_code_first_part3
github 地址:
https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/EF_code_first_part3