如何轻松将 DataReader 转换为 List<T>? [复制]

Posted

技术标签:

【中文标题】如何轻松将 DataReader 转换为 List<T>? [复制]【英文标题】:How can I easily convert DataReader to List<T>? [duplicate] 【发布时间】:2009-09-23 09:14:58 【问题描述】:

我在 DataReader 中有数据,我想将其转换为 List&lt;T&gt;。 对此有什么可能的简单解决方案?

例如在 CustomerEntity 类中,我有 CustomerId 和 CustomerName 属性。如果我的 DataReader 将这两列作为数据返回,那么如何将其转换为 List&lt;CustomerEntity&gt;

【问题讨论】:

【参考方案1】:

我建议为此编写一个扩展方法:

public static IEnumerable<T> Select<T>(this IDataReader reader,
                                       Func<IDataReader, T> projection)

    while (reader.Read())
    
        yield return projection(reader);
    

然后,您可以根据需要使用 LINQ 的 ToList() 方法将其转换为 List&lt;T&gt;,如下所示:

using (IDataReader reader = ...)

    List<Customer> customers = reader.Select(r => new Customer 
        CustomerId = r["id"] is DBNull ? null : r["id"].ToString(),
        CustomerName = r["name"] is DBNull ? null : r["name"].ToString() 
    ).ToList();

我实际上建议将FromDataReader 方法放在Customer(或其他地方):

public static Customer FromDataReader(IDataReader reader)  ... 

那会离开:

using (IDataReader reader = ...)

    List<Customer> customers = reader.Select<Customer>(Customer.FromDataReader)
                                     .ToList();

(我不认为类型推断在这种情况下会起作用,但我可能是错的......)

【讨论】:

扩展方法不应该是:while (reader.Read()) 而不是 while (drOutput.Read()) 优秀。特别是因为这种方法(稍作调整)也可以用于匿名类型,这大大简化了即席查询。 注意这里和reader.Cast&lt;IDataReader&gt;().Select一样。 @Jon: DbDataRader 确实如此。 msdn.microsoft.com/en-us/library/… @SLaks:是的,尽管 OP 从未提及正在使用哪种类型的 DataReader。不过,我个人非常喜欢扩展方法:)【参考方案2】:

我用这个案例写了下面的方法。

首先,添加命名空间:System.Reflection

例如:T 是返回类型(ClassName),dr 是映射DataReader 的参数

C#,调用映射方法如下:

List<Person> personList = new List<Person>();
personList = DataReaderMapToList<Person>(dataReaderForPerson);

这是映射方法:

public static List<T> DataReaderMapToList<T>(IDataReader dr)

    List<T> list = new List<T>();
    T obj = default(T);
    while (dr.Read()) 
        obj = Activator.CreateInstance<T>();
        foreach (PropertyInfo prop in obj.GetType().GetProperties()) 
            if (!object.Equals(dr[prop.Name], DBNull.Value)) 
                prop.SetValue(obj, dr[prop.Name], null);
            
        
        list.Add(obj);
    
    return list;

VB.NET,调用映射方法如下:

Dim personList As New List(Of Person)
personList = DataReaderMapToList(Of Person)(dataReaderForPerson)

这是映射方法:

Public Shared Function DataReaderMapToList(Of T)(ByVal dr As IDataReader) As List(Of T)
        Dim list As New List(Of T)
        Dim obj As T
        While dr.Read()
            obj = Activator.CreateInstance(Of T)()
            For Each prop As PropertyInfo In obj.GetType().GetProperties()
                If Not Object.Equals(dr(prop.Name), DBNull.Value) Then
                    prop.SetValue(obj, dr(prop.Name), Nothing)
                End If
            Next
            list.Add(obj)
        End While
        Return list
    End Function

【讨论】:

实际上,您的 DataReaderMapToList 的内部结构可以很好地作为上面 Jon Skeets 答案的默认投影。 这很好用。我对 C# 代码有一个小建议:将以 prop.SetValue 开头的行更改为 prop.SetValue(obj, Convert.ChangeType(dr[prop.Name], prop.PropertyType), null);。这将使代码适用于字符串以外的类型。 如果列可以为空,这将引发尝试将Nullable&lt;&gt; 转换为类型的异常。这是***.com/a/18015612/3956100的解决方案 可能看起来很明显,但是 SELECT 命令中的字段名称必须与数据对象中的字段名称匹配【参考方案3】:

