使用 Entity Framework 对企业应用程序进行建模






4.09/5 (17投票s)
一篇关于利用 Entity Framework 从单个表中创建一组继承的业务对象。编写代码时没有任何条件判断!
引言
企业应用程序的架构涉及 IT 专业人员、开发人员和数据库管理员之间的职责划分。尽管不同应用程序对信息的多种需求各不相同,但数据完整性是任何人都不会想妥协的。由于数据被许多应用程序共享,没有一个应用程序程序员可以随意根据单个应用程序的需求修改共享数据库架构;相反,他/她应该在现有架构之上编写数据访问层,以使应用程序需求与预先存在的数据库架构相匹配。
随着 Entity Framework (EF) 的推出,在任何数据库架构之上建模 OO 设计变得非常容易。我们将探讨一个非常有趣的示例,其中整个应用程序是使用 EF designer 直观地建模的。一组类(带有继承)由 EF designer 自动生成,无需编写一行 C#、VB 或 XML 代码。对于这项特定活动,不需要了解 XML 或 SQL -- 只需点击、选择并指定用户界面中与表列对应的对象属性就足够了。
类自动生成后;数据通过一个简单的单行 LINQ 语句获取;然后它会创建适当类型的对象(这些类的实例),对应于每一行;精确(仅加载与该特定类型相关的属性列),并且可以直接使用!
我们还将在这个应用程序的层与著名的 MVC 架构之间建立有趣的联系。
最后,为了利用继承,我们将看到如何仅用一个 display()
方法(每个具体类一个)足以呈现最准确的格式(当然,它会在呈现其自身类的特定数据之前利用基类中的相同方法)。
企业需求:这个假设练习的最终 目标 是满足 高层管理 对根据通货膨胀调整某些产品组价格的需求,通货膨胀是由 高昂的燃油价格 和随之而来的 运输成本 上升引起的;并且仍然将季节性商品进行 清仓甩卖 -- 夏天很快就要结束了!
我们将尝试
- 消除耦合(通过 EF)
- 使用继承/多态性减少重复
- 通过将相似的方法放在 C# 文件中的相邻位置来增加内聚性
尽管有五种不同的产品类型(四种具体,一种抽象),并且只需要显示/更新属于其中一种产品类型的项目;此代码中最有趣的部分是完全没有 C# 中的 if
、else
、switch
、case
语句;以及(手动编写的代码中)任何 LINQ/SQL where
子句。这样,我们就完全消除了代码重复,并实现了更高程度的内聚性。
背景
当我第一次听说 Entity Framework 时,我曾尝试在 Code Project 上查找一些示例,但找不到一个实现基于单个表的继承层次结构的。所以,我决定编写一个简单的 C# 解决方案,可以直接在 Visual Studio 2008 SP1 中使用。
在这个特定的示例中,我们有一个 SQL Server 表,其中包含几种按不同类型分类的产品(基于它们是季节性的还是已停产的)。以下四个“产品类型代码”被分配:00
表示常规产品;01
表示季节性;10
表示已停产;11
表示季节性但已停产。以下是“Product
”表
请注意,在实际应用程序中,每种产品类型(类别)可能都有成千上万条记录。但是,为了简单起见,本示例只选择了五条。尽管对于数据性能效率而言,两位字符编码系统非常棒;但如果我们直接在程序中使用这些隐晦的代码,那么很难/记不住哪个代码用于哪种类型。因此,我们决定使用更友好的名称而不是代码,即 RegularProduct
、DiscontinuedProduct
、SeasonalProduct
和 SeasonalDiscontinuedProduct
;而不是二进制编码的 00
、01
、10
和 11
(为了简洁起见,我们开始称它们为“entity
名称”或“class
名称”,具体取决于它们是来自 EF designer 还是 VB/C# 程序代码)。
我们决定使用 Entity Framework 来完成所有这些映射。这样,即使没有 VB、C#、XML 或 SQL 语言知识的任何人都可以维护和扩展映射。
我们有四种具体的产品类型,因此我们将创建对应于每种类型的一个实体名称。这样,在我们的应用程序代码中,我们将仅使用类名(例如,SeaonalProduct
)来提取对应于 ProductType
10
的记录。如何提取 Product
表中的所有记录?最好再创建一个不按任何四个条件过滤行的类。我们称之为 Product
。我们将 ProductID, Name
和 Price
映射到 Product
实体类;并将其他四个具体类从中派生出来。这样,我们就继承了所有派生类所需的(共同关心的)三个字段,而无需在所有四个类中不必要地复制它们。
某些列仅在特定产品类型的上下文中才有意义。例如,在分析 DiscontinuedProduct
时,我们想知道它们何时停产;同样,对于 SeasonalProduct
,我们应该记录我们计划在淡季给予多少折扣。因此,除了设置产品类型代码的“何时”条件外,我们还将这些附加列映射到我们实体类的属性。使用 EF designer,我们可以直观地完成所有映射,而无需编写任何一行 C#/VB/XML 代码。
实体“DiscontinueProduct
”映射到 Product
(表),何时 ProductType
= 01
。它还具有“DisconinuedDate
”属性,该属性映射到 DiscontinuedDate
列。
映射回顾
让我们回顾一下我们迄今为止所做的工作 -- 使用 VS 2008 EF designer,我们自动生成了 Objects,它们 Map 到 Relational 数据库表(ORM)。所有五个实体类都映射到单个表,并根据“When
”中指定的条件过滤行。这使我们不必编写 SQL where
子句,或在整个程序中散布 if
/switch
/case
语句(使之难以阅读/关联/维护)。
程序将以无需深入了解底层数据库架构的方式编写。事实上,编译后的程序完全与数据库、其架构、其位置、SQL 方言;甚至数据库品牌(除了 SQL Server,还可以是 Oracle、DB2 或任何已开发出 EF 提供程序的供应商)无关。
唯一提到数据库连接的地方是 App.Config 文件,无需重新编译源代码即可进行更改。数据库表与对象耦合(映射)的唯一地方是通过“Entity Designer”的用户界面。C# 代码不包含对常规 Connection
、Command
、DataSet
、DataReader
或任何其他 ADO.NET 类的引用。(要查看图 1,请双击“TablePerHierarchy.edmx”,然后通过逐个单击来查看每个实体的映射)。
使用 EF 生成的类层次结构
到目前为止,我们没有编写一行代码,EF 在后台自动创建了几个实体类。如果我们打开类图“EFHierarchy.cd”,我们会找到 EF 生成的类。
在 EF 自动生成类后,我们手动添加了一个 Display()
方法;以及所有五个类中的一个 ProductType
属性。为了利用多态性,我们在 Product
基类中将属性和方法都声明为 virtual
;并在所有四个派生类中重写它们。这个五个类的层次结构由其消费者通过 TablePerHierarchyContainer
访问。我们不应该称之为“Model”吗?
对“Model”进行操作
Operations.cs 包含具有两个基本操作的代码,用于处理 Model(MVC enthusiasts 喜欢用 Model、View 和 Controller 层来理解软件)。在此实现中,我们还将尝试从该架构中借鉴一些联系。它(Operations.cs 文件)用于通过添加两个操作(泛型方法)来扩展 EF 生成的部分类 TablePerHierarchyContainer
:Display<ProductType>()
和 ChangeByPercentagePriceOf<ProductType>()
,它们以 Product
作为参数。由于 Product
是该层次结构的基本类,我们可以将这 5 个类中的任何一个作为参数传递给这些方法,具体取决于我们是想对 Product
表的所有行进行操作,还是只对属于我们库存中特定产品类型(类别)的行进行操作。
public void Display<ProductType>() where ProductType : Product {
foreach (Product product in (
from product in Products.OfType<producttype />() select product)) {
//Display products using the appropriate Display methods of one of the
//four implementing classes
product.Display(); Console.WriteLine();
}
}
public void ChangeByPercentagePriceOf<ProductType>(
Decimal percent) where ProductType : Product {
foreach (Product product in (
from product in Products.OfType<producttype />() select product)) {
//Update price using the "Price" property implemented in base
//"Product" class
product.Price += product.Price * percent / 100;
}
SaveChanges(true);
}
在这两个操作中,只有一行 LINQ 语句会迭代整个产品集合。
- “
Display
”操作
此方法的第一行是 foreach
语句(带有嵌入的 LINQ 块)。它获取 Product
表的所有行,并将它们渲染为四个具体类之一的实例(使用基类引用)。这样,我们就可以在后续语句中调用多态 Display()
方法,以显示表行,由四个派生类之一处理,处理给定 Product
Type(作为此泛型方法的参数传递)的特定属性。
foreach (Product product in (
from product in Products.OfType<ProductType>() select product)) {
//Display products using the appropriate Display methods from one of the
//four implementing classes
product.Display();Console.WriteLine();
}
我们将在 View 部分分析 product.Dispaly()
方法。
- “按百分比更改,价格为”操作
它的第一行与上面 Display
操作中描述的第一行完全相同。第二行,它增加了 product
price
的百分比,一旦完成所有 product
的 price
更新;它就保存更改。
foreach (Product product in (
from product in Products.OfType<ProductType>() select product)) {
//Update price using the "Price" property implemented in base "Product" class
product.Price += product.Price * percent / 100;
}
SaveChanges(true);
渲染“View”
DisplayFormatting.cs 包含用于显示模型数据的部分类。它们实现了我们 EF 生成的 Model 的一个非常简单的View。它们只是使用 Console.Write
语句转储它们的字段。例如,基类显示 Product
ID 以及所有五个类中通用的三个其他属性
public virtual void Display() { Console.Write("{0}:{1}: {2}\t {3:$###.00}",
ProductID, ProductType, Name, Price); }
季节性产品首先调用基类来显示常规产品字段,之后,它还写入了淡季折扣。
public override void Display() { base.Display(); Console.Write(
"; Discount {0:##.00}%", OffSeasonDiscount); }
我认为我们不需要对其他具体类做进一步的解释,因为它们只是一行方法,目的相同。唯一特殊之处在于它们被封装在各自的产品类型类中,因此它们包含对仅在其自身范围内可用的字段的引用。
通过“Controller”实现业务应用程序
整个“业务应用程序”都写在一个 MainProgram.cs 文件中。Main
程序是一系列单行调用“Model
”的操作(我们只有显示和价格更改)。如果你真的想将此应用程序的某一部分指定为 MVC 的“Controller
”,那就是这个序列。可执行文件调用“Model
”上的操作,传递不同的产品类型。我添加了很多注释来使其目的清晰。
我唯一想强调的是,这可以由一个仅了解企业业务需求以及如何激活 Model
的两个操作的开发团队来编写。他们只需传递一种 Product
类型即可从 Product
表中获取正确的行集,并可选地传递一个百分比数字来调整成本。我已剥离所有花哨的东西,使其即使对初学者来说也非常容易理解。
//CODED BY APPLICATION PROGRAMMERS
//Auditors want the entire inventory displayed, with all possible
//specifics -- irrespective of items' product classinfaction
Console.WriteLine("****** FIRST DISPLAY THE ENTIRE PRODUCT IN INVENTORY ***");
ef.Display<Product>();
//Finance Department is worried about increasing fuel cost.
//In an email, they asked us to raise regular item price by 2% to cover the
//transportation expenses
Console.WriteLine(
"\n\n****** INFLATION! -- INCREASE PRICE OF THE REGULAR PRODUCTS BY 2%***");
//Change price of "Regular Products" by 2 percent
ef.ChangeByPercentagePriceOf<RegularProduct>(2);
Console.WriteLine("\n\n****** NEW PRICE OF THE REGULAR PRODUCTS After 2% INCREASE***");
ef.Display<RegularProduct>();
//Marketing Department is worried about low sales. They want to sell everything which
//would die in next 15 days.
Console.WriteLine(
"\n\n****** CLEARANCE SALE! -- REDUCE PRICE OF THE SEASONAL PRODUCTS BY ANOTHER 40%***");
//Change price of "Regular Products" by -40 percent
ef.ChangeByPercentagePriceOf<SeasonalProduct>(-40);
Console.WriteLine("\n\n****** NEW PRICE OF THE SEASONAL PRODUCTS After 40% DISCOUNT***");
ef.Display<SeasonalProduct>();
程序输出
以下是程序输出。您可能想将其与 Product
表内容(Background 部分的第一个插图)进行比较。
****** FIRST DISPLAY THE ENTIRE PRODUCT IN INVENTORY ***
1:RegularProduct: Nail Box $5.09
2:DiscontinuedProduct: Bubble level $5.99; Since 02/02/07
3:SeasonalProduct: Flowers $9.07; Discount 40.00%
4:SeasonalDiscontinuedProduct: Roses $12.00; Discount 50.00%, Since 04/02/07
5:RegularProduct: Nuts and Bolts $3.18
****** INFLATION! -- INCREASE PRICE OF THE REGULAR PRODUCTS BY 2%***
****** NEW PRICE OF THE REGULAR PRODUCTS After 2% INCREASE***
1:RegularProduct: Nail Box $5.19
5:RegularProduct: Nuts and Bolts $3.24
****** CLEARANCE SALE! -- REDUCE PRICE OF THE SEASONAL PRODUCTS BY ANOTHER 40%**
****** NEW PRICE OF THE SEASONAL PRODUCTS After 40% DISCOUNT***
3:SeasonalProduct: Flowers $5.44; Discount 40.00%
Using the Code
- 首先,需要创建包含
Product
表的数据库EFHierarchy
。要做到这一点,在 SQL Server 2005 Management Studio 中,打开文件(File->Open->File),选择 CreateEFHierarchyDB.sql,然后按 Open 按钮。
如果您想在其他位置创建数据库,请编辑默认的 "C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\DATA" 路径。按执行按钮。 - 刷新视图以看到
EFHierarchy
数据库后,打开空的Product
表。现在打开 Excel 表格 EFHierarchy.xls,选择其第 2 到第 6 行,然后复制并粘贴到空白的Product
表中。确保按执行按钮将数据填充到表中。 - 将 BusinessApplication.zip 解压到您想要提取项目文件的位置。
- 升级您的 Visual Studio 2008 至 SP1 Visual Studio 2008 Service Pack 1 (SP1) 和 .NET Framework 3.5 SP1 下载现已可从 MSDN 下载。
- 打开 BusinessApplication.sln(File-> Open -> Project/Solution),生成并运行。
- 每次运行项目时,常规产品(项目 #1 和 #5)将变得越来越昂贵(每次上涨 2%);而季节性产品将贬值 40%。
看点/鸣谢
虽然我们从单个表开始,但通过 Visual Studio 友好的 EF designer 应用面向对象原则,使得能够以最少的手写代码编写一个相当复杂的应用程序。将应用程序分成三个源文件/两个程序集是为了使它们能够由不同技能集/职责的团队拥有/签入/打包。本文中的示例基于 Erick Thompson 的文章,ADO.NET 团队博客。架构理念深受 Martin Fowler 和 Robert C. Martin 教诲的影响。
我最近发现了一个很棒的高级建模工具。非常感谢微软的Diego Vega,他在回答我在 ADO.NET Entity Framework and LINQ to Entities (Pre-release) forum 中的问题时,向我推荐了这个非常有用的资源。我建议读者下载简单但令人兴奋的 Entity Framework Mapping Helper v1.0。