正如您可能期望的那样,实体框架提供了许多 API 来将数据从数据库获取到对象中。这些设计涵盖了不同的场景;我们将一个接一个地看到它们。
所有数据访问层都必须支持主键加载,当然 EFCF 也是这样做的。为此,我们有找的方法。
//retrieving a record by a single primary key consisting of an integer
var project = ctx.Projects.Find(1);
该方法可以采用任意数量的参数;它支持具有复合主键的实体。下面的例子就说明了这一点。
//retrieving a record by a composite primary key consisting of two integers
var project = ctx.SomeEntities.Find(1, 2);
如果没有找到匹配的记录,查找将返回空;不会抛出异常,因为这是一个完全有效的结果。
| | 提示:参数的顺序和类型应该与该实体映射中定义的键相同。 |
自从它与。NET 3.5,语言集成查询(LINQ)已经成为事实上的标准,用于查询任何类型的数据,所以 EF 有 LINQ 支持也就不足为奇了。对于大多数场景,它可能是您的首选应用编程接口。它是强类型的,这意味着您可以在编译时判断某些事情是不正确的,重构友好,并且它的语法很容易理解。让我们看一些例子。
一个 LINQ 查询是从上下文暴露的 DbSet < T > 类型的实体集合属性构建的,它由一个 IQueryable < T > 实现组成。
| | 提示:别忘了 LINQ 是关于质疑的。您在这里找不到任何更改数据的方法。 |
如果不包括终端操作员,如 ToList 、 ToArray 、 ToDictionary 、 Any 、 Count 、 LongCount 、 Single 、 SingleOrDefault 、 First 、 FirstOrDefault 、 Last 、您可以选择该查询并开始添加限制,如分页或排序。
//create a base query
var projectsQuery = from p in ctx.Projects select p;
//add sorting
var projectsSortedByDateQuery = projectsQuery.OrderBy(x => x.Start);
//execute and get the sorted results
var projectsSortedByDateResults = projectsSortedByDateQuery.ToList();
//add paging and ordering (required for paging)
var projectsWithPagingQuery = projectsQuery.OrderBy(x => x.Start).Take(5).Skip(0);
//execute and get the first 5 results
var projectsWithPagingResults = projectsWithPagingQuery.ToList();
//add a restriction
var projectsStartingAWeekAgoQuery = projectsQuery.Where(x => x.Start >= EntityFunctions.AddDays(DateTime.Today, -7));
//execute and get the projects that started a week ago
var projectsStartingAWeekAgoResults = projectsStartingAWeekAgoQuery.ToList();
你最多只能得到一个结果。
//retrieving at most a single record with a simple filter var project = ctx.Projects.Where(x => x.ProjectId == 1).SingleOrDefault();
受几个属性的限制也同样容易。
//retrieving multiple record with two filters var projects = ctx.Projects.Where(x => x.Name.Contains("Something") && x.Start >= DateTime.Today.AddDays(-7)).ToList();
或者两个条件中的一个匹配。
//or
var resourcesKnowingVBOrCS = ctx.Technologies.Where(t => t.Name == "VB.NET"
|| t.Name == "C#").SelectMany(x => x.Resources).Select(x => x.Name).ToList();
统计结果。
//count
var numberOfClosedProjects = ctx.Projects
.Where(x => x.End != null && x.End < DateTime.Now).Count();
检查记录是否存在。
//check existence
var existsProjectBySomeCustomer = ctx.Projects
.Any(x => x.Customer.Name == "Some Customer");
执行投影,即只获得实体的某些部分。
//get only the name of the resource and the name of the associated project
var resourcesXprojects = ctx.Projects.SelectMany(x => x.ProjectResources)
.Select(x => new { Resource = x.Resource.Name, Project = x.Project.Name }).ToList();
做聚合。
//average project duration
var averageProjectDuration = ctx.Projects.Where(x => x.End != null)
.Average(x => EntityFunctions.DiffDays(x.Start, x.End));
//sum of project durations by customer
var sumProjectDurationsByCustomer = ctx.Projects.Where(x => x.End != null)
.Select(x => new { Customer = x.Customer.Name, Days = EntityFunctions
.DiffDays(x.Start, x.End) }).GroupBy(x => x.Customer)
.Select(x => new { Customer = x.Key, Sum = x.Sum(y => y.Days) }).ToList();
获取不同的值。
//distinct roles performed by a resource
var roles = ctx.Resources.SelectMany(x => x.ProjectResources)
.Where(x => x.Resource.Name == "Ricardo Peres").Select(x => x.Role).Distinct()
.ToList();
您也可以对某个属性进行分组。
//grouping and projecting
var resourcesGroupedByProjectRole = ctx.Projects.SelectMany(x => x.ProjectResources)
.Select(x => new { Role = x.Role, Resource = x.Resource.Name }).GroupBy(x => x.Role)
.Select(x => new { Role = x.Key, Resources = x }).ToList();
//grouping and counting
var projectsByCustomer = ctx.Projects.GroupBy(x => x.Customer)
.Select(x => new { Customer = x.Key.Name, Count = x.Count() }).ToList();
//top 10 customers having more projects in descending order
var top10CustomersWithMoreProjects = ctx.Projects.GroupBy(x => x.Customer.Name)
.Select(x => new { x.Key, Count = x.Count() }).OrderByDescending(x => x.Count)
.Take(10).ToList();
或者使用子查询的结果。
//subquery
var usersKnowingATechnology = (from r in ctx.Resources where r.Technologies.Any(x => (from t in ctx.Technologies where t.Name == "ASP.NET" select t).Contains(x)) select r).ToList();
最后,检查一组值中的一个。
//contains
var customersToFind = new String[] { "Some Customer", "Another Customer" };
var projectsOfCustomers = ctx.Projects
.Where(x => customersToFind.Contains(x.Customer.Name)).ToList();
| | 注意:如果你想知道,LINQ 查询中的所有文字——字符串、数字、日期等等。—将被转换为参数,以便正确执行计划重用。 |
您可能已经注意到,每当我们进行日期操作时,我们都会使用一些实体函数方法,例如实体函数。DiffDays 。这个类有一些 LINQ 没有提供的操作扩展方法。这些可分为:
- 日期和时间:添加时间间隔,截断日期或时间,计算两个日期之间的时间间隔。
- 字符串:获取字符串的左或右部分,反转它,将其转换为 ASCII 或 UNICODE。
- 统计学:基于总体的方差和方差。
还有一个类似的类 SqlFunctions ,它提供了调用特定于 SQL Server 的函数的扩展方法,即:
- 数学:三角函数,对数,幂,弧度转换,随机数。
- String:从字符串中获取 ASCII 和 UNICODE 代码,计算两个字符串之间的差异,字符串中一个字符的位置,将一个字符串插入另一个字符串,获取字符串的 SOUNDEX 值。
- 校验和:计算列的校验和。
- 日期和时间:计算日期之间的时间间隔,添加时间间隔。
- 系统:获取数据库日期和时间、当前用户、主机名。
- 数据类型:检查一列是否可以转换为另一个数据类型,进行转换。
尽管 LINQ 很灵活,但也有一些局限性。动态构建查询并不容易,例如,在编译时不知道过滤器或所需的排序属性。让我们看看其他 API,看看我们如何处理这些情况。
| | 提示:不要查询计算列,只查询映射列,因为否则会出错,因为实体框架对此一无所知。 |
实体 SQL(或 ESQL)是实体框架的查询语言。它与 SQL 非常相似,但它有一些很大的优势。
- 它非常类似于 SQL,所以我们可以重用一些我们对它的知识。
- 它是独立于数据库的,这意味着无论我们的目标是什么样的实际数据库,它都是一样的。
- 它是面向对象的,而不是设置的,所以在某些方面它类似于我们在代码中所做的。
- 它知道实体关系,所以我们不需要指定它们。
- 与 LINQ 不同,查询可以在运行时动态创建。
实体 SQL 命令是字符串,这意味着我们可以进行通常的字符串操作,并且只有在实际执行时才知道语法是否正确。
虽然实体框架代码优先肯定可以使用实体 SQL,但它不能直接使用:无论是 DbContext 还是任何其他代码优先类都没有公开使用它的方法。我们首先需要得到它的底层对象上下文。
//get the ObjectContext from the DbContext
ObjectContext octx = (ctx as IObjectContextAdapter).ObjectContext;
而实际的动作发生在 CreateQuery 方法上。
//all values from the Projects collection
var allProjects = octx.CreateQuery<Resource>("SELECT VALUE p FROM Projects AS p")
.ToList();
参数在名称前用@前缀显式命名,并且必须给定一个值。
//simple filter
var usersInProject = octx.CreateQuery<Resource>(
"SELECT VALUE pr.Resource FROM ProjectResources AS pr WHERE pr.Project.Name = @name", new ObjectParameter("name", "Big Project")).ToList();
对以下内容使用子查询。
//contains
var usersKnowingATechnology = octx.CreateQuery<Resource>(
"SELECT VALUE r FROM Resources AS r WHERE EXISTS (SELECT VALUE t FROM Technologies AS t WHERE t.Name = @name AND r IN t.Resources)",
new ObjectParameter("name", "ASP.NET")).ToList();
使用以下内容进行分页。
//paging
var pagedResources = octx.CreateQuery<Resource>(
"SELECT VALUE r FROM Resources AS r ORDER BY r.Name SKIP 5 LIMIT(5)").ToList();
//paging with parameters
var pagedResourcesWithParameters = octx.CreateQuery<Resource>(
"SELECT VALUE r FROM Resources AS r ORDER BY r.Name SKIP @skip LIMIT(@limit)",
new ObjectParameter("skip", 5), new ObjectParameter("limit", 5)).ToList();
//single first record ordered descending
var lastProject = octx.CreateQuery<Project>(
"SELECT VALUE TOP(1) p FROM Projects AS p ORDER BY p.Start DESC").SingleOrDefault();
使用下列范围。
//between with parameters
var projectsStartingInADateInterval = octx.CreateQuery<Project>(
"SELECT VALUE p FROM Projects AS P WHERE p.Start BETWEEN @start AND @end",
new ObjectParameter("start", DateTime.Today.AddDays(14)),
new ObjectParameter("end", DateTime.Today.AddDays(-7))).ToList();
//in with inline values
var projectsStartingInSetOfDates = octx.CreateQuery<Project>(
"SELECT VALUE p FROM Projects AS P WHERE p.Start IN MULTISET(DATETIME
'2013-12-25 0:0:0', DATETIME '2013-12-31 0:0:0')").ToList();
对计数记录使用以下内容。
//count records
var numberOfClosedProjects = octx.CreateQuery<Int32>(
"SELECT VALUE COUNT(p.ProjectId) FROM Projects AS p WHERE p.[End] IS NOT NULL AND p.[End] < @now", new ObjectParameter("now", DateTime.Now)).Single();
使用以下内容进行预测。
//projection with date difference
var projectNameAndDuration = octx.CreateQuery<Object>(
"SELECT p.Name AS Name, DIFFDAYS(p.Start, p.[End]) FROM Projects AS p WHERE p.[End]
IS NOT NULL").ToList();
//projection with count
var customersAndProjectCount = octx.CreateQuery<Object>(
"SELECT p.Customer.Name, COUNT(p.Name) FROM Projects AS p GROUP BY p.Customer")
.ToList();
//projection with case
var customersAndProjectCountIndicator = octx.CreateQuery<Object>(
"SELECT p.Customer.Name, CASE WHEN COUNT(p.Name) > 10 THEN 'Lots' ELSE 'Few' END AS Amount FROM Projects AS p GROUP BY p.Customer").ToList();
| | 提示:与保留关键字同名的属性名必须在[ ]中转义。 |
| | 提示:使用多个属性执行投影时,CreateQuery 的模板参数类型必须是对象。 |
当投影时,我们失去了强类型,所以我们必须直接访问返回的 IDataRecord 实例。
if (customersAndProjectCountIndicator.Any() == true)
{
var r = customersAndProjectCountIndicator.OfType<IDataRecord>().First();
var nameIndex = r.GetOrdinal("Name");
var nameValue = r.GetString(nameIndex);
}
至于一些内置功能。
//max number of days
var maxDurationDays = octx.CreateQuery<Int32?>(
"SELECT VALUE MAX(DIFFDAYS(p.Start, p.[End])) FROM Projects AS p WHERE p.[End] IS NOT NULL").SingleOrDefault();
//string matching (LIKE)
var technologiesContainingDotNet = octx.CreateQuery<String>(
"SELECT VALUE t.Name FROM Technologies AS T WHERE CONTAINS(t.Name, '.NET')")
.ToList();
尽管我们可能会尝试,但事实是,逃避使用 SQL 是不可能的。这可能是因为性能通常更好,或者因为使用任何其他 API 都很难甚至不可能表达某些查询,但事实就是如此。实体框架代码优先完全支持 SQL,包括:
- 获取实体和值。
- 执行 INSERTs、UPDATEs 和 DELETEs。
- 调用函数和存储过程。
它确实有你可能会想到的所有缺点:
- 不是强类型的。
- 没有编译时检查。
- 如果使用特定于数据库的函数,并且以新数据库为目标,则必须重写 SQL。
- 即使是简单的事情,例如分页或限制要返回的记录数量,您也必须知道正确的语法。
我演示的第一个案例是如何执行 SELECT 并将结果转换为实体。为此,我们将使用数据库设置<测试>T3 的查询方法。
//simple select
var projectFromSQL = ctx.Projects.SqlQuery(
"SELECT * FROM Project WHERE Name = @p0", "Big Project").SingleOrDefault();
| | 提示:注意我们如何在 SQL 之后直接传递参数;每个都必须命名为@p0、@p1 等等。 |
如果我们想从表值函数中检索一个实体,我们可以使用。
//table-valued function
var projectFromFunction = ctx.Projects.SqlQuery(
"SELECT * FROM dbo.GetProjectById @p0", 1).SingleOrDefault();
GetProjectById 函数可能在哪里。
CREATE FUNCTION
dbo.GetProjectById
(
@ProjectID INT
)
RETURNS TABLE
AS
RETURN
(
SELECT *
FROM Project
WHERE ProjectId = @ProjectID
)
GO
| | 提示:从 SQL 返回实体的方法不能返回复杂类型,只能返回标量和枚举类型。这是实体框架的一个已知问题,在http://entityframework.codeplex.com/workitem/118有一个公开的问题。 |
| | 提示:不要忘记,如果您想要返回实体,您的 SQL 必须返回与这些实体的属性相匹配的列,正如在其映射中所指定的那样。 |
虽然 SqlQuery 主要是为了处理实体而设计的,但是我们也可以使用它来检索标量。
//current date and time
var now = ctx.Database.SqlQuery<DateTime>("SELECT GETDATE()").Single();
如果我们想要执行任意的 SQL 命令(UPDATE、DELETE、INSERT),我们将需要 ExecuteSqlCommand ,如下例所示。
//update records
var updateCount = ctx.Database.ExecuteSqlCommand(
"UPDATE ProjectDetail SET Budget = Budget * 1.1 WHERE ProjectId = {0}", 1);
最后,对于真正特殊的情况,我们总是可以求助于底层的 DbConnection 。
//create
the connection in a using block so that it is disposed at the end
using (var cmd = ctx.Database.Connection.CreateCommand())
{
if (ctx.Database.Connection.State == ConnectionState.Closed)
{
ctx.Database.Connection.Open();
}
cmd.CommandText = "Some weird SQL command";
//the
number of affected records, if the query returns it
var result = cmd.ExecuteNonQuery();
//or
a single scalar value
//var result = cmd.ExecuteScalar();
//or
even a data reader
var result = cmd.ExecuteReader();
ctx.Database.Connection.Close();
}
默认情况下,所有引用(一对一、多对一)和集合(一对多、多对多)都是延迟加载的,这意味着实体框架不会真正尝试加载其值,直到有人尝试访问它们。例如,考虑这个查询,其中我们按照项目的 id 加载项目。
//load a project by id
var p = ctx.Projects.Find(1);
这个查询将生成这个 SQL。
SELECT TOP (2)
[Extent1].[ProjectId] AS
[ProjectId],
[Extent1].[Description_Description] AS
[Description_Description],
[Extent1].[Name] AS
[Name],
[Extent1].[Start] AS
[Start],
[Extent1].[End] AS
[End],
[Extent1].[Customer_CustomerId] AS
[Customer_CustomerId]
FROM [dbo].[Project] AS [Extent1]
WHERE [Extent1].[ProjectId] = @p0
-- p0
(dbtype=Int32, size=0, direction=Input) = 1
| | 注意:您可能已经注意到 TOP(2)子句:这仅仅是为了确保选择了一条记录,正如 Find 方法所期望的那样;如果不是这样,将引发异常。 |
可以看到,唯一被触碰的桌子是项目那张。然而,当我们进入客户物业时。
//access the customer
var c = p.Customer;
然后将加载客户引用属性,为此,EF 将发出另一个查询。
SELECT
[Extent2].[CustomerId] AS
[CustomerId],
[Extent2].[Contact_Email] AS
[Contact_Email],
[Extent2].[Contact_Phone] AS
[Contact_Phone],
[Extent2].[Name] AS
[Name]
FROM [dbo].[Project] AS [Extent1]
INNER JOIN
[dbo].[Customer] AS [Extent2] ON [Extent1].[Customer_CustomerId] =
[E
xtent2].[CustomerId]
WHERE [Extent1].[ProjectId] = @EntityKeyValue1
--
EntityKeyValue1 (dbtype=Int32, size=0, direction=Input) = 1
或者我们走另一条路,先载一个客户。
//load a customer by id
var customer = ctx.Customers.Find(1);
将执行以下 SQL。
SELECT TOP (2)
[Extent1].[CustomerId] AS
[CustomerId],
[Extent1].[Contact_Email] AS
[Contact_Email],
[Extent1].[Contact_Phone] AS
[Contact_Phone],
[Extent1].[Name] AS
[Name]
FROM [dbo].[Customer] AS [Extent1]
WHERE [Extent1].[CustomerId] = @p0
-- p0
(dbtype=Int32, size=0, direction=Input) = 1
请注意,仅检索客户数据,这与前面的查询一致。现在让我们进入项目集合。
//load all
projects
var projects = customer.Projects;
该 SQL 被发送到服务器。
SELECT
[Extent1].[ProjectId] AS
[ProjectId],
[Extent1].[Description_Description] AS
[Description_Description],
[Extent1].[Name] AS
[Name],
[Extent1].[Start] AS
[Start],
[Extent1].[End] AS
[End],
[Extent1].[Customer_CustomerId] AS
[Customer_CustomerId]
FROM [dbo].[Project] AS [Extent1]
WHERE [Extent1].[Customer_CustomerId]
= @EntityKeyValue1
--
EntityKeyValue1 (dbtype=Int32, size=0, direction=Input) = 1
该客户的项目都已加载到内存中。实体框架负责为我们生成适当的 SQL,在背后打开和关闭连接,并从返回的结果集中实例化实体。请记住,导航属性仅在第一次访问时才从数据库加载,因此您可能会在第一次访问时注意到延迟,但之后它们总是从内存中返回。
| | 注意:别管实际的 SQL 细节;它们是从一个通用算法中生成的,即使它们不是您所期望的那样。他们会成功的! |
这是可能的,因为当 EF 从查询中返回实体时,它不会返回确切声明的类的实例,而是从派生的特殊类中返回。这个类称为代理类,由 EF 自动生成。
图 39:实体的代理
看到以系统开头的搞笑类名。检查器中的数据.实体.动态近似.项目 _ ?这是自动生成的类。这个类引用了它所来自的上下文,并且覆盖了所有的导航属性,因此当我们试图通过代码访问它们时,它只会加载相关的实体。为此,您必须同时确保:
- 班级不是密封的。
- 该类不是私有的或内部的。
- 所有打算延迟加载的导航属性(引用和集合)都是虚拟的。
- 所有导航属性都实现为自动属性,没有支持字段和自定义逻辑。
似乎很容易,你不觉得吗?如果这些条件中的任何一个没有得到验证,那么就不能使用延迟加载。例如,如果我们选择项目实体并更改其客户属性,使其不是虚拟的,那么加载项目的结果将总是客户属性为空值。
| | 提示:当惰性加载被禁用时,尚未显式加载的导航属性的值总是为空。 |
数据库上下文中有允许配置延迟加载的属性:数据库。配置。激光激活和临近激活。请注意,即使有两个属性,它们也必须同时为真,才能让延迟加载工作,默认情况下,它们是这样的。
为了查看是否加载了引用属性,我们使用如下代码。
//load a project
var project = ctx.Projects.Find(1);
//see if the Customer property is loaded
var customerLoaded = ctx.Entry(project).Reference(x => x.Customer).IsLoaded;
如果您在处置了一个惰性导航属性的原始上下文后试图加载该属性,您将获得一个ObjectDeposedException,因为该实体依赖于一个活动上下文从数据库中获取数据。
假设您禁用惰性加载。
//disable
lazy loading
ctx.Configuration.LazyLoadingEnabled
= false;
如果关闭了惰性加载,无论是全局加载还是针对某个属性,我们仍然可以强制显式加载导航属性。
//explicitly
load the Customer property
ctx.Entry(project).Reference(x => x.Customer).Load();
不要忘记,这只有在惰性加载被禁用的情况下才有必要。这同样适用于集合。
//see if the ProjectResources collection is loaded
var resourcesLoaded = ctx.Entry(project).Collection(x => x.ProjectResources)
.IsLoaded;
if (resourcesLoaded == false)
{
//explicitly load the ProjectResources collection
ctx.Entry(project).Collection(x => x.ProjectResources).Load();
}
另一个有趣的情况是,您希望通过过滤掉与给定条件不匹配的实体来仅加载某个集合的一部分,或者甚至计算其成员而不实际加载它们。用英孚可以做到,为此你会发出这样的查询。
//count an entity's collection entities without loading them
var countDevelopersInProject = ctx.Entry(project).Collection(x => x.ProjectResources).Query().Where(x => x.Role == Role.Developer).Count();
//filter
an entity’s collection without loading it
var developersInProject = ctx.Entry(project).Collection(x => x.ProjectResources)
.Query().Where(x => x.Role == Role.Developer).ToList();
惰性加载和渴望加载的区别在于,有了惰性加载,你不需要做任何显式的事情,你只需要访问导航属性,甚至不用考虑它;而对于显式加载,您必须执行一些操作。
即使惰性和显式加载在大多数情况下是好的,您也只加载您需要的数据,当您需要它时,可能会有希望同时加载主实体和关联实体的所有数据的情况。这很可能是因为两个原因之一:
- 您肯定需要访问一些导航属性,出于性能原因,您需要预先加载它们(例如,您需要查看所有订单的详细信息)。
- 实体(及其关联的实体)的生命周期可能会比从其获得的上下文更长(例如,您要将实体存储在某个缓存中),因此它将无法访问它,因此延迟加载是不可能的。
进入紧急装载。急切加载的意思是,当发出查询时,您显式声明实体框架将与根实体一起带来的扩展路径。然后,对于所有必需的关联,EF 将生成一个不同于使用 JOINs 通常会生成的 SQL 表达式。
例如,下面的查询带来了一个客户及其所有项目,并引入了 Include 方法。
//explicitly eager
load the Customer for each project
var projectsAndTheirCustomers = ctx.Projects.Include(x => x.Customer).ToList();
对于记录,这将产生以下 SQL 语句。
SELECT
[Extent1].[ProjectId] AS
[ProjectId],
[Extent1].[Name] AS
[Name],
[Extent1].[Start] AS
[Start],
[Extent1].[End] AS
[End],
[Extent2].[CustomerId] AS
[CustomerId],
[Extent2].[Contact_Email] AS
[Contact_Email],
[Extent2].[Contact_Phone] AS
[Contact_Phone],
[Extent2].[Name] AS
[Name1]
FROM [dbo].[Project] AS [Extent1]
INNER JOIN
[dbo].[Customer] AS [Extent2] ON
[Extent1].[Customer_CustomerId] =
[Extent2].[CustomerId]
Include 方法也可以取一个 String 作为参数,这个参数必须是一个导航属性的名称(引用或者集合)。
//explicitly eager
load the Customer for each project
var projectsAndTheirCustomers = ctx.Projects.Include("Customer").ToList();
可以指定多个路径。
//two
independent include paths
var resourcesProjectResourcesAndTechnologies = ctx.Resources
.Include(x => x.ProjectResources).Include(x => x.Technologies).ToList();
在这种情况下,SQL 将看起来像。
SELECT
[UnionAll1].[ResourceId] AS
[C1],
[UnionAll1].[ResourceId1] AS
[C2],
[UnionAll1].[ResourceId2] AS
[C3],
[UnionAll1].[Contact_Email] AS
[C4],
[UnionAll1].[Contact_Phone] AS
[C5],
[UnionAll1].[Name] AS
[C6],
[UnionAll1].[C1] AS
[C7],
[UnionAll1].[ProjectResourceId] AS
[C8],
[UnionAll1].[ProjectResourceId1] AS
[C9],
[UnionAll1].[Role] AS
[C10],
[UnionAll1].[Project_ProjectId] AS
[C11],
[UnionAll1].[Resource_ResourceId] AS
[C12],
[UnionAll1].[C2] AS
[C13],
[UnionAll1].[C3] AS
[C14]
FROM (SELECT
CASE WHEN ([Extent2].[ProjectResourceId]
IS NULL) THEN CAST(NULL AS int)
ELSE 1 END AS [C1],
[Extent1].[ResourceId] AS [ResourceId],
[Extent1].[ResourceId] AS [ResourceId1],
[Extent1].[ResourceId] AS [ResourceId2],
[Extent1].[Contact_Email] AS [Contact_Email],
[Extent1].[Contact_Phone] AS [Contact_Phone],
[Extent1].[Name] AS [Name],
[Extent2].[ProjectResourceId] AS
[ProjectResourceId],
[Extent2].[ProjectResourceId] AS
[ProjectResourceId1],
[Extent2].[Role] AS [Role],
[Extent2].[Project_ProjectId] AS
[Project_ProjectId],
[Extent2].[Resource_ResourceId] AS
[Resource_ResourceId],
CAST(NULL AS int) AS [C2],
CAST(NULL AS varchar(1)) AS [C3]
FROM [dbo].[Resource] AS [Extent1]
LEFT OUTER JOIN [dbo].[ProjectResource] AS [Extent2] ON [Extent1].[ResourceId] = [Extent2].[Resource_ResourceId]
UNION ALL
SELECT
2 AS [C1],
[Extent3].[ResourceId] AS [ResourceId],
[Extent3].[ResourceId] AS [ResourceId1],
[Extent3].[ResourceId] AS [ResourceId2],
[Extent3].[Contact_Email] AS [Contact_Email],
[Extent3].[Contact_Phone] AS [Contact_Phone],
[Extent3].[Name] AS [Name],
CAST(NULL AS int) AS [C2],
CAST(NULL AS int) AS [C3],
CAST(NULL AS int) AS [C4],
CAST(NULL AS int) AS [C5],
CAST(NULL AS int) AS [C6],
[Join2].[TechnologyId] AS [TechnologyId],
[Join2].[Name] AS [Name1]
FROM [dbo].[Resource] AS [Extent3]
INNER JOIN (SELECT [Extent4].[Resource_ResourceId]
AS [Resource_ResourceId], [Extent5].[TechnologyId] AS [TechnologyId], [Extent5].[Name] AS [Name]
FROM [dbo].[TechnologyResource]
AS [Extent4]
INNER JOIN [dbo].[Technology] AS [Extent5] ON [Extent5].[TechnologyId] = [Extent4].[Technology_TechnologyId] )
AS [Join2] ON [Extent3].[ResourceId] = [Join2].[Resource_ResourceId]) AS [UnionAll1]
ORDER BY
[UnionAll1].[ResourceId1] ASC, [UnionAll1].[C1] ASC
多级包含的最后一个示例:
//multilevel
include paths
var resourcesProjectResourcesCustomers = ctx.Resources.Include(x => x.ProjectResources.Select(y => y.Project.Customer)).ToList();
生成的 SQL 将如下所示。
SELECT
[Project1].[ResourceId] AS
[ResourceId],
[Project1].[Contact_Email] AS
[Contact_Email],
[Project1].[Contact_Phone] AS
[Contact_Phone],
[Project1].[Name] AS
[Name],
[Project1].[C1] AS
[C1],
[Project1].[ProjectResourceId] AS
[ProjectResourceId],
[Project1].[Role] AS
[Role],
[Project1].[ProjectId] AS
[ProjectId],
[Project1].[Name1] AS
[Name1],
[Project1].[Start] AS
[Start],
[Project1].[End] AS
[End],
[Project1].[CustomerId] AS
[CustomerId],
[Project1].[Contact_Email1] AS
[Contact_Email1],
[Project1].[Contact_Phone1] AS
[Contact_Phone1],
[Project1].[Name2] AS
[Name2],
[Project1].[Resource_ResourceId] AS
[Resource_ResourceId]
FROM ( SELECT
[Extent1].[ResourceId] AS [ResourceId],
[Extent1].[Contact_Email] AS [Contact_Email],
[Extent1].[Contact_Phone] AS [Contact_Phone],
[Extent1].[Name] AS [Name],
[Join2].[ProjectResourceId] AS
[ProjectResourceId],
[Join2].[Role] AS [Role],
[Join2].[Resource_ResourceId] AS
[Resource_ResourceId],
[Join2].[ProjectId] AS [ProjectId],
[Join2].[Name1] AS [Name1],
[Join2].[Start] AS [Start],
[Join2].[End] AS [End],
[Join2].[CustomerId] AS [CustomerId],
[Join2].[Contact_Email] AS [Contact_Email1],
[Join2].[Contact_Phone] AS [Contact_Phone1],
[Join2].[Name2] AS [Name2],
CASE WHEN ([Join2].[ProjectResourceId]
IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
FROM [dbo].[Resource] AS [Extent1]
LEFT OUTER JOIN (SELECT [Extent2].[ProjectResourceId] AS
[ProjectResourceId],
[Extent2].[Role] AS [Role], [Extent2].[Resource_ResourceId] AS
[Resource_ResourceId],
[Extent3].[ProjectId] AS [ProjectId], [Extent3].[Name] AS [Name1], [Extent3].[Start] AS [Start], [Extent3].[End] AS [End], [Extent4].[CustomerId] AS [CustomerId], [Extent4].[Contact_Email] AS [Contact_Email], [Extent4].[Contact_Phone] AS [Contact_Phone], [Extent4].[Name] AS [Name2]
FROM [dbo].[ProjectResource] AS [Extent2]
INNER JOIN [dbo].[Project] AS [Extent3] ON [Extent2].[Project_ProjectId] =
[Extent3].[ProjectId]
INNER JOIN [dbo].[Customer] AS [Extent4] ON [Extent3].[Customer_CustomerId] =
[Extent4].[CustomerId] ) AS [Join2] ON [Extent1].[ResourceId] = [Join2].[Resource_ResourceId]
) AS
[Project1]
ORDER BY
[Project1].[ResourceId] ASC, [Project1].[C1] ASC
可以想象,EF 为了同时连接所有需要获取的数据做了大量的工作,因此产生了非常复杂的 SQL。
实体框架上下文所知的实体(从其中加载或标记为删除或插入)存储在所谓的本地或一级缓存中。马丁·福勒称之为身份地图,你可以在 http://martinfowler.com/eaaCatalog/identityMap.html 阅读更多关于这个概念的信息。基本上,上下文跟踪所有这些实体,因此,每当执行返回它们相关记录的查询时,它不需要物化它们。可以通过数据库集T5 的本地属性访问该缓存:
//all
local Projects
var projectsAlreadyLoaded = ctx.Projects.Local;
//filtered
local Projects
var projectsAlreadyLoadedBelongingToACustomer = ctx.Projects.Local.Where(x
=> x.Customer.Name == "Some Customer");
这些查询都不会进入数据库,它们都是在内存中执行的。 Local 属性实际上是ObservableCollection的一个实例,这意味着每当在本地缓存中添加或删除新项目时,都会有一个事件通知我们, CollectionChanged 。
ctx.Projects.Local.CollectionChanged += (sender, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
//an entity was added to the local cache
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
//an entity was removed from the local cache
}
};
//discard all known projects
(stop tracking their changes)
ctx.Projects.Local.Clear();
可以知道本地缓存中存在的所有实体,并查看它们的状态,如上下文所示。跟踪所有这些实体是变更跟踪者的责任。
//get the projects in local cache that have been modified
var modifiedProjects = ctx.ChangeTracker.Entries<Project>()
.Where(x => x.State == EntityState.Modified).Select(x => x.Entity);
正如您所猜测的,从本地缓存中获取实体比从数据库中加载实体要快得多。考虑到这一点,我们可以编写一个方法,比如下一个方法,用于透明地返回一个本地实体或者用 SQL 获取它。
//retrieve
from cache or the database
public static IQueryable<T> LocalOrDatabase<T>(this DbContext context,
Expression<Func<T, Boolean>> expression) where T : class
{
var
localResults = context.Set<T>().Local.Where(expression.Compile());
if (localResults.Any() == true)
{
return (localResults.AsQueryable());
}
return (context.Set<T>().Where(expression));
}