实体框架批量插入/更新关系

时间:2017-04-06 10:12:32

标签: c# sql-server database performance entity-framework

我有一个场景,我需要为大量数据执行数据库更新。有外键关系需要同时添加,我得到一个外来对象的列表,所以我不必每次都检查它们是否存在/添加等等:

using(DbEntities db = new DbEntities())
{
   // Get list of all books so don't have to hit every time
   Dictionary<int, Book> books = db.Books.ToDictionary(k => k.BookId, v => v);

   // Loop through big file to import each row
   foreach(var item in bigFile)
   {
      Person person = new Person { FirstName = item.FirstName, LastName = item.LastName };

      foreach(var book in item.Books)
      {
         if(!books.ContainsKey(book.BookId))
         {
            // Add book to DB if doesn't exist
            Book bookToAdd = new Book { BookId = book.BookId, Name = book.Name };
            db.Books.Add(bookToAdd);

            books.Add(bookToAdd.BookId, bookToAdd);
         }

         person.Books.Add(books[book.BookId]);
      }

      db.People.Add(person);
   }

   db.SaveChanges();
}

此解决方案的问题在于导入快速启动并且随着速度变慢而变慢。这似乎取决于变化跟踪变得臃肿的背景。

我看过帖子建议使用db.Configuration.AutoDetectChangesEnabled = false但是当我这样做时,关系不会被添加。我可以通过强制DetectChanges()来完成这项工作,但这似乎打败了目的,因为我必须在循环的每次迭代中都这样做。

因此,我在循环中移动了DB上下文,因此每次都会重新创建它。这样做意味着我不能再拥有分离的书籍清单了,所以我必须为每一行.Any().Single()调用数据库(我不知道这是否是一个专业性能问题,但总是尝试尽可能少地访问数据库):

