[c#] SqlException da Entity Framework: la nuova transazione non è consentita poiché sono presenti altri thread in esecuzione nella sessione



Answers

Come hai già identificato, non puoi salvare da una foreach che sta ancora pescando dal database tramite un lettore attivo.

Calling ToList() o ToArray() va bene per i piccoli set di dati, ma quando si hanno migliaia di righe, si consumerà una grande quantità di memoria.

È meglio caricare le righe in blocchi.

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++;
        }
    }
}

Dati i metodi di estensione di cui sopra, puoi scrivere la tua query in questo modo:

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

L'oggetto interrogabile su cui si chiama questo metodo deve essere ordinato. Questo perché Entity Framework supporta solo IQueryable<T>.Skip(int) su query ordinate, il che ha senso quando si considera che più query per intervalli diversi richiedono che l'ordine sia stabile. Se l'ordine non è importante per te, basta ordinare per chiave primaria in quanto è probabile che abbia un indice cluster.

Questa versione interrogherà il database in batch di 100. Si noti che SaveChanges() viene chiamato per ogni entità.

Se si desidera migliorare notevolmente il throughput, è consigliabile chiamare SaveChanges() meno frequentemente. Usa invece un codice come questo:

foreach (var chunk in clientList.OrderBy(c => c.Id).QueryChunksOfSize(100))
{
    foreach (var client in chunk)
    {
        // do stuff
    }
    context.SaveChanges();
}

Ciò si traduce in 100 volte meno chiamate di aggiornamento del database. Ovviamente ognuna di queste chiamate richiede più tempo per essere completata, ma alla fine si esce sempre più avanti. Il tuo chilometraggio può variare, ma questo è stato per me più veloce.

E si aggira intorno all'eccezione che stavi vedendo.

EDIT Ho rivisitato questa domanda dopo aver eseguito SQL Profiler e aggiornato alcune cose per migliorare le prestazioni. Per chiunque sia interessato, ecco alcuni esempi di SQL che mostrano cosa viene creato dal DB.

Il primo ciclo non ha bisogno di saltare nulla, quindi è più semplice.

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

Le chiamate successive devono saltare i blocchi di risultati precedenti, quindi introduce l'uso di 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
Question

Attualmente sto ottenendo questo errore:

System.Data.SqlClient.SqlException: la nuova transazione non è consentita perché ci sono altri thread in esecuzione nella sessione.

durante l'esecuzione di questo codice:

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

Modello n. 1 - Questo modello si trova in un database sul nostro server di sviluppo. Modello n. 1 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/bdb2b000-6e60-4af0-a7a1-2bb6b05d8bc1/Model1.png

Modello n. 2 - Questo modello si trova in un database sul nostro server Prod e viene aggiornato ogni giorno da feed automatici. alt text http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/4260259f-bce6-43d5-9d2a-017bd9a980d4/Model2.png

Nota - Gli elementi cerchiati rossi nel Modello # 1 sono i campi che uso per "mappare" al Modello # 2. Si prega di ignorare i cerchi rossi nel Modello n. 2, ovvero da un'altra domanda che ho ricevuto e che ora ha una risposta.

Nota: ho ancora bisogno di inserire un controllo isDeleted in modo da poterlo eliminare facilmente da DB1 se è uscito dall'inventario del nostro cliente.

Tutto quello che voglio fare, con questo particolare codice, è connettere una società in DB1 con un client in DB2, ottenere la loro lista prodotti da DB2 e INSERISCIla in DB1 se non è già lì. La prima volta dovrebbe essere un inventario completo. Ogni volta che viene eseguito lì dopo che nulla dovrebbe accadere a meno che un nuovo inventario è entrato nel feed durante la notte.

Quindi la grande domanda: come posso risolvere l'errore di transazione che sto ottenendo? Devo rilasciare e ricreare il mio contesto ogni volta attraverso i loop (non ha senso per me)?




