EF core (code first) 通过自定义 Migration History 实现多租户使用同一数据库时更新数据库结构
前言
写这篇文章的原因,其实由于我写EF core 实现多租户的时候,遇到的问题。
具体文章的链接:
Asp.net core下利用EF core实现从数据实现多租户(1)
Asp.net core下利用EF core实现从数据实现多租户(2) : 按表分离 (主要关联文章)
这里我遇到的最主要问题是:由于多租户的表使用的是同一个数据库。由于这个原因,无法通过 Database.EnsureCreated() 自动创建多个结构相同但名字不同的表。
所以我在文中提到,需要自己跑脚本去创建多有的表。
虽然我依然认为在多租户的情况下使用sql管理表是更可靠的方案,但如果可以利用EF core原生提供的Migration机制,在运行时自动创建和更新数据表结构,那更加友好。
实现的思路
其实我们都知道,EF core (code first) 会在数据库中生成唯一一个 __EFMigrationHistory 表,数据库的版本记录在这里。
在我们文章的场景下,由于有多个租户同时使用,同一个表结构(Products)会出现多次,那么意思就是一个 __EFMigrationHistory 无法同时记录多个租户的数据表版本。
好了,既然问题的关键已经知道了,我们可以在这里先把答案揭晓,在下问在详细说明实现方法:
图中可以看到,我们自定义MigrationHistory表,并且在一个数据下,同时出现了store1和store2的 MigrationHistory 表。

实施
项目介绍
这是一个多租户系统,具体来说就是根据不同的租户,创建相同的所有数据表。
项目依赖:
1. .net core app 3.1。在机器上安装好.net core SDK, 版本3.1
2. Mysql. 使用 Pomelo.EntityFrameworkCore.MySql 包
3. EF core,Microsoft.EntityFrameworkCore, 版本3.1.1。这里必须要用3.1的,因为ef core3.0是面向.net standard 2.1.
4. EF core design, Microsoft.EntityFrameworkCore.Design, 版本 3.1.1
5. dotnet-ef tool, 版本 3.1.1
关键的对象:
1. MigrationsAssembly, 利用此类去实现创建对应的Migration单元。
2. Migration files, 这里指的是一批Migration相关的文件,利用执行dotnet-ef 命令生成具体的文件,从而真正地去创建和更新数据库。
实施步骤
1. 运行dotnet-ef命令,生成Migration files
命令:
dotnet-ef migrations add init
执行后,会在项目中的Migrations文件夹下生成多个*.cs文件,其实他们也是可执行C#对象
机构如下:

