Lector de datos SQL: manejo de valores de columna nulos

298

Estoy usando un SQLdatareader para construir POCO a partir de una base de datos. El código funciona excepto cuando encuentra un valor nulo en la base de datos. Por ejemplo, si la columna Nombre en la base de datos contiene un valor nulo, se genera una excepción.

employee.FirstName = sqlreader.GetString(indexFirstName);

¿Cuál es la mejor manera de manejar valores nulos en esta situación?

DenaliHardtail
fuente

Respuestas:

471

Necesita verificar IsDBNull:

if(!SqlReader.IsDBNull(indexFirstName))
{
  employee.FirstName = sqlreader.GetString(indexFirstName);
}

Esa es su única forma confiable de detectar y manejar esta situación.

Envolví esas cosas en métodos de extensión y tiendo a devolver un valor predeterminado si la columna es null:

public static string SafeGetString(this SqlDataReader reader, int colIndex)
{
   if(!reader.IsDBNull(colIndex))
       return reader.GetString(colIndex);
   return string.Empty;
}

Ahora puedes llamarlo así:

employee.FirstName = SqlReader.SafeGetString(indexFirstName);

y nunca más tendrá que preocuparse por una excepción o un nullvalor.

marc_s
fuente
65
Si alguien necesita el nombre de la columna en lugar del índice, puede hacer lo siguiente: int colIndex = reader.GetOrdinal(fieldname);y sobrecargar fácilmente la SafeGetStringfunción de @ marc_s .
ilans
No puedo creer que esté aquí en 2019, en VB no menos .......... Gracias, gran ayuda
JimmyB
También se puede hacer así: int ordinal = reader.GetOrdinal ("col_name"); uint? val = reader.IsDBNull (ordinal)? (uint?) null: reader.GetUInt32 (ordinal);
ed22
¡Hola chicos! Traté de copiar y pegar en el formulario y regresé con un error. "El método de extensión debe definirse en una clase estática no genérica".
Jansen Malaggay
Si está utilizando reader.GetOrindal dentro de SafeGetString y desea usar SafeGetString dentro de un bucle, ¿cómo se puede lograr esto sin afectar el rendimiento?
AspUser7724
223

Debe usar el asoperador combinado con el ??operador para los valores predeterminados. Los tipos de valor deberán leerse como anulables y tener un valor predeterminado.

employee.FirstName = sqlreader[indexFirstName] as string;
employee.Age = sqlreader[indexAge] as int? ?? default(int);

El asoperador maneja el casting, incluida la verificación de DBNull.

stevehipwell
fuente
66
Si alguien cambia la columna Age de int a bigint SQL (c # long), su código fallará en silencio al devolver 0. La respuesta de ZXX es IMO más confiable.
Martin Ørding-Thomsen
Me pregunto si puede anular el valor predeterminado (int) para que sea -1 en lugar de 0
Chris
55
@Chris: debería poder reemplazar el valor predeterminado (int) con -1.
stevehipwell
@ Stevo3000 ¡Estás en lo correcto! Lo intenté y funcionó como dijiste justo después de publicar, pero olvidé volver a esta página :)
Chris
55
Tenga en cuenta que usar "como" aquí puede ocultar los errores de índice. Si usas accidentalmente sqlreader[indexAge] as string ?? "", siempre obtendrás "". Considere si realmente quiere (int?)sqlreader[indexAge] ?? defaultValue, así que si su SQL cambia, obtendrá excepciones en lugar de valores incorrectos. @ Stevo3000: el valor predeterminado (int) es 0, no -1. @ Chris: Asegúrate de estar usando el que realmente quieres.
me22
30

Para una cadena, simplemente puede convertir la versión del objeto (a la que se accede mediante el operador de matriz) y terminar con una cadena nula para nulos:

employee.FirstName = (string)sqlreader[indexFirstName];

o

employee.FirstName = sqlreader[indexFirstName] as string;

Para enteros, si convierte a un int que admite valores NULL, puede usar GetValueOrDefault ()

employee.Age = (sqlreader[indexAge] as int?).GetValueOrDefault();

o el operador de fusión nula ( ??).

