使用表示式目錄樹實現SqlDataReader到實體的對映
SqlDataReader對映實體,是ORM的基礎功能,常見的實現方式有反射、表示式目錄樹和emit,這裡要說的就是用表示式目錄樹生成實體的方法。
先分析下思路:
假設有個資料實體類,Student
public class Student { public int Id { get; set; } public string Name { get; set; } public int ClazzId { get; set; } }
獲取到SqlDataReader,手擼程式碼的話,我們可能是這樣做的
public Student ConvertToStudent(SqlDataReader sdr) {var entity = new Student { Id = sdr.GetInt32(0), Name = sdr.GetString(1), ClazzId = sdr.GetInt32(2) }; return entity; }
無疑,這種效率是最高的,原因是:一、直接通過索引獲取Reader內的資料;二、直接為屬性賦值,而無需通過PropertyInfo.SetValue,既避開了反射,又無需對資料型別進行轉換,減少了裝箱拆箱的操作。
但是,我們需要的是一個通用方法,上面的寫法只能針對具體型別,所以我們真正需要的是這樣一個類似的泛型版本
public T ConvertToEntity<T>(SqlDataReader reader) where T : new() { var t = new T(); t.a = reader.GetInt32(0); t.b = reader.GetString(1); return t; }
問題來了,我們並不知道實體的具體定義,所以上面的程式碼根本無法實現。所以現在需要解決兩個問題:
1、要獲取到T的所有屬性,這裡假設實體是個POCO模型
2、為T專門構建一個方法,實現類似ConvertToStudent()那樣的功能
第一個問題很簡單,反射,要動態獲取實體的屬性,這一步是不可避免的,通用做法就是先做一次反射,然後把實體的PropertyInfo快取起來,這樣只要反射一次,這點效能損耗還是可以承受的
第二個問題比較麻煩,動態生成方法,在我的知識體系內是沒有辦法的,但恰巧知道一個動態生成委託的方法,那就是表示式目錄樹,我們知道,委託delegate 就是對方法的封裝,動態生成委託,正好可以解決眼下的問題
現在將ConvertToStudent改造成委託,這段程式碼就是我們構造表示式目錄樹的模板
private Func<SqlDataReader, Student> ConvertToStudentFunc = (reader) => { var entity = new Student(); entity.Id = reader.GetInt32(0); entity.Name = reader.GetString(1); entity.ClazzId = reader.GetInt32(2); return entity; };
因為沒有找到系統的表示式目錄樹的資料,所以對語法不甚瞭解,下面的程式碼是結合網上的資料一點一點摸索出來的,具體不解釋了,怕說錯了誤人子弟,直接看程式碼吧:
private static Func<SqlDataReader, T> Converter<T>(IDataReader reader) { var sdrParameter = Expression.Parameter(typeof(SqlDataReader), "sdr"); var memberBindings = new List<MemberBinding>(); var properties = typeof(T).GetProperties().Where(p => p.CanWrite); for (var i = 0; i < reader.FieldCount; i++) { var fieldName = reader.GetName(i); var property = properties.SingleOrDefault(p => p.Name == fieldName && p.PropertyType == reader.GetFieldType(i)); if (property == null) continue; var methodName = "GetValue"; if (property.PropertyType == typeof(string)) { methodName = "GetString"; } else if (property.PropertyType == typeof(int)) { methodName = "GetInt32"; } else if (property.PropertyType == typeof(DateTime)) { methodName = "GetDateTime"; } else if (property.PropertyType == typeof(decimal)) { methodName = "GetDecimal"; } else if (property.PropertyType == typeof(Guid)) { methodName = "GetGuid"; } else if (property.PropertyType == typeof(bool)) { methodName = "GetBoolean"; } else { continue; } var methodCall = Expression.Call(sdrParameter, typeof(SqlDataReader).GetMethod(methodName) ?? throw new InvalidOperationException(), Expression.Constant(i)); memberBindings.Add(Expression.Bind(property, methodCall)); } var initExpression = Expression.MemberInit(Expression.New(typeof(T)), memberBindings); return Expression.Lambda<Func<SqlDataReader, T>>(initExpression, sdrParameter).Compile(); }
程式碼並不嚴謹,很多東西沒做,只是個框框,理解原理用的,下面是使用方法:
public static List<T> Fetch<T>(int count) { var list = new List<T>(); Func<SqlDataReader, T> func = null; var topStr = count <= 0 ? "" : $"TOP {count}"; using (var conn = new SqlConnection(ConfigurationManager.AppSettings["DbConnection"])) { using (var command = new SqlCommand($"SELECT {topStr} * FROM {typeof(T).Name}", conn)) { conn.Open(); using (var reader = command.ExecuteReader()) { while (reader.Read()) { if (func == null) { func = ExpressionShow.ConvertEntity<T>(reader); list.Add(func(reader)); } else { list.Add(func(reader)); } } reader.Close(); } } } return list; }
到這裡,基本功能就完成了。