前言

写这篇文章的原因,其实由于我写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

命令:

  1. dotnet-ef migrations add init

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

机构如下:

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

打开之后我们需要对他进行修改

  1. using Microsoft.EntityFrameworkCore.Metadata;
  2. using Microsoft.EntityFrameworkCore.Migrations;
  3.  
  4. namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Migrations
  5. {
  6. public partial class init : Migration
  7. {
  8. private readonly string prefix;
  9. public init(string prefix)
  10. {
  11. if (string.IsNullOrEmpty(prefix))
  12. {
  13. throw new System.ArgumentNullException();
  14. }
  15. this.prefix = prefix;
  16. }
  17.  
  18. protected override void Up(MigrationBuilder migrationBuilder)
  19. {
  20. migrationBuilder.CreateTable(
  21. name: prefix + "_Products",
  22. columns: table => new
  23. {
  24. Id = table.Column<int>(nullable: false)
  25. .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
  26. Name = table.Column<string>(maxLength: 50, nullable: false),
  27. Category = table.Column<string>(maxLength: 50, nullable: true),
  28. Price = table.Column<double>(nullable: true)
  29. },
  30. constraints: table =>
  31. {
  32. table.PrimaryKey("PK__Products", x => x.Id);
  33. });
  34. }
  35.  
  36. protected override void Down(MigrationBuilder migrationBuilder)
  37. {
  38. migrationBuilder.DropTable(
  39. name: prefix + "_Products");
  40. }
  41. }
  42. }

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了

  1. using System;
  2. using System.Reflection;
  3. using Microsoft.EntityFrameworkCore;
  4. using Microsoft.EntityFrameworkCore.Diagnostics;
  5. using Microsoft.EntityFrameworkCore.Infrastructure;
  6. using Microsoft.EntityFrameworkCore.Migrations;
  7. using Microsoft.EntityFrameworkCore.Migrations.Internal;
  8.  
  9. namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure
  10. {
  11. public class MigrationByTenantAssembly : MigrationsAssembly
  12. {
  13. private readonly DbContext context;
  14.  
  15. public MigrationByTenantAssembly(ICurrentDbContext currentContext,
  16. IDbContextOptions options, IMigrationsIdGenerator idGenerator,
  17. IDiagnosticsLogger<DbLoggerCategory.Migrations> logger)
  18. : base(currentContext, options, idGenerator, logger)
  19. {
  20. context = currentContext.Context;
  21. }
  22.  
  23. public override Migration CreateMigration(TypeInfo migrationClass,
  24. string activeProvider)
  25. {
  26. if (activeProvider == null)
  27. throw new ArgumentNullException($"{nameof(activeProvider)} argument is null");
  28.  
  29. var hasCtorWithSchema = migrationClass
  30. .GetConstructor(new[] { typeof(string) }) != null;
  31.  
  32. if (hasCtorWithSchema && context is ITenantDbContext tenantDbContext)
  33. {
  34. var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), tenantDbContext?.TenantInfo?.Name);
  35. instance.ActiveProvider = activeProvider;
  36. return instance;
  37. }
  38.  
  39. return base.CreateMigration(migrationClass, activeProvider);
  40. }
  41. }
  42. }

MigrationByTenantAssembly

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

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

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

3. 在 MultipleTenancyExtension 类的AddDatabase方法中,添加自定义MigrationHistory表名

  1. var dbOptionBuilder = options.UseMySql(resolver.GetConnection(), builder =>
  2. {
  3. if (option.Type == ConnectionResolverType.ByTabel)
  4. {
  5. builder.MigrationsHistoryTable(${tenantInfo.Name}__EFMigrationsHistory");
  6. }
  7. });
  8.  
  9. dbOptionBuilder.ReplaceService<Microsoft.EntityFrameworkCore.Migrations.IMigrationsAssembly, MigrationByTenantAssembly>();

最关键的一点是第5行,调用 MigrationsHistoryTable 设置MigrationHistory表名

另外一点是第10行,用 MigrationByTenantAssembly 类替换 EF core 中默认的实现(IMigrationsAssembly接口)

4. 在ProductController的构造函数中,修改成如下

Database.Migrate 的作用主要是在运行时可以执行数据库的创建和更新

  1. public ProductController(StoreDbContext storeDbContext)
  2. {
  3. this.storeDbContext = storeDbContext;
  4. this.storeDbContext.Database.Migrate();
  5. }

查看效果

调用接口

跟系列文章一样,我们先调用创建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 实现多租户使用同一数据库时更新数据库结构的更多相关文章

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

    前言 本文是多租户系列文章的附加操作文章,如果想查看系列中的其他文章请查看下列文章 主线文章 Asp.net core下利用EF core实现从数据实现多租户(1) Asp.net core下利用EF ...

  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. .Net Core+Angular6 学习 第四部分(EF Core(Code First))

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

  5. ef core code frist

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

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

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

  7. EF数据迁移(当模型改变时更新数据库)

    https://msdn.microsoft.com/zh-CN/data/jj591621 Enable-Migrations Add-Migration 名称 Update-Database –V ...

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

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

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

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

随机推荐

  1. 0x80070035找不到网络路径

    如果这个报错发生:自己的网络正常,其他人可以正常访问服务器,而自己无法访问服务器.原因就是TCP/IP NetBIOS Helper服务被停止. 打开services.msc,启动此服务即可. 该服务 ...

  2. 《【面试突击】— Redis篇》-- Redis的主从复制?哨兵机制?

    能坚持别人不能坚持的,才能拥有别人未曾拥有的.关注左上角编程大道公众号,让我们一同坚持心中所想,一起成长!! <[面试突击]— Redis篇>-- Redis的主从复制?哨兵机制? 在这个 ...

  3. 动态内存分配(C++)

    C++中的动态内存分配 C++中通过new关键字进行动态内存分配 C++中的动态内存申请是基于类型进行的 delet关键字用于内存释放 //变量申请 Type*pointer = new Type; ...

  4. dp - 求符合题意的序列的个数

    The sequence of integers a1,a2,…,ak is called a good array if a1=k−1 and a1>0. For example, the s ...

  5. ELK学习实验018:filebeat收集docker日志

    Filebeat收集Docker日志 1 安装docker [root@node4 ~]# yum install -y yum-utils device-mapper-persistent-data ...

  6. 菜鸟学习Fabric源码学习 — kafka共识机制

    Fabric 1.4源码分析 kafka共识机制 本文档主要介绍kafka共识机制流程.在查看文档之前可以先阅览raft共识流程以及orderer服务启动流程. 1. kafka 简介 Kafka是最 ...

  7. ThinkPad全家族机型对比

    如图所示

  8. java intellij 工具的简单用法

    一.目录结构 1.新建项目(Empty Project) ->  新建module(可以有多个) => 出来src文件夹 -> 在src文件夹中新建package -> 在pa ...

  9. python sys.modules 和 sys.path 及 __name__

    1.sys.modules 存放已经缓存的模块 值是dict 2.sys.path 搜索路径 值是list 3.if __name__= __main__ 可以看成python的程序入口,如果直接执行 ...

  10. Dart语言学习( 一) 为什么学习Dart?

    为什么学习Dart? Google及全球的其他开发者,使用 Dart 开发了一系列高质量. 关键的 iOS.Android 和 web 应用. Dart 非常适合移动和 web 应用的开发. 高效 D ...