EF core (code first) 經過自定義 Migration History 實現多租戶使用同一數據庫時更新數據庫結構

前言

寫這篇文章的緣由,其實因爲我寫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 }
init migration

這裏修改的主要是:

 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 }
MigrationByTenantAssembly

 

這個類中沒有什麼特別的,關鍵在於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

相關文章
相關標籤/搜索