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方法自定义配置表名,则最终以其自定义表名为准。那么问题来了,对于属性而言是否能够依此类推呢?想知道,只能您亲自去看源码了,逐步调试源码验证使得整个逻辑可以自圆其说、阅读博客是否有语句不通畅或错别字,两篇文章花费我一天多的时间,但愿对阅读本文的您能有些许收获,谢谢。

相关文章
相关标签/搜索