Asp.net core下利用EF core實現從資料實現多租戶(2) : 按表分離
前言
在上一篇文章中,我們介紹瞭如何根據不同的租戶進行資料分離,分離的辦法是一個租戶一個數據庫。
也提到了這種模式還是相對比較重,所以本文會介紹一種更加普遍使用的辦法: 按表分離租戶。
這樣做的好處是什麼:
在目前的to B的系統中,其實往往會有一個Master資料庫,裡面使用的是系統中主要的資料,各個租戶的資料,往往只是對應的訂單、配置、客戶資訊。
這就造成了,租戶的資料不會有很多的種類,他的資料表的數量相對Master來說還是比較少的。
所以在單一租戶資料量沒有十分龐大的時候,就沒有必要對單一租戶資料獨立到單一資料庫。多個租戶資料共享使用一個數庫是一個折中的選擇。
下圖就是對應的資料表結構,其中store1和store2使用不同的資料表,但有同一個表名字尾和相同結構。
實施
專案介紹
本文的專案還是沿用上一篇文章的程式碼,進行加以修改。所以專案中的依賴項還是那些。
但由於程式碼中有很多命名不好的地方我進行了修改。並且,由於程式碼結構太簡單,對這個示例實現起來不好,進行了少量的結構優化。
專案中新增的物件有什麼:
1. ModelCacheKeyFactory,這個是EF core提供的物件,主要是要來產生ModelCacheKey
2. ModelCacheKey, 這個跟ModelCacheKeyFactory是一對的,如果需要自定義的話一般要同時實現他們倆
3. ConnectionResolverOption,這個是專案自定義的物件,用於配置。因為我們專案中現在需要同時支援多種租戶資料分離的方式
實施步驟
1. 新增 ITenantDbContext 介面,它的作用是要來規定StoreDbContext中,必須可以返回TenantInfo。
1 public interface ITenantDbContext 2 { 3 TenantInfo TenantInfo{get;} 4 }
我們同時也需要修改StoreDbContext去實現 ITenantDbContext 介面,並且在建構函式上新增TenantInfo的注入
其中Products已經不是原來簡單的一個Property,這裡使用DbSet來獲取對應的物件,因為表物件還是使用只讀Property會好點。
新增一個方法的重寫OnModelCreating,這個方法的主要規定EF core 的表實體(本文是Product)怎麼跟資料庫匹配的,簡單來說就是配置。
可以看到表名的規則是TenantInfo.Name+"_Products"
1 public class StoreDbContext : DbContext,ITenantDbContext 2 { 3 public DbSet<Product> Products => this.Set<Product>(); 4 5 public TenantInfo TenantInfo => tenantInfo; 6 7 private readonly TenantInfo tenantInfo; 8 9 public StoreDbContext(DbContextOptions options, TenantInfo tenantInfo) : base(options) 10 { 11 this.tenantInfo = tenantInfo; 12 } 13 14 protected override void OnModelCreating(ModelBuilder modelBuilder) 15 { 16 modelBuilder.Entity<Product>().ToTable(this.tenantInfo.Name + "_Products"); 17 } 18 }StoreDbContext
2. 建立 TenantModelCacheKeyFactory 和 TenantModelCacheKey
TenantModelCacheKeyFactory的作用主要是建立TenantModelCacheKey例項。TenantModelCacheKey的作用是作為一個鍵值,標識dbContext中的OnModelCreating否需要呼叫。
為什麼這樣做呢?因為ef core為了優化效率,避免在dbContext每次例項化的時候,都需要重新構建資料實體模型。
在預設情況下,OnModelCreating只會呼叫一次就會存在快取。但由於我們建立了TenantModelCacheKey,使得我們有機會判斷在什麼情況下需要重新呼叫OnModelCreating
這裡是本文中最關鍵的改動
1 using System; 2 using Microsoft.EntityFrameworkCore; 3 using Microsoft.EntityFrameworkCore.Infrastructure; 4 5 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 6 { 7 internal sealed class TenantModelCacheKeyFactory<TContext> : ModelCacheKeyFactory 8 where TContext : DbContext, ITenantDbContext 9 { 10 11 public override object Create(DbContext context) 12 { 13 var dbContext = context as TContext; 14 return new TenantModelCacheKey<TContext>(dbContext, dbContext?.TenantInfo?.Name ?? "no_tenant_identifier"); 15 } 16 17 public TenantModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies) 18 { 19 } 20 } 21 22 internal sealed class TenantModelCacheKey<TContext> : ModelCacheKey 23 where TContext : DbContext, ITenantDbContext 24 { 25 private readonly TContext context; 26 private readonly string identifier; 27 public TenantModelCacheKey(TContext context, string identifier) : base(context) 28 { 29 this.context = context; 30 this.identifier = identifier; 31 } 32 33 protected override bool Equals(ModelCacheKey other) 34 { 35 return base.Equals(other) && (other as TenantModelCacheKey<TContext>)?.identifier == identifier; 36 } 37 38 public override int GetHashCode() 39 { 40 var hashCode = base.GetHashCode(); 41 if (identifier != null) 42 { 43 hashCode ^= identifier.GetHashCode(); 44 } 45 46 return hashCode; 47 } 48 } 49 }TenantModelCacheKeyFactory & TenantModelCacheKey
3. 新增 ConnectionResolverOption 類和 ConnectionResolverType 列舉。
1 using System; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 4 { 5 public class ConnectionResolverOption 6 { 7 public string Key { get; set; } = "default"; 8 9 public ConnectionResolverType Type { get; set; } 10 11 public string ConnectinStringName { get; set; } 12 } 13 14 public enum ConnectionResolverType 15 { 16 Default = 0, 17 ByDatabase = 1, 18 ByTabel = 2 19 } 20 }ConnectionResolverOption & ConnectionResolverType
4. 調整 MultipleTenancyExtension 的程式碼結構,並且新增2個擴充套件函式用於對配置相關的注入。
下面貼出修改過後最主要的3個方法
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 services.AddDbContext<TDbContext>((serviceProvider, options) => 10 { 11 var resolver = serviceProvider.GetRequiredService<ISqlConnectionResolver>(); 12 13 var dbOptionBuilder = options.UseMySql(resolver.GetConnection()); 14 if (option.Type == ConnectionResolverType.ByTabel) 15 { 16 dbOptionBuilder.ReplaceService<IModelCacheKeyFactory, TenantModelCacheKeyFactory<TDbContext>>(); 17 } 18 }); 19 20 return services; 21 } 22 23 public static IServiceCollection AddTenantDatabasePerTable<TDbContext>(this IServiceCollection services, 24 string connectionStringName, string key = "default") 25 where TDbContext : DbContext, ITenantDbContext 26 { 27 var option = new ConnectionResolverOption() 28 { 29 Key = key, 30 Type = ConnectionResolverType.ByTabel, 31 ConnectinStringName = connectionStringName 32 }; 33 34 return services.AddTenantDatabasePerTable<TDbContext>(option); 35 } 36 37 public static IServiceCollection AddTenantDatabasePerTable<TDbContext>(this IServiceCollection services, 38 ConnectionResolverOption option) 39 where TDbContext : DbContext, ITenantDbContext 40 { 41 if (option == null) 42 { 43 option = new ConnectionResolverOption() 44 { 45 Key = "default", 46 Type = ConnectionResolverType.ByTabel, 47 ConnectinStringName = "default" 48 }; 49 } 50 51 52 return services.AddDatabase<TDbContext>(option); 53 }MultipleTenancyExtension functions
其中有一個關鍵的配置, 需要把上文提到的 TenantModelCacheKeyFactory 配置到dbOptionBuilder
1 if (option.Type == ConnectionResolverType.ByTabel) 2 { 3 dbOptionBuilder.ReplaceService<IModelCacheKeyFactory,TenantModelCacheKeyFactory<TDbContext>>(); 4 }
5. 在 TenantSqlConnectionResolver 的GetConnection方法中修改邏輯,讓它同時支援按表分離資料和前文的按資料庫分離資料
這個類的名字已經改了,前文的命名不合適。 方法中用到的 option 是 ConnectionResolverOption 型別,需要加到建構函式。
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 connectionString = configuration.GetConnectionString(this.option.ConnectinStringName); 11 break; 12 } 13 14 if (string.IsNullOrEmpty(connectionString)) 15 { 16 throw new NullReferenceException("can not find the connection"); 17 } 18 return connectionString; 19 }TenantSqlConnectionResolver.GetConnection
驗證效果
前提條件
在本文中,並沒有使用Code First配置資料庫。所以資料庫和資料表需要自行建立。
這樣做其實更加貼合專案實際,因為具有這種軟體架構的專案,往往需要在新增租戶的時候進行自動化處理,普遍做法是準備好一批sql,在新增租戶的時候自動在對應的資料庫中建立一批表
可能會有人提出疑問,覺得ef core提供的Migration是具有同樣的作用的。這個的確是,但是我們這裡的表是動態的,ef core生成的Migration plan其實是需要做手動修改的。
Migration 的修改和自定義話是一個大話題,這個需要開另外的文章談
建表指令碼
create table
呼叫介面
我們還是跟前文一樣,分別使用store1和store2仲新增一些資料。
調動查詢所有product介面
store1:
store2:
總結
這個示例已經完成了。跟前文一樣,是一個實操型別的文章。
下一步是什麼:
下一次我們談談怎麼根據Schema分離資料。但是Mysql是沒有Schema這個概念的,所以我們需要把SqlServer整合進來
但這樣把專案的複雜性又提高的。所以這一次必須把程式碼抽象好了。
關於程式碼
程式碼已經傳上github,請檢視part2的分支或檢視commit tag是part2的程式碼內容。
https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/part2
&n