前言
上一節我們針對最開始拋出的異常只是進行了淺嘗輒止的解析,是不是有點意猶未盡的感覺,是的,我也有這種感覺,看到這里相信您和我會有一些疑惑,要是我們接下來通過注解、Fluent APi、DbSet分別對表名進行如下設置,是否會拋出異常呢?若不是,有其優先級,那么其優先級到底是怎樣的呢?內置具體是如何實現的呢?讓我們從頭開始揭開其神秘的面紗。
EntityFramework Core表名原理解析
我們暫不知道到底是否有其優先級還是會拋出異常,那么接下來我們進行如下配置(模型請參考上一節《https://www.cnblogs.com/CreateMyself/p/12175618.html》)進行原理分析:
public DbSet<Blog> Blog1 { get; set; } [Table("Blog2")] public class Blog { public int Id { get; set; } public string Name { get; set; } public List<Post> Posts { get; set; } } modelBuilder.Entity<Blog>().ToTable("Blog3");
在還未進入原理解析之前,讓我們大膽猜測通過如上配置后優先級將是怎樣的呢?是Fluent Api > 注解 > DbSet > 約定嗎?假設是這樣的話,EntityFramework Core內置是怎樣實現的呢?是采用覆蓋的機制嗎?一堆疑問浮現在我們眼前,來,讓我們進入探究枯燥源碼的世界,為您一一解惑。 首先我們需要明確的是,在我們實例化上下文進行操作之前,EntityFramework Core具體做了些什么?故事就要從我們派生自DbContext上下文說起,如下:
public class EFCoreDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer(@"Server=.;Database=EFTest;Trusted_Connection=True;"); public DbSet<Blog> Blog1 { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>(b => { b.ToTable("Blog3"); }); base.OnModelCreating(modelBuilder); } }
在EntityFramework Core中我們利用上下文進行操作之前就是按照上述代碼由上至下整體上做了如下三步准備工作:
【1】實例化上下文時,查找DbSet屬性並緩存到內存中
【2】以上下文作為緩存的鍵,將上下文中的所有模型數據緩存在內存中,若未緩存執行第【3】步。
【3】創建上下文中所有模型有關數據。
查找DbSet屬性並緩存
接下來我們步步分析,步步逼近以上三步操作實現,無論是主動實例化還是在Web中添加上下文中間件時,都必須經過將我們需要用到所有接口進行依賴注入,當然EntityFramework Core是用的【 Microsoft.Extensions.DependencyInjection 】庫,至於注冊了哪些,這些細節我們並不關心,我們只關注所需要用到的且會一一說明,獲取接口【IDbSetInitializer】的具體實現【DbSetInitializer】,調用該類中的如下方法:
public virtual void InitializeSets(DbContext context) { foreach (var setInfo in _setFinder.FindSets(context.GetType()).Where(p => p.Setter != null)) { setInfo.Setter.SetClrValue( context, ((IDbSetCache)context).GetOrAddSet(_setSource, setInfo.ClrType)); } }
接下來獲取接口【IDbSetFinder】的具體實現【DbSetFinder】去過濾查找存在Setter屬性的DbSet(這點就不用我解釋),查找細節我們不關心,每個DbSet都有其【DbSetProperty】屬性,所以查找到后添加到該屬性並緩存到【IDbSetCache】中,到此對於DbSet的查找和緩存就已完事,接下來去創建上下文中的所有模型數據。
創建上下文模型
首先是去獲取上下文中所有模型數據,以上下文為鍵去查找緩存的模型數據,若沒有則創建,否則創建緩存,如下:
public virtual IModel GetModel( DbContext context, IConventionSetBuilder conventionSetBuilder) { var cache = Dependencies.MemoryCache; var cacheKey = Dependencies.ModelCacheKeyFactory.Create(context); if (!cache.TryGetValue(cacheKey, out IModel model)) { // Make sure OnModelCreating really only gets called once, since it may not be thread safe. lock (_syncObject) { if (!cache.TryGetValue(cacheKey, out model)) { model = CreateModel(context, conventionSetBuilder); model = cache.Set(cacheKey, model, new MemoryCacheEntryOptions { Size = 100, Priority = CacheItemPriority.High }); } } } return model; }
接下來到了緩存不存在創建模型的環節,創建模型主要做了以下三件事。
protected virtual IModel CreateModel( [NotNull] DbContext context, [NotNull] IConventionSetBuilder conventionSetBuilder) { Check.NotNull(context, nameof(context)); //構建默認約定集合,通過約定分發機制去處理各個約定 var modelBuilder = new ModelBuilder(conventionSetBuilder.CreateConventionSet()); //處理OnModelCreating方法中的自定義配置 Dependencies.ModelCustomizer.Customize(modelBuilder, context); //模型構建完畢后,重新根據約定分發機制使得模型數據處於最新狀態 return modelBuilder.FinalizeModel(); }
當實例化ModelBuilder通過約定分發機制處理各個約定,具體做了哪些操作呢?主要做了以下三件事
【1】各個約定進行初始化做一些准備工作,並將其添加到對應約定集合中去。
【2】遍歷自定義約定插件集合,修改對應默認約定並返回最新約定集合。
【3】通過約定分發機制,處理獲取得到的最新約定集合。
上述第【1】和【2】步通過如下代碼實現:
public virtual ConventionSet CreateConventionSet() { var conventionSet = _conventionSetBuilder.CreateConventionSet(); foreach (var plugin in _plugins) { conventionSet = plugin.ModifyConventions(conventionSet); } return conventionSet; }
EntityFramework Core內置提供了三個創建默認約定集合提供者接口【IProviderConventionSetBuilder】的具體實現,分別是【ProviderConventionSetBuilder】用來構建針對數據庫使用的默認約定集合的提供者,【RelationalConventionSetBuilder】用來構建模型與數據庫映射的默認約定集合的提供者,【SqlServerConventionSetBuilder】用來針對SQL Server數據庫構建默認約定集合的提供者,三者繼承關系如下:
public class SqlServerConventionSetBuilder : RelationalConventionSetBuilder { var conventionSet = base.CreateConventionSet(); ...... } public abstract class RelationalConventionSetBuilder : ProviderConventionSetBuilder { public override ConventionSet CreateConventionSet() { var conventionSet = base.CreateConventionSet(); var tableNameFromDbSetConvention = new TableNameFromDbSetConvention(Dependencies, RelationalDependencies); conventionSet.EntityTypeAddedConventions.Add(new RelationalTableAttributeConvention(Dependencies, RelationalDependencies)); conventionSet.EntityTypeAddedConventions.Add(tableNameFromDbSetConvention); ReplaceConvention(conventionSet.EntityTypeBaseTypeChangedConventions, valueGenerationConvention); conventionSet.EntityTypeBaseTypeChangedConventions.Add(tableNameFromDbSetConvention); return conventionSet; } } public class ProviderConventionSetBuilder : IProviderConventionSetBuilder { public virtual ConventionSet CreateConventionSet() {
...... } }
如上多余我們用不到的約定已經剔除,我們看到往【EntityTypeAddedConventions】約定集合中先后添加了【RelationalTableAttributeConvention】和【TableNameFromDbSetConvention】對於表名的約定,對於【TableNameFromDbSetConvention】約定在構造實例化時做了如下操作:
public class TableNameFromDbSetConvention : IEntityTypeAddedConvention, IEntityTypeBaseTypeChangedConvention { private readonly IDictionary<Type, DbSetProperty> _sets; public TableNameFromDbSetConvention( [NotNull] ProviderConventionSetBuilderDependencies dependencies, [NotNull] RelationalConventionSetBuilderDependencies relationalDependencies) { _sets = dependencies.SetFinder.CreateClrTypeDbSetMapping(dependencies.ContextType); Dependencies = dependencies; } ...... }
我們繼續看上述通過上下文是如何獲取對應模型的DbSet屬性的呢?
public static IDictionary<Type, DbSetProperty> CreateClrTypeDbSetMapping( [NotNull] this IDbSetFinder setFinder, [NotNull] Type contextType) { var sets = new Dictionary<Type, DbSetProperty>(); var alreadySeen = new HashSet<Type>(); foreach (var set in setFinder.FindSets(contextType)) { if (!alreadySeen.Contains(set.ClrType)) { alreadySeen.Add(set.ClrType); sets.Add(set.ClrType, set); } else { sets.Remove(set.ClrType); } } return sets; }
因為在初始化上下文時我們就已經對上下文中的所有DbSet屬性進行了緩存,所以通過如上方法就是獲取模型與對應上下文緩存的DbSet屬性的映射,還是很好理解,如下也給出調試源碼時所顯示Blog對應的DbSet屬性信息。
現在我們已經獲取到了所有默認約定集合,接下來實例化ModelBuilder,將默認約定集合作為參數傳進去,如下:
public class ModelBuilder : IInfrastructure<InternalModelBuilder> { private readonly InternalModelBuilder _builder; public ModelBuilder([NotNull] ConventionSet conventions) { _builder = new InternalModelBuilder(new Model(conventions)); } }
接下來繼續實例化Model,傳入默認約定集合,開始實例化約定分配類並通過約定分發機制對模型進行處理,如下:
public class Model : ConventionAnnotatable, IMutableModel, IConventionModel { public Model([NotNull] ConventionSet conventions) { var dispatcher = new ConventionDispatcher(conventions); var builder = new InternalModelBuilder(this); ConventionDispatcher = dispatcher; Builder = builder; dispatcher.OnModelInitialized(builder); } }
上述【ConventionDispatcher】類就是對模型的各個階段進行分發處理(關於分發處理機制后續再單獨通過一篇博客來詳細分析),因為上述我們將表名的兩個約定放在【EntityTypeAddedConventions】集合中,接下來我們來到約定分發機制對該約定集合中12個默認約定遍歷處理,如下:
public override IConventionEntityTypeBuilder OnEntityTypeAdded(IConventionEntityTypeBuilder entityTypeBuilder) { using (_dispatcher.DelayConventions()) { _entityTypeBuilderConventionContext.ResetState(entityTypeBuilder); foreach (var entityTypeConvention in _conventionSet.EntityTypeAddedConventions) { entityTypeConvention.ProcessEntityTypeAdded(entityTypeBuilder, _entityTypeBuilderConventionContext); } } return entityTypeBuilder; }
因為首先添加的【RelationalTableAttributeConvention】約定,所以當遍歷到【RelationalTableAttributeConvention】約定時,就去到處理該約定的具體實現,說白了該約定就是獲取表名的注解即遍歷特性,如下:
public virtual void ProcessEntityTypeAdded( IConventionEntityTypeBuilder entityTypeBuilder, IConventionContext<IConventionEntityTypeBuilder> context) { Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); var attributes = type.GetTypeInfo().GetCustomAttributes<TAttribute>(true); foreach (var attribute in attributes) { ProcessEntityTypeAdded(entityTypeBuilder, attribute, context); } }
方法【ProcessEntityTypeAdded】的最終具體實現就是設置對應具體模型的表名,如下:
protected override void ProcessEntityTypeAdded( IConventionEntityTypeBuilder entityTypeBuilder, TableAttribute attribute, IConventionContext<IConventionEntityTypeBuilder> context) { //若定義架構特性,則為模型添加架構名稱和表名特性 if (!string.IsNullOrWhiteSpace(attribute.Schema)) { entityTypeBuilder.ToTable(attribute.Name, attribute.Schema, fromDataAnnotation: true); } else if (!string.IsNullOrWhiteSpace(attribute.Name)) { //若表名非空,則添加模型表名為定義的表名特性 entityTypeBuilder.ToTable(attribute.Name, fromDataAnnotation: true); } }
有童鞋就問了,我們在表特性上只定義架構名稱,那么上述不就產生bug了嗎,用過注解的都知道既然在表特性上提供了架構名稱,那么表名必須提供,但是表名提供,架構名稱可不提供,所以上述處理邏輯並沒任何毛病。
我們繼續看上述在【RelationalEntityTypeBuilderExtensions】類中對於ToTable方法的實現,如下:
public static IConventionEntityTypeBuilder ToTable( [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, [CanBeNull] string name, bool fromDataAnnotation = false) { if (!entityTypeBuilder.CanSetTable(name, fromDataAnnotation)) { return null; } entityTypeBuilder.Metadata.SetTableName(name, fromDataAnnotation); return entityTypeBuilder; }
我們看到該方法主要目的是判斷該表名是否可設置,若不可設置則返回空,否則將設置該注解的名稱作為模型的表名,我們看看上述CanSetTable又是如何判斷是否可設置呢?
public static bool CanSetTable( [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, [CanBeNull] string name, bool fromDataAnnotation = false) { Check.NullButNotEmpty(name, nameof(name)); return entityTypeBuilder.CanSetAnnotation(RelationalAnnotationNames.TableName, name, fromDataAnnotation); }
真是一層套一層,上述【RelationalAnnotationNames.TableName】是專為通過注解獲取表名而定義的常量,其值為【Relational:TableName】,此時在注解字典中不存在該鍵,最終當然也就將模型的表特性名稱作為模型的表名,如下:
public virtual bool CanSetAnnotation([NotNull] string name, [CanBeNull] object value, ConfigurationSource configurationSource) { var existingAnnotation = Metadata.FindAnnotation(name); return existingAnnotation == null || CanSetAnnotationValue(existingAnnotation, value, configurationSource, canOverrideSameSource: true); } public virtual Annotation FindAnnotation([NotNull] string name) { Check.NotEmpty(name, nameof(name)); return _annotations == null ? null : _annotations.TryGetValue(name, out var annotation) ? annotation : null; } private static bool CanSetAnnotationValue( ConventionAnnotation annotation, object value, ConfigurationSource configurationSource, bool canOverrideSameSource) { if (Equals(annotation.Value, value)) { return true; } var existingConfigurationSource = annotation.GetConfigurationSource(); return configurationSource.Overrides(existingConfigurationSource) && (configurationSource != existingConfigurationSource || canOverrideSameSource); }
上述就是ToTable方法中調用第一個方法CanSetTable是否可設置表名的過程,主要就是在注解字典中查找注解名稱為Relational:TableName是否已存在的過程,我們可以看到注解字典中不存在表名的注解名稱,接下來調用第二個方法SetTableName方法去設置表名
public static void SetTableName( [NotNull] this IConventionEntityType entityType, [CanBeNull] string name, bool fromDataAnnotation = false) => entityType.SetOrRemoveAnnotation( RelationalAnnotationNames.TableName, Check.NullButNotEmpty(name, nameof(name)), fromDataAnnotation);
接下來將是向注解字典中添加名為Relational:TableName,值為Blog2的注解,通過如下圖監控可以清楚看到:
到目前為止,對於模型Blog已經通過注解即表特性設置了表名,接下來處理約定【TableNameFromDbSetConvention】,到底是覆蓋還是跳過呢?我們還是一探其實現,如下:
public virtual void ProcessEntityTypeAdded( IConventionEntityTypeBuilder entityTypeBuilder, IConventionContext<IConventionEntityTypeBuilder> context) { var entityType = entityTypeBuilder.Metadata; if (entityType.BaseType == null && entityType.ClrType != null && _sets.ContainsKey(entityType.ClrType)) { entityTypeBuilder.ToTable(_sets[entityType.ClrType].Name); } }
首先獲取模型Blog的元數據,接下來判斷其基類是否為空,該類型的原始類型不能為空,同時在其暴露的DbSet屬性中包含該類型,很顯然都滿足條件,最后將我們上述對模型和DbSet屬性進行了映射,所以設置其表名為Blog1,如下:
如上只是滿足了條件進行設置,我們還要看看方法【ToTable】的具體實現才能最終下結論,此時依然會和注解判斷邏輯一樣,但是此時在注解字典中已存在鍵Relational:TableName,所以將跳過,如下:
好了,到此為止針對注解和DbSet對表名的設置已經討論完畢,接下來我們進行到執行OnModelCreating方法即我們自定義的設置,如下代碼:
Dependencies.ModelCustomizer.Customize(modelBuilder, context); public virtual void Customize(ModelBuilder modelBuilder, DbContext context) { context.OnModelCreating(modelBuilder); }
此時將執行到我們對Blog自定義設置的表名Blog3,我們看看最終其ToTable方法直接跳過了CanSetTable方法,直接將參數名稱賦值作為模型表名。
public static EntityTypeBuilder ToTable( [NotNull] this EntityTypeBuilder entityTypeBuilder, [CanBeNull] string name) { entityTypeBuilder.Metadata.SetTableName(name); entityTypeBuilder.Metadata.RemoveAnnotation(RelationalAnnotationNames.ViewDefinition); return entityTypeBuilder; }
到此為止對模型的初始化准備工作已經完成,接下來開始利用上下文進行操作,此時我們回到上一節利用上下文獲取表名的方法,如下:
public static string GetTableName([NotNull] this IEntityType entityType) => entityType.BaseType != null ? entityType.GetRootType().GetTableName() : (string)entityType[RelationalAnnotationNames.TableName] ?? GetDefaultTableName(entityType);
通過分析可知,無論是根據DbSet配置表名還是通過注解配置表名又或者是通過在OnModelCreating方法中自定義配置表名,最終在落地設置時,都統一以RelationalAnnotationNames.TableName即常量Relational:TableName為鍵設置表名值,所以上述若基類不存在就獲取該表名常量的值,否則都未配置表名的話,才去以模型名稱作為表名。
總結
通過此篇和上一篇我們才算對EntityFramework Core中表名的詳細解析才算明朗,我們下一個結論:EntityFramework Core對於表名的配置優先級是自定義(OnModelCreating方法)> 注解(表特性)> DbSet屬性名稱 > 模型名稱,可能我們會想何不先注冊DbSet約定,然后再注冊表特性約定,采取覆蓋的機制呢?但是事實並非如此,這里我們僅僅只是研究源碼的冰山一角或許是為了考慮其他吧。若暴露DbSet屬性,根據注冊的默認約定表名為DbSet屬性名稱,否則表名為模型名稱,若通過注解設置表名,此時上下文中暴露的DbSet屬性將會被忽略,若通過OnModelCreating方法自定義配置表名,則最終以其自定義表名為准。那么問題來了,對於屬性而言是否可以依此類推呢?想知道,只能您親自去看源碼了,逐步調試源碼驗證使得整個邏輯能夠自圓其說、閱讀博客是否有語句不通暢或錯別字,兩篇文章花費我一天多的時間,希望對閱讀本文的您能有些許收獲,謝謝。