我见过使用反射和属性或字段上的属性将 DataReader 映射到对象的系统。 (有点像 LinqToSql 所做的。)它们节省了一些输入,并且在为 DBNull 等编码时可以减少错误的数量。一旦你缓存了生成的代码,它们也可以比大多数手写代码更快,所以 如果您经常这样做,请考虑“大路”。

请参阅"A Defense of Reflection in .NET" 了解其中的一个示例。

然后你可以编写类似的代码

class CustomerDTO  

    [Field("id")]
    public int? CustomerId;

    [Field("name")]
    public string CustomerName;

...

using (DataReader reader = ...)
    
   List<CustomerDTO> customers = reader.AutoMap<CustomerDTO>()
                                    .ToList();

(AutoMap(),是一种扩展方法)


@Stilgar,感谢您的精彩评论

如果能够您可能会更好地使用NHibernate, EF or Linq to Sql, etc 但是在旧项目(或出于其他(有时有效)原因,例如“不是在这里发明”、“喜欢存储procs”等)并不总是可以使用 ORM,因此轻量级的系统对于“袖手旁观”很有用

如果您每个人都需要编写大量 IDataReader 循环,您将看到减少编码(和错误)的好处无需更改您正在处理的系统的架构。这并不是说它是一个很好的架构。..

我假设 CustomerDTO 不会脱离数据访问层,复合对象等将由数据访问层使用 DTO 对象构建。


在我写完这个答案 Dapper 进入 .NET 世界几年后,它可能是编写 onw AutoMapper 的一个很好的起点,也许它会完全消除你这样做的需要。

【讨论】:

这种方法的问题是一旦开始使用复合对象就会遇到很多麻烦。如果客户与公司相关联,则您需要公司属性。您可以递归,但 Company 可能具有 List 属性,然后您必须遍历图表。为此,您需要一个表示关联的属性。这就是 LINQ to SQL 和 Entity Framework 所做的,但它们是大型产品,您无法轻松开发内部解决方案。如果你打算这样做,为什么不使用 EF 呢? 为什么我没有 AutoMap() 我需要添加哪些引用才能使[Field(“id”)] 工作? @Stilgar 对具有“非 EF 规则”的现有数据库使用 EF 是 使用 EF 的一个非常令人信服的原因 ..(不,此评论不是 6晚了几年,因为 EF 仍然存在与当时相同的问题) 你在某个地方有 AutoMap 的实现吗?【参考方案4】:

最简单的解决方案:

var dt = new DataTable();
dt.Load(myDataReader);
List<DataRow> rows = dt.AsEnumerable();
var customers = rows.Select(dr=>new Customer(...)).ToList();

【讨论】:

我没有在 dt 的枚举器上找到 ToList。这段代码有效吗? @coolcake:添加using System.Data;,它是一个扩展方法。 这是一个很酷的解决方案 +1,但请记住 DataTable 解决方案带走了数据读取器的最大优势,即负载需求。 DataTable 首先将整个数据读取到内存中。顺便说一句,AsEnumerable 扩展方法在 System.Data.DataSetExtensions 程序集中(我必须说的程序集的名称很奇怪,听起来更像命名空间)。 我在这里遗漏了什么吗?此解决方案不会产生List(Of MyType),它会产生List(Of DataRow),这与操作员请求的解决方案有很大不同。 给他一个编程奥斯卡奖。【参考方案5】:

我会(并且已经)开始使用Dapper。使用你的例子就像(从记忆中写的):

public List<CustomerEntity> GetCustomerList()

    using (DbConnection connection = CreateConnection())
    
        return connection.Query<CustomerEntity>("procToReturnCustomers", commandType: CommandType.StoredProcedure).ToList();
    

CreateConnection() 将处理访问您的数据库并返回连接。

Dapper 自动处理将数据字段映射到属性。它还支持多种类型和结果集,速度非常快。

查询返回IEnumerable,因此返回ToList()

