sql >> Database teknologi >  >> RDS >> Database

Brug af udtryk til at filtrere data i databasen

Jeg vil gerne starte med en beskrivelse af det problem, jeg stødte på. Der er enheder i databasen, der skal vises som tabeller på brugergrænsefladen. Entity Framework bruges til at få adgang til databasen. Der er filtre til disse tabelkolonner.

Det er nødvendigt at skrive en kode for at filtrere enheder efter parametre.

For eksempel er der to enheder:Bruger og Produkt.

public class User{ public int Id { get; sæt; } offentlig streng Navn { get; sæt; }}public class Product{ public int Id { get; sæt; } offentlig streng Navn { get; sæt; }}

Antag, at vi skal filtrere brugere og produkter efter navn. Vi opretter metoder til at filtrere hver enhed.

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

Som du kan se, er disse to metoder næsten identiske og adskiller sig kun i entitetsegenskaben, som filtrerer dataene med.

Det kan være en udfordring, hvis vi har snesevis af entiteter med snesevis af felter, der kræver filtrering. Kompleksiteten ligger i kodeunderstøttelse, tankeløs kopiering og som et resultat langsom udvikling og høj sandsynlighed for fejl.

Når man omskriver Fowler, begynder det at lugte. Jeg vil gerne skrive noget standard i stedet for kodeduplikering. For eksempel:

offentlig IQueryable FilterUsersByName(IQueryable-brugere, strengtekst){ return FilterContainsText(users, user => user.Name, text);}offentlig IQueryable FilterProductsByName(IQueryable-produkter, streng text){ return FilterContainsText(products, propduct => propduct.Name, text);}public IQueryable FilterContainsText(IQueryable entities, Func getProperty, string text){ return entities. Where(entity => getProperty(entity).Indeholder(tekst));}

Desværre, hvis vi prøver at filtrere:

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

Vi får fejlen «Testmetode ExpressionTests.ExpressionTest.TestFilter kastede undtagelsen:
System.NotSupportedException :LINQ-udtryksknudetypen 'Invoke' er ikke understøttet i LINQ til Entities.

Udtryk

Lad os tjekke, hvad der gik galt.

Where-metoden accepterer en parameter af typen Expression>. Linq arbejder således med udtrykstræer, hvorved den bygger SQL-forespørgsler, snarere end med delegerede.

Udtrykket beskriver et syntakstræ. For bedre at forstå, hvordan de er struktureret, kan du overveje udtrykket, som kontrollerer, at et navn er lig med en række.

Udtryk> forventet =produkt => produkt.Navn =="mål";

Ved fejlfinding kan vi se strukturen af ​​dette udtryk (nøgleegenskaber er markeret med rødt).

Vi har følgende træ:

Når vi sender en delegeret som en parameter, genereres der et andet træ, som kalder Invoke-metoden på (delegeret) parameteren i stedet for at kalde entity-egenskaben.

Når Linq forsøger at bygge en SQL-forespørgsel efter dette træ, ved den ikke, hvordan den skal fortolke Invoke-metoden og kaster NotSupportedException.

Vores opgave er således at erstatte castet til entity-egenskaben (trædelen markeret med rødt) med det udtryk, der sendes via denne parameter.

Lad os prøve:

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

Nu kan vi se fejlen «Metodenavn forventet» på kompileringsstadiet.

Problemet er, at et udtryk er en klasse, der repræsenterer noder i et syntakstræ, snarere end den delegerede, og det kan ikke kaldes direkte. Nu er hovedopgaven at finde en måde at skabe et udtryk ved at overføre en anden parameter til det.

Den besøgende

Efter en kort Google-søgning fandt jeg en løsning på det lignende problem hos StackOverflow.

For at arbejde med udtryk er der klassen ExpressionVisitor, som bruger Visitor-mønsteret. Det er designet til at krydse alle noderne i udtrykstræet i rækkefølgen for at analysere syntakstræet og tillader at ændre dem eller returnere en anden node i stedet for. Hvis hverken noden eller dens underordnede noder ændres, returneres det oprindelige udtryk.

