Database
 sql >> база данни >  >> RDS >> Database

Използване на изрази за филтриране на данни от базата данни

Бих искал да започна с описание на проблема, който срещнах. В базата данни има обекти, които трябва да се показват като таблици в потребителския интерфейс. Entity Framework се използва за достъп до базата данни. Има филтри за тези колони на таблицата.

Необходимо е да се напише код за филтриране на обекти по параметри.

Например има два обекта:потребител и продукт.

public class User{ public int Id { get; комплект; } публичен низ Име { get; комплект; }}продукт от публичен клас{ public int Id { get; комплект; } публичен низ Име { get; комплект; }}

Да приемем, че трябва да филтрираме потребители и продукти по име. Създаваме методи за филтриране на всеки обект.

public IQueryable FilterUsersByName(IQueryable потребители, текст на низ){ return users.Where(user => user.Name.Contains(text));}public IQueryable FilterProductsByName(IQueryable продукти, текст на низ){ return products.Where(product => product.Name.Contains(text));}

Както можете да видите, тези два метода са почти идентични и се различават само по свойството на обекта, чрез което филтрира данните.

Може да е предизвикателство, ако имаме десетки обекти с десетки полета, които изискват филтриране. Сложността е в поддръжката на код, необмисленото копиране и в резултат на това бавното развитие и високата вероятност за грешка.

Парафразирайки Фаулър, започва да мирише. Бих искал да напиша нещо стандартно вместо дублиране на код. Например:

public IQueryable FilterUsersByName(IQueryable потребители, низ текст){ return FilterContainsText(users, user => user.Name, text);}public IQueryable FilterProductsByName(IQueryable продукти текст){ return FilterContainsText(products, propduct => propduct.Name, text);}public IQueryable FilterContainsText(IQueryable entities, Func getProperty, string text){ връщане на обекти. Където(entity => getProperty(entity).Contains(text));}

За съжаление, ако се опитаме да филтрираме:

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

Ще получим грешката «Тестовият метод ExpressionTests.ExpressionTest.TestFilter хвърли изключението:
System.NotSupportedException :Типът възел на израза LINQ „Извикване“ не се поддържа в LINQ to Entities.

Изрази

Нека проверим какво се обърка.

Методът Where приема параметър от типа Expression>. По този начин Linq работи с дървета на изрази, чрез които изгражда SQL заявки, а не с делегати.

Изразът описва синтактично дърво. За да разберете по-добре как са структурирани, разгледайте израза, който проверява дали името е равно на ред.

Израз> очаквано =продукт => product.Name =="target";

При отстраняване на грешки можем да видим структурата на този израз (ключовите свойства са маркирани в червено).

Имаме следното дърво:

Когато предаваме делегат като параметър, се генерира различно дърво, което извиква метода Invoke на параметъра (delegate), вместо да извиква свойството на обекта.

Когато Linq се опитва да изгради SQL заявка от това дърво, той не знае как да интерпретира метода Invoke и хвърля NotSupportedException.

По този начин нашата задача е да заменим прехвърлянето към свойството на обекта (дървовата част, маркирана в червено) с израза, който се предава чрез този параметър.

Нека опитаме:

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

Сега можем да видим грешката „очаква се име на метода“ на етапа на компилация.

Проблемът е, че изразът е клас, който представлява възли на синтактично дърво, а не делегат и не може да бъде извикан директно. Сега основната задача е да се намери начин да се създаде израз, като му се предава друг параметър.

Посетителят

След кратко търсене в Google намерих решение на подобен проблем в StackOverflow.

За работа с изрази има клас ExpressionVisitor, който използва модела Visitor. Той е проектиран да преминава през всички възли на дървото на изразите в реда на анализиране на синтаксичното дърво и позволява да ги модифицирате или вместо това да върнете друг възел. Ако нито възелът, нито неговите дъщерни възли са променени, оригиналният израз се връща.

