sql >> Databáze >  >> RDS >> Database

Použití výrazů k filtrování dat databáze

Rád bych začal popisem problému, na který jsem narazil. V databázi jsou entity, které je třeba zobrazit jako tabulky v uživatelském rozhraní. Pro přístup k databázi se používá Entity Framework. Pro tyto sloupce tabulky existují filtry.

Pro filtrování entit podle parametrů je nutné napsat kód.

Například existují dvě entity:Uživatel a Produkt.

public class User{ public int Id { get; soubor; } public string Name { get; soubor; }}public class Produkt{ public int Id { get; soubor; } public string Name { get; soubor; }}

Předpokládejme, že potřebujeme filtrovat uživatele a produkty podle názvu. Vytváříme metody pro filtrování každé entity.

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

Jak vidíte, tyto dvě metody jsou téměř totožné a liší se pouze ve vlastnosti entity, podle které se data filtrují.

Může to být problém, pokud máme desítky subjektů s desítkami polí, která vyžadují filtrování. Složitost spočívá v podpoře kódu, bezmyšlenkovitém kopírování a v důsledku toho pomalém vývoji a vysoké pravděpodobnosti chyby.

Parafrázuji Fowlera, začíná to zapáchat. Chtěl bych místo duplikace kódu napsat něco standardního. Například:

public IQueryable FilterUsersByName(IQueryable users, string text){ return FilterContainsText(users, user => user.Name, text);} public IQueryable FilterProductsByName(IQueryable products, string text){ return FilterContainsText(products, produkt => produkt.Name, text);}veřejné IQueryable FilterContainsText(IQueryable entity, Func getProperty, string text){ return entity. Where(entita => getProperty(entita).Contains(text));}

Bohužel, pokud zkusíme filtrovat:

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

Zobrazí se chyba «Testovací metoda ExpressionTests.ExpressionTest.TestFilter vyvolala výjimku:
System.NotSupportedException :Typ uzlu výrazu LINQ ‚Invoke‘ není podporován v LINQ to Entities.

Výrazy

Pojďme zkontrolovat, co se pokazilo.

Metoda Where přijímá parametr typu Expression>. Linq tedy pracuje se stromy výrazů, pomocí kterých vytváří SQL dotazy, spíše než s delegáty.

Výraz popisuje strom syntaxe. Chcete-li lépe porozumět tomu, jak jsou strukturovány, zvažte výraz, který kontroluje, zda se název rovná řádku.

Výraz> očekávaný =produkt => produkt. Název =="target";

Při ladění můžeme vidět strukturu tohoto výrazu (klíčové vlastnosti jsou označeny červeně).

Máme následující strom:

Když předáme delegáta jako parametr, vygeneruje se jiný strom, který místo vyvolání vlastnosti entity volá metodu Invoke u parametru (delegate).

Když se Linq pokouší vytvořit SQL dotaz pomocí tohoto stromu, neví, jak interpretovat metodu Invoke, a vyvolá NotSupportedException.

Naším úkolem je tedy nahradit přetypování vlastnosti entity (červeně označená část stromu) výrazem, který je předán přes tento parametr.

Zkusme to:

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

Nyní můžeme vidět chybu «Očekávaný název metody» ve fázi kompilace.

Problém je v tom, že výraz je třída, která představuje uzly stromu syntaxe, nikoli delegát a nelze jej volat přímo. Nyní je hlavním úkolem najít způsob, jak vytvořit výraz, který mu předá další parametr.

Návštěvník

Po krátkém hledání Google jsem našel řešení podobného problému na StackOverflow.

Pro práci s výrazy existuje třída ExpressionVisitor, která využívá vzor Návštěvník. Je navržen tak, aby procházel všemi uzly stromu výrazů v pořadí analýzy syntaxe stromu a umožňuje je upravit nebo místo toho vrátit jiný uzel. Pokud se nezmění ani uzel, ani jeho podřízené uzly, vrátí se původní výraz.

Při dědění z třídy ExpressionVisitor můžeme libovolný uzel stromu nahradit výrazem, který předáme přes parametr. Potřebujeme tedy do stromu vložit nějaký node-label, který nahradíme parametrem. Chcete-li to provést, napište metodu rozšíření, která bude simulovat volání výrazu a bude značkou.

