10

La nostra azienda fornisce una suite di varie applicazioni che manipolano i dati in un database. Ogni applicazione ha una logica aziendale specifica, ma tutte le applicazioni condividono un sottoinsieme comune di regole aziendali. Le cose comuni sono incapsulate in un gruppo di DLL COM legacy, scritte in C++, che usano "ADO classico" (di solito chiamano stored procedure, a volte usano SQL dinamico). La maggior parte di queste DLL ha metodi basati su XML (per non parlare dei metodi basati su formati proprietari!) Per creare, modificare, eliminare e recuperare oggetti, e anche azioni extra come metodi che copiano e trasformano rapidamente molte entità.Sovrascrittura di SaveChanges nel codice Entity Framework 5 Prima di replicare il comportamento di una vecchia libreria legacy

Le DLL middleware sono ormai molto vecchie, i nostri sviluppatori di applicazioni desiderano un nuovo middleware orientato agli oggetti (non orientato al xml) che possa essere facilmente utilizzato dalle applicazioni C#. Molte persone nell'azienda dicono che dovremmo dimenticare i vecchi paradigmi e passare a nuove cose interessanti come Entity Framework. Sono affascinati dalla semplicità dei POCO e vorrebbero utilizzare LINQ per recuperare i dati (i metodi di query basati su Xml delle DLL non sono così facili da usare e non saranno mai flessibili come LINQ).

Quindi sto provando a creare un modello per uno scenario semplificato (lo scenario reale è molto più complesso, e qui posterò solo un sottoinsieme semplificato dello scenario semplificato!). Sto utilizzando Visual Studio 2010, Entity Framework 5 Code First, SQL Server 2008 R2. Per favore abbi pietà se faccio degli errori stupidi, sono una novità di Entity Framework. Dato che ho molti dubbi diversi, li posterò in thread separati. Questo è il primo. metodi Legacy XML hanno una firma come questo:

bool Edit(string xmlstring, out string errorMessage) 

Con un formato come questo:

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

Il metodo Edit implementato la seguente logica di business: quando una quantità viene modificato, un must "ridimensionamento automatico" essere applicato a tutti gli ordini che hanno la stessa etichetta. E.g. ci sono tre ordini: OrderA ha quantity = 3, label = X. OrderB ha quantity = 4, label = X. OrderC ha quantity = 5, label = Y. Io chiamo il metodo Edit che fornisce una nuova quantità = 6 per OrderA, cioè sto raddoppiando la quantità di OrderA. Quindi, secondo la logica aziendale, la quantità di OrderB deve essere automaticamente raddoppiata e deve diventare 8, poiché OrderB e OrderA hanno la stessa etichetta. OrderC non deve essere modificato perché ha un'etichetta diversa.

Come posso replicarlo con le classi POCO e Entity Framework? È un problema perché il vecchio metodo Modifica può cambiare solo un ordine alla volta, mentre il Entity Framework può cambiare molti ordini quando viene chiamato SaveChanges. Inoltre, una singola chiamata a SaveChanges può anche creare nuovi ordini. Presupposti temporanei, solo per questo test: 1) se molte quantità di ordine sono cambiate contemporaneamente, e il fattore di scala non è lo stesso per tutti, NO si verifica il ridimensionamento; 2) Gli ordini appena aggiunti non vengono ridimensionati automaticamente anche se hanno la stessa etichetta di un ordine ridimensionato.

Ho provato a implementarlo ignorando SaveChanges.Classe

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

file di migrazione (per creare gli indici):

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

sembra funzionare (salvo errori ovviamente), ma questo era un esempio di una sola classe: una vera applicazione di produzione può avere centinaia di classi! Temo che in uno scenario reale, con molti vincoli e logica aziendale, l'override di SaveChanges potrebbe rapidamente diventare lungo, ingombrante e soggetto a errori. Alcuni colleghi sono anche preoccupati per le prestazioni. Nelle nostre legacy DLL, molta logica di business (come le azioni "automatiche") vive in stored procedure, alcuni colleghi temono che l'approccio basato su SaveChanges possa introdurre troppi round-trip e ostacolare le prestazioni. Nell'override di SaveChanges possiamo anche invocare stored procedure, ma per quanto riguarda l'integrità transazionale? Cosa succede se apporto modifiche al database prima di chiamare "base.SaveChanges()" e "base.SaveChanges()" non riesce?

C'è un approccio diverso? Mi sto perdendo qualcosa?

Grazie mille!

Demetrio

p.s. A proposito, c'è una differenza tra l'override di SaveChanges e la registrazione all'evento "SavingChanges"? Ho letto questo documento, ma non spiega se c'è una differenza: http://msdn.microsoft.com/en-us/library/cc716714(v=vs.100).aspx

