Database
 sql >> Base de données >  >> RDS >> Database

Utilisation d'expressions pour filtrer les données de la base de données

Je voudrais commencer par une description du problème que j'ai rencontré. Certaines entités de la base de données doivent être affichées sous forme de tables sur l'interface utilisateur. Entity Framework est utilisé pour accéder à la base de données. Il existe des filtres pour ces colonnes de tableau.

Il est nécessaire d'écrire un code pour filtrer les entités par paramètres.

Par exemple, il existe deux entités :Utilisateur et Produit.

public class User{ public int Id { get ; Positionner; } public string Nom { get; Positionner; }}public class Product{ public int Id { get ; Positionner; } public string Nom { get; Positionner; }}

Supposons que nous ayons besoin de filtrer les utilisateurs et les produits par nom. Nous créons des méthodes pour filtrer chaque entité.

public IQueryable FilterUsersByName(IQueryable users, string text){ return users.Where(user => user.Name.Contains(text));}public IQueryable FilterProductsByName(IQueryable products, string text){ return products.Where(product => product.Name.Contains(text));}

Comme vous pouvez le voir, ces deux méthodes sont presque identiques et ne diffèrent que par la propriété de l'entité, par laquelle filtre les données.

Cela peut être un défi si nous avons des dizaines d'entités avec des dizaines de champs qui nécessitent un filtrage. La complexité réside dans la prise en charge du code, la copie irréfléchie et, par conséquent, le développement lent et la forte probabilité d'erreur.

Paraphrasant Fowler, ça commence à sentir mauvais. Je voudrais écrire quelque chose de standard au lieu de la duplication de code. Par exemple :

public IQueryable FilterUsersByName(IQueryable users, string text){ return FilterContainsText(users, user => user.Name, text);}public IQueryable FilterProductsByName(IQueryable products, string text){ return FilterContainsText(products, propduct => propduct.Name, text);}public IQueryable FilterContainsText(IQueryable entity, Func getProperty, string text){ return entity. Où(entité => getProperty(entité).Contains(texte));}

Malheureusement, si nous essayons de filtrer :

public void TestFilter(){ using (var context =new Context()) { var filteredProducts =FilterProductsByName(context.Products, "name").ToArray(); }}

Nous aurons l'erreur "La méthode de test ExpressionTests.ExpressionTest.TestFilter a lancé l'exception :
System.NotSupportedException  :Le type de nœud d'expression LINQ 'Invoke' n'est pas pris en charge dans LINQ to Entities.

Expressions

Vérifions ce qui n'allait pas.

La méthode Where accepte un paramètre de type Expression>. Ainsi, Linq fonctionne avec des arborescences d'expressions, par lesquelles il construit des requêtes SQL, plutôt qu'avec des délégués.

L'expression décrit un arbre de syntaxe. Pour mieux comprendre comment ils sont structurés, considérons l'expression, qui vérifie qu'un nom est égal à une ligne.

Expression> attendu =produit => produit.Name =="target" ;

Lors du débogage, nous pouvons voir la structure de cette expression (les propriétés des clés sont marquées en rouge).

Nous avons l'arborescence suivante :

Lorsque nous transmettons un délégué en tant que paramètre, un arbre différent est généré, qui appelle la méthode Invoke sur le paramètre (délégué) au lieu d'invoquer la propriété de l'entité.

Lorsque Linq essaie de construire une requête SQL par cet arbre, il ne sait pas comment interpréter la méthode Invoke et lève NotSupportedException.

Ainsi, notre tâche consiste à remplacer la conversion en propriété de l'entité (la partie de l'arbre marquée en rouge) par l'expression transmise via ce paramètre.

Essayons :

Expression> propertyGetter =product => product.Name;Expression> filter =product => propertyGetter(product) =="target"

Maintenant, nous pouvons voir l'erreur « Nom de méthode attendu » à l'étape de la compilation.

Le problème est qu'une expression est une classe qui représente les nœuds d'un arbre de syntaxe, plutôt que le délégué et qu'elle ne peut pas être appelée directement. Maintenant, la tâche principale est de trouver un moyen de créer une expression en lui passant un autre paramètre.

Le visiteur

Après une brève recherche sur Google, j'ai trouvé une solution au problème similaire sur StackOverflow.

Pour travailler avec des expressions, il existe la classe ExpressionVisitor, qui utilise le modèle Visitor. Il est conçu pour parcourir tous les nœuds de l'arbre d'expression dans l'ordre d'analyse de l'arbre de syntaxe et permet de les modifier ou de renvoyer un autre nœud à la place. Si ni le nœud ni ses nœuds enfants ne sont modifiés, l'expression d'origine est renvoyée.

