数据点
实体框架问题与解答
John Papa
代码下载位置:
DataPoints2008_05.exe
(962 KB)
Browse the Code Online

目录
我收到过数以百计与本主题相关的问题,通过分析,我发现开发人员对于实体框架及其数据访问和建模意义非常感兴趣。尽管无法回答所有的问题,但在本月的专栏中,我将回答一些具有代表性的问题。
理解实体建模、实体模型到关系数据库的映射以及实体数据模型 (EDM) 设计是掌握实体框架的首要步骤。在本问题与解答中,我将首先回答有关实体框架基础(包括 ObjectContext)的一些问题,然后介绍将实体客户端与实体 SQL 搭配使用的适宜时机和位置。我还将解释 EntityClient 和对象服务之间的差异,并介绍将 LINQ 和实体 SQL 与这些服务一起使用的意义。
分析以目标为 Microsoft® .NET Framework 的代码和以本机 SQL 代码创建的查询也是实体框架的一个重要部分,因此我会通过查看生成的 SQL 来探讨显式加载和预先加载。可从 MSDN® 杂志网站下载此专栏中的所有代码示例和示例 NorthwindEF 数据库。
可使用 LINQ 获取实体时为什么要使用实体 SQL? 每次讲到将实体 SQL 与 EntityClient 或对象服务一起使用时,都会有人问到此问题。(这是无可厚非的。刚开始探究实体框架时,我也是先想到这个问题!)LINQ 的强类型化和查询语法非常有吸引力,以致于开发人员不禁疑惑为什么需要使用新的语言来与实体进行交互。
为全面回答此问题,我必须先介绍可用于与 EDM 进行交互的三种主要技术:
- 使用 EntityClient 提供程序编写实体 SQL 查询
- 使用对象服务编写实体 SQL 查询
- 使用对象服务编写 LINQ 查询
每种技术都拥有共同的特征;例如,都直接或间接使用 EntityClient 提供程序。但是,它们产生的结果以及获得这些结果的方式却有所不同。
EntityClient 提供程序具有一系列对象,如果了解 ADO.NET 对象模型,则您应该熟悉这些对象。EntityConnection 用于连接到 EDM,EntityCommand 用于针对 EDM 发出查询,而命令的结果则通过 DbDataReader 返回。无论是通过对象服务直接还是间接使用 EntityClient,EntityClient 最终都会发出查询并返回结果。
这使得问题再次摆上桌面,为什么有 LINQ 却要使用实体 SQL?答案在于每种技术的优缺点。
EntityClient + 实体 SQL
通过使用 EntityClient API 编写代码,可最精细地控制这三种技术。可创建 EntityConnection 来连接 EDM,以 Entity SQL 编写一个查询并使用 EntityCommand 执行该查询,然后通过 DbDataReader 返回结果。此技术要精简一些,省去了 LINQ 和对象服务提供的一些语法修饰。
实体 SQL 最大的优点是其灵活性。基于字符串的语法有助于轻松地构建动态查询。如果需要创建临时查询,它将非常有用。
但是,这种灵活和精简也使得只能通过 DbDataReader 返回结果。无法使用 EntityClient 和实体 SQL 从 EDM 返回纯实体。DbDataReader 可供检索并用于在满足实体 SQL 查询的行集合中执行迭代。图 1 中的代码允许通过 DbDataReader 而非 Customers 实体在客户记录中执行迭代。

Figure 1 通过 DbDataReader 在各行中迭代
string city = "London";
using (EntityConnection cn = new EntityConnection("Name=Entities"))
{
cn.Open();
EntityCommand cmd = cn.CreateCommand();
cmd.CommandText = @"SELECT VALUE c FROM Entities.Customers AS c WHERE
c.Address.City = @city";
cmd.Parameters.AddWithValue("city", city);
DbDataReader rdr = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
while (rdr.Read())
Console.WriteLine(rdr["CompanyName"].ToString());
rdr.Close();
}
以下是一个有用的小窍门:当处于调试模式时,您可能收到一个错误,指出您无法读取列 X。此错误仅出现在调试模式中,并且可通过关闭调试模式的自动窗口来避免出现此错误。它是实体框架测试版 3 中的一个已知问题。
目前尚无适用于实体 SQL 的数据操作语言 (DML)。这意味着无法直接针对 EDM 使用 Insert、Update 或 Delete 语句(请参见图 2)。