employee.Age = (sqlreader[indexAge] as int?) ?? 0;
Codificación ido
fuente
2
El reparto explícito, como en su primer ejemplo, no funciona.
Lanza
@musefan: ¿Tu campo es realmente una cadena? Si no, obtendrá un error diferente. Esto funciona y no hay diferencia real entre los ejemplos 1 y 2 (aparte de la sintaxis).
Se fue la codificación el
1
@GoneCoding: Sí, es una cadena anulable, y definitivamente es un caso, el primero causa problemas cuando el segundo funciona. Me imagino que el problema es causado por cómo se manejan los valores nulos. Como en, no son nullsino un objeto DBNull. La diferencia entre las dos declaraciones es que la primera fallará si no es una cadena, mientras que la segunda simplemente devolverá nulo si no es una cadena.
musefan
23

IsDbNull(int)generalmente es mucho más lento que usar métodos como GetSqlDateTimey luego compararlos DBNull.Value. Pruebe estos métodos de extensión para SqlDataReader.

public static T Def<T>(this SqlDataReader r, int ord)
{
    var t = r.GetSqlValue(ord);
    if (t == DBNull.Value) return default(T);
    return ((INullable)t).IsNull ? default(T) : (T)t;
}

public static T? Val<T>(this SqlDataReader r, int ord) where T:struct
{
    var t = r.GetSqlValue(ord);
    if (t == DBNull.Value) return null;
    return ((INullable)t).IsNull ? (T?)null : (T)t;
}

public static T Ref<T>(this SqlDataReader r, int ord) where T : class
{
    var t = r.GetSqlValue(ord);
    if (t == DBNull.Value) return null;
    return ((INullable)t).IsNull ? null : (T)t;
}

Úselos así:

var dd = r.Val<DateTime>(ords[4]);
var ii = r.Def<int>(ords[0]);
int nn = r.Def<int>(ords[0]);
ZXX
fuente
55
Estoy descubriendo que los operadores explícitos en los tipos System.Data.SqlTypes arrojan errores en todas partes tratando de usar este código ...
Tetsujin no Oni
Consulte stackoverflow.com/a/21024873/1508467 para obtener una explicación de por qué esto a veces falla (intente usar Val <int> para leer una columna SQL int).
Rhys Jones
12

Una forma de hacerlo es verificar los nulos db:

employee.FirstName = (sqlreader.IsDBNull(indexFirstName) 
    ? ""
    : sqlreader.GetString(indexFirstName));
Michael Todd
fuente
12

reader.IsDbNull(ColumnIndex) funciona como muchas respuestas dice.

Y quiero mencionar que si trabaja con nombres de columna, solo comparar tipos puede ser más cómodo.

