翻译的初衷以及为什么选择《Entity Framework 6 Recipes》来学习,请看本系列开篇
5-2 预先加载关联实体
问题
你想在一次数据交互中加载一个实体和与它相关联实体。
解决方案
假设你有如图5-2所示的模型。
图5-2 包含Customer和与它相关联信息的实体
和5-1节一样,在模型中,有一个Customer实体,一个与它关联的CustomerType和多个与它关联的CustomerEamil。它与CustomerType的关系是一对多关系,这是一个实体引用(译注:Customer中的导航属性CustomerType)。
Customer与CustomerEmail也是一对多关系,只是这时CustomerEmail在多的这一边。这是一个实体集合(译注:Customer中的导航属性CustomerEmails)。
为了在一次查询中,获取父对象customer和与它关联的实体CustomerEamil和CustomrType的所有数据,我们使用Include()方法。如代清单5-2所示。
代码清单5-2. 预先加载与Customer相关联的CustomerType和CustomerEmail实例
using (var context = new EFRecipesEntities())
{
var web = new CustomerType {Description = "Web Customer", CustomerTypeId = };
var retail = new CustomerType {Description = "Retail Customer", CustomerTypeId = };
var customer = new Customer {Name = "Joan Smith", CustomerType = web};
customer.CustomerEmails.Add(new CustomerEmail {Email = "jsmith@gmail.com"});
customer.CustomerEmails.Add(new CustomerEmail {Email = "joan@smith.com"});
context.Customers.Add(customer);
customer = new Customer {Name = "Bill Meyers", CustomerType = retail};
customer.CustomerEmails.Add(new CustomerEmail {Email = "bmeyers@gmail.com"});
context.Customers.Add(customer);
context.SaveChanges();
} using (var context = new EFRecipesEntities())
{ //Include()方法,使用基于字符串类型的,与导航属性相对应的查询路径
var customers = context.Customers
.Include("CustomerType")
.Include("CustomerEmails");
Console.WriteLine("Customers");
Console.WriteLine("=========");
foreach (var customer in customers)
{
Console.WriteLine("{0} is a {1}, email address(es)", customer.Name,
customer.CustomerType.Description);
foreach (var email in customer.CustomerEmails)
{
Console.WriteLine("\t{0}", email.Email);
}
}
} using (var context = new EFRecipesEntities())
{
//Include()方法,使用基于强类型的,与导航属性相对应的查询路径
var customerTypes = context.CustomerTypes
.Include(x => x.Customers
.Select(y => y.CustomerEmails)); Console.WriteLine("\nCustomers by Type");
Console.WriteLine("=================");
foreach (var customerType in customerTypes)
{
Console.WriteLine("Customer type: {0}", customerType.Description);
foreach (var customer in customerType.Customers)
{
Console.WriteLine("{0}", customer.Name);
foreach (var email in customer.CustomerEmails)
{
Console.WriteLine("\t{0}", email.Email);
}
}
}
}
代码清单5-2的输出如下:
Customers
=========
Joan Smith is a Web Customer, email address(es)
jsmith@gmail.com
joan@smith.com
Bill Meyers is a Retail Customer, email address(es)
bmeyers@gmail.com
Customers by Type
=================
Customer type: Web Customer
Joan Smith
jsmith@gmail.com
joan@smith.com
Customer type: Retail Customer
Bill Meyers
bmeyers@gmail.com
原理
默认情况下,实体框架只加载你指定的实体,这就是所谓的延迟加载。用户在你的应用中会根据他的需要浏览不同的视图,在这种情况下延迟加载很有效。
与之相反的是,立即加载父实体和与之关联的子实体(记住,对象图是基于关联的父实体和子实体,就像数据库中基于外键的父表和子表)。它叫做Eager Loading(预先加载)。它在需要大量关联数据时很有效,因为它在一个单独的查询中获取所有的数据(父实体和与之关联的子实体)。
在代码清单5-2中,我们两次使用Include()方法(译注:第一段代码块中),立即获取对象图。第一次,我们加载一个包含Customer实体和实体引用CustmerType的对象图。CustomerType在一对多关联中的一这边。第二次,我们使用Include()方法(用相同的代码串连在一起)获取一对多有关联中多一边的CustomerEmails。两次通过fluent API方式将Include()方法链接在一起,我们从Customer的导航属性获取与其关联的实体。注意,我们在示例中使用字符串类型来表示导航属性,使用"."字符来分隔(译注:示例中没有用到,比如这样的的形式Include(“CustomerType.Customers”))。这种字符串形式的表示方式叫做关联实体的查询路径(query path)。
在接下来的代码块中,我们执行一样的操作,但使用了强类型的查询路径。请注意我们是如何使用lambda表达式来标识每一个关联实体的。强类型的用法给我们带来了智能提示、编译时检查和重构支持。
请注意,代码清单5-3中使用Include()方法产生的SQL查询语句 。在结果集被实例化和返回之前,实体框架自动移除查询中重复的数据。如图5-3所示。
代码清单5-3. 使用Include()方法产生的SQL查询语句
SELECT
[Project1].[CustomerId] AS [CustomerId],
[Project1].[Name] AS [Name],
[Project1].[CustomerTypeId] AS [CustomerTypeId],
[Project1].[CustomerTypeId1] AS [CustomerTypeId1],
[Project1].[Description] AS [Description],
[Project1].[C1] AS [C1],
[Project1].[CustomerEmailId] AS [CustomerEmailId],
[Project1].[CustomerId1] AS [CustomerId1],
[Project1].[Email] AS [Email]
FROM ( SELECT
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Name] AS [Name],
[Extent1].[CustomerTypeId] AS [CustomerTypeId],
[Extent2].[CustomerTypeId] AS [CustomerTypeId1],
[Extent2].[Description] AS [Description],
[Extent3].[CustomerEmailId] AS [CustomerEmailId],
[Extent3].[CustomerId] AS [CustomerId1],
[Extent3].[Email] AS [Email],
CASE WHEN ([Extent3].[CustomerEmailId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
FROM [Chapter5].[Customer] AS [Extent1]
INNER JOIN [Chapter5].[CustomerType] AS [Extent2] ON
[Extent1].[CustomerTypeId] = [Extent2].[CustomerTypeId]
LEFT OUTER JOIN [Chapter5].[CustomerEmail] AS [Extent3] ON
[Extent1].[CustomerId] = [Extent3].[CustomerId]
) AS [Project1]
ORDER BY [Project1].[CustomerId] ASC, [Project1].[CustomerTypeId1] ASC, [Project1].[C1] ASC
图5-3 通过使用Include()方法产生的冗余数据
5-3 快速查询一个单独的实体
问题
你想加载一个单独的实体,但是,如果该实体已经加载到上下文中时,你不想再进行一次数据库交互。同时,你想使用code-first 来管理数据访问。
解决方案
假设你有如图5-4所示的模型。
图5-4 包含一个Club实体类型的模型
在这个模型中,我们有一个实体类型Club,你可以通过查询获取各种各样的俱乐部(Clubs).
在Visual Studio中添加一个名为Recipe3的控制台应用,并确保引用了实体框架6的库,NuGet可以很好的完成这个任务。在Reference目录上右键,并选择 Manage NeGet Packages(管理NeGet包),在Online页,定位并安装实体框架6的包。这样操作后,NeGet将下载,安装和配置实体框架6的库到你的项目中。
创建一个名为Club类,复制代码清单5-4中的属性到这个类中,创建club实体。 (译注:本书是多位作者写的,描述的风格肯定有所不同)
代码清单5-4. Club 实体类
public class Club
{
public int ClubId { get; set; }
public string Name { get; set; }
public string City { get; set; }
}
接下来,创建一个名为Recipe3Context的类,并将代码清单5-5中的代码添加到其中,并确保其派生到DbContext类。
public class Recipe3Context : DbContext
{
public Recipe3Context()
: base("Recipe3ConnectionString")
{
// 禁用实体框架的模型兼容性
Database.SetInitializer<Recipe3Context>(null);
} protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Club>().ToTable("Chapter5.Club");
} public DbSet<Club> Clubs { get; set; }
}
接下来添加App.Config文件到项目中,并使用代码清单5-6中的代码添加到文件的ConnectionStrings小节下。
<connectionStrings>
<add name="Recipe3ConnectionString"
connectionString="Data Source=.;
Initial Catalog=EFRecipes;
Integrated Security=True;
MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
如果我们正使用一个关键词来搜索实体,一般是这样操作过程,凭借Find()方法,在从数据库中获取之前,先在内存中查找。记住,实体框架的默认行为,当你给出一个获取数据的操作时,它会去查询数据库,即使数据已经被加载到上下文中。
方法Find()是DbSet类中的成员函数,它是我们用来注册实体到上下文对象中的类。代码清单5-7将对此进行演示。
代码清单5-7. 凭借实体框架中的Find()方法,避免获取已经加载到上下文对象中的数据。
int starCityId;
int desertSunId;
int palmTreeId; using (var context = new Recipe3Context())
{
var starCity = new Club {Name = "Star City Chess Club", City = "New York"};
var desertSun = new Club {Name = "Desert Sun Chess Club", City = "Phoenix"};
var palmTree = new Club {Name = "Palm Tree Chess Club", City = "San Diego"}; context.Clubs.Add(starCity);
context.Clubs.Add(desertSun);
context.Clubs.Add(palmTree);
context.SaveChanges(); // SaveChanges()返回每个最新创建的Club Id
starCityId = starCity.ClubId;
desertSunId = desertSun.ClubId;
palmTreeId = palmTree.ClubId;
} using (var context = new Recipe3Context())
{
var starCity = context.Clubs.SingleOrDefault(x => x.ClubId == starCityId);
starCity = context.Clubs.SingleOrDefault(x => x.ClubId == starCityId);
starCity = context.Clubs.Find(starCityId);
var desertSun = context.Clubs.Find(desertSunId);
var palmTree = context.Clubs.AsNoTracking().SingleOrDefault(x => x.ClubId == palmTreeId);
palmTree = context.Clubs.Find(palmTreeId);
var lonesomePintId = -;
context.Clubs.Add(new Club {City = "Portland", Name = "Lonesome Pine", ClubId = lonesomePintId,});
var lonesomePine = context.Clubs.Find(lonesomePintId);
var nonexistentClub = context.Clubs.Find();
} Console.WriteLine("Please run this application using SQL Server Profiler...");
Console.ReadLine();
原理
当使用上下文对象查询时,即使数据已经加载到上下文中,仍会产生一次获取数据的数据库交互。当一次查询完成时,不存在上下文中的实体对象将被添加到上下文中,并被跟踪。在默认情况下,如果实体对象已经在上下文中,实体框架不会使用数据库中较新的值重写它。
然后, DbSet对象,它包装着我们的实体对象,公布了一个Find()方法。特别地,Find()方法期望得到一个被查询对象的主键(ID)参数。Find()方法非常有效率,因为它会先为目标对象查询上下文。如果对象不存在,它会自动去查询底层的数据存储。如果仍然没有找到,Find()方法将返回NULL给调用者。另外,Find()方法将返回已添加到上下文中(状态为"Added"),但还没有保存到数据库中的对象。Find()方法对三种建模方式均有效:Database First,Model First,Code First。
在示例中,我们添加三个clubs实体到Club实体集合。请注意,在调用SaveChanges()后,我们是如何引用新创建的Club实体的ID的。当SaveChages()操作完成后,上下文会立即返回新创建对象的ID.
接下来,我们从DbContext中查询实体,并返回StarCity Club 实体。注意,我们是如何凭借LINQ扩展方法SingleOrDefault(),返回一个对象的,如果在底层数据库中不存在要查找的对象,它返回NULL。当发现多个符合给定条件的对象时,SingleOrDefault()方法将抛出一个异常。SingleOrDefault()在通过主键查找对象时,是一个非常好的方法。如果存在多个对象且你希望返回第一个时,可以考虑使用FirstOrDefault()方法。
如果你运行SQL Profiler Tool(在SQL Server Developer Edition版本或更高版本中,SQL Express版本不包含),检查底层数据库的活动,你会看见如图5-5所示的SQL查询语句产生。
图5-5 返回 Star City Club的SQL的查询语句
请注意图5-5,为何在上下文对象中查询Clubs,总是会产生一个针对底层数据库的SQL查询语句。这里我们获取ID为80的Club,将数据实例化到Club实体对象,并存放在上下文对象中。有趣的是,为什么LINQ扩展方法SingleOrDefault()总是产生一个Select Top 2 的SQL查询。 Select Top 2 这条SQL查询确保只有一行数据被返回。 如果多于一条数据返回, 实体框架将抛出一个异常,因为 SingleOrDefault()方法保证只返回一个单独的结果。
下一行代码(译注:指的是 starCity = context.Clubs.SingleOrDefault(x => x.ClubId == starCityId);),重新查询数据库获取相同的对象,Star City Club。请注意,虽然对象已经存在上下文中,但实体框架DbContext的默认行为,仍会重新查询数据库获取记录。在Profiler中,我们看相同的SQL语句被产生。不仅如此,因为Star City实体已经加载到上下文中,DbContext不会使用数据库中的新值来替换当前的值,如图5-6所示。
图5-6 返回Star City Club的SQL语句
下一行代码,我们再一次查找Star City Club。然后,这次我们使用的是Find()方法,它是在DbSet类中公布的。因为Clubs是一个DbSet类,因此,我们只是在它身上简单地调用Find()方法,并把要查找对象的主键作为参数传递线它。在我们示例中,主键的值为80。
Find()方法首先在上下文对象中查找Star City Club,找到对象后,它返回该对象的引用。关键点是,Find()方法只有在上下文中没有找需要的对象时,才去数据库中查询。请注意,图5-7中为什么没有产生SQL语句。
图5-7 Find()在上下文中找到了对象,没有产生任何针对数据库查询语句
接下来,我们再次使用Find()方法去获取实体对象Desert Sun Club。方法Find()没有在上下文中找到该对象,它将查询数据库并返回信息。图5-8是它查询该对象产生的SQL语句。
图5-8 返回Desert Sun Club对象产生的SQL语句
在下一个查询中,我们获取实体对象Palm Tree Club的信息,但是我们这次使用LINQ查询。 注意AsNotracking()从句,它被添加到Clubs后面。NoTracking 选项将禁用指定对象的对象跟踪。没有了对象跟踪,实体框架将不在跟踪Palm Tree Club对象的改变。也不会将对象加载到上下文中。
随后,当我们查询并获取Palm Tree Club实体对象时,Find()方法将产生一个SQL查询语句并从数据库从获取实体。如图5-9所示。因为我们使用AsNoTracking()从句指示实体框架不要在上下文中跟踪对象,所以,数据库交互就成了必须的了。记住,Find()方法需要对象跟踪,以避免数据库调用 。
图5-9 返回Desert Sun Club实体产生的SQL查询语句
接下来,我们添加一个新的Club实体到上下文中。我们实例化一个Club实体类,并填充必要的数据。为Id分配一个临时的值-999。记住,我们不需要调用SaveChage()来提交新的Club对象,Lonesome Pine Club,到数据库。有趣的是,我们使用Find()方法并给它传递参数-999,实体框架从上下文中返回最新创建的 Lonesome Pine Club实体对象。你可以从图5-10中看到,这次调用Find()方法没有产生数据库活动。注意,Find()方法会返回一个最近添加到上下文中的实例,即使它还没有被保存到数据库中。
图5-10 Find()方法在上下文中定位一个刚创建,但没有保存的对象并返回,这个过程不生成sql查询语句
最后,我们给Find()方法传递一个数据库中不存在的Id作为参数。这个Id的值为10001.如图5-11所示,Find()方法生成SQL查询并试图在数据库中返回Id为10001的记录。跟LINQ扩展方法SingleOrDefault()一样,如果没有找到指定的记录,会向调用方返回NULL。
图5-11 Find()方法生成一个SQL查询,如果数据库中不存在要查找的记录便返回null
实体框架交流QQ群: 458326058,欢迎有兴趣的朋友加入一起交流
谢谢大家的持续关注,我的博客地址:http://www.cnblogs.com/VolcanoCloud/