// Loop through big file to import each row
foreach(var item in bigFile)
{
   // Create DB context each time
   using(DbEntities db = new DbEntities())
   {
      Person person = new Person { FirstName = item.FirstName, LastName = item.LastName };

      foreach(var book in item.Books)
      {
         if(!db.Books.Any(m => m.BookId = bookId))
         {
            // Add book to DB if doesn't exist
            Book bookToAdd = new Book { BookId = bookId, Name = book.Name

            db.Books.Add(bookToAdd);
         }

         person.Books.Add(db.Books.Single(m => m.BookId = bookId));
      }

      db.People.Add(person);

      db.SaveChanges();
   }
}

这大大加快了速度但是在大约5,000-10,000行后它仍然开始减速,我想知道我的选择是什么? ...除了使用存储过程完成所有操作外!

3 个答案:

答案 0 :(得分:1)

IMO两种解决方案都不好。第一个是在内存中加载整个现有的Books表(和db上下文),第二个是每人书执行2个db查询 - 一个使用Any,另一个使用Single

由于我的测试没有显示上下文更改跟踪的性能问题,我将使用第一种方法的变体与第二种方法的元素。我将使用按需填充的本地字典,而不是加载整个Books表,每个新书都使用单个数据库查询Id:

using (DbEntities db = new DbEntities())
{
    // The local book dictionary
    Dictionary<int, Book> books = new Dictionary<int, Book>();

    // Loop through big file to import each row
    foreach (var item in bigFile)
    {
        Person person = new Person { FirstName = item.FirstName, LastName = item.LastName };

        foreach (var itemBook in item.Books)
        {
            Book book;

            // Try get from local dictionary
            if (!books.TryGetValue(itemBook.BookId, out book))
            {
                // Try get from db
                book = db.Books.FirstOrDefault(e => e.BookId == itemBook.BookId);
                if (book == null)
                {
                    // Add book to DB if doesn't exist
                    book = new Book { BookId = itemBook.BookId, Name = itemBook.Name };
                    db.Books.Add(book);
                }
                // add to local dictionary
                books.Add(book.BookId, book);
            }

            person.Books.Add(book);
        }

        db.People.Add(person);
    }

    db.SaveChanges();
}

答案 1 :(得分:0)

听起来你有内存泄漏,我之前使用过PerfView来比较不同时间内存在的对象。我猜测你的上下文类没有被处理掉(即由于某些原因它们被保留)。如果您有使用这些性能工具的经验,那么这将是一个很好的起点,但如果您没有,那么学习曲线就会很陡峭。

就个人而言,我会使用单个存储过程和一个或多个表值参数来执行像您这样的大数据导入。根据我的经验,它们要快得多。

*修改

刚刚注意到代码中的一些错误......你错过了一些比较运算符:

 // Loop through big file to import each row
foreach(var item in bigFile)
{
   // Create DB context each time
   using(DbEntities db = new DbEntities())
   {
      Person person = new Person { FirstName = item.FirstName, LastName = item.LastName };

      foreach(var book in item.Books)
      {
         if(!db.Books.Any(m => m.BookId == bookId))
         {
            // Add book to DB if doesn't exist
            Book bookToAdd = new Book { BookId = bookId, Name = book.Name

            db.Books.Add(bookToAdd);
         }

         person.Books.Add(db.Books.Single(m => m.BookId == bookId));
      }

      db.People.Add(person);

      db.SaveChanges();
   }
}

答案 2 :(得分:0)

<强> db.Books.Any

  

我不知道这是否是一个主要的性能问题,但总是试图尽可能少地访问数据库

是的,这是一个主要的性能问题。对于每本书,你都会进行一次数据库往返,这是非常低效的。

提议的解决方案

(是的,这与第一个例子的解决方案相同)

使一个数据库往返并改为使用字典。

// var bookIds = ctx.EntitySimples.Select(x => x.Id).ToDictionary(x => x);
var books = db.Books.ToDictionary(k => k.BookId, v => v);

// ...code...

if(!bookIds.ContainsKey(bookId))
{
}

Add + AutoDectectChangesEnabled = false vs AddRange

  

我看到过建议使用的帖子   db.Configuration.AutoDetectChangesEnabled = false但是当我这样做时   关系不会被添加。

禁用AutoDetectChanges可以在Add和AddRange之间获得相同的性能。但是,如果这不起作用,那肯定会成为一个问题!

提议的解决方案

使用AddRange

using(DbEntities db = new DbEntities())
{
   var listToAdd = new List<Book>();
   var personToAdd = new List<Person>();

   // Get list of all books so don't have to hit every time
   Dictionary<int, Book> books = db.Books.ToDictionary(k => k.BookId, v => v);

   // Loop through big file to import each row
   foreach(var item in bigFile)
   {
      Person person = new Person { FirstName = item.FirstName, LastName = item.LastName };

      foreach(var book in item.Books)
      {
         if(!books.ContainsKey(book.BookId))
         {
            // Add book to DB if doesn't exist
            Book bookToAdd = new Book { BookId = book.BookId, Name = book.Name };

            // ADD to list instead
            listToAdd.Add(bookToAdd);
         }

         person.Books.Add(books[book.BookId]);
      }

      // ADD to list instead
      personToAdd.Add(person);
   }

   // USE AddRange here instead
   db.Books.AddRange(listToAdd);
   db.People.AddRange(person);

   db.SaveChanges();
}

<强>的SaveChanges

对于每本书和每个人,您需要添加或更新,执行数据库往返。

因此,如果你需要插入10000本书,将会执行10000次数据库往返 INSANELY SLOW

提议的解决方案

使用允许您执行批量操作的库。

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

此库允许您执行所有批量操作:

  • BulkSaveChanges
  • BulkInsert
  • BulkUpdate
  • BulkDelete
  • BulkMerge
  • BulkSynchronize

示例:

// Easy to use
context.BulkSaveChanges();

// Easy to customize
context.BulkSaveChanges(bulk => bulk.BatchSize = 100);

// Perform Bulk Operations
context.BulkDelete(customers);
context.BulkInsert(customers);
context.BulkUpdate(customers);

// Customize Primary Key
context.BulkMerge(customers, operation => {
   operation.ColumnPrimaryKeyExpression = 
        customer => customer.Code;
});

编辑:回答子问题

  

我看到了AddRange的好处,但是如果我将该代码示例扩展到不仅添加新的Person实体而且如果它们已经存在则也会更新呢?

如果所有内容都可以加载到内存中,您可以使用与目前相同的解决方案。

var people = db.People.ToDictionary(k => k.PersonId, v => v);

只需编辑它即可。

请记住,如果您没有进行任何并发检查,如果导入需要花费大量时间,则可以覆盖修改后的值。

小心常见的陷阱:

  • 有些人会推荐AddOrUpdate方法,它每次都会使数据库往返。因此,增加输入时间。

另一种技术可能是使用我库中的BulkMerge方法。