Lors de l'héritage de la classe ExpressionVisitor, nous pouvons remplacer n'importe quel nœud d'arbre par l'expression, que nous transmettons via le paramètre. Ainsi, nous devons mettre une étiquette de nœud, que nous remplacerons par un paramètre, dans l'arbre. Pour cela, écrivez une méthode d'extension qui simulera l'appel de l'expression et sera un marqueur.

public static class ExpressionExtension{ public static TFunc Call(this Expression expression) { throw new InvalidOperationException("Cette méthode ne doit jamais être appelée. C'est un marqueur de remplacement."); }}

Maintenant, nous pouvons remplacer une expression par une autre

Expression> propertyGetter =product => product.Name;Expression> filter =product => propertyGetter.Call())(product) =="target"; 

Il faut écrire un visiteur, qui remplacera la méthode Call par son paramètre dans l'arbre d'expression :

classe publique SubstituteExpressionCallVisitor :ExpressionVisitor{ lecture seule privée MethodInfo _markerDesctiprion ; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtension).GetMethod(nameof(ExpressionExtension.Call)).GetGenericMethodDefinition(); } protected override Expression VisitMethodCall(MethodCallExpression node) { if (IsMarker(node)) { return Visit(ExtractExpression(node)); } return base.VisitMethodCall(nœud); } private LambdaExpression ExtractExpression(MethodCallExpression node) { var target =node.Arguments[0]; return (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { return node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}

Nous pouvons remplacer notre marqueur :

public static Expression SubstituteMarker(this Expression expression){ var visitor =new SubstituteExpressionCallVisitor(); return (Expression)visitor.Visit(expression);}Expression> propertyGetter =product => product.Name;Expression> filter =product => propertyGetter.Call ()(product).Contains("123");Expression> finalFilter =filter.SubstituteMarker();

Au débogage, nous pouvons voir que l'expression n'est pas celle que nous attendions. Le filtre contient toujours la méthode Invoke.

Le fait est que les expressions parametersGetter et finalFilter utilisent deux arguments différents. Ainsi, nous devons remplacer un argument dans parameterGetter par l'argument dans finalFilter. Pour cela, nous créons un autre visiteur :

Le résultat est le suivant :

public class SubstituteParameterVisitor :ExpressionVisitor{ private readonly LambdaExpression _expressionToVisit ; dictionnaire privé en lecture seule  _substitutionByParameter ; public SubstituteParameterVisitor(Expression[] parametersSubstitutions, LambdaExpression expressionToVisit) { _expressionToVisit =expressionToVisit; _substitutionByParameter =expressionToVisit .Parameters .Select((parameter, index) => new {Parameter =parameter, Index =index}) .ToDictionary(pair => pair.Parameter, pair => parametersSubstitutions[pair.Index]); } public Expression Replace() { return Visit(_expressionToVisit.Body); } protected override Expression VisitParameter(noeud ParameterExpression) { Substitution d'expression ; if (_substitutionByParameter.TryGetValue(node, out substitution)) { return Visit(substitution); } return base.VisitParameter(noeud); }} classe publique SubstituteExpressionCallVisitor :ExpressionVisitor{ lecture seule privée MethodInfo _markerDesctiprion ; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtensions) .GetMethod(nameof(ExpressionExtensions.Call)) .GetGenericMethodDefinition(); } protected override Expression VisitInvocation(InvocationExpression node) { var isMarkerCall =node.Expression.NodeType ==ExpressionType.Call &&IsMarker((MethodCallExpression) node.Expression); if (isMarkerCall) { var parametersReplacer =new SubstituteParameterVisitor(node.Arguments.ToArray(), Unwrap((MethodCallExpression) node.Expression)); var cible =paramètreReplacer.Replace(); retour Visite(cible); } return base.VisitInvocation(noeud); } private LambdaExpression Unwrap(MethodCallExpression node) { var target =node.Arguments[0]; return (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { return node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}

Maintenant, tout fonctionne comme il se doit et nous pouvons enfin écrire notre méthode de filtration

public IQueryable FilterContainsText(IQueryable entités, Expression> getProperty, string text){ Expression> filter =entity => getProperty. Appel ()(entité). Contient (texte); renvoyer des entités.Where(filter.SubstituteMarker());}

Conclusion

L'approche avec le remplacement d'expression peut être utilisée non seulement pour le filtrage mais aussi pour le tri et toute requête à la base de données.

De plus, cette méthode permet de stocker des expressions avec la logique métier séparément des requêtes vers la base de données.

Vous pouvez consulter le code sur GitHub.

Cet article est basé sur une réponse StackOverflow.