Entity Framework 基础知识走马观花
本文目录:
一、EF中的edmx文件
1.1 emdx文件本质:一个XML文件
(1)通过选择以XML方式打开edmx文件,我们可以可以清楚地看到,edmx模型文件本质就是一个XML文件;
(2)可以清楚地看到,edmx模型文件是一个XML文件,其中定义了三大组成部分,这三大组成部分构成了所谓的ORM(对象关系映射);
(3)再通过解决方案管理器分析edmx模型文件,其包含了三个子文件:
①第一个是xxx.Context.tt,这个首先是一个T4的模板文件,它生成了我们这个模型的上下文类;
public partial class LearnEntities : DbContext { public LearnEntities() : base("name=LearnEntities") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { throw new UnintentionalCodeFirstException(); } public DbSet<T_Class> T_Class { get; set; } public DbSet<T_Message> T_Message { get; set; } public DbSet<T_Person> T_Person { get; set; } }
②第二个是设计器部分,它定义了模型关系图的元数据,比如每个类图的宽度多少,在图中的坐标(X、Y轴)等;
<edmx:Diagrams> <Diagram DiagramId="edad56670ede49c5ae2e343c9da730f1" Name="关系图1"> <EntityTypeShape EntityType="LearnModel.T_Class" Width="1.5" PointX="0.75" PointY="1.75" /> <EntityTypeShape EntityType="LearnModel.T_Message" Width="1.5" PointX="5.25" PointY="1" /> <EntityTypeShape EntityType="LearnModel.T_Person" Width="1.5" PointX="3" PointY="1.25" /> <AssociationConnector Association="LearnModel.FK_T_Person_T_Class" /> <AssociationConnector Association="LearnModel.FK_T_Message_T_Person1" /> <AssociationConnector Association="LearnModel.FK_T_Message_T_Person2" /> </Diagram> </edmx:Diagrams>
③第三个就是数据库表中所对应的实体类对象,它也是一个T4模板文件,对应了所有选择的数据库表:
public partial class T_Class { public T_Class() { this.T_Person = new HashSet<T_Person>(); } public int Id { get; set; } public string Name { get; set; } public virtual ICollection<T_Person> T_Person { get; set; } }
1.2 emdx组成部分:SSDL、CSDL、C-S Mapping
(1)SSDL
它定义了数据库中所对应的表的定义,也可以称为存储模型:
<edmx:StorageModels> <Schema Namespace="LearnModel.Store" Alias="Self" Provider="System.Data.SqlClient" ProviderManifestToken="2008" xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="http://schemas.microsoft.com/ado/2009/02/edm/ssdl"> <EntityContainer Name="LearnModelStoreContainer"> <EntitySet Name="T_Class" EntityType="LearnModel.Store.T_Class" store:Type="Tables" Schema="dbo" /> <EntitySet Name="T_Message" EntityType="LearnModel.Store.T_Message" store:Type="Tables" Schema="dbo" /> <EntitySet Name="T_Person" EntityType="LearnModel.Store.T_Person" store:Type="Tables" Schema="dbo" /> <AssociationSet Name="FK_T_Message_T_Person1" Association="LearnModel.Store.FK_T_Message_T_Person1"> <End Role="T_Person" EntitySet="T_Person" /> <End Role="T_Message" EntitySet="T_Message" /> </AssociationSet> <AssociationSet Name="FK_T_Message_T_Person2" Association="LearnModel.Store.FK_T_Message_T_Person2"> <End Role="T_Person" EntitySet="T_Person" /> <End Role="T_Message" EntitySet="T_Message" /> </AssociationSet> <AssociationSet Name="FK_T_Person_T_Class" Association="LearnModel.Store.FK_T_Person_T_Class"> <End Role="T_Class" EntitySet="T_Class" /> <End Role="T_Person" EntitySet="T_Person" /> </AssociationSet> </EntityContainer> <EntityType Name="T_Class"> <Key> <PropertyRef Name="Id" /> </Key> <Property Name="Id" Type="int" Nullable="false" StoreGeneratedPattern="Identity" /> <Property Name="Name" Type="nvarchar" MaxLength="100" /> </EntityType> <EntityType Name="T_Message"> <Key> <PropertyRef Name="Id" /> </Key> <Property Name="Id" Type="int" Nullable="false" StoreGeneratedPattern="Identity" /> <Property Name="Title" Type="nvarchar" Nullable="false" MaxLength="250" /> <Property Name="Message" Type="nvarchar(max)" Nullable="false" /> <Property Name="NickName" Type="nvarchar" Nullable="false" MaxLength="50" /> <Property Name="IsAnonymous" Type="bit" Nullable="false" /> <Property Name="IPAddress" Type="nvarchar" Nullable="false" MaxLength="50" /> <Property Name="PostDate" Type="datetime" Nullable="false" /> <Property Name="PostManId" Type="int" Nullable="false" /> <Property Name="ReceiveManId" Type="int" Nullable="false" /> </EntityType> <EntityType Name="T_Person"> <Key> <PropertyRef Name="Id" /> </Key> <Property Name="Id" Type="int" Nullable="false" StoreGeneratedPattern="Identity" /> <Property Name="Name" Type="nvarchar" Nullable="false" MaxLength="50" /> <Property Name="Age" Type="int" Nullable="false" /> <Property Name="Email" Type="nvarchar" Nullable="false" MaxLength="100" /> <Property Name="ClassId" Type="int" Nullable="false" /> </EntityType> <Association Name="FK_T_Message_T_Person1"> <End Role="T_Person" Type="LearnModel.Store.T_Person" Multiplicity="1" /> <End Role="T_Message" Type="LearnModel.Store.T_Message" Multiplicity="*" /> <ReferentialConstraint> <Principal Role="T_Person"> <PropertyRef Name="Id" /> </Principal> <Dependent Role="T_Message"> <PropertyRef Name="PostManId" /> </Dependent> </ReferentialConstraint> </Association> <Association Name="FK_T_Message_T_Person2"> <End Role="T_Person" Type="LearnModel.Store.T_Person" Multiplicity="1" /> <End Role="T_Message" Type="LearnModel.Store.T_Message" Multiplicity="*" /> <ReferentialConstraint> <Principal Role="T_Person"> <PropertyRef Name="Id" /> </Principal> <Dependent Role="T_Message"> <PropertyRef Name="ReceiveManId" /> </Dependent> </ReferentialConstraint> </Association> <Association Name="FK_T_Person_T_Class"> <End Role="T_Class" Type="LearnModel.Store.T_Class" Multiplicity="1" /> <End Role="T_Person" Type="LearnModel.Store.T_Person" Multiplicity="*" /> <ReferentialConstraint> <Principal Role="T_Class"> <PropertyRef Name="Id" /> </Principal> <Dependent Role="T_Person"> <PropertyRef Name="ClassId" /> </Dependent> </ReferentialConstraint> </Association> </Schema> </edmx:StorageModels>
例如,我们可以通过下面的一个代码片段来看看它在说明什么?
<EntityType Name="T_Person"> <Key> <PropertyRef Name="Id" /> </Key> <Property Name="Id" Type="int" Nullable="false" StoreGeneratedPattern="Identity" /> <Property Name="Name" Type="nvarchar" Nullable="false" MaxLength="50" /> <Property Name="Age" Type="int" Nullable="false" /> <Property Name="Email" Type="nvarchar" Nullable="false" MaxLength="100" /> <Property Name="ClassId" Type="int" Nullable="false" /> </EntityType>
是不是跟我们在MSSQL中所进行的数据表设计差不多?指定主键、指定字段的类型、是否为NULL,最大长度等等;
(2)CSDL
它定义了EF模型中与SSDL对应的实体类对象的定义,这里C代表Concept,即概念模型;
<edmx:ConceptualModels> <Schema Namespace="LearnModel" Alias="Self" xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns="http://schemas.microsoft.com/ado/2008/09/edm"> <EntityContainer Name="LearnEntities" annotation:LazyLoadingEnabled="true"> <EntitySet Name="T_Class" EntityType="LearnModel.T_Class" /> <EntitySet Name="T_Message" EntityType="LearnModel.T_Message" /> <EntitySet Name="T_Person" EntityType="LearnModel.T_Person" /> <AssociationSet Name="FK_T_Person_T_Class" Association="LearnModel.FK_T_Person_T_Class"> <End Role="T_Class" EntitySet="T_Class" /> <End Role="T_Person" EntitySet="T_Person" /> </AssociationSet> <AssociationSet Name="FK_T_Message_T_Person1" Association="LearnModel.FK_T_Message_T_Person1"> <End Role="T_Person" EntitySet="T_Person" /> <End Role="T_Message" EntitySet="T_Message" /> </AssociationSet> <AssociationSet Name="FK_T_Message_T_Person2" Association="LearnModel.FK_T_Message_T_Person2"> <End Role="T_Person" EntitySet="T_Person" /> <End Role="T_Message" EntitySet="T_Message" /> </AssociationSet> </EntityContainer> <EntityType Name="T_Class"> <Key> <PropertyRef Name="Id" /> </Key> <Property Type="Int32" Name="Id" Nullable="false" annotation:StoreGeneratedPattern="Identity" /> <Property Type="String" Name="Name" MaxLength="100" FixedLength="false" Unicode="true" /> <NavigationProperty Name="T_Person" Relationship="LearnModel.FK_T_Person_T_Class" FromRole="T_Class" ToRole="T_Person" /> </EntityType> <EntityType Name="T_Message"> <Key> <PropertyRef Name="Id" /> </Key> <Property Type="Int32" Name="Id" Nullable="false" annotation:StoreGeneratedPattern="Identity" /> <Property Type="String" Name="Title" Nullable="false" MaxLength="250" FixedLength="false" Unicode="true" /> <Property Type="String" Name="Message" Nullable="false" MaxLength="Max" FixedLength="false" Unicode="true" /> <Property Type="String" Name="NickName" Nullable="false" MaxLength="50" FixedLength="false" Unicode="true" /> <Property Type="Boolean" Name="IsAnonymous" Nullable="false" /> <Property Type="String" Name="IPAddress" Nullable="false" MaxLength="50" FixedLength="false" Unicode="true" /> <Property Type="DateTime" Name="PostDate" Nullable="false" Precision="3" /> <Property Type="Int32" Name="PostManId" Nullable="false" /> <Property Type="Int32" Name="ReceiveManId" Nullable="false" /> <NavigationProperty Name="T_Person" Relationship="LearnModel.FK_T_Message_T_Person1" FromRole="T_Message" ToRole="T_Person" /> <NavigationProperty Name="T_Person1" Relationship="LearnModel.FK_T_Message_T_Person2" FromRole="T_Message" ToRole="T_Person" /> </EntityType> <EntityType Name="T_Person"> <Key> <PropertyRef Name="Id" /> </Key> <Property Type="Int32" Name="Id" Nullable="false" annotation:StoreGeneratedPattern="Identity" /> <Property Type="String" Name="Name" Nullable="false" MaxLength="50" FixedLength="false" Unicode="true" /> <Property Type="Int32" Name="Age" Nullable="false" /> <Property Type="String" Name="Email" Nullable="false" MaxLength="100" FixedLength="false" Unicode="true" /> <Property Type="Int32" Name="ClassId" Nullable="false" /> <NavigationProperty Name="T_Class" Relationship="LearnModel.FK_T_Person_T_Class" FromRole="T_Person" ToRole="T_Class" /> <NavigationProperty Name="T_Message" Relationship="LearnModel.FK_T_Message_T_Person1" FromRole="T_Person" ToRole="T_Message" /> <NavigationProperty Name="T_Message1" Relationship="LearnModel.FK_T_Message_T_Person2" FromRole="T_Person" ToRole="T_Message" /> </EntityType> <Association Name="FK_T_Person_T_Class"> <End Type="LearnModel.T_Class" Role="T_Class" Multiplicity="1" /> <End Type="LearnModel.T_Person" Role="T_Person" Multiplicity="*" /> <ReferentialConstraint> <Principal Role="T_Class"> <PropertyRef Name="Id" /> </Principal> <Dependent Role="T_Person"> <PropertyRef Name="ClassId" /> </Dependent> </ReferentialConstraint> </Association> <Association Name="FK_T_Message_T_Person1"> <End Type="LearnModel.T_Person" Role="T_Person" Multiplicity="1" /> <End Type="LearnModel.T_Message" Role="T_Message" Multiplicity="*" /> <ReferentialConstraint> <Principal Role="T_Person"> <PropertyRef Name="Id" /> </Principal> <Dependent Role="T_Message"> <PropertyRef Name="PostManId" /> </Dependent> </ReferentialConstraint> </Association> <Association Name="FK_T_Message_T_Person2"> <End Type="LearnModel.T_Person" Role="T_Person" Multiplicity="1" /> <End Type="LearnModel.T_Message" Role="T_Message" Multiplicity="*" /> <ReferentialConstraint> <Principal Role="T_Person"> <PropertyRef Name="Id" /> </Principal> <Dependent Role="T_Message"> <PropertyRef Name="ReceiveManId" /> </Dependent> </ReferentialConstraint> </Association> </Schema> </edmx:ConceptualModels>
当然,我们再通过一个代码片段来看看在概念模型中是如何定义的?
<EntityType Name="T_Person"> <Key> <PropertyRef Name="Id" /> </Key> <Property Type="Int32" Name="Id" Nullable="false" annotation:StoreGeneratedPattern="Identity" /> <Property Type="String" Name="Name" Nullable="false" MaxLength="50" FixedLength="false" Unicode="true" /> <Property Type="Int32" Name="Age" Nullable="false" /> <Property Type="String" Name="Email" Nullable="false" MaxLength="100" FixedLength="false" Unicode="true" /> <Property Type="Int32" Name="ClassId" Nullable="false" /> <NavigationProperty Name="T_Class" Relationship="LearnModel.FK_T_Person_T_Class" FromRole="T_Person" ToRole="T_Class" /> <NavigationProperty Name="T_Message" Relationship="LearnModel.FK_T_Message_T_Person1" FromRole="T_Person" ToRole="T_Message" /> <NavigationProperty Name="T_Message1" Relationship="LearnModel.FK_T_Message_T_Person2" FromRole="T_Person" ToRole="T_Message" /> </EntityType>
在CSDL中,大部分都与SSDL中所对应的一致,但是我们发现多了一些属性。例如:NavigationProperty 导航属性,因为T_Person表与T_Class、T_Message表都存在一对一或一对多的关系(即存在外键),因此在EF模型所生成的对象实体中,加入了外键所在实体的导航属性。
(3)C-S Mapping
它是一个映射关系,它将SSDL与CSDL对应了起来,因此我们在用EF操作实体类时才可以正确地生成对相应数据表的SQL语句。
<EntitySetMapping Name="T_Person"> <EntityTypeMapping TypeName="LearnModel.T_Person"> <MappingFragment StoreEntitySet="T_Person"> <ScalarProperty Name="ClassId" ColumnName="ClassId" /> <ScalarProperty Name="Email" ColumnName="Email" /> <ScalarProperty Name="Age" ColumnName="Age" /> <ScalarProperty Name="Name" ColumnName="Name" /> <ScalarProperty Name="Id" ColumnName="Id" /> </MappingFragment> </EntityTypeMapping> </EntitySetMapping> </EntityContainerMapping>
可以看出,这里将SSDL中的T_Person与CSDL中的LearnModel.T_Person对应了起来,当然还将他们各自的属性进行了一一对应。
二、EF中的代理类对象
2.1 代理模式初探
通过上面的图片,我们可以看到,通过增加代理类B来解耦A与C之间的调用,这样可以封装原来C调用A的一些相关细节,转换成C直接调用B中封装后的代理方法,则等同于访问A。在实际应用中,例如对于WebService的远程调用时,如果我们使用添加Web引用的方式,那么WebService会为我们自动生成代理类,我们所有的交互都只是和代理类进行的,而没有直接和服务提供者进行。
代理模式:可以看看园友程兴亮的一篇文章《极速理解设计模式之代理模式》
2.2 EF中的代理应用
(1)我们首先有下面这样一段代码,它要进行的是一个简单的修改操作:
static void Edit() { // 下面返回的是一个Person类的代理类对象 T_Person person = db.T_Person.FirstOrDefault(p => p.Id == 11); Console.WriteLine("Before update:{0}", person.ToString()); // 此时操作的也只是Person类的代理类对象,同时标记此属性为已修改 person.Name = "周旭龙"; person.Email = "edisonchou7@cuit.edu.cn"; person.Age = 25; // 此时EF上下文会检查容器内部所有的对象, // 找到标记为修改的对象属性生成对应的修改SQL语句 db.SaveChanges(); Console.WriteLine("Update Successfully~~"); }
(2)通过分析这段代码,我们知道要进行一个修改操作,需要经过三个步凑:第一是查询到要修改的那一行,然后进行修改操作赋值,最后提交修改操作。(当然,这是官方推荐的修改操作步凑,你也可以不经过查询而直接修改,这需要利用到DbEntityEntry)那么,为什么要经过这几个步凑呢?
①我们首先来看看第一步:查询
db.T_Person.FirstOrDefault(p => p.Id == 11);
在实际开发中,我们的应用程序不会直接和数据库打交道,而是和EF数据上下文中的代理类打交道。首先,通过查询操作数据库返回了一行数据,EF上下文将其接收并将其“包装”起来,于是就有了代理类。在代理类中,真实的实体类对象被封装了起来,并且在代理类中为每个属性都设置了一个标志,用来标识其状态(是否被修改)。而我们在程序中所获得的数据,都是从代理类中返回的。
②再来看看第二步:修改
person.Name = "周旭龙";
当执行完修改操作之后,代理类中对应字段的标志会被修改,例如我们这里修改了Name属性,那么其对应的标志就会由false变为true。虽然只是变了一个标志位,但是却对EF生成SQL语句产生了重大影响。如果我们只修改了一个属性,那么其生成的SQL语句只会有一个Update ** Set Name='***'。
③最后来看看第三步:提交
db.SaveChanges();
当SaveChanges方法触发时,EF上下文会遍历代理类对象中的状态标志,如果发现有修改的(即为True)则将其加入生成的SQL语句中。这里,因为Name被修改了,所以在生成的SQL语句中会将Name加入,而其他未修改的则不会加入。
我们也可以通过SQLServer Profiler来查看EF所生成的SQL语句:
三、EF中的延迟加载与即时加载
3.1 浅谈延迟加载
所谓延迟加载,就是只有在我们需要数据的时候才去数据库读取加载它。
在Queryable类中的扩展方法中,Where方法就是一个典型的延迟加载案例。在实际的开发中,我们往往会使用一些ORM框架例如EF去操作数据库,Where方法的使用则是每次调用都只是在后续生成SQL语句时增加一个查询条件,EF无法确定本次查询是否已经添加结束,所以没有办法木有办法在每个Where方法执行的时候确定最终的SQL语句,只能返回一个DbQuery对象,当使用到这个DbQuery对象的时候,才会根据所有条件生成最终的SQL语句去查询数据库。
(1)针对条件的延迟加载
DbQuery<T_Person> query = db.T_Person.Where(p => p.Id == 11).OrderBy(p => p.Age) as DbQuery<T_Person>; // 用的时候才去加载:根据之前的条件生成SQL语句访问数据库 T_Person person = query.FirstOrDefault();
通过SQLServer Profiler调试跟踪,当执行完第一行代码时,是没有进行对数据库的查询操作的。而当执行到第二行的FirstOrDefault()方法时,EF才根据前面的条件生成了查询SQL语句去加载数据。
(2)针对外键的延迟加载
首先,我们有这样两张表,他们是1:N的关系;其中ClassId是T_Person的外键;
其次,在EF所生成的实体对象中,在T_Person的代码中会有一个T_Class的对象属性;因为一个T_Person对应一个T_Class;
public partial class T_Person { public T_Person() { this.T_Message = new HashSet<T_Message>(); this.T_Message1 = new HashSet<T_Message>(); } public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public string Email { get; set; } public int ClassId { get; set; } public virtual T_Class T_Class { get; set; } }
最后,在实际开发中,我们有如下一段代码:
IQueryable<T_Person> dbQuery = db.T_Person.Where(p => p.ClassId == 1); T_Person person = dbQuery.FirstOrDefault(); Console.WriteLine(person.T_Class.Name);
通过调试我们可以发现,在执行最后一段代码输出Person对象所属班级的信息之前,都没有对Class进行查询。而在执行到最后一句时,才去数据库查询所对应的Class信息;
3.2 浅谈即时加载
所谓即时加载,就是在加载数据时就把该对象相关联的其它表的数据一起加载到内存对象中去。
在Queryable类中的扩展方法中,ToList()方法就是一个典型的即时加载案例。与延迟加载相对应,在开发中如果使用ToList()方法,EF会根据方法中的条件自动生成SQL语句,然后立即与数据库进行交互获取查询结果,并加载到内存中去。
(1)例如,我们有以下一段代码,在执行到第一句的ToList()方法时,EF就立即对数据库发起访问,并将结果记载到了内存中,最后将personList指向了这块记录在堆中的地址;
List<T_Person> personList = db.T_Person.Where(p => p.ClassId == 1).ToList(); personList.ForEach(p => Console.WriteLine(p.ToString()));
(2)这时候,如果我们再想对查询到的结果进行排序,我们该怎么写?是不是写出了以下的代码:
List<T_Person> personList = db.T_Person.Where(p => p.ClassId == 1).ToList().OrderBy(p => p.Name).ToList(); personList.ForEach(p => Console.WriteLine(p.ToString()));
这时我们发现,当ToList()之后,OrderBy()方法就没有对SQL语句进行调整,也没有再对数据库发起请求了。因为,这里的OrderBy()方法是对内存中的数据进行的排序,而不是和前面的Where()方法一起拼接成SQL语句。
3.3 使用Include提高查询效率
前面我们看到了延迟加载在EF中被广泛应用,但是延迟加载对于外键的加载也存在不足:那就是每次调用外键实体都会去查数据库。
(1)我们来看看下面一段代码,它的作用是通过每个班级查询所对应的所有学生信息:
IQueryable<T_Class> classQuery = db.T_Class; foreach (T_Class c in classQuery) { Console.WriteLine(c.Id + "-" + c.Name + ":"); ICollection<T_Person> pList = c.T_Person; foreach (T_Person p in pList) { Console.WriteLine(p.Id + "-" + p.Name); } }
其显示结果如下图所示:
(2)通过SQLServer Profiler跟踪,可以发现,每次调用外键实体属性时都会对数据库发出一起查询请求,从下图也可以看出,总共发出了接近10个请求;
(3)但是,EF也做了一个小优化:对于相同外键的加载请求,只会执行一次;例如,这里存在多个ClassId=1的Person记录,因此它们都只会执行一次即可;
(4)虽然EF做了一些优化,但是有木有一种方法能够让我们只通过一次请求就获取所有的信息呢?在SQL语句中,我们可以通过一个超级简单的连接查询就可以实现,那么在EF中呢如何实现呢?还好,微软早就想到了这一点,为我们提供了一个Include方法。我们可以对上面的代码段进行修改,得到下面的代码:
IQueryable<T_Class> classQuery = db.T_Class.Include("T_Person"); foreach (T_Class c in classQuery) { Console.WriteLine("班级:" + c.Id + " " + c.Name + ":"); ICollection<T_Person> pList = c.T_Person; foreach (T_Person p in pList) { Console.WriteLine(p.Id + "-" + p.Name); } }
这下程序在执行到Include方法时,便会与T_Person表进行一个连接查询,将连接查询到的T_Person部分数据存入T_Class的T_Person属性中,也就是都存入了内存中,后面再次访问外键实体只需要从内存中读取而不用再发出多个数据库查询请求了。
Include方法跟ToList方法一样,也是即时加载类型的一种具体方法,其本质是生成连接查询的SQL语句。从整体来看,通过Include将以空间换取效率,在某些具体的应用场合可以适当使用。
参考资料
(1)陈少鑫,《EF贪婪加载与延迟加载的选择和使用》:http://www.cnblogs.com/chenshao/p/4169210.html
(2)强子,《解析ASP.NET MVC开发方式之EF延迟加载》:http://www.cnblogs.com/qq731109249/p/3502874.html
(3)Liam Wang,《ASP.NET MVC小牛之路:使用EF》:http://www.cnblogs.com/willick/p/3304534.html