Figure 2 实体框架 API
| |
EntityClient 和实体 SQL |
对象服务和实体 SQL |
对象服务和 LINQ |
| 定向到 EntityClient 提供程序 |
是 |
否 |
否 |
| 适合临时查询 |
是 |
是 |
否 |
| 可直接发出 DML |
否 |
否 |
否 |
| 强类型化 |
否 |
否 |
是 |
| 可将实体作为结果返回 |
否 |
是 |
是 |
对象服务 + 实体 SQL
另一技术是使用对象服务并利用实体 SQL 来执行查询。从而不再与 EntityClient 提供程序直接交互(尽管实际上它仍会与提供程序通信)。您是使用 ObjectContext 和 ObjectQuery<T> 来针对 EDM 发出查询。
此技术非常适合于发出临时查询(与第一种技术相同)。但是,它不是通过 DbDataReader 返回数据,将对象服务与实体 SQL 一起使用时可从 EDM 返回实体。因此,可牢靠地提供以下优点:查询灵活且返回最佳实体。
由于实体 SQL 目前缺少 DML 构造,因此无法使用实体 SQL 和对象服务来发出 Insert、Update 或 Delete 命令。但是,可使用此技术从 EDM 检索实体,然后使用 ObjectContext 中的 SaveChanges 方法来更新实体。正如您所看到的,下面的代码示例会在 Customers 集合中执行迭代:
string city = "London";
using (Entities entities = new Entities())
{
ObjectQuery<Customers> query = entities.CreateQuery<Customers>(
"SELECT VALUE c FROM Customers AS c WHERE c.Address.City = @city",
new ObjectParameter("city", city)
);
foreach (Customers c in query)
Console.WriteLine(c.CompanyName);
}
对象服务 + LINQ
将对象服务 LINQ 一起使用并不适合于临时查询,这一点与其他技术不同。下面的代码示例从 EDM 返回了 Customers 集合:
string city = "London";
using (Entities entities = new Entities())
{
var query = from c in entities.Customers
where c.Address.City == city
select c;
foreach (Customers c in query)
Console.WriteLine(c.CompanyName);
}
与实体 SQL 一样,LINQ 也不支持 DML 语句的直接语法。目前,仅可在使用对象服务(通过 SaveChanges 方法)时针对数据库更新实体。通过从 EDM(实体框架会跟踪其更改)返回实体来实现这个目的。简而言之,既非 LINQ 也非实体 SQL 执行更新操作,实际是 EDM 的 ObjectContext 执行此操作。
我在图 2 中总结了这些技术的不同之处。因此,为什么有 LINQ 却要使用实体 SQL?如果需要临时查询或希望创建比使用 LINQ 实现的查询更加灵活的查询,则应使用实体 SQL。否则,建议使用 LINQ 和对象服务,以便可受益于其强类型化以及返回实体和投影的功能。
如使用 LINQ 的强类型化语法,还可在设计时发现许多错误,不必等到运行应用程序时才发现。我非常喜欢这一功能——它使我可以一心一意编写代码,不必通过执行构建和运行来找出错误。
ObjectContext 的作用是什么? ObjectContext 是到对象服务的 EntityConnection 的通道。它通过底层 EntityConnection 提供对 EDM 的访问。例如,可通过 ObjectContext 访问实体,询问 ObjectContext 以找出有关对象状态的信息,以及使用 CreateQuery 方法创建 ObjectQuery<T> 查询。
ObjectContext 的另一目的是为对象提供获取数据库条目更新信息的方法。例如,可使用 ObjectContext 方法来将实体添加到 ObjectContext、删除实体、操作实体以及最终将实体更改保存到数据库(通过 SaveChanges 方法)。
显式加载和预先加载在实体框架中的工作原理是什么? 显式加载是 LINQ to Entities 和实体框架中的默认行为。在实体框架中执行查询时,可完全访问查询所返回的实体,但不会立即加载任何相关实体。例如,如果编写一个查询来检索 EDM 中的所有 Order,实际执行的 SQL 查询将获取订单记录并返回 Orders 实体集合。然而,查询不会获取与订单相关的客户记录,所以不会加载 Orders 实体的相关 Customers 实体。因此,由于并未加载,当以下代码示例尝试访问 Order 的 Customers 时,它会抛出异常:
using (Entities entities = new Entities())
{
var query = (from o in entities.Orders
where o.Customers.CustomerID == "ALFKI"
select o).First<Orders>();
Orders order = query as Orders;
Console.WriteLine(order.OrderID);
Console.WriteLine(order.Customers.CompanyName);
}
实体框架针对 EntityReference 类的每个实例提供一个 Load 方法。此方法可用于显式加载与另一实体相关的一个集合。例如,可更改之前的代码示例,告诉实体框架需要获取 Order 的 Customers 记录。图 3 显示了执行显式加载的修订后代码。它首先检查是否已加载 Customers 实体。如果没有,则为该 Order 加载 Customers。此技术称为显式加载。

