Como posso consultar os valores nulos na estrutura da entidade?

109

Eu quero executar uma consulta como esta

   var result = from entry in table
                     where entry.something == null
                     select entry;

e obter um IS NULLgerado.

Editado: Após as duas primeiras respostas, sinto a necessidade de esclarecer que estou usando o Entity Framework e não Linq to SQL. O método object.Equals () não parece funcionar no EF.

Edição nº 2: A consulta acima funciona conforme o esperado. É gerado corretamente IS NULL. Meu código de produção, entretanto, era

value = null;
var result = from entry in table
                         where entry.something == value
                         select entry;

e o SQL gerado era something = @p; @p = NULL. Parece que EF traduz corretamente a expressão constante, mas se uma variável estiver envolvida, ela a trata como uma comparação normal. Faz sentido, na verdade. Vou fechar essa questão

Adrian Zanescu
fonte
17
Acho que realmente não faz sentido ... O conector deve ser um pouco inteligente e não nos pedir para fazer o seu trabalho: realizar uma tradução correta em SQL da consulta C # correta. Isso gera um comportamento inesperado.
Julien N
6
Estou com Julien, isso é um fracasso por parte da EF
Sr. Bell
1
Isso é uma falha dos padrões e só está piorando agora que a comparação com o valor nulo resulta permanentemente em indefinido no SQL Server 2016 com ANSI NULLs permanentemente ativados. Nulo pode representar um valor desconhecido, mas "nulo" em si não é um valor desconhecido. A comparação de um valor nulo com um valor nulo deve resultar absolutamente verdadeiro, mas infelizmente o padrão se afasta do bom senso, bem como da lógica booleana.
Triynko,

Respostas:

126

Solução alternativa para Linq-to-SQL:

var result = from entry in table
             where entry.something.Equals(value)
             select entry;

Solução alternativa para Linq-to-Entities (ouch!):

var result = from entry in table
             where (value == null ? entry.something == null : entry.something == value)
             select entry;

Este é um bug desagradável que me mordeu várias vezes. Se este bug afetou você também, visite o relatório de bug no UserVoice e informe a Microsoft que este bug afetou você também.


Edit: Este bug está sendo corrigido no EF 4.5 ! Obrigado a todos por votar a favor deste bug!

Para compatibilidade com versões anteriores, será opcional - você precisa habilitar manualmente uma configuração para que entry == valuefuncione. Nenhuma palavra ainda sobre o que é essa configuração. Fique ligado!


Edição 2: De acordo com esta postagem da equipe EF, esse problema foi corrigido no EF6! Uau!

Alteramos o comportamento padrão do EF6 para compensar a lógica de três valores.

Isso significa que o código existente que depende do comportamento antigo ( null != nullmas apenas quando comparado a uma variável) precisará ser alterado para não depender desse comportamento ou definido UseCSharpNullComparisonBehaviorcomo false para usar o comportamento antigo quebrado.

