前言
前段時間寫了EF core實現多租戶的文章,實現了根據數據庫,數據表進行多租戶數據隔離。
今天開始寫按照Schema分離的文章。
其實還有一種,是通過在數據表內添加一個字段做多租戶的,但是這種模式我不打算講了。
如果大家看了文章感覺完全銜接不上,可以先看看前面的系列文章:
Asp.net core下利用EF core實現從數據實現多租戶(1)
Asp.net core下利用EF core實現從數據實現多租戶(2) : 按表分離
EF core (code first) 通過自定義 Migration History 實現多租戶使用同一數據庫時更新數據庫結構
關於EF core自動遷移:
可能有朋友會覺得EF core不使用自動遷移就偏離了EF core的設計初衷。其實我覺得技術是實施的手段,而不是束縛項目的絆腳石。
1. 首先EF core並不是只有code first模式,
2. 其次EF core對db first模式支持很好,對於一些經歷幾年發展的項目會更加友好,因為對舊數據庫進行O/R不是1,2周可以完成的,
3. 再次在以往的EF migration經驗中,即使項目完全按照code first模式發展,但實際上更新數據庫的並不是通過的Web項目,而是通過一個控制台,里面包含了Migration文件、數據遷移、結構校驗數據校驗。
這個控制台,一般通過CI/CD執行或手動執行。這是由於數據量、系統結構(例如多租戶等)決定的。
所以,EF core的自動遷移不是這個系列文章的主線主分支
如果想參考自動遷移的實施步驟,歡迎查看我的另一篇文章,是根據本文背景實現的自動遷移實施步驟:
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 (本文新增的依賴)
3. EF core,Microsoft.EntityFrameworkCore, 版本3.1.1。這里必須要用3.1的,因為ef core3.0是面向.net standard 2.1.
實施步驟
1. 由於我們引入了MsSql,所以要對 MultipleTenancyExtension 進行修改,對立面的所有方法都要添加db類型進行傳參。
修改 AddDatabase 方法,立面需要對sql server和MySql進行判斷

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 15 DbContextOptionsBuilder dbOptionBuilder = null; 16 switch (option.DBType) 17 { 18 case DatabaseIntegration.SqlServer: 19 dbOptionBuilder = options.UseSqlServer(resolver.GetConnection()); 20 break; 21 case DatabaseIntegration.Mysql: 22 dbOptionBuilder = options.UseMySql(resolver.GetConnection()); 23 break; 24 default: 25 throw new System.NotSupportedException("db type not supported"); 26 } 27 if (option.Type == ConnectionResolverType.ByTabel || option.Type == ConnectionResolverType.BySchema) 28 { 29 dbOptionBuilder.ReplaceService<IModelCacheKeyFactory, TenantModelCacheKeyFactory<TDbContext>>(); 30 } 31 }); 32 33 return services; 34 }
添加2個 AddTenantDatabasePerSchema 方法,實現根據Schema的分離

1 public static IServiceCollection AddTenantDatabasePerSchema<TDbContext>(this IServiceCollection services, 2 string connectionStringName, string key = "default") 3 where TDbContext : DbContext, ITenantDbContext 4 { 5 var option = new ConnectionResolverOption() 6 { 7 Key = key, 8 Type = ConnectionResolverType.BySchema, 9 ConnectinStringName = connectionStringName, 10 DBType = DatabaseIntegration.SqlServer 11 }; 12 13 14 return services.AddTenantDatabasePerSchema<TDbContext>(option); 15 } 16 17 public static IServiceCollection AddTenantDatabasePerSchema<TDbContext>(this IServiceCollection services, 18 ConnectionResolverOption option) 19 where TDbContext : DbContext, ITenantDbContext 20 { 21 if (option == null) 22 { 23 option = new ConnectionResolverOption() 24 { 25 Key = "default", 26 Type = ConnectionResolverType.BySchema, 27 ConnectinStringName = "default", 28 DBType = DatabaseIntegration.SqlServer 29 }; 30 } 31 32 return services.AddTenantDatabasePerTable<TDbContext>(option); 33 }
2. 添加 DatabaseIntegration 枚舉,用於標記db的類型
1 public enum DatabaseIntegration 2 { 3 None = 0, 4 Mysql = 1, 5 SqlServer = 2 6 }
3. 修改 TenantSqlConnectionResolver 類立面的GetConection 方法,在switch中添加添加一個case。就是代碼中高亮部分
1 public string GetConnection() 2 { 3 string connectionString = null; 4 switch (this.option.Type) 5 { 6 case ConnectionResolverType.ByDatabase: 7 connectionString = configuration.GetConnectionString(this.tenantInfo.Name); 8 break; 9 case ConnectionResolverType.ByTabel: 10 case ConnectionResolverType.BySchema: 11 connectionString = configuration.GetConnectionString(this.option.ConnectinStringName); 12 break; 13 } 14 15 if (string.IsNullOrEmpty(connectionString)) 16 { 17 throw new NullReferenceException("can not find the connection"); 18 } 19 return connectionString; 20 }
4. 修改 StoreDbContext 里的 OnModelCreating 方法,把之前按照Table分離數據的代碼注釋,重新添加按Schema分離數據的代碼。
這里需要注意的是,在項目目前的結構,同一個DbContext,同時只能支持按Table或Schema其中的一種。
其實實際項目中,也的確沒有必要需要對一個DbContext同時支持Table或Schema的支持,因為本質上這2種方式都是同時保存在一個數據庫。
不過這是能實現的,本文暫不做實現。
1 protected override void OnModelCreating(ModelBuilder modelBuilder) 2 { 3 // seperate by table 4 // modelBuilder.Entity<Product>().ToTable(this.tenantInfo.Name + "_Products"); 5 6 // seperate by Schema 7 modelBuilder.Entity<Product>().ToTable(nameof(this.Products), "dbo."+this.tenantInfo.Name); 8 }
5. 修改 Startup 類里面的 ConfigureServices 方法,把之前按照Table分離數據的注入代碼注釋,重新添加新的代碼。
1 public void ConfigureServices(IServiceCollection services) 2 { 3 // services.AddConnectionByDatabase<StoreDbContext>(); 4 // services.AddTenantDatabasePerTable<StoreDbContext>("default"); 5 services.AddTenantDatabasePerSchema<StoreDbContext>("mssql"); 6 services.AddControllers(); 7 }
驗證效果
啟動項目
本文沒有加入EF core的自動遷移代碼,如果需要需要查看如果實現自動遷移,請參考我的另一篇文章,是系列文章的附加內容,並不是項目中的主要內容。
自動遷移文章
EF core (code first) 通過自動遷移實現多租戶數據分離 :按Schema分離數據
調用接口
1. 我們還是跟本系列的其他文章一樣,分別在store1和store2中添加數據。
其中怎么添加的就不再重復貼圖了,簡單來說就是調用controller的post方法在數據庫中添加數據
下面是store1的查詢結果
store2的查詢結果
查看數據庫數據和結構
可以看到在multiple_tenancy_default3里面,有4個Schema,其中 dbo.store1 和 dob.store2 是存放我們的表。
store1中的數據
store2中的數據
總結
本文跟本系列一樣,都是非常簡單的實操性指引。完成本文之后,實際上已經實現了本項目的所有需求,分別是按數據庫,按表,按Schema分離數據。
但是如果把這種代碼結構全搬進去實際商用項目中,還是操之過早,希望閣下可以等待我們把代碼結構整理和抽象后再結合到項目。因為系列主線文章比較簡單,這種代碼結構實際上不適合項目長期發展。
關於代碼
本系列文章全部在github上。
請查看part3分支的代碼
https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/part3