前言

本文是多租户系列文章的附加操作文章,如果想查看系列中的其他文章请查看下列文章

主线文章

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

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

Asp.net core下利用EF core实现从数据实现多租户(3): 按Schema分离 附加:EF Migration 操作   (强关联文章,建议先阅读

附加文章

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

EF core (code first) 通过自动迁移实现多租户数据分离 :按Schema分离数据   (本文)

实施

项目介绍

本项目是用系列文章的主分支代码进行修改的。目前项目主要支持使用MySql,通过分库,分表实现多租户。

本文需要实现分Schema,MySql不能实现,所以引入了MSSqlServer。

项目依赖

1. .net core app 3.1。在机器上安装好.net core SDK, 版本3.1

2. Mysql. 使用 Pomelo.EntityFrameworkCore.MySql 包, 版本3.1.1

3. MS Sql Server. 使用 Microsoft.EntityFrameworkCore.SqlServer 包,版本3.1.1

4. EF core,Microsoft.EntityFrameworkCore, 版本3.1.1。这里必须要用3.1的,因为ef core3.0是面向.net standard 2.1.

5. EF core design, Microsoft.EntityFrameworkCore.Design, 版本 3.1.1

6. dotnet-ef tool, 版本 3.1.1

关键要点

其实如果读过我之前的EF core 自动迁移的文章,就会发现其实有几个关键点

1. 通过ef core CLI 生成Migration文件,并且在所有版本的Migration文件中添加一个带参数的构造函数

2. 自定义 MigrationByTenantAssembly 类,通过重写 CreateMigration 实现对修改后的Migration文件进行实例化

3. 自定义 __EFMigrationsHistory 的命名和存放位置

实施步骤

1. 运行dotnet-ef命令,生成Migration files

命令:

1 dotnet-ef migrations add init_schema

执行后,会在项目中的Migrations文件夹下生成多个*.cs文件,其实他们也是可执行C#对象

这3个文件中,主要起作用的是*_init_schema.cs这个文件

打开之后我们需要对他进行修改(所有修改内容已进行高亮)

这里修改的主要是:

1.1 新增构造函数,并且在里面添加一个  schema 参数。

1.2 在Up方法中,对调用 EnsureSchema 进行修改,把 schema 变量加在name参数(第16行)

1.3 在Up方法中,对调用 CreateTable 的 schema 参数中添加自定义变量schema (第20行)

1.4 在Down方法中,对调用 DropTable 的 schema 参数中添加自定义变量schema(第39行)

 using Microsoft.EntityFrameworkCore.Migrations;

 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Migrations
{
public partial class init_schema : Migration
{
private readonly string schema;
public init_schema(string schema)
{
this.schema = schema; }
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "dbo." + schema); migrationBuilder.CreateTable(
name: "Products",
schema: "dbo." + schema,
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(maxLength: , nullable: false),
Category = table.Column<string>(maxLength: , nullable: true),
Price = table.Column<double>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Products", x => x.Id);
});
} protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Products",
schema: "dbo." + schema);
}
}
}

2. 添加 MigrationByTenantAssembly 类,同时需要实现 MigrationsAssembly 类和重写 CreateMigration。

 using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Internal; namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure
{
public class MigrationByTenantAssembly : MigrationsAssembly
{
private readonly DbContext context; public MigrationByTenantAssembly(ICurrentDbContext currentContext,
IDbContextOptions options, IMigrationsIdGenerator idGenerator,
IDiagnosticsLogger<DbLoggerCategory.Migrations> logger)
: base(currentContext, options, idGenerator, logger)
{
context = currentContext.Context;
} public override Migration CreateMigration(TypeInfo migrationClass,
string activeProvider)
{
if (activeProvider == null)
throw new ArgumentNullException($"{nameof(activeProvider)} argument is null"); var hasCtorWithSchema = migrationClass
.GetConstructor(new[] { typeof(string) }) != null; if (hasCtorWithSchema && context is ITenantDbContext tenantDbContext)
{
var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), tenantDbContext?.TenantInfo?.Name);
instance.ActiveProvider = activeProvider;
return instance;
} return base.CreateMigration(migrationClass, activeProvider);
}
}
}

MigrationByTenantAssembly

这个类中没有什么特别的,关键在于29~37行。首先需要判断目标 Migration 对象的是否有一个构造函数的参数有且仅有一个string 类型

判断DbContext是否有实现ITenantDbContext接口。

利用 Activator 创建 Migration 实例(把tenant Name传进构造函数)

3. 修改在 MultipleTenancyExtension 类的AddDatabase方法。(所有修改部分已经高亮) (这是非常关键的步骤)

关键点:

必须为 UseMySql 和 UseSqlServer 添加第二个参数,同时定义 __EFMigrationsHistory 的命名和存放位置。注意SqlServer中的schema命名必须跟dbContext中的schema的名字完全相同。由于MySql没有schema的概念,所以MySql中不能加入对应的schema参数。

在最后一行高亮的代码,通过 ReplaceService 替换 dbContext 中默认的 MigrationAssembly 实现类

 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 dbContextManager = serviceProvider.GetService<IDbContextManager>();
