最近在工作中的一个项目使用了一种很小众的数据库:Vertica。小众的东西有很多缺点,比如它的.Net client没有实现LINQ。而这个项目有大量的读取数据库的操作,之前用惯了LINQ,现在却要回到原始社会,写command,从DbReader里把值读出来,写到对象的属性上,想想就各种不爽:大量的hard code的字段名称,不光容易写错,还容易写漏;赋值时,又要处理null值,又要处理各种数据类型的转换。不光代码写起来麻烦,还要写很多测试来保证这些代码的正确。而在LINQ的帮助下,这一切都是透明的。
为了能心情愉悦的完成项目,指望HP能出个LINQ provider是没戏的了,我决定从LINQ中借鉴点东西来。由于项目中大部分查询都会比较复杂,把一个lambda表达式翻译成sql的实现太复杂了,根本没办法在一个很短的时间内完成,而写个sql语句,调用DbCommand对象执行下,也不是很麻烦的事,我真正在意的是诸如下面的代码,写起来很麻烦。
obj.PropertyA = (int)reader.GetValue(reader.GetOrdinal("FieldA"))
而如果可以按照下面的方式来写,就比较容易了,不光不容易写错,而且还有智能提示的帮助,在代码的输入速度上也会有很大的提升。
reader.BindValue(obj, o => o.PropertyA);
现在目标有了,问题是如何将其转换为我实际想要的功能。而要实现从DbReader里读取数据。将数据做适当的类型转换(如果需要的话),然后赋值到对应的属性上这一系列操作,需要从上面这个代码里获取到如下信息:
- 属性名称
- 属性类型
- 数据库字段名称
- 数据库字段类型
- 属性的setValue方法
所幸,这一切信息我们都能从上面代码中的lambda表达式中获取出来。
首先,我们需要来定义BindValue方法:
public static void BindValue<T>(this DbReader, T obj, Expression<Func<T, TProp>> prop) { }
我们不能把prop参数定义成委托,而是Expression<TDelegate>类型。有了这个表达式对象,一切都变的很容易了:
var memberExp = prop.Body as MemberExpression; var memberInfo = memberExp.Member; var fieldName = memberInfo.Name; var typeName = memberInfo.DeclaringType.FullName; var propType = (memberInfo as PropertyInfo).PropertyType;
通过上面的代码,我们知道了字段的名称,字段的类型,以及字段的MemberInfo,有了这些,我们就可以通过反射将DbReader里的值获取出来,然后赋值到对象的属性上了。
不过由于读取数据库是个频繁的操作,如果每次都通过反射的方式去赋值,在性能上有些损失。为了程序编写的方便而损失性能有些不太合适,因此,我们需要更近一步:动态的生成一个方法,而以后就可以直接调用方法,而不用通过反射的方法了。在以往,想动态的生成一些代码,只能通过System.Reflection.Emit命名空间下的类来实现,十分困难;而有了Expression这一利器以后,做这件事就容易多了。我们要做的事就是将obj.PropertyA = (int)reader.GetValue(reader.GetOrdinal("FieldA"))这一操作分解,将各个部分对应到相应的Expression对象上,然后将这些对象组合起来,得到一个LambdaExpression对象,最后编译一下,得到我们最终想要的方法,一个代理对象。最终效果如下:
var company = new CompanyEntity(); company.BindTo(reader) .With(item => item.CompanyId) .With(item => item.Name) .With(item => item.CountryId) .With(item => item.Address);
下面给出完整的实现 (由于项目是3.5的,如果是在4.0下,实现则会更简单些):
/// <summary> /// this class provides some functions about the data from the database, like convert the data to target's type. /// </summary> public static class DbDataHelper { readonly static object lockObj = new object(); private static Dictionary<string, Delegate> propBinders = new Dictionary<string, Delegate>(); /// <summary> /// bind the data from DbReader to the property in the expression. /// </summary> /// <typeparam name="TObj"></typeparam> /// <typeparam name="TProp"></typeparam> /// <param name="obj"></param> /// <param name="prop">to specify the property which you want to bind</param> /// <param name="reader"></param> /// <param name="colName"></param> /// <param name="customerConv"></param> private static void BindValue<TObj, TProp>( this TObj obj, Expression<Func<TObj, TProp>> prop, DbDataReader reader, string colName, Func<object, TProp> customerConv) { if (prop.NodeType != ExpressionType.Lambda) throw new ArgumentException("prop"); if (prop.Body.NodeType != ExpressionType.MemberAccess) throw new ArgumentException("prop"); var memberExp = prop.Body as MemberExpression; var memberInfo = memberExp.Member; var fieldName = memberInfo.Name; var typeName = memberInfo.DeclaringType.FullName; var propType = (memberInfo as PropertyInfo).PropertyType; if (!propBinders.ContainsKey(typeName + "-" + fieldName)) { lock (lockObj) { if (!propBinders.ContainsKey(typeName + "-" + fieldName)) { if (colName == null) { colName = fieldName; // if colName != fieldName, get colName from DbColumnAttribute var colAttrs = memberInfo.GetCustomAttributes(typeof(DbColumnAttribute), false); if (colAttrs != null && colAttrs.Length == 1) { colName = (colAttrs[0] as DbColumnAttribute).Name; } } var paraObjExp = Expression.Parameter(typeof(TObj), "obj"); var paraDbReaderExp = Expression.Parameter(typeof(DbDataReader), "reader"); var fldIdx = reader.GetOrdinal(colName); var constFldIdxExp = Expression.Constant(fldIdx); var fldType = reader.GetFieldType(fldIdx); var getValueCallExp = Expression.Call( Expression.Convert(paraDbReaderExp, reader.GetType()), reader.GetType().GetMethod("GetValue"), constFldIdxExp); MethodCallExpression convValueExp = null; if (customerConv != null) { convValueExp = Expression.Call( Expression.Constant(customerConv.Target), customerConv.Method, getValueCallExp); } else { convValueExp = Expression.Call( typeof(DbDataHelper).GetMethod("GetValue", BindingFlags.NonPublic | BindingFlags.Static), getValueCallExp, Expression.Constant(fldType), Expression.Constant(propType)); } var setValueExp = Expression.Call( Expression.Convert(paraObjExp, typeof(TObj)), (memberInfo as PropertyInfo).GetSetMethod(), Expression.Convert(convValueExp, propType)); var lambda = Expression.Lambda<Action<TObj, DbDataReader>>(setValueExp, paraObjExp, paraDbReaderExp); var action = lambda.Compile(); propBinders.Add(typeName + "-" + fieldName, action); } } } var setAction = propBinders[typeName + "-" + fieldName] as Action<TObj, DbDataReader>; setAction(obj, reader); } /// <summary> /// type convert help function /// </summary> /// <param name="input"></param> /// <param name="inputType"></param> /// <param name="outputType"></param> /// <returns></returns> private static object GetValue(object input, Type inputType, Type outputType) { if (inputType == outputType) if (input == DBNull.Value) return null; else return input; if (input == DBNull.Value && (outputType.IsGenericType && outputType.GetGenericTypeDefinition() == typeof(Nullable<>) || outputType == typeof(string))) return null; if (outputType.IsGenericType && typeof(Nullable<>) == outputType.GetGenericTypeDefinition()) { var argType = outputType.GetGenericArguments()[0]; if (argType == inputType) { return input; } else { var val = Convert.ChangeType(input, argType); return val; } } return Convert.ChangeType(input, outputType); } /// <summary> /// bind the data from DbDataReader to the property in the expression /// </summary> /// <typeparam name="TObj"></typeparam> /// <typeparam name="TProp"></typeparam> /// <param name="binder"></param> /// <param name="prop"></param> /// <returns></returns> public static DataBinder<TObj> With<TObj, TProp>( this DataBinder<TObj> binder, Expression<Func<TObj, TProp>> prop) { binder.Obj.BindValue(prop, binder.Reader, null, null); return binder; } /// <summary> /// bind the data from DbDataReader to the property in the expression /// </summary> /// <typeparam name="TObj"></typeparam> /// <typeparam name="TProp"></typeparam> /// <param name="binder"></param> /// <param name="prop"></param> /// <param name="colName"></param> /// <param name="custConv"></param> /// <returns></returns> public static DataBinder<TObj> With<TObj, TProp>( this DataBinder<TObj> binder, Expression<Func<TObj, TProp>> prop, string colName, Func<object, TProp> custConv) { binder.Obj.BindValue(prop, binder.Reader, colName, custConv); return binder; } /// <summary> /// create a binder between obj and DbDataReader /// </summary> /// <typeparam name="TObj"></typeparam> /// <param name="obj"></param> /// <param name="reader"></param> /// <returns></returns> public static DataBinder<TObj> BindTo<TObj>(this TObj obj, DbDataReader reader) { return new DataBinder<TObj>(obj, reader); } /// <summary> /// a container to keep the object and DbDataReader for BindTo function /// </summary> /// <typeparam name="TObj"></typeparam> public class DataBinder<TObj> { /// <summary> /// /// </summary> public TObj Obj { get; private set; } /// <summary> /// /// </summary> public DbDataReader Reader { get; private set; } internal DataBinder(TObj obj, DbDataReader reader) { this.Obj = obj; this.Reader = reader; } } } /// <summary> /// if the property name is not equals to the name in the database, use it to specify the name in database /// </summary> public class DbColumnAttribute : Attribute { /// <summary> /// /// </summary> /// <param name="name"></param> public DbColumnAttribute(string name) { this.Name = name; } /// <summary> /// /// </summary> public string Name { get; set; } }