Переопределение SaveChanges в Entity Framework 5 Код Сначала для репликации поведения старой библиотеки устаревших

Наша компания предоставляет набор различных приложений, которые манипулируют данными в базе данных. Каждое приложение имеет свою специфическую бизнес-логику, но все приложения имеют общий поднабор бизнес-правил. Обычные вещи инкапсулируются в кучу устаревших COM-библиотек DLL, написанных на С++, которые используют "классический ADO" (обычно они вызывают хранимые процедуры, иногда они используют динамический SQL). Большинство из этих DLL имеют XML-методы (не говоря уже о методах, основанных на собственном формате!) Для создания, редактирования, удаления и извлечения объектов, а также дополнительных действий, таких как методы, которые быстро копируют и преобразуют многие объекты.

Библиотеки промежуточного ПО теперь очень старые, нашим разработчикам приложений требуется новое объектно-ориентированное (а не xml-ориентированное) промежуточное программное обеспечение, которое может быть легко использовано приложениями С#. Многие люди в компании говорят, что мы должны забыть старые парадигмы и перейти к новой классной вещи, такой Entity Framework. Они заинтригованы простотой POCOs, и они хотели бы использовать LINQ для извлечения данных (методы запросов на основе Xml для DLL не так просты в использовании и никогда не будут такими гибкими, как 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>

В методе "Редактирование" реализована следующая бизнес-логика: при изменении количества "автоматическое масштабирование" должно применяться ко всем ордерам, которые имеют одну и ту же метку. Например. существует три порядка: OrderA имеет величину = 3, label = X. OrderB имеет величину = 4, метку = X. OrderC имеет величину = 5, label = Y. Я вызываю метод Edit, который снабжает новую величину = 6 для OrderA, то есть я удваиваю количество OrderA. Затем, согласно бизнес-логике, количество OrderB должно быть автоматически удвоено и должно стать 8, потому что OrderB и OrderA имеют одну и ту же метку. OrderC не может быть изменен, поскольку он имеет другую метку.

Как я могу воспроизвести это с помощью классов POCO и Entity Framework? Это проблема, потому что старый метод Edit может изменять только один порядок за раз, в то время как Entity Framework может изменять множество ордеров при вызове SaveChanges. Кроме того, один вызов SaveChanges может также создавать новые Заказы. Временные предположения, только для этого теста: 1) если одновременно изменяется количество заказов, и коэффициент масштабирования для всех из них не одинаковый, происходит масштабирование НЕТ; 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");
        }
    }
}

DbContext:

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);
        }
    }
}

Кажется, что работает (конечно, запрет ошибок), но это был пример всего с одним классом: у реального производственного приложения могут быть сотни классов! Я боюсь, что в реальном сценарии, с множеством ограничений и бизнес-логики, переопределение 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". Но существуют ли другие оговорки/преимущества/недостатки?

Ответ 1

Я бы сказал, что эта логика принадлежит либо в вашем классе MockOrders.Order, либо в классе с более высокого уровня, который использует ваш класс Order (например BusinessLogic.Order) или в классе Label. Похоже, что ваш ярлык действует как атрибут соединения, поэтому, не зная подробностей, я бы сказал, вытащить его и сделать его самостоятельным, это даст вам свойства навигации, чтобы вы могли более естественно получить доступ ко всем ордерам с одинаковой меткой,

Если модификация БД для нормализации Ярлыки не являются посетителями, создайте представление и принесите это в свою модель сущности для этой цели.

Ответ 2

Мне нужно было сделать что-то подобное, но я создал интерфейс IPrepForSave и реализовал этот интерфейс для любых объектов, которым необходимо выполнить некоторую бизнес-логику, прежде чем они будут сохранены.

Интерфейс (pardon the VB.NET):

Public Interface IPrepForSave
    Sub PrepForSave()
End Interface

Отмена dbContext.SaveChanges:

Public Overloads Overrides Function SaveChanges() As Integer
    ChangeTracker.DetectChanges()

    '** Any entities that implement IPrepForSave should have their PrepForSave method called before saving.
    Dim changedEntitiesToPrep = From br In ChangeTracker.Entries(Of IPrepForSave)()
                                Where br.State = EntityState.Added OrElse br.State = EntityState.Modified
                                Select br.Entity

    For Each br In changedEntitiesToPrep
        br.PrepForSave()
    Next

    Return MyBase.SaveChanges()
End Function

И затем я могу сохранить бизнес-логику в самом Entity, в реализованном методе PrepForSave():

Partial Public Class MyEntity
    Implements IPrepForSave

    Public Sub PrepForSave() Implements IPrepForSave.PrepForSave
        'Do Stuff Here...
    End Sub

End Class

Обратите внимание, что я устанавливаю некоторые ограничения на то, что можно сделать в методе PrepForSave():

  • Любые изменения в сущности не могут заставить логику проверки сущности терпеть неудачу, потому что она будет вызываться после вызова логики проверки.
  • Доступ к базе данных должен быть сведен к минимуму и должен быть доступен только для чтения.
  • Любые объекты, которые не должны выполнять бизнес-логику до сохранения, не должны реализовывать этот интерфейс.