2010-01-26 23 views
27

Primero, déjenme explicarles la situación actual: estoy leyendo registros de una base de datos y los coloco en un objeto para su uso posterior; hoy surgió una pregunta sobre el tipo de base de datos para la conversión de tipo C# (¿casting?).Cómo (eficientemente) convertir (¿emitir?) Un campo SqlDataReader a su correspondiente tipo de C#?

Veamos un ejemplo:

namespace Test 
{ 
    using System; 
    using System.Data; 
    using System.Data.SqlClient; 

    public enum MyEnum 
    { 
     FirstValue = 1, 
     SecondValue = 2 
    } 

    public class MyObject 
    { 
     private String field_a; 
     private Byte field_b; 
     private MyEnum field_c; 

     public MyObject(Int32 object_id) 
     { 
      using (SqlConnection connection = new SqlConnection("connection_string")) 
      { 
       connection.Open(); 

       using (SqlCommand command = connection.CreateCommand()) 
       { 
        command.CommandText = "sql_query"; 

        using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow)) 
        { 
         reader.Read(); 

         this.field_a = reader["field_a"]; 
         this.field_b = reader["field_b"]; 
         this.field_c = reader["field_c"]; 
        } 
       } 
      } 
     } 
    } 
} 

Ésta es (obviamente) no porque los tres this.field_x = reader["field_x"]; llamadas están lanzando el error Cannot implicitly convert type 'object' to 'xxx'. An explicit conversion exists (are you missing a cast?). compilador.

Para corregir esto Actualmente conocimientos de dos maneras (Vamos a usar el ejemplo field_b): es el número uno y el número dos this.field_b = (Byte) reader["field_b"]; es this.field_b = Convert.ToByte(reader["field_b"]);.

El problema con la opción número uno es que DBNull campos están lanzando excepciones como el elenco está fallando (incluso con tipos anulables como String), la hormiga el problema número dos es que no es la preservación de los valores nulos (el Convert.ToString(DBNull) produce una String.Empty), y no puedo usarlos con enumeraciones también.

Así, después de un par de búsquedas en Internet y aquí en stackoverflow, lo que se me ocurrió es:

public static class Utilities 
{ 
    public static T FromDatabase<T>(Object value) where T: IConvertible 
    { 
     if (typeof(T).IsEnum == false) 
     { 
      if (value == null || Convert.IsDBNull(value) == true) 
      { 
       return default(T); 
      } 
      else 
      { 
       return (T) Convert.ChangeType(value, typeof(T)); 
      } 
     } 
     else 
     { 
      if (Enum.IsDefined(typeof(T), value) == false) 
      { 
       throw new ArgumentOutOfRangeException(); 
      } 

      return (T) Enum.ToObject(typeof(T), value); 
     } 
    } 
} 

De esta manera me debería manejar cada caso.

Pregunta es: ¿Emite algo más? ¿Estoy haciendo un WOMBAT (Waste of Money, Brain And Time) ya que hay una forma más rápida y limpia de hacerlo? Está todo correcto? ¿Lucro?

+3

Esto se ve muy amplio y genérico para mí. La clase SqlDataReader tiene algunas funciones de tipo .GetInt32(), .GetBytes() para la conversión que harán algo de esto por usted, pero creo que todavía necesita verificar nulos. También buscaría en LINQ o en un ORM, ellos se encargarían de detalles como este para usted. –

+0

Mire los diversos métodos GetXXX del lector de datos. Tal vez son lo que estás buscando. –

+0

¿Has intentado utilizar *** FromDatabase ***? final simple al respecto usando *** int, int ?, string, DateTime ?, Enum *** values? – Kiquenet

Respuesta

36

Si un campo permite nulos, no use tipos primitivos regulares. Use C# nullable type y as keyword.

int? field_a = reader["field_a"] as int?; 
string field_b = reader["field_a"] as string; 

Adición de un ? a cualquier C# tipo no anulable hace "anulable". El uso de la palabra clave as intentará enviar un objeto al tipo especificado. Si el yeso falla (como lo haría si el tipo es DBNull), el operador devuelve null.

Nota: Otra pequeña ventaja de usar as es que es slightly faster que la fundición normal. Dado que también puede tener algunas desventajas, como hacer que sea más difícil rastrear errores si intentas lanzar el tipo incorrecto, esto no se debe considerar como una razón para usar siempre as sobre el casting tradicional. El casting regular ya es una operación bastante barata.

+0

No se puede convertir el tipo 'System.DBNull' a 'int?'a través de una conversión de referencia, conversión de boxeo, conversión de unboxing, conversión de envoltura o conversión de tipo nulo. –

+0

¿Has probado el código anterior? Funcionará. Lo que dijiste es absolutamente cierto y es por lo que mi respuesta funciona: http://msdn.microsoft.com/en-us/library/cscsdfbt.aspx –

+0

Mi error. Estaba probando 'DBNull.Value como int?' En lugar de '(object) DBNull.Value as int?'. –

12

¿no quieres usar el reader.Get* methods? La única cosa molesta es que toman números de columna por lo que tiene para envolver el descriptor de acceso en una llamada a GetOrdinal()

