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.

c# entity-framework inversion-of-control transactions

Question

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 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 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. texte alt 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 pour pouvoir le supprimer en douceur de DB1 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 passer, à moins que de nouveaux stocks ne parviennent dans l'alimentation pendant la nuit.

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

Réponse acceptée

Après avoir tiré beaucoup de cheveux, j'ai découvert que ce sont les boucles foreach sont 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;
    // ...
}

Réponse populaire

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

Appeler ToList() ou ToArray() convient pour de petits ensembles de données, mais lorsque vous avez des milliers de lignes, vous allez utiliser 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é. 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 plages nécessitent une organisation 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, appelez moins souvent SaveChanges() . 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 loin devant. Votre kilométrage peut varier, mais cela a été plus rapide pour moi.

Et cela contourne l'exception que vous avez vue.

EDIT J'ai revisité 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 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


Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi