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

Наша компания поставляет набор различных приложений, которые управляют данными в базе данных. Каждое приложение имеет свою специфическую бизнес-логику, но все приложения имеют общее подмножество бизнес-правил. Общие вещи инкапсулированы в кучу устаревших 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». Но есть ли другие предостережения/преимущества/недостатки?


person Demetrio78    schedule 18.02.2013    source источник
comment
На данный момент проект прототипа застрял, и мой босс отправил меня обратно работать над устаревшими вещами: хранимыми процедурами T-SQL, собственными библиотеками C++, чрезвычайно тонкими слоями C#, основанными на RCW, и т. д. (я называю их устаревшими библиотеками, но им все время нужны новые функции и расширения, так что для управления они еще не легаси). Но рано или поздно мне снова придется столкнуться с Entity Framework. Так что да, вопрос пока открыт...   -  person Demetrio78    schedule 08.04.2013
comment
Как вы думаете, лучший подход в этом случае — код сначала? Разве первый модельный подход не подходит для вашего случая лучше? вы можете вызывать хранимые процедуры напрямую, вместо сохранения изменений. Наличие частичного класса для каждой сущности, которую можно сохранить с помощью метода Save(), который вызывает соответствующее хранилище.   -  person KinSlayerUY    schedule 08.04.2013
comment
@KinSlayerUY, может быть, ваша идея лучше, но, к сожалению, модель сначала не поддерживает автоматические миграции, очень полезную функцию, которую мы хотели бы использовать.   -  person Demetrio78    schedule 08.04.2013


Ответы (2)


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

Если изменение БД для нормализации меток не подходит, создайте представление и перенесите его в свою модель объекта для этой цели.

person A Aiston    schedule 08.04.2013
comment
Похоже, что ваша метка действует как объединяющий атрибут, конечно, так оно и есть, но я опубликовал слишком упрощенный пример, пытаясь сконцентрировать материал в 1 классе. Философия проблемы не сильно отличалась бы, например, от класс OrderGroup, собирающий объекты Order в список. чтобы вы могли более естественно получить доступ ко всем заказам с одним и тем же ярлыком, проблема заключается не в доступе ко всем заказам с одним и тем же ярлыком, проблема в том, что при изменении заказа изменение всех заказов в группе должно автоматически применяться в соответствии с определенной бизнес-логикой. - person Demetrio78; 11.04.2013
comment
эта логика принадлежит (...) классу более высокого уровня, который использует ваш класс Order (например,BusinessLogic.Order) Я начинаю думать, что совершенно не понимаю, как EF предполагается использовать. Может быть, мне не следует предоставлять классы EF (например, мой класс, производный от DbContext) НЕПОСРЕДСТВЕННО приложениям, не так ли? По поводу IQueryable ведутся бурные споры, такие как (mikehadlow.blogspot .de/2009/01/) или (weirdlover.com/2010/05/11/), а как насчет объектов DbContext? - person Demetrio78; 11.04.2013
comment
Я нашел это (stackoverflow.com/questions/11658060/), я прочитаю соответствующие ссылки... Кстати, большое спасибо за ваш ответ. - person Demetrio78; 11.04.2013
comment
Поскольку других комментариев или ответов нет, я приму этот. Спасибо! - person Demetrio78; 11.07.2013
comment
Извините, я так и не ответил вам. Я бы отказался от связывания вашего кода приложения более высокого уровня с вашей технологией сохранения данных. Если вы сделаете это и, скажем, обнаружится, что производительность EF не на высоте, у вас будет головная боль, связанная с заменой другой технологии персистентности. Домен здесь является королем, если вы определяете некоторые интерфейсы, которые должен будет реализовать ваш уровень сохраняемости данных, используете эти интерфейсы на уровне домена и внедряете реализующие классы, вы будете в хорошей форме в течение долгого времени. - person A Aiston; 11.07.2013

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

Интерфейс (простите за 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():

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