EFCore高级玩法单DbContext支持多数据库迁移

前言

随着系统的不断开发和迭代默认的efcore功能十分强大,但是随着Saas系统的引进efcore基于表字段的多租户模式已经非常完美了,但是基于数据库的多租户也是可以用的,但是也存在缺点,缺点就是没有办法支持不同数据库,migration support multi database provider with single dbcontext,本人不才,查询了一下,官方文档只说明了dbcontext的迁移如何实现多数据源,但是缺不是单个dbcontext,这个就让人很头疼。所以秉着尝试一下的原则进行了这篇博客的编写,因为本人只有mmsql和mysql所以这次就用这两个数据库来做测试

广告时间

本人开发了一款efcore的分表分库读写分离组件

https://github.com/dotnetcore/sharding-core

希望有喜欢的小伙伴给我点点star谢谢

那么废话不多说我们马上开始migration support multi database provider with single dbcontext

新建项目

1.按装依赖

2.新建一个User类

[Table(nameof(User))]
public class User
{
public string UserId { get; set; }
public string UserName { get; set; }
}

3.创建DbContext

public class MyDbContext:DbContext
{
public DbSet<User> Users { get; set; }
public MyDbContext(DbContextOptions<MyDbContext> options):base(options)
{ }

4.StartUp配置

var provider = builder.Configuration.GetValue("Provider", "UnKnown");

//Add-Migration InitialCreate -Context MyDbContext -OutputDir Migrations\SqlServer -Args "--provider SqlServer"
//Add-Migration InitialCreate -Context MyDbContext -OutputDir Migrations\MySql -Args "--provider MySql"
builder.Services.AddDbContext<MyDbContext>(options =>
{
_ = provider switch
{
"MySql" => options.UseMySql("server=127.0.0.1;port=3306;database=DBMultiDataBase;userid=root;password=L6yBtV6qNENrwBy7;", new MySqlServerVersion(new Version())),
"SqlServer" => options.UseSqlServer("Data Source=localhost;Initial Catalog=DBMultiDataBase;Integrated Security=True;"),
_ => throw new Exception($"Unsupported provider: {provider}")
};
});

迁移区分数据库

新建一个迁移命名空间提供者


public interface IMigrationNamespace
{
string GetNamespace();
}

mysql和sqlserver的实现分别是项目名称迁移文件夹

    public class MySqlMigrationNamespace:IMigrationNamespace
{
public string GetNamespace()
{
return "EFCoreMigrateMultiDatabase.Migrations.MySql";
}
} public class SqlServerMigrationNamespace:IMigrationNamespace
{
public string GetNamespace()
{
return "EFCoreMigrateMultiDatabase.Migrations.SqlServer";
}
}

efcore扩展

添加efcore扩展

    public class MigrationNamespaceExtension : IDbContextOptionsExtension
{
public IMigrationNamespace MigrationNamespace { get; } public MigrationNamespaceExtension(IMigrationNamespace migrationNamespace)
{
MigrationNamespace = migrationNamespace;
}
public void ApplyServices(IServiceCollection services)
{
services.AddSingleton<IMigrationNamespace>(sp => MigrationNamespace);
} public void Validate(IDbContextOptions options)
{
} public DbContextOptionsExtensionInfo Info => new MigrationNamespaceExtensionInfo(this); private class MigrationNamespaceExtensionInfo : DbContextOptionsExtensionInfo
{
private readonly MigrationNamespaceExtension _migrationNamespaceExtension;
public MigrationNamespaceExtensionInfo(IDbContextOptionsExtension extension) : base(extension)
{
_migrationNamespaceExtension = (MigrationNamespaceExtension)extension;
} public override int GetServiceProviderHashCode() => _migrationNamespaceExtension.MigrationNamespace.GetNamespace().GetHashCode(); public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => true; public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
{
} public override bool IsDatabaseProvider => false;
public override string LogFragment => "MigrationNamespaceExtension";
}
}

重写MigrationsAssembly支持多数据库