BlueRaja - Danny Pflughoeft
fonte
6
Votei no relatório do bug. Espero que eles consertem isso. Não posso dizer que realmente me lembro desse bug estar presente na versão beta do vs2010 ...
noobish,
2
oh vamos lá microsoft ... sério?!?!? Na versão 4.1?!?! +1
David
1
Essa solução alternativa Linq-To-SQL não parece funcionar (tentando com um Guid?). Usar a solução alternativa de entidades funciona em L2S, mas gera SQL horrível. Tive que fazer uma instrução if no código(var result = from ...; if(value.HasValue) result = result.Where(e => e.something == value) else result = result.Where(e => e.something == null);
Michael Stum
5
Object.Equals funciona na verdade(where Object.Equals(entry.something,value))
Michael Stum
5
@ leen3o (ou qualquer outra pessoa) - Alguém já descobriu onde está essa suposta correção no EF 4.5 / 5.0? Estou usando o 5.0 e ainda está funcionando mal.
Shaul Behr
17

Desde o Entity Framework 5.0, você pode usar o seguinte código para resolver seu problema:

public abstract class YourContext : DbContext
{
  public YourContext()
  {
    (this as IObjectContextAdapter).ObjectContext.ContextOptions.UseCSharpNullComparisonBehavior = true;
  }
}

Isso deve resolver seus problemas, pois o Entity Framerwork usará a comparação nula 'C # like'.

ITmeze
fonte
16

Há uma solução um pouco mais simples que funciona com LINQ to Entities:

var result = from entry in table
         where entry.something == value || (value == null && entry.something == null)
         select entry;

Isso funciona porque, como AZ observou, LINQ to Entities casos especiais x == null (ou seja, uma comparação de igualdade contra a constante nula) e traduz para x IS NULL.

No momento, estamos considerando alterar esse comportamento para introduzir as comparações de compensação automaticamente se ambos os lados da igualdade forem anuláveis. No entanto, existem alguns desafios:

  1. Isso poderia quebrar o código que já depende do comportamento existente.
  2. A nova tradução pode afetar o desempenho das consultas existentes, mesmo quando um parâmetro nulo raramente é usado.

Em qualquer caso, se vamos trabalhar nisso vai depender muito da prioridade relativa que nossos clientes atribuem a isso. Se você se preocupa com o problema, incentivo-o a votar em nosso novo site de sugestões de recursos: https://data.uservoice.com .

divega
fonte
9

Se for um tipo anulável, talvez tente usar a propriedade HasValue?

var result = from entry in table
                 where !entry.something.HasValue
                 select entry;

Não tem nenhum EF para testar aqui ... apenas uma sugestão =)

Svish
fonte
1
Bem ... isso só funciona se você estiver procurando apenas nulos, mas o uso == nullnão é atingido pelo bug de qualquer maneira. O objetivo é filtrar pelo valor de uma variável, cujo valor pode ser nulo, e fazer com que o valor nulo encontre os registros nulos.
Dave Cousineau
1
Sua resposta me salvou. Esqueci de usar um tipo anulável em minha classe de modelo de entidade e não consegui fazer (x => x.Column == null)funcionar. :)
Reuel Ribeiro
Isso dá System.NullReferenceException , visto que o objeto allready é nulo!
TiyebM
5

para lidar com comparações nulas, use em Object.Equals()vez de==

verifique esta referência

Oscar Cabrero
fonte
Isso funciona perfeitamente no Linq-To-Sql e também gera o SQL adequado (algumas outras respostas aqui geram SQL horrendo ou resultados errados).
Michael Stum
Suponha que eu queira compaire com null, Object.Equals(null)e se o Objectpróprio for nulo?
TiyebM
4

Ressaltando que todas as sugestões do Entity Framework <6.0 geram um SQL estranho. Veja o segundo exemplo para correção "limpa".

Solução Alternativa Ridícula

// comparing against this...
Foo item = ...

return DataModel.Foos.FirstOrDefault(o =>
    o.ProductID == item.ProductID
    // ridiculous < EF 4.5 nullable comparison workaround http://stackoverflow.com/a/2541042/1037948
    && item.ProductStyleID.HasValue ? o.ProductStyleID == item.ProductStyleID : o.ProductStyleID == null
    && item.MountingID.HasValue ? o.MountingID == item.MountingID : o.MountingID == null
    && item.FrameID.HasValue ? o.FrameID == item.FrameID : o.FrameID == null
    && o.Width == w
    && o.Height == h
    );

resulta em SQL como:

SELECT TOP (1) [Extent1].[ID]                 AS [ID],
       [Extent1].[Name]               AS [Name],
       [Extent1].[DisplayName]        AS [DisplayName],
       [Extent1].[ProductID]          AS [ProductID],
       [Extent1].[ProductStyleID]     AS [ProductStyleID],
       [Extent1].[MountingID]         AS [MountingID],
       [Extent1].[Width]              AS [Width],
       [Extent1].[Height]             AS [Height],
       [Extent1].[FrameID]            AS [FrameID],