if(reader["TeacherImage"].GetType() == typeof(DBNull)) { //logic }
PJ3
fuente
Esto también funciona en versiones antiguas de System.Data y .NET FW
RaSor
11

No creo que haya un valor de columna NULL , cuando las filas se devuelven dentro de un lector de datos utilizando el nombre de la columna.

Si lo hace datareader["columnName"].ToString();, siempre le dará un valor que puede ser una cadena vacía ( String.Emptysi necesita comparar).

Usaría lo siguiente y no me preocuparía demasiado:

employee.FirstName = sqlreader["columnNameForFirstName"].ToString();
el bayames
fuente
44
Puede hacer un lector [FieldName] == DBNull.Value, para verificar NULL's
Ralph Willgoss
11

Esta solución depende menos del proveedor y funciona con un lector SQL, OleDB y MySQL:

public static string GetStringSafe(this IDataReader reader, int colIndex)
{
    return GetStringSafe(reader, colIndex, string.Empty);
}

public static string GetStringSafe(this IDataReader reader, int colIndex, string defaultValue)
{
    if (!reader.IsDBNull(colIndex))
        return reader.GetString(colIndex);
    else
        return defaultValue;
}

public static string GetStringSafe(this IDataReader reader, string indexName)
{
    return GetStringSafe(reader, reader.GetOrdinal(indexName));
}

public static string GetStringSafe(this IDataReader reader, string indexName, string defaultValue)
{
    return GetStringSafe(reader, reader.GetOrdinal(indexName), defaultValue);
}
Hora de verano
fuente
1
Copiar y personalizar este código directamente en una clase de extensiones en este momento.
qxotk
8

Lo que tiendo a hacer es reemplazar los valores nulos en la instrucción SELECT con algo apropiado.

SELECT ISNULL(firstname, '') FROM people

Aquí reemplazo cada nulo con una cadena en blanco. Su código no arrojará un error en ese caso.

alex
fuente
Si es posible, use esto para evitar valores nulos. De lo contrario, me gusta la respuesta de Sonny Boy de los métodos de ayuda.
Sin reembolsos Sin devoluciones
3
¿Por qué un método auxiliar separado y estático? ¿No parece un método de extensión en el SqlDataReader más convincente y más intuitivo?
marc_s
7

Puede escribir una función genérica para verificar Nulo e incluir el valor predeterminado cuando es NULO. Llame a esto cuando lea Datareader

public T CheckNull<T>(object obj)
        {
            return (obj == DBNull.Value ? default(T) : (T)obj);
        }

Al leer el uso del lector de datos

                        while (dr.Read())
                        {
                            tblBPN_InTrRecon Bpn = new tblBPN_InTrRecon();
                            Bpn.BPN_Date = CheckNull<DateTime?>(dr["BPN_Date"]);
                            Bpn.Cust_Backorder_Qty = CheckNull<int?>(dr["Cust_Backorder_Qty"]);
                            Bpn.Cust_Min = CheckNull<int?>(dr["Cust_Min"]);
                         }
Vijai
fuente
6

Verifique sqlreader.IsDBNull(indexFirstName)antes de intentar leerlo.

CesarGon
fuente
5

Al influir en la respuesta de getpsyched , creé un método genérico que verifica el valor de la columna por su nombre

public static T SafeGet<T>(this System.Data.SqlClient.SqlDataReader reader, string nameOfColumn)
{
  var indexOfColumn = reader.GetOrdinal(nameOfColumn);
  return reader.IsDBNull(indexOfColumn) ? default(T) : reader.GetFieldValue<T>(indexOfColumn);
}

Uso:

var myVariable = SafeGet<string>(reader, "NameOfColumn")
Proyecto Mayhem
fuente
No sé quién eres, pero te bendigo. Esta función ahorró más de tres horas de trabajo.
Ramneek Singh
4

cómo crear métodos auxiliares

Para cadena

private static string MyStringConverter(object o)
    {
        if (o == DBNull.Value || o == null)
            return "";

        return o.ToString();
    }

Uso

MyStringConverter(read["indexStringValue"])

Para int

 private static int MyIntonverter(object o)
    {
        if (o == DBNull.Value || o == null)
            return 0;

        return Convert.ToInt32(o);
    }

Uso

MyIntonverter(read["indexIntValue"])

Para la fecha

private static DateTime? MyDateConverter(object o)
    {
        return (o == DBNull.Value || o == null) ? (DateTime?)null : Convert.ToDateTime(o);
    }

Uso

MyDateConverter(read["indexDateValue"])

Nota: para DateTime declare varialbe como

DateTime? variable;
Usman Ali
fuente
4

Como una adición a la respuesta de marc_s, puede usar un método de extensión más genérico para obtener valores de SqlDataReader:

public static T SafeGet<T>(this SqlDataReader reader, int col)
    {
        return reader.IsDBNull(col) ? default(T) : reader.GetFieldValue<T>(col);
    }
emborracharse
fuente
No llamaría a este método "SafeGet" porque si T es una estructura, convertirá nulos al valor predeterminado no nulo para T, lo que no es realmente seguro. Quizás "GetValueOrDefault".
Rhys Jones
@RhysJones ¿Por qué tendrías T como una estructura en este caso? Incluso si lo hace, argumentaría que el comportamiento no nulo en la estructura es el comportamiento esperado.
getpsyched
@RhysJones Pero estoy de acuerdo en que este método podría no ser seguro, necesitaría manejar excepciones como InvalidCastException del SqlDataReader.
getpsyched
3

Creo que te gustaría usar:

SqlReader.IsDBNull(indexFirstName)
bytebender
fuente
2

Utilizamos una serie de métodos estáticos para extraer todos los valores de nuestros lectores de datos. Entonces, en este caso, estaríamos llamando. DBUtils.GetString(sqlreader(indexFirstName)) El beneficio de crear métodos estáticos / compartidos es que no tiene que hacer las mismas verificaciones una y otra vez ...

El (los) método (s) estático (s) contendría código para verificar nulos (ver otras respuestas en esta página).

muchacho
fuente
2

Puede usar el operador condicional:

employee.FirstName = sqlreader["indexFirstName"] != DBNull.Value ? sqlreader[indexFirstName].ToString() : "";
Panayot Minkov
fuente
¡Igual que una de las respuestas a continuación, pero 8 años atrás!
beercohol
2

Aquí hay muchas respuestas con información útil (y alguna información incorrecta), me gustaría reunir todo.

La respuesta corta a la pregunta es verificar DBNull: casi todos están de acuerdo con este bit :)

