Ieri, effettuando una query con LINQ su una fonte dati SQL mi sono imbattuto in un comportamento che mi ha spinto a scavare un pò in profondità per quanto riguarda la sintassi di conversione che LINQ adotta quando si vogliono estrarre righe con valori NULL presenti in una tabella.
Cominciamo dall'inizio: tabella semplicissima, Person, con tre campi:
- Id: int not null PK Identity
- Name: not null varchar(50)
- Age: int null
Tutto qui. Dopo aver popolato la tabella come segue vogliamo effettuare delle estrazioni dei dati.
Id | Name | Age |
1 | Mario | NULL |
2 | Franco | 30 |
3 | Alice | 19 |
4 | Luca | NULL |
5 | Alfredo | NULL |
6 | Giuseppe | 45 |
1) C# Estrazione di tutte le righe con Age NULL
var _persons = from _p in _dc.Persons where _p.Age == null select _p;
Debug:
SELECT [t0].[Id], [t0].[Name], [t0].[Age]
FROM [dbo].[Person] AS [t0]
WHERE [t0].[Age] IS NULL
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
Mario Age:
Luca Age:
Alfredo Age:
La query è corretta e la sintassi SQL generata anche. I risultati sono quelli che ci si aspetta.
2) VB Estrazione di tutte le righe con Age NULL
Dim _persons = From _p In _dc.Persons Where _p.Age = Nothing Select _p
Debug:
SELECT [t0].[Id], [t0].[Name], [t0].[Age]
FROM [dbo].[Person] AS [t0]
WHERE (COALESCE(
(CASE
WHEN [t0].[Age] = NULL THEN 1
WHEN NOT ([t0].[Age] = NULL) THEN 0
ELSE NULL
END),@p0)) = 1
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [0]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
La query viene tradotta in tutt'altro modo rispetto a C#, e non vi sono risultati. Questo perchè? Se proviamo a modificarla come segue:
Dim _persons = From _p In _dc.Persons Where _p.Age Is Nothing Select _p
Debug:
SELECT [t0].[Id], [t0].[Name], [t0].[Age]
FROM [dbo].[Person] AS [t0]
WHERE [t0].[Age] IS NULL
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
Mario Age:
Luca Age:
Alfredo Age:
Quindi bisogna fare attenzione alla sintassi, se si lavora in VB e ricordare che se si vogliono effettuare confronti con valori NULL nel DB si deve usare Is Nothing e non =Nothing.
Questa cosa, però, ha suscitato in me un'altro interrogativo: e se, al posto di una costante (null o nothing) ci fosse stato un valore nullable integer? Come si sarebbe comportato il sistema se il valore dell'integer fosse stato effettivamente null o nothing?
3) C# variabile nullInt dichiarata ed usata nella query con valore null:
int? nullInt = null;
var _persons = from _p in _dc.Persons where _p.Age == nullInt select _p;
Debug:
SELECT [t0].[Id], [t0].[Name], [t0].[Age]
FROM [dbo].[Person] AS [t0]
WHERE [t0].[Age] = @p0
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [Null]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
Query "errata" e nessun risultato. Non è ciò che mi sarei aspettato. Se imposto il valore a 45, ottengo un risultato (Giuseppe).
4) VB varibile nullInt dichiarata ed usata nella query con valore Nothing:
Dim _nullInt? As Integer
Dim _persons = From _p In _dc.Persons Where _p.Age = _nullInt Select _p
Debug:
SELECT [t0].[Id], [t0].[Name], [t0].[Age]
FROM [dbo].[Person] AS [t0]
WHERE (COALESCE(
(CASE
WHEN [t0].[Age] = @p0 THEN 1
WHEN NOT ([t0].[Age] = @p0) THEN 0
ELSE NULL
END),@p1)) = 1
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [Null]
-- @p1: Input Int (Size = 0; Prec = 0; Scale = 0) [0]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
Ancora una volta, query diversa da C# e nessun risultato.
Ma allora? Cosa significa? Significa che in VB.net, per ottenere la stessa sintassi di C# con i valori null si deve usare o il confronto tramite Is Nothing o con il metodo Equals(Nothing), ovvero:
Dim _persons = From _p In _dc.Persons Where _p.Age.Equals(Nothing) Select _p
Dim _persons = From _p In _dc.Persons Where _p.Age Is Nothing Select _p
var _persons = from _p in _dc.Persons where _p.Age == null select _p;
Sono equivalenti, in termini di SQL generato, e quindi di risultati. La cosa che non si riesce a fare, è quella di utilizzare un nullable type e a seconda del suo valore (se null o valorizzato) ottenere una query che vari, adoperando l'operatore IS nel caso in cui il valore della variabile sia null, oppure = nel caso il valore della variabile sia diverso da null. Questo vale sia per C# che per VB.
Un'altra tecnica per estrarre i valori null, in caso di nullable type, può basarsi sull'utilizzo del metodo HasValue() che viene tradotto con un efficace NOT IS NULL. Scrivendo quindi:
Dim _persons = From _p In _dc.Persons Where Not _p.Age.HasValue Select _p
Si ottengono risultati analoghi a quelli ottenuti tramite confronto con Nothing.