Puis-je créer plusieurs DBConnections de manière asynchrone?

asynchronous c# dapper sql-server

Question

J'essaie d'améliorer les performances d'une opération de lecture de base de données complexe. J'ai trouvé du code qui, dans des tests limités, fonctionne beaucoup plus rapidement que les tentatives précédentes en utilisant diverses techniques, notamment une procédure stockée à la main. Il utilise Dapper, mais Dapper n'est pas la principale source de préoccupation.

public IEnumerable<Order> GetOpenOrders(Guid vendorId)
{
    var tasks = GetAllOrders(vendorId)
        .Where(order => !order.IsCancelled)
        .Select(async order => await GetLineItems(order))
        .Select(async order =>
        {
            var result = (await order);
            return result.GetBalance() > 0M ? result : null;
        })
        .Select(async order => await PopulateName(await order))
        .Select(async order => await PopulateAddress(await order))
        .ToList();
    Task.WaitAll(tasks.ToArray<Task>());
    return tasks.Select(t => t.Result);
}

private IDbConnection CreateConnection()
{
    return new SqlConnection("...");
}

private IEnumerable<Order> GetAllOrders(Guid vendorId)
{
    using (var db = CreateConnection())
    {
        return db.Query<Order>("...");
    }
}

private async Task<Order> GetLineItems(Order order)
{
    using (var db = CreateConnection())
    {
        var lineItems = await db.QueryAsync<LineItem>("...");
        order.LineItems = await Task.WhenAll(lineItems.Select(async li => await GetPayments(li)));
        return order;
    }
}

private async Task<LineItem> GetPayments(LineItem lineItem)
{
    using (var db = CreateConnection())
    {
        lineItem.Payments = await db.QueryAsync<Payment>("...");
        return lineItem;
    }
}

private async Task<Order> PopulateName(Order order)
{
    using (var db = CreateConnection())
    {
        order.Name = (await db.QueryAsync<string>("...")).FirstOrDefault();
        return order;
    }
}

private async Task<Order> PopulateAddress(Order order)
{
    using (var db = CreateConnection())
    {
        order.Address = (await db.QueryAsync<string>("...")).FirstOrDefault();
        return order;
    }
}

Ceci est quelque peu simplifié, mais j'espère que cela met en évidence mon principal problème:

  • Ce code est-il une bonne idée?

Je sais qu'il est possible de le rendre plus sûr en réutilisant la même connexion, mais la création de nombreuses connexions le rend plus rapide d'un ordre de grandeur dans mes tests. J'ai également testé / compté le nombre de connexions simultanées à partir de la base de données elle-même et je vois des centaines de déclarations exécutées en même temps.

Quelques questions connexes:

  • Dois-je utiliser plus d'async (ex: CreateConnection (), GetAllOrders), ou moins?
  • Quel type de test peut / dois-je faire avant de mettre ce type de code en production?
  • Existe-t-il des stratégies alternatives pouvant produire des performances similaires mais nécessitant moins de connexions?

Réponse acceptée

Le plus gros problème avec votre code est que vous récupérez beaucoup plus de données de votre base de données que ce dont vous avez réellement besoin pour répondre à la requête. Ceci est connu sous le nom d' extraction étrangère .

Dapper est génial, mais contrairement à Entity Framework et à d'autres solutions, ce n'est pas un fournisseur LINQ . Vous devez exprimer l'intégralité de votre requête dans le SQL, y compris la clause WHERE . Dapper vous aide simplement à le matérialiser en objets. Il retourne IEnumerable<T> , pas IQueryable<T> .

Donc votre code:

GetAllOrders(vendorId)
    .Where(order => !order.IsCancelled)

Demande en fait toutes les commandes dans la base de données - pas seulement celles non annulées. Le filtre se passe en mémoire, ensuite.

Également:

order.Name = (await db.QueryAsync<string>("...")).FirstOrDefault();

Le ... de votre requête comprend mieux un SELECT TOP 1 , ou vous récupérerez tous les éléments, juste pour jeter tous les éléments sauf le premier.

En outre, considérez que vous effectuez de nombreux appels plus petits pour remplir chaque segment d'une commande. A chaque commande, vous avez 3 requêtes supplémentaires, avec N lignes supplémentaires. Ceci est un anti-pattern commun, appelé SELECT N + 1 . Il est toujours préférable d'exprimer l'intégralité de votre requête sous la forme d'une opération "volumineuse" que d'émettre de nombreuses requêtes bavardes vers la base de données. Ceci est également décrit comme l' anti-modèle d'E / S bavard .

En ce qui concerne les questions asynchrones - bien qu'il n'y ait rien de mal à faire plusieurs appels de bases de données en parallèle, ce n'est pas exactement ce que vous faites ici. Comme vous attendez chaque étape, vous faites toujours les choses en série.

Eh bien, au moins, vous les faites en série pour chaque commande. Vous obtenez un parallélisme dans la boucle externe. Mais toutes les choses internes sont essentiellement sérielles. Task.WaitAll bloquera jusqu'à ce que toutes les tâches externes (une par ordre filtré) soient terminées.

Un autre problème est que vous n'êtes pas dans un contexte asynchrone lorsque vous appelez GetOpenOrders en premier lieu. Les véritables avantages de l'async / wait ne sont pas réalisés tant que vous n'avez pas asynchrone tout au long de la pile. Je vous suggère également de regarder cette série de vidéos sur Async depuis Channel 9 .

Ma recommandation est de:

  • Déterminez la requête complète à exécuter pour récupérer toutes les données de la base de données, mais pas plus que ce dont vous avez réellement besoin.
  • Exécutez cette requête dans Dapper. Utilisez Query si vous êtes dans un contexte synchrone ( IEnumerable<Order> GetOpenOrders ) ou utilisez QueryAsync si vous vous trouvez dans un contexte asynchrone ( async Task<IEnumerable<Order>> GetOpenOrdersAsync ). N'essayez pas d'utiliser la requête asynchrone à partir d'un contexte non asynchrone.
  • Utilisez la fonctionnalité de mappage de Dapper pour récupérer plusieurs objets à partir d'une seule requête.


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