veřejná statická třída ExpressionExtension{ public static TFunc Call(tento výraz Expression) { throw new InvalidOperationException("Tato metoda by nikdy neměla být volána. Je to značka pro nahrazení."); }}

Nyní můžeme nahradit jeden výraz jiným

Výraz> propertyGetter =produkt => produkt.Název;Výraz> filtr =produkt => propertyGetter.Call()(produkt) =="target"; 

Je potřeba napsat návštěvníka, který nahradí metodu Call svým parametrem ve stromu výrazů:

public class SubstituteExpressionCallVisitor :ExpressionVisitor{ private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtension).GetMethod(nameof(ExpressionExtension.Call)).GetGenericMethodDefinition(); } protected override Expression VisitMethodCall(MethodCallExpression node) { if (IsMarker(uzel)) { return Visit(ExtractExpression(uzel)); } return base.VisitMethodCall(node); } private LambdaExpression ExtractExpression(MethodCallExpression node) { var target =node.Arguments[0]; return (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression uzel) { return node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}

Můžeme vyměnit naši značku:

veřejný statický výraz SubstituteMarker(tento výraz výraz){ var visitor =new SubstituteExpressionCallVisitor(); return (Expression)visitor.Visit(expression);}Výraz> propertyGetter =product => product.Name;Expression> filter =product => propertyGetter.Call ()(produkt).Contains("123");Výraz> finalFilter =filter.SubstituteMarker();

Při ladění můžeme vidět, že výraz není takový, jaký jsme očekávali. Filtr stále obsahuje metodu Invoke.

Faktem je, že výrazy parametrGetter a finalFilter používají dva různé argumenty. Potřebujeme tedy nahradit argument v parametruGetter argumentem v finalFilter. Za tímto účelem vytvoříme dalšího návštěvníka:

Výsledek je následující:

public class SubstituteParameterVisitor :ExpressionVisitor{ private readonly LambdaExpression _expressionToVisit; private readonly Dictionary _substitutionByParameter; public SubstituteParameterVisitor(Expression[] parametrSubstitutions, LambdaExpression expressionToVisit) { _expressionToVisit =expressionToVisit; _substitutionByParameter =expressionToVisit .Parameters .Select((parametr, index) => new {Parameter =parametr, Index =index}) .ToDictionary(pair => pair.Parameter, pair => parametrSubstitutions[pair.Index]); } public Expression Replace() { return Visit(_expressionToVisit.Body); } chráněné přepsání Expression VisitParameter(ParameterExpression node) { Substituce výrazu; if (_substitutionByParameter.TryGetValue(uzel, out substituce)) { return Visit(substituce); } return base.VisitParameter(node); }}veřejná třída SubstituteExpressionCallVisitor :ExpressionVisitor{ private readonly 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 parametrReplacer =new SubstituteParameterVisitor(node.Arguments.ToArray(), Unwrap((MethodCallExpression) uzel.Expression)); var target =parametrReplacer.Replace(); návratová návštěva (cíl); } return base.VisitInvocation(node); } private LambdaExpression Unwrap(MethodCallExpression node) { var target =node.Arguments[0]; return (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression uzel) { return node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}

Nyní vše funguje jak má a my konečně můžeme napsat naši metodu filtrace

public IQueryable FilterContainsText(IQueryable entity, Expression> getProperty, text řetězce){ Expression> filtr =entita => getProperty. Call()(entita).Contains(text); return entity.Where(filter.SubstituteMarker());}

Závěr

Přístup s nahrazením výrazu lze použít nejen pro filtrování, ale také pro řazení a jakýkoli dotaz do databáze.

Tato metoda také umožňuje ukládat výrazy spolu s obchodní logikou odděleně od dotazů do databáze.

Na kód se můžete podívat na GitHubu.

Tento článek je založen na odpovědi StackOverflow.


  1. INSERT INTO vs. SELECT INTO

  2. Jak mohu provést spouštěč PŘED AKTUALIZACÍ se serverem SQL?

  3. Volání uložené procedury s parametrem Out pomocí PDO

  4. Syntaxe for-loop v SQL Server