【问题标题】:Entity framework 6 code first Many to many insert slow实体框架6代码优先多对多插入慢
【发布时间】:2016-08-24 16:26:00
【问题描述】:

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

//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 的软件中的任何操作。

谢谢!

【问题讨论】:

    标签: performance entity-framework ef-code-first entity-framework-6 many-to-many


    【解决方案1】:

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

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

    你可以找到更多关于这是做什么的信息here

    【讨论】:

      【解决方案2】:

      我看到您的代码运行缓慢的两个原因。

      添加与添加范围

      您使用 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 的建议工作,但我个人不喜欢这种解决方案。

      数据库往返

      添加、修改或删除每条记录都需要一个数据库往返。因此,如果您插入 3,000 条记录,则需要 3,000 次数据库往返,这非常慢。

      您已经尝试过 EntityFramework.BulkInsert 或 SqlBulkCopy,这很棒。我建议您先使用“AddRange”修复再次尝试它们,以查看新的性能。

      以下是支持 BulkInsert for EF 的库的偏差比较: Entity Framework - Bulk Insert Library Reviews & Comparisons

      免责声明:我是项目的所有者Entity Framework Extensions

      此库允许您在数据库中 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。一份用于产品,一份用于订购。

      您可能会在像 answer 这样的不同线程中找到比我更好的答案。

      Add方法成功附加产品,后续调用同一个产品不会报错,因为是同一个产品。

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

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

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

      虽然可能不够优雅,但性能会有所提升。

      【讨论】:

      • 它的速度更快,但听起来它不喜欢带有子引用的对象。说:一个实体对象不能被多个 IEntityChangeTracker 实例引用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多