Как (эффективно) преобразовать (привести?) поле SqlDataReader к соответствующему типу С#?

Во-первых, позвольте мне объяснить текущую ситуацию: я читаю записи из базы данных и помещаю их в объект для последующего использования; сегодня возник вопрос о преобразовании типа базы данных в тип C# (приведение?).

Давайте посмотрим пример:

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

Это (очевидно) терпит неудачу, потому что три вызова this.field_x = reader["field_x"]; вызывают ошибку компилятора Cannot implicitly convert type 'object' to 'xxx'. An explicit conversion exists (are you missing a cast?)..

Чтобы исправить это, я на данный момент знаю два способа (давайте воспользуемся примером field_b): номер один — this.field_b = (Byte) reader["field_b"];, а номер два — this.field_b = Convert.ToByte(reader["field_b"]);.

Проблема с вариантом номер один заключается в том, что поля DBNull генерируют исключения при сбое приведения (даже с типами, допускающими значение NULL, такими как String), а проблема с номером два заключается в том, что он не сохраняет нулевые значения (Convert.ToString(DBNull) дает String.Empty), и я нельзя использовать их и с перечислениями.

Итак, после пары поисков в Интернете и здесь, в StackOverflow, я придумал следующее:

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

Таким образом, я должен обрабатывать каждый случай.

Вопрос: я что-то упустил? Делаю ли я вомбат (пустая трата денег, мозгов и времени), поскольку есть более быстрый и чистый способ сделать это? Все правильно? Выгода?


person Albireo    schedule 26.01.2010    source источник
comment
Посмотрите на различные методы чтения данных GetXXX. Возможно, это то, что вы ищете.   -  person Chris Dunaway    schedule 26.01.2010
comment
Это выглядит довольно всеобъемлющим и общим для меня. В классе SqlDataReader есть некоторые функции типа .GetInt32(), .GetBytes() для преобразования, которые сделают это за вас, но я думаю, что вам все равно нужно проверять наличие нулей. Я бы также посмотрел на LINQ или ORM, они позаботятся о таких деталях для вас.   -  person David Hogue    schedule 26.01.2010
comment
вы пытались использовать FromDatabase? final просто об этом, используя значения int, int?, string, DateTime?, Enum?   -  person Kiquenet    schedule 13.04.2015


Ответы (5)


Если поле допускает пустые значения, не используйте обычные примитивные типы. Используйте тип C# nullable и asключевое слово.

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

Добавление ? к любому типу C#, не допускающему значения NULL, делает его "обнуляемым". Использование ключевого слова as попытается привести объект к указанному типу. Если приведение не удается (как если бы тип был DBNull), то оператор возвращает null.

Примечание. Еще одно небольшое преимущество использования as заключается в том, что оно немного быстрее, чем обычное приведение. Поскольку у него также могут быть некоторые недостатки, например, усложняется отслеживание ошибок, если вы пытаетесь выполнить приведение как неправильный тип, это не должно рассматриваться как причина всегда использовать as вместо традиционного приведения. Обычный кастинг уже достаточно дешевая операция.

person Dan Herbert    schedule 26.01.2010
comment
Не удается преобразовать тип «System.DBNull» в «int?» через преобразование ссылки, преобразование упаковки, преобразование распаковки, преобразование упаковки или преобразование нулевого типа. - person Joel Mueller; 26.01.2010
comment
Вы пробовали код выше? Это будет работать. То, что вы сказали, абсолютно верно, и именно поэтому мой ответ работает: msdn.microsoft.com /en-us/library/cscsdfbt.aspx - person Dan Herbert; 26.01.2010
comment
Моя ошибка. Я тестировал DBNull.Value as int?, а не (object)DBNull.Value as int?. - person Joel Mueller; 26.01.2010
comment
Да, о типах, допускающих значение NULL, я забыл добавить эти чертовы вопросительные знаки при написании примера, в реальном коде они есть. Что касается as, до сих пор я думал, что это ключевое слово только для VB... Узнал что-то новое... Единственное, что пугает, это то, что сказано на странице MSDN, на которую вы ссылаетесь: «Оператор as подобен литейная операция. Однако, если преобразование невозможно, as возвращает null вместо того, чтобы вызвать исключение.» Но тип базы данных не будет меняться каждые пару дней, не так ли? (После этого мгновенно сработает закон Мерфи...) - person Albireo; 27.01.2010
comment
Я думаю, что выбор между вашим ответом и ответом Бибопа - это только вопрос вкуса, поэтому я отмечу его как выбранный, но оба варианта действительны. - person Albireo; 27.01.2010