Figure 3 显式加载
using (Entities entities = new Entities())
{
var query = (from o in entities.Orders
where o.Customers.CustomerID == "ALFKI"
select o);
foreach (Orders order in query)
{
if (!order.CustomersReference.IsLoaded)
order.CustomersReference.Load();
Console.WriteLine(order.OrderID + " --- " +
order.Customers.CompanyName);
}
}
图 3 中的示例将执行 SQL 查询来获取所处理的每个订单的 Customer 记录。如果是迭代几百个或者几十个订单,可能会针对数据库执行多个单独的查询。如果提前知道所需数据多于查询所返回的数据(如订单的客户信息),可提前加载此信息。
如需有关此主题的更多信息(直接取自负责实体框架的 Microsoft 团队),请参阅侧栏:“深入分析:实体框架和数据加载。”
图 4 展示了预先加载技术。LINQ 查询中针对 Orders 实体调用的 Include 方法接受了一个参数,该参数在本示例中将要求查询不仅要检索 Orders,而且还要检索相关的 Customers。此技术将生成单个 SQL 语句,它会加载满足 LINQ 查询条件的所有 Order 和 Customer。

Figure 4 预先加载
using (Entities entities = new Entities())
{
var query = (from o in entities.Orders.Include("Customers")
where o.ShipCountry == "USA"
select o);
foreach (Orders order in query)
Console.WriteLine(order.OrderID + " --- " +
order.Customers.CompanyName);
}
您必须了解加载类型的重要性。如果在迭代集合的过程中使用 Load 方法(如图 3 所示)来显式加载实体,它会造成多个查询作用于数据库,对 Load 方法的每个调用都有一个查询。
如果仅需访问一次或两次数据,则该技术的效果极好。然而,如果知道对于给定的实体,您将始终需要访问相关实体中的数据,则使用 Include 方法的预先加载(如图 4 所示)会更好。
最佳行动方案是分析可供使用的不同选项的性能,并对其进行测试以确定哪个最适合您的具体情况。与大多数决策一样,是使用预先加载还是显式加载取决于具体情形。
如何了解将执行的 SQL? 使用 ObjectQuery 创建查询时,我常常希望了解将执行什么 SQL,我发现了两项非常有用的技术。第一项是使用 SQL Server® Profiler 工具(或任何数据库引擎的可用分析工具)。第二项技术是使用 ObjectQuery 类的 ToTraceString 方法。
图 5 展示了如何对 ObjectQuery<T> 调用 ToTraceString 方法。请注意,ObjectContext 的连接是打开的(EntityConnection)。ToTraceString 方法要求连接处于打开状态,这样它才可确定将执行的查询。ObjectQuery<T> 和 EntityCommand 类中均提供 ToTraceString 方法。

Figure 5 使用 ToTraceString
string city = "London";
using (Entities entities = new Entities())
{
ObjectQuery<Customers> query = entities.CreateQuery<Customers>(
"SELECT VALUE c FROM Customers AS c WHERE c.City = @city",
new ObjectParameter("city", city)
);
entities.Connection.Open();
Console.WriteLine(query.ToTraceString());
foreach (Customers c in query)
Console.WriteLine(c.CompanyName);
}
使用复杂类型可以执行哪些操作? 实体框架提供称为复杂类型的一个数据结构来代表一组属性,它们通常彼此紧密相关。例如 Address 类型。复杂类型可用于为客户创建 Address 类型,这样 Customer 实体与地址相关的属性(如 City、Region 和 Phone)将在 Address 类型之下而非直接位于 Customer 类型之下。复杂类型提供类似标量属性的逻辑分组,从而可更轻松地找出某个实体的紧密相关属性并在 EDM 中对它们进行逻辑分组。与实体相似的是,复杂类型也包含标量属性;与其不同的是,它们没有标识(键值),并且无法通过 ObjectContext 将它们保存到数据库中。复杂类型是实体的一个方面,并非实体本身。如果对实体的相关属性执行逻辑分组,它们是不错的工具。
如何创建复杂类型? 由于 EDM 设计器现在并不支持以可视化方式创建复杂类型,因此执行创建时必须在 XML 编辑器中采用破解方式打开 EDMX 文件。第一步是使用概念架构定义语言 (CSDL) 创建 ComplexType;然后,修改将引用复杂类型的 EntityType。图 6 显示 CSDL 格式的 Customers EntityType 以及新创建的 AddressType 复杂类型。请注意,Customers EntityType 删除了 Address、City、Region 和其他与地址相关的属性并替换为名为 Address 的新属性。这个新的 Address 属性代表 AddressType 复杂类型。

