Question SqlException from Entity Framework - Nouvelle transaction non 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 cours d'exécution de 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 # 1 - Ce modèle se trouve 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 # 2 - Ce modèle se trouve dans une base de données sur notre serveur Prod et est mis à jour chaque jour 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 encerclés en rouge dans le modèle n ° 1 sont les champs que j'utilise pour «mapper» au modèle n ° 2. S'il vous plaît ignorer les cercles rouges dans le modèle n ° 2: c'est à partir d'une autre question que j'avais qui est maintenant répondu.

Note: J'ai encore besoin de mettre une vérification isDeleted pour que je puisse l'effacer de DB1 si elle est sortie de l'inventaire de notre client.

Tout ce que je veux faire, avec ce code particulier, est de connecter une société dans DB1 avec un client dans DB2, obtenir leur liste de produits à partir de DB2 et l’Insérer dans DB1 si elle ne l’est pas déjà. La première fois devrait être un tirage complet de l'inventaire. Chaque fois qu'il est lancé là-bas après rien ne devrait arriver, sauf si un nouvel inventaire est entré dans le flux pendant la nuit.

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


498
2018-01-21 22:37


origine


Réponses:


Après beaucoup de traction sur les cheveux, j'ai découvert que le foreach les boucles étaient les coupables. Il faut appeler EF mais le renvoyer dans un IList<T> de ce type de cible, puis en boucle 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;
    // ...
}

578
2018-02-01 23:46



Comme vous avez déjà identifié, vous ne pouvez pas enregistrer dans un foreach Cela provient toujours de la base de données via un lecteur actif.

Appel ToList() ou ToArray() convient aux petits ensembles de données, mais lorsque vous avez des milliers de lignes, vous consommerez 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++;
        }
    }
}

Compte tenu des 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é.  En effet, Entity Framework ne prend en charge que IQueryable<T>.Skip(int) sur les requêtes ordonnées, ce qui est logique lorsque vous considérez que plusieurs requêtes pour différentes gammes nécessitent que le classement soit stable. Si le classement n’est pas important pour vous, il vous suffit d’ordonner par clé primaire car cela est susceptible d’avoir un index clusterisé.

Cette version va interroger 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 fréquemment. 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 entraîne 100 fois moins d'appels de mise à jour de la base de données. Bien sûr, chacun de ces appels prend plus de temps à terminer, mais vous sortez toujours à l'avance à la fin. Votre kilométrage peut varier, mais c'était plus rapide pour moi.

Et ça contourne l'exception que tu voyais.

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

La première boucle n'a pas besoin de sauter quoi que ce soit, alors c'est 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 segments de résultats précédents, ce qui introduit 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

241
2017-10-11 00:17



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

Cette erreur est due à la création d'une transaction implicite par Entity Framework lors de l'appel SaveChanges (). La meilleure façon de contourner l’erreur consiste à utiliser un modèle différent (c’est-à-dire ne pas économiser en cours de lecture) ou à 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();
    }
} 

113
2017-10-05 22:32



Mettez juste context.SaveChanges() après la fin de votre foreach(boucle).


12
2017-07-06 12:05



Je recevais 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 processus stocké pour supprimer l'élément, car la suppression de l'élément nécessite beaucoup de logique. Lorsque j'appelle la procédure stockée, la suppression fonctionne correctement, mais tout appel futur à SaveChanges provoquera l'erreur. Ma solution consistait à appeler le processus stocké en dehors de EF et cela fonctionnait bien. Pour une raison quelconque, lorsque j'appelle le proc mémorisé en utilisant la manière EF de faire les choses, il reste quelque chose d'ouvert.


4
2018-05-25 00:21



FYI: à partir d'un livre et quelques lignes ajustées car son stil valide:

L'appel de la méthode SaveChanges () lance une transaction qui annule automatiquement toutes les modifications apportées à 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'entité plutôt qu'une fois l'itération terminée, en particulier lorsque vous mettez à jour ou supprimez un grand nombre d'entités.

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

Parfois c'est mieux de comprendre pourquoi les choses se passent ;-)


3
2018-01-13 15:28



Toujours utiliser votre sélection en tant que liste

Par exemple:

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

Puis boucle à travers la collection tout en sauvegardant les modifications

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

             }

3
2017-09-19 06:07



Donc, dans le projet si j'avais exactement le même problème, le problème n'était pas dans le foreach ou la .toList() c'était en fait dans la configuration AutoFac que nous utilisions. Cela a créé des situations étranges où l'erreur ci-dessus a été lancée, mais un tas d'autres erreurs équivalentes ont été lancées.

C'était notre solution: A changé ceci:

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
2017-07-21 13:48