来自实体框架的SqlException - 不允许新事务,因为会话中还有其他线程在运行

Kei*_*ows 571 c# entity-framework transactions inversion-of-control

我目前收到此错误:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

模型#1 - 该模型位于我们的Dev Server上的数据库中. 模型#1 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/bdb2b000-6e60-4af0-a7a1-2bb6b05d8bc1/Model1.png

模型#2 - 该模型位于我们的Prod服务器上的数据库中,并且每天通过自动提要进行更新. alt text http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/4260259f-bce6-43d5-9d2a-017bd9a980d4/Model2.png

注 - 模型#1中带红色圆圈的项目是我用来"映射"到模型#2的字段.请忽略模型#2中的红色圆圈:这是我现在回答的另一个问题.

注意:我仍然需要输入一个isDeleted检查,以便我可以从DB1中删除它,如果它已经超出了我们客户的库存.

我想用这个特定的代码做的就是将DB1中的公司与DB2中的客户端连接起来,从DB2获取产品列表,如果它不存在则将其插入DB1中.第一次通过应该是一个完整的库存.每次运行之后都不会发生任何事情,除非饲料中的新库存过夜.

所以最大的问题是 - 如何解决我遇到的交易错误?每次通过循环时我是否需要删除并重新创建我的上下文(对我来说没有意义)?

Kei*_*ows 649

经过大量拔毛后,我发现这些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;
    // ...
}
Run Code Online (Sandbox Code Playgroud)

  • 问题是,当你仍然从数据库中提取结果时,你不能调用`SaveChanges`.因此,另一种解决方案是在循环完成后保存更改. (144认同)
  • 我们的开发人员倾向于将.ToList()附加到任何LINQ查询而不考虑后果.这必须是第一次追加.ToList()非常有用! (35认同)
  • 是的,这也让我很头疼.当我发现问题时,我差点从椅子上掉下来!我理解这个问题背后的技术原因,但这并不直观,也没有帮助开发人员陷入"成功的陷阱"http://blogs.msdn.com/brada/archive/2003/10/ 02/50420.aspx (14认同)
  • 对于大型数据集的性能来说,这不是很糟糕吗?如果表中有数百万条记录.ToList()会将它们全部吸入内存中.我遇到了这个问题,并想知道以下是否可行a)分离实体b)创建一个新的ObjectContext并将分离的实体附加到它.c)在新的ObjectContext上调用SaveChanges()d)从新的ObjectContext中分离实体e)将它附加回旧的ObjectContext (8认同)
  • 在被咬过之后,我将其添加到Microsoft Connect:https://connect.microsoft.com/VisualStudio/feedback/details/612369/misleading-error-message-in-sqlexception-when-using-entity-framework随意投票吧. (4认同)

Dre*_*kes 258

正如您已经确定的那样,您无法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++;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

鉴于上述扩展方法,您可以像这样编写查询:

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

必须对您调用此方法的可查询对象进行排序. 这是因为实体框架仅支持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();
}
Run Code Online (Sandbox Code Playgroud)

这导致数据库更新调用减少了100倍.当然,这些电话中的每一个都需要更长的时间才能完成,但最终你还是会走在前面.你的里程可能会有所不同,但这对我来说世界更快.

它绕过你所看到的例外.

编辑我在运行SQL事件探查器后重新审视了这个问题并更新了一些内容以提高性能.对于任何感兴趣的人,这里有一些示例SQL,它显示了DB创建的内容.

第一个循环不需要跳过任何东西,因此更简单.

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

后续调用需要跳过以前的结果块,因此引入了以下用法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
Run Code Online (Sandbox Code Playgroud)

  • 谢谢.您的解释比标记为"已回答"的解释更有用. (17认同)
  • 这很棒。只有一件事:如果您正在查询一列并更新该列的值,您需要注意 chunkNumber++;。假设您有一列“ModifiedDate”并且您正在查询 .Where(x=&gt; x.ModifiedDate != null),并且在 foreach 末尾为 ModifiedDate 设置一个值。通过这种方式,您不会迭代一半的记录,因为一半的记录被跳过。 (2认同)

Mar*_*SFT 121

我们现在已对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();
    }
} 
Run Code Online (Sandbox Code Playgroud)

  • 如果你采用事务路由,只是抛出一个TransactionScope可能无法解决它 - 如果你正在做的事可能需要很长时间,不要忘记延长超时 - 例如,如果你将以交互方式调试代码,那么数据库调用.这是将事务超时扩展到一小时的代码:using(var transaction = new TransactionScope(TransactionScopeOption.Required,new TimeSpan(1,0,0))) (6认同)

Maj*_*jid 17

放在context.SaveChanges()你的foreach(循环)结束后.

  • 这并不总是一种选择。 (3认同)

Woj*_*ryn 16

将您的可查询列表设置为 .ToList() 并且它应该可以正常工作。

  • 请提供示例,而不仅仅是发布解决方案。 (2认同)

小智 15

实际上,您无法foreach使用Entity Framework在C#中的环路中保存更改.

context.SaveChanges() 方法就像在常规数据库系统(RDMS)上提交一样.

只需进行所有更改(实体框架将缓存),然后SaveChanges()在循环(在其外部)之后立即保存所有更改,就像数据库提交命令一样.

如果您可以一次保存所有更改,则此方法有效.

  • 我认为在这里看到“常规数据库系统(RDMS)”很有趣 (2认同)

Her*_*lom 7

仅供参考:从一本书和一些行调整,因为它的stil有效:

调用SaveChanges()方法开始一个事务,如果在迭代完成之前发生异常,它会自动回滚持久存储到数据库的所有更改; 否则交易提交.您可能想要在每次实体更新或删除之后而不是在迭代完成之后应用该方法,尤其是在您更新或删除大量实体时.

如果在处理完所有数据之前尝试调用SaveChanges(),则会产生"不允许新事务,因为会话中有其他线程运行"异常.发生异常是因为SQL Server不允许在打开SqlDataReader的连接上启动新事务,即使连接字符串启用多个活动记录集(MARS)(EF的默认连接字符串启用MARS)

有时候更好地理解事情发生的原因;-)


mzo*_*erz 6

一律使用您的选择作为列表

例如:

var tempGroupOfFiles = Entities.Submited_Files.Where(r => r.FileStatusID == 10 && r.EventID == EventId).ToList();
Run Code Online (Sandbox Code Playgroud)

然后循环浏览集合,同时保存更改

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

             }
Run Code Online (Sandbox Code Playgroud)

  • 这根本不是一个好习惯。如果您不需要,您不应该经常执行 SaveChanges,并且您绝对不应该“始终将您的选择用作列表” (4认同)

Mik*_*lls 5

我遇到了同样的问题,但情况有所不同。我在列表框中有一个项目列表。用户可以单击一个项目并选择Delete(删除),但是我正在使用存储的proc来删除该项目,因为删除该项目涉及很多逻辑。当我调用存储的proc时,删除工作正常,但是以后再调用SaveChanges都会导致错误。我的解决方案是在EF外部调用存储的proc,这很好用。出于某种原因,当我使用EF的处理方式调用存储的proc时,会留下一些空白。

  • 最近有类似的问题:在我的情况下,原因是存储过程中的“ SELECT”语句产生了空的结果集,如果未读取该结果集,则“ SaveChanges”会抛出该异常。 (3认同)

Jam*_*kle 5

从 EF5 迁移到 EF6 后,我们开始看到此错误“不允许新事务,因为会话中正在运行其他线程” 。

谷歌把我们带到了这里,但我们并没有SaveChanges()在循环内调用。在从数据库读取数据的 foreach 循环中使用 ObjectContext.ExecuteFunction 执行存储过程时,会引发错误。

对 ObjectContext.ExecuteFunction 的任何调用都会将该函数包装在事务中。在已经有打开的读取器的情况下开始事务会导致错误。

可以通过设置以下选项来禁用事务中的 SP 包装。

_context.Configuration.EnsureTransactionsForFunctionsAndCommands = false;
Run Code Online (Sandbox Code Playgroud)

EnsureTransactionsForFunctionsAndCommands选项允许 SP 在不创建自己的事务的情况下运行,并且不再引发错误。

DbContextConfiguration.EnsureTransactionsForFunctionsAndCommands 属性