    public class EFCoreMultiDatabaseMigrationsAssembly: IMigrationsAssembly
{
public string MigrationNamespace { get; }
private readonly IMigrationsIdGenerator _idGenerator;
private readonly IDiagnosticsLogger<DbLoggerCategory.Migrations> _logger;
private IReadOnlyDictionary<string, TypeInfo>? _migrations;
private ModelSnapshot? _modelSnapshot;
private readonly Type _contextType; /// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public EFCoreMultiDatabaseMigrationsAssembly(
IMigrationNamespace migrationNamespace,
ICurrentDbContext currentContext,
IDbContextOptions options,
IMigrationsIdGenerator idGenerator,
IDiagnosticsLogger<DbLoggerCategory.Migrations> logger)
{ _contextType = currentContext.Context.GetType(); var assemblyName = RelationalOptionsExtension.Extract(options)?.MigrationsAssembly;
Assembly = assemblyName == null
? _contextType.Assembly
: Assembly.Load(new AssemblyName(assemblyName)); MigrationNamespace = migrationNamespace.GetNamespace();
_idGenerator = idGenerator;
_logger = logger;
} /// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual IReadOnlyDictionary<string, TypeInfo> Migrations
{
get
{
IReadOnlyDictionary<string, TypeInfo> Create()
{
var result = new SortedList<string, TypeInfo>();
var items
= from t in Assembly.GetConstructibleTypes()
where t.IsSubclassOf(typeof(Migration))&& print(t)
&& t.Namespace.Equals(MigrationNamespace)
&& t.GetCustomAttribute<DbContextAttribute>()?.ContextType == _contextType
let id = t.GetCustomAttribute<MigrationAttribute>()?.Id
orderby id
select (id, t);
Console.WriteLine("Migrations:" + items.Count());
foreach (var (id, t) in items)
{
if (id == null)
{
_logger.MigrationAttributeMissingWarning(t); continue;
} result.Add(id, t);
} return result;
} return _migrations ??= Create();
}
} private bool print(TypeInfo t)
{
Console.WriteLine(MigrationNamespace);
Console.WriteLine(t.Namespace);
return true;
} /// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual ModelSnapshot? ModelSnapshot
=> GetMod(); private ModelSnapshot GetMod()
{
Console.WriteLine("_modelSnapshot:"+ _modelSnapshot);
if (_modelSnapshot == null)
{
Console.WriteLine("_modelSnapshot:null");
_modelSnapshot = (from t in Assembly.GetConstructibleTypes()
where t.IsSubclassOf(typeof(ModelSnapshot)) && print(t)
&& MigrationNamespace.Equals(t?.Namespace)
&& t.GetCustomAttribute<DbContextAttribute>()?.ContextType == _contextType
select (ModelSnapshot)Activator.CreateInstance(t.AsType())!)
.FirstOrDefault(); Console.WriteLine("_modelSnapshot:" + _modelSnapshot);
}
return _modelSnapshot;
} /// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual Assembly Assembly { get; } /// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual string? FindMigrationId(string nameOrId)
=> Migrations.Keys
.Where(
_idGenerator.IsValidId(nameOrId)
// ReSharper disable once ImplicitlyCapturedClosure
? id => string.Equals(id, nameOrId, StringComparison.OrdinalIgnoreCase)
: id => string.Equals(_idGenerator.GetName(id), nameOrId, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault(); /// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual Migration CreateMigration(TypeInfo migrationClass, string activeProvider)
{
Console.WriteLine(migrationClass.FullName); var migration = (Migration)Activator.CreateInstance(migrationClass.AsType())!;
migration.ActiveProvider = activeProvider; return migration;
}
}

编写startup

参考 https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=vs

//Add-Migration InitialCreate -Context MyDbContext -OutputDir Migrations\SqlServer -Args "--provider SqlServer"
//Add-Migration InitialCreate -Context MyDbContext -OutputDir Migrations\MySql -Args "--provider MySql"
//update-database -Args "--provider MySql"
//update-database -Args "--provider SqlServer"
builder.Services.AddDbContext<MyDbContext>(options =>
{
options.ReplaceService<IMigrationsAssembly, EFCoreMultiDatabaseMigrationsAssembly>();
_ = provider switch
{
"MySql" => options.UseMySql("server=127.0.0.1;port=3306;database=DBMultiDataBase;userid=root;password=L6yBtV6qNENrwBy7;", new MySqlServerVersion(new Version()))
.UseMigrationNamespace(new MySqlMigrationNamespace()),
"SqlServer" => options.UseSqlServer("Data Source=localhost;Initial Catalog=DBMultiDataBase;Integrated Security=True;")
.UseMigrationNamespace(new SqlServerMigrationNamespace()),
_ => throw new Exception($"Unsupported provider: {provider}")
};
});

到此为止我这边想我们应该已经实现了把,但是如果我们分别执行两个迁移命令会导致前一个迁移命令被覆盖掉,经过一整个下午的debug调试最后发现是因为在迁移脚本生成写入文件的时候会判断当前DbContext'的ModelSnapshot,同一个dbcontext生成的文件是一样的,所以我们这边有两个选择

  • 1.让生成的文件名不一样
  • 2.让ModelSnapshot不进行深度查询只在当前目录下处理

    这边选了第二种
    public class MyMigrationsScaffolder: MigrationsScaffolder
{
private readonly Type _contextType;
public MyMigrationsScaffolder(MigrationsScaffolderDependencies dependencies) : base(dependencies)
{
_contextType = dependencies.CurrentContext.Context.GetType();
}
protected override string GetDirectory(string projectDir, string? siblingFileName, string subnamespace)
{
var defaultDirectory = Path.Combine(projectDir, Path.Combine(subnamespace.Split('.'))); if (siblingFileName != null)
{
if (!siblingFileName.StartsWith(_contextType.Name + "ModelSnapshot."))
{
var siblingPath = TryGetProjectFile(projectDir, siblingFileName);
if (siblingPath != null)
{
var lastDirectory = Path.GetDirectoryName(siblingPath)!;
if (!defaultDirectory.Equals(lastDirectory, StringComparison.OrdinalIgnoreCase))
{
Dependencies.OperationReporter.WriteVerbose(DesignStrings.ReusingNamespace(siblingFileName)); return lastDirectory;
}
}
}
} return defaultDirectory;
}
}

添加designservices

    public class MyMigrationDesignTimeServices: IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<IMigrationsScaffolder, MyMigrationsScaffolder>();
}
}

