SqlException из Entity Framework - новая транзакция не допускается, поскольку в сеансе есть другие потоки

В настоящее время я получаю эту ошибку:

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 Server. Модель # 1 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/bdb2b000-6e60-4af0-a7a1-2bb6b05d8bc1/Model1.png

Модель № 2 - эта модель находится в базе данных на нашем Prod Server и обновляется каждый день автоматическими каналами. 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 и INSERT в DB1, если оно еще не существует. Первый раз через полный поток инвентаря. Каждый раз, когда он запускается там, после того, как ничего не случится, если в ночное время не появится новый инвентарь.

Итак, большой вопрос - как я могу решить ошибку транзакции, которую я получаю? Нужно ли мне каждый раз пересказывать и обновлять мой контекст через циклы (не имеет смысла для меня)?

Ответ 1

После многого вытягивания волос я обнаружил, что петли 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;
    // ...
}

Ответ 2

Как вы уже определили, вы не можете сохранить из 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 раз меньшему количеству запросов на обновление базы данных. Конечно, каждый из этих вызовов занимает больше времени, но вы все равно выходите вперед в конце. Ваш пробег может отличаться, но это было быстрее для меня.

И он обойдет исключение, которое вы видели.

EDIT. Я повторно рассмотрел этот вопрос после запуска SQL Profiler и обновил несколько вещей, чтобы повысить производительность. Для любого, кто интересуется, вот несколько примеров 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

Ответ 3

Теперь мы опубликовали официальный ответ на ошибку, открытую в 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();
    }
} 

Ответ 4

Просто поместите context.SaveChanges() после конца foreach (цикл).

Ответ 5

У меня была такая же проблема, но в другой ситуации. У меня был список элементов в списке. Пользователь может щелкнуть элемент и выбрать удаление, но я использую сохраненный proc для удаления элемента, потому что при удалении элемента существует много логики. Когда я вызываю сохраненную proc, удаление работает нормально, но любой будущий вызов SaveChanges вызывает ошибку. Мое решение состояло в том, чтобы вызвать хранимую процедуру вне EF, и это сработало нормально. По какой-то причине, когда я вызываю хранимую процедуру, используя способ EF делать что-то, он оставляет что-то открытое.

Ответ 6

FYI: из книги и некоторых строк, скорректированных, потому что ее действительность действительна:

Вызов метода SaveChanges() начинает транзакцию, которая автоматически откатывает все изменения, сохраняемые в базе данных, если возникает исключение до завершения итерации; в противном случае транзакция завершается. Возможно, у вас возникнет соблазн применить метод после каждого обновления или удаления сущности, а не после завершения итерации, особенно когда вы обновляете или удаляете массивные числа объектов.

Если вы попытаетесь вызвать SaveChanges() перед тем, как все данные будут обработаны, вы понесете "Новая транзакция не допускается из-за исключения других потоков, запущенных в сеансе". Исключение возникает из-за того, что SQL Server не разрешает запускать новую транзакцию при подключении, в котором открыт SqlDataReader, даже с несколькими включенными наборами записей (MARS), подключенными по строке подключения (строка подключения по умолчанию EF включает MARS)

Иногда лучше понять, почему все происходит; -)

Ответ 7

Итак, в проекте у меня была такая же проблема, проблема была не в foreach или .toList(), это было фактически в конфигурации AutoFac, которую мы использовали. Это создало некоторые странные ситуации, когда была вышеназванная ошибка, но также была выброшена куча других эквивалентных ошибок.

Это было наше исправление: Изменено:

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

To:

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

Ответ 8

Всегда используйте свой выбор в виде списка

Например:

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

             }

Ответ 9

У меня также была проблема с тем же.

Вот причина и решение.

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

Перед запуском команд обработки данных, таких как вставки, обновления, убедитесь, что вы закрыли все предыдущие активные читатели SQL.

Наиболее распространенной ошибкой являются функции, которые считывают данные из db и возвращаемых значений. Например, такие функции, как isRecordExist.

В этом случае мы немедленно возвращаемся от функции, если нашли запись и забыли закрыть читателя.

Ответ 10

В моем случае проблема возникла, когда я вызвал Хранимую процедуру через 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
   }
}

Ответ 11

Мне нужно было прочитать огромный ResultSet и обновить некоторые записи в таблице. Я попытался использовать куски, как было предложено в Drew Noakes .

К сожалению, после 50000 записей у меня есть OutofMemoryException. Ответ Большой набор данных инфраструктуры Entity, исключение из памяти объясняет, что

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.

Ответ 12

Вот еще 2 варианта, которые позволяют вам вызывать SaveChanges() в для каждого цикла.

Первым вариантом является использование одного DBContext для генерации объектов списка для перебора, а затем создания второго DBC-контекста для вызова 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 (предположительно int) и получаем объект, соответствующий каждому int, и вызываем SaveChanges() таким образом. Идея этого метода - захват большого списка целых чисел, намного эффективнее, чем получение большого списка объектов db и вызов .ToList() для всего объекта. Вот пример этого метода:

//Get the list of objects you want from your DBContext, and select just the Id 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()
}

Ответ 13

Код ниже работает для меня:

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

Ответ 14

Я очень опаздываю на вечеринку, но сегодня я столкнулся с такой же ошибкой, и как я решил, было просто. Мой сценарий был похож на этот код, в котором я делал транзакции DB внутри вложенных для каждого цикла.

Проблема заключается в том, что транзакция Single DB занимает немного больше времени, чем для каждого цикла, поэтому, как только предыдущая транзакция не будет завершена, новое тяга генерирует исключение, поэтому решение заключается в создании нового объекта в -each loop, где вы делаете транзакцию db.

Для вышеупомянутых сценариев решение будет таким:

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 ### //
                    }
                }

Ответ 15

Я немного опоздал, но у меня тоже была эта ошибка. Я решил проблему, проверив, в каких местах значения, где происходит обновление.

Я узнал, что мой запрос был неправильным и что там, где еще 250+ исправлений. Поэтому я исправил свой запрос, и теперь он работает правильно.

Итак, в моей ситуации: Проверьте запрос на ошибки, отлаживая результат, возвращаемый запросом. После этого исправьте запрос.

Надеюсь, это поможет решить будущие проблемы.

Ответ 16

Я знаю, что это старый вопрос, но сегодня я столкнулся с этой ошибкой.

и я обнаружил, что эту ошибку можно вызвать, когда триггер таблицы базы данных получает ошибку.

для вашей информации, вы можете также проверить свои триггеры таблиц, когда вы получите эту ошибку.