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