Superior por grupo: Take (1) funciona, mas FirstOrDefault () não funciona?

9

Estou usando o EF 4.3.1 ... atualizado para o 4.4 (problema permanece) com as primeiras entidades POCO geradas pelo EF 4.x DbContext Generator . Eu tenho o seguinte banco de dados chamado 'Wiki' (script SQL para criar tabelas e dados é aqui ):

Quando um artigo wiki é editado, em vez de seu registro ser atualizado, a nova revisão é inserida como um novo registro com o contador de revisão incrementado. No meu banco de dados há um autor, "John Doe", que tem dois artigos, "Artigo A" e "Artigo B", onde o artigo A tem duas versões (1 e 2), mas o artigo B tem apenas uma versão. p>

Eu tenho o carregamento lento e a criação de proxy desativada ( aqui é a solução de amostra que estou usando com o LINQPad). Desejo obter as revisões mais recentes de artigos criados por pessoas cujo nome começa com "João", portanto, faço a seguinte consulta:

Authors.Where(au => au.Name.StartsWith("John"))
       .Select(au => au.Articles.GroupBy(ar => ar.Title)
                                .Select(g => g.OrderByDescending(ar => ar.Revision)
                                              .FirstOrDefault()))

Isso produz o resultado errado e recupera apenas o primeiro artigo:

Fazendo uma pequena alteração na consulta, substituindo .FirstOrDefault() por .Take(1) resulta na seguinte consulta:

Authors.Where(au => au.Name.StartsWith("John"))
       .Select(au => au.Articles.GroupBy(ar => ar.Title)
                                .Select(g => g.OrderByDescending(ar => ar.Revision)
                                              .Take(1)))

Surpreendentemente, esta consulta produz resultados corretos (embora com mais aninhamento):

Assumi que a EF está gerando consultas SQL ligeiramente diferentes, uma que retorna apenas a revisão mais recente de um único artigo, e a outra retorna a revisão mais recente de todos os artigos. O SQL feio gerado pelas duas consultas diferem apenas ligeiramente (compare: SQL para .FirstOrDefault () vs SQL para .Take (1) ), mas ambos retornam o resultado correto:

.FirstOrDefault()

.Take(1) (ordem de coluna reorganizada para fácil comparação)

O culpado, portanto, não é o SQL gerado, mas a interpretação do resultado pelo EF. Por que a EF está interpretando o primeiro resultado em uma única Article instance enquanto interpreta o segundo resultado como two Article instances? Por que a primeira consulta retorna resultados incorretos?

EDITAR: eu abri um relatório de erros no Connect. Por favor, faça um upvote se achar que é importante corrigir este problema.

    
por Allon Guralnek 27.08.2012 в 10:10
fonte

3 respostas

3

Olhando para: Ссылка
="http://msdn.microsoft.com/pt-br/library/bb503062.aspx"> Tópicos
Há uma explicação muito boa sobre como o Take funciona (preguiçoso, começo de moda), mas não é nada do FirstOrDefault. Além do mais, vendo a explicação do Take, eu gostaria de estimar que as consultas com o Take podem reduzir o número de linhas devido a um tente emular a avaliação lenta no SQL, e seu caso indica que é o contrário! Eu não entendo porque você está observando esse efeito.

Provavelmente é apenas específico para implementação. Para mim, Take (1) e FirstOrDefault podem parecer com TOP 1 , mas do ponto de vista funcional, pode haver uma pequena diferença em sua 'preguiça': uma função pode avalie todos os elementos e retorne primeiro, segundo pode avaliar primeiro, depois devolvê-lo e interromper a avaliação. É apenas uma "dica" sobre o que pode ter acontecido. Para mim, é um absurdo, porque não vejo nenhum documento sobre este assunto e, em geral, tenho certeza de que ambos os Take / FirstOrDefault são preguiçosos e devem avaliar apenas os primeiros N elementos.

Na primeira parte de sua consulta, o grupo.Selecionar + ordemBy + TOP1 é uma "indicação clara" de que você está interessado na única linha com o maior 'valor' em uma coluna por grupo - mas, de fato, há nenhuma maneira simples de declarar isso no SQL , então a indicação não é tão clara para o mecanismo SQL nem para o mecanismo EF.

Quanto a mim, o comportamento que você apresenta pode indicar que o FirstOrDefault foi 'propagado' pelo tradutor de EF para cima em uma camada de consultas internas em excesso, como se fosse o Articles.GroupBy () (você tem certeza de que não perdeu parens adter the OrderBy? :)) - e isso seria um bug.

Mas -

Como a diferença deve estar em algum lugar no significado e / ou ordem de execução, vamos ver o que a EF pode adivinhar sobre o significado da sua consulta. Como a entidade de autor recebe seus artigos? Como a EF sabe qual artigo é vincular ao seu autor? Claro, a propriedade nav. Mas como acontece que apenas alguns dos artigos são pré-carregados? Parece simples - a consulta retorna alguns resultados com colunas vem, colunas descrevem autor inteiro e artigos inteiros, então permite mapeá-los para autores e artigos e permite combiná-los uns com os outros nas teclas de navegação. ESTÁ BEM. Mas adicione o complexo de filtragem a isso ..?

