LINQ之路(2):LINQ to SQL本质
在前面一篇文章中回顾了LINQ基本语法规则,在本文将介绍LINQ to SQL的本质。LINQ to SQL是microsoft针对SQL Server的一个ORM的解决方案。既然是ORM,那对象(Object)、关系数据(Relation)和映射(Mapping)这三个组成要素是不可或缺的。本文将分为对象和关系数据的映射、从查询表达式到SQL的转换和DataContext数据上下文三个部分来说明。
1.对象和关系数据的映射
通过定义对象和关系数据的映射关系,我们可以以面向对象的方式来处理关系数据。通常,定义对象和关系数据的映射有两种方式:一是自定义特性,二是XML描述。前者是通过自定义一些描述关系数据结构的Attribute来修饰类,从而建立映射关系,而后者则是通过XML来描述这种映射关系。在LINQ to SQL中,为了简化工作,微软为我们提供了非常好用的工具SqlMetal,利用它,我们可以为 LINQ to SQL 的 .NET Framework 组件生成代码和映射。例如,我们可以利用
C:Program Files (x86)Microsoft Visual Studio 11.0VC>SqlMetal /conn:"server=.;
database=IT_Company_New;UID=sa;Password=123456" /language:csharp /namespace:jell
o.test /code:d:CompanyDataContext.cs
来生成数据上下文代码,我们也可以利用
C:Program Files (x86)Microsoft Visual Studio 11.0VC>SqlMetal /conn:"server=.;
database=IT_Company_New;UID=sa;Password=123456" /dbml:d:IT_Company_new.dbml
来生成中间数据库标记语言(.dbml)文件。
其实,这也是LINQ to SQL设计器采取的方式。
通过LINQ to SQL设计器将会为我们生成一下三个文件:
- .dbml文件:中间数据库标记语言文件,描述映射,由SqlMetal生成
- .dbml.layout文件:设计器界面布局文件,描述组件位置
- .designer.cs文件:生成代码部分,由SqlMetal生成
这里详细介绍下生成的代码,代码大致分为两个部分:数据上下文和表对象。数据上下文对应数据库,表对象。数据上下文继承自System.Data.Linq.DataContext,表对象继承自INotifyPropertyChanging, INotifyPropertyChanged。数据上下文采用AttributeMappingSource来指定映射方式,除了AttributeMappingSource方式还可以采用XmlMappingSource,使用DatabaseAttribute修饰以映射数据库,表对象使用TableAttribute修饰以映射表。数据上下文包含表对应的Table
2.将查询表达式转换为SQL
var query = from student in _dataContext.T_Student
where student.Age < 20
select student;
foreach (var student in query)
{
Console.WriteLine(student.Name);
}
上面的查询表达式可以从数据库中检索出年龄小于20岁的学生。那么它是如何工作的呢?
这和我们上一篇中的写法几乎完全一样,唯一的区别是上一篇中数据源是IEnumerable
public interface IQueryable : IEnumerable
{
Type ElementType { get; }
Expression Expression { get; }
IQueryProvider Provider { get; }
}
包含三个属性:
ElementType:包含元素类型
Expression:要执行的操作的Expression
Provider:实现了IQueryProvider接口的Provider
再来看下IQueryProvider接口定义:
public interface IQueryProvider
{
IQueryable CreateQuery(Expression expression);
IQueryable<TElement> CreateQuery<TElement>(Expression expression);
object Execute(Expression expression);
TResult Execute<TResult>(Expression expression);
}
包含四个方法,CreateQuery和Execute分别一个泛型和一个非泛型方法。
-
CreateQuery:构造一个IQueryable或IQueryable
对象,该对象可计算指定表达式目录树所所示的查询。 -
Execute:执行指定表达式目录树所表示的查询。
通过这种接口的设计,IQueryable允许我们创建支持其它非SQL Server数据库的实现。Provider将借助于IQueryable所提供的种种信息把查询语句转换为另一种方式,转换的实际操作将由CreateQuery方法实现,随后转换的结果将由Execute方法使用。
LINQ使用哪种方式进行转换,取决于数据源类型。若数据源未实现IQueryable或IQueryable
var query = from student in _dataContext.T_Student
where student.Age < 22
let Len = student.Name.Length
orderby Len
select student;
foreach (var student in query)
{
Console.WriteLine(student.Name);
}
步骤如下:
1.查询表达式转换为扩展方法:
var query = _dataContext.T_Student.Where(student => student.Age < 22)
.Select(student => new { Len = student.Name.Length, student = student })
.OrderBy(student => student.Len)
.Select(student => new
{
ID = student.student.ID,
Name = student.student.Name,
Age = student.student.Age,
ClassID = student.student.ClassID
});
2.生成表达式目录树,通过调用Queryable一系列的扩展方法构造IQueryable
3.在进行迭代等真正获取数据操作中调用source.Provider.Execute方法来执行表达式以获取结果。这个过程比较复杂,在Execute方法中,.net framework是通过ExpressionVisitor类以Visitor模式方式来将表达式目录树解析,根据AttributeMapping或XmlMapping映射规则生成如下SQL,然后执行该SQL。
SELECT [t1].[ID], [t1].[Name], [t1].[Age], [t1].[ClassID]
FROM (
SELECT LEN([t0].[Name]) AS [value], [t0].[ID], [t0].[Name], [t0].[Age], [t0].[ClassID]
FROM [dbo].[T_Student] AS [t0]
) AS [t1]
WHERE [t1].[Age] < @p0
ORDER BY [t1].[value]
3.DataContext数据上下文
在LINQ to SQL中,DataContext是一个不能不说的东东。它拥有这么几个功能:管理业务实体、管理数据库连接、数据库映射、查询转换、对象标识和跟踪变化。在DataContext中维护着Table
每次查询数据库时,DataContext都会使用IdentityManager来判断是否已有某个对象的缓存,若已缓存则DataContext从内部缓存中获取。
在修改业务实体时,DataContext将会使用ChangeTracker同时保留修改前后的两个值,当提交修改过的记录时才会向数据库发出请求。