EFCore高級Saas系統(tǒng)下單DbContext如何支持不同數(shù)據(jù)庫的遷移
前言
隨著系統(tǒng)的不斷開發(fā)和迭代默認的efcore功能十分強大,但是隨著Saas系統(tǒng)的引進efcore基于表字段的多租戶模式已經(jīng)非常完美了,但是基于數(shù)據(jù)庫的多租戶也是可以用的,但是也存在缺點,缺點就是沒有辦法支持不同數(shù)據(jù)庫,migration support multi database provider with single dbcontext,本人不才,查詢了一下,官方文檔只說明了dbcontext的遷移如何實現(xiàn)多數(shù)據(jù)源,但是缺不是單個dbcontext,這個就讓人很頭疼。所以秉著嘗試一下的原則進行了這篇博客的編寫,因為本人只有mmsql和mysql所以這次就用這兩個數(shù)據(jù)庫來做測試
廣告時間
本人開發(fā)了一款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.創(chuàng)建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}")
};
});遷移區(qū)分數(shù)據(jù)庫
新建一個遷移命名空間提供者
public interface IMigrationNamespace
{ string GetNamespace();
}mysql和sqlserver的實現(xiàn)分別是項目名稱遷移文件夾
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支持多數(shù)據(jù)庫
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}")
};
});到此為止我這邊想我們應(yīng)該已經(jīng)實現(xiàn)了把,但是如果我們分別執(zhí)行兩個遷移命令會導(dǎo)致前一個遷移命令被覆蓋掉,經(jīng)過一整個下午的debug調(diào)試最后發(fā)現(xiàn)是因為在遷移腳本生成寫入文件的時候會判斷當(dāng)前DbContext'的ModelSnapshot,同一個dbcontext生成的文件是一樣的,所以我們這邊有兩個選擇
1.讓生成的文件名不一樣
2.讓ModelSnapshot不進行深度查詢只在當(dāng)前目錄下處理
這邊選了第二種
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>();
}
}遷移
分別運行兩個遷移命令
運行更新數(shù)據(jù)庫命令
記得我們需要在參數(shù)里面添加選項
下期預(yù)告
下期我們將實現(xiàn)efcore在Saas系統(tǒng)下的多租戶+code-first(遷移)+分表+分庫+讀寫分離+動態(tài)分表+動態(tài)分庫+動態(tài)讀寫分離+動態(tài)添加多租戶 全程零sql腳本的解決方案
是不是buffer疊滿
最后的最后
附上demo:EFCoreMigrateMultiDatabase https://github.com/xuejmnet/EFCoreMigrateMultiDatabase
您都看到這邊了確定不點個star或者贊嗎,一款.Net不得不學(xué)的分庫分表解決方案,簡單理解為sharding-jdbc在.net中的實現(xiàn)并且支持更多特性和更優(yōu)秀的數(shù)據(jù)聚合,擁有原生性能的97%,并且無業(yè)務(wù)侵入性,支持未分片的所有efcore原生查詢
github地址 https://github.com/xuejmnet/sharding-core
gitee地址 https://gitee.com/dotnetchina/sharding-core
