Entity Framework教程(第二版)

发布时间:2026/7/3 1:13:27

Entity Framework教程(第二版) 很多年前刚毕业那阵写过一篇关于Entity Framework的文章没发首页却得到100的推荐。可能是当时Entity Framework刚刚发布介绍EF的文章比较少。一晃这么多年过去了EF6.1已经发布很久EF7马上就到来。那篇文章已经显得相当过时这期间园子里出现了很多介绍EF4/5/6版本的精彩文章我的工作中也没有在持续使用EF本来也就不准备再写现在这篇文章了。后来看到之前那篇文章还是有很多朋友在评论里给予鼓励再加上自己确实在使用新版EF的过程中也总结了一些心得解决了一些问题。这里打算将这些新的经验分享出来本文不会像之前那片文章一样从头到尾完整的讲解EFEF现在覆盖的面太大了完整的介绍需要一本很厚的书本文主要是总结一些我感觉EF中特别重要的组件和一些可以被称作最佳实践的使用方式。当然有不对的地方还请各位赐教。由于对传说中的领域设计不懂一小部分内容可能不符合ldquo;领域设计rdquo;的要求都是自己项目用着顺手的写法本文更没有完整的示例大部分代码都是边修改边测试边粘贴到文章中下文代码在EF6.1.3中测试过。我的大部分EF使用经验都学习自大名鼎鼎的nopCommerce这个开源项目后文会提到有兴趣的TX可以看一下这个项目的代码一定会有很大收获。当然每家的代码都有自己的风格能取长补短也是很不错的。另外感谢园友liulun提供51cnblogs这个颜值很高的博客园博文编辑器。下面开始正题文章很长慢慢看吧。EF的发展历程还是先来说一下EF从诞生到现在这几年的发展历程吧。在EF最初的版本中作为一个ORM组件其通过EDM文件(里面是一些xml)来配置数据库与实体类之间的映射实现数据进出数据库的控制。最初的版本中只支持Database First即由已有数据库结构生成EDM继而得到实体类。后来EF在4.0版本起开始支持Model First即先建立EDM然后生成数据库。在4.1版本开始EF迎来了最大的变化--开始支持Code First模式值得注意的是Code First不是和Database First或Model First平级的概念而是和EDM平级的概念。使用Code First不再需要EDM来维护实体与数据库之间的映射关系这个映射完全通过代码来完成并在程序开始运行时在内存中建立一个映射模型这也就是Code First这个名称中Code的含义。使用Code First一般都是先建立实体然后通过代码配置实体到数据库的映射继而生成数据库(如果数据库已存在就不需要再生成数据库可以直接建立代码映射模型)这也就是所谓的Model First模式。当然Code First也支持Database First通过工具由现有数据库生成实体及实体映射数据库的代码。选择关于EDM的详细信息可以参考前文提到的那片文章由于Code First的魅力极大EDM文件又存在不利于版本管理等天生缺陷基本上处于一个被抛弃的状态。而且看园子中一些文章说在EF7版本可能取消EDM的支持只保留Code First。我在第一次接触Code Frist后就一直在使用它了完全不在考虑EDM。下文中我们将只以Code First为例来介绍EF而不再涉及EDM。核心随着Code First一起出现的DbContext和DbSet类绝对可以称得上EF的功能核心其取代了之前的ObjectContext和ObjectSet类提供了与数据库通信管理内存中实体的重要功能。DbContext类主要是负责与数据库进行通信管理实体到数据库的映射模型跟踪实体的更改正如这个类名字Context所示其维护了一个EF内存中容器保存所有被加载的实体并跟踪其状态。关于模型映射和更改跟踪下面都有专门的小节来讨论。Dbcontext中最常用的几个方法如SaveChanges(和6.0开始增加的异步方法SaveChangesAsync)用于将实体的修改保存到数据库。SetT获取实体相应的DbSet对象我们对实体的增删改查操作都是通过这个对象来进行的。还有几个次常用但很重要的属性方法Database属性一个数据库对象的表示通过其SqlQuery、ExecuteSqlCommand等方法可以直接执行一些Sql语句或SqlCommandEF6起可以通过Database对象控制事务。Entry获取EF Context中的实体的状态在更改跟踪一节会讨论其作用。ChangeTracker返回一个DbChangeTracker对象通过这个对象的Entries属性我们可以查询EF Context中所有缓存的实体的状态。DbSet类这个类的对象正是通过刚刚提到的SetT方法获取的对象。其中的方法都与操作实体有关如Find/FindAsync按主键获取一个实体首先在EF Context中查找是否有被缓存过的实体如果查找不到再去数据库查找如果数据库中存在则缓存到EF Context并返回否则返回null。Attach将一个已存在于数据库中的对象添加到EF Context中实体状态被标记为Unchanged。对于已有相同key的对象存在于EF Context的情况如果这个已存在对象状态为Unchanged则不进行任何操作否则将其状态更改为Unchanged。Add将一个已存在于数据库中的对象添加到EF Context中实体状态被标记为Added。对于已有相同key的对象存在于EF Context且状态为Added则不进行任何操作。Remove将一个已存在于EF Context中的对象标记为Deleted当SaveChanges时这个对象对应的数据库条目被删除。注意调用此方法需要对象已经存在于EF Context。Include详见下面预加载一节。AsNoTracking相见变更跟踪一节。Local属性用来跟踪所有EF Context中状态为AddedModified、Unchanged的实体。作用好像不是太大。没怎么用过。Create这个方法至今好像没有用到过不知道干啥的。有了解的评论中给解释下吧。映射说一千道一万EF还是一个ORM工具映射永远是最核心的部分。所以接下来详细介绍Code First模式下EF的映射配置。通过Code First来实现映射模型有两种方式Data Annotation和Fluent API。Data Annotation需要在实体类我通常的称呼一般就是一个Plain Object的属性上以Attribute的方式表示主键、外键等映射信息。这种方式不符合解耦合的要求所以一般不建议使用。第二种方式就是要重点介绍的Fluent API。Fluent API的配置方式将实体类与映射配置进行解耦合有利于项目的扩展和维护。Fluent API方式中的核心对象是DbModelBuilder。在重写的DbContext的OnModelCreating方法中我们可以这样配置一个实体的映射123456protectedoverridevoidOnModelCreating(DbModelBuilder modelBuilder){modelBuilder.EntityProduct().HasKey(t t.Id);base.OnModelCreating(modelBuilder);}使用上面这种方式的一个问题是OnModelCreating方法会随着映射配置的增多越来越大。一种更好的方式是继承EntityTypeConfigurationEntityType并在这个类中添加映射代码如123456789publicclassProductMap : EntityTypeConfigurationProduct{publicProductMap(){this.ToTable(Product);this.HasKey(p p.Id);this.Property(p p.Name).IsRequired();}}然后将这个类的实例添加到modelBuilder的Configurations就可以了。1modelBuilder.Configurations.Add(newProductMap());如果不想手动一个个添加自定的映射配置类对象还可以使用反射将程序集中所有的EntityTypeConfiguration一次性添加到modelBuilder.Configurations集合中下面的代码展示了这个操作代码来自nopCommerce项目12345678vartypesToRegister Assembly.GetExecutingAssembly().GetTypes().Where(type !String.IsNullOrEmpty(type.Namespace)).Where(type type.BaseType !null type.BaseType.IsGenericType type.BaseType.GetGenericTypeDefinition() typeof(EntityTypeConfiguration));foreach(vartypeintypesToRegister){dynamic configurationInstance Activator.CreateInstance(type);modelBuilder.Configurations.Add(configurationInstance);}这样OnModelCreating就大大简化并且一劳永逸的是以后添加新的实体映射只需要添加新的继承自EntityTypeConfiguration的XXXMap类而不需要修改OnModelCreating方法。这种方式给实体和映射提供最佳的解耦合强烈推荐。EF CodeFirst的自动发现例如我们的程序中有一个名为Employee的实体类我们没有为其定义映射配置(EntityTypeConfigurationEmployee)但如果我们使用类似下面这样的代码去进行调用EF会自动为Employee创建默认映射并进行迁移等一系列操作。1varemployeeList context.SetEmployee().ToList();当然为了能更灵活的配置映射还是建议手动创建EntityTypeConfigurationEmployee。另外2种情况下EF也会自动创建映射。类A的对象作为类B的一个导航属性存在如果类B被包含在EF映射中则EF也会为类A创建默认映射。类A继承自类B如果类A或类B中的一个被包含在EF映射中则EF也会为另一个创建默认映射且使用TPH方式进行详见下文映射高级话题。通过上面的介绍可以看到EntityTypeConfiguration类正事Fluent API的核心下面我们以EntityTypeConfiguration的方法为线依次了解如何进行Fluent API配置。基本方法ToTable指定映射到的数据库表的名称。HasKey配置主键也用于配置关联主键Property这个方法返回PrimitivePropertyConfiguration的对象根据属性不同可能是子类StringPropertyConfiguration的对象。通过这个对象可以详细配置属性的信息如IsRequired()或HasMaxLength(400)。Ignore指定忽略哪个属性不映射到数据表对于基本映射这几个方法几乎包括了一切下面是个综合示例1234567ToTable(Product);ToTable(Product,newdbo);//指定schema不使用默认的dboHasKey(p p.Id);//普通主键HasKey(p new{p.Id, p.Name});//关联主键Property(p p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);//不让主键作为Identity自动生成Property(p p.Name).IsRequired().HasMaxLength(20).HasColumnName(ProductName).IsUnicode(false);//非空最大长度20自定义列名列类型为varchar而非nvarcharIgnore(p p.Description);使用modelBuilder.HasDefaultSchema(newdbo);可以给所有映射实体指定schema。PrimitivePropertyConfiguration还有许多可配置的选项如HasColumnOrder指定列在表中次序IsOptional指定列是否可空HasPrecision指定浮点数的精度等等不再列举。配置关联下面一系列示例的主角是产品为了配合演示还请了产品小伙伴们它们将在演示过程中逐一登场。基本上下面展示的关联的配置都可以从关联类的任意一方的EntityTypeConfigurationT开始配置。无论从哪一方起开始配置不同的写法最终都能实现相同的效果。下面的示例将只展示其中之一配置的方式等价的另一种配置不再展示。产品类的基本结构如下后面演示过程中将根据需要为其添加新的属性。123456publicclassProduct{publicintId{get;set; }publicstringName {get;set; }publicstringDescription {get;set; }}1 - 1关联虽然看起来最简单但这个好像是理解起来最麻烦的一种配置这种关联从实际关系上来看是两个类共享相同的值作为主键比如有User表和UserPhoto表他们都应该使用UserId作为主键并且通过相同的UserId值进行关联。但这种关系反映在数据库中必须通过外键的概念来实现这时候就需要一个表的主键既作为主键又作为关联表的外键。EF中各种配置方式无非就是告诉EF CodeFirst让那个表的主键作为另一个表的外键而已现在不理解的看一下下面的例子就明白了。其实如果用Data Annotation配置反而很简单[Key],[ForeignKey]标一标就可以了这节使用到的是保修卡这个角色我们知道一个产品对应一个保修卡产品和保修卡使用相同的产品编号。这正是我们说的1对1的好例子。123456publicclassWarrantyCard{publicintProductId {get;set; }publicDateTime ExpiredDate {get;set; }publicvirtualProduct Product {get;set; }}我们给Product也增加保修卡属性1publicvirtualWarrantyCard WarrantyCard {get;set; }下面来看看怎么把Product和WarrantyCard关联起来。经过ldquo;千百rdquo;次的尝试终于找到了下面这些结果看起来很正确的组合先列于下方后面慢慢分析12345678910111213141516171819202122232425publicclassProductMap : EntityTypeConfigurationProduct{publicProductMap(){ToTable(Product);HasKey(p p.Id);//第一组两条效果完全相同HasRequired(p p.WarrantyCard).WithRequiredDependent(i i.Product);HasRequired(p p.WarrantyCard).WithOptional(i i.Product);//第二组两条效果完全相同HasRequired(p p.WarrantyCard).WithRequiredPrincipal(i i.Product);HasOptional(p p.WarrantyCard).WithRequired(i i.Product);}}publicclassWarrantyCardMap : EntityTypeConfigurationWarrantyCard{publicWarrantyCardMap(){ToTable(WarrantyCard);HasKey(i i.ProductId);}}除了以上这些组合其它组合都没法达到效果都会生成多余的外键。第一组Fluent API生成的迁移代码1234567891011121314151617181920CreateTable(dbo.Product,c new{Id c.Int(nullable:false),Name c.String(),Description c.String(maxLength: 200),}).PrimaryKey(t t.Id).ForeignKey(dbo.WarrantyCard, t t.Id).Index(t t.Id);CreateTable(dbo.WarrantyCard,c new{ProductId c.Int(nullable:false, identity:true),ExpiredDate c.DateTime(nullable:false),}).PrimaryKey(t t.ProductId);值得注意的是外键指定在Product表的Id列上Product的主键Id不作为标识列。再来看看第二组Fluent API生成的迁移代码1234567891011121314151617181920CreateTable(dbo.Product,c new{Id c.Int(nullable:false, identity:true),Name c.String(),Description c.String(maxLength: 200),}).PrimaryKey(t t.Id);CreateTable(dbo.WarrantyCard,c new{ProductId c.Int(nullable:false),ExpiredDate c.DateTime(nullable:false),}).PrimaryKey(t t.ProductId).ForeignKey(dbo.Product, t t.ProductId).Index(t t.ProductId);变化就在于外键添加到WarrantyCard表的主键ProductId上而且这个键也不做标识列使用了。对于当前场景这两组配置应该选择那一组呢。对于产品和保修卡肯定是先有产品后有保修卡保修卡应该依赖于产品而存在。所以第二组配置把外键设置到WarrantyCard的主键更为合适让WarrantyCard依赖Product符合当前场景。即Product作为Principal而WarrantyCard作为Dependent其实这么多代码也无非就是明确两个关联对象Principal和Dependent的地位而已。使用第二组配置创建表后我们可以添加数据可以一次性添加保修卡和合格证1234567891011varproduct newProduct(){Name 空调,Description 冰冰凉,WarrantyCard newWarrantyCard(){ExpiredDate DateTime.Now.AddYears(3)}};context.SetProduct().Add(product);context.SaveChanges();也可以分开进行123456789101112131415varproduct newProduct(){Name 投影仪,Description 高分辨率};context.SetProduct().Add(product);context.SaveChanges();WarrantyCard card newWarrantyCard(){ProductId product.Id,ExpiredDate DateTime.Now.AddYears(3)};context.SetWarrantyCard().Add(card);context.SaveChanges();对于查询来说第一组和第二组配置生成的SQL相同。都是INNER JOIN这里就不再列出了。

相关新闻