Наша компания поставляет набор различных приложений, которые управляют данными в базе данных. Каждое приложение имеет свою специфическую бизнес-логику, но все приложения имеют общее подмножество бизнес-правил. Общие вещи инкапсулированы в кучу устаревших COM-библиотек DLL, написанных на C++, которые используют «классический ADO» (обычно они вызывают хранимые процедуры, иногда используют динамический SQL). Большинство этих библиотек DLL имеют методы на основе XML (не говоря уже о методах на основе проприетарного формата!) для создания, редактирования, удаления и извлечения объектов, а также дополнительные действия, такие как методы, которые быстро копируют и преобразовывают множество сущностей.
Библиотеки промежуточного программного обеспечения сейчас очень старые, нашим разработчикам приложений требуется новое объектно-ориентированное (не ориентированное на xml) промежуточное программное обеспечение, которое можно легко использовать в приложениях C#. Многие в компании говорят, что мы должны забыть старые парадигмы и перейти на новые классные вещи, такие как Entity Framework. Их заинтриговала простота POCO, и они хотели бы использовать LINQ для извлечения данных (методы запросов DLL на основе Xml не так просты в использовании и никогда не будут такими гибкими, как LINQ).
Итак, я пытаюсь создать макет для упрощенного сценария (реальный сценарий намного сложнее, и здесь я опубликую только упрощенную часть упрощенного сценария!). Я использую Visual Studio 2010, Entity Framework 5 Code First, SQL Server 2008 R2. Пожалуйста, смилуйтесь, если я делаю глупые ошибки, я новичок в Entity Framework. Поскольку у меня много разных сомнений, я опубликую их в отдельных темах. Это первый. Устаревшие XML-методы имеют такую сигнатуру:
bool Edit(string xmlstring, out string errorMessage)
С таким форматом:
<ORDER>
<ID>234</ID>
<NAME>SuperFastCar</NAME>
<QUANTITY>3</QUANTITY>
<LABEL>abc</LABEL>
</ORDER>
В методе Edit реализована следующая бизнес-логика: при изменении количества «автоматическое масштабирование» должно применяться ко всем заказам, имеющим одинаковую метку. Например. есть три заказа: OrderA имеет количество = 3, метка = X. OrderB имеет количество = 4, метка = X. OrderC имеет количество = 5, метка = Y. Я вызываю метод Edit, предоставляя новое количество = 6 для OrderA, т.е. Я удваиваю количество OrderA. Затем, согласно бизнес-логике, количество OrderB должно автоматически удвоиться и стать равным 8, потому что OrderB и OrderA имеют одинаковую метку. OrderC нельзя изменять, поскольку он имеет другую метку.
Как я могу воспроизвести это с помощью классов POCO и Entity Framework? Это проблема, потому что старый метод Edit может изменять только один заказ за раз, в то время как Entity Framework может изменять множество заказов при вызове SaveChanges. Кроме того, один вызов SaveChanges также может создавать новые заказы. Временные допущения, только для этого теста: 1) если многие Объемы Заказов изменяются одновременно, и коэффициент масштабирования не одинаков для всех из них, НЕ происходит масштабирования; 2) вновь добавленные Заказы не масштабируются автоматически, даже если они имеют ту же метку масштабируемого заказа.
Я попытался реализовать это, переопределив SaveChanges.
ПОКО класс:
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()» завершится ошибкой?
Есть ли другой подход? Я что-то пропустил?
Большое спасибо!
Деметрио
p.s. Кстати, есть ли разница между переопределением SaveChanges и регистрацией на событие "SavingChanges"? Я прочитал этот документ, но в нем не объясняется, есть ли разница: http://msdn.microsoft.com/en-us/library/cc716714(v=vs.100).aspx
Этот пост: Entity Framework SaveChanges — настроить поведение?
говорит, что «при переопределении SaveChanges вы можете поместить пользовательскую логику до и ПОСЛЕ вызова base.SaveChanges». Но есть ли другие предостережения/преимущества/недостатки?