web-dev-qa-db-fra.com

SqlException from Entity Framework - La nouvelle transaction n'est pas autorisée car d'autres threads sont en cours d'exécution dans la session.

Je reçois actuellement cette erreur:

System.Data.SqlClient.SqlException: la nouvelle transaction n'est pas autorisée car d'autres threads sont en cours d'exécution dans la session.

en exécutant ce code:

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

Modèle n ° 1 - Ce modèle figure dans une base de données sur notre serveur de développement . Modèle n ° 1 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/bdb2b000-6e60-4af0 -a7a1-2bb6b05d8bc1/Model1.png

Modèle n ° 2 - Ce modèle se trouve dans une base de données sur notre serveur de production et est mis à jour quotidiennement par des flux automatiques. alt text http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/4260259f-bce6-43d5-9d2a-017bd9a980d4/Model2.png

Remarque - Les éléments entourés en rouge dans le modèle n ° 1 sont les champs que j'utilise pour "mapper" sur le modèle n ° 2. Veuillez ignorer les cercles rouges dans le modèle n ° 2: il s'agit d'une autre question que j'avais à laquelle vous avez maintenant répondu.

Remarque: Je dois encore mettre un chèque isDeleted afin de pouvoir le supprimer de DB1 en douceur s'il est sorti de l'inventaire de notre client.

Tout ce que je veux faire, avec ce code particulier, est de connecter une société de DB1 à un client de DB2, d'obtenir sa liste de produits auprès de DB2 et de l'insérer dans DB1 si elle ne s'y trouve pas déjà. La première fois devrait être un tirage complet de l'inventaire. Chaque fois qu'il fonctionne là-bas, rien ne devrait se produire sauf si un nouvel inventaire est entré dans le fourrage pendant la nuit.

Alors la grande question - comment résoudre l'erreur de transaction que je reçois? Dois-je abandonner et recréer mon contexte à chaque fois à travers les boucles (cela n'a pas de sens pour moi)?

528
Keith Barrows

Après avoir tiré beaucoup de cheveux, j'ai découvert que les boucles foreach étaient les coupables. Ce qui doit arriver, c'est d'appeler EF mais de le retourner dans un IList<T> de ce type de cible, puis de le boucler sur le IList<T>.

Exemple:

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;
    // ...
}
605
Keith Barrows

Comme vous l'avez déjà identifié, vous ne pouvez pas enregistrer à partir d'une foreach qui tire toujours de la base de données via un lecteur actif.

L'appel de ToList() ou ToArray() convient pour de petits ensembles de données, mais lorsque vous avez des milliers de lignes, vous consommez une grande quantité de mémoire.

Il est préférable de charger les lignes en morceaux.

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

Étant donné les méthodes d'extension ci-dessus, vous pouvez écrire votre requête comme ceci:

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

L'objet interrogeable sur lequel vous appelez cette méthode doit être commandé. Ceci est dû au fait qu'Entity Framework ne prend en charge que IQueryable<T>.Skip(int) pour les requêtes ordonnées, ce qui est logique lorsque vous considérez que plusieurs requêtes pour différentes plages nécessitent que l'ordre soit stable. Si l'ordre ne vous intéresse pas, il vous suffit de classer par clé primaire, car il est probable que vous disposiez d'un index clusterisé.

Cette version interrogera la base de données par lots de 100. Notez que SaveChanges() est appelé pour chaque entité.

Si vous souhaitez améliorer considérablement votre débit, vous devez appeler SaveChanges() moins souvent. Utilisez un code comme celui-ci à la place:

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

Cela se traduit par 100 fois moins d'appels de mise à jour de base de données. Bien sûr, chacun de ces appels prend plus de temps, mais vous arrivez toujours en avance. Votre kilométrage peut varier, mais cela a été plus rapide pour moi.

Et cela contourne l'exception que vous avez vue.

EDITJe suis revenu sur cette question après avoir exécuté SQL Profiler et mis à jour quelques éléments pour améliorer les performances. Pour ceux qui sont intéressés, voici un exemple de code SQL qui montre ce qui est créé par la base de données.

La première boucle n'a rien à ignorer, c'est donc plus simple.

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

Les appels suivants doivent ignorer les fragments de résultats précédents, introduisent donc l'utilisation de 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
246
Drew Noakes

Nous avons maintenant posté une réponse officielle à le bogue ouvert sur Connect . Les solutions de contournement que nous recommandons sont les suivantes:

