Entity Framework, soyez vigilant avec la méthode Include !
La méthode Include est très facile à utiliser ce qui est une bonne chose. Cependant son implémentation est loin d’être la plus optimale !
Pourquoi ? La méthode Include permet de récupérer des graphes d’entité en une seule requête SQL. L’idée est excellente mais…
Le problème c’est que pour faire cela, EF doit utiliser un join.
Qu’est-ce que ça implique ?
Ca implique une requête lente qui retourne trop d’octets.
A la place de :
context.Categories.Include("Products")
C’est souvent plus performant de faire cela :
foreach (var p in context.Products.Where(p => p.Category != null)) ;
context.Categories
Le problème avec ce moyen c’est qu’il implique plusieurs accès à la base de données. C’est dommageable.
Il est possible de corriger ce défaut en utilisant les MutiResultSets.
J’ai utilsé pour cela mon dernier post et j’ai rajouté la méthodes suivantes dans la classe ObjectContextExtension.
public static IEnumerable<T1> Include<T1, T2>(this ObjectContext context, IQueryable<T1> query1, IQueryable<T2> query2, MergeOption mergeOption = MergeOption.AppendOnly)
where T1 : class, new()
where T2 : class, new()
{
if (mergeOption == MergeOption.NoTracking)
throw new NotImplementedException();
foreach (T1 item in from kvp in Execute(context, query2, query1, mergeOption).SkipWhile(elt => elt.Key != query1)
select kvp.Value)
yield return item;
}
public static IEnumerable<T1> Include<T1, T2, T3>(this ObjectContext context, IQueryable<T1> query1, IQueryable<T2> query2, IQueryable<T3> query3, MergeOption mergeOption = MergeOption.AppendOnly)
where T1 : class, new()
where T2 : class, new()
where T3 : class, new()
{
if (mergeOption == MergeOption.NoTracking)
throw new NotImplementedException();
foreach (T1 item in from kvp in Execute(context, query2, query3, query1, mergeOption).SkipWhile(elt => elt.Key != query1)
select kvp.Value)
yield return item;
}
Maintenant, exécutons quelques tests de performance sur la base Northwind :
test1:
context.Categories.Include("Products")
test2:
context.Categories.Include("Products").Where(c => c.CategoryName == "Beverages")
test3:
context.Categories.Include("Products.OrderDetails")
test4:
context.Categories.Include("Products.OrderDetails").Where(c => c.CategoryName == "Beverages")
test5:
context.Categories.Include("Products.OrderDetails").Where(c => c.CategoryName == "Unexisting")
Il est possible d’avoir les mêmes résultats avec plusieurs requêtes simples :
test1:
foreach (var p in context.Products.Where(p => p.CategoryID != null)) ;
context.Categories
test2:
foreach (var p in context.Products.Where(p => p.Category.CategoryName == "Beverages")) ;
context.Categories.Where(c => c.CategoryName == "Beverages")
test3:
foreach (var od in context.OrderDetails.Where(od => od.Product.CategoryID != null)) ;
foreach (var p in context.Products.Where(p => p.CategoryID != null)) ;
context.Categories
test4:
foreach (var od in context.OrderDetails.Where(od => od.Product.Category.CategoryName == "Beverages")) ;
foreach (var p in context.Products.Where(p => p.Category.CategoryName == "Beverages")) ;
context.Categories.Where(c => c.CategoryName == "Beverages")
test 5:
foreach (var od in context.OrderDetails.Where(od => od.Product.Category.CategoryName == "Unexisting")) ;
foreach (var p in context.Products.Where(p => p.Category.CategoryName == "Unexisting")) ;
context.Categories.Where(c => c.CategoryName == "Unexisting")
Ou avec ma méthode Include :
test 1:
context.Include(context.Categories, context.Products.Where(p => p.CategoryID != null))
test 2:
context.Include(
context.Categories.Where(c => c.CategoryName == "Beverages"),
context.Products.Where(p => p.Category.CategoryName == "Beverages"))
test 3:
context.Include(context.Categories,
context.Products.Where(p => p.CategoryID != null),
context.OrderDetails.Where(od => od.Product.CategoryID != null))
test 4 :
context.Include(
context.Categories.Where(c => c.CategoryName == "Beverages"),
context.Products.Where(p => p.Category.CategoryName == "Beverages"),
context.OrderDetails.Where(od => od.Product.Category.CategoryName == "Beverages"))
test 5:
context.Include(
context.Categories.Where(c => c.CategoryName == "Unexisting"),
context.Products.Where(p => p.Category.CategoryName == "Unexisting"),
context.OrderDetails.Where(od => od.Product.Category.CategoryName == "Unexisting"))
Les résultats sont les suivants :
|
|
test 1
|
Coeff d’amélioration
|
test 2
|
Coeff d’amélioration
|
test 3
|
Coeff d’amélioration
|
test 4
|
Coeff d’amélioration
|
test 5
|
Coeff d’amélioration
|
|
Temps d’exécution (ms)
|
L2E Include
|
222
|
|
95
|
|
342
|
|
163
|
|
93
|
|
|
Plusieurs select
|
159
|
1,40
|
61
|
1,56
|
124
|
2,76
|
66
|
2,47
|
24
|
3,88
|
|
Mon Include
|
57
|
3,89
|
22
|
4,32
|
433
|
0,79
|
128
|
1,27
|
22
|
4,23
|
|
Taille du paquet retourné par la base (octets)
|
L2E Include
|
847110
|
|
132699
|
|
2.375347E7
|
|
4462675
|
|
1534
|
|
|
Plusieurs select
|
95335
|
8,89
|
12619
|
10,52
|
145134
|
163,67
|
22065
|
202,25
|
663
|
2,31
|
|
Mon Include
|
95335
|
8,89
|
12619
|
10,52
|
145134
|
163,67
|
22065
|
202,25
|
663
|
2,31
|
A noter que, dans le cadre de mes tests, la base est locale. Les résultats auraient été encore pire pour la méthode Include de L2E sinon.
De plus, si Product.CategoryID n’était pas nullable, on améliorerait encore le coeff d’amélioration pour test1 et test3 parce qu’on éviterait un join dans la requête SQL générées.
Pour les tests test 3 et test 4, plusieurs select sont plus performant que mon Include. Je ne comprends pas pourquoi. La différence de performance se fait en base comme si SQL Server avait du mal avec les multi-resultset. Si quelqu’un peut m’expliquer, merci d’avance.
La méthode Include de L2E peut également s’avérer être la meilleure dans le cas suivant par exemple :
context.OrderDetails.Include("Product.Category").First()
Pour conclure, la méthode Include est très cool car très facile à utiliser. Cependant, dans certains cas, ce n’est pas la plus performante. Comme je viens de l’illustrer, il est parfois possible de l’améliorer de façon très significative.
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :