10

Nasza firma dostarcza zestaw różnych aplikacji, które manipulują danymi w bazie danych. Każda aplikacja ma określoną logikę biznesową, ale wszystkie aplikacje mają wspólny podzbiór reguł biznesowych. Najczęściej spotykane są w starych bibliotekach DLL COM, napisanych w C++, które używają "klasycznego ADO" (zwykle nazywają je procedurami przechowywanymi, czasami używają dynamicznego SQL). Większość z tych bibliotek DLL ma metody oparte na XML (nie wspominając o metodach opartych na formacie prawnie zastrzeżonym!) Do tworzenia, edycji, usuwania i pobierania obiektów, a także dodatkowe działania, takie jak metody, które szybko kopiują i przekształcają wiele jednostek.Zastępowanie zmian SaveChanges w Entity Framework 5 Code Najpierw replikować zachowanie starej starszej biblioteki

DLL oprogramowania warstwy pośredniej są już bardzo stare, a nasi programiści aplikacji chcą nowego oprogramowania zorientowanego obiektowo (nie w języku xml), które może być z łatwością używane przez aplikacje C#. Wiele osób w firmie mówi, że powinniśmy zapomnieć o starych paradygmatach i przejść do nowych, fajnych rzeczy, takich jak Entity Framework. Intryguje ich prostota POCO i chcieliby używać LINQ do pobierania danych (Metody zapytań oparte na Xml bibliotek DLL nie są tak łatwe w użyciu i nigdy nie będą tak elastyczne jak LINQ).

Więc próbuję stworzyć makietę dla uproszczonego scenariusza (rzeczywisty scenariusz jest znacznie bardziej złożony, a tutaj zamieszczę tylko uproszczony podzestaw uproszczonego scenariusza!). Używam Visual Studio 2010, Entity Framework 5 Code First, SQL Server 2008 R2. Prosimy o litość, jeśli popełnię głupie błędy, jestem nowy w Entity Framework. Ponieważ mam wiele różnych wątpliwości, opublikuję je w osobnych wątkach. To jest pierwszy. Metody Legacy XML mieć podpis takiego:

bool Edit(string xmlstring, out string errorMessage) 

w formacie jak poniżej:

<ORDER> 
    <ID>234</ID> 
    <NAME>SuperFastCar</NAME> 
    <QUANTITY>3</QUANTITY> 
    <LABEL>abc</LABEL> 
</ORDER> 

Edit sposób realizowane następującą logikę biznesową: kiedy ilość zmienia się się, że „automatyczne skalowanie” Must stosować się do wszystkich zamówień, które mają tę samą etykietę. E.g. są trzy rozkazy: OrderA ma ilość = 3, label = X. OrderB ma ilość = 4, label = X. OrderC ma ilość = 5, label = Y. Zadzwonię do metody Edit, dostarczając nową ilość = 6 dla OrderA, tj. podwojenie ilości OrderA. Następnie, zgodnie z logiką biznesową, ilość zamówienia B musi być automatycznie podwojona i musi wynosić 8, ponieważ OrderB i OrderA mają tę samą etykietę. OrderC nie może zostać zmieniony, ponieważ ma inną etykietę.

Jak mogę skopiować to za pomocą klas POCO i Entity Framework? Jest to problem, ponieważ stara metoda edycji może zmienić tylko jedno zamówienie naraz, podczas gdy Entity Framework może zmienić wiele zamówień, gdy zostanie wywołana funkcja SaveChanges. Ponadto pojedyncze połączenie z SaveChanges może również tworzyć nowe zamówienia. Tymczasowe założenia, tylko dla tego testu: 1) jeśli wiele Ilości Zamówień zmienia się w tym samym czasie, a współczynnik skalowania nie jest taki sam dla wszystkich z nich, NIE występuje skalowanie; 2) nowo dodane zamówienia nie są automatycznie skalowane, nawet jeśli mają taką samą etykietę skalowanego zamówienia.

Próbowałem go zaimplementować, zastępując SaveChanges.

POCO klasy:

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

plik migracji (do tworzenia indeksów):

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

wydaje się działać (wyjąwszy błędy oczywiście), ale był to przykład z tylko jedną klasą: prawdziwa aplikacja do produkcji może mieć setki klas! Obawiam się, że w prawdziwym scenariuszu, z wieloma ograniczeniami i logiką biznesową, nadpisanie SaveChanges może szybko stać się długie, zagracone i podatne na błędy. Niektórzy koledzy martwią się także o wydajność. W naszych starszych bibliotekach DLL wiele logiki biznesowej (takich jak "automatyczne" działania) żyje w procedurach przechowywanych, niektórzy koledzy obawiają się, że podejście oparte na SaveChanges może wprowadzić zbyt wiele podróży w obie strony i utrudnić działanie. W override SaveChanges moglibyśmy również wywołać procedury składowane, ale co z integralnością transakcyjną? Co się stanie, jeśli wprowadzę zmiany do bazy danych , zanim zadzwonię do "base.SaveChanges()", a "base.SaveChanges()" nie powiedzie się?

Czy istnieje inne podejście? Czy czegoś brakuje?

Dziękuję bardzo!

Demetrio

