寫這篇文章的緣由,其實因爲我寫EF core 實現多租戶的時候,遇到的問題。html
具體文章的連接:git
Asp.net core下利用EF core實現從數據實現多租戶(1)github
Asp.net core下利用EF core實現從數據實現多租戶(2) : 按表分離 (主要關聯文章)sql
這裏我遇到的最主要問題是:因爲多租戶的表使用的是同一個數據庫。因爲這個緣由,沒法經過 Database.EnsureCreated() 自動建立多個結構相同但名字不一樣的表。數據庫
因此我在文中提到,須要本身跑腳本去建立多有的表。app
雖然我依然認爲在多租戶的狀況下使用sql管理表是更可靠的方案,但若是能夠利用EF core原生提供的Migration機制,在運行時自動建立和更新數據表結構,那更加友好。ide
實現的思路函數
其實咱們都知道,EF core (code first) 會在數據庫中生成惟一一個 __EFMigrationHistory 表,數據庫的版本記錄在這裏。post
在咱們文章的場景下,因爲有多個租戶同時使用,同一個表結構(Products)會出現屢次,那麼意思就是一個 __EFMigrationHistory 沒法同時記錄多個租戶的數據表版本。ui
好了,既然問題的關鍵已經知道了,咱們能夠在這裏先把答案揭曉,在下問在詳細說明實現方法:
圖中能夠看到,咱們自定義MigrationHistory表,而且在一個數據下,同時出現了store1和store2的 MigrationHistory 表。
這是一個多租戶系統,具體來講就是根據不一樣的租戶,建立相同的全部數據表。
1. .net core app 3.1。在機器上安裝好.net core SDK, 版本3.1
2. Mysql. 使用 Pomelo.EntityFrameworkCore.MySql 包
3. EF core,Microsoft.EntityFrameworkCore, 版本3.1.1。這裏必需要用3.1的,由於ef core3.0是面向.net standard 2.1.
4. EF core design, Microsoft.EntityFrameworkCore.Design, 版本 3.1.1
5. dotnet-ef tool, 版本 3.1.1
1. MigrationsAssembly, 利用此類去實現建立對應的Migration單元。
2. Migration files, 這裏指的是一批Migration相關的文件,利用執行dotnet-ef 命令生成具體的文件,從而真正地去建立和更新數據庫。
1. 運行dotnet-ef命令,生成Migration files
命令:
1 dotnet-ef migrations add init
執行後,會在項目中的Migrations文件夾下生成多個*.cs文件,其實他們也是可執行C#對象
機構以下:
這3個文件中,主要起做用的是*_init.cs這個文件
打開以後咱們須要對他進行修改
1 using Microsoft.EntityFrameworkCore.Metadata; 2 using Microsoft.EntityFrameworkCore.Migrations; 3 4 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Migrations 5 { 6 public partial class init : Migration 7 { 8 private readonly string prefix; 9 public init(string prefix) 10 { 11 if (string.IsNullOrEmpty(prefix)) 12 { 13 throw new System.ArgumentNullException(); 14 } 15 this.prefix = prefix; 16 } 17 18 protected override void Up(MigrationBuilder migrationBuilder) 19 { 20 migrationBuilder.CreateTable( 21 name: prefix + "_Products", 22 columns: table => new 23 { 24 Id = table.Column<int>(nullable: false) 25 .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), 26 Name = table.Column<string>(maxLength: 50, nullable: false), 27 Category = table.Column<string>(maxLength: 50, nullable: true), 28 Price = table.Column<double>(nullable: true) 29 }, 30 constraints: table => 31 { 32 table.PrimaryKey("PK__Products", x => x.Id); 33 }); 34 } 35 36 protected override void Down(MigrationBuilder migrationBuilder) 37 { 38 migrationBuilder.DropTable( 39 name: prefix + "_Products"); 40 } 41 } 42 }
這裏修改的主要是:
1.1 新增構造函數,而且在裏面添加一個 prefix 參數。
1.2 在Up方法中,對table Name進行修改,把prefix變量加在_Product前面(第21行)
1.3 在Down方法中,對table Name進行修改,把prefix變量加在_Product前面 (第39行)
2. 建立 MigrationByTenantAssembly 文件。
因爲上一步講Migration file的構造函數修改了,理論上EF core已經五法經過默認的方式成功執行改Migration file了
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方法中,添加自定義MigrationHistory表名
1 var dbOptionBuilder = options.UseMySql(resolver.GetConnection(), builder => 2 { 3 if (option.Type == ConnectionResolverType.ByTabel) 4 { 5 builder.MigrationsHistoryTable(${tenantInfo.Name}__EFMigrationsHistory"); 6 } 7 }); 8 9 10 dbOptionBuilder.ReplaceService<Microsoft.EntityFrameworkCore.Migrations.IMigrationsAssembly, MigrationByTenantAssembly>();
最關鍵的一點是第5行,調用 MigrationsHistoryTable 設置MigrationHistory表名
另一點是第10行,用 MigrationByTenantAssembly 類替換 EF core 中默認的實現(IMigrationsAssembly接口)
4. 在ProductController的構造函數中,修改爲以下
Database.Migrate 的做用主要是在運行時能夠執行數據庫的建立和更新
1 public ProductController(StoreDbContext storeDbContext) 2 { 3 this.storeDbContext = storeDbContext; 4 this.storeDbContext.Database.Migrate(); 5 }
跟系列文章同樣,咱們先調用建立product的接口分別在store1和store2中添加記錄。
下面是store1 的查詢結果
store2的查詢結果
數據庫的表結構
store1_Products 表數據
store2_Products 表數據
本文中咱們介紹了ef core 的code first模式下是如何更新數據庫的,而且經過添加 Migration 對象的構造函數 ,自行添加了必要參數。
經過替換EF core中默認的 IMigrationsAssembly 實現, MigrationByTenantAssembly 中自定對Migration對象實例化。
替換EF core中默認的MigrationHistory最終實現需求。
本文雖然只是一個示例,可是卻能夠在真實項目中使用相同的手段以實現需求。不過仍是那句話,對於多租戶狀況下,我推薦使用db first模式。
代碼已經傳上github,請查看EF_code_first的分支的代碼。
https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/EF_code_first