本文咱們來探討下映射動態模型的幾種方式,相信一部分童鞋項目有這樣的需求,好比天天/每小時等生成一張表,此種動態模型映射很是常見,經我摸索,這裏給出每一步詳細思路,但願能幫助到沒有任何頭緒的童鞋,本文以.NET Core 3.1控制檯,同時以SQL Server數據庫做爲示例演示(其餘數據庫同理照搬),因爲會用到內置APi,因版本不一樣可能好比構造函數需略微進行調整便可。注:雖爲示例代碼,但我將其做爲實際項目皆已進行封裝,基本徹底通用。本文略長,請耐心。git
首先咱們給出所須要用到的特性以及對應枚舉,看註釋一看便知github
public enum CustomTableFormat { /// <summary> /// 天天,(yyyyMMdd) /// </summary> [Description("天天")] DAY, /// <summary> /// 每小時,(yyyyMMddHH) /// </summary> [Description("每小時")] HOUR, /// <summary> /// 每分鐘(yyyyMMddHHmm) /// </summary> [Description("每分鐘")] MINUTE } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class EfEntityAttribute : Attribute { /// <summary> /// 是否啓用動態生成表 /// </summary> public bool EnableCustomTable { get; set; } = false; /// <summary> /// 動態生成表前綴 /// </summary> public string Prefix { get; set; } /// <summary> /// 表生成規則 /// </summary> public CustomTableFormat Format { get; set; } = CustomTableFormat.DAY; public override string ToString() { if (EnableCustomTable) { return string.IsNullOrEmpty(Prefix) ? Format.FormatToDate() : $"{Prefix}{Format.FormatToDate()}"; } return base.ToString(); } } public static class CustomTableFormatExetension { public static string FormatToDate(this CustomTableFormat tableFormat) { return tableFormat switch { CustomTableFormat.DAY => DateTime.Now.ToString("yyyyMMdd"), CustomTableFormat.HOUR => DateTime.Now.ToString("yyyyMMddHH"), CustomTableFormat.MINUTE => DateTime.Now.ToString("yyyyMMddHHmm"), _ => DateTime.Now.ToString("yyyyMMdd"), }; } }
經過定義特性,主要出發點基於兩點考慮:其一:由外部注入模型而非寫死DbSet屬性訪問、其二:每一個模型可定義動態映射表規則web
首先咱們給出須要用到的上下文,爲方便演示咱們以每分鐘自動映射模型爲例數據庫
public class EfDbContext : DbContext { public string Date { get; set; } = CustomTableFormat.MINUTE.FormatToDate(); public EfDbContext(DbContextOptions<EfDbContext> options) : base(options) { } }
動態模型即指表名不一樣,好比咱們實現天天/每小時/每分鐘動態映射模型和生成一張表。在下面接口中咱們須要用到每分鐘生成一張表格式,因此在上下文中定義每分鐘屬性。第一種方式則是經過實現IModelCacheKeyFactory接口,此接口將指定上下文下全部模型表名進行了緩存,因此咱們能夠根據所需動態模型表名進行更改便可,以下:緩存
public class CustomModelCacheKeyFactory : IModelCacheKeyFactory { public object Create(DbContext context) { var efDbContext = context as EfDbContext; if (efDbContext != null) { return (context.GetType(), efDbContext.Date); } return context.GetType(); } }
上述其實現貌似感受有點看不太懂,主要這是直接實現接口一步到位,底層本質則是額外調用實例一個緩存鍵類,咱們將上述改成以下兩步則一目瞭然數據結構
public class CustomModelCacheKeyFactory : ModelCacheKeyFactory { private string _date; public CustomModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies) { } public override object Create(DbContext context) { if (context is EfDbContext efDbContext) { _date = efDbContext.Date; } return new CustomModelCacheKey(_date, context); } } public class CustomModelCacheKey : ModelCacheKey { private readonly Type _contextType; private readonly string _date; public CustomModelCacheKey(string date, DbContext context) : base(context) { _date = date; _contextType = context.GetType(); } public virtual bool Equals(CustomModelCacheKey other) => _contextType == other._contextType && _date == other._date; public override bool Equals(object obj) => (obj is CustomModelCacheKey otherAsKey) && Equals(otherAsKey); public override int GetHashCode() => _date.GetHashCode(); }
而後在OnModelCreating方法裏面進行掃描特性標識模型進行註冊,以下:架構
protected override void OnModelCreating(ModelBuilder modelBuilder) { var entityMethod = typeof(ModelBuilder).GetMethod(nameof(modelBuilder.Entity), new Type[] { }); var assembly = Assembly.GetExecutingAssembly(); //【1】使用Entity方法註冊 foreach (var type in assembly.ExportedTypes) { if (!(type.GetCustomAttribute(typeof(EfEntityAttribute)) is EfEntityAttribute attribute)) { continue; } if (type.IsNotPublic || type.IsAbstract || type.IsSealed || type.IsGenericType || type.ContainsGenericParameters) { continue; } entityMethod.MakeGenericMethod(type) .Invoke(modelBuilder, new object[] { }); } //【2】使用IEntityTypeConfiguration<T>註冊 modelBuilder.ApplyConfigurationsFromAssembly(assembly); base.OnModelCreating(modelBuilder); }
上述第一種方式則經過反射將模型註冊,其本質則是調用modeBuilder.Entity方法,若咱們在模型上使用註解,則對應也會將其應用app
但註解不夠靈活,好比要標識聯合主鍵,則只能使用Fluent APi,因此咱們經過在外部實現IEntityTypeConfiguration進行註冊,而後EF Core提供針對該接口程序集註冊,其底層本質也是掃描程序集,兩種方式都支持,不用再擔憂外部模型註冊問題ide
緊接着咱們給出測試模型,表名爲當前分鐘,表名利用註解則不行(值必須爲常量),因此咱們使用以下第二種映射模型函數
[EfEntity(EnableCustomTable = true, Format = CustomTableFormat.MINUTE)] public class Test { [Table(DateTime.Now.ToString("yyyyMMdd"))] public int Id { get; set; } public string Name { get; set; } } public class TestEntityTypeConfiguration : IEntityTypeConfiguration<Test> { public void Configure(EntityTypeBuilder<Test> builder) { builder.ToTable(DateTime.Now.ToString("yyyyMMddHHmm")); } }
上述第二種配置何嘗不可,但咱們還有更加簡潔一步到位的操做,因此這裏刪除上述第二種方式,由於在OnModelCreating方法裏面,咱們反射了調用了Entity方法,因此咱們直接將反射調用Entity方法強制轉換爲EntityTypeBuilder,在已有基礎上,代碼作了重點標識
protected override void OnModelCreating(ModelBuilder modelBuilder) { var entityMethod = typeof(ModelBuilder).GetMethod(nameof(modelBuilder.Entity), new Type[] { }); var assembly = Assembly.GetExecutingAssembly(); //【1】使用Entity方法註冊 foreach (var type in assembly.ExportedTypes) { if (!(type.GetCustomAttribute(typeof(EfEntityAttribute)) is EfEntityAttribute attribute)) { continue; } if (type.IsNotPublic || type.IsAbstract || type.IsSealed || type.IsGenericType || type.ContainsGenericParameters) { continue; } // 強制轉換爲EntityTypeBuilder var entityBuilder = (EntityTypeBuilder)entityMethod.MakeGenericMethod(type) .Invoke(modelBuilder, new object[] { }); if (attribute.EnableCustomTable) { entityBuilder.ToTable(attribute.ToString()); } } //【2】使用IEntityTypeConfiguration<T>註冊 modelBuilder.ApplyConfigurationsFromAssembly(assembly); base.OnModelCreating(modelBuilder); }
最後則是注入上下文,這裏咱們將內外部容器進行區分(EF Core爲什麼份內部容器,具體緣由請參看文章《EntityFramework Core 3.x上下文構造函數能夠注入實例呢?》)
因在實際項目中上下文可能須要在上下文構造函數中注入其餘接口,好比咱們就有可能在上下文構造函數中注入接口從而根據具體接口實現來更改表架構或不一樣表名規則等等
static IServiceProvider Initialize() { var services = new ServiceCollection(); services.AddEntityFrameworkSqlServer() .AddDbContext<EfDbContext>( (serviceProvider, options) => options.UseSqlServer("server=.;database=efcore;uid=sa;pwd=sa123;") .UseInternalServiceProvider(serviceProvider)); services.Replace(ServiceDescriptor.Singleton<IModelCacheKeyFactory, CustomModelCacheKeyFactory>()); return services.BuildServiceProvider(); }
因爲咱們已區分EF Core內外部容器,因此在替換自定義緩存鍵工廠時,不能再像以下直接調用ReplaceService方法替換,勢必會拋出異常
options.UseSqlServer("server=.;database=efcore;uid=sa;pwd=sa123;") .ReplaceService<IModelCacheKeyFactory, CustomModelCacheKeyFactory>()
同時謹記在非Web項目中利用EF Core始終要使用做用域(scope)來釋放上下文,不像Web可基於HTTP請求做爲scope,最後咱們測試以下
using (var scope1 = ServiceProvider.CreateScope()) { var context1 = scope1.ServiceProvider.GetService<EfDbContext>(); context1.Database.EnsureCreated(); var type = context1.Model.FindEntityType(typeof(Test)); Console.WriteLine(type?.GetTableName()); var tests = context1.Set<Test>().ToList(); } Thread.Sleep(60000); using (var scope2 = ServiceProvider.CreateScope()) { var context2 = scope2.ServiceProvider.GetService<EfDbContext>(); context2.Database.EnsureCreated(); var type = context2.Model.FindEntityType(typeof(Test)); Console.WriteLine(type?.GetTableName()); var tests1 = context2.Set<Test>().ToList(); }
爲方便看到實際效果,咱們構建兩個scope,而後睡眠一分鐘,在界面上打印輸出表名,若兩分鐘後打印表名不一致,說明達到預期
上述咱們使用每分鐘規則動態映射表,同時可針對不一樣模型有各自規則(前綴,每小時或天天)等等,這是第一種方式
若是對第一種方式實現徹底看懂了,可能會有所疑惑,由於第一種方式其接口生命週期爲單例,若不須要豈不仍是會將上下文中全部模型都會進行緩存嗎
調用OnModelCreating方法只是進行模型構建,但咱們現直接調用內置APi來手動使用全部模型,此時將再也不緩存,因此再也不須要IModelCacheKeyFactory接口
對EF Core稍微瞭解一點的話,咱們知道OnModelCreating方法僅僅只會調用一次,咱們經過手動使用和處置全部模型,換言之每次請求都會使用新的模型,說了這麼多,那麼咱們到底該如何作呢?
若是看過我以前原理分析的話,大概能知道EntityFramework Core對於模型的處理(除卻默認模型緩存)分爲三步,除卻模型緩存:構建模型,使用模型,處置模型。
咱們將OnModelCreating方法代碼所有直接複製過來,只是多了上面三步而已,在咱們實例化ModelBuilder時,咱們須要提供對應數據庫默認約定,而後使用模型、處置模型,結果變成以下這般
services.AddEntityFrameworkSqlServer() .AddDbContext<EfDbContext>( (serviceProvider, options) => { options.UseSqlServer("server=.;database=efcore;uid=sa;pwd=sa123;") .UseInternalServiceProvider(serviceProvider); var conventionSet = SqlServerConventionSetBuilder.Build(); var modelBuilder = new ModelBuilder(conventionSet); // OnModelCreating方法,代碼複製 options.UseModel(modelBuilder.Model); modelBuilder.FinalizeModel(); )};
運行第一種方式測試代碼,而後麼有問題
問題來了,要是有多個數據庫,豈不是都要像上述再來一遍?上述實現本質上是每次構造一個上下文則會構建並從新使用新的模型,因此咱們將其統一放到上下文構造函數中去,而後寫個擴展方法構建模型,以下:
public static class ModelBuilderExetension { public static ModelBuilder BuildModel(this ModelBuilder modelBuilder) { var entityMethod = typeof(ModelBuilder).GetMethod(nameof(modelBuilder.Entity), new Type[] { }); var assembly = Assembly.GetExecutingAssembly(); //【1】使用Entity方法註冊 foreach (var type in assembly.ExportedTypes) { if (!(type.GetCustomAttribute(typeof(EfEntityAttribute)) is EfEntityAttribute attribute)) { continue; } if (type.IsNotPublic || type.IsAbstract || type.IsSealed || type.IsGenericType || type.ContainsGenericParameters) { continue; } var entityBuilder = (EntityTypeBuilder)entityMethod.MakeGenericMethod(type) .Invoke(modelBuilder, new object[] { }); if (attribute.EnableCustomTable) { entityBuilder.ToTable(attribute.ToString()); } } //【2】使用IEntityTypeConfiguration<T>註冊 modelBuilder.ApplyConfigurationsFromAssembly(assembly); return modelBuilder; } }
最後在上下文構造函數中,簡潔調用,以下:
public class EfDbContext : DbContext { public string Date { get; set; } = CustomTableFormat.MINUTE.FormatToDate(); public EfDbContext(DbContextOptions<EfDbContext> options) : base(options) { //提供不一樣數據庫默認約定 ConventionSet conventionSet = null; if (Database.ProviderName == "Microsoft.EntityFrameworkCore.SqlServer") { conventionSet = SqlServerConventionSetBuilder.Build(); } else if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqllite") { conventionSet = SqliteConventionSetBuilder.Build(); } else if (Database.ProviderName == "Microsoft.EntityFrameworkCore.MySql") { conventionSet = MySqlConventionSetBuilder.Build(); } var modelBuilder = new ModelBuilder(conventionSet); var optionBuilder = new DbContextOptionsBuilder(options); //使用模型 optionBuilder.UseModel(modelBuilder.Model); //處置模型 modelBuilder.FinalizeModel(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { //構建模型 modelBuilder.BuildModel(); base.OnModelCreating(modelBuilder); } }
看到這裏,細心的你不知道有沒有發現,我寫的打印結果怎麼成功了,竟然沒拋出任何異常,實際狀況是必須會拋出異常,由於咱們只作到了模型動態映射,但表自動生成我在此以前將其忽略了,以下:
表如何生成這個也看實際狀況分析,好比SQL Server寫個做業天天自動生成表等,若需兼容多個數據庫,怕是有點麻煩
我沒花太多時間去看源碼,稍微看了下,碰碰運氣或許能直接找到根據模型來建立表的接口實現,結果好像沒有,即便有也比較麻煩,那麼咱們就手動構建SQL語句或者經過lambda構建也可
上下文中實現其特性需動態生成的模型咱們能夠獲取獲得,而後搞個定時器每分鐘去執行生成對應表,針對不一樣數據庫類型,咱們能夠經過以下屬性獲取獲得(和包同名)
// 好比SQL Server:Microsoft.EntityFrameworkCore.SqlServer context.Database.ProviderName
這裏我以SQL Server數據庫爲例,其餘數據庫好比MySqL、Sqlite惟一區別則是自增加設置和列類型不一樣而已,建立表,經過五部分組成:表是否存在,表名,主鍵,全部列,約束。咱們定義以下:
internal sealed class CustomTableModel { public CustomEntityType CustomEntityType { get; set; } public string TableName { get; set; } = string.Empty; public string CheckTable { get; set; } = string.Empty; public string PrimaryKey { get; set; } = string.Empty; public string Columns { get; set; } = string.Empty; public string Constraint { get; set; } = string.Empty; public override string ToString() { var placeHolder = $"{CheckTable} create table {TableName} ({PrimaryKey} {Columns}"; placeHolder = string.IsNullOrEmpty(Constraint) ? $"{placeHolder.TrimEnd(',')})" : $"{placeHolder}{Constraint})"; return placeHolder.Replace("@placeholder_table_name", CustomEntityType.ToString()); } }
因爲每次生成只有表名不一樣,因此咱們將整個表數據結構進行緩存,在其內部將表名進行替換就好。整個實現邏輯以下:
public static void Execute() { using var scope = Program.ServiceProvider.CreateScope(); var context = scope.ServiceProvider.GetService<EfDbContext>(); context.Database.EnsureCreated(); var cache = scope.ServiceProvider.GetService<IMemoryCache>(); var cacheKey = context.GetType().FullName; if (!cache.TryGetValue(cacheKey, out List<CustomTableModel> models)) { lock (_syncObject) { if (!cache.TryGetValue(cacheKey, out models)) { models = CreateModels(context); models = cache.Set(cacheKey, models, new MemoryCacheEntryOptions { Size = 100, Priority = CacheItemPriority.High }); } } } Create(context, models); } private static void Create(EfDbContext context, List<CustomTableModel> models) { foreach (var m in models) { context.Execute(m.ToString()); } } internal static void CreateEntityTypes(CustomEntityType customEntityType) { EntityTypes.Add(customEntityType); }
上述標紅部分很重要,爲何呢?讓其先執行OnModelCreating方法,也就是說咱們必須保證全部模型已經構建完畢,咱們才能在上下文中拿到全部模型元數據
接下來則是在OnModeCreating方法中,在啓動自動映射模型的基礎上,添加以下代碼(固然也需檢查表名是否存在重複):
if (attribute.EnableCustomTable) { entityBuilder.ToTable(attribute.ToString()); var customType = new CustomEntityType() { ClrType = type, Prefix = attribute.Prefix, Format = attribute.Format }; var existTable = CreateCustomTable.EntityTypes.FirstOrDefault(c => c.ToString() == customType.ToString()); if (existTable != null) { throw new ArgumentNullException($"Cannot use table '{customType}' for entity type '{type.Name}' since it is being used for entity type '{existTable.ClrType.Name}' "); } CreateCustomTable.CreateEntityTypes(customType); }
相信構建SQL語句這塊都不在話下,就再也不給出了,真的有須要的童鞋,可私信我,人比較多的話,我會將兼容不一樣數據庫的SQL語句構建都會放到github上去,控制檯入口方法調用以下:
private const int TIME_INTERVAL_IN_MILLISECONDS = 60000; private static Timer _timer { get; set; } public static IServiceProvider ServiceProvider { get; set; } static void Main(string[] args) { ServiceProvider = Initialize(); //初始化時檢查一次 CreateCustomTable.Execute(); //定時檢查 _timer = new Timer(TimerCallback, null, TIME_INTERVAL_IN_MILLISECONDS, Timeout.Infinite); using (var scope1 = ServiceProvider.CreateScope()) { var context1 = scope1.ServiceProvider.GetService<EfDbContext>(); context1.Database.EnsureCreated(); var type = context1.Model.FindEntityType(typeof(Test1)); Console.WriteLine(type?.GetTableName()); var tests = context1.Set<Test1>().ToList(); } Thread.Sleep(60000); using (var scope2 = ServiceProvider.CreateScope()) { var context2 = scope2.ServiceProvider.GetService<EfDbContext>(); context2.Database.EnsureCreated(); var type = context2.Model.FindEntityType(typeof(Test2)); Console.WriteLine(type?.GetTableName()); var tests1 = context2.Set<Test2>().ToList(); } Console.ReadKey(); }
接下來則是經過定義上述定時器,回調調用上述Execute方法,以下:
static void TimerCallback(object state) { var watch = new Stopwatch(); watch.Start(); CreateCustomTable.Execute(); _timer.Change(Math.Max(0, TIME_INTERVAL_IN_MILLISECONDS - watch.ElapsedMilliseconds), Timeout.Infinite); }
最後咱們來兩個模型測試下實際效果
[EfEntity(EnableCustomTable = true, Prefix = "test1", Format = CustomTableFormat.MINUTE)] public class Test1 { public int Id { get; set; } public int UserId { get; set; } public string Name { get; set; } } public class Test1EntityTypeConfiguration : IEntityTypeConfiguration<Test1> { public void Configure(EntityTypeBuilder<Test1> builder) { builder.HasKey(k => new { k.Id, k.UserId }); } } [EfEntity(EnableCustomTable = true, Prefix = "test2", Format = CustomTableFormat.MINUTE)] public class Test2 { public int Id { get; set; } public int UserId { get; set; } public string Name { get; set; } } public class Test2EntityTypeConfiguration : IEntityTypeConfiguration<Test2> { public void Configure(EntityTypeBuilder<Test2> builder) { builder.HasKey(k => new { k.Id, k.UserId }); } }
最後的最後,老規矩,實現動態映射模型有如上兩種方式,經過手動構建SQL語句並緩存,總結以下!
💡 使用IModelCacheKeyFactory
💡 手動使用模型、處置模型
💡 兼容不一樣數據庫,手動構建SQL語句並緩存