Questo post: Entity Framework SaveChanges - Customize Behavior?

dice che "quando si esegue l'override SaveChanges si può mettere la logica personalizzata prima e dopo aver chiamato base.SaveChanges". Ma ci sono altri avvertimenti/vantaggi/inconvenienti?

+0

ancora fino a questo? – NSGaga

+0

Al momento il progetto prototipo è bloccato, e il mio capo mi ha rimandato a lavorare sul materiale "legacy": stored procedure T-SQL, DLL native C++, strati C# estremamente sottili basati su RCW e così via (li chiamo librerie "legacy", ma hanno sempre bisogno di nuove funzionalità ed estensioni, quindi per la gestione non sono ancora "legacy"). Ma prima o poi dovrò affrontare nuovamente Entity Framework. Quindi sì, la domanda è ancora aperta ... – Demetrio78

+0

pensi che il codice sia l'approccio migliore in questo caso? Non è possibile che un primo approccio al modello si adatti meglio al tuo caso? è possibile chiamare le stored procedure direttamente anziché utilizzare le modifiche di salvataggio. Avere una classe parziale per ogni entità che può essere salvata con un metodo Save() che richiama l'archivio appropriato. – KinSlayerUY

risposta

1

Direi che questa logica appartiene alla classe MockOrders.Order, in una classe da un livello superiore che utilizza la classe Order (ad esempio BusinessLogic.Order) o in una classe Label. Sembra che la tua etichetta agisca come un attributo di join quindi, senza conoscere i particolari, direi estraendola e rendendola un'entità a sé stante, questo ti darà le proprietà di navigazione in modo da poter accedere più naturalmente a tutti gli ordini con la stessa etichetta .

Se si modifica il DB per normalizzare le etichette, non è un utente, creare una vista e inserirla nel modello di entità a tale scopo.

+0

_ "Sembra che la tua etichetta agisca come un attributo di join" _ ovviamente lo fa, ma ho postato un esempio semplicistico cercando di concentrare le cose in 1 classe. La filosofia del problema non sarebbe stata tanto diversa con per es. una classe OrderGroup che raccoglie gli oggetti Order in un elenco. _ "in modo che tu possa accedere più naturalmente a tutti gli ordini con la stessa etichetta" _ il problema non è accedere a tutti gli ordini con la stessa etichetta, il problema è: quando un ordine viene modificato, una modifica a tutti gli ordini nel gruppo deve essere applicata automaticamente secondo una specifica logica di business. – Demetrio78

+0

_ "questa logica appartiene (...) in una classe da un livello superiore che utilizza la classe Order (es .BusinessLogic.Order)" _ Sto iniziando a pensare che mi manca completamente il punto su come si suppone EF da utilizzare. Forse non dovrei esporre DIRETTAMENTE le classi EF (come la mia classe derivata da DbContext) alle applicazioni, dovrei? Ci sono forti dibattiti su IQueryable come (http://mikehadlow.blogspot.de/2009/01/should-my-repository-expose-iqueryable.html) o (http://www.weirdlover.com/2010/05/11/Iqueryable-can-kill-your-dog-ruba-la-tua-moglie-uccide-la-tua-volontà-per-vivere-etc /) ma per quanto riguarda gli oggetti DbContext? – Demetrio78

+0

Ho trovato questo (http://stackoverflow.com/questions/11658060/should-dbcontext-and-dbset-be-exposed-for-a-pure-poco-application), leggerò i collegamenti correlati ... BTW , grazie mille per la tua risposta – Demetrio78

1

Ho dovuto fare qualcosa di simile, ma ho creato un'interfaccia IPrepForSave e ho implementato quell'interfaccia per tutte le entità che hanno bisogno di fare qualche logica di business prima di essere salvate.

L'interfaccia (pardon il VB.NET):

Public Interface IPrepForSave 
    Sub PrepForSave() 
End Interface 

Il DbContext.SaveChanges ignorare:

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 

E poi posso mantenere la logica di business nella propria Entità, nel implementato PrepForSave() metodo:

Partial Public Class MyEntity 
    Implements IPrepForSave 

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

End Class 

Si noti che ho posto alcune restrizioni su ciò che può essere fatto in PrepForSave() metodo:

  1. Eventuali modifiche all'entità non possono rendere la logica di convalida dell'entità non riuscita, poiché questo verrà chiamato dopo che è stata richiamata la logica di convalida.
  2. L'accesso al database deve essere ridotto al minimo e dovrebbe essere di sola lettura.
  3. Qualsiasi entità che non ha bisogno di fare business logic prima di salvare non dovrebbe implementare questa interfaccia.
Problemi correlati