Sto cercando di trovare un modo per migliorare le prestazioni degli inserimenti con il seguente codice (per favore, leggi le mie domande dopo il blocco di codice):
//Domain classes
[Table("Products")]
public class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Sku { get; set; }
[ForeignKey("Orders")]
public virtual ICollection<Order> Orders { get; set; }
public Product()
{
Orders = new List<Order>();
}
}
[Table("Orders")]
public class Order
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Title { get; set; }
public decimal Total { get; set; }
[ForeignKey("Products")]
public virtual ICollection<Product> Products { get; set; }
public Order()
{
Products = new List<Product>();
}
}
//Data access
public class MyDataContext : DbContext
{
public MyDataContext()
: base("MyDataContext")
{
Configuration.LazyLoadingEnabled = true;
Configuration.ProxyCreationEnabled = true;
Database.SetInitializer(new CreateDatabaseIfNotExists<MyDataContext>());
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().ToTable("Products");
modelBuilder.Entity<Order>().ToTable("Orders");
}
}
//Service layer
public interface IServices<T, K>
{
T Create(T item);
T Read(K key);
IEnumerable<T> ReadAll(Expression<Func<IEnumerable<T>, IEnumerable<T>>> pre);
T Update(T item);
void Delete(K key);
void Save();
void Dispose();
void BatchSave(IEnumerable<T> list);
void BatchUpdate(IEnumerable<T> list, Action<UpdateSpecification<T>> spec);
}
public class BaseServices<T, K> : IDisposable, IServices<T, K> where T : class
{
protected MyDataContext Context;
public BaseServices()
{
Context = new MyDataContext();
}
public T Create(T item)
{
T created;
created = Context.Set<T>().Add(item);
return created;
}
public void Delete(K key)
{
var item = Read(key);
if (item == null)
return;
Context.Set<T>().Attach(item);
Context.Set<T>().Remove(item);
}
public T Read(K key)
{
T read;
read = Context.Set<T>().Find(key);
return read;
}
public IEnumerable<T> ReadAll(Expression<Func<IEnumerable<T>, IEnumerable<T>>> pre)
{
IEnumerable<T> read;
read = Context.Set<T>().ToList();
read = pre.Compile().Invoke(read);
return read;
}
public T Update(T item)
{
Context.Set<T>().Attach(item);
Context.Entry<T>(item).CurrentValues.SetValues(item);
Context.Entry<T>(item).State = System.Data.Entity.EntityState.Modified;
return item;
}
public void Save()
{
Context.SaveChanges();
}
}
public interface IOrderServices : IServices<Order, int>
{
//custom logic goes here
}
public interface IProductServices : IServices<Product, int>
{
//custom logic goes here
}
//Web project's controller
public ActionResult TestCreateProducts()
{
//Create 100 new rest products
for (int i = 0; i < 100; i++)
{
_productServices.Create(new Product
{
Sku = i.ToString()
});
}
_productServices.Save();
var products = _productServices.ReadAll(r => r); //get a list of saved products to add them to orders
var random = new Random();
var orders = new List<Order>();
var count = 0;
//Create 3000 orders
for (int i = 1; i <= 3000; i++)
{
//Generate a random list of products to attach to the current order
var productIds = new List<int>();
var x = random.Next(1, products.Count() - 1);
for (int j = 0; j < x; j++)
{
productIds.Add(random.Next(products.Min(r => r.Id), products.Max(r => r.Id)));
}
//Create the order
var order = new Order
{
Title = "Order" + i,
Total = i,
Products = products.Where(p => productIds.Contains(p.Id))
};
orders.Add(order);
}
_orderServices.CreateRange(orders);
_orderServices.Save();
return RedirectToAction("Index");
}
Questo codice funziona bene ma è MOLTO lento quando viene eseguito il SaveChanges.
Dietro la scena, le annotazioni sugli oggetti del dominio creano tutte le relazioni necessarie: una tabella OrderProducts con le chiavi esterne appropriate viene creata automaticamente e gli inserimenti vengono eseguiti correttamente da EF.
Ho provato molte cose con gli inserimenti di massa utilizzando EntityFramework.Utilities, SqlBulkCopy, ecc ... ma nessuno ha funzionato. C'è un modo per ottenere questo? Capisco che questo è solo a scopo di test e il mio obiettivo è quello di ottimizzare al meglio le operazioni nei nostri software usando EF.
Grazie!
Vedo due motivi per cui il tuo codice è lento.
Aggiungi vs AggiungiRange
Aggiungi entità una per una usando il metodo Crea .
Dovresti sempre usare AddRange su Aggiungi. Il metodo Aggiungi proverà a Rilevare modifiche ogni volta che viene richiamato il metodo di aggiunta mentre AddRange una sola volta.
Dovresti aggiungere un metodo " CreateRange " nel tuo codice.
public IEnumerable<T> CreateRange(IEnumerable<T> list)
{
return Context.Set<T>().AddRange(list);
}
var products = new List<Product>();
//Create 100 new rest products
for (int i = 0; i < 100; i++)
{
products.Add(new Product { Sku = i.ToString() });
}
_productServices.CreateRange(list);
_productServices.Save();
Disabilitare / Abilitare la proprietà AutoDetectChanges funziona anche come @mark_h proposto, ma personalmente non mi piace questo tipo di soluzione.
Database Round Trip
È necessario un round trip del database per ogni record da aggiungere, modificare o eliminare. Pertanto, se si inseriscono 3.000 record, sarà necessario eseguire 3.000 round trip del database, che è MOLTO lento.
Hai già provato EntityFramework.BulkInsert o SqlBulkCopy, il che è fantastico. Vi raccomando di provare nuovamente con la correzione " AddRange " per vedere le nuove prestazioni.
Ecco un confronto parziale della libreria che supporta BulkInsert per EF: Entity Framework - Bulk Insert Library Reviews & Comparisons
Disclaimer : sono il proprietario del progetto Entity Framework Extensions
Questa libreria ti permette di BulkSaveChanges, BulkInsert, BulkUpdate, BulkDelete e BulkMerge all'interno del tuo Database.
Supporta tutte le eredità e le associazioni.
// Easy to use
public void Save()
{
// Context.SaveChanges();
Context.BulkSaveChanges();
}
// Easy to customize
public void Save()
{
// Context.SaveChanges();
Context.BulkSaveChanges(bulk => bulk.BatchSize = 100);
}
EDIT : aggiunta risposta alla domanda secondaria
Un oggetto entità non può essere referenziato da più istanze di IEntityChangeTracker
Il problema si verifica perché si utilizzano due diversi DbContext. Uno per il prodotto e uno per l'ordine.
Potresti trovare una risposta migliore della mia in una discussione diversa come questa risposta .
Il metodo Add allegare correttamente il prodotto, la chiamata successiva dello stesso prodotto non genera un errore perché è lo stesso prodotto.
Il metodo AddRange, tuttavia, collega il prodotto più volte poiché non proviene dallo stesso contesto, quindi quando Rileva modifiche viene chiamato, non sa come gestirlo.
Un modo per risolverlo è riutilizzando lo stesso contesto
var _productServices = new BaseServices<Product, int>();
var _orderServices = new BaseServices<Order, int>(_productServices.Context);
Anche se potrebbe non essere elegante, le prestazioni saranno migliorate.
Appena prima di inserire i tuoi inserti, disattiva AutoDetectChangesEnabled del tuo contesto (impostandolo su false). Inserisci i tuoi inserti e quindi imposta AutoDetectChangesEnabled su true es;
try
{
MyContext.Configuration.AutoDetectChangesEnabled = false;
// do your inserts updates etc..
}
finally
{
MyContext.Configuration.AutoDetectChangesEnabled = true;
}
Puoi trovare maggiori informazioni su ciò che questo sta facendo qui