通用DbDataReader到List 映射

我的属性绑定数据访问类有一个小问题(更像是烦恼)。 问题是当读取器中没有列中的相应属性时映射失败。

这是mapper类:

// Map our datareader object to a strongly typed list private static IList Map(DbDataReader dr) where T : new() { try { // initialize our returnable list List list = new List(); // fire up the lamda mapping var converter = new Converter(); while (dr.Read()) { // read in each row, and properly map it to our T object var obj = converter.CreateItemFromRow(dr); // add it to our list list.Add(obj); } // reutrn it return list; } catch (Exception ex) { return default(List); } } 

转换器类:

 ///  /// Converter class to convert returned Sql Records to strongly typed classes ///  /// Type of the object we'll convert too internal class Converter where T : new() { // Concurrent Dictionay objects private static ConcurrentDictionary _convertActionMap = new ConcurrentDictionary(); // Delegate action declaration private Action _convertAction; // Build our mapping based on the properties in the class/type we've passed in to the class private static Action GetMapFunc() { var exps = new List(); var paramExp = Expression.Parameter(typeof(IDataReader), "o7thDR"); var targetExp = Expression.Parameter(typeof(T), "o7thTarget"); var getPropInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) }); var _props = typeof(T).GetProperties(); foreach (var property in _props) { var getPropExp = Expression.MakeIndex(paramExp, getPropInfo, new[] { Expression.Constant(property.Name, typeof(string)) }); var castExp = Expression.TypeAs(getPropExp, property.PropertyType); var bindExp = Expression.Assign(Expression.Property(targetExp, property), castExp); exps.Add(bindExp); } // return our compiled mapping, this will ensure it is cached to use through our record looping return Expression.Lambda<Action>(Expression.Block(exps), new[] { paramExp, targetExp }).Compile(); } internal Converter() { // Fire off our mapping functionality _convertAction = (Action)_convertActionMap.GetOrAdd(typeof(T), (t) => GetMapFunc()); } internal T CreateItemFromRow(IDataReader dataReader) { T result = new T(); _convertAction(dataReader, result); return result; } } 

例外

 System.IndexOutOfRangeException {"Mileage"} 

