Banco de Dados e Null values
- c#
É comum na modelagem de banco de dados termos colunas com valores nulos. Para tipos como string, que são "reference type", valor null é extremamente comum, mas e quanto a valores primitivos como: int, float, double, DateTime (struct no C#) e entre outros?
Na versão 2.0 do .NET, sim numa versão já adotada no mercado (.NET 1.1 é bem pouco, espero) apresentou o conceito de Nullable Types que permite você inferir valores nulos à built-in types (ou tipos primitos). Não irei entrar em detalhes muito profundos, pois pretendo ser breve nest post.
A sintaxe é bem simples:
int? a = null;
// dois properties para Nullable Types, HasValue e Value
if (a.HasValue) Console.WriteLine(a.Value);
Assim podemos atribuir valores para nulos e não mais valores default para estes tipos de dados.
Ok, isso é bonito mas o IDataReader
não possui um método para tratar quando valores são nulos, o que fazemos? Realizamos uma checagem com o método IsDbNull
, e se for podemos retornar nulo ou o valor caso OK. Veja abaixo:
int? coluna = reader.IsDBNull(i) ? (int?)null : (int?)reader.GetInt32(i);
Isso resolveria o nosso problema, mas e se tivermos 10 colunas na tabela, esse código polui um pouco, não? Pena que o GetInt32(int i) não lida com campos nulos.
Como podemos melhorar isso?
Podemos usar Extension Methods!
Num post anterior eu cheguei a falar bem básicamente sobre extension methods, veja como resolveríamos este problema:
static class DataReaderTypeHelper
{
public static int? GetNullOrInt32(this IDataReader reader, int index)
{
return reader.IsDBNull(index) ? (int?)null : (int?)reader.GetInt32(index);
}
public static string GetNullOrString(this IDataReader reader, int index)
{
return reader.IsDBNull(index) ? null : reader.GetString(index);
}
public static DateTime? GetNullOrDateTime(this IDataReader reader, int index)
{
return reader.IsDBNull(index) ? (DateTime?)null : (DateTime?)reader.GetDateTime(index);
}
}
Agora podemos este método e tornar a coisa mais bonita e direta:
int? coluna = reader.GetNullOrInt32(index);
Muito mais simples, fácil e direto de usar. Podemos fazer também para inserção, remoção ou atualização onde valores nulos encrencam no banco de dados também. Normalmente teríamos que fazer:
cmd.Parameters.AddWithValue("@param1", param1);
cmd.Parameters.AddWithValue("@param2", param2);
Se usassemos o código acima, se @param2 pudesse ser nulo no banco de dados, este código geraria uma SqlException caso o valor de param2 fosse nulo, pois o null do c# é diferente do null para o banco de dados, então teríamos que verificar o valor e assim retornar DBNull.Value.
Com Nullable Types, isso ficaria extremamente simples, nada de if
's:
cmd.Parameters.AddWithValue("@param2", (object)param2 ?? DBNull.Value);
Observe que temos de fazer um cast para object em param2
, pois para o compilador o operador ??
deve garantir que param2
e DBNull.Value
sejam do mesmo tipo, por que Nullable usa Generics para inferir tipos, embora mascare isso.
Usando novamente Extension Methods podemos fazer uma classe que nos ajude nisso:
static class DbHelper
{
public static void AddWithValueOrNull(this SqlParameterCollection collection, string param, object value)
{
collection.AddWithValue(param, (object)value ?? DBNull.Value);
}
}
Agora temos um método AddWithValueOrNull para o Parameters que é do tipo SqlParametersCollection. Assim encapsulamos tudo numa sintaxe mais limpa:
cmd.Parameters.AddWithValueOrNull("@param2", param2);
Nota usamos este método apenas para valores que possam ser nulos e não para todos, no caso de chaves primárias devemos usar o AddWithValue
mesmo por que devemos reforçar a necessidade de valores não nulos.