Jak (efektywnie) konwertować (cast?) pole SqlDataReader do odpowiedniego typu c#?

Najpierw pozwól mi wyjaśnić obecną sytuację: czytam rekordy z bazy danych i umieszczam je w obiekcie do późniejszego wykorzystania; dzisiaj pytanie o konwersję typu bazy danych na typ C# (casting?).

Zobaczmy przykład:

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

Jest to (oczywiście) błąd, ponieważ trzy wywołania this.field_x = reader["field_x"]; wywołują błąd kompilatora Cannot implicitly convert type 'object' to 'xxx'. An explicit conversion exists (are you missing a cast?)..

Aby to poprawić i obecnie znam dwa sposoby (użyjmy przykładu field_b): Numer jeden jest this.field_b = (Byte) reader["field_b"];, a numer dwa to this.field_b = Convert.ToByte(reader["field_b"]);.

Problem z opcją numer jeden polega na tym, że DBNull pola rzucają wyjątki, ponieważ Cast zawodzi (nawet z typami nullable jako String), a problem z numerem dwa polega na tym, że nie zachowuje wartości null (Convert.ToString(DBNull) daje String.Empty), i nie mogę ich używać z enumami.

Więc, po kilku poszukiwaniach w Internecie i tutaj w StackOverflow, to co wymyśliłem to:

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

W ten sposób ja powinienem zająć się każdym case.

Pytanie brzmi: czy coś przeoczyłem? Czy robię WOMBAT (strata pieniędzy, mózgu i czasu), ponieważ jest na to szybszy i czystszy sposób? Wszystko w porządku? Zysk?

Author: Kiquenet, 2010-01-26

5 answers

Jeśli pole pozwala na NULL, nie używaj zwykłych, prymitywnych typów. Użyj typu C # nullable i as Słowo kluczowe .

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

Dodanie ? do dowolnego nie-nullable typu C# sprawia, że jest "nullable". Użycie słowa kluczowego as spowoduje próbę oddania obiektu do określonego typu. Jeśli cast nie powiedzie się (jak gdyby typ był DBNull), wtedy operator zwraca null.

Uwaga: kolejną niewielką zaletą używania as jest to, że jest to nieco szybsze niż normalny casting. Ponieważ może mieć również pewne wady, takie jak utrudnianie śledzenia błędów, jeśli próbujesz rzucać jako niewłaściwy typ, nie powinno to być uważane za powód, dla którego zawsze używasz as zamiast tradycyjnego rzucania. Regularne odlewanie jest już dość tanią operacją.

 39
Author: Dan Herbert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-26 18:41:15

Nie chcesz używać reader.Get* Metody ? Jedyną irytującą rzeczą jest to, że biorą numery kolumn, więc musisz zawinąć accessor w wywołanie 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
}
 13
Author: Sam Holder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-26 18:27:44

Tak sobie z tym radziłem w przeszłości:

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

Użycie:

int? myInt = reader.GetNullableField<int>(reader.GetOrdinal("myIntField"));
 6
Author: BFree,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-26 18:52:11

Możesz utworzyć zestaw metod rozszerzenia, po jednej parze dla każdego typu danych:

    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);
    }
To jest trochę żmudne do wdrożenia, ale jest dość wydajne. W Systemie.Data.DataSetExtensions.dll, Microsoft rozwiązał ten sam problem dla zbiorów danych z Field<T> Metoda , która ogólnie obsługuje wiele typów danych i może zmienić DBNull w Nullable.

Jako eksperyment, kiedyś zaimplementowałem równoważną metodę dla Datareaderów, ale skończyło się na użyciu reflektora, aby pożyczyć wewnętrzny klasa z DataSetExtensions (UnboxT), aby efektywnie wykonywać rzeczywiste konwersje typu. Nie jestem pewien co do legalności dystrybucji tej zapożyczonej klasy, więc prawdopodobnie nie powinienem dzielić się kodem, ale dość łatwo jest poszukać siebie.

 4
Author: Joel Mueller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-26 18:37:13

Ogólny kod hanlding opublikowany tutaj jest fajny, ale ponieważ tytuł pytania zawiera słowo "efektywnie", opublikuję moją mniej ogólną, ale (mam nadzieję) bardziej skuteczną odpowiedź.

Sugeruję użycie metod getXXX, o których wspominali inni. Aby poradzić sobie z problemem numer kolumny, o którym mówi bebop, używam enum, jak to:

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

To trochę dodatkowe typowanie, ale nie musisz wywoływać GetOrdinal, aby znaleźć indeks dla każdej kolumny. I zamiast martwić się o nazwy kolumn, martwisz się o pozycje kolumn.

Aby poradzić sobie z nullable columns, musisz sprawdzić DBNull i być może podać domyślną wartość:

string phoneNumber;
if (Convert.IsDBNull(sqlDataReader[(int)readerFields.PhoneNumber]) {
  phoneNumber = string.Empty;
}
else {
  phoneNumber = sqlDataReader.getString((int)readerFields.PhoneNumber);
}
 3
Author: Ray,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-26 19:05:57