堆栈跟踪

 at System.Data.ProviderBase.FieldNameLookup.GetOrdinal(String fieldName) at System.Data.SqlClient.SqlDataReader.GetOrdinal(String name) at System.Data.SqlClient.SqlDataReader.get_Item(String name) at lambda_method(Closure , IDataReader , Typing ) at o7th.Class.Library.Data.Converter`1.CreateItemFromRow(IDataReader dataReader) in d:\Backup Folder\Development\o7th Web Design\o7th.Class.Library.C-Sharp\o7th.Class.Library\Data Access Object\Converter.cs:line 50 at o7th.Class.Library.Data.Wrapper.Map[T](DbDataReader dr) in d:\Backup Folder\Development\o7th Web Design\o7th.Class.Library.C-Sharp\o7th.Class.Library\Data Access Object\Wrapper.cs:line 33 

我如何修复它,以便当我有一个读者可能没有列的额外属性时它不会失败,反之亦然? 当然,快速创可贴只是在示例中简单地将NULL As Mileage添加到此查询中,但是,这不是问题的解决方案:)


这是使用reflection的Map

 // Map our datareader object to a strongly typed list private static IList Map(DbDataReader dr) where T : new() { try { // initialize our returnable list List list = new List(); T item = new T(); PropertyInfo[] properties = (item.GetType()).GetProperties(); while (dr.Read()) { int fc = dr.FieldCount; for (int j = 0; j < fc; ++j) { var pn = properties[j].Name; var gn = dr.GetName(j); if (gn == pn) { properties[j].SetValue(item, dr[j], null); } } list.Add(item); } // return it return list; } catch (Exception ex) { // Catch an exception if any, an write it out to our logging mechanism, in addition to adding it our returnable message property _Msg += "Wrapper.Map Exception: " + ex.Message; ErrorReporting.WriteEm.WriteItem(ex, "o7th.Class.Library.Data.Wrapper.Map", _Msg); // make sure this method returns a default List return default(List); } } 

注意:此方法比使用表达式树慢63%…

如注释中所述,问题是读者中没有指定属性的列。 我们的想法是首先按读者的列名循环,然后检查是否存在匹配属性。 但是如何预先获得列名列表呢?

  1. 一种想法是使用表达式树本身来构建阅读器中的列名列表,并根据类的属性进行检查。 像这样的东西

     var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR"); var loopIncrementVariableExp = Expression.Parameter(typeof(int), "i"); var columnNamesExp = Expression.Parameter(typeof(List), "columnNames"); var columnCountExp = Expression.Property(paramExp, "FieldCount"); var getColumnNameExp = Expression.Call(paramExp, "GetName", Type.EmptyTypes, Expression.PostIncrementAssign(loopIncrementVariableExp)); var addToListExp = Expression.Call(columnNamesExp, "Add", Type.EmptyTypes, getColumnNameExp); var labelExp = Expression.Label(columnNamesExp.Type); var getColumnNamesExp = Expression.Block( new[] { loopIncrementVariableExp, columnNamesExp }, Expression.Assign(columnNamesExp, Expression.New(columnNamesExp.Type)), Expression.Loop( Expression.IfThenElse( Expression.LessThan(loopIncrementVariableExp, columnCountExp), addToListExp, Expression.Break(labelExp, columnNamesExp)), labelExp)); 

    将相当于

     List columnNames = new List(); for (int i = 0; i < reader.FieldCount; i++) { columnNames.Add(reader.GetName(i)); } 

    人们可以继续最后的表达,但是这里有一个问题,在这条线上做出任何进一步的努力是徒劳的。 上面的表达式树将在每次调用最终委托时获取列名称,在您的情况下,每个对象创建都是如此,这违背了您的要求。

  2. 另一种方法是让转换器类通过属性( 参见示例 )或维护静态字典(如Dictionary> )对给定类型的列名进行预定义的感知。 )。 虽然它提供了更大的灵活性,但另一方面,您的查询不必总是包含表的所有列名,任何reader[notInTheQueryButOnlyInTheTableColumn]都会导致exception。

  3. 我看到的最好的方法是从reader对象中获取列名,但只获取一次。 我会重写这样的事情:

     private static List columnNames; private static Action GetMapFunc() { var exps = new List(); var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR"); var targetExp = Expression.Parameter(typeof(T), "o7thTarget"); var getPropInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) }); foreach (var columnName in columnNames) { var property = typeof(T).GetProperty(columnName); if (property == null) continue; // use 'columnName' instead of 'property.Name' to speed up reader lookups //in case of certain readers. var columnNameExp = Expression.Constant(columnName); var getPropExp = Expression.MakeIndex( paramExp, getPropInfo, new[] { columnNameExp }); var castExp = Expression.TypeAs(getPropExp, property.PropertyType); var bindExp = Expression.Assign( Expression.Property(targetExp, property), castExp); exps.Add(bindExp); } return Expression.Lambda>( Expression.Block(exps), paramExp, targetExp).Compile(); } internal T CreateItemFromRow(IDataReader dataReader) { if (columnNames == null) { columnNames = Enumerable.Range(0, dataReader.FieldCount) .Select(x => dataReader.GetName(x)) .ToList(); _convertAction = (Action)_convertActionMap.GetOrAdd( typeof(T), (t) => GetMapFunc()); } T result = new T(); _convertAction(dataReader, result); return result; } 

    现在问题为什么不直接将数据读取器传递给构造函数? 那会更好。

     private IDataReader dataReader; private Action GetMapFunc() { var exps = new List(); var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR"); var targetExp = Expression.Parameter(typeof(T), "o7thTarget"); var getPropInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) }); var columnNames = Enumerable.Range(0, dataReader.FieldCount) .Select(x => dataReader.GetName(x)); foreach (var columnName in columnNames) { var property = typeof(T).GetProperty(columnName); if (property == null) continue; // use 'columnName' instead of 'property.Name' to speed up reader lookups //in case of certain readers. var columnNameExp = Expression.Constant(columnName); var getPropExp = Expression.MakeIndex( paramExp, getPropInfo, new[] { columnNameExp }); var castExp = Expression.TypeAs(getPropExp, property.PropertyType); var bindExp = Expression.Assign( Expression.Property(targetExp, property), castExp); exps.Add(bindExp); } return Expression.Lambda>( Expression.Block(exps), paramExp, targetExp).Compile(); } internal Converter(IDataReader dataReader) { this.dataReader = dataReader; _convertAction = (Action)_convertActionMap.GetOrAdd( typeof(T), (t) => GetMapFunc()); } internal T CreateItemFromRow() { T result = new T(); _convertAction(dataReader, result); return result; } 

    称之为

     List list = new List(); var converter = new Converter(dr); while (dr.Read()) { var obj = converter.CreateItemFromRow(); list.Add(obj); } 

不过,我可以提出一些改进建议。

  1. 你在CreateItemFromRow调用的通用new T()速度较慢, 它在后台使用reflection 。 您可以将该部分委托给表达式树, 这应该更快

  2. 现在GetProperty调用不区分大小写,这意味着您的列名必须与属性名称完全匹配。 我会使用其中一个Bindings.Flag使它不区分大小写。

  3. 我不确定为什么你在这里使用ConcurrentDictionary作为缓存机制。 generics类中的静态字段对于每个T都是唯一的 。 通用字段本身可以充当缓存。 另外为什么ConcurrentDictionaryValue部分是object类型?

  4. 正如我之前所说,强制绑定类型和列名称(通过缓存每个类型的一个特定Action委托来执行)并不是最好的。 即使对于相同类型,您的查询也可以选择不同的列集。 最好留给数据读者来决定。

  5. 使用Expression.Convert而不是Expression.TypeAs来从object进行值类型转换。

  6. 另请注意, reader.GetOrdinal是执行数据读取器查找的快捷方式。

我会重写整个事情:

 readonly Func _converter; readonly IDataReader dataReader; private Func GetMapFunc() { var exps = new List(); var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR"); var targetExp = Expression.Variable(typeof(T)); exps.Add(Expression.Assign(targetExp, Expression.New(targetExp.Type))); //does int based lookup var indexerInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(int) }); var columnNames = Enumerable.Range(0, dataReader.FieldCount) .Select(i => new { i, name = dataReader.GetName(i) }); foreach (var column in columnNames) { var property = targetExp.Type.GetProperty( column.name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (property == null) continue; var columnNameExp = Expression.Constant(column.i); var propertyExp = Expression.MakeIndex( paramExp, indexerInfo, new[] { columnNameExp }); var convertExp = Expression.Convert(propertyExp, property.PropertyType); var bindExp = Expression.Assign( Expression.Property(targetExp, property), convertExp); exps.Add(bindExp); } exps.Add(targetExp); return Expression.Lambda>( Expression.Block(new[] { targetExp }, exps), paramExp).Compile(); } internal Converter(IDataReader dataReader) { this.dataReader = dataReader; _converter = GetMapFunc(); } internal T CreateItemFromRow() { return _converter(dataReader); }