相信EF大家都不陌生了,因為數據庫表跟程序實體是一一對應的原因,我們能夠通過lambda這種函數式的編程方式進行操作數據庫,感覺非常清晰明了。與我們直接寫SQL相比,lambda是強類型,擁有更好的擴展性,伸縮性,而且編程更加的方便,快捷。。下面我們就基於Expression和lambda來與大家構建一個屬於自己的ORM框架。
思路的話很簡單,就是將lambda轉換成我們對應的數據庫所需的查詢條件,然后執行查詢,再將結果以反射的方式封裝成List<T>返回出去。
Expression
大家使用EF的時候多多少少會留意到有Expression這個東西。特別是查詢時會看到要你傳入Expression<Func<T,bool>>這樣類型的參數,它又和Func<T,bool>有什么比同呢?
Expression<Func<T,bool>>是表達式樹,我們可以通過它來分析我們的委托中的函數。當調用Compile方法后就會變成委托,才能執行。
Func<T,bool>只是一個普通的委托。
例如我們現在有個實體類Staff
public class Staff { public string Name { get; set; } public int Age { get; set; } public string Code { get; set; } public DateTime? Birthday { get; set; } public bool Deletion { get; set; } }
我們還有一個這樣的方法
class Program { static void Main(string[] args) { FindAs<Staff>(x => x.Code == "張三" && x.Name.Contains("張")); } public static List<T> FindAs<T>(Expression<Func<T, bool>> func) { //將func轉換成對應數據庫的查詢條件,然后執行查詢 return null;//將結果返回 } }
我們希望通過 FindAs<Staff>(x => x.Age <50 && x.Name.Contains("張")); 就能查詢出Staff表中Age<50並且Name包含有“張”字的人的信息。而生成的sql語句應該是select * from staff where Age<50 and Name like '%張%'。現在我們就來分析下這個func
從上面的圖我們可以看到當前的Expression是一個lambda表達式,我們點開它的body看看。
我們可以看到body里分為左邊和右邊,還有NodeType。和我們的lambda對比下看看'x => x.Code =="張三" && x.Name.Contains("張")'是不是找到點靈感了?我們再繼續把左邊和右邊拆開看看。
可以看到我們需要的信息都有了,看來轉換成SQL已經不是什么難事了,動手開搞了。
class Program { static void Main(string[] args) { FindAs<Staff>(x => x.Code == "張三" && x.Name.Contains("張")); FindAs<Staff>(x => x.Age <= 12 && x.Name.Contains("張")); Console.ReadKey(); } public static List<T> FindAs<T>(Expression<Func<T, bool>> func) { BinaryExpression Binary = func.Body as BinaryExpression; string left = ResovleFunc(Binary.Left); string right = ResovleLinqToObject(Binary.Right); string oper = GetOperator(Binary.NodeType); string sql = string.Format("select * from {0} where {1}", typeof(T).Name, left + oper + right); Console.WriteLine(sql); return null;//將結果返回 } //解析一般的條件,例如x=>x.name==xxxx x.age==xxx public static string ResovleFunc(Expression express) { var inner = express as BinaryExpression; string Name = (inner.Left as MemberExpression).Member.Name; object Value = (inner.Right as ConstantExpression).Value; var Operator = GetOperator(inner.NodeType); string Result = string.Format("({0} {1} '{2}')", Name, Operator, Value); return Result; } //解析linq to object這類擴展方法 public static string ResovleLinqToObject(Expression expression) { var MethodCall = expression as MethodCallExpression; var MethodName = MethodCall.Method.Name; if (MethodName == "Contains") { object Temp_Vale = (MethodCall.Arguments[0] as ConstantExpression).Value; string Value = string.Format("%{0}%", Temp_Vale); string Name = (MethodCall.Object as MemberExpression).Member.Name; string Result = string.Format("{0} like '{1}'", Name, Value); return Result; } return null; } public static string GetOperator(ExpressionType expressiontype) { switch (expressiontype) { case ExpressionType.And: return "and"; case ExpressionType.AndAlso: return "and"; case ExpressionType.Or: return "or"; case ExpressionType.OrElse: return "or"; case ExpressionType.Equal: return "="; case ExpressionType.NotEqual: return "<>"; case ExpressionType.LessThan: return "<"; case ExpressionType.LessThanOrEqual: return "<="; case ExpressionType.GreaterThan: return ">"; case ExpressionType.GreaterThanOrEqual: return ">="; default: throw new Exception(string.Format("不支持{0}此種運算符查找!" + expressiontype)); } } }
已經初步的達到了我們的目的了,但是我們的查詢條件不可能固定是2個,有可能是N個,這時左邊和右邊又要繼續再分下去,直到無法再分(想到遞歸了吧?)。而且我們還需要將查詢條件參數化。而且我們的條件刪除時也會用到。所以我們應該把它獨立出來。傳入一個lambda,生成sql where部分的語句,生成sqlparameter[]。這才是關鍵。。於是我們來構建一個解析Expresstion的類。。下面我就直接給出我自己寫的實現代碼了。。
public class ResolveExpress { public Dictionary<string, object> Argument; public string SqlWhere; public SqlParameter[] Paras; /// <summary> /// 解析lamdba,生成Sql查詢條件 /// </summary> /// <param name="expression"></param> /// <returns></returns> public void ResolveExpression(Expression expression) { this.Argument = new Dictionary<string, object>(); this.SqlWhere = Resolve(expression); this.Paras = Argument.Select(x => new SqlParameter(x.Key, x.Value)).ToArray(); } private string Resolve(Expression expression) { if (expression is LambdaExpression) { LambdaExpression lambda = expression as LambdaExpression; expression = lambda.Body; return Resolve(expression); } if (expression is BinaryExpression) { BinaryExpression binary = expression as BinaryExpression; if (binary.Left is MemberExpression && binary.Right is ConstantExpression)//解析x=>x.Name=="123" x.Age==123這類 return ResolveFunc(binary.Left, binary.Right, binary.NodeType); if (binary.Left is MethodCallExpression && binary.Right is ConstantExpression)//解析x=>x.Name.Contains("xxx")==false這類的 { object value = (binary.Right as ConstantExpression).Value; return ResolveLinqToObject(binary.Left, value, binary.NodeType); } if (binary.Left is MemberExpression && binary.Right is MemberExpression)//解析x=>x.Date==DateTime.Now這種 { LambdaExpression lambda = Expression.Lambda(binary.Right); Delegate fn = lambda.Compile(); ConstantExpression value = Expression.Constant(fn.DynamicInvoke(null), binary.Right.Type); return ResolveFunc(binary.Left, value, binary.NodeType); } } if (expression is UnaryExpression) { UnaryExpression unary = expression as UnaryExpression; if (unary.Operand is MethodCallExpression)//解析!x=>x.Name.Contains("xxx")或!array.Contains(x.Name)這類 return ResolveLinqToObject(unary.Operand, false); if (unary.Operand is MemberExpression && unary.NodeType == ExpressionType.Not)//解析x=>!x.isDeletion這樣的 { ConstantExpression constant = Expression.Constant(false); return ResolveFunc(unary.Operand, constant, ExpressionType.Equal); } } if (expression is MemberExpression && expression.NodeType == ExpressionType.MemberAccess)//解析x=>x.isDeletion這樣的 { MemberExpression member = expression as MemberExpression; ConstantExpression constant = Expression.Constant(true); return ResolveFunc(member, constant, ExpressionType.Equal); } if (expression is MethodCallExpression)//x=>x.Name.Contains("xxx")或array.Contains(x.Name)這類 { MethodCallExpression methodcall = expression as MethodCallExpression; return ResolveLinqToObject(methodcall, true); } var body = expression as BinaryExpression; if (body == null) throw new Exception("無法解析" + expression); var Operator = GetOperator(body.NodeType); var Left = Resolve(body.Left); var Right = Resolve(body.Right); string Result = string.Format("({0} {1} {2})", Left, Operator, Right); return Result; } /// <summary> /// 根據條件生成對應的sql查詢操作符 /// </summary> /// <param name="expressiontype"></param> /// <returns></returns> private string GetOperator(ExpressionType expressiontype) { switch (expressiontype) { case ExpressionType.And: return "and"; case ExpressionType.AndAlso: return "and"; case ExpressionType.Or: return "or"; case ExpressionType.OrElse: return "or"; case ExpressionType.Equal: return "="; case ExpressionType.NotEqual: return "<>"; case ExpressionType.LessThan: return "<"; case ExpressionType.LessThanOrEqual: return "<="; case ExpressionType.GreaterThan: return ">"; case ExpressionType.GreaterThanOrEqual: return ">="; default: throw new Exception(string.Format("不支持{0}此種運算符查找!" + expressiontype)); } } private string ResolveFunc(Expression left, Expression right, ExpressionType expressiontype) { var Name = (left as MemberExpression).Member.Name; var Value = (right as ConstantExpression).Value; var Operator = GetOperator(expressiontype); string CompName = SetArgument(Name, Value.ToString()); string Result = string.Format("({0} {1} {2})", Name, Operator, CompName); return Result; } private string ResolveLinqToObject(Expression expression, object value, ExpressionType? expressiontype = null) { var MethodCall = expression as MethodCallExpression; var MethodName = MethodCall.Method.Name; switch (MethodName)//這里其實還可以改成反射調用,不用寫switch { case "Contains": if (MethodCall.Object != null) return Like(MethodCall); return In(MethodCall, value); case "Count": return Len(MethodCall, value, expressiontype.Value); case "LongCount": return Len(MethodCall, value, expressiontype.Value); default: throw new Exception(string.Format("不支持{0}方法的查找!", MethodName)); } } private string SetArgument(string name, string value) { name = "@" + name; string temp = name; while (Argument.ContainsKey(temp)) { int code = Guid.NewGuid().GetHashCode(); if (code < 0) code *= -1; temp = name + code; } Argument[temp] = value; return temp; } private string In(MethodCallExpression expression, object isTrue) { var Argument1 = (expression.Arguments[0] as MemberExpression).Expression as ConstantExpression; var Argument2 = expression.Arguments[1] as MemberExpression; var Field_Array = Argument1.Value.GetType().GetFields().First(); object[] Array = Field_Array.GetValue(Argument1.Value) as object[]; List<string> SetInPara = new List<string>(); for (int i = 0; i < Array.Length; i++) { string Name_para = "InParameter" + i; string Value = Array[i].ToString(); string Key = SetArgument(Name_para, Value); SetInPara.Add(Key); } string Name = Argument2.Member.Name; string Operator = Convert.ToBoolean(isTrue) ? "in" : " not in"; string CompName = string.Join(",", SetInPara); string Result = string.Format("{0} {1} ({2})", Name, Operator, CompName); return Result; } private string Like(MethodCallExpression expression) { object Temp_Vale = (expression.Arguments[0] as ConstantExpression).Value; string Value = string.Format("%{0}%", Temp_Vale); string Name = (expression.Object as MemberExpression).Member.Name; string CompName = SetArgument(Name, Value); string Result = string.Format("{0} like {1}", Name, CompName); return Result; } private string Len(MethodCallExpression expression, object value, ExpressionType expressiontype) { object Name = (expression.Arguments[0] as MemberExpression).Member.Name; string Operator = GetOperator(expressiontype); string CompName = SetArgument(Name.ToString(), value.ToString()); string Result = string.Format("len({0}){1}{2}", Name, Operator, CompName); return Result; } }
static void Main(string[] args) { string[] Names = { "Andy", "Amy", "Mike" }; Expression<Func<Staff, bool>> func = x => (!Names.Contains(x.Name) && (x.Name == "A" || x.Name.Count() > 5)); ResolveExpress resolve = new ResolveExpress(); resolve.ResolveExpression(func); Console.WriteLine(resolve.SqlWhere); foreach (var item in resolve.Paras) { Console.WriteLine(item.ParameterName + ":" + item.Value); } Console.ReadKey(); }
結果:
這里有幾個重要的東西要給大家講下
string[] Names={"Andy","Amy","Mike"};
1.)x => Names.Contains(x.Name);
2.)x => Names.Contains(x.Name)==false;
3.)x => !Names.Contains(x.Name);
這3種在Expression中的表現都不一樣
1的話會看成是一個靜態方法(MethodCallExpression)
2的話會看成是一個2元運算(BinaryExpression)
3的話會看成是一個1元運算(UnaryExpression)
所以我們都要支持,處理都有所不同。
還有
x=>x.Birthday<DateTime.Now;
string name="123";
x=>x.Name==name;
和
x=>x.Name=="123"
的處理也不一樣。大家可以在例子中細細的看看。
這樣的構造使得我們切換數據庫變得非常簡單。因為我們程序中的查詢都是基於lambda。換了數據庫只要添加一個對應的lamdba轉數據庫查詢條件的實現就可以了。寫得夠多了。至於數據層怎么封裝,到了這一步它已經變得沒什么難度了。希望大家能從文章中有所啟發和幫助
下篇文章將結合解析Expression和IQueryable<T>來實現延遲加載
補充點東西
IEnumerable和IQueryable有什么不同?
為什么EF查詢后返回的是IQueryable<T>而不是IEnumerable<T>。我們對着IQueryable<T>F12去看看。
啥都沒,就繼承了幾個接口。鼠標移到IQueryable上。F12
IQueryable中有3個屬性。
Type是類型。
Expresstion是表達式。
那IQueryProvider是什么?
再看看IQueryProvider接口的定義。
CreateQuery是創建查詢條件
Execute是執行查詢(通常在GetEnumerator()中調用)
當我們IQueryable<T>.Where(x=>x.xxx=="123")時。其實Where方法內部應該是調用了IQueryable接口中的IQueryProvider屬性的CreateQuery(Expresstion expresstion)方法,然后將方法的返回值又返回出來。
而參數(Expresstion )呢?則是IQueryable<T>.Where(x=>x.xxx=="123")2部分的Expresstion相並。所以IQueryable只是創建條件。所以51樓的朋友說得非常對。
那什么時候執行呢?因為我們的IQueryable<T>繼承了IEnumabler<T>,所以我們必須實現GetEnumerator()。我們ToList或foreach時,其實就會調用GetEnumerator()。這時我們就調用Execute進行解析Expresstion,從而得到我們想要的結果。
總結就是IQueryable只是創建條件,當我們調用a.Where(x=>xxx)時,其實是將a與后面的條件相並,生成一個新的IQueryable。當我們foreach時就會調用GetEnumerator()。這時我們一般會調用IQueryProvider里的Execute去解析Expresstion並查詢出我們想要的結果
我也知道這篇文章介紹的和我們所說的“ORM”相差很遠,但是所謂的ORM最復雜的莫非查詢部分了,而依照我這思路走下去,我覺得是可以自己完成一個的。。
我剛開始寫博客第二天,沒想到這文章反響這么大。我承認有點重復造輪子,也非常不成熟,但我還是想通過自己的思考去構造屬於自己的東西。
不知道大家有沒看過頭文字D,里頭有個組織叫東堂墊,他們里面的人是拆掉ABS的。因為他們會長說,你要先學會不使用ABS進行剎車才知道ABS的真諦