【讨论】:

这是一个很棒的答案!!!我刚刚下载了 Dapper,它运行良好,为我节省了很多时间和头痛!谢谢【参考方案6】:

显然@Ian Ringrose 的中心论点是您应该为此使用库是这里最好的单一答案(因此是 +1),但对于最少的一次性或演示代码,这里是@SLaks 的具体说明对@Jon Skeet 的更细化(+1'd)答案的微妙评论:

public List<XXX> Load( <<args>> )

    using ( var connection = CreateConnection() )
    using ( var command = Create<<ListXXX>>Command( <<args>>, connection ) )
    
        connection.Open();
        using ( var reader = command.ExecuteReader() )
            return reader.Cast<IDataRecord>()
                .Select( x => new XXX( x.GetString( 0 ), x.GetString( 1 ) ) )
                .ToList();
    


正如@Jon Skeet 的回答,

            .Select( x => new XXX( x.GetString( 0 ), x.GetString( 1 ) ) )

bit 可以提取到帮助器中(我喜欢将它们转储到查询类中):

    public static XXX FromDataRecord( this IDataRecord record)
    
        return new XXX( record.GetString( 0 ), record.GetString( 1 ) );
    

并用作:

            .Select( FromDataRecord )

13 年 3 月 9 日更新:另见 Some excellent further subtle coding techniques to split out the boilerplate in this answer

【讨论】:

【参考方案7】:

您不能简单(直接)将数据读取器转换为列表。

你必须遍历datareader中的所有元素并插入到列表中

示例代码下方

using (drOutput)   

            System.Collections.Generic.List<CustomerEntity > arrObjects = new System.Collections.Generic.List<CustomerEntity >();        
            int customerId = drOutput.GetOrdinal("customerId ");
            int CustomerName = drOutput.GetOrdinal("CustomerName ");

        while (drOutput.Read())        
        
            CustomerEntity obj=new CustomerEntity ();
            obj.customerId = (drOutput[customerId ] != Convert.DBNull) ? drOutput[customerId ].ToString() : null;
            obj.CustomerName = (drOutput[CustomerName ] != Convert.DBNull) ? drOutput[CustomerName ].ToString() : null;
            arrObjects .Add(obj);
        


【讨论】:

【参考方案8】:

我已经在一个宠物项目中介绍了这一点。 使用你想要的。

请注意,ListEx 实现了 IDataReader 接口。


people = new ListExCommand(command)
.Map(p=> new ContactPerson()

  Age = p.GetInt32(p.GetOrdinal("Age")),
  FirstName = p.GetString(p.GetOrdinal("FirstName")),
  IdNumber = p.GetInt64(p.GetOrdinal("IdNumber")),
  Surname = p.GetString(p.GetOrdinal("Surname")),
  Email = "z.evans@caprisoft.co.za"
)
.ToListEx()
.Where("FirstName", "Peter");

或者像下面的例子那样使用对象映射。


people = new ListExAutoMap(personList)
.Map(p => new ContactPerson()

    Age = p.Age,
    FirstName = p.FirstName,
    IdNumber = p.IdNumber,
    Surname = p.Surname,
    Email = "z.evans@caprisoft.co.za"
)
.ToListEx()
.Where(contactPerson => contactPerson.FirstName == "Zack");

看看http://caprisoft.codeplex.com

【讨论】:

这不是和 Jon Skeet 的 Func&lt;IDataReader, T&gt; 方法完全一样吗?是的。并且通过引入您自己的ICommandIConnections,它更难在各种ADO.NET 提供程序之间进行互操作。我不明白为什么首先需要它。 在你的ListExCommand 类中,如果我们必须手动提供映射器,为什么还要使用反射来绑定属性?在您的ListExAutoMap 类中,我们在哪里将IEnumerable&lt;T&gt; 传递给构造函数,因为我们只剩下IEnumerable (DbDataReader)。如果我们必须手动在其上添加foreach(例如reader.Cast&lt;IDataRecord&gt;()),那么这会使您在类中的内部foreach 循环变得多余,总体而言使这种方法非常缓慢。 +1 的努力..【参考方案9】:

我知道这个问题很老,并且已经回答了,但是......

既然 SqlDataReader 已经实现了 IEnumerable,为什么还需要在记录上创建一个循环呢?

我一直在使用下面的方法,没有任何问题,也没有任何性能问题:到目前为止,我已经测试了 IList、List(Of T)、IEnumerable、IEnumerable(Of T)、IQueryable 和 IQueryable(Of T )

Imports System.Data.SqlClient
Imports System.Data
Imports System.Threading.Tasks

Public Class DataAccess
Implements IDisposable

#Region "   Properties  "

''' <summary>
''' Set the Query Type
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property QueryType() As CmdType
    Set(ByVal value As CmdType)
        _QT = value
    End Set
End Property
Private _QT As CmdType

''' <summary>
''' Set the query to run
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property Query() As String
    Set(ByVal value As String)
        _Qry = value
    End Set
End Property
Private _Qry As String

''' <summary>
''' Set the parameter names
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property ParameterNames() As Object
    Set(ByVal value As Object)
        _PNs = value
    End Set
End Property
Private _PNs As Object

''' <summary>
''' Set the parameter values
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property ParameterValues() As Object
    Set(ByVal value As Object)
        _PVs = value
    End Set
End Property
Private _PVs As Object

''' <summary>
''' Set the parameter data type
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property ParameterDataTypes() As DataType()
    Set(ByVal value As DataType())
        _DTs = value
    End Set
End Property
Private _DTs As DataType()

''' <summary>
''' Check if there are parameters, before setting them
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Private ReadOnly Property AreParams() As Boolean
    Get
        If (IsArray(_PVs) And IsArray(_PNs)) Then
            If (_PVs.GetUpperBound(0) = _PNs.GetUpperBound(0)) Then
                Return True
            Else
                Return False
            End If
        Else
            Return False
        End If
    End Get
End Property

''' <summary>
''' Set our dynamic connection string
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Private ReadOnly Property _ConnString() As String
    Get
        If System.Diagnostics.Debugger.IsAttached OrElse My.Settings.AttachToBeta OrElse Not (Common.CheckPaid) Then
            Return My.Settings.DevConnString
        Else
            Return My.Settings.TurboKitsv2ConnectionString
        End If
    End Get
End Property

Private _Rdr As SqlDataReader
Private _Conn As SqlConnection
Private _Cmd As SqlCommand

#End Region

#Region "   Methods "

''' <summary>
''' Fire us up!
''' </summary>
''' <remarks></remarks>
Public Sub New()
    Parallel.Invoke(Sub()
                        _Conn = New SqlConnection(_ConnString)
                    End Sub,
                    Sub()
                        _Cmd = New SqlCommand
                    End Sub)
End Sub

''' <summary>
''' Get our results
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function GetResults() As SqlDataReader
    Try
        Parallel.Invoke(Sub()
                            If AreParams Then
                                PrepareParams(_Cmd)
                            End If
                            _Cmd.Connection = _Conn
                            _Cmd.CommandType = _QT
                            _Cmd.CommandText = _Qry
                            _Cmd.Connection.Open()
                            _Rdr = _Cmd.ExecuteReader(CommandBehavior.CloseConnection)
                        End Sub)
        If _Rdr.HasRows Then
            Return _Rdr
        Else
            Return Nothing
        End If
    Catch sEx As SqlException
        Return Nothing
    Catch ex As Exception
        Return Nothing
    End Try
End Function

''' <summary>
''' Prepare our parameters
''' </summary>
''' <param name="objCmd"></param>
''' <remarks></remarks>
Private Sub PrepareParams(ByVal objCmd As Object)
    Try
        Dim _DataSize As Long
        Dim _PCt As Integer = _PVs.GetUpperBound(0)

        For i As Long = 0 To _PCt
            If IsArray(_DTs) Then
                Select Case _DTs(i)
                    Case 0, 33, 6, 9, 13, 19
                        _DataSize = 8
                    Case 1, 3, 7, 10, 12, 21, 22, 23, 25
                        _DataSize = Len(_PVs(i))
                    Case 2, 20
                        _DataSize = 1
                    Case 5
                        _DataSize = 17
                    Case 8, 17, 15
                        _DataSize = 4
                    Case 14
                        _DataSize = 16
                    Case 31
                        _DataSize = 3
                    Case 32
                        _DataSize = 5
                    Case 16
                        _DataSize = 2
                    Case 15
                End Select
                objCmd.Parameters.Add(_PNs(i), _DTs(i), _DataSize).Value = _PVs(i)
            Else
                objCmd.Parameters.AddWithValue(_PNs(i), _PVs(i))
            End If
        Next
    Catch ex As Exception
    End Try
End Sub

#End Region

#Region "IDisposable Support"

Private disposedValue As Boolean ' To detect redundant calls

' IDisposable
Protected Overridable Sub Dispose(ByVal disposing As Boolean)
    If Not Me.disposedValue Then
        If disposing Then
        End If
        Try
            Erase _PNs : Erase _PVs : Erase _DTs
            _Qry = String.Empty
            _Rdr.Close()
            _Rdr.Dispose()
            _Cmd.Parameters.Clear()
            _Cmd.Connection.Close()
            _Conn.Close()
            _Cmd.Dispose()
            _Conn.Dispose()
        Catch ex As Exception

        End Try
    End If
    Me.disposedValue = True
End Sub

' TODO: override Finalize() only if Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources.
Protected Overrides Sub Finalize()
    ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
    Dispose(False)
    MyBase.Finalize()
End Sub

' This code added by Visual Basic to correctly implement the disposable pattern.
Public Sub Dispose() Implements IDisposable.Dispose
    ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
    Dispose(True)
    GC.SuppressFinalize(Me)
End Sub

#End Region

End Class

强类型类

Public Class OrderDCTyping
    Public Property OrderID As Long = 0
    Public Property OrderTrackingNumber As String = String.Empty
    Public Property OrderShipped As Boolean = False
    Public Property OrderShippedOn As Date = Nothing
    Public Property OrderPaid As Boolean = False
    Public Property OrderPaidOn As Date = Nothing
    Public Property TransactionID As String
End Class

用法

Public Function GetCurrentOrders() As IEnumerable(Of OrderDCTyping)
    Try
        Using db As New DataAccess
            With db
                .QueryType = CmdType.StoredProcedure
                .Query = "[Desktop].[CurrentOrders]"
                Using _Results = .GetResults()
                    If _Results IsNot Nothing Then
                        _Qry = (From row In _Results.Cast(Of DbDataRecord)()
                                    Select New OrderDCTyping() With 
                                        .OrderID = Common.IsNull(Of Long)(row, 0, 0),
                                        .OrderTrackingNumber = Common.IsNull(Of String)(row, 1, String.Empty),
                                        .OrderShipped = Common.IsNull(Of Boolean)(row, 2, False),
                                        .OrderShippedOn = Common.IsNull(Of Date)(row, 3, Nothing),
                                        .OrderPaid = Common.IsNull(Of Boolean)(row, 4, False),
                                        .OrderPaidOn = Common.IsNull(Of Date)(row, 5, Nothing),
                                        .TransactionID = Common.IsNull(Of String)(row, 6, String.Empty)
                                    ).ToList()
                    Else
                        _Qry = Nothing
                    End If
                End Using
                Return _Qry
            End With
        End Using
    Catch ex As Exception
        Return Nothing
    End Try
End Function

【讨论】:

IDataReader 已继承 IEnumerable IDataReader 不继承 IEnumerable。 System.Data 命名空间中的 .NET 4 Framework 中的至少标准版本不在我的机器上。 你是对的......不是继承......实现:msdn.microsoft.com/en-us/library/… 我是对的,因为错误的原因,我很抱歉我查看的是 IDataReader 接口而不是 DataReader 本身。对工具和继承的挑剔实际上是无意的。

以上是关于如何轻松将 DataReader 转换为 List<T>? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

c#如何将查询后的结果放入list中

如何将 DataSet a 转换为 DataReader?

如何在C#中使用存储过程和datareader返回记录列表

C# - 将 DataReader 转换为 DataTable

Spring Boot 中使用一个注解轻松将 List 转换为 Excel 下载

轻松将列表列表转换为用于大数据的 numpy Multidim 数组