前言

上一篇文章中,我们介绍了如何根据不同的租户进行数据分离,分离的办法是一个租户一个数据库。

也提到了这种模式还是相对比较重,所以本文会介绍一种更加普遍使用的办法: 按表分离租户。

这样做的好处是什么:

在目前的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 = ,
ByDatabase = ,
ByTabel =
}
}

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

关于本示例的ef core Migration 实操,请参阅我的另一篇文章

EF core (code first) 通过自定义 Migration History 实现多租户使用同一数据库时更新数据库结构

建表脚本

 CREATE TABLE `store1_Products` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(50) CHARACTER SET utf8mb4 NOT NULL,
`Category` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL,
`Price` double DEFAULT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; CREATE TABLE `store2_Products` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(50) CHARACTER SET utf8mb4 NOT NULL,
`Category` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL,
`Price` double DEFAULT NULL,
PRIMARY KEY (`Id`)
) 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

Asp.net core下利用EF core实现从数据实现多租户(2) : 按表分离的更多相关文章

  1. Asp.net core下利用EF core实现从数据实现多租户(1)

    前言 随着互联网的的高速发展,大多数的公司由于一开始使用的传统的硬件/软件架构,导致在业务不断发展的同时,系统也逐渐地逼近传统结构的极限. 于是,系统也急需进行结构上的升级换代. 在服务端,系统的I/ ...

  2. Asp.net core下利用EF core实现从数据实现多租户(3): 按Schema分离 附加:EF Migration 操作

    前言 前段时间写了EF core实现多租户的文章,实现了根据数据库,数据表进行多租户数据隔离. 今天开始写按照Schema分离的文章. 其实还有一种,是通过在数据表内添加一个字段做多租户的,但是这种模 ...

  3. 【asp.net core 系列】8 实战之 利用 EF Core 完成数据操作层的实现

    0. 前言 通过前两篇,我们创建了一个项目,并规定了一个基本的数据层访问接口.这一篇,我们将以EF Core为例演示一下数据层访问接口如何实现,以及实现中需要注意的地方. 1. 添加EF Core 先 ...

  4. EF Core 快速上手——EF Core 入门

    EF Core 快速上手--EF Core 介绍 本章导航 从本书你能学到什么 对EF6.x 程序员的一些话 EF Core 概述 1.3.1 ORM框架的缺点 第一个EF Core应用   本文是对 ...

  5. EF Core 快速上手——EF Core的三种主要关系类型

    系列文章 EF Core 快速上手--EF Core 入门 本节导航 三种数据库关系类型建模 Migration方式创建和习修改数据库 定义和创建应用DbContext 将复杂查询拆分为子查询   本 ...

  6. EF Core下利用Mysql进行数据存储在并发访问下的数据同步问题

    小故事 在开始讲这篇文章之前,我们来说一个小故事,纯素虚构(真实的存钱逻辑并非如此) 小刘发工资后,赶忙拿着现金去银行,准备把钱存起来,而与此同时,小刘的老婆刘嫂知道小刘的品性,知道他发工资的日子,也 ...

  7. ASP.NET Core 中使用EF Core 将实体映射到数据库表的方法(SQL Server)

    前段时间听过一个关于使用ASP.NET Core建立项目的视频.其中使用EF Core映射到数据库的部分是按部就班地学习.今天自己建立项目时,有些步骤已经有一些遗忘.所以写下这篇文章,顺便理清思路. ...

  8. 在ASP.NET Core中通过EF Core实现一个简单的全局过滤查询

    前言 不知道大家是否和我有同样的问题: 一般在数据库的设计阶段,会制定一些默认的规则,其中有一条硬性规定就是一定不要对任何表中的数据执行delete硬删除操作,因为每条数据对我们来说都是有用的,并且是 ...

  9. Asp.net core 学习笔记 ( ef core )

    更新 : 2018-11-26 这里记入一下关于 foreignKey cascade action 默认情况下如果我们使用 data annotation required + foreginkey ...

随机推荐

  1. Spring--2.Spring之IOC--IOC容器的23个实验(2)

    Spring--2.Spring之IOC--IOC容器的23个实验(1) 中的所有实验我都是在同一个工程中进行的,从第十个实验开始,我将新建一个新的工程开始实验. 目前导包还是跟第一个项目一致,bea ...

  2. MapGIS文件如何压缩存盘

    经过多次编辑修改的MapGIS数据,含有大量逻辑上已删除的节点或图元,数据冗余复杂, 在转换过程前应注意一定要采用压缩存盘方式处理,目的是确保编辑状态已删除的数据真正从物理存储层删除,以确保数据的精简 ...

  3. JS的var和let的区别(详细讲解)

    let是ES6新增的,它主要是弥补var的缺陷,你也可以把let看做var的升级版.下面我就来详细讲讲var和let的区别 相同点: var和let都有函数级作用域 不同点: (1)var是全局作用域 ...

  4. cogs 1316. 数列操作B 区间修改 单点查询

    1316. 数列操作B ★★   输入文件:shulieb.in   输出文件:shulieb.out   简单对比时间限制:1 s   内存限制:128 MB [问题描述] 假设有一个大小为 n(n ...

  5. 成功填坑! Java引入QQ登录时,AccessToken [accessToken=, expireIn=];

    主要就是会一直进入下面这一行,也就是accessTokenObj.getAccessToken().equals("") 此时前端显示如下 AccessToken [accessT ...

  6. 【大道至简】NetCore3.1快速开发框架一:搭建框架

    这一章,我们直接创建NetCore3.1的项目 主要分为1个Api项目,和几个类库 解释: 项目——FytSoa.Api:提供前端接口的Api项目 类库——FytSoa.Core:包含了数据库操作类和 ...

  7. 第一个javaWeb项目-注册界面

    基本功能: 实现信息录入到数据库和信息规范检查 题目要求: 项目目录: 网页界面: 程序源码: package Dao; import java.sql.Connection; import java ...

  8. Exception in thread "main" java.lang.AbstractMethodError: org.springframework.boot.context.config.ConfigFileApplicationListener.supportsSourceType(Ljava/lang/Class;)Z

    依赖冲突,查看pom.xml文件 查看parent项目的依赖版本为 <parent> <groupId>org.springframework.boot</groupId ...

  9. [状压DP思路妙题]图

    源自 luhong 大爷的 FJ 省冬令营模拟赛题 Statement 给定一个 \(n\) 个点 \(m\) 条边的图,没有重边与自环 每条边的两端点编号之差不超过 \(12\) 求选出一个非空点集 ...

  10. 美食家App开发日记3

    由于个人原因,感觉Android的学习特别复杂,初次接触,实在难以完成最初设想,所以将最初的设想做减法. 今天学习了ListView控件,将图片和美食名字使用ListView界面显示出来,并学习提升L ...