using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow)) 
{ 
    reader.Read(); 

    this.field_a = reader.GetString(reader.GetOrdinal("field_a")); 
    this.field_a = reader.GetDouble(reader.GetOrdinal("field_b")); 
    //etc 
} 
+1

Siempre evité los métodos 'reader.GetX' porque debe pasarles el número de la columna, y porque es" malo "(¿Qué ocurre si, por ejemplo, el procedimiento almacenado subyacente obtiene algunas columnas adicionales? Incluso si no le importa ellos arruinarán el código.) y no estaba al tanto del método 'reader.GetOrdinal', nunca los consideré. – Albireo

+0

Supongo que probablemente podría agregar una serie de sobrecargas a través de un método de extensión que le permitiría pasar el nombre de la columna al lector y recuperar los diversos tipos –

+0

. No sé cuánto ayuda OP. OP ya usa 'reader [" field_a "]' en su código. Es solo cuestión de lanzar al tipo real. 'GetOrdinal' de todos modos hace la búsqueda de cadena para obtener el ordinal, si eso es lo que estás tratando de evitar. Si está utilizando GetOrdinal, tiene que almacenarlo en caché: http://stackoverflow.com/questions/1079366/why-use-the-getordinal-method-of-the-sqldatareader – nawfal

3

Se puede hacer una serie de métodos de extensión, un par por tipo de datos:

public static int? GetNullableInt32(this IDataRecord dr, string fieldName) 
    { 
     return GetNullableInt32(dr, dr.GetOrdinal(fieldName)); 
    } 

    public static int? GetNullableInt32(this IDataRecord dr, int ordinal) 
    { 
     return dr.IsDBNull(ordinal) ? null : (int?)dr.GetInt32(ordinal); 
    } 

Esto se vuelve un poco tedioso para poner en práctica, pero es bastante eficiente. En System.Data.DataSetExtensions.dll, Microsoft resolvió el mismo problema para DataSets con un Field<T> method, que maneja genéricamente múltiples tipos de datos, y puede convertir DBNull en Nullable.

Como experimento, una vez implementé un método equivalente para DataReaders, pero terminé usando Reflector para tomar prestada una clase interna de DataSetExtensions (UnboxT) para hacer las conversiones de tipo reales de manera eficiente. No estoy seguro de la legalidad de distribuir esa clase prestada, por lo que probablemente no debería compartir el código, pero es bastante fácil de buscar por uno mismo.

6

Esta es la forma en que he tratado con él en el pasado:

public Nullable<T> GetNullableField<T>(this SqlDataReader reader, Int32 ordinal) where T : struct 
    { 
     var item = reader[ordinal]; 

     if (item == null) 
     { 
      return null; 
     } 

     if (item == DBNull.Value) 
     { 
      return null; 
     } 

     try 
     { 
      return (T)item; 
     } 
     catch (InvalidCastException ice) 
     { 
      throw new InvalidCastException("Data type of Database field does not match the IndexEntry type.", ice); 
     } 
    } 

Uso:

int? myInt = reader.GetNullableField<int>(reader.GetOrdinal("myIntField")); 
2

El código genérico hanlding publicado aquí es fresco, pero como el título de la pregunta incluye la palabra "eficientemente" publicaré mi respuesta menos genérica pero (espero) más eficiente.

Le sugiero que use los métodos getXXX que otros han mencionado. Para hacer frente al problema del número de columna que bebop refiere, yo uso una enumeración, así:

enum ReaderFields { Id, Name, PhoneNumber, ... } 
int id = sqlDataReader.getInt32((int)readerFields.Id) 

Es un poco escribiendo extra, pero entonces no necesito llamar GetOrdinal para encontrar el índice de cada columna . Y, en lugar de preocuparte por los nombres de las columnas, te preocupan las posiciones de las columnas.

Para hacer frente a columnas anulables, es necesario comprobar si hay DBNull, y tal vez proporcionar un valor por defecto:

string phoneNumber; 
if (Convert.IsDBNull(sqlDataReader[(int)readerFields.PhoneNumber]) { 
    phoneNumber = string.Empty; 
} 
else { 
    phoneNumber = sqlDataReader.getString((int)readerFields.PhoneNumber); 
} 
+0

Esto no resuelve el problema que me mantenía alejado de los métodos 'reader.GetX': si el diseño del conjunto de registros subyacente cambia, particularmente si devuelve columnas adicionales entre las antiguas, el código se estropeará incluso si no lo hace se preocupan por las nuevas columnas, ya que su número ordinal cambia, y su enumeración no lo hace. – Albireo

+0

Si su conjunto de registros cambia, puede causar problemas sin importar el esquema que use. En la respuesta aceptada de Dan, si los nombres de campo cambian, el código está roto. Una vez más, la razón por la que publiqué esta respuesta fue porque mencionó la eficiencia. Acceder a los campos por nombre utiliza un bucle 'for' internamente para recorrer todos los campos de los resultados hasta que se encuentre el correspondiente. Esto se hace para cada acceso de campo, en cada fila. Entonces, digamos, 20 campos y 50 registros devueltos: tiene 1,000 'por' bucles. Además de 1,000 moldes tipo y 1,000 tipos anulables (que son un poco más grandes que los tipos estándar). – Ray

Cuestiones relacionadas