如何(高效地)将 SqlDataReader 字段转换(强制类型转换?)为相应的 C# 类型?

31

首先,让我解释一下当前的情况:我正在从数据库中读取记录并将它们放入一个对象中以备后用;今天有一个关于数据库类型到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字段会抛出异常,因为强制转换失败(即使是可空类型如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);
        }
    }
}

我应该处理每种情况。

问题是:我有什么遗漏吗?我是否在浪费金钱、时间和精力(WOMBAT),因为有更快更简洁的方法?这一切都正确吗?盈利?


看看数据读取器中的各种GetXXX方法。也许它们就是你要找的。 - Chris Dunaway
3
对我来说,这看起来相当全面和通用。SqlDataReader类确实有一些.GetInt32()、.GetBytes()之类的函数可以进行转换,但我认为您仍然需要检查空值。我还建议研究LINQ或ORM,它们可以为您处理这样的细节。 - David Hogue
你尝试过使用FromDatabase吗?最终简单的是使用int,int?,string,DateTime?和Enum值。 - Kiquenet
5个回答

42
如果一个字段允许为空,不要使用普通的基本类型。应该使用 C# nullable类型as关键字
int? field_a = reader["field_a"] as int?;
string field_b = reader["field_a"] as string;

在任何非可空 C# 类型后面添加一个 "?" 就可以使其变为 "可空"。使用 "as" 关键字会尝试将对象强制转换为指定类型。如果强制转换失败(例如类型为 DBNull),则该操作符返回 null。
注意:使用 "as" 的另一个小优点是它比普通转换 稍快。由于它也可能有一些缺点,例如如果您尝试将其转换为错误的类型,则很难跟踪错误,因此这不应被视为始终使用 "as" 而不是传统转换的理由。常规转换已经是一种相当便宜的操作。

无法通过引用转换、装箱转换、拆箱转换、包装转换或空类型转换将类型'System.DBNull'转换为'tnt?'。 - Joel Mueller
你尝试过上面的代码吗?它会起作用的。你说的绝对正确,这就是为什么我的答案有效的原因:http://msdn.microsoft.com/en-us/library/cscsdfbt.aspx - Dan Herbert
我的错误。我测试的是 DBNull.Value as int? 而不是 (object)DBNull.Value as int? - Joel Mueller
1
是的,关于可空类型,在写示例的时候我忘记加那些该死的问号了,但在实际代码中它们是存在的。至于 as 的事情,直到现在我认为它只是 VB 专有的关键字……学到了新东西……唯一“令人害怕”的是 MSDN 页面上所说的:“as 操作符就像转换操作一样。然而,如果无法转换,则 as 返回 null 而不是引发异常。” 但数据库类型不会每隔几天就变一次,对吧?(在这之后,墨菲定律会立即生效……) - Albireo
选择你的答案和Bebop给出的答案只是个人口味的问题,我认为这是被选择的但两者都是有效的。 - Albireo

13

不想使用 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
}

1
我一直避免使用 reader.GetX 方法,因为你必须传递列号,而这样做是“不好的”(例如如果底层存储过程添加了一些附加列怎么办?即使你不关心它们,它们也会破坏代码)。我之前不知道 reader.GetOrdinal 方法,所以甚至没有考虑过使用它们。 - Albireo
我认为你可以通过扩展方法添加一系列重载,使您可以将列名称传递给阅读器并获得各种类型的返回。 - Sam Holder
我不知道这个有多少帮助OP。 OP已经在他的代码中使用了 reader["field_a"]。这只是一个类型转换问题。GetOrdinal无论如何都会执行字符串查找以获取序号,如果这是您想要避免的。如果你正在使用GetOrdinal,则必须将其缓存:https://dev59.com/8nNA5IYBdhLWcg3wIqLr - nawfal
@nawfal 这种方法处理了 OP 提出的 DBNull 问题,当定义为扩展方法时,它们阅读起来更好(仅个人意见)。使用 reader["field_a"]GetOrdinal 结果缓存问题也适用,但在读取大型数据集时通常是微优化,对我而言,提高可读性比使用转换并担心缓存序数更重要。因人而异。 - Sam Holder
1)我明白了。所以在DBNull的情况下,“reader.GetInt32(1)”返回0? 2)是的,扩展方法确实更易读,但是任何东西(我的意思是两种方法)都可以放入该扩展方法中,对吗?您的方法是否更加友好? 3)缓存GetOrdinal的问题是什么,它适用于“reader [“field_a”]”? - nawfal

7
这是我过去处理这个问题的方式:
    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"));

5
你可以创建一组扩展方法,每个数据类型对应一对方法:
    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 中,微软使用 Field<T> 方法 解决了 DataSets 的同样问题,该方法可通用处理多种数据类型,并将 DBNull 转换为 Nullable。
作为一个实验,我曾经为 DataReaders 实现了一个等效的方法,但最终我使用 Reflector 从 DataSetExtensions 借用了一个内部类(UnboxT)来高效地执行类型转换。我不确定分发这个借用的类是否合法,所以我可能不应该分享代码,但很容易自己查找。

3

这里发布的通用处理代码很棒,但由于问题标题包含“高效”一词,因此我将发布我的不太通用但(我希望)更有效的答案。

我建议您使用其他人提到的getXXX方法。为了解决bebop所说的列号问题,我使用枚举,像这样:

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

需要稍微多打一些字,但是这样你就不用调用 GetOrdinal 来查找每一列的索引了。而且,你只需要担心列位置,而无需担心列名。

要处理可空列,你需要检查 DBNull,并可能提供默认值:

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

这并没有解决让我无法使用reader.GetX方法的问题:如果底层记录集布局发生更改,特别是在旧列之间返回其他列,即使您不关心新列,代码也会出错,因为它们的序数已经改变,而您的枚举类型没有更新。 - Albireo
如果您的记录集发生变化,无论使用什么方案都可能会导致问题。在丹的答案中,如果字段名称更改,则代码将无法正常工作。再次强调我发布此答案的原因是因为您提到了效率。通过名称访问字段会在内部使用“for”循环来遍历结果中的所有字段,直到找到匹配的字段。这对于每个字段访问,在每一行中都要执行。因此,假设返回了20个字段和50条记录-您有1,000个“for”循环。加上1,000个类型转换和1,000个可空类型(比标准类型稍大)。 - Ray

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接