在上一篇文章中,咱們介紹瞭如何根據不一樣的租戶進行數據分離,分離的辦法是一個租戶一個數據庫。html
也提到了這種模式仍是相對比較重,因此本文會介紹一種更加廣泛使用的辦法: 按表分離租戶。git
在目前的to B的系統中,其實每每會有一個Master數據庫,裏面使用的是系統中主要的數據,各個租戶的數據,每每只是對應的訂單、配置、客戶信息。github
這就形成了,租戶的數據不會有不少的種類,他的數據表的數量相對Master來講仍是比較少的。sql
因此在單一租戶數據量沒有十分龐大的時候,就沒有必要對單一租戶數據獨立到單一數據庫。多個租戶數據共享使用一個數庫是一個折中的選擇。數據庫
下圖就是對應的數據表結構,其中store1和store2使用不一樣的數據表,但有同一個表名後綴和相同結構。緩存
本文的項目仍是沿用上一篇文章的代碼,進行加以修改。因此項目中的依賴項仍是那些。
架構
但因爲代碼中有不少命名很差的地方我進行了修改。而且,因爲代碼結構太簡單,對這個示例實現起來很差,進行了少許的結構優化。ide
1. ModelCacheKeyFactory,這個是EF core提供的對象,主要是要來產生ModelCacheKey函數
2. ModelCacheKey, 這個跟ModelCacheKeyFactory是一對的,若是須要自定義的話通常要同時實現他們倆post
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 }
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 }
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 }
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 }
其中有一個關鍵的配置, 須要把上文提到的 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 }
在本文中,並無使用Code First配置數據庫。因此數據庫和數據表須要自行建立。
這樣作其實更加貼合項目實際,由於具備這種軟件架構的項目,每每須要在新增租戶的時候進行自動化處理,廣泛作法是準備好一批sql,在新增租戶的時候自動在對應的數據庫中建立一批表
可能會有人提出疑問,以爲ef core提供的Migration是具備一樣的做用的。這個的確是,可是咱們這裏的表是動態的,ef core生成的Migration plan實際上是須要作手動修改的。
Migration 的修改和自定義話是一個大話題,這個須要開另外的文章談
關於本示例的ef core Migration 實操,請參閱個人另外一篇文章
EF core (code first) 經過自定義 Migration History 實現多租戶使用同一數據庫時更新數據庫結構
1 CREATE TABLE `store1_Products` ( 2 `Id` int(11) NOT NULL AUTO_INCREMENT, 3 `Name` varchar(50) CHARACTER SET utf8mb4 NOT NULL, 4 `Category` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL, 5 `Price` double DEFAULT NULL, 6 PRIMARY KEY (`Id`) 7 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; 8 9 CREATE TABLE `store2_Products` ( 10 `Id` int(11) NOT NULL AUTO_INCREMENT, 11 `Name` varchar(50) CHARACTER SET utf8mb4 NOT NULL, 12 `Category` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL, 13 `Price` double DEFAULT NULL, 14 PRIMARY KEY (`Id`) 15 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1;
咱們仍是跟前文同樣,分別使用store1和store2仲添加一些數據。
調動查詢全部product接口
store1:
store2:
這個示例已經完成了。跟前文同樣,是一個實操類型的文章。
下一次咱們談談怎麼根據Schema分離數據。可是Mysql是沒有Schema這個概念的,因此咱們須要把SqlServer集成進來
但這樣把項目的複雜性又提升的。因此這一次必須把代碼抽象好了。
代碼已經傳上github,請查看part2的分支或查看commit tag是part2的代碼內容。
https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/part2