En lugar de utilizar un método auxiliar para leer valores anulables por tipo de datos SQL, un método genérico nos permite abordar esto con mucho menos código. Sin embargo, no puede tener un único método genérico tanto para los tipos de valores anulables como para los tipos de referencia, ¿esto se discute extensamente en el tipo Nullable como un parámetro genérico posible? y restricción de tipo genérico C # para todo lo que se puede anular .

Entonces, siguiendo las respuestas de @ZXX y @getpsyched, terminamos con esto, 2 métodos para obtener valores anulables y he agregado un tercero para valores no nulos (completa el conjunto basado en el nombre del método).

public static T? GetNullableValueType<T>(this SqlDataReader sqlDataReader, string columnName) where T : struct
{
    int columnOrdinal = sqlDataReader.GetOrdinal(columnName);
    return sqlDataReader.IsDBNull(columnOrdinal) ? (T?)null : sqlDataReader.GetFieldValue<T>(columnOrdinal);
}

public static T GetNullableReferenceType<T>(this SqlDataReader sqlDataReader, string columnName) where T : class
{
    int columnOrdinal = sqlDataReader.GetOrdinal(columnName);
    return sqlDataReader.IsDBNull(columnOrdinal) ? null : sqlDataReader.GetFieldValue<T>(columnOrdinal);
}

public static T GetNonNullValue<T>(this SqlDataReader sqlDataReader, string columnName)
{
    int columnOrdinal = sqlDataReader.GetOrdinal(columnName);
    return sqlDataReader.GetFieldValue<T>(columnOrdinal);
}

Usualmente uso nombres de columna, modifíquelos si usa índices de columna. En base a estos nombres de métodos, puedo decir si espero que los datos sean anulables o no, bastante útil cuando se mira el código escrito hace mucho tiempo.

Consejos;

  • No tener columnas anulables en la base de datos evita este problema. Si tiene control sobre la base de datos, las columnas deben ser no nulas por defecto y solo anulables cuando sea necesario.
  • No convierta los valores de la base de datos con el operador C # 'as' porque si la conversión es incorrecta, silenciosamente devolverá nulo.
  • El uso de una expresión de valor predeterminada cambiará los valores nulos de la base de datos a valores no nulos para tipos de valores como int, datetime, bit, etc.

Por último, al probar los métodos anteriores en todos los tipos de datos de SQL Server descubrí que no puede obtener directamente un char [] de un SqlDataReader, si desea un char [] tendrá que obtener una cadena y usar ToCharArray ().

Rhys Jones
fuente
1

Estoy usando el código enumerado a continuación para manejar celdas nulas en una hoja de Excel que se lee en una tabla de datos.

if (!reader.IsDBNull(2))
{
   row["Oracle"] = (string)reader[2];
}
Tequila
fuente
1
private static void Render(IList<ListData> list, IDataReader reader)
        {
            while (reader.Read())
            {

                listData.DownUrl = (reader.GetSchemaTable().Columns["DownUrl"] != null) ? Convert.ToString(reader["DownUrl"]) : null;
                //没有这一列时,让其等于null
                list.Add(listData);
            }
            reader.Close();
        }
xux
fuente
1

y / o usar operador ternario con asignación:

employee.FirstName = rdr.IsDBNull(indexFirstName))? 
                     String.Empty: rdr.GetString(indexFirstName);