迁移

分别运行两个迁移命令



运行更新数据库命令



记得我们需要在参数里面添加选项

下期预告

下期我们将实现efcore在Saas系统下的多租户+code-first(迁移)+分表+分库+读写分离+动态分表+动态分库+动态读写分离+动态添加多租户 全程零sql脚本的解决方案

是不是buffer叠满

最后的最后

附上demo:EFCoreMigrateMultiDatabase https://github.com/xuejmnet/EFCoreMigrateMultiDatabase

您都看到这边了确定不点个star或者赞吗,一款.Net不得不学的分库分表解决方案,简单理解为sharding-jdbc在.net中的实现并且支持更多特性和更优秀的数据聚合,拥有原生性能的97%,并且无业务侵入性,支持未分片的所有efcore原生查询

EFCore高级Saas系统下一个DbContext如何支持多数据库迁移的更多相关文章

  1. efcore在Saas系统下多租户零脚本分表分库读写分离解决方案

    efcore在Saas系统下多租户零脚本分表分库读写分离解决方案 ## 介绍 本文ShardinfCore版本x.6.0.20+ 本期主角: - [`ShardingCore`](https://gi ...

  2. Linux系统下授权MySQL账户访问指定数据库和数据库操作

    Linux系统下授权MySQL账户访问指定数据库 需求: 1.在MySQL中创建数据库mydata 2.新建MySQL账户admin密码123456 3.赋予账户admin对数据库mydata具有完全 ...

  3. Linux系统下 解决Qt5无法连接MySQL数据库的方法

    Linux平台下解决Qt5连接mysql数据库的问题:输入sudo apt-get install libqt5sql5-mysql解决,这种方法只能解决Qt是用sudo apt-get instal ...

  4. Windows系统下安装MySQL 8.0.11数据库

    MySQL数据库是常用的数据库之一,而且该数据库开源免费,所以很多公司在使用.本文记录如何在Windows系统下安装MySQL数据库,本次安装的版本号为8.0.11,这个版本是当前的最新版本,据宣传, ...

  5. Linux系统下一个冷门的RAID卡ioc0及其监控mpt-status

    新接手了一台Linux服务器,准备检查是否有配置RAID.参考(http://mip.0834jl.com) 先查看是否有RAID卡: 复制代码 代码如下: # dmesg|grep -i raid ...

  6. MacOS系统下简单安装以及配置MongoDB数据库(一)

    最近写了一个用node来操作MongoDB完成增.删.改.查.排序.分页功能的示例,并且已经放在了服务器上地址:http://39.105.32.180:3333. 项目一共四部分: 1.MacOS下 ...

  7. Mac系统下安装ipython分别支持python2和python3

    操作系统:Mac10.11.5 python2.7.13 python3.6.1 安装python2: brew install python 安装python3: brew install pyth ...

  8. windows系统下mysql5.5查看和设置数据库编码

    1.显示当前编码命令: show variables like 'char%'; 2.设置编码为utf8命令:set names 'utf8';

  9. 在linux 系统下 使用命令行对mysql 数据库进行操作

    1.连接mysql root@test:/home# mysql -uroot -proot <uroot是用户名,proot是密码> 2.查询所有的库 mysql> show da ...

随机推荐

  1. Android 12(S) 图像显示系统 - SurfaceFlinger GPU合成/CLIENT合成方式 - 随笔1

    必读: Android 12(S) 图像显示系统 - 开篇 一.前言 SurfaceFlinger中的图层选择GPU合成(CLIENT合成方式)时,会把待合成的图层Layers通过renderengi ...

  2. 微信H5页面唤醒APP并传参跳转uniapp

    主要实现是利用微信内置浏览器支持的<wx-open-launch-app>开放标签可以让你的H5网页拉起APP   在链接https://developers.weixin.qq.com/ ...

  3. 大陆出境海缆TPE APCN NCP APG简介

    目前我国的登陆站主要设立在三个城市 山东 山东青岛登陆站(隶属中国联通) EAC-C2C TPE(美国方向) 上海 上海崇明登陆站(隶属中国电信) APCN2(亚太) NCP(长线--美国,新建,亚太 ...

  4. 817. Linked List Components - LeetCode

    Question 817. Linked List Components Solution 题目大意:给一个链表和该链表元素组成的一个子数组,求子数组在链表中组成多少个片段,每个片段中可有多个连续的元 ...

  5. 记一次 JDK SPI 配置不生效的问题 → 这么简单都不会,还是回家养猪吧

    开心一刻 今天去幼儿园接小侄女,路上聊起了天 小侄女:小叔,今天我吃东西被老师发现了 我:老师说了什么 小侄女:她说拿出来,跟小朋友一起分享 我:那你拿出来了吗 小侄女一脸可怜的看向我,说道:没有,我 ...

  6. 论文解读(GCC)《GCC: Graph Contrastive Coding for Graph Neural Network Pre-Training》

    论文信息 论文标题:GCC: Graph Contrastive Coding for Graph Neural Network Pre-Training论文作者:Jiezhong Qiu, Qibi ...

  7. JNPF移动办公解决方案

    市场背景 随着办公自动化系统的普及,电子化.数据化的办公方式已进入越来越多的企业和政府单位,信息化的办公系统在企事业内部编织起一套高效.畅通的信息互联体系,极大推动了企事业单位生产力的发展.但与此同时 ...

  8. vs2022+resharper创建模板——实现在新建文件的时候自动生成防卫式声明和自定义语句

    在网上找了很久如何让visual studio新建文件的时候自动生成注释和防卫式声明,虽然防卫式声明可以用#proga once替代,但是在clion里可以自动生成vs里面没法自动生成还是觉得难受,于 ...

  9. POJ1821 Fence 题解报告

    传送门 1 题目描述 A team of $k (1 <= K <= 100) $workers should paint a fence which contains \(N (1 &l ...

  10. Java - 四种引用类型及应用场景

    1. 强引用 new 一个对象的时候,就是强引用 Object object = new Object(); 只要强引用存在,垃圾回收就不会回收该对象,内存不足时会抛出OOM. 2. 软引用 定义:非 ...