[C#] Entity Framework의 SqlException-세션에서 실행중인 다른 스레드가 있으므로 새 트랜잭션이 허용되지 않습니다.

현재이 오류가 발생합니다.

System.Data.SqlClient.SqlException : 세션에서 실행중인 다른 스레드가 있으므로 새 트랜잭션이 허용되지 않습니다.

이 코드를 실행하는 동안 :

public class ProductManager : IProductManager
{
    #region Declare Models
    private RivWorks.Model.Negotiation.RIV_Entities _dbRiv = RivWorks.Model.Stores.RivEntities(AppSettings.RivWorkEntities_connString);
    private RivWorks.Model.NegotiationAutos.RivFeedsEntities _dbFeed = RivWorks.Model.Stores.FeedEntities(AppSettings.FeedAutosEntities_connString);
    #endregion

    public IProduct GetProductById(Guid productId)
    {
        // Do a quick sync of the feeds...
        SyncFeeds();
        ...
        // get a product...
        ...
        return product;
    }

    private void SyncFeeds()
    {
        bool found = false;
        string feedSource = "AUTO";
        switch (feedSource) // companyFeedDetail.FeedSourceTable.ToUpper())
        {
            case "AUTO":
                var clientList = from a in _dbFeed.Client.Include("Auto") select a;
                foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
                {
                    var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
                    foreach (RivWorks.Model.Negotiation.AutoNegotiationDetails companyFeedDetail in companyFeedDetailList)
                    {
                        if (companyFeedDetail.FeedSourceTable.ToUpper() == "AUTO")
                        {
                            var company = (from a in _dbRiv.Company.Include("Product") where a.CompanyId == companyFeedDetail.CompanyId select a).First();
                            foreach (RivWorks.Model.NegotiationAutos.Auto sourceProduct in client.Auto)
                            {
                                foreach (RivWorks.Model.Negotiation.Product targetProduct in company.Product)
                                {
                                    if (targetProduct.alternateProductID == sourceProduct.AutoID)
                                    {
                                        found = true;
                                        break;
                                    }
                                }
                                if (!found)
                                {
                                    var newProduct = new RivWorks.Model.Negotiation.Product();
                                    newProduct.alternateProductID = sourceProduct.AutoID;
                                    newProduct.isFromFeed = true;
                                    newProduct.isDeleted = false;
                                    newProduct.SKU = sourceProduct.StockNumber;
                                    company.Product.Add(newProduct);
                                }
                            }
                            _dbRiv.SaveChanges();  // ### THIS BREAKS ### //
                        }
                    }
                }
                break;
        }
    }
}

모델 # 1-이 모델은 개발자 서버의 데이터베이스에 있습니다.
모델 # 1 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/bdb2b000-6e60-4af0-a7a1-2bb6b05d8bc1/Model1.png

모델 # 2-이 모델은 Prod Server의 데이터베이스에 있으며 매일 자동 피드로 업데이트됩니다. 대체 텍스트 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/4260259f-bce6-43d5-9d2a-017bd9a980d4/Model2.png

참고 – 모델 # 1의 빨간색 원으로 표시된 항목은 모델 # 2에 “매핑”하는 데 사용되는 필드입니다. 모델 # 2의 빨간색 원은 무시하십시오. 즉, 다른 질문에 대한 답변입니다.

참고 : 여전히 클라이언트의 인벤토리에서 벗어난 경우 DB1에서 소프트 삭제를 수행 할 수 있도록 isDeleted 검사를 수행해야합니다.

이 특정 코드를 사용하여 DB1의 회사와 DB2의 클라이언트를 연결하고 DB2에서 제품 목록을 가져 와서 DB1에없는 경우 삽입하십시오. 처음부터 재고를 완전히 가져와야합니다. 밤새 피드에 새로운 인벤토리가 들어오지 않는 한 아무 것도 발생하지 않은 후에 실행됩니다.

그래서 큰 질문-내가받는 트랜잭션 오류를 해결하는 방법은 무엇입니까? 루프를 통해 매번 컨텍스트를 삭제하고 다시 만들어야합니까 (나에게 이해가되지 않습니까)?



답변

머리카락을 많이 빼낸 후 foreach고리가 범인 이라는 것을 알았습니다 . 발생해야하는 것은 EF를 호출하지만 IList<T>해당 대상 유형 으로 리턴 한 후IList<T> 입니다.

예:

IList<Client> clientList = from a in _dbFeed.Client.Include("Auto") select a;
foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
{
   var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
    // ...
}


답변

이미 식별 했으므로 foreach활성 판독기를 통해 데이터베이스에서 아직 드로잉중인 데이터를 저장할 수 없습니다 .

작은 데이터 세트의 경우 호출 ToList()또는 문제 ToArray()가 없지만 수천 개의 행이있는 경우 많은 양의 메모리를 사용하게됩니다.

행을 청크로로드하는 것이 좋습니다.

public static class EntityFrameworkUtil
{
    public static IEnumerable<T> QueryInChunksOf<T>(this IQueryable<T> queryable, int chunkSize)
    {
        return queryable.QueryChunksOfSize(chunkSize).SelectMany(chunk => chunk);
    }

    public static IEnumerable<T[]> QueryChunksOfSize<T>(this IQueryable<T> queryable, int chunkSize)
    {
        int chunkNumber = 0;
        while (true)
        {
            var query = (chunkNumber == 0)
                ? queryable
                : queryable.Skip(chunkNumber * chunkSize);
            var chunk = query.Take(chunkSize).ToArray();
            if (chunk.Length == 0)
                yield break;
            yield return chunk;
            chunkNumber++;
        }
    }
}

위의 확장 방법이 주어지면 다음과 같이 쿼리를 작성할 수 있습니다.

foreach (var client in clientList.OrderBy(c => c.Id).QueryInChunksOf(100))
{
    // do stuff
    context.SaveChanges();
}

이 메소드를 호출 할 수있는 조회 가능 오브젝트를 주문해야합니다. 이는 Entity Framework IQueryable<T>.Skip(int)가 순서가 지정된 쿼리에 대해서만 지원하기 때문에 서로 다른 범위에 대한 여러 쿼리가 순서가 안정적이어야한다는 것을 고려할 때 적합합니다. 순서가 중요하지 않은 경우 클러스터 된 인덱스를 가질 가능성이 있기 때문에 기본 키로 만 주문하십시오.

이 버전은 데이터베이스를 100 개씩 일괄 적으로 쿼리합니다. SaveChanges() . 각 엔터티마다 호출됩니다.

처리량을 대폭 향상 시키려면 전화 SaveChanges()횟수를 줄이십시오. 대신 다음과 같은 코드를 사용하십시오.

foreach (var chunk in clientList.OrderBy(c => c.Id).QueryChunksOfSize(100))
{
    foreach (var client in chunk)
    {
        // do stuff
    }
    context.SaveChanges();
}

결과적으로 데이터베이스 업데이트 호출이 100 배 줄어 듭니다. 물론 각 통화를 완료하는 데 시간이 오래 걸리지 만 결국에는 앞서 나갑니다. 마일리지가 다를 수 있지만 이것은 세계에서 더 빨랐습니다.

그리고 그것은 당신이보고있는 예외를 극복합니다.

편집하다 나는 SQL 프로파일 러를 실행 한 후이 질문을 다시 방문하고 성능을 향상시키기 위해 몇 가지 사항을 업데이트했습니다. 관심있는 사람이라면 여기 DB가 생성 한 내용을 보여주는 샘플 SQL이 있습니다.

첫 번째 루프는 아무것도 건너 뛸 필요가 없으므로 더 간단합니다.

SELECT TOP (100)                     -- the chunk size
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
FROM [dbo].[Clients] AS [Extent1]
ORDER BY [Extent1].[Id] ASC

후속 호출은 이전 결과 덩어리를 건너 뛰어야하므로 다음 사용법을 소개합니다 row_number.

SELECT TOP (100)                     -- the chunk size
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
FROM (
    SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], row_number()
    OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
    FROM [dbo].[Clients] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > 100   -- the number of rows to skip
ORDER BY [Extent1].[Id] ASC


답변

이제 Connect에서 열린 버그 에 대한 공식 답변을 게시했습니다 . 권장되는 해결 방법은 다음과 같습니다.

이 오류는 Entity Framework가 SaveChanges () 호출 중에 암시 적 트랜잭션을 생성하기 때문에 발생합니다. 오류를 해결하는 가장 좋은 방법은 다른 패턴을 사용하거나 (읽는 동안 저장하지 않음) 트랜잭션을 명시 적으로 선언하는 것입니다. 가능한 세 가지 해결책은 다음과 같습니다.

// 1: Save after iteration (recommended approach in most cases)
using (var context = new MyContext())
{
    foreach (var person in context.People)
    {
        // Change to person
    }
    context.SaveChanges();
}

// 2: Declare an explicit transaction
using (var transaction = new TransactionScope())
{
    using (var context = new MyContext())
    {
        foreach (var person in context.People)
        {
            // Change to person
            context.SaveChanges();
        }
    }
    transaction.Complete();
}

// 3: Read rows ahead (Dangerous!)
using (var context = new MyContext())
{
    var people = context.People.ToList(); // Note that this forces the database
                                          // to evaluate the query immediately
                                          // and could be very bad for large tables.

    foreach (var person in people)
    {
        // Change to person
        context.SaveChanges();
    }
} 


답변

실제로 foreachEntity Framework를 사용하여 C # 의 루프 내에서 변경 내용을 저장할 수 없습니다 .

context.SaveChanges() 메소드는 일반 데이터베이스 시스템 (RDMS)에서 커미트처럼 작동합니다.

모든 변경 사항 (Entity Framework가 캐시)을 변경 한 다음 SaveChanges()데이터베이스 커밋 명령과 같이 루프 이후 (외부)를 호출하면 모든 변경 사항을 한 번에 저장 하면됩니다.

모든 변경 사항을 한 번에 저장할 수있는 경우 작동합니다.


답변

context.SaveChanges()당신의 foreach(루프)의 끝을 놓아 두십시오 .


답변

항상 선택 사항을 목록으로 사용

예 :

var tempGroupOfFiles = Entities.Submited_Files.Where(r => r.FileStatusID == 10 && r.EventID == EventId).ToList();

그런 다음 변경 사항을 저장하면서 컬렉션을 반복합니다.

 foreach (var item in tempGroupOfFiles)
             {
                 var itemToUpdate = item;
                 if (itemToUpdate != null)
                 {
                     itemToUpdate.FileStatusID = 8;
                     itemToUpdate.LastModifiedDate = DateTime.Now;
                 }
                 Entities.SaveChanges();

             }


답변

참고 : 책과 일부 줄은 여전히 ​​유효하기 때문에 조정되었습니다.

SaveChanges () 메서드를 호출하면 반복이 완료되기 전에 예외가 발생하면 데이터베이스에 유지 된 모든 변경 내용을 자동으로 롤백하는 트랜잭션이 시작됩니다. 그렇지 않으면 트랜잭션이 커밋됩니다. 특히 많은 수의 엔티티를 업데이트하거나 삭제할 때 반복이 완료된 후가 아니라 각 엔티티 업데이트 또는 삭제 후에 메소드를 적용하려는 유혹이있을 수 있습니다.

모든 데이터가 처리되기 전에 SaveChanges ()를 호출하려고하면 “세션에서 실행중인 다른 스레드가 있으므로 새 트랜잭션이 허용되지 않습니다”예외가 발생합니다. 연결 문자열에서 MARS (Multiple Active Record Sets)를 활성화 한 경우에도 SQL Server에서 SqlDataReader가 열려있는 연결에서 새 트랜잭션을 시작할 수 없기 때문에 예외가 발생합니다 (EF의 기본 연결 문자열은 MARS를 활성화 함)

왜 일이 일어나고 있는지 이해하는 것이 더 좋습니다. 😉