一、前因和存在的問題
前面我寫了一篇《使用Emit把Datatable轉換為對象集合(List<T>)》的博文,其實起源於我自己編寫的一個orm工具(見前面幾篇博文有介紹),里面已有用emit把datareader轉換為List<T>的實現方法,但是需要增加一個把DataTable轉換為List<T>的方法,在網上搜索了一些代碼,經過改造,加入緩存設計,整理了一下代碼結構,簡單測試沒有問題后就發了《使用Emit把Datatable轉換為對象集合(List<T>)》一文,但是不久以后我拿這些代碼和我以前寫的對datareader的轉換的代碼比較,發現差異較大,於是仔細對比研究了一下,發現datatable的轉換方法存在一個不足。即待轉換的datatable的架構被嚴格限制,不夠靈活。
二、分析產生原因
我們看一下利用emit動態創建一個 把datarow轉換為一個實體對象的方法的核心部分代碼
1 for (int index = 0; index < dt.Columns.Count; index++) 2 { 3 PropertyInfo propertyInfo = typeof(T).GetProperty(dt.Columns[index].ColumnName,StringComparison.CurrentCultureIgnoreCase); 4 Label endIfLabel = generator.DefineLabel(); 5 if (propertyInfo != null && propertyInfo.GetSetMethod() != null) 6 { 7 generator.Emit(OpCodes.Ldarg_0); 8 generator.Emit(OpCodes.Ldc_I4, index); 9 generator.Emit(OpCodes.Callvirt, isDBNullMethod); 10 generator.Emit(OpCodes.Brtrue, endIfLabel); 11 generator.Emit(OpCodes.Ldloc, result); 12 generator.Emit(OpCodes.Ldarg_0); 13 generator.Emit(OpCodes.Ldc_I4, index); 14 generator.Emit(OpCodes.Callvirt, getValueMethod); 15 generator.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType); 16 generator.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod()); 17 generator.MarkLabel(endIfLabel); 18 } 19 }
emit的語法比較奧澀難懂,我們不追究細節,假設datatable的列集合為{"a","b","c"},實體對象E的屬性有{a,b,c},粗略模擬生成的代碼
大致為
public E Convert(DataRow dr)
{
E e = new E();
....
if(dr[0]!=DBNull.Value)
e.a=dr[0];
if(dr[1]!=DBNull.Value)
e.b=dr[1];
if(dr[2]!=DBNull.Value)
e.c=dr[2];
return e;
}
這里有什么問題呢?就是所生成的代碼,是先遍歷datatable的列,然后檢查實體E中是否含有匹配的屬性,如果把此方法緩存起來,下一次調用時,碰巧datatable的架構有所變化,如列數只有兩列,執行到 if(dr[2]!=DBNull.Value), 就會出現“索引超出范圍”了。所以我認為應該先遍歷E的屬性,然后檢查datatable是否含匹配的列。動態生成的代碼應該大致如下
public E Convert(DataRow dr)
{
E e = new E();
if dr.Table.Columns.Contains("a") && !dr.IsNull("a")
e.a = dr["a"];
if dr.Table.Columns.Contains("b") && !dr.IsNull("b")
e.b = dr["b"];
if dr.Table.Columns.Contains("c") && !dr.IsNull("c")
e.c = dr["c"];
return e;
}
上述代碼,不管datatable如何變化,都只會轉換匹配的列而不會出錯。
三、解決的辦法和成果
所以,我后來還是把我以前的代碼加以改造實現了datatable的轉換,並把datareader和datatable兩個轉換方法合並到一個類下面。
代碼如下
using System; using System.Collections.Generic; using System.Text; using System.Data; using System.Reflection; using System.Reflection.Emit; using System.Web.Caching; using System.Web; namespace LinFramework { /// <summary> /// 實體轉換 /// </summary> public class EntityConverter { //數據類型和對應的強制轉換方法的methodinfo,供實體屬性賦值時調用 private static Dictionary<Type, MethodInfo> ConvertMethods = new Dictionary<Type, MethodInfo>() { {typeof(int),typeof(Convert).GetMethod("ToInt32",new Type[]{typeof(object)})}, {typeof(Int16),typeof(Convert).GetMethod("ToInt16",new Type[]{typeof(object)})}, {typeof(Int64),typeof(Convert).GetMethod("ToInt64",new Type[]{typeof(object)})}, {typeof(DateTime),typeof(Convert).GetMethod("ToDateTime",new Type[]{typeof(object)})}, {typeof(decimal),typeof(Convert).GetMethod("ToDecimal",new Type[]{typeof(object)})}, {typeof(Double),typeof(Convert).GetMethod("ToDouble",new Type[]{typeof(object)})}, {typeof(Boolean),typeof(Convert).GetMethod("ToBoolean",new Type[]{typeof(object)})}, {typeof(string),typeof(Convert).GetMethod("ToString",new Type[]{typeof(object)})} }; //把datarow轉換為實體的方法的委托定義 public delegate T LoadDataRow<T>(DataRow dr); //把datareader轉換為實體的方法的委托定義 public delegate T LoadDataRecord<T>(IDataRecord dr); //emit里面用到的針對datarow的元數據信息 private static readonly AssembleInfo dataRowAssembly = new AssembleInfo(typeof(DataRow)); //emit里面用到的針對datareader的元數據信息 private static readonly AssembleInfo dataRecordAssembly = new AssembleInfo(typeof(IDataRecord)); /// <summary> /// 構造轉換動態方法(核心代碼),根據assembly可處理datarow和datareader兩種轉換 /// </summary> /// <typeparam name="T">返回的實體類型</typeparam> /// <param name="assembly">待轉換數據的元數據信息</param> /// <returns>實體對象</returns> private static DynamicMethod BuildMethod<T>(AssembleInfo assembly) { DynamicMethod method = new DynamicMethod(assembly.MethodName + typeof(T).Name, MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(T), new Type[] { assembly.SourceType }, typeof(EntityContext).Module, true); ILGenerator generator = method.GetILGenerator(); LocalBuilder result = generator.DeclareLocal(typeof(T)); generator.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes)); generator.Emit(OpCodes.Stloc, result); foreach (PropertyInfo property in typeof(T).GetProperties()) { Label endIfLabel = generator.DefineLabel(); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldstr, property.Name); generator.Emit(OpCodes.Callvirt, assembly.CanSettedMethod); generator.Emit(OpCodes.Brfalse, endIfLabel); generator.Emit(OpCodes.Ldloc, result); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldstr, property.Name); generator.Emit(OpCodes.Callvirt, assembly.GetValueMethod); if (property.PropertyType.IsValueType || property.PropertyType == typeof(string)) generator.Emit(OpCodes.Call, ConvertMethods[property.PropertyType]); else generator.Emit(OpCodes.Castclass, property.PropertyType); generator.Emit(OpCodes.Callvirt, property.GetSetMethod()); generator.MarkLabel(endIfLabel); } generator.Emit(OpCodes.Ldloc, result); generator.Emit(OpCodes.Ret); return method; } /// <summary> /// 從Cache獲取委托 LoadDataRow<T>的方法實例,沒有則調用BuildMethod構造一個。 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> private static LoadDataRow<T> GetDataRowMethod<T>() { string key = dataRowAssembly.MethodName + typeof(T).Name; LoadDataRow<T> load = null; if (HttpRuntime.Cache[key] == null) { load = (LoadDataRow<T>)BuildMethod<T>(dataRowAssembly).CreateDelegate(typeof(LoadDataRow<T>)); HttpRuntime.Cache[key] = load; } else { load = HttpRuntime.Cache[key] as LoadDataRow<T>; } return load; } /// <summary> /// 從Cache獲取委托 LoadDataRecord<T>的方法實例,沒有則調用BuildMethod構造一個。 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> private static LoadDataRecord<T> GetDataRecordMethod<T>() { string key = dataRecordAssembly.MethodName + typeof(T).Name; LoadDataRecord<T> load = null; if (HttpRuntime.Cache[key] == null) { load = (LoadDataRecord<T>)BuildMethod<T>(dataRecordAssembly).CreateDelegate(typeof(LoadDataRecord<T>)); HttpRuntime.Cache[key] = load; } else { load = HttpRuntime.Cache[key] as LoadDataRecord<T>; } return load; } public static T ToItem<T>(DataRow dr) { LoadDataRow<T> load = GetDataRowMethod<T>(); return load(dr); } public static List<T> ToList<T>(DataTable dt) { List<T> list = new List<T>(); if (dt == null || dt.Rows.Count == 0) { return list; } LoadDataRow<T> load = GetDataRowMethod<T>(); foreach (DataRow dr in dt.Rows) { list.Add(load(dr)); } return list; } public static List<T> ToList<T>(IDataReader dr) { List<T> list = new List<T>(); LoadDataRecord<T> load = GetDataRecordMethod<T>(); while (dr.Read()) { list.Add(load(dr)); } return list; } } /// <summary> /// emit所需要的元數據信息 /// </summary> public class AssembleInfo { public AssembleInfo(Type type) { SourceType = type; MethodName = "Convert" + type.Name + "To"; CanSettedMethod = this.GetType().GetMethod("CanSetted", new Type[] { type, typeof(string) }); GetValueMethod = type.GetMethod("get_Item", new Type[] { typeof(string) }); } public string MethodName; public Type SourceType; public MethodInfo CanSettedMethod; public MethodInfo GetValueMethod; /// <summary> /// 判斷datareader是否存在某字段並且值不為空 /// </summary> /// <param name="dr">當前的datareader</param> /// <param name="name">字段名</param> /// <returns></returns> public static bool CanSetted(IDataRecord dr, string name) { bool result = false; for (int i = 0; i < dr.FieldCount; i++) { if (dr.GetName(i).Equals(name, StringComparison.CurrentCultureIgnoreCase) && !dr[i].Equals(DBNull.Value)) { result = true; break; } } return result; } /// <summary> /// 判斷datarow所在的datatable是否存在某列並且值不為空 /// </summary> /// <param name="dr">當前datarow</param> /// <param name="name">字段名</param> /// <returns></returns> public static bool CanSetted(DataRow dr, string name) { return dr.Table.Columns.Contains(name) && !dr.IsNull(name); } } }
四、如何使用
使用起來就很簡單了
List<E> list = EntityConverter.ToList<E>(dr);
...
List<E> list = EntityConverter.ToList<E>(dt);
當然,利用泛型還可以再進一步封裝,
public List<TEntity> QueryBySQL<TEntity>(string sql) { using (IDataReader dr = sqlCommand.ExecuteReader(sql) { return EntityConverter.ToList<TEntity>(dr); } }
實際上實現了把查詢結果轉換為實體對象,已經具備了orm的核心功能了。因此,如果你有開發自己的orm平台的想法,不妨關注一下,歡迎參與共同研究!如果覺得此文對你有所幫助,請高抬貴鼠,點一下推薦!