最近在工做中的一個項目使用了一種很小衆的數據庫:Vertica。小衆的東西有不少缺點,好比它的.Net client沒有實現LINQ。而這個項目有大量的讀取數據庫的操做,以前用慣了LINQ,如今卻要回到原始社會,寫command,從DbReader裏把值讀出來,寫到對象的屬性上,想一想就各類不爽:大量的hard code的字段名稱,不光容易寫錯,還容易寫漏;賦值時,又要處理null值,又要處理各類數據類型的轉換。不光代碼寫起來麻煩,還要寫不少測試來保證這些代碼的正確。而在LINQ的幫助下,這一切都是透明的。sql
爲了能心情愉悅的完成項目,期望HP能出個LINQ provider是沒戲的了,我決定從LINQ中借鑑點東西來。因爲項目中大部分查詢都會比較複雜,把一個lambda表達式翻譯成sql的實現太複雜了,根本沒辦法在一個很短的時間內完成,而寫個sql語句,調用DbCommand對象執行下,也不是很麻煩的事,我真正在乎的是諸以下面的代碼,寫起來很麻煩。數據庫
obj.PropertyA = (int)reader.GetValue(reader.GetOrdinal("FieldA"))
而若是能夠按照下面的方式來寫,就比較容易了,不光不容易寫錯,並且還有智能提示的幫助,在代碼的輸入速度上也會有很大的提高。express
reader.BindValue(obj, o => o.PropertyA);
如今目標有了,問題是如何將其轉換爲我實際想要的功能。而要實現從DbReader裏讀取數據。將數據作適當的類型轉換(若是須要的話),而後賦值到對應的屬性上這一系列操做,須要從上面這個代碼裏獲取到以下信息:ide
所幸,這一切信息咱們都能從上面代碼中的lambda表達式中獲取出來。性能
首先,咱們須要來定義BindValue方法:測試
public static void BindValue<T>(this DbReader, T obj, Expression<Func<T, TProp>> prop) { }
咱們不能把prop參數定義成委託,而是Expression<TDelegate>類型。有了這個表達式對象,一切都變的很容易了:this
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裏的值獲取出來,而後賦值到對象的屬性上了。spa
不過因爲讀取數據庫是個頻繁的操做,若是每次都經過反射的方式去賦值,在性能上有些損失。爲了程序編寫的方便而損失性能有些不太合適,所以,咱們須要更近一步:動態的生成一個方法,而之後就能夠直接調用方法,而不用經過反射的方法了。在以往,想動態的生成一些代碼,只能經過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; } }