Database
 sql >> Base de Dados >  >> RDS >> Database

Usando expressões para filtrar dados do banco de dados


Gostaria de começar com uma descrição do problema que encontrei. Existem entidades no banco de dados que precisam ser exibidas como tabelas na interface do usuário. O Entity Framework é usado para acessar o banco de dados. Existem filtros para essas colunas da tabela.

É necessário escrever um código para filtrar as entidades por parâmetros.

Por exemplo, existem duas entidades:Usuário e Produto.
public class User{ public int Id { get; definir; } public string Nome { get; definir; }}public class Produto{ public int Id { get; definir; } public string Nome { get; definir; }}

Suponha que precisamos filtrar usuários e produtos por nome. Criamos métodos para filtrar cada entidade.
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));}

Como você pode ver, esses dois métodos são quase idênticos e diferem apenas na propriedade da entidade, pela qual filtra os dados.

Pode ser um desafio se tivermos dezenas de entidades com dezenas de campos que requerem filtragem. A complexidade está no suporte ao código, na cópia impensada e, como resultado, no desenvolvimento lento e na alta probabilidade de erro.

Parafraseando Fowler, começa a cheirar mal. Eu gostaria de escrever algo padrão em vez de duplicação de código. Por exemplo:
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 entidades, Func getProperty, string text){ return entidades. Where(entity => getProperty(entity).Contains(text));}

Infelizmente, se tentarmos filtrar:
public void TestFilter(){ using (var context =new Context()) { var filtradoProdutos =FilterProductsByName(context.Products, "name").ToArray(); }}

Receberemos o erro «O método de teste ExpressionTests.ExpressionTest.TestFilter lançou a exceção:
System.NotSupportedException :O tipo de nó de expressão LINQ 'Invoke' não é suportado em LINQ to Entities.

Expressões


Vamos verificar o que deu errado.

O método Where aceita um parâmetro do tipo Expression>. Assim, o Linq trabalha com árvores de expressão, pelas quais constrói consultas SQL, e não com delegados.

A Expressão descreve uma árvore de sintaxe. Para entender melhor como eles são estruturados, considere a expressão, que verifica se um nome é igual a uma linha.
Expressão> esperado =produto => produto.Nome =="destino";

Ao depurar, podemos ver a estrutura dessa expressão (as propriedades principais estão marcadas em vermelho).



Temos a seguinte árvore:



Ao passar um delegado como parâmetro, uma árvore diferente é gerada, que chama o método Invoke no parâmetro (delegate) em vez de invocar a propriedade da entidade.

Quando o Linq está tentando construir uma consulta SQL por esta árvore, ele não sabe como interpretar o método Invoke e lança NotSupportedException.

Assim, nossa tarefa é substituir a conversão para a propriedade da entidade (a parte da árvore marcada em vermelho) pela expressão que é passada através deste parâmetro.

Vamos tentar:
Expressão> propertyGetter =produto => product.Name;Expression> filter =product => propertyGetter(product) =="destino"

Agora, podemos ver o erro «Nome do método esperado» na fase de compilação.

O problema é que uma expressão é uma classe que representa os nós de uma árvore sintática, em vez do delegado, e não pode ser chamada diretamente. Agora, a principal tarefa é encontrar uma maneira de criar uma expressão passando outro parâmetro para ela.

O visitante


Após uma breve pesquisa no Google, encontrei uma solução para o problema semelhante no StackOverflow.

Para trabalhar com expressões, existe a classe ExpressionVisitor, que utiliza o padrão Visitor. Ele é projetado para percorrer todos os nós da árvore de expressão na ordem de análise da árvore sintática e permite modificá-los ou retornar outro nó. Se nem o nó nem seus nós filhos forem alterados, a expressão original será retornada.

