Asp.net core下利用EF core實現從數據實現多租戶(2) : 按表分離

前言

上一篇文章中,咱們介紹瞭如何根據不一樣的租戶進行數據分離,分離的辦法是一個租戶一個數據庫。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     }
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 的修改和自定義話是一個大話題,這個須要開另外的文章談

關於本示例的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;
Create tables

 

 

 

調用接口

咱們仍是跟前文同樣,分別使用store1和store2仲添加一些數據。

調動查詢全部product接口

store1:

 

 store2:

 

 

 

總結

這個示例已經完成了。跟前文同樣,是一個實操類型的文章。

下一步是什麼:

下一次咱們談談怎麼根據Schema分離數據。可是Mysql是沒有Schema這個概念的,因此咱們須要把SqlServer集成進來

但這樣把項目的複雜性又提升的。因此這一次必須把代碼抽象好了。

 

關於代碼

代碼已經傳上github,請查看part2的分支或查看commit tag是part2的代碼內容。

https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/part2

相關文章
相關標籤/搜索