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?

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.


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.


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;

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

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

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 }

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

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

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.


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"]);
                         }

6

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


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")

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;

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

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


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).


2

Puede usar el operador condicional:

employee.FirstName = sqlreader["indexFirstName"] != DBNull.Value ? sqlreader[indexFirstName].ToString() : "";

¡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 ().


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];
}

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

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 ...


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"];
}

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


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

0

Convierta las manijas DbNull con sensatez.

employee.FirstName = Convert.ToString(sqlreader.GetValue(indexFirstName));

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)
{ ....}

-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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.