web-dev-qa-db-fra.com

Comment (efficacement) convertir (transtyper?) Un champ SqlDataReader en son type c # correspondant?

Tout d'abord, permettez-moi d'expliquer la situation actuelle: je lis des enregistrements d'une base de données et je les place dans un objet pour une utilisation ultérieure; aujourd'hui, une question sur la conversion du type de base de données en type C # (casting?) s'est posée.

Voyons un exemple:

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

Cela échoue (évidemment) car les trois appels this.field_x = reader["field_x"]; Génèrent l'erreur du compilateur Cannot implicitly convert type 'object' to 'xxx'. An explicit conversion exists (are you missing a cast?)..

Pour corriger cela, je actuellement connais deux façons (utilisons l'exemple field_b): Le numéro un est this.field_b = (Byte) reader["field_b"]; et le numéro deux est this.field_b = Convert.ToByte(reader["field_b"]);.

Le problème avec l'option numéro un est que les champs DBNull lèvent des exceptions car le transtypage échoue (même avec des types nullables comme String), et le problème avec le numéro deux est qu'il ne préserve pas les valeurs nulles (la Convert.ToString(DBNull) donne un String.Empty), et je ne peux pas les utiliser avec des énumérations aussi.

Donc, après quelques recherches sur Internet et ici à StackOverflow, ce que j'ai trouvé est:

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 cette façon, je devrais gérer chaque cas.

La question est: Est-ce que je manque quelque chose? Suis-je en train de faire un WOMBAT ( Gaspillage d'argent, de cerveau et de temps ) car il existe un moyen plus rapide et plus propre de le faire? Tout est correct? Profit?

29
Albireo

Si un champ autorise les valeurs NULL, n'utilisez pas de types primitifs normaux. Utilisez le type C # nullable et le mot clé as .

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

Ajout d'un ? à tout type C # non nullable le rend "nullable". L'utilisation du mot clé as tentera de convertir un objet dans le type spécifié. Si la conversion échoue (comme si le type était DBNull), l'opérateur retourne null.

Remarque: Un autre petit avantage de l'utilisation de as est qu'il est légèrement plus rapide que le casting normal. Puisqu'il peut également avoir des inconvénients, comme rendre plus difficile le suivi des bogues si vous essayez de caster comme le mauvais type, cela ne devrait pas être considéré comme une raison pour toujours utiliser as par rapport à la distribution traditionnelle. La coulée régulière est déjà une opération assez bon marché.

38
Dan Herbert

ne voulez-vous pas utiliser le reader.Get* méthodes ? La seule chose ennuyeuse est qu'ils prennent des numéros de colonne, vous devez donc envelopper l'accesseur dans un appel à 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
}
12
Sam Holder

Voici comment je l'ai traité dans le passé:

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

Usage:

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

Vous pouvez créer un ensemble de méthodes d'extension, une paire par type de données:

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

Cela devient un peu fastidieux à mettre en œuvre, mais c'est assez efficace. Dans System.Data.DataSetExtensions.dll, Microsoft a résolu le même problème pour les ensembles de données avec un Field<T> method , qui gère génériquement plusieurs types de données et peut transformer DBNull en Nullable.

À titre expérimental, j'ai une fois implémenté une méthode équivalente pour DataReaders, mais j'ai fini par utiliser Reflector pour emprunter une classe interne à DataSetExtensions (UnboxT) pour effectuer efficacement les conversions de type réelles. Je ne suis pas sûr de la légalité de la distribution de cette classe empruntée, donc je ne devrais probablement pas partager le code, mais il est assez facile de rechercher par soi-même.

4
Joel Mueller

Le code générique de mise en ligne affiché ici est cool, mais puisque le titre de la question inclut le mot "efficacement", je publierai ma réponse moins générique mais (j'espère) plus efficace.

Je vous suggère d'utiliser les méthodes getXXX que d'autres ont mentionnées. Pour faire face au problème de numéro de colonne dont parle bebop, j'utilise une énumération, comme celle-ci:

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

C'est un peu plus de frappe, mais vous n'avez pas besoin d'appeler GetOrdinal pour trouver l'index de chaque colonne. Et, au lieu de vous soucier des noms de colonnes, vous vous inquiétez des positions des colonnes.

Pour gérer les colonnes nullables, vous devez vérifier DBNull et peut-être fournir une valeur par défaut:

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