使用适配器模式处理 Entity Framework Database First 存储过程结果






4.60/5 (6投票s)
在本文中,我们将展示一种使用适配器模式将 Entity Framework 生成的存储过程代码与上层代码分离的方法
引言
在本文中,我们将展示一种方法,通过使用 Adapter Pattern 来将 Entity Framework 为存储过程生成的代码与上层代码分离。这将产生更结构化、易于维护的代码。
背景
自 Visual Studio 2008/.NET Framework 3.5 起,微软就引入了一种新的数据访问技术:Entity Framework (EF)。
虽然 EF 是一个对象关系映射工具,与 Java 中的 Hibernate 和 JPA 类似,但对于 .Net 开发者来说,它实际上是 DataSet 的一种替代方案,而 DataSet 是随 .Net 平台一同诞生的。
当我们谈论 EF 时,通常会强调它将数据库的关系模型转换为面向对象模型的能力,这是现代编程语言的通用语言。然而,EF 也支持使用数据库存储过程,这对于考虑到当前商业数据库管理系统中存储过程的流行度来说是必须的。最近,我发现自己需要升级一个旧的 Web 应用程序,该应用程序通过 DataSet 来实现数据库访问,而 DataSet 是从存储过程返回的结果集中构建的。DataSet 也广泛支持直接访问数据库表,但这对客户来说是禁忌。他只允许通过存储过程访问后端数据库。我个人认为这并非坏事,特别是如果客户的所有业务规则都已在后端存储过程中实现。
需要替换的 Web 应用程序使用 DataSet 作为业务层(实现为数据库中的存储过程)和表示层之间的数据传输对象。我一直认为 DataSet 的这种用法效率低下。DataSet 中包含了大量用于直接访问数据库表的功能,这使得它们在我看来“过于沉重”,不适合用作数据传输对象。因此,对于新的 Web 应用程序,我决定用轻量级的数据传输对象替换 DataSet。顺便也给新的 Entity Framework 一个表现的机会。
需要替换的 Web 应用程序使用 DataSet 作为业务层(实现为数据库中的存储过程)和表示层之间的数据传输对象。我一直认为 DataSet 的这种用法效率低下。DataSet 中包含了大量用于直接访问数据库表的功能,这使得它们在我看来“过于沉重”,不适合用作数据传输对象。因此,对于新的 Web 应用程序,我决定用轻量级的数据传输对象替换 DataSet。顺便也给新的 Entity Framework 一个表现的机会。
查看 EF 生成的代码
我喜欢 EF 的第一点是它能够自动生成 C# 和存储过程之间的粘合代码。不再需要敲击键盘输入大量的 SqlCommand / SqlParameter,并将 Sql 类型适配到 C# 类型。只需选择要调用的存储过程,代码就会自动生成。存储过程调用被封装起来,你可以像调用 C# 函数一样调用存储过程。参数直接映射为可空类型。结果以某种复杂类型的集合形式返回……你猜对了:这个复杂类型是从结果集元数据派生的。这里不讨论存储过程返回多个结果集时会发生什么。只说至少在 EF 5 中,它不支持自动处理。要知道 EF 通过调用存储过程,并将所有参数设置为 null,然后设置 Set FMTONLY On 来提取结果集元数据。你可以利用这一点来调试 EF 代码生成可能出现的问题。
好了,最后你得到了宝贵的数据库数据,以 C# 对象集合的形式……没那么简单。生成的集合只能遍历一次。例如,如果你先调用集合的 Count 属性,那么你就无法检索实际数据,因为 Count 属性的内部实现已经遍历了集合……所以,将返回的对象移到一个标准的、广泛使用的 List<> 集合中是很有意义的。另一个问题 arises from the dependencies the EF code generator injects into your code. The generated objects code is under the control of the generator. You should not modify this because this code will be deleted the next time EF decides to refresh the code. Then not only your higher level code result dependent on lower level objects violating the dependency inversion principle but furthermore you have no control at all over this lower level code.(生成的对象代码受生成器控制。你不应该修改它,因为下次 EF 决定刷新代码时,这些代码将被删除。这样一来,你的上层代码不仅会因为依赖底层对象而违反依赖倒置原则,而且你对这些底层代码也完全没有控制权。)
依赖倒置原则提倡通过让高层和低层都依赖于抽象来实现高层与低层之间的解耦。为了让我们的代码回归良好的设计实践,我建议为包含存储过程结果集的数据传输对象提供接口,并使用 Adapter Pattern 将 EF 生成的数据类型转换为我们的接口。通过这种方式,我们可以在 EF 和上层之间创建一个薄层,将它们与 EF 特定代码解耦。
动手实践代码
让我们开始创建 Sql 数据库存储过程。我们将使用此处可用的 Northwind 数据库。为了方便起见,我们将添加以下基本存储过程:
CREATE PROCEDURE CustomersByCountry
@sCountry Varchar(max)
AS
BEGIN
Select customers.*
From Customers customers
Where Country = @sCountry
END
GO
CREATE PROCEDURE CustomersWhoBoughtProduct
@nProductId Int
AS
BEGIN
Select Distinct customers.*
From [Order Details] orderDetails
Inner Join Orders orders
On orders.OrderId = orderDetails.OrderId
Inner Join Customers customers
On customers.CustomerId = orders.CustomerId
Where orderDetails.ProductId = @nProductId
END
GO
注意,这两个存储过程都返回来自 Customers 表的类似数据集。
现在让我们来处理 C# 代码。我使用的是 Visual Studio 2012。
- 创建一个控制台应用程序项目。
- 使用“管理 NuGet 程序包”安装 Entity Framework。在我的例子中,它下载了 EF 版本 6.1。由于 EF 6 未随 VS2012 预装,我需要下载并安装 EF6 工具for VS2012。
- 添加数据库连接(localhost 服务器,Northwind 数据库)
- 通过点击“项目”->“添加新项”->“ADO.NET Entity Data Model”(命名为 MyModel)->“空的 EF Designer 模型”来添加 Entity Framework。
- 在生成的 MyModel.edmx 文件上右键单击,选择“从数据库更新模型”。
- 添加存储过程和函数
- 勾选 `CustomersByCountry`、`CustomerWhoBoughtProduct`。
- 保存 MyModel.edmx 文件,然后再查看生成的代码。
- 在解决方案资源管理器中选择 *MyModel.edmx* 文件。单击左侧面板的任意位置。右侧面板(就在解决方案资源管理器选项卡左侧)应该会出现一个模型浏览器选项卡。
- 选择模型浏览器。生成的模型将显示出来。
- 在 MyModel 下,查看“复杂类型”:注意从选定的存储过程的输出结果集中生成了两个类型:`CustomersByCountry_Result` 和 `CustomerWhoBoughtProduct_Result`。
- 现在选择解决方案资源管理器。搜索 `CustomersByCountry_Result` 和 `CustomerWhoBoughtProduct_Result`。这将指向生成的代码。
(注意,在查看生成的代码之前,您应该始终保存对 .edmx 文件的任何更改。代码生成器仅在该文件保存到磁盘时调用。)
检查代码,不出所料,EF 生成了 `CustomersByCountry` 和 `CustomerWhoBoughtProduct` 函数,它们是各自存储过程的 C# 镜像。生成的 `CustomersByCountry_Result` 和 `CustomerWhoBoughtProduct_Result` 数据类型是 Customers 数据库元数据的 C# 翻译。请注意,EF 缺乏将 `CustomersByCountry` 和 `CustomerWhoBoughtProduct` 合并到同一类型 Customers 中的智能。EF 不这样做是好的。否则,如果有人更改了其中一个存储过程的结果集……那可能会破坏我们所有的代码。这也是抽象生成类级的另一个原因。另外,请注意使用 `ObjectResult` 集合作为结果容器。这就是上面提到的只能遍历一次的集合。此外,`ObjectResult` 本身也是一个类,这有点出乎我的意料。我更喜欢广泛使用的 List<> 容器。
那么,让我们概述一下我们将要做什么
- 创建一个 Customer 接口来抽象 `CustomersByCountry_Result` 和 `CustomerWhoBoughtProduct_Result`。
- 创建适配器,将 `CustomersByCountry_Result` 和 `CustomerWhoBoughtProduct_Result` 对象转换为新接口。
- 封装 EF 创建的 `CustomersByCountry` 和 `CustomerWhoBoughtProduct` 函数,使其返回 `List
` 而不是 `ObjectResult<>`。
让我们创建一个名为 dao(数据访问对象)的文件夹,将我们的代码与应用程序的其余部分分开。
首先,我们将声明一个接口来抽象 EF 生成的复杂类型。我将其命名为 `ICustomer`。它的成员将与 `CustomersByCountry_Result` 相同。只需应用复制粘贴编程,并将生成的类转换为具有只读属性的接口。我们将遵循从数据库提取的数据是只读的理念。如果你想修改数据,请调用另一个存储过程来执行创建、更新和删除操作。这样,我们将使我们的数据传输对象不可变。处理不可变对象有很多优点,所以让我们通过使用它们来改进我们的代码。
然后,我们将复制 EF 生成的代码
public partial class CustomersByCountry_Result
{
public string CustomerID { get; set; }
public string CompanyName { get; set; }
...
}
public interface ICustomer
{
string CustomerID { get; }
string CompanyName { get; }
...
}
接下来,实现适配器。请注意,我们需要两个适配器来从 `CustomersByCountry_Result` 和 `CustomerWhoBoughtProduct_Result` 转换为我们的 `ICustomer` 接口。将适配器命名为 `CustomersByCountry_Result_Adapter` 和 `CustomerWhoBoughtProduct_Result_Adapter`。
public class CustomersByCountry_Result_Adapter : ICustomer
{
private CustomersByCountry_Result adaptee = null;
public CustomersByCountry_Result_Adapter(CustomersByCountry_Result adaptee)
{
this.adaptee = adaptee;
}
public string CustomerID
{
get { return adaptee.CustomerID; }
}
public string CompanyName
{
get { return adaptee.CompanyName; }
}
...
}
`CustomerWhoBoughtProduct_Result_Adapter` 的代码类似。
为了最终与 EF 解耦,我们将 EF 导入的函数封装在我们的数据访问类中。
public static class CustomerDao
{
public static List<ICustomer> CustomersByCountry(string country)
{
List<ICustomer> result = new List<ICustomer>();
using (MyModelContainer ctx = new MyModelContainer())
{
var entityList = ctx.CustomersByCountry(country).ToList();
foreach (var entity in entityList)
{
result.Add(new CustomersByCountry_Result_Adapter(entity));
}
}
return result;
}
public static List<ICustomer> CustomersWhoBoughtProduct(int productId)
{
List<ICustomer> result = new List<ICustomer>();
using (MyModelContainer ctx = new MyModelContainer())
{
var entityList = ctx.CustomersWhoBoughtProduct(productId).ToList();
foreach (var entity in entityList)
{
result.Add(new CustomersWhoBoughtProduct_Result_Adapter(entity));
}
}
return result;
}
}
(所有这些复制粘贴的代码都急需一个宏来自动化这个过程!)
现在,让我们编写一些示例代码,演示如何使用我们的数据访问代码。
class Program
{
static void Main(string[] args)
{
var customers = CustomerDao.CustomersByCountry("USA");
foreach (var customer in customers)
Console.WriteLine(customer.ContactName);
var moreCustomers = CustomerDao.CustomersWhoBoughtProduct(1);
foreach (var otherCustomer in moreCustomers)
Console.WriteLine(otherCustomer.ContactName);
Console.ReadLine();
}
}
瞧!请注意,我们应用程序的上层将只了解 `ICustomer` 和 `List
让我知道你对这种方法的看法。
结论
在本文中,我们通过遵循现代设计实践并使用 Adapter Pattern,将 Entity Framework 的存储过程调用生成代码与应用程序的上层进行了解耦。
参考文献
- Head First Design Patterns。Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra (O'Reilly Media 2004)
- Effective Java (2nd Edition) by Joshua Bloch, (Addison-Wesley, 2008)