创建1000个实体框架对象时应何时调用SaveChanges()?(例如在导入过程中)


80

我正在运行一个导入,每次运行都会有1000条记录。只是根据我的假设寻找一些确认:

以下哪一项最有意义:

  1. 运行SaveChanges()每个AddToClassName()电话。
  2. SaveChanges()nAddToClassName()电话运行一次。
  3. 运行SaveChanges()后,所有的的AddToClassName()电话。

第一种选择可能很慢吧?由于它将需要分析内存中的EF对象,生成SQL等。

我认为第二种选择是两全其美的,因为我们可以在该SaveChanges()调用周围包含try catch ,并且如果其中一个失败,一次仅丢失n条记录。也许将每个批次存储在List <>中。如果SaveChanges()呼叫成功,请删除列表。如果失败,则记录项目。

最后一个选项可能最终也会非常慢,因为每个单个EF对象都必须在内存中直到SaveChanges()被调用。如果保存失败,什么也不会提交,对吗?

Answers:


62

我先测试一下以确定。性能不必这么差。

如果需要在一个事务中输入所有行,请在所有AddToClassName类之后调用它。如果可以独立输入行,请在每行之后保存更改。数据库一致性很重要。

第二个选择我不喜欢。(从最终用户的角度来看)如果导入到系统,这将使我感到困惑,并且由于1个不好,它将减少1000行中的10行。您可以尝试导入10,如果导入失败,请一个接一个地尝试,然后登录。

测试是否需要很长时间。不要写“适当”。你还不知道。仅当确实存在问题时,才考虑其他解决方案(marc_s)。

编辑

我已经做了一些测试(时间以毫秒为单位):

10000行:

1行后的SaveChanges():18510,534
100行后的
SaveChanges():4350,3075 10000行后的SaveChanges():5233,0635

50000行

1行后的SaveChanges():78496,929
500行后的
SaveChanges():22302,2835 50000行后的SaveChanges():24022,8765

因此,实际上在n行之后提交比在所有行中提交要快。

我的建议是:

  • n行之后的SaveChanges()。
  • 如果一次提交失败,请一一尝试查找有问题的行。

测试类别:

表:

CREATE TABLE [dbo].[TestTable](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [SomeInt] [int] NOT NULL,
    [SomeVarchar] [varchar](100) NOT NULL,
    [SomeOtherVarchar] [varchar](50) NOT NULL,
    [SomeOtherInt] [int] NULL,
 CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

类:

public class TestController : Controller
{
    //
    // GET: /Test/
    private readonly Random _rng = new Random();
    private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private string RandomString(int size)
    {
        var randomSize = _rng.Next(size);

        char[] buffer = new char[randomSize];

        for (int i = 0; i < randomSize; i++)
        {
            buffer[i] = _chars[_rng.Next(_chars.Length)];
        }
        return new string(buffer);
    }


    public ActionResult EFPerformance()
    {
        string result = "";

        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>";
        TruncateTable();

        return Content(result);
    }

    private void TruncateTable()
    {
        using (var context = new CamelTrapEntities())
        {
            var connection = ((EntityConnection)context.Connection).StoreConnection;
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = @"TRUNCATE TABLE TestTable";
            command.ExecuteNonQuery();
        }
    }

    private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows)
    {
        var startDate = DateTime.Now;

        using (var context = new CamelTrapEntities())
        {
            for (int i = 1; i <= noOfRows; ++i)
            {
                var testItem = new TestTable();
                testItem.SomeVarchar = RandomString(100);
                testItem.SomeOtherVarchar = RandomString(50);
                testItem.SomeInt = _rng.Next(10000);
                testItem.SomeOtherInt = _rng.Next(200000);
                context.AddToTestTable(testItem);

                if (i % commitAfterRows == 0) context.SaveChanges();
            }
        }

        var endDate = DateTime.Now;

        return endDate.Subtract(startDate);
    }
}

我写“大概”的原因是我做出了有根据的猜测。为了更清楚地表明“我不确定”,我提出了一个问题。另外,我认为在遇到潜在问题之前先考虑一下是完全有意义的。这就是我问这个问题的原因。我希望有人会知道哪种方法最有效,并且我可以马上使用。
John Bubriski

了不起的家伙。正是我想要的。感谢您抽出宝贵的时间对此进行测试!我猜想我可以将每个批次存储在内存中,尝试提交,然后如果失败,请按照您所说的逐一进行。然后,完成该批处理后,释放对这100个项目的引用,以便可以将其清除出内存。再次感谢!
约翰·布布里斯基

3
内存将不会被释放,因为所有对象都将由ObjectContext保留,但是如今具有50000或100000的上下文并不会占用太多空间。
LukLed

6
实际上,我发现每次调用SaveChanges()之间的性能都会降低。解决方案是在每次SaveChanges()调用之后实际处理上下文,并为要添加的下一批数据重新实例化一个新的上下文。
肖恩·德·

