前言

上一篇随笔我们谈到了多租户模式,通过多租户模式的演化的例子。大致归纳和总结了几种模式的表现形式。

并且顺带提到了读写分离。

通过好几次的代码调整,使得这个库更加通用。今天我们聊聊怎么通过该类库快速接入多租户。

类库地址:

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的更多相关文章

  1. EF多租户实例:快速实现分库分表

    前言 来到这篇随笔,我们继续演示如何实现EF多租户. 今天主要是演示多租户下的变形,为下图所示 实施 项目结构 这次我们的示例项目进行了精简,仅有一个API项目,直接包含所有代码. 其中Control ...

  2. EF多租户实例:演变为读写分离

    前言 我又来写关于多租户的内容了,这个系列真够漫长的. 如无意外这篇随笔是最后一篇了.内容是讲关于如何利用我们的多租户库简单实现读写分离. 分析 对于读写分离,其实有很多种实现方式,但是总体可以分以下 ...

  3. Wcf+EF框架搭建实例

    一.最近在使用Wcf创建数据服务,但是在和EF框架搭建数据访问时遇到了许多问题 下面是目前整理的基本框架代码,经供参考使用,源代码地址:http://git.oschina.net/tiama3798 ...

  4. EF调用存储过程实例

    创建实体: public class User { public string UserID { get; set; } public string UserName { get; set; } pu ...

  5. eureka 服务实例实现快速下线快速感知快速刷新配置解析

    Spirng Eureka 默认配置解读 默认的Spring Eureka服务器,服务提供者和服务调用者配置不够灵敏,总是服务提供者在停掉很久之后,服务调用者很长时间并没有感知到变化.或者是服务已经注 ...

  6. Emmet语法实例(帮助快速开发)

    写完命令后按键 tab 就可以生成了. 应用于大多数已经内置或可以安装emmet的编辑器下级元素命令 > <!--div>p--> <div> <p>& ...

  7. Ef+T4模板实现代码快速生成器

    转载请注明地址:http://www.cnblogs.com/cainiaodage/p/4953601.html 效果如图,demo(点击demo可下载案例) 项目结构如图 T4BLL添加BLL.t ...

  8. python实例:快速找出多个字典中的公共键

    1.生成随机字典 # 从abcdefg 中随机取出 3-6个,作为key, 1-4 的随机数作为 value s1 = {x : randint(1, 4) for x in sample('abcd ...

  9. EF CodeFirst简单实例

    运行环境:VS2012,添加的EntityFramework为6.0.2 版本不用太关心,只要知道原理就行了: 基本代码就这几行: namespace ConsoleApplication1 {    ...

随机推荐

  1. SSH(struts+spring+hibernate)常用配置整理

    SSH(struts+spring+hibernate)常用配置整理 web.xml配置 <?xml version="1.0" encoding="UTF-8&q ...

  2. python数据类型:列表List和Set

    python数据类型:列表List, Set集合 序列是Python中最基本的数据结构 序列中每个元素都分配一个数字,表示索引 列表的数据项不需要具有相同的类型        列表中的值可以重复并且有 ...

  3. 数字签名和数字证书等openssl加密基本术语

    openssl 算法基础 1.1 对称算法 : 密钥相同,加密解密使用同一密钥 1.2 摘要算法:无论用户输入的数据什么长度,输出的都是固定长度的密文:即信息摘要,可用于判断数据的完整性:目前常用的有 ...

  4. vue项目中net::ERR_CONNECTION_TIMED_OUT错误

    我出错的原因时network地址与我本机ip地址不一致 Network: http://192.168.13.30:8080/ 处理方法: 在vue项目中新建一个vue.config.js文件 配置上 ...

  5. mysql 优化配置和方面

    MySQL性能优化的参数简介 公司网站访问量越来越大,MySQL自然成为瓶颈,因此最近我一直在研究 MySQL 的优化,第一步自然想到的是 MySQL 系统参数的优化,作为一个访问量很大的网站(日20 ...

  6. 从源码看commit和commitAllowingStateLoss方法区别

    Fragment介绍 在很久以前,也就是我刚开始写Android时(大约在2012年的冬天--),那时候如果要实现像下面微信一样的Tab切换页面,需要继承TabActivity,然后使用TabHost ...

  7. NIO详解

    目录 NIO 前言 IO与NIO的区别 Buffer(缓冲区) Channel(通道) Charset(字符集) NIO遍历文件 NIO 前言 NIO即New IO,这个库是在JDK1.4中才引入的. ...

  8. 默认的Settings.xml文件(无修改过)-Maven

    Tip: 当什么都不作修改时,默认是从Maven中央仓库进行下载,https://repo.maven.apache.org/maven2. 打开maven源码下的lib文件夹,找到maven-mod ...

  9. Leetcode刷题记录 旋转矩阵

    https://leetcode-cn.com/problems/spiral-matrix/submissions/ class Solution(object): def spiralOrder( ...

  10. Python建立web静态服务器

    原文地址:http://www.bugingcode.com/blog/python_html_web_server.html python作为工具,提供了很多好用的命令,比如有时候突然需要建立一个解 ...