使用 Entity Framework 进行业务逻辑






4.42/5 (17投票s)
如何在 Entity Framework 中进行基本逻辑,当相关实体或关联发生更改时。

开始阅读之前
我假设您已经具备 Entity Framework 的基本知识和 C# 3.x 的良好编码技能,以便理解我的代码并使用它。如果不是,请查阅其他关于 Entity Framework 的文章或文档。
介绍
在本文中,我将演示如何使用 Entity Framework 在相关实体或“关联”发生更改时执行一些基本逻辑。为了简单起见,我将以订单的详细信息被更改、添加或删除时更新订单的总金额为例进行说明。
背景
嗯,有些开发者喜欢固守自己已知的方法来解决所有问题,正如老话所说:“手里拿着锤子,看什么都像钉子”,但我喜欢探索新事物并了解其中的区别。
首先,我想给您介绍解决此问题的一些可能方案及其挑战。
1. 使用子表上的数据库触发器更新父表。
挑战
- 您将在用户更新数据时在 UI 上进行计算,并在提交更新或更改回数据库时再次进行计算,这是重复工作。
- 除非您使用的是具有某种通知机制的数据库引擎,否则您的应用程序不会收到数据库更改的通知。
- 您可能不了解太多 SQL 或特定供应商的语言或扩展来执行此类触发器,而您拥有的只是您的 IDE 和编程语言。
2. 使用数据库视图在需要显示给用户时执行计算。
挑战
- 您将把大部分业务逻辑放在数据库中,并需要一个优秀的数据库管理员来维护您的数据库性能和代码。
- 计算仅在用户请求查看数据时发生,这在需要对计算逻辑的更改维护某些逻辑的系统中可能会有问题。
- 同样,如果您不了解太多 SQL 或者想坚持使用您的 IDE,这不是最好的选择,除非您愿意学习如何正确管理数据库。
3. 使用本文提供的解决方案,将您的业务逻辑附加到您的实体。
挑战
- 您需要对 ORM 技术有基本了解,并具备大量的 OOP 知识。
- 您的逻辑现在被困在您的客户端应用程序或数据访问代码中,这违背了 N 层架构。
- 还有其他您认为有问题的地方吗?{ 由您自由想象 }
解决方案
好的,现在我们对什么是好什么是坏有了一个概念,那么让我们继续开始学习我们的解决方案。为了演示我们的解决方案,我创建了一个包含两个表的小型数据库,如以下图所示:

因此,我们有 Orders
和 OrderItems
,我们想要实现的是,当订单的 OrderItems
通过添加、删除或更新而更改时,我们希望更新父 Order
的 TotalAmount
,以反映其子 OrderItems
的 (Quantity
x Price
) 的总和。
为了增加难度,我们希望计算包含我们内存或缓存中的所有 OrderItems
,以及在更新发生时我们不在内存中的任何其他数据库 OrderItem
。
让我们开始在 Visual Studio 中创建我们的解决方案,在这种情况下,您必须使用安装了 SP1 的 VS 2008,否则我将不解释任何替代方案。
我创建了一个名为“EFLogicDemo
”的新 Windows Forms 应用程序,并向其中添加了一个新的 ADO.NET Entity Data Model,并将其命名为“OrdersModel
”。

一个向导会询问您是否要从数据库生成模型,这在我们的情况下是可以的,因为我们不打算做任何高级或花哨的事情,所以只需将向导指向我们之前创建的数据库,让它完成工作,并且不要忘记让向导将我们的连接字符串保存在 App.Config
中。
作为一项调整,EF 不像旧的 LINQ to SQL 那样区分复数名称,所以我更改了为每个生成的实体生成的“Entity Set Name”和“Name”的默认名称,使其如下所示:
SalesOrders
- Entity Set Name =
SalesOrders
- Name =
SalesOrder
OrderItems
- Entity Set Name =
OrderItems
- Name =
OrderItem
完成后,我们的实体模型看起来是这样的:

好的,现在我们已经准备好发挥我们的编码技巧了,对吧?我们可以开始向我们的项目添加一个新类,并将其命名为我们想要实现其逻辑的父实体的确切名称,在我们的例子中是 SalesOrder
,如下所示:

并确保将其访问修饰符更改为 public partial
,以便扩展 EF 为我们的实体生成的现有类。
Using the Code
现在我们需要一个起点,在这种情况下,我们想监听我们相关的子实体上的 AssociationChanged
事件,而执行此操作的最佳位置是类的构造函数,因为它确保了我们的事件监听器将在我们应用程序内创建的任何实例上被调用,以下是执行此操作的代码:
public SalesOrder()
{
this.OrderItems.AssociationChanged +=
new CollectionChangeEventHandler(OrderItems_AssociationChanged);
}
到目前为止一切顺利,我们有一个名为 OrderItems_AssociationChanged
的方法,当与 SalesOrder
的当前实例相关的 OrderItem
发生任何更改时,该方法将被调用。此方法的类型包含一个类型为 System.ComponentModel.CollectionChangeEventArgs
的参数,它为我们提供了 Action
和 Element
这两个属性,告诉我们发生了什么以及在何处发生。
现在在我们的方法中,我们得到了需要响应的更改通知。所以,我将要做的是,如果可能的话,从数据库中急切地加载所有详细信息,然后对其进行计算,并通过新计算出的 TotalAmount
更新我们的 SalesOrder
。为了做到这一点,我创建了一个新方法来执行计算,并称之为 UpdateTotalAmount
,以下是代码:
private void UpdateTotalAmount()
{
// load all our OrderItems from the database if and ONLY if this SalesOrder
// was saved in the database, otherwise the load will give an exception
if (this.EntityState == System.Data.EntityState.Unchanged ||
this.EntityState == System.Data.EntityState.Modified)
{
// while loading back database changes, we want to preserve any modification\
// that was made to our local instances
this.OrderItems.Load(System.Data.Objects.MergeOption.PreserveChanges);
}
// calculate the new TotalAmount if there is any OrderItems found in the collection
var newAmount = OrderItems == null ? 0 : OrderItems.Sum(
d => d.Quantity * d.UnitPrice);
// apply the changes and update the current instance with the new value
this.TotalAmount = newAmount - (this.Discount ?? 0);
}
嗯,代码非常简单且带有注释,确实不需要更多的解释。
现在我们只需要在我们的事件监听器 OrderItems_AssociationChanged
中调用这个方法,但这里还有一件事在我们继续之前,如果 OrderItem
的属性发生了更改怎么办?会发生什么?
嗯,我们只监控关系或关联的更改,如果子实体本身的属性值发生了更改,我们将永远不会收到通知,除非我们监听它的更改,这里是使用 INotifyPropertyChanged
这个神圣接口的方法。我们所要做的就是为我们集合中的每个项目附加另一个监听器到 PropertyChanged
事件,我们就会收到关于子元素任何更改的通知,甚至我们可以进一步在子元素类本身执行一些逻辑,这应该更容易,但我选择在一个地方处理所有事情,这是我们 AssociationChanged
的最终代码:
private void OrderItems_AssociationChanged(object sender,
System.ComponentModel.CollectionChangeEventArgs e)
{
// if a new SalesItem is being added to this SalesOrder
if (e.Action == CollectionChangeAction.Add)
{
var detail = (OrderItem)e.Element;
// attache some code when this SalesItem has its properties changed
// the following code uses Lambda expression and anonymous method syntax
detail.PropertyChanged += (s, a) =>
{
// now we are here invoked from the SalesItem that was changed and
// we have 2 parameters we can use
// (s) which is the instance of SalesItem that was changes
// (a) which is the arguments that will tell us what exactly did change
// now we only want to re-calculate if the Quantity or Unit price changed
if (a.PropertyName == "Quantity" || a.PropertyName == "UnitPrice")
{
// do the re-calculation
UpdateTotalAmount();
}
};
}
// we are called here by AssociationChanged and this means
// we need to recalculate too.
UpdateTotalAmount();
}
测试解决方案
现在我能听到有人在谈论它是否有效,我回答:**单元测试** ;) 因为它速度快,它容易,并且在开始使用你的代码时不需要任何设计上的努力。我开发了这个单元测试来测试我们实体可能发生的绝大多数情况,例如添加、删除和编辑/更新子实体……所以让我们在这里试试吧。
[TestMethod()]
public void SalesOrderLogicTest()
{
string connection =
"metadata=res://*/OrdersModel.csdl|res://*/OrdersModel.ssdl|res://*/" +
"OrdersModel.msl;provider=System.Data.SqlClient;"
+ "provider connection string='Data Source=.\\SQLExpress;" +
"Initial Catalog=EFLogicDemo;Integrated Security=True;" +
"MultipleActiveResultSets=True'";
using (EFLogicDemoEntities db = new EFLogicDemoEntities(connection))
{
SalesOrder order = new SalesOrder();
order.CustomerName = "Me";
order.OrderDatetime = DateTime.Now;
order.Discount = 2;
db.AddToSalesOrders(order);
OrderItem item1 = new OrderItem();
item1.ProductName = "an Apple";
item1.Quantity = 5;
item1.UnitPrice = 2;
order.OrderItems.Add(item1);
Assert.IsTrue(order.TotalAmount == 8);
OrderItem item2 = new OrderItem();
item2.ProductName = "an Orange";
item2.Quantity = 1;
item2.UnitPrice = 10;
order.OrderItems.Add(item2);
Assert.IsTrue(order.TotalAmount == 18);
db.DeleteObject(item1);
Assert.IsTrue(order.TotalAmount == 8);
item2.Quantity = 3;
Assert.IsTrue(order.TotalAmount == 28);
}
}
那个测试完美奏效,现在你可以随意添加、删除或更改你实体属性和关系中的任何内容,并放心,你的计算和逻辑将始终在后台运行。
最后,既然您喜欢这篇文章并获得了一些免费的代码和知识,请为文章投票,并分享您的意见和建议,以鼓励我继续前进并思考添加更多文章。
关注点
Entity Framework 并非一项新技术,它和 ORM 一样古老,但 ADO.NET 团队正将其作为下一代数据访问技术给予更多关注,所以密切关注这个产品将有助于您快速了解数据库应用程序的未来。
历史
- 2009 年 5 月 26 日,星期二 - 首次发布