FROM   [dbo].[Foos] AS [Extent1]
WHERE  (CASE
  WHEN (([Extent1].[ProductID] = 1 /* @p__linq__0 */)
        AND (NULL /* @p__linq__1 */ IS NOT NULL)) THEN
    CASE
      WHEN ([Extent1].[ProductStyleID] = NULL /* @p__linq__2 */) THEN cast(1 as bit)
      WHEN ([Extent1].[ProductStyleID] <> NULL /* @p__linq__2 */) THEN cast(0 as bit)
    END
  WHEN (([Extent1].[ProductStyleID] IS NULL)
        AND (2 /* @p__linq__3 */ IS NOT NULL)) THEN
    CASE
      WHEN ([Extent1].[MountingID] = 2 /* @p__linq__4 */) THEN cast(1 as bit)
      WHEN ([Extent1].[MountingID] <> 2 /* @p__linq__4 */) THEN cast(0 as bit)
    END
  WHEN (([Extent1].[MountingID] IS NULL)
        AND (NULL /* @p__linq__5 */ IS NOT NULL)) THEN
    CASE
      WHEN ([Extent1].[FrameID] = NULL /* @p__linq__6 */) THEN cast(1 as bit)
      WHEN ([Extent1].[FrameID] <> NULL /* @p__linq__6 */) THEN cast(0 as bit)
    END
  WHEN (([Extent1].[FrameID] IS NULL)
        AND ([Extent1].[Width] = 20 /* @p__linq__7 */)
        AND ([Extent1].[Height] = 16 /* @p__linq__8 */)) THEN cast(1 as bit)
  WHEN (NOT (([Extent1].[FrameID] IS NULL)
             AND ([Extent1].[Width] = 20 /* @p__linq__7 */)
             AND ([Extent1].[Height] = 16 /* @p__linq__8 */))) THEN cast(0 as bit)
END) = 1

Outrageous Workaround

Se você deseja gerar um SQL mais limpo, algo como:

// outrageous < EF 4.5 nullable comparison workaround http://stackoverflow.com/a/2541042/1037948
Expression<Func<Foo, bool>> filterProductStyle, filterMounting, filterFrame;
if(item.ProductStyleID.HasValue) filterProductStyle = o => o.ProductStyleID == item.ProductStyleID;
else filterProductStyle = o => o.ProductStyleID == null;

if (item.MountingID.HasValue) filterMounting = o => o.MountingID == item.MountingID;
else filterMounting = o => o.MountingID == null;

if (item.FrameID.HasValue) filterFrame = o => o.FrameID == item.FrameID;
else filterFrame = o => o.FrameID == null;

return DataModel.Foos.Where(o =>
    o.ProductID == item.ProductID
    && o.Width == w
    && o.Height == h
    )
    // continue the outrageous workaround for proper sql
    .Where(filterProductStyle)
    .Where(filterMounting)
    .Where(filterFrame)
    .FirstOrDefault()
    ;

resulta no que você queria em primeiro lugar:

SELECT TOP (1) [Extent1].[ID]                 AS [ID],
           [Extent1].[Name]               AS [Name],
           [Extent1].[DisplayName]        AS [DisplayName],
           [Extent1].[ProductID]          AS [ProductID],
           [Extent1].[ProductStyleID]     AS [ProductStyleID],
           [Extent1].[MountingID]         AS [MountingID],
           [Extent1].[Width]              AS [Width],
           [Extent1].[Height]             AS [Height],
           [Extent1].[FrameID]            AS [FrameID],
FROM   [dbo].[Foos] AS [Extent1]
WHERE  ([Extent1].[ProductID] = 1 /* @p__linq__0 */)
   AND ([Extent1].[Width] = 16 /* @p__linq__1 */)
   AND ([Extent1].[Height] = 20 /* @p__linq__2 */)
   AND ([Extent1].[ProductStyleID] IS NULL)
   AND ([Extent1].[MountingID] = 2 /* @p__linq__3 */)
   AND ([Extent1].[FrameID] IS NULL)