1
@LukLed不太...您正在For循环内调用SaveChanges ...因此代码可以继续添加更多项目以在ctx的同一实例上的for循环内保存,并稍后在同一实例上再次调用SaveChanges 。
肖恩·德·

18

我只是在自己的代码中优化了一个非常类似的问题,并想指出对我有用的优化。

我发现处理SaveChanges的大部分时间(无论一次处理100还是1000条记录)都受CPU限制。因此,通过使用生产者/消费者模式(由BlockingCollection实现)处理上下文,我能够更好地利用CPU内核,并且总共得到4000次更改/秒(如SaveChanges的返回值所报告)每秒超过14,000次更改。CPU利用率从大约13%(我有8个内核)提高到大约60%。即使使用多个使用者线程,我也几乎不给(非常快的)磁盘IO系统增加负担,并且SQL Server的CPU利用率不超过15%。

通过将保存卸载到多个线程,您可以调整提交之前的记录数和执行提交操作的线程数。

我发现创建1个生产者线程和(CPU核心数)-1个使用者线程使我能够调整每批提交的记录数,以使BlockingCollection中的项目数在0到1之间波动(在使用者线程取一个之后项目)。这样,就足以使使用线程最佳地工作。

当然,这种情况下需要为每个批处理创建一个新的上下文,即使在我的用例的单线程情况下,我也发现这会更快。


嗨,@ eric-j能否请您详细说明这一行“通过使用生产者/消费者模式处理上下文(由BlockingCollection实现)”,以便我可以尝试使用我的代码?
Foyzul Karim

14

如果您需要导入成千上万条记录,则可以使用SqlBulkCopy之类的东西,而不要使用Entity Framework。


15
当人们不回答我的问题时,我讨厌它:)好吧,假设我“需要”使用EF。然后怎样呢?
John Bubriski

3
好吧,如果您真的必须使用EF,那么我将尝试在一批说500或1000条记录之后提交。否则,您最终将使用过多的资源,而当第100000行失败时,故障可能会回滚您更新的所有99999行。
marc_s

同样的问题,我以使用SqlBulkCopy作为结束,在这种情况下,它比EF的性能更高。尽管我不喜欢使用几种方式来访问数据库。
朱利安N

2
我也正在研究这个解决方案,因为我遇到了同样的问题……批量复制将是一个很好的解决方案,但是我的托管服务不允许使用它(我想其他人也可以使用),所以这不是可行的有些人的选择。
丹尼斯·沃德

3
@marc_s:使用SqlBulkCopy时,如何处理强制执行业务对象中固有的业务规则的需求?如果不冗余执行规则,我看不到如何使用EF。
Eric J.

2

使用存储过程。

  1. 在Sql Server中创建用户定义的数据类型。
  2. 在您的代码中创建并填充此类型的数组(非常快)。
  3. 一次调用(非常快)将数组传递到存储过程。

我相信这将是最简单,最快的方法。


7
通常,在SO上,“这是最快的”声明需要用测试代码和结果来证实。
迈克尔·布莱克本

2

抱歉,我知道这个线程很旧,但是我认为这可以帮助其他人解决这个问题。

我遇到了同样的问题,但是可以在提交更改之前先进行验证。我的代码看起来像这样,并且运行良好。使用“chUser.LastUpdated我”检查它是一个新条目还是仅一个更改。因为无法重新加载数据库中尚未存在的条目。

// Validate Changes
var invalidChanges = _userDatabase.GetValidationErrors();
foreach (var ch in invalidChanges)
{
    // Delete invalid User or Change
    var chUser  =  (db_User) ch.Entry.Entity;
    if (chUser.LastUpdated == null)
    {
        // Invalid, new User
        _userDatabase.db_User.Remove(chUser);
        Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey);
    }
    else
    {
        // Invalid Change of an Entry
        _userDatabase.Entry(chUser).Reload();
        Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey);
    }                    
}

_userDatabase.SaveChanges();

是的,这是同样的问题,对吗?这样,您可以添加所有1000条记录,并且在运行之前 saveChanges()可以删除会导致错误的记录。
Jan Leuenberger

1
但是问题的重点是在一次SaveChanges调用中要有效提交多少次插入/更新。您没有解决这个问题。请注意,SaveChanges失败的潜在原因多于验证错误。顺便说一句,您也可以将实体标记为,Unchanged而不是重新加载/删除它们。
Gert Arnold

1
没错,它并不能直接解决问题,但是我认为大多数迷失于此线程的人都存在验证问题,尽管还有其他原因导致SaveChanges失败。这就解决了问题。如果此帖子真的在这个线程中打扰您,我可以删除它,我的问题就解决了,我只是想帮助别人。
Jan Leuenberger

我对此有一个疑问。当您调用时GetValidationErrors(),它“伪造”对数据库的调用并检索错误或什么?感谢您的回复:)
Jeancarlo Fontalvo '19
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.