IT TIP

1000 개의 Entity Framework 개체를 만들 때 SaveChanges ()를 언제 호출해야합니까?

itqueen 2020. 10. 24. 12:10
반응형

1000 개의 Entity Framework 개체를 만들 때 SaveChanges ()를 언제 호출해야합니까? (가져 오는 동안처럼)


각 실행에 대해 1000 개의 레코드가있는 가져 오기를 실행하고 있습니다. 내 가정에 대한 확인을 찾고 있습니다.

다음 중 가장 의미가있는 것은 무엇입니까?

  1. SaveChanges()모든 AddToClassName()통화를 실행하십시오 .
  2. n 번의 호출 SaveChanges()마다 실행 합니다.AddToClassName()
  3. 실행 SaveChanges()모든AddToClassName()호출.

첫 번째 옵션은 아마 느립니다. 메모리의 EF 개체를 분석하고 SQL을 생성하는 등의 작업이 필요하기 때문입니다.

두 번째 옵션이 두 세계 모두에서 최고라고 가정합니다. 그 SaveChanges()호출에 대해 try catch를 래핑하고 둘 중 하나가 실패 할 경우 한 번에 n 개의 레코드 만 잃을 수 있기 때문입니다. 각 배치를 List <>에 저장할 수 있습니다. 경우 SaveChanges()호출이 성공 목록을 없애. 실패하면 항목을 기록하십시오.

모든 단일 EF 개체 SaveChanges()가 호출 될 때까지 메모리에 있어야하기 때문에 마지막 옵션도 매우 느려질 것 입니다. 그리고 저장이 실패하면 아무것도 커밋되지 않을 것입니다.


확실히하기 위해 먼저 테스트하겠습니다. 성능이 그렇게 나쁠 필요는 없습니다.

하나의 트랜잭션에 모든 행을 입력해야하는 경우 모든 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);
    }
}

방금 내 코드에서 매우 유사한 문제를 최적화했으며 저에게 효과가있는 최적화를 지적하고 싶습니다.

한 번에 100 개 또는 1000 개의 레코드를 처리하든 SaveChanges를 처리하는 데 많은 시간이 CPU 제한이라는 것을 알았습니다. 따라서 생산자 / 소비자 패턴 (BlockingCollection으로 구현 됨)으로 컨텍스트를 처리함으로써 CPU 코어를 훨씬 더 잘 사용할 수 있었고 초당 총 4000 회의 변경 (SaveChanges의 반환 값으로보고 됨)에서 초당 14,000 개 이상의 변경. CPU 사용률이 약 13 % (코어가 8 개 있음)에서 약 60 %로 이동했습니다. 여러 소비자 스레드를 사용하더라도 (매우 빠른) 디스크 IO 시스템에 거의 부담을주지 않았고 SQL Server의 CPU 사용률은 15 %를 넘지 않았습니다.

By offloading the saving to multiple threads, you have the ability to tune both the number of records prior to commit and the number of threads performing the commit operations.

I found that creating 1 producer thread and (# of CPU Cores)-1 consumer threads allowed me to tune the number of records committed per batch such that the count of items in the BlockingCollection fluctuated between 0 and 1 (after a consumer thread took one item). That way, there was just enough work for the consuming threads to work optimally.

This scenario of course requires creating a new context for every batch, which I find to be faster even in a single-threaded scenario for my use case.


If you need to import thousands of records, I'd use something like SqlBulkCopy, and not the Entity Framework for that.


Use a stored procedure.

  1. Create a User-Defined Data Type in Sql Server.
  2. Create and populate an array of this type in your code (very fast).
  3. Pass the array to your stored procedure with one call (very fast).

I believe this would be the easiest and fastest way to do this.


Sorry, I know this thread is old, but I think this could help other people with this problem.

I had the same problem, but there is a possibility to validate the changes before you commit them. My code looks like this and it is working fine. With the chUser.LastUpdated I check if it is a new entry or only a change. Because it is not possible to reload an Entry that is not in the database yet.

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

참고URL : https://stackoverflow.com/questions/1930982/when-should-i-call-savechanges-when-creating-1000s-of-entity-framework-object

반응형