Когато наследяваме от класа ExpressionVisitor, можем да заменим всеки възел на дърво с израза, който предаваме чрез параметъра. По този начин трябва да поставим някакъв етикет на възел, който ще заменим с параметър, в дървото. За да направите това, напишете метод за разширение, който ще симулира извикването на израза и ще бъде маркер.

public static class ExpressionExtension{ public static TFunc Call(този Expression израз) { throw new InvalidOperationException("Този метод никога не трябва да се извиква. Това е маркер за замяна."); }}

Сега можем да заменим един израз с друг

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

Необходимо е да се напише посетител, който ще замени метода Call с неговия параметър в дървото на изразите:

public class SubstituteExpressionCallVisitor :ExpressionVisitor{ private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtension).GetMethod(nameof(ExpressionExtension.Call)).GetGenericMethodDefinition(); } защитено отмяна Expression VisitMethodCall(MethodCallExpression възел) { if (IsMarker(node)) { return Visit(ExtractExpression(node)); } return base.VisitMethodCall(node); } private LambdaExpression ExtractExpression(възел на MethodCallExpression) { 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; }}

Можем да заменим нашия маркер:

public static Expression SubstituteMarker(този Expression израз){ var visitor =new SubstituteExpressionCallVisitor(); return (Expression)visitor.Visit(expression);}Expression> propertyGetter =продукт => product.Name;Expression> филтър =продукт => propertyGetter.Call ()(продукт).Contains("123");Expression> finalFilter =filter.SubstituteMarker();

При отстраняване на грешки можем да видим, че изразът не е това, което очаквахме. Филтърът все още съдържа метода Invoke.

Факт е, че изразите parameterGetter и finalFilter използват два различни аргумента. По този начин трябва да заменим аргумент в parameterGetter с аргумента в finalFilter. За да направим това, създаваме друг посетител:

Резултатът е следният:

public class SubstituteParameterVisitor :ExpressionVisitor{ private readonly LambdaExpression _expressionToVisit; частен речник само за четене _substitutionByParameter; public SubstituteParameterVisitor(Expression[] parameterSubstitutions, LambdaExpression expressionToVisit) { _expressionToVisit =ExpressionToVisit; _substitutionByParameter =expressionToVisit .Parameters .Select((параметър, индекс) => new {Parameter =parameter, Index =index}) .ToDictionary(pair => pair.Parameter, pair => parameterSubstitutions[pair.Index]); } public Expression Replace() { return Visit(_expressionToVisit.Body); } защитено замяна на Expression VisitParameter(ParameterExpression node) { Замяна на израз; if (_substitutionByParameter.TryGetValue(node, out substitution)) { return Visit(substitution); } върне base.VisitParameter(node); }} публичен клас SubstituteExpressionCallVisitor :ExpressionVisitor{ частен само за четене MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtensions) .GetMethod(nameof(ExpressionExtensions.Call)) .GetGenericMethodDefinition(); } защитено отмяна на Expression VisitInvocation(InvocationExpression възел) { 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(); обратно посещение(цел); } return base.VisitInvocation(node); } private LambdaExpression Unwrap(MethodCallExpression възел) { var target =node.Arguments[0]; return (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression възел) { return node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}

Сега всичко работи както трябва и най-накрая можем да напишем нашия метод за филтриране

public IQueryable FilterContainsText(IQueryable entities, Expression> getProperty, string text){ Expression> filter =entity => getProperty. Извикване()(обект). Съдържа(текст); връщане на обекти.Where(filter.SubstituteMarker());}

Заключение

Подходът със замяната на израза може да се използва не само за филтриране, но и за сортиране и всякакви заявки към базата данни.

Освен това този метод позволява съхраняване на изрази заедно с бизнес логиката отделно от заявките към базата данни.

Можете да разгледате кода в GitHub.

Тази статия е базирана на отговор на StackOverflow.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Модел на данни за сватбена организация

  2. Няколко бързи неща за обратната връзка PASS

  3. Ако използвате индексирани изгледи и MERGE, моля, прочетете това!

  4. Изпращане на данни от SentryOne към калкулатора DTU на базата данни на Azure SQL

  5. Подобрете производителността на базата данни с 400%