EasySharding.EFCore 如何设计使用一套代码完成的EFCore Migration 构建Saas系统多租户不同业务需求且满足租户自定义分库分表、数据迁移能力?
下面用一篇文章来完成这些事情
多租户系统的设计单纯的来说业务,一套Saas多租户的系统,面临很多业务复杂性,不同的租户存在不同的业务需求,大部分相同的表结构,那么如何使用EFCore来完成这样的设计呢?满足不同需求的数据库结构迁移
这里我准备设计一套中间件来完成大部分分库分表的工作,然后可以通过自定义的Migration 数据库文件来迁移构建不同的租户数据库和表,抛开业务处理不谈,单纯提供给业务处理扩展为前提的设计,姑且把这个中间件命名为:
EasySharding
原理:数据库Migation创建是利用 ModelCacheKeyFactory监控ModelCacheKey的模型是否存在变化来完成Create,并不是每次都需要Create
ModelCacheKeyFactory 通过 ModelCacheKey 的 Equals 方法返回的 Bool值 来确定是否需要Create
所以我们通过自定的类ShardingModelCacheKeyFactory来重写ModelCacheKeyFactory 的Create方法 ,ShardingModelCacheKey来重写ModelCacheKey的Equal方法
public class ShardingModelCacheKeyFactory<T> : ModelCacheKeyFactory where T : DbContext, IShardingDbContext
{ public ShardingModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies)
{
}
/// <summary>
/// 重写创建
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override object Create(DbContext context)
{
var dbContext = context as T;
var key = string.Format("{0}{1}", dbContext?.ShardingInfo?.DatabaseTagName, dbContext?.ShardingInfo?.StufixTableName);
var strchangekey = string.IsNullOrEmpty(key) ? "0" : key;
return new ShardingModelCacheKey<T>(dbContext, strchangekey);
} }
ShardingModelCacheKeyFactory
internal class ShardingModelCacheKey<T> : ModelCacheKey where T : DbContext, IShardingDbContext
{
private readonly T _context;
/// <summary>
/// _hashchangedid
/// </summary>
private readonly string _hashchangedid;
public ShardingModelCacheKey(T context, string hashchangedid) : base(context)
{
this._context = context;
this._hashchangedid = hashchangedid;
} public override int GetHashCode()
{ var hashCode = base.GetHashCode(); if (_hashchangedid != null)
{
hashCode ^= _hashchangedid.GetHashCode();
}
else
{
//构成已经默认了 一般不得出发异常
throw new Exception("this is no tenantid");
} return hashCode;
} /// <summary>
/// 判断模型更改缓存是否需要创建Migration
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
protected override bool Equals(ModelCacheKey other)
{
return base.Equals(other) && (other as ShardingModelCacheKey<T>)?._hashchangedid == _hashchangedid;
}
}
ShardingModelCacheKey
设计一个变量值,通过记录并比较 _hashchangedid 一个改变的标识的hashcode值来确定,所以后续只需要 ModelCacheKey Equal 的返回值 来告诉你什么时候应该发生Migration数据迁移创建了,为后续的业务提供支持
做到这一步骤看起来一切都是ok的,然而有很多问题?怎么按照租户去生成库或表,不同租户表结构不同怎么办?Migration迁移文件怎么维护?多个租户Migration交错混乱怎么办?
为了满足不同租户的需求,为此设计了一个ShardingInfo的类对租户提供可改变的数据库上下文对象以及数据库或表区别的类来告诉ModelCacheKey 不同的变化,为了提供更多的场景,这里提供租户 可以分库,可以分表、亦可以区分数据行,都需要结合业务实现,这里不做过多讨论
/// <summary>
/// 黎又铭 2021.10.30
/// </summary>
public class ShardingInfo
{
/// <summary>
/// 数据库地址 租户可分库
/// </summary>
public string ConStr { get; set; }
/// <summary>
/// 数据库名称标识,分库特殊意义,唯一,结合StufixTableName来识别Migration文件变化缓存hashcode值
/// </summary>
public string DatabaseTagName { get; set; }
/// <summary>
/// 分表处理 租户可分表
/// </summary>
public string StufixTableName { get; set; }
/// <summary>
/// 租户也可分数据行
/// </summary>
public string TenantId { get; set; }
}
设计这个类很有必要,第一为了提供给数据库上下文扩展变化,第二利用它的字段来确定变化,第三后续根据它来完成Migiration的差异变化
接下来就是需要根据ShardingInfo的变化来创建不同的的表结构了,怎么来实现呢?
添加模型类,通过命令生成Migration迁移文件,程序第一次生成不会有错,而当我们的ShardingInfo发生变化出发CreateMigration的时候表就会存在发生二次创建错误,原因是我们记录了创建变化而没有记录Migration文件的变化关联
所以这里还需要处理一个关键类,重写MigrationsAssembly的CreateMigration方法,将ShardingInfo变化告诉Migration文件,所以要做到这一步骤,还需要对每次数据迁移变化的Migration文件进行改造以及CodeFirst中自定义的数据表结构稍微修改下
public ShardingMigrationAssembly(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(ShardingInfo) }) != null; if (hasCtorWithSchema && context is IShardingDbContext tenantDbContext)
{
var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), tenantDbContext?.ShardingInfo);
instance.ActiveProvider = activeProvider;
return instance;
}
return base.CreateMigration(migrationClass, activeProvider); }
ShardingMigrationAssembly
将变化告诉Migration迁移文件,好在迁移文件中做对于修改 下面是一个Demo Migrationi迁移文件
public partial class initdata : Migration
{
private readonly ShardingInfo _shardingInfo;
public initdata(ShardingInfo shardingInfo)
{
_shardingInfo = shardingInfo;
}
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4"); migrationBuilder.CreateTable(
name: $"OneDayTable{_shardingInfo.GetName()}" ,
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Day = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_OneDayTables", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4"); migrationBuilder.CreateTable(
name: $"Test{_shardingInfo.GetName()}",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
TestName = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Tests", x => x.Id);
}
// schema: _shardingInfo.StufixTableName
)
.Annotation("MySql:CharSet", "utf8mb4");
} protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: $"OneDayTable{_shardingInfo.GetName()}"); migrationBuilder.DropTable(
name: $"Test{_shardingInfo.GetName()}");
}
}
initdata
通过构造函数接受迁移变化类,就可以告诉它不同的变化生成不同的表了
做到这里似乎可以生成了,这里还需要注意Migraion文件迁移表__efmigrationshistory的变化问题,需要为不同的表结构变化生成不同的 __efmigrationshistory 历史记录表,防止同一套系统中不同的表结构迁移被覆盖的情况
需要注意的是这里的记录表需要结合变化类ShardingInfo文件来完成
builder.MigrationsHistoryTable($"__EFMigrationsHistory_{base.ShardingInfo.GetName()}");
可以参见下这里:https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/history-table
我还不得不为它提供一些方便的扩展方法来更好的完成这个操作,例如 IEntityTypeConfiguration 的处理,可能就是为了少写结构构造函数,尴尬~
public class ShardingEntityTypeConfigurationAbstract<T> : IEntityTypeConfiguration<T> where T : class
{
/// <summary>
/// 这个字段将提供扩展自定义规则的后缀名称
/// </summary>
public string _suffix { get; set; } = string.Empty; public ShardingEntityTypeConfigurationAbstract(string suffix)
{
this._suffix = suffix; }
public ShardingEntityTypeConfigurationAbstract()
{
}
public virtual void Configure(EntityTypeBuilder<T> builder)
{ }
}
ShardingEntityTypeConfigurationAbstract
public class TestMap : ShardingEntityTypeConfigurationAbstract<Test>
{ public override void Configure(EntityTypeBuilder<Test> builder)
{
builder.ToTable($"Test"+ _suffix); }
}
TestMap
最后为了支持EFCore的查询我们还需要处理查询DbSet<Test> 属性,为了让我们在调用查询的时候不用去考虑当前分表的是哪一个分表,只需要关注 Tests本身 不用去管变化,扩展了ToTableSharding的方法
public DbSet<Test> Tests { get; set; }
结合上诉处理,我在模型创建重载里面这样处理如下:自定义的模型对象关系配置
modelBuilder.ApplyConfiguration(new ShardingEntityTypeConfigurationAbstract<Test>(ShardingInfo.GetName()));
modelBuilder.ApplyConfiguration(new ShardingEntityTypeConfigurationAbstract<OneDayTable>(ShardingInfo.GetName()));
modelBuilder.ApplyConfiguration(new ShardingEntityTypeConfigurationAbstract<TestTwo>());
查询对象表关系配置
modelBuilder.Entity<Test>().ToTableSharding("Test", ShardingInfo);
modelBuilder.Entity<OneDayTable>().ToTableSharding("OneDayTable", ShardingInfo);
modelBuilder.Entity<TestTwo>().ToTable("TestTwo");
这个时候我们是不是该注入DbContext上下文对象了,这里修改了一些东西,定义业务上下文对象BusinessContext ,这里需要继承分库上下文对象
public class BusinessContext : ShardingDbContext
{ IConfiguration _configuration; #region Migrations 修改文件 #endregion
public DbSet<Test> Tests { get; set; }
public DbSet<OneDayTable> OneDayTables { get; set; } public DbSet<TestTwo> TestTwos { get; set; }
public BusinessContext(DbContextOptions<BusinessContext> options, ShardingInfo shardingInfo, IConfiguration configuration) : base(options, shardingInfo)
{
_configuration = configuration;
} public BusinessContext(ShardingInfo shardingInfo, IConfiguration configuration) : base(shardingInfo)
{
_configuration = configuration;
} }
BusinessContext
这里我提供了一个构造函数来接受创建自定义变化的上下文对象,并且在扩展变化的构造函数ShardingDbContext中执行了一次Migrate来促发更改迁移,这个自定义的上下文对象在创建的时候促发Migrate,然后根据传递的变化文件的hashcode值来确定是否需要CreateMigration操作
为了让调用看起来不会有那么的new BusinessContext(new Sharding{ });这样的操作,何况存在多个数据库上下文对象的情况,这样就不漂亮了,所以稍加修改了下:
public class ShardingConnection<TContext> where TContext : DbContext
{
public TContext con;
public ShardingConnection(TContext context)
{
con = context;
}
public TContext GetContext()
{
return con;
}
public TContext GetShardingContext(ShardingInfo shardingInfo)
{ return (TContext)Activator.CreateInstance(typeof(TContext), shardingInfo,null); }
}
ShardingConnection
定义了ShardingConnection泛型类来获取未区分的表结构连接或创建区分的表结构上下文对象,让后DI注入下,这样写起来好像就ok了
下面来实现看看,这里定义了分库 两个库easysharding 和 easysharding1 ,easysharding 按 oranizeA 分了表, easysharding1 按 oranizeB 和当前日期分了表
贴上测试代码:
public class HomeController : Controller
{
ShardingConnection<BusinessContext> context;
public HomeController(ShardingConnection<BusinessContext> shardingConnection)
{
context = shardingConnection; }
[HttpGet]
public IActionResult Index(string id)
{
var con = context.GetContext();
con.Tests.Add(new MySql.Test { Id = new Random().Next(0, 10000), TestName = "11111111" });
con.SaveChanges();
var c = con.Tests.AsNoTracking().ToList(); var con1 = context.GetShardingContext(new ShardingInfo
{
DatabaseTagName = $"easysharding",
StufixTableName = $"oranizeA",
ConStr = $"server=192.168.0.208;port=3306;user=root;password=N5_?MCaE$wDD;database=easysharding;SslMode=none;",
}); con1.Tests.Add(new MySql.Test { Id = new Random().Next(0, 10000), TestName = "oranizeA" });
con1.SaveChanges();
var c1 = con1.Tests.AsNoTracking().ToList(); var con2 = context.GetShardingContext(new ShardingInfo
{
DatabaseTagName = $"easysharding1",
StufixTableName = $"oranizeB",
ConStr = $"server=192.168.0.208;port=3306;user=root;password=N5_?MCaE$wDD;database=easysharding1;SslMode=none;",
}); con2.Tests.Add(new MySql.Test { Id = new Random().Next(0, 10000), TestName = "oranizeB" });
con2.SaveChanges();
var c2 = con2.Tests.AsNoTracking().ToList(); //针对一些需求,需要每天生成表的
var con3 = context.GetShardingContext(new ShardingInfo
{
DatabaseTagName = $"easysharding1",
StufixTableName = $"{string.Format("{0:yyyyMMdd}",DateTime.Now)}",
ConStr = $"server=192.168.0.208;port=3306;user=root;password=N5_?MCaE$wDD;database=easysharding1;SslMode=none;",
}); con3.Tests.Add(new MySql.Test { Id = new Random().Next(0, 10000), TestName = "日期日期日期" });
con3.SaveChanges();
var c3 = con3.Tests.AsNoTracking().ToList(); return Ok();
}
}
测试代码
查看结果,添加查询都到了对应的库或表里面,注意前面说到的查询 Tests 始终如一,但是数据却来之不同的库 不同的表了,有点那个么个意思了~
接下来看下数据库迁移情况,忽略我前面测试创建的表
这样看起来似乎就没有问题了吗?其实还是存在问题,其一,没有完成前面说的 不同数据库表结构不同,不同租户表结构不同的要求,其二,如果业务中更新的表是通用的表结构迁移文件,在不同租户访问触发migraion文件改变导致 创建的表结构已经存在的问题,为了解决这2个问题又不得不处理下了,上图中其实细心发现 testtwo这个表结构只在easysharding1库中存在,testtwo可视为对某一个差异结构变化而特殊生成的migraton迁移文件,从而满足上面的要求,定义自己的规则方法MigrateWhenNoSharding来确定这个变化对那些变化执行
if (_shardingInfo.MigrateWhenNoSharding())
{ migrationBuilder.CreateTable(
name: "TestTwo",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
TestName = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_TestTwo", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
testtwo
处理好这一步,基本就完成了
参见:
项目GitHub 地址:https://github.com/woshilangdanger/easysharding.efcore
EasySharding.EFCore 如何设计使用一套代码完成的EFCore Migration 构建Saas系统多租户不同业务需求且满足租户自定义分库分表、数据迁移能力?的更多相关文章
- 【转】MySQL分库分表数据迁移工具的设计与实现
一.背景 MySQL作为最流行的关系型数据库产品之一,当数据规模增大遭遇性能瓶颈时,最容易想到的解决方案就是分库分表.无论是进行水平拆分还是垂直拆分,第一步必然需要数据迁移与同步.由此可以衍生出一系列 ...
- 数据库分库分表(sharding)系列(五) 一种支持自由规划无须数据迁移和修改路由代码的Sharding扩容方案
作为一种数据存储层面上的水平伸缩解决方案,数据库Sharding技术由来已久,很多海量数据系统在其发展演进的历程中都曾经历过分库分表的Sharding改造阶段.简单地说,Sharding就是将原来单一 ...
- DB 分库分表(5):一种支持自由规划无须数据迁移和修改路由代码的 Sharding 扩容方案
作为一种数据存储层面上的水平伸缩解决方案,数据库Sharding技术由来已久,很多海量数据系统在其发展演进的历程中都曾经历过分库分表的Sharding改造阶段.简单地说,Sharding就是将原来单一 ...
- DB层面上的设计 分库分表 读写分离 集群化 负载均衡
第1章 引言 随着互联网应用的广泛普及,海量数据的存储和访问成为了系统设计的瓶颈问题.对于一个大型的 互联网应用,每天几十亿的PV无疑对数据库造成了相当高的负载.对于系统的稳定性和扩展性造成了极大的 ...
- 企业项目实战 .Net Core + Vue/Angular 分库分表日志系统二 | 简单的分库分表设计
教程预览 01 | 前言 02 | 简单的分库分表设计 03 | 控制反转搭配简单业务 04 | 强化设计方案 05 | 完善业务自动创建数据库 06 | 最终篇-通过AOP自动连接数据库-完成日志业 ...
- 企业项目实战 .Net Core + Vue/Angular 分库分表日志系统 | 简单的分库分表设计
前言 项目涉及到了一些设计模式,如果你看的不是很明白,没有关系坚持下来,写完之后去思考去品,你就会有一种突拨开云雾的感觉,所以请不要在半途感觉自己看不懂选择放弃,如果我哪里写的详细,或者需要修正请联系 ...
- 基于efcore的分表组件开源
ShardingCore ShardingCore 是一个支持efcore 2.x 3.x 5.x的一个对于数据库分表的一个简易扩展, 目前该库暂未支持分库(未来会支持),仅支持分表,该项目的理念是让 ...
- .NetCore技术研究-一套代码同时支持.NET Framework和.NET Core
在.NET Core的迁移过程中,我们将原有的.NET Framework代码迁移到.NET Core.如果线上只有一个小型的应用还好,迁移升级完成后,只需要维护.NET Core这个版本的代码. 但 ...
- 一套代码同时支持.NET Framework和.NET Core
转自:https://www.cnblogs.com/tianqing/p/11614303.html 在.NET Core的迁移过程中,我们将原有的.NET Framework代码迁移到.NET C ...
随机推荐
- liunx 安装ActiveMQ 及 spring boot 初步整合 activemq
源码地址: https://gitee.com/kevin9401/microservice.git 一.安装 ActiveMQ: 1. 下载 ActiveMQ wget https://arch ...
- 注册页面css版本
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...
- Log4j2 Jndi 漏洞原理解析、复盘
" 2021-12-10一个值得所有研发纪念的日子." 一波操作猛如虎,下班到了凌晨2点25. 基础组件的重要性,在此次的Log4j2漏洞上反应的淋漓尽致,各种"核弹级漏 ...
- Declarative Pipeline 基础语法
Declarative Pipeline(声明式)核心概念 核心概念用来组织pipeline的运行流程 1.pipeline :声明其内容为一个声明式的pipeline脚本 2.agent:执行节点( ...
- bjdctf_2020_babyrop2(没有成功拿到shell)
看到程序先例行检查一下 可以看到开启了canary和nx保护,需要注意的是这个acnary 将程序放入ida中shift+f12 没有关键性函数.我们进入main函数中 在main的gift程序里面我 ...
- [V&N2020 公开赛]babybabypwn
写在开头,感谢"影二つ"师傅的指点. 题目的做法思路网上一搜一大把,这篇博客主要记录一下这道题用pwntools写srop的时候,为什么需要省略前面8个字节. 在看题目之前,先来学 ...
- 工期设定(Project)
<Project2016 企业项目管理实践>张会斌 董方好 编著 任务录入好以后,就得安排工期了不是,要不然每一个任务都如自动设置的从今天开始一个工作日内完成,这么简单的话,还要Proje ...
- LuoguP7222 [RC-04] 信息学竞赛 题解
Content 给定一个角 \(\alpha\),求 \(\beta=90^\circ-\alpha\). 数据范围:\(\alpha\in[-2^{31},2^{31}-1]\). Solution ...
- mysql数据库,当数据类型是float时,查询居然查询不出数据来
mysql数据库,当数据类型是float时,查询居然查询不出数据来,类似如下: 以后mysql数据库不用float类型,而double类型可以查得出来.
- 在JSP页面里,时间控件的JS位置在下面然后就显示不出来
在JSP页面里,时间空间的JS位置在下面然后就显示不出来,放到前面然后就显示出来了, 情何以堪啊,开始还以为是什么错误.