вы не хотите использовать методы reader.Get* ? Единственное, что раздражает, это то, что они берут номера столбцов, поэтому вам нужно обернуть метод доступа вызовом 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
}
person Sam Holder    schedule 26.01.2010
comment
Я всегда избегал методов reader.GetX, потому что вы должны передать им номер столбца, и потому что это плохо (что, если, например, базовая хранимая процедура получит несколько дополнительных столбцов? Даже если вы не заботитесь о них, они испортят код .) и я не знал о методе reader.GetOrdinal, я даже никогда не рассматривал их. - person Albireo; 27.01.2010
comment
Я предполагаю, что вы, вероятно, могли бы добавить серию перегрузок с помощью метода расширения, который позволил бы вам передать имя столбца читателю и вернуть различные типы. - person Sam Holder; 27.01.2010
comment
Я не знаю, насколько это помогает ОП. OP уже использует reader["field_a"] в своем коде. Это просто вопрос приведения к фактическому типу. GetOrdinal в любом случае выполняет поиск строки, чтобы получить порядковый номер, если это то, чего вы пытаетесь избежать. Если вы используете GetOrdinal, вам необходимо кэшировать его: stackoverflow.com/questions/1079366/ - person nawfal; 31.07.2015
comment
@nawfal Этот подход решает проблему DBNull, о которой сообщает OP, и когда он определяется как методы расширения, они лучше читаются (ИМХО). Проблема с кэшированием результата GetOrdinal относится и к использованию reader["field_a"], но это часто является микрооптимизацией при чтении больших наборов данных, и для меня улучшенная читаемость превосходит использование приведения и беспокойство о кэшированном порядковом номере в большинстве ситуаций. YMMV. - person Sam Holder; 31.07.2015
comment
@SamHolder 1) Понятно. Итак, reader.GetInt32(1) возвращает 0 в случаях DBNull? 2) Да, методы расширения читаются лучше, но все (я имею в виду любой подход) может войти в этот метод расширения, нет? Является ли ваш подход более удобным для расширения? Я не понимаю. 3) В чем проблема с кэшированием GetOrdinal, которое применяется к reader[field_a]? - person nawfal; 31.07.2015

Вот как я справлялся с этим в прошлом:

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

Применение:

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

Вы можете создать набор методов расширения, по одной паре для каждого типа данных:

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

Это становится немного утомительным для реализации, но это довольно эффективно. В System.Data.DataSetExtensions.dll Microsoft решила ту же проблему для наборов данных с помощью Field<T> метод, который обычно обрабатывает несколько типов данных и может превратить DBNull в Nullable.

В качестве эксперимента я однажды реализовал эквивалентный метод для DataReaders, но в итоге я использовал Reflector, чтобы позаимствовать внутренний класс из DataSetExtensions (UnboxT) для эффективного преобразования фактических типов. Я не уверен в законности распространения этого заимствованного класса, поэтому, вероятно, мне не следует делиться кодом, но его довольно легко найти самому.

person Joel Mueller    schedule 26.01.2010

Общий код обработки, опубликованный здесь, классный, но, поскольку в заголовке вопроса есть слово «эффективно», я опубликую свой менее общий, но (надеюсь) более эффективный ответ.

Я предлагаю вам использовать методы getXXX, о которых упоминали другие. Чтобы решить проблему с номером столбца, о которой говорит бибоп, я использую перечисление, например:

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

Это требует немного дополнительного ввода, но тогда вам не нужно вызывать GetOrdinal, чтобы найти индекс для каждого столбца. И вместо того, чтобы беспокоиться об именах столбцов, вы беспокоитесь о позициях столбцов.

Чтобы иметь дело со столбцами, допускающими значение NULL, вам нужно проверить наличие DBNull и, возможно, указать значение по умолчанию:

string phoneNumber;
if (Convert.IsDBNull(sqlDataReader[(int)readerFields.PhoneNumber]) {
  phoneNumber = string.Empty;
}
else {
  phoneNumber = sqlDataReader.getString((int)readerFields.PhoneNumber);
}
person Ray    schedule 26.01.2010
comment
Это не решает проблему, которая удерживала меня от методов reader.GetX: если изменяется макет базового набора записей, особенно если возвращаются дополнительные столбцы между старыми, код испортится, даже если вам не нужны новые столбцы. , так как их порядковый номер меняется, а ваше перечисление - нет. - person Albireo; 27.01.2010
comment
Если ваш набор записей изменится, это может вызвать проблемы независимо от того, какую схему вы используете. В принятом ответе Дэна, если имена полей меняются, код не работает. Опять же, причина, по которой я опубликовал этот ответ, заключалась в том, что вы упомянули об эффективности. При доступе к полям по именам используется внутренний цикл for, чтобы пройтись по всем полям в результатах, пока не будет найдено подходящее. Это делается для доступа к каждому полю в каждой строке. Итак, скажем, возвращено 20 полей и 50 записей — у вас есть 1000 циклов for. Плюс 1000 приведений типов и 1000 типов, допускающих значение NULL (что немного больше стандартных типов). - person Ray; 27.01.2010