LINQ: Execução de consultas

LINQ

Durante a escrita de um teste para uma nova funcionalidade no sistema, reparei que havia da minha parte um esforço desnecessário para bater determinados valores em um relatório. Não era possível, tinha que ter algo de errado, pois minha lógica estava (humildemente falando) perfeita.

Fuça daqui, mexe ali e repentinamente os valores agora batem com toda a precisão matemática que era exigida. Inicio minha busca em prol de encontrar o erro e me deparo com algo interessante. Vamos fazer um teste para simular meu problema:

01

Tenho minha fonte de dados que neste caso é uma lista de inteiros. Note que tenho três itens adicionados a lista. Neste momento crio minha consulta LINQ para retornar apenas os itens maiores que 2. Pelo estado da lista, o correto era inferir que o único item maior que 2 é 3, logo somente um item vai ser retornado.

Então logo após a criação da consulta adiciono mais um item a lista. No momento da exibição do resultado o que vai ser retornado? Apenas o 3 ou 3 | 4 ?

?

?

?

?

O resultado retornado foi precisamente 3 | 4 |.

  Vamos fazer uma breve investigação sobre o ocorrido analisando a imagem abaixo:

02

  1. No primeiro quadrante temos a lista preenchida com 3 itens no momento da criação da consulta.
  2. No segundo quadrante a consulta já existe e aponta para a fonte de dados contendo 3 itens.
  3. No terceiro quadrante adicionamos um item a fonte de dados. No momento da exibição da consulta ela aponta para a fonte de dados agora contendo 4 itens.

 

Em consulta...

As operações de consulta LINQ seguem a seguinte lógica:

  1. É obtida a fonte de dados;
  2. A consulta é criada;
  3. A consulta é executada.

A consulta especifica quais informações recuperar a partir da(s) fonte(s) de dados indicadas. A consulta também pode especificar como as informações devem ser classificadas, agrupadas e como serão retornadas. Uma consulta é armazenada em uma variável de consulta e inicializada com uma expressão de consulta.

03

Parece-me bem lógico que uma consulta LINQ seja feita sobre uma fonte de dados, um DataSet, XML, coleção de objetos e etc. O que não tinha ficado muito claro era a separação entre criação e execução da consulta.

 

Execução da consulta

Uma função importante da maioria dos operadores de consulta é a capacidade de executarem não quando construídos, mas quando enumerados. Isto significa que a consulta é executada no momento que o método MoveNext() de IEnumerator é chamado.

Quando imprimimos o resultado do código da figura 1 percebemos que o número 4 está presente. Mesmo sendo inserido após a criação da consulta, ele está garantido no resultado porque somente na execução do foreach temos uma chamada a MoveNext() no enumerador da consulta.

É importante destacar que no LINQ, a variável de consulta não pratica nenhuma ação e não retorna nenhum dado. Ela apenas armazena as informações que são necessárias para produzir os resultados quando a consulta realmente for executada.

Com isso temos duas maneiras de executar as consultas LINQ: Execução tardia ou imediata.

 

Execução Tardia (Lazy Loading )

Basicamente a execução tardia atrasa a execução da consulta até o ultimo momento possível já que o LINQ atrasa a execução das queries até sua real necessidade. Este recurso é muito importante porque “separa” a construção da consulta de sua execução. Isto possibilita que você construa uma consulta em muitos passos e possibilita as consultas de LINQ to SQL.

Com isso você pode trafegar uma consulta entre camadas e incrementa-la de acordo com regras de negócio e outras necessidades.

Execução tardia também tem suas desvantagens. Sempre que uma consulta for enumerada ela sofre uma reavaliação tendo como base o estado atual da fonte de dados. Isto pode ser desvantajoso nos seguintes cenários:

  • Quando você quer guardar os resultados em um ponto determinado;
  • Quando temos uma consulta computacionalmente intensa, acessou remoto ou nas nuvens e queremos evitar o acesso desnecessário.

Para estes casos a melhor opção é trabalhar com a execução imediata.

 

Execução Imediata

Como vimos anteriormente a maioria dos operadores de consulta proporcionam execução tardia, com exceção de:

  • Operadores que retornam um único elemento ou valor escalar como First, Count, Max ou Average;
  • Operadores de conversão: ToArray, ToList, ToDictionary, ToLookUp.

Estes operadores realizam uma execução imediata da consulta pois seus mecanismos não dão suporte a execução tardia. Se repararmos a execução do método Count, é possível perceber que ele retorna apenas um simples inteiro que não é enumerado e portando não chama MoveNext.

A consulta abaixo é executada imediatamente e retorna o resultado tendo como base a lista no momento da criação da consulta. O número 4 foi desprezado.

04

Na execução tardia de consultas, a definição de consulta é armazenada em uma variável de consulta para posterior execução. Na execução imediata, a consulta é executada no momento da sua definição. Execução é disparada quando você aplica um método que requer acesso aos elementos individuais do resultado da consulta.

 

Finalizando

Vamos analisar mais um exemplo:

05

Somente utilizar um ToList ou Count não garante que sua consulta será executada de forma imediata, pois deve-se atentar em como faze-lo. Devemos utilizar os operadores no momento exato da criação da consulta ou então ele será um lazy loading de qualquer forma.

Como é possível notar, o que separa a execução tardia da execução imediata são apenas detalhes. Detalhes que fazem muita diferença em determinadas situações como no meu caso, sendo assim meu concelho é: Procure sempre investigar os recursos que a linguagem te oferece pois, muito provavelmente eles irão salvar seu dia…

Você pode baixar o código fonte deste artigo aqui!


Um grande abraço e ótimo estudo!


Author's profile picture

Vitor is a computer scientist who is passionate about creating software that will positively change the world we live in.

MVP Azure - Cloud Architect - Data science enthusiast


5 minutes to read