我们公司提供了一套处理数据库中数据的各种应用程序。每个应用程序都有其特定的业务逻辑,但所有应用程序共享一个公共的业务规则子集。常见的东西被封装在一堆用 C++ 编写的遗留 COM DLL 中,它们使用“经典 ADO”(它们通常调用存储过程,有时它们使用动态 SQL)。这些 DLL 中的大多数都具有基于 XML 的方法(更不用说基于专有格式的方法!)来创建、编辑、删除和检索对象,以及额外的操作,例如快速复制和转换许多实体的方法。
中间件 DLL 现在已经很老了,我们的应用程序开发人员想要一个新的面向对象(不是面向 xml)的中间件,它可以很容易地被 C# 应用程序使用。公司里的很多人说我们应该忘记旧的范式,转向新的酷东西,比如实体框架。他们对 POCO 的简单性很感兴趣,并希望使用 LINQ 来检索数据(DLL 的基于 Xml 的查询方法并不那么容易使用,并且永远不会像 LINQ 那样灵活)。
所以我试图为一个简化的场景创建一个模型(真实的场景要复杂得多,在这里我将只发布简化场景的一个简化子集!)。我正在使用 Visual Studio 2010、Entity Framework 5 Code First、SQL Server 2008 R2。如果我犯了愚蠢的错误,请原谅,我是实体框架的新手。由于我有很多不同的疑问,我将它们发布在单独的线程中。这是第一个。旧版 XML 方法具有如下签名:
bool Edit(string xmlstring, out string errorMessage)
使用这样的格式:
<ORDER>
<ID>234</ID>
<NAME>SuperFastCar</NAME>
<QUANTITY>3</QUANTITY>
<LABEL>abc</LABEL>
</ORDER>
Edit 方法实现了以下业务逻辑:当 Quantity 发生变化时,必须对所有具有相同 Label 的 Orders 应用“自动缩放”。例如,有三个订单:OrderA 的数量 = 3,标签 = X。OrderB 的数量 = 4,标签 = X。OrderC 的数量 = 5,标签 = Y。我调用 Edit 方法,为 OrderA 提供一个新的数量 = 6,即,我将 OrderA 的数量增加一倍。那么,按照业务逻辑,OrderB的数量一定是自动加倍的,一定是8个,因为OrderB和OrderA的label是一样的。不得更改 OrderC,因为它具有不同的标签。
如何使用 POCO 类和实体框架复制它?这是一个问题,因为旧的 Edit 方法一次只能更改一个订单,而 Entity Framework 在调用 SaveChanges 时可以更改很多订单。此外,对 SaveChanges 的一次调用也可以创建新订单。临时假设,仅用于本次测试: 1) 如果多个 Order Quantities 同时更改,且缩放因子对于所有它们都不相同,则不会发生缩放;2) 新添加的订单即使与缩放订单的标签相同,也不会自动缩放。
我试图通过覆盖 SaveChanges 来实现它。
POCO 类:
using System;
namespace MockOrders
{
public class Order
{
public Int64 Id { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public decimal Quantity { get; set; }
}
}
迁移文件(创建索引):
namespace MockOrders.Migrations
{
using System;
using System.Data.Entity.Migrations;
public partial class UniqueIndexes : DbMigration
{
public override void Up()
{
CreateIndex("dbo.Orders", "Name", true /* unique */, "myIndex1_Order_Name_Unique");
CreateIndex("dbo.Orders", "Label", false /* NOT unique */, "myIndex2_Order_Label");
}
public override void Down()
{
DropIndex("dbo.Orders", "myIndex2_Order_Label");
DropIndex("dbo.Orders", "myIndex1_Order_Name_Unique");
}
}
}
数据库上下文:
using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
namespace MockOrders
{
public class MyContext : DbContext
{
public MyContext() : base(GenerateConnection())
{
}
private static string GenerateConnection()
{
var sqlBuilder = new System.Data.SqlClient.SqlConnectionStringBuilder();
sqlBuilder.DataSource = @"localhost\aaaaaa";
sqlBuilder.InitialCatalog = "aaaaaa";
sqlBuilder.UserID = "aaaaa";
sqlBuilder.Password = "aaaaaaaaa!";
return sqlBuilder.ToString();
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new OrderConfig());
}
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
var groupByLabel = from changedEntity in ChangeTracker.Entries<Order>()
where changedEntity.State == System.Data.EntityState.Modified
&& changedEntity.Property(o => o.Quantity).IsModified
&& changedEntity.Property(o => o.Quantity).OriginalValue != 0
&& !String.IsNullOrEmpty(changedEntity.Property(o => o.Label).CurrentValue)
group changedEntity by changedEntity.Property(o => o.Label).CurrentValue into x
select new { Label = x.Key, List = x};
foreach (var labeledGroup in groupByLabel)
{
var withScalingFactor = from changedEntity in labeledGroup.List
select new
{
ChangedEntity = changedEntity,
ScalingFactor = changedEntity.Property(o => o.Quantity).CurrentValue / changedEntity.Property(o => o.Quantity).OriginalValue
};
var groupByScalingFactor = from t in withScalingFactor
group t by t.ScalingFactor into g select g;
// if there are too many scaling factors for this label, skip automatic scaling
if (groupByScalingFactor.Count() == 1)
{
decimal scalingFactor = groupByScalingFactor.First().Key;
if (scalingFactor != 1)
{
var query = from oo in this.AllTheOrders where oo.Label == labeledGroup.Label select oo;
foreach (Order ord in query)
{
if (this.Entry(ord).State != System.Data.EntityState.Modified
&& this.Entry(ord).State != System.Data.EntityState.Added)
{
ord.Quantity = ord.Quantity * scalingFactor;
}
}
}
}
}
return base.SaveChanges();
}
public DbSet<Order> AllTheOrders { get; set; }
}
class OrderConfig : EntityTypeConfiguration<Order>
{
public OrderConfig()
{
Property(o => o.Name).HasMaxLength(200).IsRequired();
Property(o => o.Label).HasMaxLength(400);
}
}
}
它似乎有效(当然排除错误),但这是一个只有 1 个类的示例:一个真正的生产应用程序可能有数百个类!恐怕在现实场景中,由于有很多约束和业务逻辑,SaveChanges 的覆盖可能很快变得冗长、混乱且容易出错。一些同事也关心性能。在我们遗留的 DLL 中,许多业务逻辑(例如“自动”操作)存在于存储过程中,一些同事担心基于 SaveChanges 的方法可能会引入过多的往返并影响性能。在 SaveChanges 的覆盖中,我们还可以调用存储过程,但是事务完整性呢?如果我在调用“base.SaveChanges()”之前对数据库进行了更改,并且“base.SaveChanges()”失败了怎么办?
有不同的方法吗?我错过了什么吗?
非常感谢你!
德米特里奥
ps 顺便问一下,覆盖 SaveChanges 和注册到“SavingChanges”事件之间有区别吗?我阅读了这份文件,但它没有解释是否有区别: http: //msdn.microsoft.com/en-us/library/cc716714 (v=vs.100).aspx
这篇文章: 实体框架 SaveChanges - 自定义行为?
说“当覆盖 SaveChanges 时,您可以在调用 base.SaveChanges 之前和之后放置自定义逻辑”。但是还有其他警告/优点/缺点吗?