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

时间:2010-01-21 22:37:15

标签: 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;
        }
    }
}

Model#1 - 该模型位于Dev Server的数据库中。 Model #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中。第一次通过应该是一个完整的库存。每次运行之后都不会发生任何事情,除非饲料中的新库存过夜。

  

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

21 个答案:

答案 0 :(得分:627)

在拔掉头发后,我发现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;
    // ...
}

答案 1 :(得分:251)

正如您已经确定的那样,您无法从仍通过有效阅读器从数据库中提取的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();
}

必须对您调用此方法的可查询对象进行排序。这是因为实体框架仅在有序查询上支持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事件探查器后重新审视了这个问题并更新了一些内容以提高性能。对于任何感兴趣的人,这里有一些示例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

答案 2 :(得分:119)

我们现已发布对the bug opened on 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();
    }
} 

答案 3 :(得分:14)

context.SaveChanges()(循环)结束后放置foreach

答案 4 :(得分:10)

实际上,您无法使用Entity Framework将更改保存在C#中的foreach循环内。

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

只需进行所有更改(Entity Framework将缓存的内容),然后在循环之后(在其外部)一次调用SaveChanges()即可保存所有更改,就像数据库提交命令一样。

如果您可以一次保存所有更改,则可以使用此功能。

答案 5 :(得分:7)

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

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

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

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

答案 6 :(得分:4)

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

答案 7 :(得分:4)

始终将您的选择用作列表

例如:

let

然后在保存更改的同时循环收集

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

答案 8 :(得分:3)

以下是另外两个选项,允许您在每个循环中调用SaveChanges()。

第一个选项是使用一个DBContext生成要迭代的列表对象,然后创建第二个DBContext以调用SaveChanges()。这是一个例子:

//Get your IQueryable list of objects from your main DBContext(db)    
IQueryable<Object> objects = db.Object.Where(whatever where clause you desire);

//Create a new DBContext outside of the foreach loop    
using (DBContext dbMod = new DBContext())
{   
    //Loop through the IQueryable       
    foreach (Object object in objects)
    {
        //Get the same object you are operating on in the foreach loop from the new DBContext(dbMod) using the objects id           
        Object objectMod = dbMod.Object.Find(object.id);

        //Make whatever changes you need on objectMod
        objectMod.RightNow = DateTime.Now;

        //Invoke SaveChanges() on the dbMod context         
        dbMod.SaveChanges()
    }
}

第二个选项是从DBContext获取数据库对象列表,但只选择id。然后遍历id列表(可能是一个int)并获取对应于每个int的对象,并以这种方式调用SaveChanges()。这个方法背后的想法是抓取一个大的整数列表,比获取大量的db对象并在整个对象上调用.ToList()要高效得多。以下是此方法的示例:

//Get the list of objects you want from your DBContext, and select just the Id's and create a list
List<int> Ids = db.Object.Where(enter where clause here)Select(m => m.Id).ToList();

var objects = Ids.Select(id => db.Objects.Find(id));

foreach (var object in objects)
{
    object.RightNow = DateTime.Now;
    db.SaveChanges()
}

答案 9 :(得分:2)

我需要读取一个巨大的ResultSet并更新表中的一些记录。 我尝试按照Drew Noakes answer中的建议使用块。

不幸的是,在50000条记录之后我得到了OutofMemoryException。 答案Entity framework large data set, out of memory exception解释说,

  

EF创建第二个数据副本,用于更改检测(所以   它可以持续更改数据库)。 EF持有第二盘   对于上下文的生命周期及其设置会让你失望   记忆。

建议每批更新您的上下文。

所以我检索了主键的最小值和最大值 - 表将主键作为自动增量整数。然后我通过打开每个块的上下文从数据库块中检索。处理完块上下文后,关闭并释放内存。它确保内存使用率不会增长。

以下是我的代码中的代码段:

  public void ProcessContextByChunks ()
  {
        var tableName = "MyTable";
         var startTime = DateTime.Now;
        int i = 0;
         var minMaxIds = GetMinMaxIds();
        for (int fromKeyID= minMaxIds.From; fromKeyID <= minMaxIds.To; fromKeyID = fromKeyID+_chunkSize)
        {
            try
            {
                using (var context = InitContext())
                {   
                    var chunk = GetMyTableQuery(context).Where(r => (r.KeyID >= fromKeyID) && (r.KeyID < fromKeyID+ _chunkSize));
                    try
                    {
                        foreach (var row in chunk)
                        {
                            foundCount = UpdateRowIfNeeded(++i, row);
                        }
                        context.SaveChanges();
                    }
                    catch (Exception exc)
                    {
                        LogChunkException(i, exc);
                    }
                }
            }
            catch (Exception exc)
            {
                LogChunkException(i, exc);
            }
        }
        LogSummaryLine(tableName, i, foundCount, startTime);
    }

    private FromToRange<int> GetminMaxIds()
    {
        var minMaxIds = new FromToRange<int>();
        using (var context = InitContext())
        {
            var allRows = GetMyTableQuery(context);
            minMaxIds.From = allRows.Min(n => (int?)n.KeyID ?? 0);  
            minMaxIds.To = allRows.Max(n => (int?)n.KeyID ?? 0);
        }
        return minMaxIds;
    }

    private IQueryable<MyTable> GetMyTableQuery(MyEFContext context)
    {
        return context.MyTable;
    }

    private  MyEFContext InitContext()
    {
        var context = new MyEFContext();
        context.Database.Connection.ConnectionString = _connectionString;
        //context.Database.Log = SqlLog;
        return context;
    }

FromToRange是一个包含From和To属性的简单结构。

