"理解IQueryable的最簡單方式就是,把它看做一個查詢,在執行的時候,將會生成結果序列。" - Jon Skeethtml
LINQ to SQL能夠將查詢表達式轉換爲SQL語句,而後在數據庫中執行。相比LINQ to Object,則是將查詢表達式直接轉化爲Enumerable的一系列方法,最終在C#內部執行。LINQ to Object的數據源老是實現IEnumerable<T>(因此不如叫作LINQ to IEnumerable<T>),相對的,LINQ to SQL的數據源老是實現IQueryable<T>並使用Queryable的擴展方法。程序員
將查詢表達式轉換爲SQL語句並不保證必定能夠成功。面試
理解IQueryable的最簡單方式就是,把它看做一個查詢,在執行的時候,將會生成結果序列。sql
IQueryable是一個繼承了IEnumerable接口的另外一個接口。數據庫
Queryable是一個靜態類型,它集合了許多擴展方法,擴展的目標是IQueryable和IEnumerable。它令IQueryable和IEnumerable同樣,擁有強大的查詢能力。express
AsQueryable方法將IEnumerable<T>轉換爲IQueryable<T>。緩存
var seq = Enumerable.Range(0, 9).ToList(); IEnumerable<int> seq2 = seq.Where(o => o > 5); IQueryable<int> seq3 = seq.Where(o => o > 4).AsQueryable();
下面試圖實現一個很是簡單的查詢提供器(即LINQ to xxx),其能夠將簡單的where lambda表達式轉換爲SQL,功能很是有限。在LINQ to SQL中lambda表達式首先被轉化爲表達式樹,而後再轉換爲SQL語句。架構
咱們試圖實現一個能夠將where這個lambda表達式翻譯爲SQL語句的查詢提供器。oracle
首先在本地創建一個數據庫,而後創建一個簡單的表。以後,再插入若干測試數據。用於測試的實體爲:ide
public class Staff { public int Id { get; set; } public string Name { get; set; } public string Sex { get; set; } }
因爲VS版本是逆天的2010,且沒有EF,我採用了比較原始的方法,即創建一個mdf格式的本地數據庫。你們可使用EF或其餘方式。
public class DbHelper : IDisposable { private SqlConnection _conn; public bool Connect() { _conn = new SqlConnection { ConnectionString = "Data Source=.\\SQLEXPRESS;" + "AttachDbFilename=Your DB Path" + "Integrated Security=True;Connect Timeout=30;User Instance=True" }; _conn.Open(); return true; } public void ExecuteSql(string sql) { SqlCommand cmd = new SqlCommand(sql, _conn); cmd.ExecuteNonQuery(); } public List<Staff> GetEmployees(string sql) { List<Staff> employees = new List<Staff>(); SqlCommand cmd = new SqlCommand(sql, _conn); SqlDataReader sdr = cmd.ExecuteReader(); while (sdr.Read()) { employees.Add(new Staff{ Id = sdr.GetInt32(0), Name = sdr.GetString(1), Sex = sdr.GetString(2) }); } return employees; } public void Dispose() { _conn.Close(); _conn = null; } }
這個很是簡陋的DbHelper擁有鏈接數據庫,簡單執行sql語句(不須要返回值,用於DDL或delete語句)和經過執行Sql語句,返回若干實體的功能(用於select語句)。
public static List<Staff> Employees; static void Main(string[] args) { using (DbHelper db = new DbHelper()) { db.Connect(); //db.ExecuteSql("CREATE TABLE Staff ( Id int, Name nvarchar(10), Sex nvarchar(1))"); db.ExecuteSql("DELETE FROM Staff"); db.ExecuteSql("INSERT INTO Staff VALUES (1, 'Frank','M')"); db.ExecuteSql("INSERT INTO Staff VALUES (2, 'Mary','F')"); Employees = db.GetEmployees("SELECT * FROM Staff"); } Console.ReadKey(); }
在主函數中咱們執行建表(只有第一次才須要),刪除記錄,並插入兩行新紀錄的工做。最後,咱們選出新紀錄並存在List中,這樣咱們的準備工做就作完了。咱們的目標是解析where表達式,將其轉換爲SQL,而後調用ExecuteSql方法返回數據,和經過直接調用where進行比較。
首先咱們自建一個類別FrankQueryable,繼承IQueryable<T>。由於IQueryable<T>繼承了IEnumerable<T>,因此咱們同樣要實現GetEnumerator方法。只有當表達式須要被計算時,纔會調用GetEnumerator方法(例如純Select就不會)。另外,IQueryable<T>還有三個屬性:
public class FrankQueryable<T> : IQueryable<T> { public IEnumerator<T> GetEnumerator() { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public Expression Expression { get; private set; } public Type ElementType { get; private set; } public IQueryProvider Provider { get; private set; } public FrankQueryable() { } }
咱們須要實現構造函數和GetEnumerator方法。
構建一個本身的查詢提供器須要繼承IQueryable<T>。查詢提供器將會作以下事情:
咱們要本身寫一個簡單的查詢提供器,因此咱們要寫一個IQueryProvider,而後在構造函數中傳入。咱們再次新建一個類型,繼承IQueryProvider,此時咱們又須要實現四個方法。其中非泛型版本的兩個方法能夠暫時不用理會。
public class FrankQueryProvider : IQueryProvider { public IQueryable CreateQuery(Expression expression) { throw new NotImplementedException(); } public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { throw new NotImplementedException(); } public object Execute(Expression expression) { throw new NotImplementedException(); } public TResult Execute<TResult>(Expression expression) { throw new NotImplementedException(); } }
此時FrankQueryable類型的構造函數能夠將屬性賦成適合的值,它變成這樣了:
public FrankQueryable(Expression expression, FrankQueryProvider provider) { Expression = expression; ElementType = typeof(T); Provider = provider; }
其中CreateQuery方法的實現很簡單。
public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { Console.WriteLine("Going to CreateQuery"); return new FrankQueryable<TElement>(this, expression); }
而後,咱們能夠實現FrankQueryable的GetEnumerator方法,它的目的在於呼叫其配套的provider中的Execute方法,從而令咱們本身的邏輯得以執行(咱們已經在構造函數中傳入了本身的provider):
public IEnumerator<T> GetEnumerator() { Console.WriteLine("Begin to iterate."); var result = Provider.Execute<List<T>>(Expression); foreach (var item in result) { Console.WriteLine(item); yield return item; } }
另外爲方便起見,咱們加入一個無參數的構造函數,其會先調用有參的構造函數,而後再執行它本身,將表達式設爲一個默認值:
public FrankQueryable() : this(new FrankQueryProvider(), null) { //this is T Expression = Expression.Constant(this); }
最後就是FrankQueryProvider的Execute方法了,它的實現須要咱們本身手動解析表達式。因此咱們能夠創建一個ExpressionTreeToSql類,並在Execute方法中進行調用。
public TResult Execute<TResult>(Expression expression) { string sql = ""; //經過某種方式得到sql(謎之代碼) //ExpressionTreeToSql Console.WriteLine(sql); using (DbHelper db = new DbHelper()) { db.Connect(); dynamic ret = db.GetEmployees(sql); return (TResult) ret; } }
假設咱們得到了正確的SQL語句,那麼接下來的事情固然就是鏈接數據庫得到結果了。這個已是現成的了,那麼固然最後也是最關鍵的一步就是解析表達式得到SQL語句了。
注意,CreateQuery每次都產生新的表達式對象,無論相同的表達式是否已經存在,這構成了對錶達式進行緩存的動機。
在進行解析以前,假設咱們先把SQL語句寫死,那麼咱們將會得到正確的輸出:
public TResult Execute<TResult>(Expression expression) { string sql = "select * from staff where Name = 'Frank'"; Console.WriteLine(sql); using (DbHelper db = new DbHelper()) { db.Connect(); dynamic ret = db.GetEmployees(sql); return (TResult) ret; } }
主程序:
static void Main(string[] args) { using (DbHelper db = new DbHelper()) { db.Connect(); //db.ExecuteSql("CREATE TABLE Staff ( Id int, Name nvarchar(10), Sex nvarchar(1))"); db.ExecuteSql("DELETE FROM Staff"); db.ExecuteSql("INSERT INTO Staff VALUES (1, 'Frank','M')"); db.ExecuteSql("INSERT INTO Staff VALUES (2, 'Mary','F')"); Employees = db.GetEmployees("SELECT * FROM Staff"); } var aa = new FrankQueryable<Staff>(); //Try to translate lambda expression (where) var bb = aa.Where(t => t.Name == "Frank"); Console.WriteLine("Going to compute the expression."); var cc = bb.ToList(); Console.WriteLine("cc has {0} members.", cc.Count); Console.WriteLine("Id is {0}, and sex is {1}", cc[0].Id, cc[0].Sex); Console.ReadKey(); }
此時咱們發現,程序的行爲將按照咱們的查詢提供器來走,而不是默認的IQueryable。(默認的提供器不會打印任何東西)咱們的打印結果是:
Going to CreateQuery Going to compute the expression. Begin to iterate. select * from staff where Name = 'Frank' FrankORM.Staff cc has 1 members. Id is 1, and sex is M
當程序運行到
var bb = aa.Where(t => t.Name == "Frank");
這裏時,會先調用泛型的CreateQuery方法(由於aa對象的類型是FrankQueryable<T>因此咱們會進入本身的查詢提供器,而Where是Queryable的擴展方法因此FrankQueryable自動擁有),而後輸出Going to CreateQuery。而後,由於此時並不計算表達式,因此不會緊接着就進入Execute方法。以後主程序繼續運行,打印Going to compute the expression.
以後,在主程序的下一行,因爲咱們調用了ToList方法,此時必需要計算表達式了,故程序開始進行迭代,調用GetEnumerator方法,打印Begin to iterate,而後調用Execute方法,仍然是使用咱們本身的查詢提供器的邏輯,執行SQL,輸出正確的值。
經過此次測試,咱們瞭解到了整個IQueryable的工做流程。因爲Queryable那一大堆擴展方法,咱們能夠垂手可得的得到強大的查詢能力。那麼如今固然就是把SQL解析出來,填上整個流程最後的一塊拼圖。
咱們將解析方法放入ExpressionTreeToSql類中,並將其命名爲VisitExpression。這個類是本身寫ORM必不可少的,有時也通稱爲ExpressionVisitor類。
咱們的輸入是一個lambda表達式,它是長這樣的:
var bb = aa.Where(t => t.Name == "Frank");
咱們的目標則是這樣的:
Select * from Staff where Name = ‘Frank’
其中Staff,Name和Frank是咱們須要從外界得到的,其餘則都是語法固定搭配。因此咱們須要一個解析表達式的方法,它接受一個表達式做爲輸入,而後輸出一個字符串。經過表達式咱們能夠得到Name和Frank這兩個值。而咱們還須要知道目標實體類的類型名稱Staff,因此咱們的解析方法還須要接受一個泛型T。
另外,因爲咱們的解析方法頗有多是遞歸的(由於要解析表達式樹),咱們的輸出還須要用ref加以修飾。因此這個解析方法的簽名爲:
public static void VisitExpression<T>(T enumerable, Expression expression, ref string sql)
得到Select * from Staff這一步是比較容易的:
public static string GenerateSelectHeader<T>(T type) { var typeName = type.GetType().Name.Replace("\"", ""); return string.Format("select * from {0} ", typeName); }
咱們的解析方法首先要加上:
public static void VisitExpression<T>(T enumerable, Expression expression, ref string sql) { if (sql == String.Empty) sql = GenerateSelectHeader(enumerable); }
固然這裏咱們也默認設定是選取實體全部的列了。若是是選取一部分,則還須要解析select表達式。
回到Execute方法,如今謎之代碼也就浮出水面了,它不過是:
ExpressionTreeToSql.VisitExpression(new Staff(), expression, ref sql);
解析的第二步就是where這個表達式了。首先咱們要知道它的NodeType(即類型,Type是表達式最終計算結果值的類型)。經過設置斷點,咱們看到類型是Call類型,因此咱們須要將表達式轉爲MethodCallExpression(不然咱們將沒法得到任何細節內容,這對於全部類型的表達式都同樣)。
如今咱們得到了where這個方法名。
switch (expression.NodeType) { case ExpressionType.Call: MethodCallExpression method = expression as MethodCallExpression; if (method != null) { sql += method.Method.Name; } break; default: throw new NotSupportedException(string.Format("This kind of expression is not supported, {0}", expression.NodeType)); }
如今咱們能夠運行程序了,固然,結果sql是錯誤的,咱們的解析還沒結束,經過設置斷點檢查表達式的各個變量,咱們發現Argument[1]是表達式自己,因而咱們經過遞歸繼續解析這個表達式:
咱們能夠根據每次拋出的異常得知咱們下一個表達式的種類是什麼。經過異常發現,下一個表達式是一個Quote類型的表達式。它對應的表達式類型是Unary(即一元表達式)。一元表達式中惟一有用的東西就是Operand,因而咱們繼續解析:
case ExpressionType.Quote: UnaryExpression expUnary = expression as UnaryExpression; if (expUnary != null) { VisitExpression(enumerable, expUnary.Operand, ref sql); } break;
下一個表達式:t=>t.Name==」Frank」,顯然是一個lambda表達式。它有用的地方就是它的Body(t.Name==」Frank」):
case ExpressionType.Lambda: LambdaExpression expLambda = expression as LambdaExpression; if (expLambda != null) { VisitExpression(enumerable, expLambda.Body, ref sql); } break;
最後,咱們終於來到了終點。這回是一個Equal類型的表達式,它的左邊是t.Name,右邊則是「Frank」,都是咱們須要的值:
case ExpressionType.Equal: BinaryExpression expBinary = expression as BinaryExpression; if (expBinary != null) { var left = expBinary.Left; var right = expBinary.Right; sql += " " + left.ToString().Split('.')[1] + " = '" + right.ToString().Replace("\"", "") + "'"; } break;
將這些case合起來,一個簡陋的LINQ to SQL解釋器就作好了。此時咱們將寫死的SQL去掉,程序應當獲得正確的輸出:
public TResult Execute<TResult>(Expression expression) { string sql = ""; ExpressionTreeToSql.VisitExpression(new Staff(), expression, ref sql); Console.WriteLine(sql); using (DbHelper db = new DbHelper()) { db.Connect(); dynamic ret = db.GetEmployees(sql); return (TResult) ret; } }
能夠看到,where lambda表達式被轉化爲一個複雜的表達式樹。經過手動解析表達式樹,咱們能夠植入本身的邏輯,從而實現LINQ to SQL不能實現的功能。
固然,例子只是最最基本的狀況,若是表達式樹變得複雜,生成出的sql極可能是錯的。
咱們來看看下面這個狀況,咱們增長一個where表達式:
using (DbHelper db = new DbHelper()) { db.Connect(); //db.ExecuteSql("CREATE TABLE Staff ( Id int, Name nvarchar(10), Sex nvarchar(1))"); db.ExecuteSql("DELETE FROM Staff"); db.ExecuteSql("INSERT INTO Staff VALUES (1, 'Frank','M')"); db.ExecuteSql("INSERT INTO Staff VALUES (2, 'Mary','F')"); db.ExecuteSql("INSERT INTO Staff VALUES (1, 'Roy','M')"); Employees = db.GetEmployees("SELECT * FROM Staff"); } var test = Employees.Where(t => t.Sex == "M").Where(t => t.Name == "Frank"); var aa = new FrankQueryable<Staff>(); //Try to translate lambda expression (where) var bb = aa.Where(t => t.Sex == "M") .Where(t => t.Name == "Frank");
此時咱們用IQueryable<T>能夠得出正確的結果(test只有1筆輸出),但使用本身的查詢提供器,得到的SQL倒是錯誤的(第一個Sex = M不見了)。咱們發現,問題出在咱們解析MethodCallExpression那裏。
當只有一個where表達式時,表達式樹是這樣的:
因此咱們在解析MethodCallExpression時,直接跳過了argument[0](實際上它是一個常量表達式),而如今咱們彷佛不能跳過它了,由於如今的表達式樹中,argument[0]是:{value(FrankORM.FrankQueryable`1[FrankORM.Staff]).Where(t => (t.Sex == "M"))}
它包含了有用的信息,因此咱們不能跳過它了,咱們要解析全部的argument,並使用and進行鏈接:
case ExpressionType.Call: MethodCallExpression exp = expression as MethodCallExpression; if (exp != null) { if(!sql.Contains(exp.Method.Name)) sql += exp.Method.Name; foreach (var arg in exp.Arguments) { VisitExpression(enumerable, arg, ref sql); } sql += " and "; } break;
此時再運行程序,發生異常。系統提示咱們沒有關於constant表達式的解析,對於constant表達式,咱們什麼都不用作。
case ExpressionType.Constant: break;
使用上面的代碼,再解析一次,咱們就獲得了一條看上去比較正確的SQL:
select * from Staff Where Sex = 'M' and Name = 'Frank' Sex = 'M' and Name = 'Frank' and
結尾and多出現了一次,這是由於咱們每次解析都在最後加上了and。簡單的去掉and,程序就會輸出正確的結果。
此次表達式樹是這樣的:
固然,這個擴展的代碼質量已經很是差了,各類湊數。不過,我在這裏就僅以此爲例,解釋下如何擴展併爲表達式樹解析增長更多的功能,使之能夠應付更多類型的表達式。
首先IQueryable<T>是解析一棵樹,IEnumerable<T>則是使用委託。前者的手動實現上面已經講解了(最基本的狀況),然後者你徹底能夠用泛型委託來實現。
IQueryable<T>繼承自IEnumerable<T>,因此對於數據遍從來說,它們沒有區別。二者都具備延遲執行的效果。可是IQueryable的優點是它有表達式樹,全部對於IQueryable<T>的過濾,排序等操做,都會先緩存到表達式樹中,只有當真正發生遍歷的時候,纔會將表達式樹由IQueryProvider執行獲取數據操做。
而使用IEnumerable<T>,全部對於IEnumerable<T>的過濾,排序等操做,都是在內存中發生的。也就是說數據已經從數據庫中獲取到了內存中,在內存中進行過濾和排序操做。
當數據源不在本地時,由於IEnumerable<T>查詢必須在本地執行,因此執行查詢前咱們必須把全部的數據加載到本地。並且大部分時候,加載的數據有大量的數據是咱們不須要的無效數據,可是咱們卻不得不傳輸更多的數據,作更多的無用功。而IQueryable<T>卻總能只提供你所須要的數據,大大減小了傳輸的數據量。
好處:
缺點:
ORM的核心是DbContext。它能夠當作是一個數據庫的副本,咱們只須要訪問它的方法就能夠實現對數據庫的CRUD。
表達式樹上手指南:
http://www.cnblogs.com/Ninputer/archive/2009/09/08/expression_tree3.html
對錶達式樹緩存以進一步提升性能:
http://blog.zhaojie.me/2009/03/expression-cache-1.html
本身實現的LINQ TO 博客園:
http://www.cnblogs.com/jesse2013/p/expressiontree-part1.html
帶有GIF的IQueryable講解:
http://www.cnblogs.com/zhaopei/p/5792623.html