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




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

Considerati 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 il mondo più veloce per me.

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
c# entity-framework transactions inversion-of-control

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)?




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




Stavo ricevendo lo stesso problema ma in una situazione diversa. Ho avuto un elenco di elementi in una casella di riepilogo. L'utente può fare clic su un elemento e selezionare Elimina, ma sto utilizzando un processo memorizzato per eliminare l'elemento perché c'è molta logica nell'eliminazione dell'elemento. Quando chiamo il proc memorizzato, l'eliminazione funziona bene, ma qualsiasi chiamata futura a SaveChanges causerà l'errore. La mia soluzione era di chiamare il proc memorizzato al di fuori di EF e questo ha funzionato bene. Per qualche ragione quando chiamo il proc memorizzato usando il modo EF di fare qualcosa, lascia qualcosa di aperto.




Ecco altre 2 opzioni che ti permettono di invocare SaveChanges () in a per ogni ciclo.

La prima opzione è utilizzare un DBContext per generare gli oggetti elenco da scorrere, quindi creare un secondo DBContext per chiamare SaveChanges (). Ecco un esempio:

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

La seconda opzione è ottenere un elenco di oggetti di database da DBContext, ma selezionare solo gli id. E poi scorrere l'elenco di id (presumibilmente un int) e ottenere l'oggetto corrispondente a ogni int e invocare SaveChanges () in questo modo. L'idea alla base di questo metodo è l'acquisizione di un ampio elenco di numeri interi, è molto più efficiente di ottenere un grande elenco di oggetti db e chiamare .ToList () sull'intero oggetto. Ecco un esempio di questo metodo:

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



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



Nel mio caso, il problema è apparso quando ho chiamato Stored Procedure tramite EF e successivamente SaveChanges ha lanciato questa eccezione. Il problema era nel chiamare la procedura, l'enumeratore non era disposto. Ho corretto il codice in questo modo:

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



Il codice qui sotto funziona per me:

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



Sono un po 'in ritardo, ma ho avuto anche questo errore. Ho risolto il problema controllando quali sono i valori che dovevano essere aggiornati.

Ho scoperto che la mia query era errata e che c'erano oltre 250 edizioni in sospeso. Quindi ho corretto la mia richiesta e ora funziona correttamente.

Quindi, nella mia situazione: controlla la ricerca di errori, eseguendo il debug sul risultato restituito dalla query. Dopo quello correggi la domanda.

Spero che questo aiuti a risolvere i problemi futuri.




Se ottieni questo errore a causa di foreach e hai davvero bisogno di salvare prima un'entità all'interno del loop e utilizzare ulteriormente l'identità generata in loop, come nel mio caso, la soluzione più semplice è usare un altro DBContext per inserire un'entità che restituirà Id e userà questo ID nel contesto esterno

Per esempio

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



Related