drzaus
fonte
O código em execução no SQL será mais limpo e rápido, mas o EF gerará e armazenará em cache um novo plano de consulta para cada combinação antes de enviá-lo ao servidor sql, o que o torna mais lento do que outras soluções alternativas.
Burak Tamtürk de
2
var result = from entry in table
                     where entry.something == null
                     select entry;

A consulta acima funciona conforme o esperado. Ele gera IS NULL corretamente. Meu código de produção, entretanto, era

var value = null;
var result = from entry in table
                         where entry.something == value
                         select entry;

e o SQL gerado era algo = @p; @p = NULL. Parece que EF traduz corretamente a expressão constante, mas se uma variável estiver envolvida, ela a trata como uma comparação normal. Faz sentido, na verdade.

Adrian Zanescu
fonte
1

Parece que o Linq2Sql também tem esse "problema". Parece que há um motivo válido para esse comportamento devido ao fato de ANSI NULLs estarem ON ou OFF, mas confunde a mente por que um "== null" direto na verdade funcionará como você esperaria.

JasonCoder
fonte
1

Pessoalmente, prefiro:

var result = from entry in table    
             where (entry.something??0)==(value??0)                    
              select entry;

sobre

var result = from entry in table
             where (value == null ? entry.something == null : entry.something == value)
             select entry;

porque evita a repetição - embora isso não seja matematicamente exato, mas se encaixa bem na maioria dos casos.

Vincent Courcelle
fonte
0

Não posso comentar o post da divega, mas entre as diferentes soluções apresentadas aqui, a solução da divega produz o melhor SQL. Tanto o desempenho quanto o comprimento. Acabei de verificar com o SQL Server Profiler e olhando o plano de execução (com "SET STATISTICS PROFILE ON").

Buginator
fonte
0

Infelizmente, no Entity Framework 5 DbContext o problema ainda não foi corrigido.

Usei esta solução alternativa (funciona com MSSQL 2012, mas a configuração ANSI NULLS pode ser preterida em qualquer versão futura do MSSQL).

public class Context : DbContext
{

    public Context()
        : base("name=Context")
    {
        this.Database.Connection.StateChange += Connection_StateChange;
    }

    void Connection_StateChange(object sender, System.Data.StateChangeEventArgs e)
    {
        // Set ANSI_NULLS OFF when any connection is opened. This is needed because of a bug in Entity Framework
        // that is not fixed in EF 5 when using DbContext.
        if (e.CurrentState == System.Data.ConnectionState.Open)
        {
            var connection = (System.Data.Common.DbConnection)sender;
            using (var cmd = connection.CreateCommand())
            {
                cmd.CommandText = "SET ANSI_NULLS OFF";
                cmd.ExecuteNonQuery();
            }
        }
    }
}

Deve-se observar que é uma solução alternativa suja, mas que pode ser implementada muito rapidamente e funciona para todas as consultas.

Knaģis
fonte
Isso deixará de funcionar imediatamente quando ANSI NULLS for permanentemente definido como ON em uma versão futura do SQL Server, caso o aviso não tenha sido claro.
Triynko
0

Se você preferir usar a sintaxe do método (lambda) como eu, você poderia fazer a mesma coisa assim:

var result = new TableName();

using(var db = new EFObjectContext)
{
    var query = db.TableName;

    query = value1 == null 
        ? query.Where(tbl => tbl.entry1 == null) 
        : query.Where(tbl => tbl.entry1 == value1);

    query = value2 == null 
        ? query.Where(tbl => tbl.entry2 == null) 
        : query.Where(tbl => tbl.entry2 == value2);

    result = query
        .Select(tbl => tbl)
        .FirstOrDefault();

   // Inspect the value of the trace variable below to see the sql generated by EF
   var trace = ((ObjectQuery<REF_EQUIPMENT>) query).ToTraceString();

}

return result;
John Meyer
fonte
-1
var result = from entry in table    
             where entry.something == value||entry.something == null                   
              select entry;

Use isso

Andrew
fonte
5
Isso está MUITO errado porque selecionará todas as entradas em que o valor corresponda E todas as entradas em que algo seja nulo, mesmo se você solicitar um valor.
Michael Stum