Jak mogę wysyłać zapytania o wartości null w ramach jednostki?

109

Chcę wykonać takie zapytanie

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

i uzyskaj IS NULLwygenerowany plik .

Edytowano: Po pierwszych dwóch odpowiedziach czuję potrzebę wyjaśnienia, że ​​używam Entity Framework, a nie Linq to SQL. Wydaje się, że metoda object.Equals () nie działa w EF.

Edycja nr 2: powyższe zapytanie działa zgodnie z zamierzeniami. Generuje poprawnie IS NULL. Jednak mój kod produkcyjny był

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

a wygenerowany kod SQL to something = @p; @p = NULL. Wydaje się, że EF poprawnie tłumaczy stałe wyrażenie, ale jeśli zaangażowana jest zmienna, traktuje ją tak jak normalne porównanie. Właściwie to ma sens. Zamknę to pytanie

Adrian Zanescu
źródło
17
Myślę, że to nie ma sensu ... Łącznik powinien być trochę sprytny i nie prosić nas o wykonanie swojej pracy: wykonać poprawne tłumaczenie w SQL poprawnego zapytania C #. To generuje nieoczekiwane zachowanie.
Julien N
6
Jestem z Julienem, to porażka ze strony EF
pana Bella
1
Jest to awaria standardów, a teraz jest jeszcze gorzej, gdy porównanie z wartością null powoduje trwale niezdefiniowanie od SQL Server 2016 z ustawionymi na stałe wartościami ANSI NULL. Null może reprezentować nieznaną wartość, ale samo „null” nie jest nieznaną wartością. Porównanie wartości zerowej z wartością zerową powinno absolutnie dać prawdę, ale niestety standard odbiega zarówno od zdrowego rozsądku, jak i logiki Boole'a.
Triynko

Odpowiedzi:

126

Obejście problemu z Linq-to-SQL:

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

Obejście problemu z linq-to-Entities (ouch!):

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

To paskudny błąd, który ugryzł mnie kilka razy. Jeśli ten błąd dotyczy również Ciebie, odwiedź raport o błędzie w witrynie UserVoice i poinformuj firmę Microsoft, że ten błąd dotyczy również Ciebie.


Edycja: ten błąd jest naprawiany w EF 4.5 ! Dziękujemy wszystkim za głosowanie za tym błędem!

Aby zapewnić kompatybilność wsteczną, będzie to opt-in - musisz ręcznie włączyć ustawienie, aby entry == valuedziałało. Nie wiadomo jeszcze, czym jest to ustawienie. Bądźcie czujni!


Edycja 2: Zgodnie z tym postem zespołu EF, ten problem został rozwiązany w EF6! Woohoo!

Zmieniliśmy domyślne zachowanie EF6, aby skompensować logikę trójwartościową.

Oznacza to, że istniejący kod, który opiera się na starym zachowaniu ( null != nullale tylko przy porównywaniu ze zmienną), będzie musiał zostać zmieniony, aby nie polegał na tym zachowaniu, lub ustawić UseCSharpNullComparisonBehaviorna fałsz, aby używać starego zepsutego zachowania.

BlueRaja - Danny Pflughoeft
źródło
6
Głosowałem za zgłoszeniem błędu. Miejmy nadzieję, że to naprawią. Nie mogę powiedzieć, że naprawdę pamiętam ten błąd obecny w vs2010 beta ...
noobish
2
och, daj spokój Microsoft ... naprawdę?!?!? W wersji 4.1?!?! +1
David
1
To obejście Linq-To-SQL wydaje się nie działać (próbujesz z Guid?). Użycie Entities-Obejście działa w L2S, ale generuje przerażający SQL. Musiałem wykonać instrukcję if w kodzie(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 faktycznie działa(where Object.Equals(entry.something,value))
Michael Stum
5
@ leen3o (lub ktokolwiek inny) - Czy ktoś jeszcze znalazł, gdzie ta domniemana poprawka jest w EF 4.5 / 5.0? Używam 5.0 i nadal źle się zachowuje.
Shaul Behr
17

Od Entity Framework 5.0 możesz użyć następującego kodu w celu rozwiązania problemu:

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

Powinno to rozwiązać twoje problemy, ponieważ Entity Framerwork użyje porównania zerowego w języku C #.

ITmeze
źródło
16

Istnieje nieco prostsze obejście, które działa z LINQ to Entities:

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

Działa to, ponieważ, jak zauważyła AZ, LINQ to Entities przypadki specjalne x == null (tj. Porównanie równości ze stałą zerową) i tłumaczy to na x IS NULL.

Obecnie rozważamy zmianę tego zachowania, aby automatycznie wprowadzić porównania kompensujące, jeśli obie strony równości nie dopuszczają wartości null. Jest jednak kilka wyzwań:

  1. Może to potencjalnie uszkodzić kod, który już zależy od istniejącego zachowania.
  2. Nowe tłumaczenie może wpłynąć na wydajność istniejących zapytań, nawet jeśli rzadko używany jest parametr zerowy.

W każdym razie to, czy zajmiemy się tym, będzie w dużej mierze zależało od względnego priorytetu, jaki nadadzą temu klienci. Jeśli zależy Ci na tym problemie, zachęcam do zagłosowania na niego w naszej nowej witrynie z sugestiami dotyczącymi funkcji: https://data.uservoice.com .

divega
źródło
9

Jeśli jest to typ dopuszczający wartość null, może spróbuj użyć właściwości HasValue?

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

Nie mam tutaj żadnego EF do przetestowania ... tylko sugestia =)