Cette erreur est due à la création par Entity Framework d'une transaction implicite lors de l'appel de SaveChanges (). Le meilleur moyen de contourner l’erreur est d’utiliser un modèle différent (c’est-à-dire de ne pas enregistrer en cours de lecture) ou de déclarer explicitement une transaction. Voici trois solutions possibles:

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

Il suffit de mettre context.SaveChanges() après la fin de votre foreach (boucle).

14
Majid

En effet, vous ne pouvez pas enregistrer les modifications dans une boucle foreach en C # avec Entity Framework. 

La méthode context.SaveChanges() agit comme une validation sur un système de base de données standard (RDMS). 

Effectuez simplement toutes les modifications (qu'Entity Framework mettra en cache), puis sauvegardez-les toutes en même temps en appelant SaveChanges() après la boucle (en dehors de celle-ci), comme une commande de validation de base de données.

Cela fonctionne si vous pouvez enregistrer toutes les modifications en même temps.

8

FYI: extrait d'un livre et de quelques lignes modifiées parce que son statut est toujours valable:

L'appel de la méthode SaveChanges () commence une transaction qui annule automatiquement toutes les modifications conservées dans la base de données si une exception se produit avant la fin de l'itération; sinon la transaction est validée. Vous pourriez être tenté d'appliquer la méthode après chaque mise à jour ou suppression d'une entité plutôt qu'après la fin de l'itération, notamment lorsque vous mettez à jour ou supprimez un nombre important d'entités.

Si vous essayez d'appeler SaveChanges () avant que toutes les données aient été traitées, vous obtenez une exception "La nouvelle transaction n'est pas autorisée, car d'autres threads s'exécutent dans la session". L'exception se produit car SQL Server n'autorise pas le démarrage d'une nouvelle transaction sur une connexion ayant un SqlDataReader ouvert, même avec plusieurs ensembles d'enregistrements actifs (MARS) activés par la chaîne de connexion (la chaîne de connexion par défaut de EF active MARS)

Parfois, il vaut mieux comprendre pourquoi il se passe des choses ;-)

6
Herman Van Der Blom

Toujours utiliser votre sélection comme liste

Par exemple:

var tempGroupOfFiles = Entities.Submited_Files.Where(r => r.FileStatusID == 10 && r.EventID == EventId).ToList();

Ensuite, parcourez la collection en sauvegardant les modifications

 foreach (var item in tempGroupOfFiles)
             {
                 var itemToUpdate = item;
                 if (itemToUpdate != null)
                 {
                     itemToUpdate.FileStatusID = 8;
                     itemToUpdate.LastModifiedDate = DateTime.Now;
                 }
                 Entities.SaveChanges();

             }
4
mzonerz

Je commençais à avoir le même problème mais dans une situation différente. J'ai eu une liste d'éléments dans une zone de liste. L'utilisateur peut cliquer sur un élément et sélectionner Supprimer, mais j'utilise un proc stocké pour supprimer l'élément, car la suppression de l'élément nécessite beaucoup de logique. Lorsque j'appelle le proc stocké, la suppression fonctionne bien, mais tout appel futur à SaveChanges provoquera l'erreur. Ma solution était d'appeler le proc stocké en dehors de EF et cela a bien fonctionné. Pour une raison quelconque, lorsque j'appelle le proc stocké en utilisant la méthode EF, cela laisse quelque chose ouvert.

4
MikeKulls

Voici 2 autres options qui vous permettent d’appeler SaveChanges () dans une boucle pour chaque boucle.

La première option consiste à utiliser un seul DBContext pour générer vos objets de liste dans lesquels parcourir, puis créer un deuxième DBContext sur lequel appeler SaveChanges (). Voici un exemple:

//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 deuxième option consiste à obtenir une liste des objets de base de données à partir du DBContext, mais à sélectionner uniquement les identifiants. Et ensuite, parcourez la liste des identifiants (probablement un int) et récupérez l'objet correspondant à chaque int, puis appelez SaveChanges () de cette façon. L'idée derrière cette méthode est de récupérer une longue liste d'entiers, est beaucoup plus efficace que d'obtenir une grande liste d'objets de base de données et d'appeler .ToList () sur tout l'objet. Voici un exemple de cette méthode:

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

