前言
在上一篇文章中, 我们介绍了如何根据不同的租户进行数据分离, 分离的办法是一个租户一个数据库.
也提到了这种模式还是相对比较重, 所以本文会介绍一种更加普遍使用的办法: 按表分离租户.
这样做的好处是什么:
在目前的 to B 的系统中, 其实往往会有一个 Master 数据库, 里面使用的是系统中主要的数据, 各个租户的数据, 往往只是对应的订单, 配置, 客户信息.
这就造成了, 租户的数据不会有很多的种类, 他的数据表的数量相对 Master 来说还是比较少的.
所以在单一租户数据量没有十分庞大的时候, 就没有必要对单一租户数据独立到单一数据库. 多个租户数据共享使用一个数库是一个折中的选择.
下图就是对应的数据表结构, 其中 store1 和 store2 使用不同的数据表, 但有同一个表名后缀和相同结构.
实施
项目介绍
本文的项目还是沿用上一篇文章的代码, 进行加以修改. 所以项目中的依赖项还是那些.
但由于代码中有很多命名不好的地方我进行了修改. 并且, 由于代码结构太简单, 对这个示例实现起来不好, 进行了少量的结构优化.
项目中新增的对象有什么:
1. ModelCacheKeyFactory, 这个是 EF core 提供的对象, 主要是要来产生 ModelCacheKey
2. ModelCacheKey, 这个跟 ModelCacheKeyFactory 是一对的, 如果需要自定义的话一般要同时实现他们俩
3. ConnectionResolverOption, 这个是项目自定义的对象, 用于配置. 因为我们项目中现在需要同时支持多种租户数据分离的方式
实施步骤
1. 添加 ITenantDbContext 接口, 它的作用是要来规定 StoreDbContext 中, 必须可以返回 TenantInfo.
- public interface ITenantDbContext
- {
- TenantInfo TenantInfo{get;}
- }
我们同时也需要修改 StoreDbContext 去实现 ITenantDbContext 接口, 并且在构造函数上添加 TenantInfo 的注入
其中 Products 已经不是原来简单的一个 Property, 这里使用 DbSet 来获取对应的对象, 因为表对象还是使用只读 Property 会好点.
新增一个方法的重写 OnModelCreating, 这个方法的主要规定 EF core 的表实体 (本文是 Product) 怎么跟数据库匹配的, 简单来说就是配置.
可以看到表名的规则是 TenantInfo.Name+"_Products"
- public class StoreDbContext : DbContext,ITenantDbContext
- {
- public DbSet<Product> Products => this.Set<Product>();
- public TenantInfo TenantInfo => tenantInfo;
- private readonly TenantInfo tenantInfo;
- public StoreDbContext(DbContextOptions options, TenantInfo tenantInfo) : base(options)
- {
- this.tenantInfo = tenantInfo;
- }
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity<Product>().ToTable(this.tenantInfo.Name + "_Products");
- }
- }
- StoreDbContext
2. 创建 TenantModelCacheKeyFactory 和 TenantModelCacheKey
TenantModelCacheKeyFactory 的作用主要是创建 TenantModelCacheKey 实例. TenantModelCacheKey 的作用是作为一个键值, 标识 dbContext 中的 OnModelCreating 否需要调用.
为什么这样做呢? 因为 ef core 为了优化效率, 避免在 dbContext 每次实例化的时候, 都需要重新构建数据实体模型.
在默认情况下, OnModelCreating 只会调用一次就会存在缓存. 但由于我们创建了 TenantModelCacheKey, 使得我们有机会判断在什么情况下需要重新调用 OnModelCreating
这里是本文中最关键的改动
- using System;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.EntityFrameworkCore.Infrastructure;
- namespace kiwiho.Course.MultipleTenancy.EFcore.API.Infrastructure
- {
- internal sealed class TenantModelCacheKeyFactory<TContext> : ModelCacheKeyFactory
- where TContext : DbContext, ITenantDbContext
- {
- public override object Create(DbContext context)
- {
- var dbContext = context as TContext;
- return new TenantModelCacheKey<TContext>(dbContext, dbContext?.TenantInfo?.Name ?? "no_tenant_identifier");
- }
- public TenantModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies)
- {
- }
- }
- internal sealed class TenantModelCacheKey<TContext> : ModelCacheKey
- where TContext : DbContext, ITenantDbContext
- {
- private readonly TContext context;
- private readonly string identifier;
- public TenantModelCacheKey(TContext context, string identifier) : base(context)
- {
- this.context = context;
- this.identifier = identifier;
- }
- protected override bool Equals(ModelCacheKey other)
- {
- return base.Equals(other) && (other as TenantModelCacheKey<TContext>)?.identifier == identifier;
- }
- public override int GetHashCode()
- {
- var hashCode = base.GetHashCode();
- if (identifier != null)
- {
- hashCode ^= identifier.GetHashCode();
- }
- return hashCode;
- }
- }
- }
- TenantModelCacheKeyFactory & TenantModelCacheKey
3. 添加 ConnectionResolverOption 类和 ConnectionResolverType 枚举.
- using System;
- namespace kiwiho.Course.MultipleTenancy.EFcore.API.Infrastructure
- {
- public class ConnectionResolverOption
- {
- public string Key { get; set; } = "default";
- public ConnectionResolverType Type { get; set; }
- public string ConnectinStringName { get; set; }
- }
- public enum ConnectionResolverType
- {
- Default = 0,
- ByDatabase = 1,
- ByTabel = 2
- }
- }
- ConnectionResolverOption & ConnectionResolverType
4. 调整 MultipleTenancyExtension 的代码结构, 并且添加 2 个扩展函数用于对配置相关的注入.
下面贴出修改过后最主要的 3 个方法
- internal static IServiceCollection AddDatabase<TDbContext>(this IServiceCollection services,
- ConnectionResolverOption option)
- where TDbContext : DbContext, ITenantDbContext
- {
- services.AddSingleton(option);
- services.AddScoped<TenantInfo>();
- services.AddScoped<ISqlConnectionResolver, TenantSqlConnectionResolver>();
- services.AddDbContext<TDbContext>((serviceProvider, options) =>
- {
- var resolver = serviceProvider.GetRequiredService<ISqlConnectionResolver>();
- var dbOptionBuilder = options.UseMySql(resolver.GetConnection());
- if (option.Type == ConnectionResolverType.ByTabel)
- {
- dbOptionBuilder.ReplaceService<IModelCacheKeyFactory, TenantModelCacheKeyFactory<TDbContext>>();
- }
- });
- return services;
- }
- public static IServiceCollection AddTenantDatabasePerTable<TDbContext>(this IServiceCollection services,
- string connectionStringName, string key = "default")
- where TDbContext : DbContext, ITenantDbContext
- {
- var option = new ConnectionResolverOption()
- {
- Key = key,
- Type = ConnectionResolverType.ByTabel,
- ConnectinStringName = connectionStringName
- };
- return services.AddTenantDatabasePerTable<TDbContext>(option);
- }
- public static IServiceCollection AddTenantDatabasePerTable<TDbContext>(this IServiceCollection services,
- ConnectionResolverOption option)
- where TDbContext : DbContext, ITenantDbContext
- {
- if (option == null)
- {
- option = new ConnectionResolverOption()
- {
- Key = "default",
- Type = ConnectionResolverType.ByTabel,
- ConnectinStringName = "default"
- };
- }
- return services.AddDatabase<TDbContext>(option);
- }
- MultipleTenancyExtension functions
其中有一个关键的配置, 需要把上文提到的 TenantModelCacheKeyFactory 配置到 dbOptionBuilder
- if (option.Type == ConnectionResolverType.ByTabel)
- {
- dbOptionBuilder.ReplaceService<IModelCacheKeyFactory,TenantModelCacheKeyFactory<TDbContext>>();
- }
5. 在 TenantSqlConnectionResolver 的 GetConnection 方法中修改逻辑, 让它同时支持按表分离数据和前文的按数据库分离数据
这个类的名字已经改了, 前文的命名不合适. 方法中用到的 option 是 ConnectionResolverOption 类型, 需要加到构造函数.
- public string GetConnection()
- {
- string connectionString = null;
- switch (this.option.Type)
- {
- case ConnectionResolverType.ByDatabase:
- connectionString = configuration.GetConnectionString(this.tenantInfo.Name);
- break;
- case ConnectionResolverType.ByTabel:
- connectionString = configuration.GetConnectionString(this.option.ConnectinStringName);
- break;
- }
- if (string.IsNullOrEmpty(connectionString))
- {
- throw new NullReferenceException("can not find the connection");
- }
- return connectionString;
- }
- 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://www.cnblogs.com/woailibian/p/12317944.html