Quindi nel progetto ho avuto lo stesso identico problema il problema non si trovava in foreach o in .toList() , in realtà era nella configurazione AutoFac che usavamo. Questo ha creato alcune strane situazioni in cui l'errore di cui sopra è stato lanciato ma sono stati lanciati anche altri errori equivalenti.

Questa era la nostra soluzione: cambiato questo:

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

A:

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



Sono molto in ritardo per la festa, ma oggi ho affrontato lo stesso errore e come ho risolto era semplice. Il mio scenario era simile a questo codice dato che stavo facendo transazioni DB all'interno di nested for-each loop.

Il problema è che una transazione con DB singolo richiede un po 'più di tempo rispetto a ogni ciclo, quindi una volta che la transazione precedente non è completa, la nuova trazione genera un'eccezione, quindi la soluzione è creare un nuovo oggetto nel ciclo for-each dove stai facendo una transazione db.

Per gli scenari sopra menzionati la soluzione sarà così:

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



Avevo bisogno di leggere un enorme ResultSet e aggiornare alcuni record nella tabella. Ho provato ad usare pezzi come suggerito nella answer Drew Noakes .

Sfortunatamente dopo 50000 record ho OutofMemoryException. La risposta Entity framework set di dati di grandi dimensioni, fuori eccezione di memoria spiega che

EF crea una seconda copia dei dati che utilizza per il rilevamento delle modifiche (in modo che possa persistere le modifiche al database). EF detiene questo secondo set per tutta la durata del contesto e questo set ti sta facendo uscire di memoria.

La raccomandazione è di rinnovare il tuo contesto ogni lotto.

Quindi ho recuperato i valori Minimo e Massimo della chiave primaria - le tabelle hanno le chiavi primarie come numeri interi autoincrementali. Poi ho recuperato dai blocchi di database dei record aprendo il contesto per ogni blocco. Dopo l'elaborazione, il contesto del blocco si chiude e rilascia la memoria. Garantisce che l'utilizzo della memoria non aumenta.

Di seguito è riportato uno snippet dal mio codice:

  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 è una struttura semplice con proprietà From e To.




So che è una vecchia domanda ma ho affrontato questo errore oggi.

e ho scoperto che, questo errore può essere generato quando un trigger di tabella del database ottiene un errore.

per tua informazione, puoi anche controllare i trigger delle tabelle quando ricevi questo errore.




Stavo anche affrontando lo stesso problema.

Ecco la causa e la soluzione.

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

Assicurati prima di attivare i comandi di manipolazione dei dati come inserti, aggiornamenti, hai chiuso tutti i precedenti lettori SQL attivi.

L'errore più comune sono le funzioni che leggono i dati da db e restituiscono valori. Ad esempio, funzioni come isRecordExist.

In questo caso torniamo immediatamente dalla funzione se troviamo il record e dimentichiamo di chiudere il lettore.




Basta inserire context.SaveChanges() dopo la fine del foreach (loop).




Cordiali saluti: da un libro e alcune linee corrette perché è valido:

Richiamando il metodo SaveChanges () inizia una transazione che ripristina automaticamente tutte le modifiche persistenti nel database se si verifica un'eccezione prima del completamento dell'iterazione; altrimenti la transazione si impegna. Potresti essere tentato di applicare il metodo dopo ogni aggiornamento o eliminazione di entità piuttosto che dopo che l'iterazione è stata completata, specialmente quando stai aggiornando o eliminando un numero elevato di entità.

Se si tenta di richiamare SaveChanges () prima che tutti i dati siano stati elaborati, si incorre in una "Nuova transazione non consentita perché esistono altri thread in esecuzione nella sessione". L'eccezione si verifica perché SQL Server non consente l'avvio di una nuova transazione su una connessione con SqlDataReader aperto, anche con più record di record attivi (MARS) abilitati dalla stringa di connessione (la stringa di connessione predefinita di EF abilita MARS)

A volte è meglio capire perché le cose stanno accadendo ;-)




Related