684 votes

SqlException de Entity Framework - Nouvelle transaction n'est pas autorisée, car il y a d'autres threads d'exécution dans la session

Je suis actuellement à obtenir cette erreur:

Système.Les données.SqlClient.SqlException: la Nouvelle transaction n'est pas autorisée, car il y a d'autres threads d'exécution dans la session.

lors de l'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 Dev. Model #1

Modèle n ° 2 - Ce modèle se trouve dans une base de données sur notre Serveur de Prod et est mis à jour chaque jour automatique des flux rss. alt text

Remarque - Le rouge cerclé d'éléments dans le Modèle n ° 1 sont les champs que j'utilise pour "la carte" pour le Modèle #2. S'il vous plaît ignorer les cercles rouges dans le Modèle #2: c'est à partir d'une autre question que j'avais et qui est maintenant de répondre.

Note: j'ai encore besoin de mettre dans un isDeleted vérifier afin que je puisse doux supprimer de DB1 si elle a disparu de notre inventaire.

Tout ce que je veux faire, avec ce code, c'est de connecter une société dans DB1 avec un client DB2, obtenir leur liste de produits à partir de DB2 et de l'INSÉRER dans DB1 si elle n'est pas déjà là. Première fois devrait être une pleine tirer de l'inventaire. Chaque fois qu'il est exécuté après rien ne devrait se produire à moins que de nouveaux stocks sont venus dans les aliments au cours de la nuit.

Alors la grande question - comment puis-je résoudre la transaction d'erreur que j'obtiens? Dois-je le supprimer et recréer mon contexte à chaque fois dans les boucles (ne fait pas de sens pour moi)?

793voto

Keith Barrows Points 6836

Après beaucoup de l'arrachement des cheveux, j'ai découvert que l' foreach boucles étaient les coupables. Ce qui doit arriver est d'appeler EF, mais de retour dans un IList<T> de ce type de cible puis boucle sur l' 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;
    // ...
}

300voto

Drew Noakes Points 69288

Comme vous l'avez déjà identifié, vous ne pouvez pas enregistrer à partir de l'intérieur d'un foreach qui est encore de dessin à partir de la base de données via un lecteur actif.

Appelant ToList() ou ToArray() est très bien pour de petits ensembles de données, mais lorsque vous avez des milliers de lignes, vous serez la consommation d'une grande quantité de mémoire.

Il est préférable de charger les lignes dans les 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 de l'extension des méthodes, vous pouvez écrire votre requête comme ceci:

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

Le queryable objet que vous appelez cette méthode doit être commandé. C'est parce que Entity Framework ne supporte IQueryable<T>.Skip(int) sur commandé des requêtes, ce qui est logique si l'on considère que plusieurs requêtes pour les différentes gammes besoin de la commande pour être stable. Si l'ordre n'est pas important pour vous, il suffit de commander par clé primaire qui est susceptible d'avoir un index cluster.

Cette version permettra d'interroger la base de données par lots de 100. Notez que SaveChanges() est appelée pour chaque entité.

Si vous voulez améliorer votre capacité de production de façon spectaculaire, vous devriez appeler SaveChanges() moins souvent. Utiliser un code comme ceci à la place:

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

Il en résulte 100 fois moins de base de données de mise à jour. Bien sûr, chacune de ces appels prend plus de temps, mais vous avez encore le sortir en avance à la fin. Votre kilométrage peut varier, mais il a été mondes plus rapide pour moi.

Et il obtient autour de l'exception-vous été voir.

EDIT j'ai repensé à cette question après l'exécution du générateur de profils SQL et mise à jour de quelques choses à améliorer les performances. Pour tous ceux qui sont intéressés, voici quelques exemples de SQL qui montre ce qui est créé par la DB.

La première boucle n'a pas besoin de sauter quoi que ce soit, donc 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 besoin de passer des morceaux de résultats, donc 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

128voto

Maintenant, nous avons publié une réponse officielle pour le bug ouvert sur Connecter. Les solutions que nous recommandons sont comme suit:

Cette erreur est due à une Entité Cadre de la création d'une transaction implicite au cours de la SaveChanges (). Le meilleur moyen pour contourner l'erreur est d'utiliser un modèle différent (c'est à dire, pas d'économie alors que dans le milieu de la 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();
    }
} 

18voto

Mohammad Sepahvand Points 5377

C'est parce que vous appelez context.SaveChanges() au sein de votre foreach boucle, comme indiqué précédemment, vous devez apporter les modifications à l'entité à l'intérieur de votre foreach boucle puis appeler l' SaveChanges() méthode de DataContext juste après votre foreach boucle.

foreach(var v in vv)
{
v.ID=xxxx;
v.fName=xxxx;
v.lName=xxxx;
}
context.SaveChanges();

7voto

MikeKulls Points 1498

Je commençais à cette même question, mais dans une situation différente. J'avais une liste d'éléments dans une liste déroulante. L'utilisateur peut cliquer sur un élément et sélectionnez supprimer mais je suis à l'aide d'une procédure stockée pour la supprimer, car il y a beaucoup de logique dans la suppression de l'élément. Lorsque j'appelle la procédure stockée la suppression fonctionne très bien mais tout futur appel à SaveChanges sera la cause de l'erreur. Ma solution a été d'appeler la procédure stockée à l'extérieur de l'EF et cela a bien fonctionné. Pour une raison quelconque, quand j'appelle la procédure stockée à l'aide de l'EF façon de faire les choses, il laisse quelque chose d'ouvert.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X