SqlException от Entity Framework - Новая транзакция не разрешена, поскольку в сеансе запущены другие потоки - PullRequest
549 голосов
/ 22 января 2010

Я сейчас получаю эту ошибку:

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

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

Ответы [ 19 ]

628 голосов
/ 02 февраля 2010

После долгих выдергиваний из волос я обнаружил, что петли 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;
    // ...
}
251 голосов
/ 11 октября 2010

Как вы уже определили, вы не можете сохранить из 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 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
120 голосов
/ 06 октября 2011

Мы опубликовали официальный ответ на ошибку, открытую в 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();
    }
} 
14 голосов
/ 06 июля 2017

Просто поставьте context.SaveChanges() после окончания вашего foreach (цикла).

13 голосов
/ 25 июня 2018

Действительно, вы не можете сохранить изменения внутри цикла foreach в C #, используя Entity Framework.

context.SaveChanges() метод действует как коммит в обычной системе баз данных (RDMS).

Просто внесите все изменения (которые Entity Framework будет кэшировать), а затем сохраните их все сразу, вызывая SaveChanges() после цикла (вне его), как команда фиксации базы данных.

Это работает, если вы можете сохранить все изменения сразу.

7 голосов
/ 13 января 2014

К вашему сведению: из книги и некоторых строк откорректировано, потому что все еще действует:

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

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

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

5 голосов
/ 19 сентября 2017

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

Например:

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

             }
4 голосов
/ 25 мая 2011

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

3 голосов
/ 26 августа 2016

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

Первый вариант - использовать один 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, но выбрать только идентификаторы. Затем выполните итерацию по списку идентификаторов (предположительно, int), получите объект, соответствующий каждому int, и таким образом вызовите SaveChanges (). Идея этого метода заключается в получении большого списка целых чисел, гораздо более эффективном, чем получение большого списка объектов БД и вызов .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()
}
2 голосов
/ 11 июня 2016

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

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

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.

...