也许,我们没法改变项目的决策,但是,我们可以自己制造工具。
这里先忽略掉那些麻烦的sql,调用那个存储过程之类的事情,假设我们已经通过各种手段(不管你是SqlHelper的拥护者还是enterprise library的支持者,或者是自己动手丰衣足食的DIY派),得到了一个IDataReader实例(什么,你不知道IDataReader接口?去msdn看看吧),并且把msdn上的说明:
提供一种方法来读取一个或多个通过在数据源执行命令所获得的只进结果集流
简化成:
提供一种方法来读取一个通过在数据源执行命令所获得的只进结果集流
也就是说,忽略掉一个存储过程返回多个结果集的情况下,我们可以直接把这个IDataReader实例中承载的数据转换为一个List<T>的形式。
读到这里你可能会觉得这样的功能用一个反射就可以搞定了,何必写这么一篇水文?
不过,这里用的不是常规反射。什么意思?反射还有常规和非常规之分吗?
常规的反射是直接使用GetProperty和SetValue就可以实现这样的功能了,但是常规反射的问题是反射的效率问题,这样的代码效率是手写代码的1/1000,也就是说,虽然你写的很轻松,但是对CLR来说,这个代码执行的非常累。
我这里要用的是反射中的Emit技术,动态拼装IL,获得一个高性能转换器,其执行效率至少是手写代码1/2,也就是说,使用这个代码的目的是你写的轻松,CLR执行的也轻松。
不过,在这个之前,需要先定义数据实体,这个又是每个人的习惯都不一样的东西,我个人是习惯于利用Attribute来表明哪个属性是哪一列,不过考虑到便捷性,会用一个类级的Attribute表明这个类默认为是数据列,或者表明不是默认为是数据列:
2 internal sealed class DbResultAttribute : Attribute
3 {
4
5 public DbResultAttribute()
6 : this ( true ) { }
7
8 public DbResultAttribute( bool defaultAsDbColumn)
9 {
10 DefaultAsDbColumn = defaultAsDbColumn;
11 }
12
13 public bool DefaultAsDbColumn { get ; private set ; }
14
15 }
16
而对于普通的属性,再给与一次修改列名和排除在数据列之外的机会:
2 internal sealed class DbColumnAttribute : Attribute
3 {
4
5 public DbColumnAttribute() { }
6
7 public string ColumnName { get ; set ; }
8
9 public bool Ignore { get ; set ; }
10
11 }
12
这样,我们就可以很简单的定义一个数据实体了:
2 public class MyClass
3 {
4 public int IntValue { get ; set ; }
5 public string StringValue { get ; set ; }
6 [DbColumn(ColumnName = " DecimalValue " )]
7 public decimal ? NullableDecimalAndDifferentName { get ; set ; }
8 public MyEnum EnumIsAlsoSupportted { get ; set ; }
9 public MyEnum ? NullableEnumIsAlsoSupportted { get ; set ; }
10 [DbColumn(Ignore = true )]
11 public object NonDbValue { get ; set ; }
12 }
13
14 public enum MyEnum
15 {
16 X,
17 Y,
18 Z,
19 }
20
定义简单吧,不过,可以看出来实现部分也是异常的复杂,除了标准的int,string,decimal类型,还要支持可空类型,以及枚举类型,甚至是可空枚举类型。。。不过,如果数据库类型和定义的类型(枚举看它的基础类型)不匹配就会直接抛出异常,这点要注意一下。
千里之行始于足下,先做最简单的,搭建一个简单的环境,并且读取这个实体类的信息吧:
2 {
3
4 #region Public Static Methods
5
6 public static List < T > Select < T > ( this IDataReader reader)
7 where T : class , new ()
8 {
9 if (reader == null )
10 throw new ArgumentNullException( " reader " );
11 return EntityConverter < T > .Select(reader);
12 }
13
14 #endregion
15
16 #region Class: EntityConverter<T>
17
18 private class EntityConverter < T >
19 where T : class , new ()
20 {
21
22 #region Struct: DbColumnInfo
23
24 private struct DbColumnInfo
25 {
26 public readonly string PropertyName;
27 public readonly string ColumnName;
28 public readonly Type Type;
29 public readonly MethodInfo SetMethod;
30
31 public DbColumnInfo(PropertyInfo prop, DbColumnAttribute attr)
32 {
33 PropertyName = prop.Name;
34 ColumnName = attr.ColumnName ?? prop.Name;
35 Type = prop.PropertyType;
36 SetMethod = prop.GetSetMethod( false );
37 }
38 }
39
40 #endregion
41
42 #region Fields
43 private static Converter < IDataReader, List < T >> batchDataLoader;
44 #endregion
45
46 #region Properties
47
48 private static Converter < IDataReader, List < T >> BatchDataLoader
49 {
50 get
51 {
52 if (batchDataLoader == null )
53 batchDataLoader = CreateBatchDataLoader( new List < DbColumnInfo > (GetProperties()));
54 return batchDataLoader;
55 }
56 }
57
58 #endregion
59
60 #region Init Methods
61
62 private static IEnumerable < DbColumnInfo > GetProperties()
63 {
64 DbResultAttribute dbResult = Attribute.GetCustomAttribute( typeof (T), typeof (DbResultAttribute), true ) as DbResultAttribute;
65 foreach (var prop in typeof (T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
66 {
67 if (prop.GetIndexParameters().Length > 0 )
68 continue ;
69 var setMethod = prop.GetSetMethod( false );
70 if (setMethod == null )
71 continue ;
72 var attr = Attribute.GetCustomAttribute(prop, typeof (DbColumnAttribute), true ) as DbColumnAttribute;
73 if (dbResult != null && dbResult.DefaultAsDbColumn)
74 if (attr != null && attr.Ignore)
75 continue ;
76 else
77 attr = attr ?? new DbColumnAttribute();
78 else
79 if (attr == null || attr.Ignore)
80 continue ;
81 yield return new DbColumnInfo(prop, attr);
82 }
83 }
84
85 #endregion
86
87 }
88
这里,我们先建立一个DataReaderExtensions类,这个类的功能只有一个,给IDataReader添加一个Select<T>的方法,然后把思想转嫁给一个内部类EntityConverter<T>的Select方法(这个方法在那里?还没贴出来哪,别急),为什么用这么一个内部泛型类那?建议参考老赵的这篇文章,主要是出于性能方面的考虑。
然后我们在通过GetProperties方法,获得关于这个实体类型的信息,放到一个叫DbColumnInfo的结构体里面(为什么用结构体哪?还是性能方面的考虑,减少垃圾对象,让GC过的轻松点)。这里的GetProperties方法就是一个彻头彻尾的普通反射,那么性能自然也就是手写代码的1/1000,不过别急,这个段代码对每个T而言仅仅跑1次,也就是说,某个类型Select过了,那么下一次,这个类型就不需要跑这个GetProperties方法了。
不过,有一点不要误解,即使缓存了PropertyInfo,那也仅仅是减少了发现成员的代价,PropertyInfo.SetValue的代价依然是反射的标准代价,执行效率是1/1000。
也就是说,之后的任务才是本文的重点内容,消除掉PropertyInfo.SetValue的代价,让我们的反射跑得飞起来。
不过在这个之前,先把我们需要的一些IDataReader里面的方法先反射出来,做好缓存(放在DataReaderExtensions类里面,避免为每个T反射一次):