J'avais besoin de lire un énorme ResultSet et de mettre à jour certains enregistrements de la table ..__J'ai essayé d'utiliser des morceaux comme suggéré dans Drew Noakes 's answer .

Malheureusement, après 50000 enregistrements, j'ai reçu OutofMemoryException .. La réponse Grand ensemble de données du cadre Entity, exception de mémoire insuffisante explique, que 

EF crée une deuxième copie des données qui utilise pour la détection des modifications (de manière à pouvoir conserver les modifications dans la base de données). EF détient ce deuxième set pour la durée de vie du contexte et son ensemble, c’est ça qui vous échappe de mémoire.

La recommandation est de renouveler votre contexte chaque lot.

J'ai donc récupéré les valeurs minimale et maximale de la clé primaire - les tables ont des clés primaires sous forme d'entiers incrémentiels automatiques. Ensuite, j'ai extrait de la base de données des morceaux d'enregistrements en ouvrant un contexte pour chaque morceau. Après traitement, le contexte de bloc se ferme et libère la mémoire. Cela garantit que l'utilisation de la mémoire ne croît pas.

Ci-dessous un extrait de mon code: 

  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 est une structure simple avec les propriétés From et To.

2
Michael Freidgeim

Ainsi, dans le projet où j’avais exactement le même problème, le problème ne se trouvait pas dans la foreach ni dans la .toList() mais bien dans la configuration AutoFac que nous utilisions . d'autres erreurs équivalentes ont été jetées.

C'était notre solution: Changé cela:

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

Je faisais également face au même problème.

Voici la cause et la solution.

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

Assurez-vous qu'avant de lancer des commandes de manipulation de données telles que des insertions, des mises à jour, vous avez fermé tous les lecteurs SQL actifs précédents.

Les erreurs les plus courantes sont les fonctions qui lisent les données de la base de données et renvoient des valeurs .Par exemple, des fonctions telles que isRecordExist.

Dans ce cas, nous revenons immédiatement de la fonction si nous avons trouvé l'enregistrement et oublions de fermer le lecteur.

1
Vinod T. Patil

Dans mon cas, le problème est apparu lorsque j'ai appelé procédure stockée via EF, puis que SaveChanges lève cette exception. Le problème était en appelant la procédure, le recenseur n'était pas disposé. J'ai corrigé le code de la manière suivante:

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
   }
}
1
Tomas Kubes

Je suis beaucoup en retard à la fête, mais aujourd’hui, j’ai fait face à la même erreur et ma résolution a été simple. Mon scénario ressemblait à ce code car je faisais des transactions de base de données dans des boucles for-each imbriquées.

Le problème est qu’une transaction à base de données unique prend un peu plus de temps que pour chaque boucle. Ainsi, une fois que la transaction précédente n’est pas terminée, la nouvelle analyse génère une exception. La solution consiste donc à créer un nouvel objet dans la boucle for-each où vous effectuez une transaction db.

Pour les scénarios mentionnés ci-dessus, la solution sera la suivante:

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

Le code ci-dessous fonctionne pour moi:

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

Rendre vos listes interrogeables à .ToList () et cela devrait fonctionner correctement. 

0
Wojciech Seweryn

Je sais que c’est une vieille question, mais j’ai fait face à cette erreur aujourd’hui. 

et j'ai trouvé que, cette erreur peut être levée quand un déclencheur de table de base de données obtient une erreur.

pour votre information, vous pouvez également consulter les déclencheurs de vos tables lorsque vous obtenez cette erreur.

0
nadir

Si vous obtenez cette erreur en raison de foreach et que vous avez vraiment besoin de sauvegarder une entité dans une boucle interne et d'utiliser l'identité générée dans une boucle, comme dans mon cas, la solution la plus simple consiste à utiliser un autre DBContext pour insérer une entité qui renverra Id et utilis cet identifiant dans un contexte externe

Par exemple 

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

Je suis un peu en retard, mais j'ai aussi eu cette erreur. J'ai résolu le problème en vérifiant quelles étaient les valeurs mises à jour.

J'ai découvert que ma requête était erronée et qu'il y avait plus de 250 modifications en attente. Alors j'ai corrigé ma requête, et maintenant cela fonctionne correctement.

Donc, dans ma situation: Recherchez des erreurs dans la requête en déboguant le résultat renvoyé. Après cela, corrigez la requête.

J'espère que cela aide à résoudre les problèmes futurs.

0
Max