.NET面試題系列[14] - LINQ to SQL與IQueryable

.NET面試題系列目錄

名言警句

"理解IQueryable的最簡單方式就是,把它看做一個查詢,在執行的時候,將會生成結果序列。" - Jon Skeethtml

 

LINQ to Object和LINQ to SQL有何區別?

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

理解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 SQL

下面試圖實現一個很是簡單的查詢提供器(即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進行比較。

 

實現IQueryable<T>

首先咱們自建一個類別FrankQueryable,繼承IQueryable<T>。由於IQueryable<T>繼承了IEnumerable<T>,因此咱們同樣要實現GetEnumerator方法。只有當表達式須要被計算時,纔會調用GetEnumerator方法(例如純Select就不會)。另外,IQueryable<T>還有三個屬性:

  1. Expression:這個很好理解,就是要處理的表達式
  2. Type
  3. IQueryProvider:你本身的IQueryProvider。在構造函數中,須要傳入本身的IQueryProvider實現本身的邏輯。
    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方法。

實現IQueryProvider

構建一個本身的查詢提供器須要繼承IQueryable<T>。查詢提供器將會作以下事情:

  1. 調用CreateQuery創建一個查詢,但不計算。只在須要的時候才進行計算。
  2. 若是須要執行表達式的計算(例如調用了ToList),此時調用GetEnumerator,觸發Execute的執行,從而計算表達式。咱們須要把本身的邏輯寫在Execute方法中。並在GetEnumerator中進行調用。

咱們要本身寫一個簡單的查詢提供器,因此咱們要寫一個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每次都產生新的表達式對象,無論相同的表達式是否已經存在,這構成了對錶達式進行緩存的動機。

 

測試IQueryable的運行流程

在進行解析以前,假設咱們先把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類。

 

解析Where lambda表達式:第一步

咱們的輸入是一個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 lambda表達式:第二步

解析的第二步就是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與 IEnumerable的異同?

首先IQueryable<T>是解析一棵樹,IEnumerable<T>則是使用委託。前者的手動實現上面已經講解了(最基本的狀況),然後者你徹底能夠用泛型委託來實現。

IQueryable<T>繼承自IEnumerable<T>,因此對於數據遍從來說,它們沒有區別。二者都具備延遲執行的效果。可是IQueryable的優點是它有表達式樹,全部對於IQueryable<T>的過濾,排序等操做,都會先緩存到表達式樹中,只有當真正發生遍歷的時候,纔會將表達式樹由IQueryProvider執行獲取數據操做。

而使用IEnumerable<T>,全部對於IEnumerable<T>的過濾,排序等操做,都是在內存中發生的。也就是說數據已經從數據庫中獲取到了內存中,在內存中進行過濾和排序操做。

當數據源不在本地時,由於IEnumerable<T>查詢必須在本地執行,因此執行查詢前咱們必須把全部的數據加載到本地。並且大部分時候,加載的數據有大量的數據是咱們不須要的無效數據,可是咱們卻不得不傳輸更多的數據,作更多的無用功。而IQueryable<T>卻總能只提供你所須要的數據,大大減小了傳輸的數據量。

IQueryable總結

  1. 理解IQueryable的最簡單方式就是,把它看做一個查詢,在執行的時候,將會生成結果序列。
  2. 繼承IQueryable<T>意味着得到強大的查詢能力,這是由於自動得到了Queryable的一大堆擴展方法。
  3. 當對一個IQueryable<T>的查詢進行解析時,首先會訪問IQueryable<T>的QueryProvider,而後訪問CreateQuery<T>方法,並將輸入的查詢表達式傳入,構建查詢。
  4. 一個查詢進行執行,就是開始遍歷IQueryable的過程,其會調用Execute方法並傳遞表達式樹。
  5. 不是全部的表達式樹均可以翻譯成SQL。例如ToUpper就不行。
  6. 本身寫一個ORM意味着要本身寫一個QueryProvider,自定義Execute方法來解析表達式樹。因此,你必需要有一個解析表達式樹的類,一般你們都叫它ExpressionVisitor。
  7. 一般使用遞歸的方式解析表達式樹,這是由於表達式樹的任意結點(包括葉結點)都是表達式樹。
  8. CreateQuery每次都產生新的表達式對象,無論相同的表達式是否已經存在,這構成了對錶達式進行緩存的動機。

ORM和經典的Datatable的優劣比較

好處:

  1. 提供面向對象和強類型,慣用OO語言的程序員會很快上手。
  2. 隱藏了數據訪問細節,使得幹掉整個DAL成爲可能。在三層架構中BL要去調用DAL來得到數據,而如今BL能夠直接經過lambda表達式等各類方式得到數據,再也不須要DAL。
  3. 將程序員從對SQL語句的拼接(尤爲是insert)中解放出來,它既容易錯,又很難發現錯誤。如今插入的對象都是強類型的,就猶如插入一個List同樣。
  4. 以相同的語法操做各類不一樣的數據庫(例如oracle, SQL server等)
  5. 與經典的DataReader相比,當數據表的某欄的數據類型發生改變時,DataReader就會發生錯誤(傳統的方式是使用DataReader.Read方法一行行讀取數據,而後經過GetString,GetInt32等方法得到每一列的數據)。並且錯誤在運行時纔會發生。ORM則會在編譯時就會發生錯誤,並且只須要更改對象屬性的類型就不會發生問題。

缺點:

  1. 有些複雜的SQL或者SQL內置的方法不能經過ORM翻譯。
  2. 自動產生的SQL語句有時的性能較低,這跟產生的機理有關。對於不熟悉ORM的程序員,可能會致使編寫出的程序性能低劣。
  3. 難以替代Store procedure。

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 

相關文章
相關標籤/搜索