这3个文件中,主要起作用的是*_init.cs这个文件
打开之后我们需要对他进行修改
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Migrations
{
public partial class init : Migration
{
private readonly string prefix;
public init(string prefix)
{
if (string.IsNullOrEmpty(prefix))
{
throw new System.ArgumentNullException();
}
this.prefix = prefix;
} protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: prefix + "_Products",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(maxLength: 50, nullable: false),
Category = table.Column<string>(maxLength: 50, 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: prefix + "_Products");
}
}
}
init migration
这里修改的主要是:
1.1 新增构造函数,并且在里面添加一个 prefix 参数。
1.2 在Up方法中,对table Name进行修改,把prefix变量加在_Product前面(第21行)
1.3 在Down方法中,对table Name进行修改,把prefix变量加在_Product前面 (第39行)
2. 创建 MigrationByTenantAssembly 文件。
由于上一步讲Migration file的构造函数修改了,理论上EF core已经五法通过默认的方式成功执行改Migration file了
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方法中,添加自定义MigrationHistory表名
var dbOptionBuilder = options.UseMySql(resolver.GetConnection(), builder =>
{
if (option.Type == ConnectionResolverType.ByTabel)
{
builder.MigrationsHistoryTable(${tenantInfo.Name}__EFMigrationsHistory");
}
}); dbOptionBuilder.ReplaceService<Microsoft.EntityFrameworkCore.Migrations.IMigrationsAssembly, MigrationByTenantAssembly>();
最关键的一点是第5行,调用 MigrationsHistoryTable 设置MigrationHistory表名
另外一点是第10行,用 MigrationByTenantAssembly 类替换 EF core 中默认的实现(IMigrationsAssembly接口)
4. 在ProductController的构造函数中,修改成如下
Database.Migrate 的作用主要是在运行时可以执行数据库的创建和更新
public ProductController(StoreDbContext storeDbContext)
{
this.storeDbContext = storeDbContext;
this.storeDbContext.Database.Migrate();
}
查看效果
调用接口
跟系列文章一样,我们先调用创建product的接口分别在store1和store2中添加记录。
下面是store1 的查询结果

store2的查询结果

查看数据库验证数据
数据库的表结构

store1_Products 表数据

store2_Products 表数据

总结
本文中我们介绍了ef core 的code first模式下是如何更新数据库的,并且通过添加 Migration 对象的构造函数 ,自行添加了必要参数。
通过替换EF core中默认的 IMigrationsAssembly 实现, MigrationByTenantAssembly 中自定对Migration对象实例化。
替换EF core中默认的MigrationHistory最终实现需求。
本文虽然只是一个示例,但是却可以在真实项目中使用相同的手段以实现需求。不过还是那句话,对于多租户情况下,我推荐使用db first模式。
关于代码
代码已经传上github,请查看EF_code_first的分支的代码。
https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/EF_code_first
参考文章
Custom Migrations History Table
EF core (code first) 通过自定义 Migration History 实现多租户使用同一数据库时更新数据库结构的更多相关文章
- EF core (code first) 通过自动迁移实现多租户数据分离 :按Schema分离数据
前言 本文是多租户系列文章的附加操作文章,如果想查看系列中的其他文章请查看下列文章 主线文章 Asp.net core下利用EF core实现从数据实现多租户(1) Asp.net core下利用EF ...
- EF core Code First 简单的使用方法
好吧,我又回来了,其实一直都想写一篇关于EF core 的文章去记录自己在开发时候遇到的问题. 为什么要使用EF框架呢,因为原始的ADO.NET需要编写大量的数据访问代码,所以使用EF会更方便.但是今 ...
- ef core code first from exist db
目标 为现有数据库生成新的连接,允许只选择部分表 可以处理一些很怪的需求,比如EF升级EF Core(这个可能有其他解),EF.EF Core同时连接一个数据库 我遇到的问题是: 原项目是.net f ...
- .Net Core+Angular6 学习 第四部分(EF Core(Code First))
目的: 打算通过EF core 练习从database receive data 显示到UI. 1. 创建一个新的project Model.定义一个 base interface entity以及实 ...
- ef core code frist
https://docs.microsoft.com/zh-cn/ef/core/get-started/aspnetcore/new-db?view=aspnetcore-2.1 1.先创建对应的实 ...
- Asp.Net Core WebApi (Swagger+EF Core/Code First)
Swagger简介: Swagger™的目标是为REST APIs 定义一个标准的,与语言无关的接口,使人和计算机在看不到源码或者看不到文档或者不能通过网络流量检测的情况下能发现和理解各种服务的功能. ...
- EF数据迁移(当模型改变时更新数据库)
https://msdn.microsoft.com/zh-CN/data/jj591621 Enable-Migrations Add-Migration 名称 Update-Database –V ...
- Asp.net core下利用EF core实现从数据实现多租户(3): 按Schema分离 附加:EF Migration 操作
前言 前段时间写了EF core实现多租户的文章,实现了根据数据库,数据表进行多租户数据隔离. 今天开始写按照Schema分离的文章. 其实还有一种,是通过在数据表内添加一个字段做多租户的,但是这种模 ...
- Asp.net core下利用EF core实现从数据实现多租户(1)
前言 随着互联网的的高速发展,大多数的公司由于一开始使用的传统的硬件/软件架构,导致在业务不断发展的同时,系统也逐渐地逼近传统结构的极限. 于是,系统也急需进行结构上的升级换代. 在服务端,系统的I/ ...
随机推荐
- Java Calendar类(java.util包)
Date 类最主要的作用就是获得当前时间,同时这个类里面也具有设置时间以及一些其他的功能,但是由于本身设计的问题,这些方法却遭到众多批评,不建议使用,更推荐使用 Calendar 类进行时间和日期的处 ...
- vPlayer 模块Demo
本文出自APICloud官方论坛 vPlayer iOS封装了AVPlayer视频播放功能(支持音频播放).iOS 平台上支持的视频文件格式有:WMV,AVI,MKV,RMVB,RM,XVID,MP4 ...
- P1551 亲戚 并查集
P1551 亲戚 题目背景 若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系. 题目描述 规定:x和y是亲戚,y和z是亲戚,那么 ...
- 【DPDK】【ring】从DPDK的ring来看无锁队列的实现
[前言] 队列是众多数据结构中最常见的一种之一.曾经有人和我说过这么一句话,叫做“程序等于数据结构+算法”.因此在设计模块.写代码时,队列常常作为一个很常见的结构出现在模块设计中.DPDK不仅是一个加 ...
- Google搜索成最大入口,简单谈下个人博客的SEO
个人静态博客SEO该考虑哪些问题呢?本篇文章给你答案 咖啡君在开始写文章时首选了微信公众号作为发布平台,但公众号在PC端的体验并不好,连最基本的文章列表都没有,所以就搭建了运维咖啡吧的网站,可以通过点 ...
- 递推 dp - 求有多少个序列符合题意
题目描述 小美有一个由n个元素组成的序列{a1,a2,a3,...,an},她想知道其中有多少个子序列{ap1,ap2,...,apm}(1 ≤ m ≤ n, 1 ≤ p1 < p2 , ...
- ASP.NET Cookie是怎么生成的
ASP.NET Cookie是怎么生成的 可能有人知道Cookie的生成由machineKey有关,machineKey用于决定Cookie生成的算法和密钥,并如果使用多台服务器做负载均衡时,必须指定 ...
- noip2018 考前提醒!
适应Noilinux 1.终端操作 打开终端 \(Ctrl+Alt+T\) 打开文件夹 \(cd\) +名称 新建文件夹 \(mkdir\) +名称 打开 \(vim\) 配置 \(vim ~/.vi ...
- noip2017考前基础复习——数论数学
·最大公约数 gcd 辗转相除法 gcd(a,b)=gcd(b,a%b) int gcd(int x,int y){ ) return x; return gcd(y,x%y); } 效率O(log ...
- mysql中更改字符集为utf8&&mysql中文输入不了问题解决
写给TT:对不起啦!! 嗯,输入不了中文,大多数问题是mysql的字符集设置的问题,当然,别的问题也有可能, 这里我们用两种方法设置mysql的字符集,图形化工具和命令行的方式(一种操作完即可) 一, ...