EntityFramework Core表名原理解析,讓我來,揭開你神祕的面紗

前言

上一節咱們針對最開始拋出的異常只是進行了淺嘗輒止的解析,是否是有點意猶未盡的感受,是的,我也有這種感受,看到這裏相信您和我會有一些疑惑,要是咱們接下來經過註解、Fluent APi、DbSet分別對錶名進行以下設置,是否會拋出異常呢?若不是,有其優先級,那麼其優先級究竟是怎樣的呢?內置具體是如何實現的呢?讓咱們從頭開始揭開其神祕的面紗。html

EntityFramework Core表名原理解析

咱們暫不知道究竟是否有其優先級仍是會拋出異常,那麼接下來咱們進行以下配置(模型請參考上一節《http://www.javashuo.com/article/p-prcnjiln-da.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屬性並緩存到內存中app

【2】以上下文做爲緩存的鍵,將上下文中的全部模型數據緩存在內存中,若未緩存執行第【3】步。ide

【3】建立上下文中全部模型有關數據。ui

查找DbSet屬性並緩存

接下來咱們步步分析,步步逼近以上三步操做實現,不管是主動實例化仍是在Web中添加上下文中間件時,都必須通過將咱們須要用到全部接口進行依賴注入,固然EntityFramework Core是用的【 Microsoft.Extensions.DependencyInjection 】庫,至於註冊了哪些,這些細節咱們並不關心,咱們只關注所須要用到的且會一一說明,獲取接口【IDbSetInitializer】的具體實現【DbSetInitializer】,調用該類中的以下方法:this

        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的查找和緩存就已完事,接下來去建立上下文中的全部模型數據。spa

建立上下文模型

首先是去獲取上下文中全部模型數據,以上下文爲鍵去查找緩存的模型數據,若沒有則建立,不然建立緩存,以下:插件

         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方法自定義配置表名,則最終以其自定義表名爲準。那麼問題來了,對於屬性而言是否能夠依此類推呢?想知道,只能您親自去看源碼了,逐步調試源碼驗證使得整個邏輯可以自圓其說、閱讀博客是否有語句不通暢或錯別字,兩篇文章花費我一天多的時間,但願對閱讀本文的您能有些許收穫,謝謝。

相關文章
相關標籤/搜索