var resolver = serviceProvider.GetRequiredService<ISqlConnectionResolver>();
var tenant = serviceProvider.GetService<TenantInfo>(); DbContextOptionsBuilder dbOptionBuilder = null;
switch (option.DBType)
{
case DatabaseIntegration.MySql:
dbOptionBuilder = options.UseMySql(resolver.GetConnection(),
optionBuilder =>
{
if (option.Type == ConnectionResolverType.ByTabel)
{
optionBuilder.MigrationsHistoryTable($"{tenant.Name}__EFMigrationsHistory");
}
});
break;
case DatabaseIntegration.SqlServer:
dbOptionBuilder = options.UseSqlServer(resolver.GetConnection(),
optionBuilder =>
{
if (option.Type == ConnectionResolverType.ByTabel)
{
optionBuilder.MigrationsHistoryTable($"{tenant.Name}__EFMigrationsHistory");
}
if (option.Type == ConnectionResolverType.BySchema)
{
optionBuilder.MigrationsHistoryTable("__EFMigrationsHistory", $"dbo.{tenant.Name}");
}
});
break;
default:
throw new System.NotSupportedException("db type not supported");
}
if (option.Type == ConnectionResolverType.ByTabel || option.Type == ConnectionResolverType.BySchema)
{
dbOptionBuilder.ReplaceService<IModelCacheKeyFactory, TenantModelCacheKeyFactory<TDbContext>>();
dbOptionBuilder.ReplaceService<Microsoft.EntityFrameworkCore.Migrations.IMigrationsAssembly, MigrationByTenantAssembly>();
}
}); return services;
}

4. 修改StoreDbContext 中的 OnModelCreating 方法

 protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// seperate by table
// modelBuilder.Entity<Product>().ToTable(this.tenantInfo.Name + "_Products");
// seperate by Schema
modelBuilder.Entity<Product>().ToTable(nameof(this.Products), "dbo."+this.tenantInfo.Name);
}

5. 修改ProductController的构造函数

 public ProductController(StoreDbContext storeDbContext)
{
this.storeDbContext = storeDbContext;
// this.storeDbContext.Database.EnsureCreated();
this.storeDbContext.Database.Migrate();
}

查看效果

1. 我们还是跟本系列的其他文章一样,分别在store1和store2中添加数据。

其中怎么添加的就不再重复贴图了,简单来说就是调用controller的post方法在数据库中添加数据

查询 store1 的数据

查询 store2 的数据

2. 查看数据库的机构和数据。

这是数据库的结构, 可以看到有4个schema,其中 dbo.store1 和 dbo.store2 是存放我们自己的数据的。

dbo.store1 和 dbo.store2 里面分别有一个__EFMigrationsHistory 表,这个就是EF core自动迁移的版本记录。

store1 中的数据

store2 中的数据

添加迁移版本

读到这里的朋友可能还会有个疑问,觉得我这个自动迁移只做了一个版本,似乎不足以证明这个方案是可以行的。

1. 那我们就简单在Product里面再加一个 Discount 的字段。注意了,新加的字段我建议使用可空类型

 using System.ComponentModel.DataAnnotations;

 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.DAL
{
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; } public double? Discount { get; set; }
}
}

2. 通过dotnet-ef CLI运行命令,添加Migration版本

dotnet-ef migrations add disount_support

可以看到在Migrations 目录下多了2个*.cs文件

我们根据本文前面的步骤依样画瓢,分别添加带参数的构造函数和修改 Up 和 Down 方法

 using Microsoft.EntityFrameworkCore.Migrations;

 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Migrations
{
public partial class disount_support : Migration
{
private readonly string schema;
public disount_support(string schema)
{
this.schema = schema;
} protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<double>(
name: "Discount",
schema: "dbo." + schema,
table: "Products",
nullable: true);
} protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Discount",
schema: "dbo." + schema,
table: "Products");
}
}
}

我们启动项目,调用查询接口。然后在修改 store1 中的Coffee的Discount (这里注意的是,需要先调用接口,再到数据库中修改数据)

下面就是 store1 的查询结果,可以看到Coffee的Discount是0.6 ,就会说咖啡是打六折的

下面我们对比 store1 和 store2 的表结构和数据。发现 store2 中并没有Discount的字段。并且 __EFMigrationsHistory 中只有一条记录

原因是我们还没执行接口,导致EF core的自动迁移并没有生效

我们重新调用 store2 的接口,然后在看数据库表结构。发现 store2 中的Discount已经加上去了

总结

EF core的自动迁移的具体实操步骤就是上文所述了。

我再次重申,EF core的自动迁移并不是必备的,而是选配。 我非常不建议在运行多年并且有多租户结构的项目中使用 EF core 的 code first 模式。

不过假设你的项目已经分离得很不错,或是一个全新项目,我建议大家尝试。

关于代码

本系列的所有文章代码都会上传到github中,目前master是主分支,由于自动迁移部分只是附加内容,所以本文所有代码,请查看分支 EF_code_first_part3

github 地址:

https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/EF_code_first_part3

EF core (code first) 通过自动迁移实现多租户数据分离 :按Schema分离数据的更多相关文章

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

    前言 写这篇文章的原因,其实由于我写EF core 实现多租户的时候,遇到的问题. 具体文章的链接: Asp.net core下利用EF core实现从数据实现多租户(1) Asp.net core下 ...

  2. EF core Code First 简单的使用方法

    好吧,我又回来了,其实一直都想写一篇关于EF core 的文章去记录自己在开发时候遇到的问题. 为什么要使用EF框架呢,因为原始的ADO.NET需要编写大量的数据访问代码,所以使用EF会更方便.但是今 ...

  3. ef core code first from exist db

    目标 为现有数据库生成新的连接,允许只选择部分表 可以处理一些很怪的需求,比如EF升级EF Core(这个可能有其他解),EF.EF Core同时连接一个数据库 我遇到的问题是: 原项目是.net f ...

  4. EF Core中怎么实现自动更新实体的属性值到数据库

    我们在开发系统的时候,经常会遇到这种需求数据库表中的行被更新时需要自动更新某些列. 数据库 比如下面的Person表有一列UpdateTime,这列数据要求在行被更新后自动更新为系统的当前时间. Pe ...

  5. .Net Core+Angular6 学习 第四部分(EF Core(Code First))

    目的: 打算通过EF core 练习从database receive data 显示到UI. 1. 创建一个新的project Model.定义一个 base interface entity以及实 ...

  6. ef core code frist

    https://docs.microsoft.com/zh-cn/ef/core/get-started/aspnetcore/new-db?view=aspnetcore-2.1 1.先创建对应的实 ...

  7. Asp.Net Core WebApi (Swagger+EF Core/Code First)

    Swagger简介: Swagger™的目标是为REST APIs 定义一个标准的,与语言无关的接口,使人和计算机在看不到源码或者看不到文档或者不能通过网络流量检测的情况下能发现和理解各种服务的功能. ...

  8. EF Core ThenInclude 2.0自动完成提示有误,坑了一下

    只要代码正确,可以编译运行的... https://github.com/dotnet/roslyn/issues/8237

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

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

随机推荐

  1. Animator的小记

    前阵子在做动画相关的内容,整理一下Animator. 1.动画切换 1.1状态之间的切换,在状态间连线(Make Transition),并且设置触发条件,代码里调用SetTrigger.SetBoo ...

  2. Asp.Net Core 3.0 Kestrel服务器下 高性能 WebSocket Server

    最近研究.net core 的各种高性能类型,内存池之类的东西,基于kestrel 服务器的websocket ,写个例子练下手 把原生的Websocket用ArrayPool<T>,Me ...

  3. 如何优雅的用策略模式,取代臃肿的 if-else 嵌套,看这篇就够了

    经常听同事抱怨,订单来源又加了一种,代码又要加一层if-else判断,光判断订单来源的if-else就好几百行代码,代码我都不想看了,相信很多同行都有过这样的感受! Java的二十几种设计模式背的滚瓜 ...

  4. 【UWP】使用 LiteDB 存储数据

    序言: 在 UWP 中,常见的存储数据方式基本上就两种.第一种方案是 UWP 框架提供的 ApplicationData Settings 这一系列的方法,适用于存放比较轻量的数据,例如存个 Bool ...

  5. shh登入不能自动执行.bashrc

    在linux 上新安装的anconda来管理软件,把环境变量放在home目录的.bashrc.结果每次开终端,不能直接使用conda.需要手动加环境变量. 用户登入后计算机执行了哪些文件 用户登录时b ...

  6. 个人第4次作业:Alpha测试

    这个作业属于哪个课程 课程 这个作业要求在哪里 作业要求 团队名称 CTRL-IKun 一.测试人员个人信息 姓名 王川 学号 201731021132 所在团队 CTRL-IKun 二.测试情况及结 ...

  7. CentOS7安装MySQL、Tomcat和GitBlit记录

    一.安装MySQL 1.安装这个发布包 yum localinstall mysql-community-release-el6-5.noarch.rpm 可以通过下面的命令来确认这个仓库被成功添加: ...

  8. .net core 常见设计模式-IChangeToken

    场景 一个对象A,希望它的某些状态在发生改变时通知到B(或C.D),常见的做法是在A中定义一个事件(或直接用委托),当状态改变时A去触发这个事件.而B直接订阅这个事件 这种设计有点问题B由于要订阅A的 ...

  9. requests快速构造请求头的方法

    上图请求头内容,内容多不说,也不确认哪些数据是必须的,网上找到一个懒办法 快速一键生成 Python 爬虫请求头 实战演练 抓取网站:https://developer.mozilla.org... ...

  10. CTF--HTTP服务--路径遍历(提升root用户权限)

    开门见山 1. 在上次实验取的靶机低用户权限,查看该系统的内核版本 2. 查看该系统的发行版本 3. 查看该内核版本是否存在内核溢出漏洞,并没有 如果有内核溢出漏洞,则可以 4. 在靶机查看/etc/ ...