Svish
źródło
1
Cóż ... to działa tylko wtedy, gdy szukasz tylko wartości null, ale wtedy użycie i == nulltak nie zostanie trafione przez błąd. Chodzi o to, aby filtrować według wartości zmiennej, której wartość może być pusta, i mieć wartość null, aby znaleźć puste rekordy.
Dave Cousineau
1
Twoja odpowiedź mnie uratowała. Zapomniałem użyć typu dopuszczającego wartość null w mojej klasie modelu jednostki i nie mogłem wykonać (x => x.Column == null)pracy. :)
Reuel Ribeiro,
To daje System.NullReferenceException , ponieważ obiekt już jest zerowy!
TiyebM
5

aby poradzić sobie z pustymi porównaniami użyj Object.Equals()zamiast==

sprawdź to odniesienie

Oscar Cabrero
źródło
Działa to doskonale w Linq-To-Sql, a także generuje właściwy SQL (niektóre inne odpowiedzi tutaj generują horrendalny SQL lub błędne wyniki).
Michael Stum
Załóżmy, że chcę compaire z null, Object.Equals(null)co, jeśli Objectsam jest zerowy?
TiyebM
4

Wskazanie, że wszystkie sugestie dotyczące Entity Framework <6,0 generują niezręczny kod SQL. Zobacz drugi przykład „czystej” poprawki.

Niedorzeczne obejście

// 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
    );

wyniki w SQL, takie jak:

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

Oburzające obejście

Jeśli chcesz wygenerować czystszy SQL, coś takiego:

// 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()
    ;

skutkuje tym, czego chciałeś w pierwszej kolejności:

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
źródło
Kod działający na SQL będzie czystszy i szybszy, ale EF wygeneruje i buforuje nowy plan zapytań dla każdej kombinacji przed wysłaniem go do serwera sql, co czyni go wolniejszym niż inne obejścia.
Burak Tamtürk
2
var result = from entry in table
                     where entry.something == null
                     select entry;

Powyższe zapytanie działa zgodnie z przeznaczeniem. Prawidłowo generuje IS NULL. Jednak mój kod produkcyjny był

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

a wygenerowany SQL był czymś = @p; @p = NULL. Wydaje się, że EF poprawnie tłumaczy stałe wyrażenie, ale jeśli zaangażowana jest zmienna, traktuje ją tak, jak normalne porównanie. Właściwie to ma sens.

Adrian Zanescu
źródło
1

Wydaje się, że Linq2Sql również ma ten „problem”. Wygląda na to, że istnieje uzasadniony powód takiego zachowania, ze względu na to, czy wartości ANSI NULL są WŁĄCZONE czy WYŁĄCZONE, ale zdumiewa to, dlaczego proste "== null" w rzeczywistości będzie działać tak, jak można się spodziewać.

JasonCoder
źródło
1

Osobiście wolę:

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

nad

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

ponieważ zapobiega powtórzeniom - choć nie jest to matematycznie dokładne, ale pasuje do większości przypadków.

Vincent Courcelle
źródło
0

Nie jestem w stanie skomentować posta divegi, ale spośród różnych przedstawionych tutaj rozwiązań rozwiązanie divega generuje najlepszy SQL. Zarówno pod względem wydajności, jak i długości. Właśnie sprawdziłem z SQL Server Profiler i spojrzałem na plan wykonania (z "SET STATISTICS PROFILE ON").

Buginator
źródło
0

Niestety w Entity Framework 5 DbContext problem nadal nie został rozwiązany.

Użyłem tego obejścia (działa z MSSQL 2012, ale ustawienie ANSI NULLS może być przestarzałe w każdej przyszłej wersji 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();
            }
        }
    }
}

Należy zauważyć, że jest to brudne obejście, ale można je bardzo szybko wdrożyć i działa dla wszystkich zapytań.

Knaģis
źródło
To natychmiast przestanie działać, gdy ANSI NULLS zostanie na stałe ustawiony na ON w przyszłej wersji SQL Server, na wypadek gdyby ostrzeżenie nie było jasne.
Triynko
0

Jeśli wolisz używać składni metody (lambda) tak jak ja, możesz zrobić to samo:

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
źródło
-1
var result = from entry in table    
             where entry.something == value||entry.something == null                   
              select entry;

użyć tego

Andrzej
źródło
5
To BARDZO błędne, ponieważ wybierze wszystkie wpisy, w których wartość jest zgodna, ORAZ wszystkie wpisy, w których coś jest zerowe, nawet jeśli poprosisz o wartość.
Michael Stum