Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

EF et récursivité

A travers ce post, je vais vous présenter un cas concret de mauvaise utilisation d’EF observée récemment chez un client.

Mon modèle est le suivant :

image

Nous voulons simplement récupérer un arbre avec une contrainte : si le type du noeud est "G" et qu’il n’a pas d’enfant, il ne doit pas apparaître dans l’arborescence.

Comment faire ceci.

Voici le code que j’ai pu observer :

public static List<Node> GetTree()
{
using (var context = new TreeEntities())
{
List<Node> nodes = context.Nodes.Where(n => n.Parent == null).ToList();
foreach (var n in nodes)
LoadChildren(context, n);
foreach (var n in nodes.Where(n => n.Type == "G" && n.Children.Count == 0).ToList())
nodes.Remove(n);
return nodes;
} } public static void LoadChildren(TreeEntities context, Node parentNode) {
List<Node> nodes = context.Nodes.Where(n => n.ParentId == parentNode.Id).ToList();
     foreach (var n in nodes)
         LoadChildren(context, n);
     foreach (var n in nodes.Where(n => n.Type == "G" && n.Children.Count == 0).ToList())
         context.Nodes.Detach(n); }

J’ai effectué un test avec le jeu d’entités suivant :

using (var context = new TreeEntities())
{
     for (int i0 = 0; i0 < 10; i0++)
     {
         Node n0 = new Node { Name = i0.ToString(), Type = i0 == 9 ? null : "G" };
         context.Nodes.AddObject(n0);
         if (i0 > 7)
             break;
         for (int i1 = 0; i1 < 10; i1++)
         {
             Node n1 = new Node { Name = n0.Name + i1.ToString(), Type = i1 == 9 ? null : "G", Parent = n0 };
             context.Nodes.AddObject(n1);
             if (i1 > 7)
                 break;
             for (int i2 = 0; i2 < 10; i2++)
             {
                 Node n2 = new Node { Name = n1.Name + i2.ToString(), Type = i2 == 9 ? null : "G", Parent = n1 };
                 context.Nodes.AddObject(n2);
                 if (i2 > 7)
                     break;
                 for (int i3 = 0; i3 < 10; i3++)
                 {
                     Node n3 = new Node { Name = n2.Name + i3.ToString(), Type = i3 == 9 ? null : "G", Parent = n2 };
                     context.Nodes.AddObject(n3);
                     if (i3 > 7)
                         break;
                     for (int i4 = 0; i4 < 10; i4++)
                         context.Nodes.AddObject(new Node { Name = n3.Name + i4.ToString(), Parent = n3 });
                 }
             }
         }
     }
     context.SaveChanges(); }

Pour cette première version, ce code s’exécute en 5 minutes 41 secondes et génère 46 226 requêtes en base !

 

Il n’y a pas besoin d’être DBA pour savoir que si le nombre de requêtes dépend du nombre de datarows, le code ne supporte pas la charge.

Comment améliorer les choses ?

 

Un des gros avantages d’EF est le fait qu’il fait tout seul les relations. C’est d’ailleurs pour cela que ce code fonctionne. A aucun moment on a affecté les enfants / parents d’un des noeuds. EF l’a fait pour nous.

 

On pourrait être tenter par le code suivant :

public static List<Node> GetTree()
{
using (var context = new TreeEntities())
{


return context.Nodes.Where(n => n.Type != "G" || n.Children.Any()).AsEnumerable().Where(n => n.ParentId == null).ToList();
} }

Petite remarque avant de continuer : attention à bien utiliser ParentId != null et pas Parent != null. En effet, au moment où on énumère sur les entités, rien ne nous indique que le parent a bien été chargé. Si ce n’est pas le cas, ParentId sera différent de null alors que Parent sera égal à null.

D’autre part, ce code ne fonctionne pas comme il devrait. En effet, si j’ai un noeud de type G avec un enfant de type G sans enfant, le premier noeud doit être supprimé puisque son seul enfant doit être supprimé. Or avec mon système à un seul niveau, ce ne sera pas le cas.

 

SQL n’est cependant pas réputé pour la récursivité.

Je vais donc tenter deux approches :

  • une première dans laquelle je vais faire cette récursivité dans mon code. A noter que cela implique de charger trop d’entités (comme c’est le cas avec la première version).
  • une deuxième dans laquelle c’est SQL Server qui va se charger de la récursivité

 

Ma première solution est la suivante :

public static IEnumerable<T> GetEntitiesInCache<T>(this ObjectContext context, EntityState entityState = EntityState.Unchanged | EntityState.Modified | EntityState.Added)
{
    return context.ObjectStateManager.GetObjectStateEntries(entityState).Select(ose => ose.Entity).OfType<T>(); } public static List<Node> GetTree(TreeEntities context) {
using (var context = new TreeEntities())
{
   foreach (var n in context.Nodes);

bool continueLoop;
do
{
continueLoop = false;
foreach (var n in context.GetEntitiesInCache<Node>().Where(n => n.Type == "G" && n.Children.Count == 0).ToList())
{
continueLoop = true;
context.Nodes.Detach(n);
}
} while (continueLoop);

return context.GetEntitiesInCache<Node>().Where(n => n.ParentId == null).ToList();
} }

Dans ce cas, je suis passé de plus de 5 minutes 41 secondes à 1 seconde avec une seule requête exécutée en base.

A noter que je travaille avec une base locale. Dans le cas contraire l’écart de temps aurait été encore plus important.

Remarquez également le

       foreach (var n in context.Nodes);

qui permet de charger l’ensemble des noeuds.

 

Pour la récursivité avec SQL server, cela fera l’objet d’un post futur.

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 :

Publié jeudi 28 juillet 2011 21:02 par Matthieu MEZIL

Classé sous : , ,

Commentaires

# re: EF et récursivité @ vendredi 29 juillet 2011 15:43

Tu es un grand malade Matthieu!

Comment ton cerveau peut-il fonctionner pour sortir un truc comme ça?

L'idée est juste hallucinante. C'est génial comme truc...

Je crois qu'il faut que j'ouvre d'anciennes solutions afin de voir si ça ne peut pas s'appliquer sur mes cas ;)

JeremyJeanson

# re: EF et récursivité @ vendredi 29 juillet 2011 23:57

:)

Matthieu MEZIL

# re: EF et récursivité @ lundi 5 décembre 2011 21:30

Merci beaucoup pour cet article !

J'aimerais savoir si tu as pu avancer sur la récursivité avec sql server ? je suis curieux de voir ce que ça pourrait donner:)

Vivien Adnot

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01