Entity Framework中的SqlException - 因为会话中有其他线程正在运行,所以不允许新的事务。

679

我目前遇到了这个错误:

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 - 此模型位于我们的 Dev 服务器上的数据库中。 模型#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 中,则将其插入到 DB1 中。 第一次运行应该是完全提取库存。 之后每次运行都不应发生任何事情,除非新的库存在夜间供应中出现。

  

那么最重要的问题是 - 我如何解决我正在遇到的事务错误? 我需要在循环中每次放弃并重新创建我的上下文吗(这对我来说没有意义)?


8
这是我见过的最详细的问题。 - user220583
14
有人还想念存储过程吗? - David
23个回答

4
如果你因为使用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();
   }

4

这里有另外两个选项,可以让你在for each循环中调用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()
}

这是一个我想到并实现的好选择,但需要得到点赞。注意:i)你可以将其作为可枚举对象进行迭代,这对于非常大的集合非常有用;ii)你可以使用NoTracking命令来避免加载太多记录而出现问题(如果这是你的情况);iii)我也非常喜欢仅使用主键的选项 - 这非常聪明,因为你将少量数据加载到内存中,但不必在潜在动态底层数据集上处理Take / Skip。 - Kind Contributor

2

我需要读取一个巨大的ResultSet并更新表中的一些记录。我尝试使用Drew Noakes答案中建议的分块方法。

不幸的是,在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 属性。


我没看到你是如何“更新”你的上下文的。看起来你只是为每个块创建了一个新的上下文。 - Suncat2000
@Suncat2000,你是对的,上下文应该是短暂的对象。https://stackoverflow.com/questions/43474112/entity-framework-long-living-data-context-vs-short-living-data-context/43474208#43474208 - Michael Freidgeim

2
在我的情况下,当我通过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
   }
}

2
我也遇到了同样的问题。
以下是原因和解决方法。

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

确保在执行数据操作命令(如插入、更新)之前,关闭所有先前活动的SQL读取器。
最常见的错误是从数据库中读取数据并返回值的函数。例如像isRecordExist这样的函数。
在这种情况下,如果我们找到了记录并忘记关闭读取器,则会立即从函数返回。

8
在 Entity Framework 中,“close a reader”的意思是关闭查询结果的读取器。在像以下查询中: var result = from customer in myDb.Customers where customer.Id == customerId select customer; 返回结果之前,没有可见的读取器。 - Anthony
1
@Anthony 正如其他答案所说,如果您使用EF枚举LINQ查询(IQueryable),则底层的DataReader将保持打开状态,直到迭代完最后一行。但是,尽管在连接字符串中启用MARS是一个重要的功能,但OP中的问题仍然不能仅通过MARS解决。问题在于在底层DataReader仍然打开时尝试保存更改。 - Kind Contributor

2

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

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

有关你的信息,在你遇到这个错误时,你也可以检查你的表触发器。


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

你能详细阐述一下你认为问题是什么吗?你是通过每次创建一个新的Dbcontext来解决这个问题的吗? - eran otzap

2

最近,在我的项目中,我遇到了同样的问题,因此分享一下我的经验,希望能帮助一些和我一样的人。这个问题是由于我正在循环遍历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

1

我的情况与其他人类似。我有一个IQueryable,我正在其中进行foreach循环。这反过来调用了一个带有SaveChanges()方法的方法。由于上面的查询已经打开了一个事务,所以这里出现了异常。

// Example:

var myList = _context.Table.Where(x => x.time == null);

foreach(var i in myList)
{
    MyFunction(i); // <<-- Has _context.SaveChanges() which throws exception
}

在我的情况下,将ToList()添加到查询的末尾是解决方案。
// Fix
var myList = _context.Table.Where(x => x.time == null).ToList();

1
以下代码对我有效:

以下是代码:

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

2
欢迎来到SO!考虑添加解释和/或链接,描述为什么这对您有用。仅代码答案通常被认为不是SO的高质量答案。 - codeMagic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接