Microsoft Corporation
适用于:
-
ADO.NET
-
.NET Framework
-
语言集成查询
摘要:消除数据模型和语言间的阻抗失谐,使应用程序开发人员不必花时间处理即将推出的 ADO.NET 版本的新功能。
本页内容
简介
ADO.NET 实体框架:在正确的抽象级别上建模
ADO.NET 实体框架:对象服务
LINQ to Entities:语言集成查询
参考资料
附录
简介
为了解决不同数据表示(如对象和关系存储区)之间的阻抗失谐,应用程序开发人员必须编写大量代码。这一状况可以得到改善。的确,有许多方案可以提供正确的框架,使应用程序开发人员集中精力于开发应用程序,简化桥接异构数据表示的复杂性。
即将推出的 ADO.NET 版本要实现的基本目标,是提高数据编程的抽象级别,帮助消除数据模型和语言间的阻抗失谐,使应用程序开发人员不必另想办法加以解决。两方面创新使得实现这一目标成为可能,它们是语言集成查询和 ADO.NET 实体框架。实体框架是 ADO.NET 技术系列中的新成员。ADO.NET 将通过 LINQ 启用许多数据访问组件:LINQ to SQL、LINQ to DataSet 和 LINQ to Entities。
本文档说明 ADO.NET 实体框架、它针对什么样的问题空间以及其各组件如何解决这些问题。
我们的最终目标
用于创建业务应用程序的理想环境应该如此:开发人员在描述正在建模的业务逻辑和问题域状态时,来自底层表示和予以支持的基础结构的“噪音”为最小或没有。应用程序应能够在问题域条件下与存储区进行交互,以维持系统状态不变;特别是在概念域模型条件下,应与底层存储区的逻辑架构完全分离。
大家可能会期望开发人员编写这样一段代码:
我们将使用订单跟踪存储 using(OrderTracking orderTracking = new OrderTracking()) { //查找华盛顿销售人员的 //未决订单 var orders = from order in orderTracking.SalesOrders where order.Status == "Pending Stock Verification" && order.SalesPerson.State == "WA" select order; foreach(SalesOrder order in orders) { //获取用于验证的 StockAppProduct 对象的 //列表 List<StockAppProduct> products = new List<StockAppProduct>( from orderLine in order.Lines select new StockAppProduct { ProductID = orderLine.Product.ID, LocatorCode = ComputeLocatorCode(orderLine.Product) } ); //通过库存管理系统 //确保该订单上的所有产品 //都有库存 if(StockApp.CheckAvailability(products)) { //将订单标记为“shippable” order.Status = "Shippable"; } } //如果将一个或多个订单标记为“shippable”, //则将此更改保存到存储区中 orderTracking.SaveChanges(); }
以上代码中有两个重要元素需要注意:
无人工构造。应用程序要适应底层存储架构的特点,这很常见。例如,在关系数据库顶层构建的应用程序经常要广泛使用连接,以在关系间进行导航。相反,而在上述代码中,数据“形式”遵循的是正在建模的问题抽象;“orders”具有“order lines”,并且与一个“sales person”相关联。
无探测。该代码的数据库密集程度很高,但是代码中没有数据库连接对象、没有诸如 SQL 等外部语言查询表达式、没有参数绑定、也没有内嵌配置。从这种意义上讲,可以说这段代码采用的是“纯粹的业务逻辑”。
这是 ADO.NET,特别是在 LINQ 和实体框架的协作下,为应用程序开发提供的表达与抽象级别的类。
本文其余部分将详细描述各元素如何协作,以实现上述示例的正常运行。
ADO.NET 实体框架:在正确的抽象级别上建模
每个业务应用程序都显式或隐式地包含一个概念数据模型,用以描述问题域的各个元素,以及各元素的结构、各元素间的关系以及其约束等。
既然目前大多数应用程序都是在关系数据库顶层编写而成的,那么或早或晚,它们都必须处理以关系形式表示的数据。即便设计中使用了更高级别的概念模型,该模型一般也不是直接“可执行”的,因此需要将其转换为关系形式,并应用到逻辑数据库架构和应用程序代码中。
虽然在过去的数十年中,关系模型极为有效,但它所针对的抽象级别,通常不适合对使用现代开发环境创建的大多数业务应用程序进行建模。
我们可以通过一个示例来阐明这一点。这是 Microsoft SQL Server 2005 中包含的 AdventureWorks 示例数据库的变量段:
假设我们正在数据库顶层构建一个人力资源应用程序,并且在某时想要查找 2006 年雇佣的所有专职员工,同时要列出他们的姓名和职位,我们需要编写如下 SQL 查询:
SELECT c.FirstName, e.Title FROM Employee e INNER JOIN Contact c ON e.EmployeeID = c.ContactID WHERE e.SalariedFlag = 1 AND e.HireDate >= '2006-01-01'
该查询比实际需要的要复杂得多,原因有如下几点:
-
虽然该特定应用程序只需处理“employees”,而实际上,它仍需面对逻辑数据库架构已标准化这一现状,以使员工的联系信息存储在单独表中,如员工姓名。虽然这与应用程序无关,但开发人员还是需要在处理员工的应用程序的所有查询中包含该知识。通常,应用程序不能选择逻辑数据库架构(例如,从公司核心系统数据库读取数据的部门应用程序),而且,如何将逻辑架构映射到该应用程序所需数据的“适当”视图,这一相关知识是通过整个代码的查询隐式表达的。
-
该示例应用程序只适用于专职员工,因此理想情况下,不应看到任何其他类型的员工。然而,既然这是一个共享数据库,那么所有员工信息都会存储在此“Employee”表中,并通过“SalariedFlag”列进行分类;这又意味着,由该应用程序发出的每次查询都会嵌入如何区分各类员工的知识。理想情况下,如果应用程序处理的是数据的一个子集,系统只应显示该数据子集,而且开发人员应该能够以声明的方式指示适当的子集。
以上强调的问题基于的事实是,逻辑数据库架构并不总能为给定的应用程序显示正确的数据视图。请注意,在该特定实例中,可使用现有架构使用的同一概念创建更为合适的视图(即,关系模型中的表和列)。如果构建以数据为中心的应用程序,由于无法使用关系模型单独提供的结构进行轻松建模,还会出现其他问题。
我们来看另一个关于销售系统的应用程序,它也是在同一数据库的顶层构建的。如果使用上一示例的逻辑架构,我们将必须使用以下查询来获取销售订单金额超过 20 万美元的所有销售人员的信息:
SELECT SalesPersonID, FirstName, LastName, HireDate FROM SalesPerson sp INNER JOIN Employee e ON sp.SalesPersonID = e.EmployeeID INNER JOIN Contact c ON e.EmployeeID = c.ContactID INNER JOIN SalesOrder o ON sp.SalesPersonID = o.SalesPersonID WHERE e.SalariedFlag = 1 AND o.TotalDue > 200000
比较起概念上相对简单的问题,该查询同样相当复杂。造成这种复杂性的原因包括:
-
同样,逻辑数据库架构太过零碎,从而导致应用程序过分复杂。在该示例中,应用程序大概只考虑到了“sales persons”和“sales orders”;销售人员信息分散在 3 个表中并无关紧要,但该应用程序代码中还是需要包含这些内容。
-
从概念上说,我们知道,一名销售人员可能涉及到零个或多个销售订单;但是,查询需要以某种无法利用该知识的方式来程式化;相反,该查询必须建立显式连接来遍历该关联。
除了以上指出的问题,两个查询都引出了另一个有趣的问题:它们分别返回有关多名员工和多名销售人员的信息。然而,您却无法向系统查询某个“employee”或某个“sales person”的信息。系统并不了解它们的含义。查询所返回的所有值只是简单的投影,这些投影将表行中某些值复制到结果集,而丢失了与数据源的任何关系。这就意味着,整个应用程序代码对于核心应用程序概念(如员工)没有普遍的了解,否则它将可以充分执行与此概念相关的约束。另外,因为结果只是投影,描述数据来源的源信息丢失,所以要求开发人员使用特定的 SQL 语句,显式地告诉系统应怎样进行插入、更新和删除。
我们刚刚讨论的问题可分为两大类:
-
一些是与以下事实相关的,即逻辑(关系)模型和相关基础结构无法利用应用程序数据模型的概念域知识,也就无法理解业务实体及其彼此之间的关系或是其约束。
-
一些是与实践问题相关的,即数据库的逻辑架构通常与应用程序的需要不匹配;那些架构通常无法更改,因为它们跨多个应用程序进行共享,或由操作、数据所有权、性能或安全等非功能性要求所致。
上述问题在大多数以数据为中心的企业应用程序中非常普遍。为了解决这些问题,ADO.NET 引入了实体框架,它包括一个数据模型和设计时与运行时服务集,允许开发者描述应用程序数据以及与其在符合业务应用程序的“概念性”抽象级别上进行交互,从而帮助实现应用程序从底层逻辑数据库架构的隔离。
在概念抽象级别上建模:实体数据模型
为了解决上一节中指出的第一个问题,我们需要一种使用更高级别的构造描述数据结构(架构)的方式。
实体数据模型(或 EDM)是一个实体关系数据模型。EDM 引入的关键概念有:
-
实体:实体是“实体类型”的实例(如 Employee、SalesOrder),即含有一个关键字的充分结构化记录。实体将被归入各“实体集”中。
-
关系:关系用于关联各实体,属于“关系类型”的实例(例如 SalesPerson 发布的 SalesOrder)。关系将被归入到各关系集中。
引入实体和关系的显式概念,开发人员可以更加明确地描述架构。除了这些核心概念外,EDM 还支持各种构造以进一步扩展其表达。例如:
-
继承:实体类型可以定义为从其他类型继承(如,Employee 可以从 Contact 继承)。这种继承是严格结构化的,指的是没有继承面向对象的编程语言中发生的“行为”。要继承的是基本实体类型的结构;除继承其结构外,测试基本实体类型时,一个派生实体类型的实例可满足“is a”关系。
-
复杂类型:除大多数数据库支持的一般标量类型外,EDM 还支持复杂类型的定义,并支持将其用作实体类型的成员。例如,您可以定义一个含有 StreetAddress、City 和 State 属性的 Address 复杂类型,然后将类型 Address 属性添加到 Contact 实体类型。
通过所有这些新工具,我们可以使用一个概念模型,对上一节用到的逻辑架构进行重新定义:
浅显地说,该架构具有以下元素:
-
三个实体类型:SalesPerson、SalesOrder 和 StoreSalesOrder。请注意,StoreSalesOrder 即是 SalesOrder(继承自 SalesOrder),其特征是包含税务信息。
-
SalesOrder 和 SalesPerson 实体类型之间的关系
-
两个实体集:SalesOrders 和 SalesPeople;请注意,SalesOrders 实体集可同时包含 SalesOrder 和 StoreSalesOrder 两种实体类型的实例。
新模型更好地体现了销售应用程序应在其存储区中使用这一观点。请注意以下重要事项:销售人员信息不再分散在多个表中,而是存储在单个实体集中;而且,架构中没有主键/外键;相反,将显式声明模型中存在某种关系。
为提供具体示例,上一节中,我们需要在查询中包含 3 路连接来访问销售人员的姓名,就像这样:
SELECT sp.FirstName, sp.LastName, sp.HireDate FROM SalesPerson sp INNER JOIN Employee e ON sp.SalesPersonID = e.EmployeeID INNER JOIN Contact c ON e.EmployeeID = c.ContactID WHERE e.SalariedFlag = 1 AND e.HireDate >= '2006-01-01'
现在,既然我们有了更高级别的 EDM 模型,我们就可以把针对 SalesPeople 实体集的同一查询编写成:
SELECT sp.FirstName, sp.LastName, sp.HireDate FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE e.HireDate >= '2006-01-01'
这明显要简单得多,且具有完全相同的语义,同时,关于如何为应用程序构建适当数据视图的信息,现在可以声明性地表达到外部构造中(EDM 构架和我们将稍候讨论的映射)。
ADO.NET 包含用于设计这些架构的可视化工具。该工具的输出是一个 XML 文件,它通过“架构描述语言”(SDL) 来描述概念架构。有关上述 EDM 架构的 XML 版本,请参阅 6.1 节 - 附录“以 XML 表示的 EDM 架构示例”。
现在,如果新的概念架构与实际数据库中的逻辑架构不一样,系统是怎么知道如何在架构之间来回反复呢?答案是“映射”。
有关实体数据模型的详细信息,请参阅本文档末尾的 [EDM] 参考资料。
将数据引入 EDM 模型:映射
EDM 是一个概念数据模型,可用于对给定域的数据进行建模。然而,有时需要将数据存储在实际数据库中,特别是关系数据库中。
为了提供一种机制将使用 EDM 建模的数据存储到关系数据库中,ADO.NET 实体框架包含一个功能强大的客户端视图基础结构,专门管理关系存储区呈现的逻辑数据库架构与该应用程序使用的概念 EDM 架构之间的转换。
除了 EDM 架构外,系统会把输入看作是映射规范;该映射规范由映射工具生成,而且也是 XML 文件。
继续以该示例为例,如果要将本节开始用到的逻辑数据库架构映射到上一节的概念 EDM 架构,就需要这么做:
当使用映射工具创建从概念到逻辑的映射时,映射工具会生成一个 XML 文件,可用于 ADO.NET 实体架构的运行时组件。6.2 节 - 附录“以 XML 表示的映射示例”中包含以上所示映射的 XML 表示。幸运的是,通过该工具,绝大多数用户就没有必要理解或处理这些 XML 文件了。
除了支持将架构呈现为 EDM 架构外,ADO.NET 中的客户端视图基础结构还具有其他优点。本节开始,我们讨论了具有架构的数据库(现在为应用程序开发人员所拥有)如何将复杂性引入应用程序代码。通过使用客户端视图,外部逻辑架构引入的复杂性无需进入应用程序代码,取而代之的是,可以创建视图,来为应用程序使用的数据执行任何所需的改造。那样的话,应用程序就具有了一个数据视图,对于所要解决的问题空间来说意义重大。不管新的 EDM 构造是否用于得出的模型中,这都会很有用。
此处一个明显的问题是,为什么不能在这里只使用传统的数据库视图。虽然数据库视图可以提取许多映射,但该解决方案将因为几个流程和功能原因而无法正常运行:(a) 许多视图都太过复杂,开发人员无法以具有成本效益的方式予以生成和维护,(b) 存储区中许多具有自动更新属性的视图类受到限制,而且 (c) 大中型企业的核心系统数据库用于许多中央与部门应用程序,并且在数据库中具有创建多个视图的各个单个应用程序,这会影响数据库架构,还会给数据库管理员带来巨大的维护工作量。另外,数据库视图仅限于关系模型的表达,通常缺乏实体数据模型的一些更加现实的概念,如继承和复杂类型。
ADO.NET 客户端视图完全在客户端运行,因此每位应用程序开发人员都可以创建视图,使数据结构适合于各个特定应用程序,同时不会影响实际的数据库或其他应用程序。实体框架支持的可更新视图类,比任何关系存储区支持的种类要宽泛得多。
呈现 EDM 并映射到 ADO.NET API:映射提供程序
开始 EDM 和映射概念似乎极为抽象,所以此时您大概在想它们是如何具体呈现在 ADO.NET API 中的。
我们的选择是,为 ADO.NET 引入一种称为“映射提供程序”的数据访问提供程序。常规提供程序连接至存储区,在其逻辑架构中为应用程序提供一个存储数据的视图,与此相似,映射提供程序连接至概念 EDM 模型,为应用程序提供一个概念数据视图。
映射提供程序获得了 EDM 架构和映射信息,因此它可以在内部使用映射基础结构,来实现逻辑和概念架构之间的转换。
因此,例如要执行针对 EDM 模型的查询,以查找给定雇佣日期后销售人员的姓名和雇佣日期,ADO.NET 代码应为:
using(MapConnection con = new MapConnection(Settings.Default.AdventureWorks)) { con.Open(); MapCommand cmd = con.CreateCommand(); cmd.CommandText = "SELECT sp.FirstName, sp.LastName, sp.HireDate " + "FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " + "WHERE sp.HireDate > @date"; cmd.Parameters.AddWithValue("date", hireDate); DbDataReader r = cmd.ExecuteReader(); while(r.Read()) { Console.WriteLine("{0}\t{1}", r["FirstName"], r["LastName"]); } }
请注意,该模式对于 ADO.NET 开发人员来说应该是非常熟悉的;它和 ADO.NET 2.0 代码类似,唯一的差异是使用了不同的提供程序。
在后台,映射提供程序将使用 EDM 架构和映射/视图信息,以实现与概念模型的来回转换。然后,它将使用常规 ADO.NET 提供程序与底层数据库进行对话(如,它会使用 System.Data.SqlClient 来与 SQL Server 数据库进行对话)。
针对 EDM 模型的查询:实体 SQL
当应用程序使用 EDM 模型和映射提供程序进行访问时,将不再直接连接到数据库或查看任何特定数据库的构造;整个应用程序将在更高级的 EDM 模型条件下进行操作。
这意味着,您可以不必再使用本地数据库查询语言;数据库不会理解 EDM 模型,不仅如此,当前的数据库查询语言也没有处理 EDM 引入元素所需的构造,如继承、关系、复杂类型等。
为启用对 EDM 模型的查询,ADO.NET 实体架构引入了一种查询语言,专门设计用于 EDM 并且可以利用实体数据模型的完全表达。该语言称为 Entity SQL(实体 SQL),而且对于曾使用过 SQL 方言的所有开发人员来说应该并不陌生。实体 SQL 为实体框架提供动态查询功能,此处的查询将在后绑定应用程序的上下文环境中,在设计时进行静态公式化或在运行时进行结构化。
例如,这是来自上一示例的有效实体 SQL 查询:
SELECT sp.FirstName, sp.LastName, sp.HireDate FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE sp.HireDate > @date
实体 SQL 查询的整体结构是普通的 SELECT-FROM-WHERE 语句,在传统 SQL 中呈现。除了这一基本元素,实体 SQL 还引入各种概念,使得开发人员可以利用概念 EDM 模型的表达;下述内容为实体 SQL 所引发的其他重要概念:
处理实体。概念 EDM 架构是围绕实体设计的。业务概念被直接反映到 EDM 实体类型中,其实例存储在实体集中。同样,关系环境中的查询针对表进行公式化,EDM 环境中的查询针对实体集进行公式化。所以,查询的起始点是来自一个或多个实体集的一组实体。
根据您在每个具体方案中的需求,您可以选择投影出个别值还是保持整个实体。若您想要系统帮助实施围绕实体构建的服务时,则应该选择保持整个实体。例如,得益于 EDM 架构中提供的元数据和映射规范,ADO.NET 实体框架知道如何将更新反映回存储区的实体,使用户不必提供 ADO.NET DataAdapter 从前所需的 INSERT、UPDATE 和 DELETE 命令。
带投影的查询与常规 SQL 查询极为相似,除非另行规定,否则表别名是必需的:
SELECT sp.FirstName, sp.LastName, sp.HireDate FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE sp.HireDate > @date
如果本示例中,我们想投影出实际的销售人员实体,我们会编写如下代码:
SELECT VALUE sp FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE sp.HireDate > @date
“VALUE”修饰符指示该系统应在结果集中生成一组表示实体实例或标量的值;该结果应保留完全描述这些值所需的所有元数据,包括如何提取实体的主键、实体来自哪里等。
从结果集的角度看,当投影出一个实体时,结果 DataReader 对象会具有一个相应的列,来存储实体类型的每个顶级成员(所以,这酷似在查询中投影出所有的列,所不同的是有关实体的其他元数据也可用)。
关系导航。除实体外,贯穿关系的显式关联概念是 EDM 的另一个关键元素;因为系统知道实体间的关系,所以查询语言可用于显式导航那些关系,而不必使用诸如连接等构造。
例如,在我们使用该查询查找订单金额超过某一特定值的销售人员之前:
SELECT SalesPersonID, FirstName, LastName, HireDate FROM SalesPerson sp INNER JOIN Employee e ON sp.SalesPersonID = e.EmployeeID INNER JOIN Contact c ON e.EmployeeID = c.ContactID INNER JOIN SalesOrder o ON sp.SalesPersonID = o.SalesPersonID WHERE e.SalariedFlag = 1 AND o.TotalDue > 200000
该查询的复杂性源于 1) 销售人员信息跨表分散这一事实和 2) 需通过连接,间接地由销售人员导航到销售订单。
使用我们先前定义的新概念 EDM 架构,我们可以用以下方式来表达该查询:
SELECT VALUE sp FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp WHERE EXISTS( SELECT VALUE o FROM NAVIGATE(p, AdventureWorks.SalesPerson_Order) AS o WHERE o.TotalDue > 200000)
“NAVIGATE”运算符允许查询显式遍历一个关系;应用程序不需要知道关系的维护方式,或者不必使用诸如连接等间接方式来使用它。
继承支持。既然 EDM 支持实体类型的继承,那么查询语言应让用户可以表达查看类型继承层级的查询。
例如,我们的概念 EDM 构架有一个实体类型 SalesOrder,和一个具有具体特性的子类型 StoreSalesOrder。因为每个 StoreSalesOrder 即为 SalesOrder,所以对 SalesOrders 实体集的查询应返回一个多态结果集,其中包含 SalesOrder 和 StoreSalesOrder 两个类型的实例:
SELECT VALUE o FROM AdventureWorks.AdventureWorksDB.SalesOrders AS o
如果我们只想要存储区的销售订单,我们会通过利用类型层级来显式要求系统:
SELECT VALUE o FROM AdventureWorks.AdventureWorksDB.SalesOrders AS o WHERE o IS OF (AdventureWorks.StoreSalesOrder)
此处,IS OF 运算符将进行检查,以查看表达式(本实例中是“o”)是否为圆括号内指定类型的实例。
所获经验。实体 SQL 中的各种扩展专门设计用于提供针对 EDM 架构的第一类查询体验,除此以外,实体 SQL 还囊括了源自更多传统 SQL 方言体验的多种增强功能。
例如,实体 SQL 中,表达式可以产生标量或集合,并且集合是第一类构造,可以在大多表达式上下文中显示,使其具有完全可组合性。例如,集合可以进入 FROM 子句并作为一个查询的源,或者可以进入 SELECT 列表,其中的一列将以结果集作为集合类型,而非标量。
有关实体 SQL 的深层描述,请参阅本文档末尾参考资料中的 [eSQL]。
ADO.NET 实体框架:对象服务
绝大多数用于业务应用程序的新代码是以通用的、面向对象的编程语言编写的,如 Visual Basic 和 C#。这些编程语言及其周围开发环境以类对业务实体进行建模,以代码对其行为进行建模。为与此对照,到目前为止,ADO.NET 已将数据库中的数据作为“值”来提供,即行和列。为与数据库进行交互,应用程序必须处理数据与应用程序代码间存在的阻抗失谐;这包括公式化查询和提供结果的方式。
ADO.NET 实体框架包含一个对象服务层,可以有效降低并且常常可以消除这种阻抗失谐。
同样的数据,但作为对象来提供
应用程序,特别是由几个应用程序组成的大型应用程序或大型系统,很少会在整个代码库中只使用一种数据表示;诸如查询的静态与动态知识、结果集结构、用户应用程序交互模型等几个方面,都影响着应用程序与数据库中的数据进行交互所需的方式。
ADO.NET 实体框架并非是引入一个全新的、独立的基础结构,以将数据库数据作为对象来提供,而是包含一个“对象服务”层,以集成其余堆栈,并将实体值作为 .NET 对象来提供,以此作为一种表示选择。
不管您选择将数据作为“值”(行和列)还是对象来使用,您都会用到同样的基础结构,那就是同一个供您使用的概念 EDM 架构和同样的映射与查询语言 - 实体 SQL。
例如,以下代码段将获取系统中销售人员的一个子集,并且使用一个常规 DataReader 来处理结果(即,使用“值”):
using(MapConnection con = new MapConnection(Settings.Default.AdventureWorks)) { con.Open(); MapCommand cmd = con.CreateCommand(); cmd.CommandText = "SELECT VALUE sp " + "FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " + "WHERE sp.HireDate > @date"; cmd.Parameters.AddWithValue("date", hireDate); DbDataReader r = cmd.ExecuteReader(); while(r.Read()) { Console.WriteLine("{0}\t{1}", r["FirstName"], r["LastName"]); } }
对于后期方案(如报告和业务智能)或要直接序列化结果(如来自 webservice 的结果)来说,这种方式比较理想;但是在需要编写繁重业务逻辑的情况下,通常最好是用对象表示业务实体。以上基于对象的代码版本使用下一版 ADO.NET,如下所示:
using(MapConnection con = new MapConnection(Settings.Default.AdventureWorks)) { con.Open(); ObjectContext ctx = new ObjectContext(con); Query<SalesPerson> newSalesPeople = ctx.GetQuery<SalesPerson>( "SELECT VALUE sp " + "FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " + "WHERE sp.HireDate > @date", new QueryParameter("@date", hireDate)); foreach(SalesPerson p in newSalesPeople) { Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName); } }
并非使用一个命令对象来表示查询,我们使用作为对象服务入口点的“对象上下文”和表示对象空间中查询的 Query 对象。请注意,我们仍然使用相同的映射连接(指向相同的 EDM 架构和映射)和相同的查询语言。唯一不同的是现在结果将作为对象返回。
示例中立刻出现了一个问题:类型“SalesPerson”从哪里来?ADO.NET 实体框架包含一个工具(假设是 EDM 架构)将生成 .NET 类,用于表示 .NET 环境中的 EDM 实体。生成的类是局部类,因此可以通过自定义业务逻辑将其扩展到单独文件中,而不会妨碍代码生成器。
如果该工具可以访问整个 EDM 架构,那么它不仅可以访问实体类型定义,而且可以访问实体集、关系等。基于该信息,此工具将不仅可以为每个实体类型生成类,而且若从 EDM 的角度看,还可以通过其实体集、关系等生成表示存储区的顶级类。这就进一步简化了使用对象的数据访问代码的编写。以上所示的同一示例,若使用该工具生成的强类型化版对象上下文,会变成:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) { Query<SalesPerson> newSalesPeople = aw.GetQuery<SalesPerson>( "SELECT VALUE sp " + "FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " + "WHERE sp.HireDate > @date", new QueryParameter("@date", hireDate)); foreach(SalesPerson p in newSalesPeople) { Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName); } }
可以看到,大部分探测代码已被消除,只留下了表示应用程序意图的代码。至此,原来提到的大部分阻抗失谐已被消除。这就是 ADO.NET 实体框架要实现的主要目标。
对象空间中的 EDM 概念
2.1 节讨论了多个 EDM 概念,如实体、关系和继承。通常,所有 EDM 概念是通过对象服务映射到 .NET 环境中的。
实体只映射到遵循特定合约的类。为简化问题,那些类会通过某个工具在 EDM 架构外自动生成。
关系将被作为属性呈现在对象空间中,可以直接导航。例如,AdventureWorks 的 EDM 模型具有 SalesPerson 和 SalesOrder 实体类型之间的关系。基于该信息,代码生成工具将创建合适成员,使以下代码将按预期正常运行:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) { Query<SalesPerson> newSalesPeople = aw.GetQuery<SalesPerson>( "SELECT VALUE sp " + "FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " + "WHERE sp.HireDate > @date", new QueryParameter("@date", hireDate)); foreach(SalesPerson p in newSalesPeople) { //对于各个与特定条件相匹配的销售人员, //处理其未决订单(假定未决状态为 0) if(NeedsOrderProcessing(p)) { //查询未决订单 foreach(SalesOrder o in p.Orders.Source.Where("it.Status == 0")) { //处理订单 ProcessOrder(o); } } } }
请注意,只要引用关系终端之一的该实体中的属性,就会发生关系导航。
另一种阐明如何将 EDM 元素呈现到对象服务层的有趣方式是继承。我们已经在使用的 EDM 架构具有 SalesOrders 实体集,包含 SalesOrder 和 StoreSalesOrder 两个类的实例;这是作为类层级中的继承直接在 .NET 环境中表示的。代码生成工具将生成一个继承自 SalesOrder 类的 StoreSalesOrder 类,而且实例也会被恰当地具体化。
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) { Query<SalesOrder> pendingOrders = aw.GetQuery<SalesOrder>( "SELECT VALUE " + "FROM AdventureWorks.AdventureWorksDB.SalesOrders AS o " + "WHERE o.Status = 0"); foreach(SalesOrder o in pendingOrders) { //使用运行时类型确定该订单的处理方式 if(o is StoreSalesOrder) { ValidateTaxByState((StoreSalesOrder)o); ProcessLocalOrder(o); } else { ProcessOnlineOrder(0); } } }
处理数据并保持更改
通常,当应用程序想要对数据库中的数据进行更改时,就必须发出针对该数据的 INSERT、UPDATE 和 DELETE 语句。实际上,应用程序趋向拥有几个“实体”(即使如果是非正式或隐式定义的),而且开发人员必须为系统中的每个“实体”编写(或用工具生成)SQL DML 语句。这是一项冗长乏味而且错误百出的任务,将生成一个日后需要大量维护工作的大型代码库。
ADO.NET 实体框架有关于系统中实体的充足元数据,可以将实体中的更改反向反映到数据库,不需要用户提供 SQL 语句来实现。例如:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) { //查找雇佣时间超过 5 年的所有员工 Query<SalesPerson> oldSalesPeople = aw.GetQuery<SalesPerson>( "SELECT VALUE sp " + "FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " + "WHERE sp.HireDate < @date", new QueryParameter("@date", DateTime.Today.AddYears(-5))); foreach(SalesPerson p in oldSalesPeople) { //通过 webservice 调用 HR 系统,以检查 //该销售人员是否将要晋升(注意 //该实体类型是 XML 可序列化的) if(HRWebService.ReadyForPromotion(p)) { p.Bonus += 10; //奖金上调 10% p.Title = "Senior Sales Representative"; //给予晋升 } } //将更改返回到数据库 aw.SaveChanges(); }
更新很简单,因为实体框架正在后台进行若干操作来简化开发人员的任务。特别地,该系统会:
-
持续跟踪通过查询分发的每个对象。对象上下文,或者是常规 ObjectContext 实例或类型化的实例,如这里的 AdventureWorksDB,将作为跟踪实例的逻辑范围。
-
保证值的原始版本用于具体化每个对象。这就允许系统在更新期间,对数据库执行主动的并发检查。
-
持续跟踪被更改的对象,所以当您调用 SaveChanges 时,ADO.NET 可以知道存储区中的哪些实体需要更新。
-
保存来自结果集的元数据信息,如描述这些实体来自哪个实体集以及其实体类型是什么等。所有这类信息将允许系统生成更新所需的语句,而不需要用户提供任何信息。
-
将对象级别的更新转换为概念和逻辑(关系)更新。
更新不仅会影响查询的直接结果,还会遍历表示实体间关系的集合。例如,添加新销售订单并使其与给定的销售人员相关联就是一种更改形式,而且只需添加一个新的 SalesOrder 对象到销售人员的 Orders 集合中,然后调用更新即可。
LINQ to Entities:语言集成查询
尽管在数据库和开发环境的集成上有了巨大的改进,但两者之间仍然存在阻抗失谐,仅靠增强用于数据编程的库和 API 无法轻易解决。实体框架几乎将逻辑行和对象间的阻抗失谐程度降到了最低,而实体框架通过与现有编程语言扩展的集成,可在语言内部自然表达查询,从而帮助完全消除了阻抗失谐。
更特别地,多数业务应用程序开发人员现在必须处理至少两种编程语言:一是用来对业务逻辑和表达层进行建模的语言,通常是高级的、面向对象的语言,如 C# 或 Visual Basic;一是用来与数据库交互的语言,典型的如一些 SQL 方言。
这不仅意味着开发人员必须掌握多种语言才能有效地进行应用程序开发,而且只要存在两个开发环境之间的转换,应用程序代码中就会引入裂缝。例如在大多数情况下,应用程序会应用诸如 ADO.NET 等数据访问 API,并会指定程序内部引号内的查询,以此执行对数据库的查询;因为该查询对编译器而言只是字符串文字,所以不会检查其语法是否正确或进行验证以确保它引用了诸如表和列名等现有元素。
解决该问题是下一批 Microsoft C# 和 Visual Basic 编程语言的关键主题之一。
语言集成查询
下一代 C# 和 Visual Basic 编程语言中包含了大量创新,主要围绕如何简化应用程序代码中的数据操作。LINQ 项目包括一系列到这些语言的扩展和支持库,使用户可以在编程语言内部表达查询,不必再使用其他语言(即将其作为字符串文字嵌入用户程序,导致在编译期间无法被理解或验证)。
使用 LINQ 表达的查询可作用于多种数据源,如内存中的数据结构、XML 文档,还可通过 ADO.NET 作用于数据库、实体模型和 DataSets。虽然其中一些在后台使用不同的实现方式,但它们都会呈现同样的语法和语言构造。
查询的实际语法细节是特定于每个编程语言的,而且它们跨 LINQ 数据源保持相同。例如,这里有一个 Visual Basic 查询,是对常规内存中的数列进行的:
Dim numbers() As Integer = {5, 7, 1, 4, 9, 3, 2, 6, 8} Dim smallnumbers = From n In numbers _ Where n <= 5 _ Select n _ Order By n For Each Dim n In smallnumbers Console.WriteLine(n) Next
这是相同查询的 C# 版:
int[] numbers = new int[] {5, 7, 1, 4, 9, 3, 2, 6, 8}; var smallnumbers = from n in numbers where n <= 5 orderby n select n; foreach(var n in smallnumbers) { Console.WriteLine(n); }
对于实体模型和 DataSets 等数据源的查询在语句构成上看起来是相同的,如下节所示。
有关 LINQ 项目的背景知识和详细内容,请参阅参考资料一节中的 [LINQ]。
LINQ 和 ADO.NET 实体框架
如 ADO.NET 实体框架对象服务一节中所述,未来版本的 ADO.NET 包含一个可以将数据库数据公开为 .NET 常规对象的层。此外,ADO.NET 工具将生成 .NET 类,用于代表 .NET 环境中的 EDM 架构。从而使对象层成为 LINQ 支持的理想目标,允许开发人员从用于建立业务逻辑的编程语言对数据库生成查询。该功能称作 LINQ to Entities。
例如,我们在本文开头讨论了可查询数据库对象的代码分段:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) { Query<SalesPerson> newSalesPeople = aw.GetQuery<SalesPerson>( "SELECT VALUE sp " + "FROM AdventureWorks.AdventureWorksDB.SalesPeople AS sp " + "WHERE sp.HireDate > @date", new QueryParameter("@date", hireDate)); foreach(SalesPerson p in newSalesPeople) { Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName); } }
使用由代码生成工具自动生成的类型,再加上 ADO.NET 中的 LINQ 支持,可以重新编写代码,如下所示:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) { var newSalesPeople = from p in aw.SalesPeople where p.HireDate > hireDate select p; foreach(SalesPerson p in newSalesPeople) { Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName); } }
或者使用 Visual Basic 语法:
Using aw As New AdventureWorksDB(Settings.Default.AdventureWorks) Dim newSalesPeople = From p In aw.SalesPeople _ Where p.HireDate > hireDate _ Select p For Each p As SalesPerson In newSalesPeople Console.WriteLine("{0} {1}", p.FirstName, p.LastName) Next End Using
使用 LINQ 编写的查询将由编译器进行处理,这意味着其余的应用程序代码将在编译时进行验证。编译器将捕获语法错误以及成员名称和数据类型的错误并在编译时报告,对于使用 SQL 和宿主编程语言的开发中常见的运行时错误,则不进行报告。
这些查询的结果仍然是代表 ADO.NET 实体的对象,因此您可以通过与使用实体 SQL 查询表达式时相同的方法来处理和更新它们。
本示例仅仅展示了一个非常简单的查询,真正的 LINQ 查询则非常丰富,可以包括排序、分组、连接、投影等。通过使用 C#/Visual Basic 正则表达式来生成各行,查询可以产生单一或形式复杂的结果。
例如,对上述代码进行修改,使结果先按雇佣日期排序,然后按姓名排序,这时仅输出部分成员,而不是全部的实体。代码如下所示:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) { var newSalesPeople = from p in aw.SalesPeople where p.HireDate > hireDate orderby p.HireDate, p.FirstName select new { Name = p.FirstName + " " + p.LastName, HireDate = p.HireDate }; foreach(SalesPerson p in newSalesPeople) { Console.WriteLine("{0}\t{1}", p.FirstName, p.LastName); } }
此外,关系不仅被呈现为可用于导航的属性(如上一节中所述),而且还可用于查询,以声明的方式执行复杂的数据提取和成形操作;例如,从所有新雇佣的员工(例如今年雇佣的员工)处获取全部订单,代码如下:
using(AdventureWorksDB aw = new AdventureWorksDB(Settings.Default.AdventureWorks)) { var newSalesPeople = from o in aw.SalesOrders where o.SalesPerson.HireDate >= new DateTime(2006, 1, 1) select o; //处理订单 // ... }
DataSet 最终获得全部查询功能:LINQ to DataSet
ADO.NET 编程模型的关键元素之一是通过 DataSet 以分离式和后端不可知的方式显式缓存数据。DataSet 代表一组表与关系,以及描述其中所含数据的结构和约束的相应元数据。ADO.NET 中含有各种不同的类,有助于将数据从数据库载入 DataSet 中,以及将 DataSet 中进行的更改应用到数据库中。
DataSet 值得关注的一点是,允许应用程序将数据库中所含信息的子集载入应用程序空间,然后在保留该子集相关形式的同时在内存中对其进行处理。这使得许多方案在数据表示和处理方面获得了很高的灵活性。特别地,一般的报告、分析和智能应用程序均支持该处理方法。
为了查询 DataSet 中的数据,DataSet API 中包含的一些方法(例如 DataTable.Select())用于以某种预定义的方式搜索数据。但是,对于针对 DataSet 对象的丰富查询而言,还没有一种通用机制可提供许多以数据为中心的应用程序所需的典型表示。
LINQ 提供了一次难得的机会,可以在 DataSet 的顶部引入丰富的查询功能,以与环境集成的方式进行操作。
ADO.NET Tech Preview 中包含对在正则和类型化 DataSet 中进行 LINQ 查询的全面支持。为了说明该功能,假设 DataSet 具有两张表“SalesOrderHeader”和“SalesOrderDetail”;以下代码描述了如何从所有在线公布的订单中获取订单标识和订单日期:
DataSet ds = new DataSet(); FillOrders(ds); //该方法可从数据库填充 DataSet DataTable orders = ds.Tables["SalesOrderHeader"]; var query = from o in orders.ToQueryable() where o.Field<bool>("OnlineOrderFlag") == true select new { SalesOrderID = o.Field<int>("SalesOrderID"), OrderDate = o.Field<DateTime>("OrderDate") }; foreach(var order in query) { Console.WriteLine("{0}\t{1:d}", order.SalesOrderID, order.OrderDate); }
以下是使用 Visual Basic 语法的相同代码:
Dim ds As New DataSet() FillOrders(ds) Dim orders As DataTable = ds.Tables("SalesOrderHeader") Dim query = From o In orders.ToQueryable() _ Where o!OnlineOrderFlag = True _ Select o!SalesOrderID, o!OrderDate For Each Dim o In query Console.WriteLine("{0} {1:d}", o.SalesOrderID, o.OrderDate) Next
值得注意的是,Visual Basic 支持后期绑定,使得针对未类型化的 DataSet 进行的查询比使用 C# 更易于读取。下面将讨论类型化的 DataSet 如何解决该问题,并很好的适用于这两种语言。
对 DataSet 的一个常见要求是支持 DataTable 间的连接,现在可以通过 LINQ 实现。以下是连接 SalesOrderHeader 和 SalesOrderDetail 表的示例:
DataSet ds = new DataSet(); FillOrders(ds); DataTable orders = ds.Tables["SalesOrderHeader"]; DataTable details = ds.Tables["SalesOrderDetail"]; var query = from o in orders.ToQueryable() join d in details.ToQueryable() on o.Field<int>("SalesOrderID") equals d.Field<int>("SalesOrderID") where o.Field<bool>("OnlineOrderFlag") == true select new { SalesOrderID = o.Field<int>("SalesOrderID"), OrderDate = o.Field<DateTime>("OrderDate"), ProductID = d.Field<int>("ProductID"), Quantity = d.Field<short>("OrderQty") }; foreach(var line in query) { Console.WriteLine("{0}\t{1:d}\t{2}\t{3}", line.SalesOrderID, line.OrderDate, line.ProductID, line.Quantity); }
上述 LINQ 和 DataSet 构成了一个非常强大的工具,尽管由于各种原因代码有点混乱:
-
列访问仍然以后期绑定的方式进行,因此列名均位于引号中(不仅打乱了代码,而且让编译器无法在编译时检查列名)。
-
默认情况下,DataSet 字段访问为非类型化(即对表 ["column"] 操作的结果属于类型对象);一种解决办法是使用转换,但无法防止出现空值(在 DataSet 中表示为 DBNull.Value)。为了让字段访问保持一致并恰当的处理空值,引入了新的运算符“Field”。因此,为了访问类型 DataTime 中名为“Date”的列,可以使用 table.Field<DateTime>("Date")。
如果设计应用程序时了解 DataSet 架构,当使用 LINQ 时,可以通过一些类型化的 DataSet 提供更好的体验。类型化 DataSet 中的表和列类型的每一列都有类型化成员,从而更易于访问;此外,DataSet 本身具有的属性也使得其中所包含的不同类型的表更易于访问。
如果要创建带有与上一示例所用表格相同的 DataSet,可以将第一个查询编写为:
OrdersDataSet dsOrders = new OrdersDataSet(); FillOrders(ds); //该方法可从数据库填充 DataSet var query = from o in dsOrders.SalesOrderHeader where o.OnlineOrderFlag == true select new { o.SalesOrderID, o.OrderDate }; foreach(var order in query) { Console.WriteLine("{0}\t{1:d}", order.SalesOrderID, order.OrderDate); }
如您所见,查询变得极为简单。该方法可以用于其他的示例:
OrdersDataSet ds = new OrdersDataSet(); FillOrders(ds); var query = from o in dsOrders.SalesOrderHeader join d in dsOrders.SalesOrderDetail on o.SalesOrderID equals d.SalesOrderID where o.OnlineOrderFlag == true select new { o.SalesOrderID, o.OrderDate, d.ProductID, Quantity = d.OrderQty }; foreach(var line in query) { Console.WriteLine("{0}\t{1:d}\t{2}\t{3}", line.SalesOrderID, line.OrderDate, line.ProductID, line.Quantity); }
除了支持相应的合约以与 LINQ 集成外,DataSet LINQ 实现还可以评估特定的查询并判断默认的执行行为(通过标准查询运算符实现完成)是否适合指定的 DataSet。如果 DataSet 中创建的索引可用于加速查询,将会在运行时调整查询策略,以尝试更快的处理查询。
LINQ to SQL
对于不需要映射到概念模型的开发人员而言,可使用 LINQ to SQL(以前称为 DLinq)直接在现有数据库架构上体验 LINQ 编程模型。
与 4.2 节中所述的 ADO.NET 实体上的 LINQ 支持类似,开发人员可使用 LINQ to SQL 生成表示数据的 .NET 类。所生成的类可直接映射到数据库表、视图、存储过程和用户定义的函数,而不是映射到概念数据模型。通过 LINQ to SQL,开发人员可以根据存储架构直接编写代码,所使用的 LINQ 编程模式与先前所述的内存中的集合、实体或 DataSet 以及其他数据源(例如 XML)的编程模式相同。
使用 LINQ to SQL 生成的类,可以根据 SQL Server Northwind 数据库在本地版本的 SQLExpress 中编写以下已经熟悉的代码。
string connectString = "AttachDBFileName='C:\\ProgramFiles\\LINQ Preview\\Data\\Northwnd.mdf';" + "Server='.\\SQLEXPRESS';Integrated Security=SSPI;enlist=false"; using(Northwind db = new Northwind(connectString)) { var customers = from c in db.Customers where c.City == "London" select c; foreach(Customer c in customers) { Console.WriteLine("{0}\t{1}", c.ContactName, c.CompanyName); } }
如您所见,它与根据概念视图在 AdventureWorks 数据库中编写的代码非常相似(4.2 节中所介绍的)。LINQ 的优点在于可以对任何类型的数据使用相同的简单编程模式。
参考资料
[NGEN] 下一代数据访问:使概念级别成为现实,2006 年 6 月
[EDM] Entity Data Model.ADO.NET Technical Preview(英文),2006 年 6 月
[LINQ] The LINQ Project—.NET Language Integrated Query(英文),2005 年 9 月
[DLINQ] DLinq Overview for C# Developers & DLinq overview for Visual Basic Developers(英文),2006 年 5 月
附录
以 XML 表示的 EDM 架构示例
<?xml version="1.0" encoding="utf-8" ?> <Schema Namespace="AdventureWorks" xmlns="urn:schemas -microsoft-com:windows:storage" > <EntityContainerType Name="AdventureWorksDB"> <Property Name="SalesOrders" Type="EntitySet(SalesOrder)" /> <Property Name="SalesPeople" Type="EntitySet(SalesPerson)" /> <Property Name="SalesPersonOrders" Type="RelationshipSet(SalesPerson_Order)"> <End Name="SalesPerson" Extent="SalesPeople"/> <End Name="Order" Extent="SalesOrders" /> </Property> </EntityContainerType> <!--销售订单类型层次结构--> <EntityType Name="SalesOrder" Key="ID"> <Property Name="ID" Type ="System.Int32" Nullable="false"/> <Property Name="OrderDate" Type ="System.DateTime" Nullable="true"/> <Property Name="Status" Type ="System.Byte" Nullable="true"/> <Property Name="AccountNumber" Type ="System.String" Nullable="true" Size="15"/> <Property Name="TotalDue" Type ="System.Decimal" Nullable="true"/> </EntityType> <EntityType Name="StoreSalesOrder" BaseType="SalesOrder" > <Property Name="Tax" Type ="System.Decimal" Nullable="true"/> </EntityType> <!--人员 EntityType --> <EntityType Name="SalesPerson" Key="ID"> <!-- SalesPerson 表中的属性--> <Property Name="ID" Type="System.Int32" Nullable="false"/> <Property Name="SalesQuota" Type="System.Decimal"/> <Property Name="Bonus" Type="System.Decimal"/> <Property Name="SalesYTD" Type="System.Decimal"/> <!-- Employee 表中的属性--> <Property Name="HireDate" Type="System.DateTime" Nullable="true"/> <Property Name="Title" Type="System.String" Nullable="true" Size="50"/> <!-- contact 表中的属性--> <PROPERTY NAME="FIRSTNAME" TYPE="SYSTEM.STRING" NULLABLE ="TRUE" SIZE="50"/> <Property Name="MiddleName" Type="System.String" Nullable ="true" Size="50"/> <Property Name="LastName" Type="System.String" Nullable ="true" Size="50"/> <Property Name="ContactInformation" Type="AdventureWorks.ContactInfo" Nullable="false"/> </EntityType> <ComplexType Name="ContactInfo"> <Property Name="EmailAddress" Type="System.String" Nullable="true" Size="50"/> <Property Name="Phone" Type="System.String" Nullable ="true" Size="25"/> </ComplexType> <Association Name="SalesPerson_Order"> <End Name="Order" Type="SalesOrder" Multiplicity="*" PluralName="Orders"/> <End Name="SalesPerson" Type="SalesPerson" Multiplicity="1" /> </Association> </Schema>
以 XML 表示的映射示例
<?xml version="1.0" encoding="utf-8" ?> <Mapping xmlns="urn:schemas-microsoft-com:windows:storage:mapping:CS" xmlns:cdm="urn:schemas-microsoft-com:windows:storage:mapping:CS" cdm:Space="C-S"> <EntityContainerMapping cdm:CdmEntityContainer= "AdventureWorks.AdventureWorksDB" cdm:StorageEntityContainer="AdventureWorksTarget.dbo"> <EntitySetMapping cdm:Name="SalesOrders"> <EntityTypeMapping cdm:TypeName="AdventureWorks.StoreSalesOrder"> <TableMappingFragment cdm:TableName="SalesOrder"> <EntityKey> <ScalarProperty cdm:Name="ID" cdm:ColumnName="SalesOrderID" /> </EntityKey> <ScalarProperty cdm:Name="OrderDate" cdm:ColumnName="OrderDate" /> <ScalarProperty cdm:Name="Status" cdm:ColumnName="Status" /> <ScalarProperty cdm:Name="AccountNumber" cdm:ColumnName="AccountNumber" /> <ScalarProperty cdm:Name="TotalDue" cdm:ColumnName="TotalDue" /> <ScalarProperty cdm:Name="Tax" cdm:ColumnName="TaxAmt" /> <Condition cdm:ColumnName="OnlineOrderFlag" cdm:Value="false" /> </TableMappingFragment> </EntityTypeMapping> <EntityTypeMapping cdm:TypeName="AdventureWorks.SalesOrder"> <TableMappingFragment cdm:TableName="SalesOrder"> <EntityKey> <ScalarProperty cdm:Name="ID" cdm:ColumnName="SalesOrderID" /> </EntityKey> <ScalarProperty cdm:Name="OrderDate" cdm:ColumnName="OrderDate" /> <ScalarProperty cdm:Name="Status" cdm:ColumnName="Status" /> <ScalarProperty cdm:Name="AccountNumber" cdm:ColumnName="AccountNumber" /> <ScalarProperty cdm:Name="TotalDue" cdm:ColumnName="TotalDue" /> <Condition cdm:ColumnName="OnlineOrderFlag" cdm:Value="true" /> </TableMappingFragment> </EntityTypeMapping> </EntitySetMapping> <EntitySetMapping cdm:Name="SalesPeople"> <EntityTypeMapping cdm:TypeName="AdventureWorks.SalesPerson"> <TableMappingFragment cdm:TableName="SalesPerson"> <EntityKey> <ScalarProperty cdm:Name="ID" cdm:ColumnName="SalesPersonID" /> </EntityKey> <ScalarProperty cdm:Name="SalesQuota" cdm:ColumnName="SalesQuota" /> <ScalarProperty cdm:Name="Bonus" cdm:ColumnName="Bonus" /> <ScalarProperty cdm:Name="SalesYTD" cdm:ColumnName="SalesYTD" /> </TableMappingFragment> <TableMappingFragment cdm:TableName="Employee"> <EntityKey> <ScalarProperty cdm:Name="ID" cdm:ColumnName="EmployeeID" /> </EntityKey> <ScalarProperty cdm:Name="HireDate" cdm:ColumnName="HireDate" /> <ScalarProperty cdm:Name="Title" cdm:ColumnName="Title" /> </TableMappingFragment> <TableMappingFragment cdm:TableName="Contact"> <EntityKey> <ScalarProperty cdm:Name="ID" cdm:ColumnName="ContactID" /> </EntityKey> <ScalarProperty cdm:Name="FirstName" cdm:ColumnName="FirstName" /> <ScalarProperty cdm:Name="MiddleName" cdm:ColumnName="MiddleName" /> <ScalarProperty cdm:Name="LastName" cdm:ColumnName="LastName" /> <ComplexProperty cdm:Name="ContactInformation" cdm:TypeName="AdventureWorks.ContactInfo"> <ScalarProperty cdm:Name="EmailAddress" cdm:ColumnName="EMailAddress"/> <ScalarProperty cdm:Name="Phone" cdm:ColumnName="Phone"/> </ComplexProperty> </TableMappingFragment> </EntityTypeMapping> </EntitySetMapping> <AssociationSetMapping cdm:Name="SalesPersonOrders" cdm:TypeName="AdventureWorks.SalesPerson_Order" cdm:TableName="SalesOrder"> <EndProperty cdm:Name="SalesPerson"> <ScalarProperty cdm:Name="ID" cdm:ColumnName="SalesPersonID"/> </EndProperty> <EndProperty cdm:Name="Order"> <ScalarProperty cdm:Name="ID" cdm:ColumnName="SalesOrderID" /> </EndProperty> </AssociationSetMapping> </EntityContainerMapping> </Mapping>