• 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 的修改和自定义话是一个大话题,这个需要开另外的文章谈

    关于本示例的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

  • 相关阅读:
    dwz 嵌套网页的搜索刷新问题
    dwz 解决remote验证唯一时缓存问题。
    dwz div 局部刷新
    Oracle 递归查询子节点和父节点 函数方法
    关于未能加载文件或程序集“System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35”或它的某一个依赖项的解决办法
    CentOS7搭建vsftp服务器
    Linux下 cmatrix的安装和使用(黑客屏保)
    配置动态web服务(wsgi)
    centos7 搭建discuz 全
    Centos7 更换为阿里源
  • 原文地址:https://www.cnblogs.com/woailibian/p/12317944.html
Copyright © 2020-2023  润新知