Når vi arver fra ExpressionVisitor-klassen, kan vi erstatte en hvilken som helst træknude med udtrykket, som vi videregiver via parameteren. Derfor er vi nødt til at sætte noget node-label, som vi vil erstatte med en parameter, i træet. For at gøre dette skal du skrive en udvidelsesmetode, der simulerer udtrykkets kald og vil være en markør.

public static class ExpressionExtension{ public static TFunc Call(dette udtryk udtryk) { throw new InvalidOperationException("Denne metode bør aldrig kaldes. Den er en markør for udskiftning."); }}

Nu kan vi erstatte et udtryk med et andet

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

Det er nødvendigt at skrive en besøgende, som vil erstatte Call-metoden med dens parameter i udtrykstræet:

offentlig klasse SubstituteExpressionCallVisitor :ExpressionVisitor{ privat skrivebeskyttet 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)); } returnere base.VisitMethodCall(node); } 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; }}

Vi kan erstatte vores markør:

offentlig statisk udtryk SubstituteMarker(dette udtryk udtryk){ var besøgende =new SubstituteExpressionCallVisitor(); return (Expression)visitor.Visit(expression);}Expression> propertyGetter =product => product.Name;Expression> filter =product => propertyGetter.Call ()(produkt).Contains("123");Expression> finalFilter =filter.SubstituteMarker();

Ved debugging kan vi se, at udtrykket ikke er, hvad vi forventede. Filteret indeholder stadig Invoke-metoden.

Faktum er, at parameterGetter- og finalFilter-udtrykkene bruger to forskellige argumenter. Derfor skal vi erstatte et argument i parameterGetter med argumentet i finalFilter. For at gøre dette opretter vi en anden besøgende:

Resultatet er som følger:

offentlig klasse SubstituteParameterVisitor :ExpressionVisitor{ privat skrivebeskyttet LambdaExpression _expressionToVisit; privat skrivebeskyttet ordbog _substitutionByParameter; public SubstituteParameterVisitor(Expression[] parameterSubstitutions, LambdaExpression expressionToVisit) { _expressionToVisit =expressionToVisit; _substitutionByParameter =expressionToVisit .Parameters .Select((parameter, indeks) => ny {Parameter =parameter, Index =indeks}) .ToDictionary(par => par.Parameter, par => parameterSubstitutioner[par.Index]); } public Expression Replace() { return Visit(_expressionToVisit.Body); } protected override Expression VisitParameter(ParameterExpression node) { Udtrykssubstitution; if (_substitutionByParameter.TryGetValue(node, ud substitution)) { return Visit(substitution); } returnere base.VisitParameter(node); }}offentlig klasse 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 parameterReplacer =new SubstituteParameterVisitor(node.Arguments.ToArray(), Unwrap((MethodCallExpression) node.Expression)); var target =parameterReplacer.Replace(); returnere Besøg(mål); } returnere base.VisitInvocation(node); } 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; }}

Nu fungerer alt, som det skal, og vi kan endelig skrive vores filtreringsmetode

public IQueryable FilterContainsText(IQueryable-enheder, Udtryk> getProperty, strengtekst){ Udtryk> filter =entity => getProperty. Call()(entity).Indeholder(tekst); returnere enheder.Where(filter.SubstituteMarker());}

Konklusion

Tilgangen med udtrykserstatningen kan ikke kun bruges til filtrering, men også til sortering og enhver forespørgsel til databasen.

Denne metode tillader også at gemme udtryk sammen med forretningslogik adskilt fra forespørgslerne til databasen.

Du kan se på koden på GitHub.

Denne artikel er baseret på et StackOverflow-svar.


  1. REGEXP_COUNT() Funktion i Oracle

  2. Sådan udføres en procedure inde i en pakke i Oracle

  3. MySQL vælger hurtigt 10 tilfældige rækker fra 600.000 rækker

  4. chmod mislykkedes:EPERM (operation ikke tilladt) i Android?