Figure 6 创建 ComplexType
<EntityType Name="Customers">
<Key>
<PropertyRef Name="CustomerID" />
</Key>
<Property Name="CustomerID" Type="String" Nullable="false"
MaxLength="5" FixedLength="true" />
<Property Name="CompanyName" Type="String" Nullable="false"
MaxLength="40" />
<Property Name="ContactName" Type="String" MaxLength="30" />
<Property Name="ContactTitle" Type="String" MaxLength="30" />
<Property Name="Address" Type="Self.AddressType"
Nullable="false"/>
<NavigationProperty Name="Orders"
Relationship="NorthwindEFModel.FK_Orders_Customers"
FromRole="Customers" ToRole="Orders" />
</EntityType>
<ComplexType Name="AddressType">
<Property Name="Address" Type="String" MaxLength="60" />
<Property Name="City" Type="String" MaxLength="15" />
<Property Name="Region" Type="String" MaxLength="15" />
<Property Name="PostalCode" Type="String" MaxLength="10" />
<Property Name="Country" Type="String" MaxLength="15" />
<Property Name="Phone" Type="String" MaxLength="24" />
<Property Name="Fax" Type="String" MaxLength="24" />
</ComplexType>
下一步是修改映射规范语言 (MSL) 格式的映射,以将新的复杂类型纳入考虑范畴。图 7 显示了 Customers EntityType 与新 AddressType 复杂类型的映射。这将生成项目,并且现在可于代码中引用复杂类型。例如,可执行以下 LINQ 查询以仅筛选 London 中的客户:

Figure 7 映射复杂类型
<EntitySetMapping Name="Customers">
<EntityTypeMapping
TypeName="IsTypeOf(NorthwindEFModel.Customers)">
<MappingFragment StoreEntitySet="Customers">
<ScalarProperty Name="CustomerID" ColumnName="CustomerID" />
<ScalarProperty Name="CompanyName" ColumnName="CompanyName" />
<ScalarProperty Name="ContactName" ColumnName="ContactName" />
<ScalarProperty Name="ContactTitle" ColumnName="ContactTitle" />
<ComplexProperty Name="Address"
TypeName="NorthwindEFModel.AddressType">
<ScalarProperty Name="Address" ColumnName="Address" />
<ScalarProperty Name="City" ColumnName="City" />
<ScalarProperty Name="Region" ColumnName="Region" />
<ScalarProperty Name="PostalCode" ColumnName="PostalCode" />
<ScalarProperty Name="Country" ColumnName="Country" />
<ScalarProperty Name="Phone" ColumnName="Phone" />
<ScalarProperty Name="Fax" ColumnName="Fax" />
</ComplexProperty>
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
var query = from c in entities.Customers
where c.Address.City == "London"
select c;
结束语
在本月的专栏中,我对比了 EntityClient 和对象服务与实体 SQL 和 LINQ 一起使用的不同之处。我还简要谈及了创建复杂类型的方式和原因,并且展示了预先加载和显式加载的工作原理。我从相关开发人员那里收到了很多有关实体框架的问题,因此我打算在未来几期的“数据点”中介绍类似主题和实用提示。
请将您想向 John 询问的问题和提出的意见发送至 mmdata@microsoft.com.
John Papa 是 ASPSOFT (
aspsoft.com) 的一位资深 .NET 顾问,同时也是一位狂热的棒球迷,在夏季的大多数夜晚,他都与家人以及忠实的狗 Kadi 一起为洋基队加油。John 是 C# 领域的一位 MVP 和 INETA 发言人,撰写过多本 ADO、XML 和 SQL Server 方面的书籍。他经常在行业会议(如 DevConnections 和 VSLive)上发表演讲,或者在
johnpapa.net 上撰写博客文章。