前言

上一节我们针对最开始抛出的异常只是进行了浅尝辄止的解析,是不是有点意犹未尽的感觉,是的,我也有这种感觉,看到这里相信您和我会有一些疑惑,要是我们接下来通过注解、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 = , 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方法自定义配置表名,则最终以其自定义表名为准。那么问题来了,对于属性而言是否可以依此类推呢?想知道,只能您亲自去看源码了,逐步调试源码验证使得整个逻辑能够自圆其说、阅读博客是否有语句不通畅或错别字,两篇文章花费我一天多的时间,希望对阅读本文的您能有些许收获,谢谢。

EntityFramework Core表名原理解析,让我来,揭开你神秘的面纱的更多相关文章

  1. SpringBoot与MybatisPlus3.X整合之动态表名 SQL 解析器(七)

    pom.xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId& ...

  2. efcore分表分库原理解析

    ShardingCore ShardingCore 易用.简单.高性能.普适性,是一款扩展针对efcore生态下的分表分库的扩展解决方案,支持efcore2+的所有版本,支持efcore2+的所有数据 ...

  3. .NET CORE学习笔记系列(5)——ASP.NET CORE的运行原理解析

    一.概述 在ASP.NET Core之前,ASP.NET Framework应用程序由IIS加载.Web应用程序的入口点由InetMgr.exe创建并调用托管,初始化过程中触发HttpApplicat ...

  4. EntityFramework Core一劳永逸动态加载模型,我们要知道些什么呢?

    前言 这篇文章源于一位问我的童鞋:在EntityFramework Core中如何动态加载模型呢?在学习EntityFramwork时关于这个问题已有对应园友给出答案,故没有过多研究,虽然最后解决了这 ...

  5. EntityFramework Core高并发深挖详解,一纸长文,你准备好了吗?

    前言 之前有关EF并发探讨过几次,但是呢,博主感觉还是有问题,为什么会觉得有问题,其实就是理解不够透彻罢了,于是在项目中都是用的存储过程或者SQL语句来实现,利用放假时间好好补补EF Core并发的问 ...

  6. EntityFramework Core并发深挖详解,一纸长文,你准备好看完了吗?

    前言 之前有关EF并发探讨过几次,但是呢,博主感觉还是有问题,为什么会觉得有问题,其实就是理解不够透彻罢了,于是在项目中都是用的存储过程或者SQL语句来实现,利用放假时间好好补补EF Core并发的问 ...

  7. EntityFramework Core 运行dotnet ef命令迁移背后本质是什么?(EF Core迁移原理)

    前言 终于踏出第一步探索EF Core原理和本质,过程虽然比较漫长且枯燥乏味还得反复论证,其中滋味自知,EF Core的强大想必不用我再过多废话,有时候我们是否思考过背后到底做了些什么,到底怎么实现的 ...

  8. Cookies 初识 Dotnetspider EF 6.x、EF Core实现dynamic动态查询和EF Core注入多个上下文实例池你知道有什么问题? EntityFramework Core 运行dotnet ef命令迁移背后本质是什么?(EF Core迁移原理)

    Cookies   1.创建HttpCookies Cookie=new HttpCookies("CookieName");2.添加内容Cookie.Values.Add(&qu ...

  9. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

随机推荐

  1. 最短路径Dijkstra算法和Floyd算法整理、

    转载自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html 最短路径—Dijkstra算法和Floyd算法 Dijks ...

  2. mysql 第一课 win7艰难的配置与删除注册表重新安装

    mysql 这个安装一定要慎重!慎重!慎重! 重要的事情说三遍.. 不会的一定要按步骤来~出错了真的很难受. 详细教程 -->  https://jingyan.baidu.com/album/ ...

  3. js 制作分页

    如图所示 在html中调用方法 getpage(7, 1, 1, 'URL') 1.page.js文件 代码 function getpage(count, countPage, pageIndex, ...

  4. HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite

    org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite r ...

  5. java List接口中常用类

    Vector:线程安全,但速度慢,已被ArrayList替代. ArrayList:线程不安全,查询速度快. LinkedList:链表结构,增删速度快.取出List集合中元素的方式: get(int ...

  6. linux模块参数

    驱动需要知道的几个参数因不同的系统而不同. 从使用的设备号( 如我们在下一章见到的 ) 到驱动应当任何操作的几个方面. 例如, SCSI 适配器的驱动常常有选项控制标记命令队列 的使用, IDE 驱动 ...

  7. There is no PasswordEncoder mapped for the id "null"的解决办法

    今日在SpringBoot项目中使用 Spring Security ,登录时发现报500错,报错信息如下: There is no PasswordEncoder mapped for the id ...

  8. CSU 2005: Nearest Maintenance Point(Dijkstra + bitset)

    Description A county consists of n cities (labeled 1, 2, …, n) connected by some bidirectional roads ...

  9. WPF 使用 SharpDx 异步渲染

    本文告诉大家如何通过 SharpDx 进行异步渲染,但是因为在 WPF 是需要使用 D3DImage 画出来,所以渲染只是画出图片,最后的显示还是需要 WPF 在他自己的主线程渲染 本文是一个系列,希 ...

  10. 多校 HDU - 6614 AND Minimum Spanning Tree (二进制)

    传送门 AND Minimum Spanning Tree Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 131072/131072 ...