Ao herdar da classe ExpressionVisitor, podemos substituir qualquer nó da árvore pela expressão, que passamos por meio do parâmetro. Assim, precisamos colocar algum rótulo de nó, que substituiremos por um parâmetro, na árvore. Para fazer isso, escreva um método de extensão que simule a chamada da expressão e seja um marcador.
public static class ExpressionExtension{ public static TFunc Call(esta Expression expressão) { throw new InvalidOperationException("Este método nunca deve ser chamado. É um marcador para substituição."); }}

Agora, podemos substituir uma expressão por outra
Expression> propertyGetter =product => product.Name;Expression> filter =product => propertyGetter.Call()(product) =="target"; 
É necessário escrever um visitante, que substituirá o método Call pelo seu parâmetro na árvore de expressões:
classe pública SubstituteExpressionCallVisitor :ExpressionVisitor{ private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtension).GetMethod(nameof(ExpressionExtension.Call)).GetGenericMethodDefinition(); } substituição protegida Expressão VisitMethodCall(MethodCallExpression node) { if (IsMarker(node)) { return Visit(ExtractExpression(node)); } return base.VisitMethodCall(node); } private LambdaExpression ExtractExpression(MethodCallExpression node) { var target =node.Arguments[0]; return (ExpressãoLambda)Expressão.Lambda(destino).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { return node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}

Podemos substituir nosso marcador:
public static Expression SubstituteMarker(esta Expressão expressão){ var visitor =new SubstituteExpressionCallVisitor(); return (Expression)visitor.Visit(expression);}Expression> propertyGetter =produto => product.Name;Expression> filter =product => propertyGetter.Call ()(produto).Contains("123");Expressão> finalFilter =filter.SubstituteMarker();

Na depuração, podemos ver que a expressão não é o que esperávamos. O filtro ainda contém o método Invoke.



O fato é que as expressões parameterGetter e finalFilter usam dois argumentos diferentes. Assim, precisamos substituir um argumento em parameterGetter pelo argumento em finalFilter. Para fazer isso, criamos outro visitante:

O resultado é o seguinte:
classe pública SubstituteParameterVisitor :ExpressionVisitor{ private readonly LambdaExpression _expressionToVisit; private readonly Dictionary _substitutionByParameter; public SubstituteParameterVisitor(Expression[] parameterSubstitutions, LambdaExpression expressionToVisit) { _expressionToVisit =expressionToVisit; _substitutionByParameter =expressionToVisit .Parameters .Select((parâmetro, índice) => new {Parâmetro =parâmetro, Índice =índice}) .ToDictionary(par => par.Parâmetro, par => parâmetroSubstituições[par.Índice]); } public Expression Replace() { return Visit(_expressionToVisit.Body); } substituição protegida Expressão VisitParameter(nó ParameterExpression) { Substituição de expressão; if (_substitutionByParameter.TryGetValue(nó, substituição de saída)) { return Visita(substituição); } return base.VisitParameter(node); }}public class SubstituteExpressionCallVisitor :ExpressionVisitor{ private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtensions) .GetMethod(nameof(ExpressionExtensions.Call)) .GetGenericMethodDefinition(); } substituição protegida Expressão VisitInvocation(nó 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 alvo =parâmetroReplacer.Replace(); visita de retorno(destino); } return base.VisitInvocation(node); } private LambdaExpression Unwrap(MethodCallExpression node) { var target =node.Arguments[0]; return (ExpressãoLambda)Expressão.Lambda(destino).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { return node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}

Agora, tudo funciona como deveria e nós, finalmente, podemos escrever nosso método de filtragem


public IQueryable FilterContainsText(entidades IQueryable, Expression> getProperty, string text){ Expression> filter =entity => getProperty. Call()(entidade).Contains(texto); return entidades.Where(filter.SubstituteMarker());}

Conclusão


A abordagem com a substituição da expressão pode ser usada não apenas para filtragem, mas também para ordenação e qualquer consulta ao banco de dados.

Além disso, esse método permite armazenar expressões junto com a lógica de negócios separadamente das consultas ao banco de dados.

Você pode ver o código no GitHub.

Este artigo é baseado em uma resposta do StackOverflow.