p.s. Przy okazji, czy istnieje różnica między nadpisaniem SaveChanges i rejestracją w zdarzeniu "SavingChanges"? Czytałem ten dokument, ale nie wyjaśnia, czy istnieje różnica: http://msdn.microsoft.com/en-us/library/cc716714(v=vs.100).aspx

tego posta: Entity Framework SaveChanges - Customize Behavior?

mówi, że „gdy nadrzędne SaveChanges można umieścić niestandardową logikę przed i po wywołaniu base.SaveChanges”. Ale czy istnieją inne zastrzeżenia/zalety/wady?

+0

nadal do tego? – NSGaga

+0

W tej chwili prototypowy projekt utknął, a mój szef wysłał mnie z powrotem do pracy nad "starszymi" rzeczami: procedury przechowywane T-SQL, natywne biblioteki DLL C++, wyjątkowo cienkie warstwy C# oparte na RCW i tak dalej (nazywam je biblioteki "starsze", ale wymagają one ciągle nowych funkcji i rozszerzeń, więc w przypadku zarządzania nie są jeszcze "starsze"). Ale wcześniej czy później znowu będę musiał stawić czoła Entity Framework. Więc tak, pytanie nadal jest otwarte ... – Demetrio78

+0

myślisz, że kod pierwszy jest najlepszym podejściem w tym przypadku? Którego modelu nie da się lepiej dopasować do twojego przypadku? można wywołać procedury składowane bezpośrednio, zamiast korzystać z zapisanych zmian. Posiadanie klasy częściowej dla każdej jednostki, którą można zapisać przy użyciu metody Save(), która wywołuje odpowiedni magazyn. – KinSlayerUY

Odpowiedz

1

Powiedziałbym, że ta logika należy albo do twojej klasy MockOrders.Order, do klasy wyższej warstwy, która używa twojej klasy Order (np. BusinessLogic.Order) lub klasy Label. Wygląda na to, że twoja etykieta działa jak atrybut łączący, więc bez znajomości szczegółów, powiedziałbym, wyciągnąć go i uczynić z niego samodzielną jednostkę, to zapewni ci właściwości nawigacji, dzięki czemu będziesz mógł w naturalny sposób uzyskiwać dostęp do wszystkich zamówień z tą samą etykietą .

Jeśli modyfikowanie DB w celu znormalizowania etykiet nie jest celem, zbuduj widok i przenieś go do swojego modelu jednostki w tym celu.

+0

_ "Wygląda na to, że twoja etykieta działa jak atrybut łączący" _ oczywiście, ale napisałem uproszczony przykład próbujący skoncentrować rzeczy na 1 klasie. Filozofia problemu nie różniłaby się tak bardzo od np. Klasa OrderGroup zbierająca obiekty zamówienia na liście. _ "dzięki czemu można w naturalny sposób uzyskać dostęp do wszystkich zamówień z tą samą etykietą" _ problemem nie jest uzyskiwanie dostępu do wszystkich zamówień z tą samą etykietą, problem polega na tym, że w przypadku zmiany zamówienia zmiana wszystkich zamówień w grupie musi być automatycznie stosowana zgodnie z określoną logiką biznesową. – Demetrio78

+0

_ "ta logika należy (...) do klasy z wyższej warstwy, która wykorzystuje twoją klasę Order (np. BusinessLogic.Order)" _ Zaczynam myśleć, że zupełnie brakuje mi sensu, w jaki sposób EF ma do wykorzystania. Może nie powinienem wystawiać klas EF (takich jak moja klasa pochodna DbContext) BEZPOŚREDNIO do aplikacji, czy powinienem? W IQueryable prowadzone są intensywne debaty, takie jak (http://mikehadlow.blogspot.de/2009/01/should-my-repository-expose-iqueryable.html) lub (http://www.weirdlover.com/2010/05/11/Iqueryable-can-kill-your-dog-steal-your-wife-kill-your-will-to-live-etc /), ale co z obiektami DbContext? – Demetrio78

+0

Znalazłem to (http://stackoverflow.com/questions/11658060/should-dbcontext-and-dbset-be-exposed-for--pure-poco-application), przeczytam powiązane linki ... BTW , Bardzo dziękuję za twoją odpowiedź. – Demetrio78

1

Musiałem zrobić coś podobnego, ale stworzyłem interfejs IPrepForSave i zaimplementowałem ten interfejs dla wszystkich podmiotów, które muszą wykonać pewną logikę biznesową, zanim zostaną zapisane.

interfejs (przeproszeniem VB.NET)

Public Interface IPrepForSave 
    Sub PrepForSave() 
End Interface 

dbContext.SaveChanges przesłonić:

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 

A potem mogę zachować logikę biznesową w samą jednostkę, w realizowanej PrepForSave() metody:

Partial Public Class MyEntity 
    Implements IPrepForSave 

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

End Class 

pamiętać, że miejsce pewne ograniczenia co można zrobić w PrepForSave() metoda:

  1. Wszelkie zmiany podmiotu nie może sprawić, że logika walidacja podmiot nie, bo to będzie się nazywać po logika walidacja jest tzw.
  2. Dostęp do baz danych powinien być ograniczony do minimum i powinien być tylko do odczytu.
  3. Wszystkie podmioty, które nie muszą logiki biznesowej przed zapisaniem, nie powinny implementować tego interfejsu.
Powiązane problemy