EF多租户实例:如何快速实现和同时支持多个DbContext
前言
上一篇随笔我们谈到了多租户模式,通过多租户模式的演化的例子。大致归纳和总结了几种模式的表现形式。
并且顺带提到了读写分离。
通过好几次的代码调整,使得这个库更加通用。今天我们聊聊怎么通过该类库快速接入多租户。
类库地址:
https://github.com/woailibain/kiwiho.EFcore.MultiTenant
实施
这次实例的代码,完全引用上面github地址中的 traditional_and_multiple_context 的例子。
从实例的名称可以知道,我们主要演示典型的多组户模式,并且在同一个系统中同时支持多个 DbContext
为什么一个系统要同时支持多个 DbContext
其实回答这个问题还是要回到你们系统为什么要多租户模式上。无非是系统性能瓶颈、数据安全与隔离问题。
1. 系统性能问题,那系统是经过长时间的洗礼的,就是说多租户是系统结构演化的过程。以前的系统,以单体为主,一个系统一个数据库。
要演化, 肯定需要一个过程,所以将一个数据库按业务类型分割成多个数据库就是顺理成章的事情。
2. 数据安全与隔离问题,其实数据安全和隔离,并不需要全部数据都进行隔离。例如,一些公司可能只对自己客户的资料进行隔离,可能只对敏感数据隔离。
那么我们大可按业务分开好几个模块,将敏感的数据使用数据库分离模式隔离数据,对不敏感数据通过数据表模式进行隔离,节省资源。
项目结构
我们先来看看项目结构。分别有2个项目:一个是Api,另一个DAL。
这里涉及到一个问题,为什么要分开Api和DAL。其实是为了模拟当今项目中主流的项目结构,最起码数据层和逻辑操作层是分开的。
Api的结构和引用,可以看到Api几乎引用了MultiTenant的所有包,并且包含DAL。
其实这里的****.MySql ,***.SqlServer和****.Postgre三个包,只需要引用一个即可,由于这个example是同时使用了3个数据库,才需要同时引用多个。
DAL的结构和引用,DAL的引用就相对简单了,只需要引用DAL和Model即可
实施详解
DAL详解
DAL既然是数据层,那么DbContext和Entity是必须的。这里同时有 CustomerDbContext 和 StoreDbContext 。
我们首先看看 StoreDbContext ,它主要包含 Product 产品表。
里面有几个要点:
1. StoreDbContext 必须继承自 TenantBaseDbContext
2. 构造函数中的第一个参数 options ,需要使用泛型 DbContextOptions<> 类型传入。(如果整个系统只有一个DbContext,那么这里可以使用 DbContextOptions 代替)
3. 重写 OnModelCreating 方法。这个并不是必要步骤。但由于大部分 DbContext 都需要通过该方法定义数据库实体结构,所以如果有重写这个方法,必须要显式调用 base.OnModelCreating
4. 公开的属性 Products,代表product表。
public class StoreDbContext : TenantBaseDbContext
{
public DbSet<Product> Products => this.Set<Product>(); public StoreDbContext(DbContextOptions<StoreDbContext> options, TenantInfo tenant, IServiceProvider serviceProvider)
: base(options, tenant, serviceProvider)
{ } protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
现在看看 CustomerDbContext ,它主要包含 Instruction 订单表
这里使用了精简的DbContext实现方式,只包含了公开的Instructions属性和构造函数
public class CustomerDbContext : TenantBaseDbContext
{
public DbSet<Instruction> Instructions => this.Set<Instruction>();
public CustomerDbContext(DbContextOptions<CustomerDbContext> options, TenantInfo tenant, IServiceProvider serviceProvider)
: base(options, tenant, serviceProvider)
{
}
}
剩下的2个类分别是 Product 和 Instruction 。他们没有什么特别的,就是普通Entity
public class Product
{
[Key]
public int Id { get; set; } [StringLength(), Required]
public string Name { get; set; } [StringLength()]
public string Category { get; set; } public double? Price { get; set; }
}
Product
public class Instruction
{
[Key]
public int Id { get; set; } public DateTime Date { get; set; } public double TotalAmount { get; set; } [StringLength()]
public string Remark { get; set; } }
Instruction
Api详解
Startup
Startup作为asp.net core的配置入口,我们先看看这里
首先是ConfigureService 方法。这里主要配置需要使用的服务和注册
1. 我们通过 AddMySqlPerConnection 扩展函数,添加对 StoreDbContext 的使用,使用的利用数据库分离租户间数据的模式
里面配置的 ConnectionPerfix,代表配置文件中前缀是 mysql_ 的连接字符串,可以提供给 StoreDbContext 使用。
2. 通过 AddMySqlPerTable 扩展函数,添加对 CustomerDbContext 的使用,使用的是利用表分离租户间数据的模式。
配置的第一个参数是多租户的键值,这里使用的是customer,注意在多个 DbContext 的情况下,其中一个DbContext必须包含键值
配置的第二个参数是链接字符串的键值,由于多个租户同时使用一个数据库,所以这里只需要配置一个链接字符串
这里可以注意到,我们默认可以提供2中方式配置多租户,分别是 委托 和 参数 。
它们2个使用方式有区别,在不同的模式下都同时支持这2种模式
public void ConfigureServices(IServiceCollection services)
{
// MySql
services.AddMySqlPerConnection<StoreDbContext>(settings=>
{
settings.ConnectionPrefix = "mysql_";
}); services.AddMySqlPerTable<CustomerDbContext>("customer","mysql_default_customer"); services.AddControllers();
}
其次是 Configure 方法。这里主要是配置asp.net core的请求管道
1. 可以看到使用了好几个asp.net core的中间件,其中 UseRouting 和 UseEndpoint 是必要的。
2. 使用 UserMiddleware 扩展函数引入我们的中间件 TenantInfoMiddleware 。这个中间件是类库提供的默认支持。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.UseMiddleware<TenantInfoMiddleware>(); app.UseRouting(); app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
appsettings
修改appsettings这个文件,主要是为了在里面添加链接字符串
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings":{
"mysql_default":"server=127.0.0.1;port=3306;database=multi_tenant_default;uid=root;password=gh001;charset=utf8mb4",
"mysql_store1":"server=127.0.0.1;port=3306;database=multi_tenant_store1;uid=root;password=gh001;charset=utf8mb4",
"mysql_store2":"server=127.0.0.1;port=3306;database=multi_tenant_store2;uid=root;password=gh001;charset=utf8mb4", "mysql_default_customer":"server=127.0.0.1;port=3306;database=multi_tenant_customer;uid=root;password=gh001;charset=utf8mb4"
}
}
ProductController 和 InstructionController
productController 和 InstructionController 非常相似,他们的里面主要包含3个方法,分别是:查询所有、根据Id查询、添加
里面的代码就不一一解释了
namespace kiwiho.EFcore.MultiTenant.Example.Api.Controllers
{
[ApiController]
[Route("api/[controller]s")]
public class ProductController : ControllerBase
{
private readonly StoreDbContext storeDbContext; public ProductController(StoreDbContext storeDbContext)
{
this.storeDbContext = storeDbContext;
this.storeDbContext.Database.EnsureCreated(); // this.storeDbContext.Database.Migrate();
} [HttpPost("")]
public async Task<ActionResult<Product>> Create(Product product)
{
var rct = await this.storeDbContext.Products.AddAsync(product); await this.storeDbContext.SaveChangesAsync(); return rct?.Entity; } [HttpGet("{id}")]
public async Task<ActionResult<Product>> Get([FromRoute] int id)
{ var rct = await this.storeDbContext.Products.FindAsync(id); return rct; } [HttpGet("")]
public async Task<ActionResult<List<Product>>> Search()
{
var rct = await this.storeDbContext.Products.ToListAsync();
return rct;
}
}
}
ProductController
namespace kiwiho.EFcore.MultiTenant.Example.Api.Controllers
{
[ApiController]
[Route("api/[controller]s")]
public class InstructionController : ControllerBase
{
private readonly CustomerDbContext customerDbContext;
public InstructionController(CustomerDbContext customerDbContext)
{
this.customerDbContext = customerDbContext;
this.customerDbContext.Database.EnsureCreated();
} [HttpPost("")]
public async Task<ActionResult<Instruction>> Create(Instruction instruction)
{
var rct = await this.customerDbContext.Instructions.AddAsync(instruction); await this.customerDbContext.SaveChangesAsync(); return rct?.Entity; } [HttpGet("{id}")]
public async Task<ActionResult<Instruction>> Get([FromRoute] int id)
{ var rct = await this.customerDbContext.Instructions.FindAsync(id); return rct; } [HttpGet("")]
public async Task<ActionResult<List<Instruction>>> Search()
{
var rct = await this.customerDbContext.Instructions.ToListAsync();
return rct;
}
}
}
InstructionController
实施概括
实施过程中我们总共做了4件事:
1. 定义 DbContext 和对应的 Entity . DbContext必须继承 TenantBaseDbContext 。
2. 修改 Startup 类,配置多租户的服务,配置多租户需要使用的中间件。
3. 按照规则添加字符串。
4. 添加 Controller 。
检验结果
检验结果之前,我们需要一些原始数据。可以通过数据库插入或者调用api生成
使用 store1 查询 Product 的数据
使用 store2 查询 Product 的数据
使用 store1 查询 Instruction 的数据
使用 store2 查询 Instruction 的数据
总结
通过上述步骤,已经可以看出我们能通过简单的配置,就实施多租户模式。
这个例子有什么缺陷:
大家应该能发现,实例中Store和Customer都使用了store1和store2来请求数据。但是Customer这个域,很明显是需要用customer1和customers2等等去请求数据的。
本实例主要为了简单明了,将他们混为一谈的。
但是要解决这个事情,是可以达到的。我们将在日后的文章继续。
之后的还会有什么例子:
既然上一篇随笔提到了多租户的演化和读写分离,那么我们将会优先讲到这部分内容。
通过查看github源代码,可能有人疑问,除了MySql,SqlServer和Postgre,是不是就不能支持别的数据库了。
其实并不是的,类库里已经做好一定的扩展性,各位可以通过自行使用UseOracle等扩展方法把Oracle集成进来,代码仅需不到10行。
代码怎么看:
代码已经全部更新到github,其中本文事例代码在example/traditional_and_multiple_context 内
https://github.com/woailibain/kiwiho.EFcore.MultiTenant
EF多租户实例:如何快速实现和同时支持多个DbContext的更多相关文章
- EF多租户实例:快速实现分库分表
前言 来到这篇随笔,我们继续演示如何实现EF多租户. 今天主要是演示多租户下的变形,为下图所示 实施 项目结构 这次我们的示例项目进行了精简,仅有一个API项目,直接包含所有代码. 其中Control ...
- EF多租户实例:演变为读写分离
前言 我又来写关于多租户的内容了,这个系列真够漫长的. 如无意外这篇随笔是最后一篇了.内容是讲关于如何利用我们的多租户库简单实现读写分离. 分析 对于读写分离,其实有很多种实现方式,但是总体可以分以下 ...
- Wcf+EF框架搭建实例
一.最近在使用Wcf创建数据服务,但是在和EF框架搭建数据访问时遇到了许多问题 下面是目前整理的基本框架代码,经供参考使用,源代码地址:http://git.oschina.net/tiama3798 ...
- EF调用存储过程实例
创建实体: public class User { public string UserID { get; set; } public string UserName { get; set; } pu ...
- eureka 服务实例实现快速下线快速感知快速刷新配置解析
Spirng Eureka 默认配置解读 默认的Spring Eureka服务器,服务提供者和服务调用者配置不够灵敏,总是服务提供者在停掉很久之后,服务调用者很长时间并没有感知到变化.或者是服务已经注 ...
- Emmet语法实例(帮助快速开发)
写完命令后按键 tab 就可以生成了. 应用于大多数已经内置或可以安装emmet的编辑器下级元素命令 > <!--div>p--> <div> <p>& ...
- Ef+T4模板实现代码快速生成器
转载请注明地址:http://www.cnblogs.com/cainiaodage/p/4953601.html 效果如图,demo(点击demo可下载案例) 项目结构如图 T4BLL添加BLL.t ...
- python实例:快速找出多个字典中的公共键
1.生成随机字典 # 从abcdefg 中随机取出 3-6个,作为key, 1-4 的随机数作为 value s1 = {x : randint(1, 4) for x in sample('abcd ...
- EF CodeFirst简单实例
运行环境:VS2012,添加的EntityFramework为6.0.2 版本不用太关心,只要知道原理就行了: 基本代码就这几行: namespace ConsoleApplication1 { ...
随机推荐
- cannot be found on object of type xx.CacheExpressionRootObject
0 环境 系统环境:win10 编辑器:IDEA 1 前言->环境搭建 1-1 pom依赖 <?xml version="1.0" encoding="UTF ...
- 82)PHP,基本框架类步骤
framework.class.php 基本代码展示: <?php /** * 框架初始化功能类 */ class Framework { /** * 入口 * 里面的static和self是一 ...
- es6变量和函数的提升、暂时性死区?
es6变量和函数的提升.暂时性死区?
- FPGA设计思想之串并转换
数据流中,用面积换速度-串行转并行的操作 并行转串行数据输出:采用计数方法,将并行的数据的总数先表示出来,然后发送一位数据减一,后面的接收的这样表示: data_out <= data[cnt] ...
- python练习题——猜数字游戏
增加了按照对半找数的方法来计算最短几次就可以猜到随机数,决定到游戏结束共猜数的次数: from random import * import numpy as np from numpy import ...
- idea常见需求
1.给class加注释模板 /** *@ClassName ${NAME} *@Description TODO *@Author xxx *@Date ${DATE} ${TIME} *@Versi ...
- 吴裕雄--天生自然 R语言开发学习:回归(续一)
#------------------------------------------------------------# # R in Action (2nd ed): Chapter 8 # # ...
- pytorch的visdom启动不了、蓝屏
pytorch的visdom启动不了.蓝屏 问题描述:我是在ubuntu16.04上用python3.5安装的visdom.可是启动是蓝屏:在网上找了很久的解决方案:有三篇博文: https://bl ...
- 三年无限流量免费随身WiFi充电宝是真的还是套路?
一般来说大家现在看到"无限流量"."免费"等字眼,总会有一种"这是陷阱"."这是大坑"."就要黑你钱" ...
- Vacant Seat(Atcoder-C-交互式题目)
C - Vacant Seat Time limit : 2sec / Memory limit : 256MB Score : 500 points Problem Statement This i ...