答案 10 :(得分:2)

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

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

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

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

_context.Configuration.EnsureTransactionsForFunctionsAndCommands = false;

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

DbContextConfiguration.EnsureTransactionsForFunctionsAndCommands Property

答案 11 :(得分:2)

所以在项目中,我有这个完全相同的问题,问题不在foreach.toList()实际上我们使用的AutoFac配置中。 这创造了一些奇怪的情况,上面的错误被抛出,但也抛出了一堆其他等效的错误。

这是我们的修复: 改变了这个:

container.RegisterType<DataContext>().As<DbContext>().InstancePerLifetimeScope();
container.RegisterType<DbFactory>().As<IDbFactory>().SingleInstance();
container.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest();

要:

container.RegisterType<DataContext>().As<DbContext>().As<DbContext>();
container.RegisterType<DbFactory>().As<IDbFactory>().As<IDbFactory>().InstancePerLifetimeScope();
container.RegisterType<UnitOfWork>().As<IUnitOfWork>().As<IUnitOfWork>();//.InstancePerRequest();

答案 12 :(得分:1)

在我的情况下,当我通过EF调用存储过程时出现问题,然后SaveChanges抛出此异常。问题是在调用程序时,调查员没有被处理掉。我按照以下方式修改了代码:

public bool IsUserInRole(string username, string roleName, DataContext context)
{          
   var result = context.aspnet_UsersInRoles_IsUserInRoleEF("/", username, roleName);

   //using here solved the issue
   using (var en = result.GetEnumerator()) 
   {
     if (!en.MoveNext())
       throw new Exception("emty result of aspnet_UsersInRoles_IsUserInRoleEF");
     int? resultData = en.Current;

     return resultData == 1;//1 = success, see T-SQL for return codes
   }
}

答案 13 :(得分:1)

如果由于foreach而出现此错误,并且您确实需要首先在循环内保存一个实体并在循环中进一步使用生成的标识,就像我的情况一样,最简单的解决方案是使用另一个DBContext插入将返回的实体ID并在外部环境中使用此ID

例如

    using (var context = new DatabaseContext())
    {
        ...
        using (var context1 = new DatabaseContext())
        {
            ...
               context1.SaveChanges();
        }                         
        //get id of inserted object from context1 and use is.   
      context.SaveChanges();
   }

答案 14 :(得分:1)

我也面临同样的问题。

这是原因和解决方案。

http://blogs.msdn.com/b/cbiyikoglu/archive/2006/11/21/mars-transactions-and-sql-error-3997-3988-or-3983.aspx

确保在触发插入,更新等数据操作命令之前,您已关闭所有以前的活动SQL阅读器。

最常见的错误是从db读取数据并返回值的函数。 对于像isRecordExist这样的函数。

在这种情况下,如果我们找到记录并忘记关闭阅读器,我们会立即从函数返回。

答案 15 :(得分:0)

我迟到了,但今天我遇到了同样的错误,我的解决方法很简单。我的场景类似于给定的代码我在嵌套for-each循环中进行数据库事务。

问题是单个数据库事务比每个循环花费的时间要长一些,所以一旦先前的事务没有完成,那么新的牵引会引发异常,因此解决方案是在for中创建一个新对象-each循环,您正在进行数据库事务。

对于上述场景,解决方案将如下:

foreach (RivWorks.Model.Negotiation.AutoNegotiationDetails companyFeedDetail in companyFeedDetailList)
                {
private RivWorks.Model.Negotiation.RIV_Entities _dbRiv = RivWorks.Model.Stores.RivEntities(AppSettings.RivWorkEntities_connString);
                    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 ### //
                    }
                }

答案 16 :(得分:0)

我有点晚了,但我也有这个错误。我通过检查更新值的位置来解决问题。

我发现我的查询错误,并且有250多个编辑待处理。所以我更正了我的查询,现在它的工作正确。

  

所以在我的情况下:通过调试查询返回的结果来检查查询是否有错误。之后更正了查询。

希望这有助于解决未来的问题。

答案 17 :(得分:0)

我知道这是一个老问题,但我今天遇到了这个错误。

我发现,当数据库表触发器出错时,可能会抛出此错误。

为了您的信息,当您收到此错误时,您也可以检查您的表触发器。

答案 18 :(得分:0)

以下代码适用于我:

private pricecheckEntities _context = new pricecheckEntities();

...

private void resetpcheckedtoFalse()
{
    try
    {
        foreach (var product in _context.products)
        {
            product.pchecked = false;
            _context.products.Attach(product);
            _context.Entry(product).State = EntityState.Modified;
        }
        _context.SaveChanges();
    }
    catch (Exception extofException)
    {
        MessageBox.Show(extofException.ToString());

    }
    productsDataGrid.Items.Refresh();
}

答案 19 :(得分:0)

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

答案 20 :(得分:0)

最近我在我的项目中遇到了同样的问题,所以发布我的经验,它可能会帮助一些和我在同一条船上的人。问题是由于我正在遍历 EF 选择查询的结果(结果未检索到内存中)。

var products = (from e in _context.Products
                              where e.StatusId == 1
                              select new { e.Name, e.Type });

        foreach (var product in products)
        {
           //doing some insert EF Queries
           //some EF select quries
            await _context.SaveChangesAsync(stoppingToken); // This code breaks.
        }

我已经更新了我的产品选择查询,将结果放入 LIST 而不是 IQueryable(这似乎为每个循环打开了整个阅读器,因此保存失败)。

 var products = (from e in _context.Products
                              where e.StatusId == 1
                              select new { e.Name, e.Type })**.ToList()**; //see highlighted