reemplace el valor predeterminado (cuando sea nulo) según corresponda para cada tipo de propiedad ...

Charles Bretana
fuente
1

Este método depende de indexFirstName, que debería ser el ordinal de columna basado en cero.

if(!sqlReader.IsDBNull(indexFirstName))
{
  employee.FirstName = sqlreader.GetString(indexFirstName);
}

Si no conoce el índice de la columna pero no desea verificar un nombre, puede usar este método de extensión:

public static class DataRecordExtensions
{
    public static bool HasColumn(this IDataRecord dr, string columnName)
    {
        for (int i=0; i < dr.FieldCount; i++)
        {
            if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase))
                return true;
        }
        return false;
    }
}

Y usa el método de esta manera:

if(sqlReader.HasColumn("FirstName"))
{
  employee.FirstName = sqlreader["FirstName"];
}
Ogglas
fuente
1

Antigua pregunta, pero tal vez alguien todavía necesita una respuesta

en realidad trabajé alrededor de este problema así

Para int:

public static object GatDataInt(string Query, string Column)
    {
        SqlConnection DBConn = new SqlConnection(ConnectionString);
        if (DBConn.State == ConnectionState.Closed)
            DBConn.Open();
        SqlCommand CMD = new SqlCommand(Query, DBConn);
        SqlDataReader RDR = CMD.ExecuteReader();
        if (RDR.Read())
        {
            var Result = RDR[Column];
            RDR.Close();
            DBConn.Close();
            return Result;
        }
        return 0;
    }

lo mismo para la cadena solo devuelve "" en lugar de 0 ya que "" es una cadena vacía

para que puedas usarlo como

int TotalPoints = GatDataInt(QueryToGetTotalPoints, TotalPointColumn) as int?;

y

string Email = GatDatastring(QueryToGetEmail, EmailColumn) as string;

muy flexible para que pueda insertar cualquier consulta para leer cualquier columna y nunca volverá con error

Ahmed Kamal
fuente
0

Aquí hay una clase auxiliar que otros pueden usar si la necesitan según la respuesta de @marc_s:

public static class SQLDataReaderExtensions
    {
        public static int SafeGetInt(this SqlDataReader dataReader, string fieldName)
        {
            int fieldIndex = dataReader.GetOrdinal(fieldName);
            return dataReader.IsDBNull(fieldIndex) ? 0 : dataReader.GetInt32(fieldIndex);
        }

        public static int? SafeGetNullableInt(this SqlDataReader dataReader, string fieldName)
        {
            int fieldIndex = dataReader.GetOrdinal(fieldName);
            return dataReader.GetValue(fieldIndex) as int?;
        }

        public static string SafeGetString(this SqlDataReader dataReader, string fieldName)
        {
            int fieldIndex = dataReader.GetOrdinal(fieldName);
            return dataReader.IsDBNull(fieldIndex) ? string.Empty : dataReader.GetString(fieldIndex);
        }

        public static DateTime? SafeGetNullableDateTime(this SqlDataReader dataReader, string fieldName)
        {
            int fieldIndex = dataReader.GetOrdinal(fieldName);
            return dataReader.GetValue(fieldIndex) as DateTime?;
        }

        public static bool SafeGetBoolean(this SqlDataReader dataReader, string fieldName)
        {
            return SafeGetBoolean(dataReader, fieldName, false);
        }

        public static bool SafeGetBoolean(this SqlDataReader dataReader, string fieldName, bool defaultValue)
        {
            int fieldIndex = dataReader.GetOrdinal(fieldName);
            return dataReader.IsDBNull(fieldIndex) ? defaultValue : dataReader.GetBoolean(fieldIndex);
        }
    }
Raghav
fuente
0

Convierta las manijas DbNull con sensatez.

employee.FirstName = Convert.ToString(sqlreader.GetValue(indexFirstName));
Frank Hagenson
fuente
Tenga en cuenta que DBNull se convierte en una cadena vacía, no en un valor nulo.
Rhys Jones
-2

también puedes verificar esto

if(null !=x && x.HasRows)
{ ....}
Scooby
fuente
-1 Este no es el punto: estamos manejando el caso de un valor de columna nulo, no el de un valor nulo o vacíoSqlDataReader
azulado el