Com um filtro simples, como por data, é uma subconsulta única para todos os artigos, as linhas são truncadas por data e todas as linhas são consumidas. Mas como escrever uma consulta complexa que usaria várias ordenações intermediárias e produzir vários subconjuntos de artigos? Qual subconjunto deve ser vinculado ao autor resultante? União de todos eles? Isso anularia todas as cláusulas do tipo de nível superior. Primeiro deles? Absurdo, as primeiras subconsultas tendem a ser ajudantes intermediários. Então, provavelmente, quando uma consulta é vista como um conjunto de subconsultas com estrutura semelhante que pode ser tomada como a fonte de dados para um carregamento parcial de uma propriedade de navegação, provavelmente a última subconsulta é tomada como o resultado real. Isso é tudo pensamento abstrato, mas me fez notar que Take () versus FirstOrDefault e seu significado geral Join versus LeftJoin poderiam de fato mudar a ordem de varredura do conjunto de resultados e, de alguma forma, Take () foi de alguma forma otimizado e feito em uma varredura sobre todo o resultado, visitando todos os artigos do autor de uma só vez, e o FirstOrDefault foi executado como scan direto for each author * for each title-group * select top one and check count and substitue for null que muitas vezes produziu pequenas coleções de artigos por cada autor, e assim resultou em um resultado - vindo apenas do último agrupamento de títulos visitado.

Esta é a única explicação em que consigo pensar, exceto pelo óbvio "BUG!" gritar. Como usuário do LINQ, para mim, ainda é um bug. Essa otimização não deveria ter ocorrido, ou deveria incluir o FirstOrDef também - como é o mesmo que Take (1) .DefaultIfEmpty (). Heh, a propósito - você já tentou isso? Como eu disse, Take (1) não é o mesmo que FirstOrDefault devido ao significado JOIN / LEFTJOIN - mas Take (1) .DefaultIfEmpty () é na verdade semanticamente o mesmo. Pode ser divertido ver quais consultas SQL são produzidas no SQL e o que resulta em camadas EF.

Eu tenho que admitir, que a seleção das entidades relacionadas no carregamento parcial nunca foi clara para mim e eu realmente não usei o carregamento parcial por muito tempo, como sempre afirmei o consultas para que os resultados e agrupamentos sejam explicitamente definidos (*). Assim, eu poderia simplesmente ter esquecido algum aspecto / regra / definição chave do seu trabalho interno e talvez, isto é. Na verdade, é para selecionar todos os formulários de registros relacionados ao conjunto de resultados (não apenas a última subcoleção, como descrevi agora). Se eu tivesse esquecido alguma coisa, tudo o que acabei de descrever seria obviamente errado.

(*) No seu caso, eu transformaria o Article.AuthorID em uma propriedade de nav (Autor de Autor público definido) e, em seguida, reescreveria a consulta de forma semelhante a ser mais plana / pipeline, como:

var aths = db.Articles
              .GroupBy(ar => new {ar.Author, ar.Title})
              .Take(10)
              .Select(grp => new {grp.Key.Author, Arts = grp.OrderByDescending(ar => ar.Revision).Take(1)} )

e, em seguida, preencha a Visualização com pares de Autor e Artes separadamente, em vez de tentar preencher parcialmente o autor e usar somente autor. Btw. Eu não testei contra o EF e SServer, é apenas um exemplo de 'invertendo a consulta de cabeça para baixo' e 'achatando' as subconsultas no caso de JOINs e é inutilizável para LEFTJOINs, então se você quiser ver também o autores sem artigos, tem que partir dos Autores como sua consulta original ..

Espero que estes pensamentos soltos ajudem um pouco a encontrar "porquê".

    
por quetzalcoatl 27.08.2012 / 12:01
fonte
2

O método FirstOrDefault() é instantâneo enquanto o outro ( Take(int) ) é adiado até a execução.

    
por AgentFire 27.08.2012 / 19:59
fonte
0

Como na resposta anterior, tentei argumentar sobre o problema - renunciei, e estou escrevendo outro :) Depois de olhar de novo, acho que é um bug. Eu acho que você deveria apenas usar o Take e postar o caso no Microsoft's Connect e checar o que eles dizem sobre isso.

Veja o que eu encontrei: Ссылка

A resposta do 'Microsoft 2011-09-22 às 16:07' descreve detalhadamente alguns mecanismos de otimização dentro da EF. Em alguns lugares eles dizem sobre a reordenação de skip / take / orderby e que às vezes a lógica não reconhece algumas construções. Eu acho que você acabou de encontrar outro caso de canto que ainda não foi ramificado corretamente na 'orderby lifting'. Ao todo, no SQL resultante, você tem o select-top-1 dentro de um order-by, e o dano parece exatamente como levantar o 'top 1' um nível muito alto!

    
por quetzalcoatl 27.08.2012 / 18:44
fonte