实体框架6代码优先多对多插入缓慢



我正试图用以下代码找到一种提高插入性能的方法(请在代码块后阅读我的问题):

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

此代码运行良好,但在执行SaveChanges时速度非常慢。

在场景背后,域对象上的注释创建了所需的所有关系:自动创建具有适当外键的OrderProducts表,EF正确地执行插入。

我尝试过很多使用EntityFramework.Utilities、SqlBulkCopy等进行批量插入的方法,但都不起作用。有办法做到这一点吗?理解这只是为了测试目的,我的目标是尽我所能优化使用EF的软件中的任何操作。

谢谢!

在执行插入之前,禁用上下文的AutoDetectChangesEnabled(通过将其设置为false)。执行插入操作,然后将AutoDetectChangesEnabled设置回true,例如。;

        try
        {
            MyContext.Configuration.AutoDetectChangesEnabled = false;
            // do your inserts updates etc..
        }
        finally
        {
            MyContext.Configuration.AutoDetectChangesEnabled = true;
        }

您可以在这里找到更多关于这项工作的信息

我看到了代码速度慢的两个原因。

添加与添加范围

使用Create方法逐个添加实体。

您应该始终使用AddRange而不是Add。每次调用Add方法时,Add方法都会尝试DetectChanges,而AddRange只调用一次。

您应该在代码中添加一个"CreateRange"方法。

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

禁用/启用属性AutoDetectChanges也可以像@mark_h建议的那样工作,但我个人不喜欢这种解决方案。

数据库往返

添加、修改或删除每条记录都需要数据库往返。因此,如果您插入3000条记录,那么将需要3000个数据库往返,这是非常缓慢的。

您已经尝试过EntityFramework.BulkInsert或SqlBulkCopy,这很好。我建议您首先使用"AddRange"修复程序重试,以查看新的性能。

以下是对EF支持BulkInsert的库的有偏比较:实体框架-批量插入库评论&比较

免责声明:我是项目实体框架扩展的所有者

此库允许您在数据库中进行BulkSaveChanges、BulkInsert、BulkUpdate、BulkDelete和BulkMerge。

它支持所有的继承和关联。

// Easy to use
public void Save()
{
    // Context.SaveChanges();
    Context.BulkSaveChanges();
}
// Easy to customize
public void Save()
{
    // Context.SaveChanges();
    Context.BulkSaveChanges(bulk => bulk.BatchSize = 100);
}

编辑:添加子问题的答案

实体对象不能由的多个实例引用IEntityChangeTracker

发生此问题是因为您使用了两个不同的DbContext。一个用于产品,一个用于订单。

你可能会在不同的线索中找到比我更好的答案,比如这个答案。

Add方法成功地附加了产品,随后对同一产品的调用不会抛出错误,因为它是同一产品。

然而,AddRange方法会多次附加产品,因为它不是来自同一个上下文,所以当调用Detect Changes时,他不知道如何处理它

修复它的一种方法是重新使用相同的上下文

var _productServices = new BaseServices<Product, int>();
var _orderServices = new BaseServices<Order, int>(_productServices.Context);

虽然它可能不优雅,但性能会有所提高。

最新更新