第14章 LINQ to SOL

本章我们研究LNQ to SQL以及它的后续版本 LINQ to Entity Framework(简称 LINQ to Entities)。 Entity Framework是微软为.NET设计的一套ORM,它建立在之前的 ADO. NET之上。

LINQ to Entities和 LINQ to Object最大的不同之处在于, LINQ to Entities会被转化为表达式树,最终再转化为SQL语句,访问数据库并取回数据,数据的类型为 IQueryable。 LINQ to Object则将査询表达式和方法语法转化为委托在本地执行,数据的类型为Enumerable。作一看,似乎前者麻烦很多,但由于操作可以先被编译器缓存下来,最后当正访问数据时,才生成SQL语句返回 IQueryable,因此, LINQ to Entities j返回的数据总是你所需要的。
14.1 IQueryable
IQueryable继承了 IEnumerable接口。 Queryable是一个静态类型,它集合了许多扩是方法,扩展的目标是 IQueryable和 IEnumerable。它令 IQueryable和 IEnumerable一样,拥有强大的査询能力。但是,和 Enumerable不同的是, Queryable的扩展方法传入表达式:
在这里插入图片描述

而 Enumerable的扩展方法,传人的则是泛型委托。 LINQ to Entities 基于 IQueryable。
C#提供了 AsQueryable方法,可以将IEnumerable转换为 IQueryable:

var seq= Enumerable.Range(0, 9).Tolist();
IEnumerable<int> seq2= seq.Where(o=>o >5);
IQueryables<int> seq3 =seq.Where(o=>o >4).Asqueryable();

反过来则可以调用 AsEnumerable方法,将 IQueryable转换为 IEnumerable。

14.2 IQueryables与 IEnumerable的异同
IQueryables继承自 IEnumerable,所以对于数据遍历来说,它们没有区别。两者都具有延迟执行的效果。但IQueryables的优势是有表达式树,所有对于 IQueryables的过滤和排序等操作,都会先缓存到表达式树中,只有当真正发生遍历的时候,オ会将表达式树由IQueryable执行最终转换为SQL,获取数据。而使用 IEnumerable,所有对于IEnumerable的过滤,排序等操作,都是在内存中发生的。也就是说,数据已经从数据库中获取到内存中,在内存中进行过滤和排序操作。
当数据源不在本地时,因为 IEnumerable查询必须在本地执行,所以执行查询前必须把所有的数据加載到本地。大部分时候,加载的数据有大量是我们不需要的无效数据,但是我们却不得不传输更多的数据,做很多无用功。而 IQueryables却总能只提供你所需要的数据,大大减少了传输的数据量。
使用 LINQPad我们可以看到编译器生成的SQL。指定语言为C# Program,数据库为Northwind.mdf,我们可以键人下面的代码:

void Main()
{
	//这是 IQueryables
	var list = Customers.where(c => c.Address.Contains(s));
	//这是 IEnumerable
	IEnumerable<Customers> list2 = Customers.AsEnumerable<Customers>().Where(c =>
 	c.Address.Contains("s"));
	var a=list.First();
	var b=list2.First();
}

其中,最后两句代码通过取第一个值强迫査询运行(如果去掉的话,不会生成任何SQL)。我们按F5运行,然后从结果栏中选择SQL,即可看到两个SQL:第一条SQL对
应的是 IQueryables的SQL。这个SQL会在数据库中进行筛选(拥有一个 where子句,而且使用top关键字选择了第一条数据),最终只有一条数据回到本地。而第二条SQL对应的是 IEnumerable的查询,我们可以看到没有任何的筛选,所有的数据都会被捞回本地。代码如下:

在这里插入图片描述
当数据很多的时候,性能孰强孰弱已经十分明显了一一即使 LINQ to Entities需要多一层表达式树解析SQL,但它可以缓存所有的操作(上面的例子中,当取第一个值时,LNQto Entities缓存了之前筛选的操作,并将两项操作合二为一,生成最有效的SQL代码,捞回最少的数据),最终捞回的数据总是你所需要的。和数据库通信以及数据交换相比,本解析SQL的时间可以忽略不计,因此,如果数据量大, LINQ to Entities的性能将会更佳。
14.3数据库操作
14.3.1 弱类型实体集
如果不使用ORM,我们还可以选择用ADO.NET和数据库沟通。ADO.NET拥有连线和离线两种模式,下面的代码展示了如何连线并使用 DataReader从数据库中拿数据。相信大家一定都写过或者看过类似的代码:

//莛接到数据库
var conn= new SqlConnection("Data Source=xxx: Initial Catalog=Northwind; User ID=sa");
conn.Open();
//传入SQL语句
var cmd = new Sqlcommand("SELECT * FROM Customer WHERE CustomerID LIKE '%v%',conn);
using (var reader=cmd.ExecuteReader(Commandbehavior Closeconnection))
{
	//使用 DataReader
	while(reader.Read())
	{
		//必须手动入列名,且列名的类型和Getxxx方法必须匹配
		var s = reader.Getstring(reader.Getordinal("CustomerID"));
		//或者这样做也行,获得第一列,但你要确定第一列一定是字符串类型
		s= reader. Getstring(0);
	}
}

如果是离线的方式,则是将数据装载到一个 Dataset/ DataTable中,然后在其他地方再遍历这个 Dataset/DataTable对象,这里就不展示代码了。
这是若于年前的经典模式,并广泛存在于三层架构的数据访问对象(Data Access Object,DAO)中。ADO.NET支持传入SQL语句和参数化查询,甚至还支特调Sql Server中的存储过程。因此,存储过程一度大行其道。无论是连线还是离线的方式,取出的资料都是弱类型的,因此,这样的做法有运行异常的可能。如果哪天数据库表的定义发生改变,我们无法知道程序什么地方会出错,而且,SOL语句的拼接也很容易出错。
使用这种方式开发时, SQL Server是标配。必须先在 SQL Server I中进行调试,确保获得正确的SQL语句之后,再编写C#代码,确定其生产的SQL语句和正确的语句一样。最后,还要小心翼翼地从结果集中取出列,一一确定使用不同的 Getxxx方法获得每个不同类型的列,如果列类型是int,使用 Getstring就会出错。

14.3.2 Entity Framework
ORM解决了弱类型的问题,使得所有运行时的错误提前到编译时,在第10章,我们已经实现了一个十分简单的ORM,所以,大家应该对这个概念不陌生。ORM在传统的通过SOL访问数据库的方法上面增加了两层:
ロAPI:暴露给用户使用,并使用某种方式将API转化为SQL,我们实现的ORM用字符串拼接法转化SQL。
ロ Mapping:将C#实体类型(cs文件)和数据库表对应起来,我们实现的0RM使用了反射和特性做对应。
ロ 通过SQL访问数据库,我们实现的ORM中,最后还是用ADO.NET访问了数据库。

所有的ORM无论怎么实现,最后肯定还是要通过SQL访问数据库,差别仅在于生成SQL的方法和效率。 Entity Framework是微软提供的ORM,它的“ORM三层架构”分别是:
1)API,又分为三层。
a) LINQ to Entities,用户使用它下命令,传入的命令是表达式树并会被缓存起来。
b) Object Services,围绕 Dbcontext,暴露一部分有用的API让用户调用,包括新增、修改和删除记录,支持将更改存人数据库,并且提供了 实体的状态和变更追踪功能。
c) Entity SQL Service,将表达式树转化为SQL。用户也可以直接访问它并传人SQL,不过这里不支持新增修改和删除记录,这是 Obiect Servic该做的事情。通常来说,不应该直接在这里传人SQL,而应该使用上面两层。
2) Mapping, Entity Data Model负责将C#实体和数据库表对应起来。
3)通过SQL访问数据库,用ADO.NET。
因此, Entity Framework基于 ADO. NET之上。和LINQ相结合, Entity Framework可将LINQ指令转換为SQL语句(当然会有失败的情况),这些指令转换大部分时候的效能都是有保证的。Entity Framework也支持其他很多资料管理系统,例如 Oracle, MYSQL等。
下面,我门着重看看 Entity Framework的上面两层(第三层就是ADO.NET)。
1.Mapping层:Entity Data Model
Entity Framework使用 Entity Data Model将C#排实体和数据库表对应起来。它支持双向转化:
口读取C#实体代码,并在SQL Server中建立全新的数据库(Code First)。Entity Framework4.1之后开始支持。
口读取观成的数据库,生成全新的C#实体代码( Database First)。
口利用 Visual Studio提供的可视化工具,先建立数据库表定义,然后再根据定义生成实体( Model First)和数据库表。 Entity Framework4.0之后开始支持。
Entity Data Model实现了将C#s实体转换为数据库可以处理的形式,并作为 LINQ to Entities的基础。简单来说,它主要做下面几件事。
口描述每个C#实体中每个属性的数据类型和约束,以及实体之间的关系:通过最上层的概
概念纲要定义语言(CSDL).
口将C#实体和数据库表格打通:通过中间的 mapping schema(MSDL)。
口描述数据库表格中每个表格的数据类型和约東,以及表格之间的关系:通过最下层的纲要定义语言(SSDL)。
这样,C#实体就和实际数据库中的表格对应起来了。这三个语言都是以XML的形式,并抱合在一个.edmx文件中。不过,使用 Code First的形式不会导致.edmx文件的产生,整个实体一表格的映射通过代码完成。
2.API层
Entity SQL Service介于 Entity Data Model之上,它将下面的 Mapping层隔离开来,又作为上面 Object Service和 LINQ to Entities的基石。因此,对于程序员来说,完全可以把Entity SQL Service看成是实际数据库,它接受上层传来的查询,然后往下送到 Mapping层。
Object Service是在 Entity SQL Service之上的服务,负责 Dbcontext E的状态跟踪、新建和删除。 Object Service的核心围绕 Dbcontext对象,它提供了很多方法作为 LINQ to Entities的重要补充,例如对表格中记录的添加和删除、执行SQL指令、执行存储过程、将之前对数据库的更改保存起来等等。
位于最顶上的服务就是 LINQ to Entities,它围绕着 Dbset对象。EF中每一个表格都对应一个 Dbset,它实现了 IEnumerable,所以它也具有 LINQ to Object的查询指令。LNQ to Entities主要是负责查询的,对资料的增删改需要通过 Object Service I的方法来实现。
3.Unit of work
Unit of work是一种模式,它和事务配合,确保一连串对数据的改动要么全部发生,要么都不发生。通过 Dbcontext 的 SaveChange等方法,EF天生就具有 Unit of wor水模式。

和 Unit of work相对的就是数据访问对象(DAO)模式了,它不具备将一连串数据的改动组合起来的能力,当业务逻辑层访问数据层时,一次通常只会执行一条CRUD查淘。如果要实现将多条查询打包并使用事务管理,只能为每个组合建立专门的方法,或者使用存储过程。
14.3.3 Repository模式
与数据库中的数据沟通时,不同的功能需要实现不同的数据库访问逻辑。以前的三层架构中,业务逻辑层负责和下面的数据层沟通,并得到想要的数据,传给上面的应用层。在没有ORM之前,数据访问代码量十分庞大,因此,将访问隔离成层的想法是很自然的,效果也很好。
Repository(仓储)模式改进了业务逻辑层,并存在于业务逻辑层和数据层之间。它实现了一个仓库包裹器,使得对仓库的访问不会立刻提交到数据库,直到调用了仓库包裹器为止(即 Unit of work),然后,仓库包裹器就如同事务提交一般将所有数据库操作一并提交给数据库。Repository是仓库管理员,上层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,而不需要知道东西实际放在哪。
使用 Repository模式的以下几点好处:
口方便单元测试。
口通过依赖注入实现ORM本身的置换。
口Unit of work
而现在使用EF之后,数据层基本上可以说是一键搞定了,上面的应用层也可以使用很简短的 Lambda表达式来直接访问数据,似乎仓储模式变得没有必要了,是这样吗?在网络上经常可以看到这种类似的讨论。对于小型工程,ORM完全可以替代仓储模式,首先,ORM本身的置换发生概率是非常低的;其次,EF本身已经实现了仓库包裹器(它的Dbcontext就是),不需要自行再实现一次。拿掉仓储模式之后,单元测试并不是不能做,只不过测试对象变成了上层方法而已。
14.4使用 LINQ to Entity Framework
说完 Entity Framework的概念之后,我们就在实际项目中使用一下吧。
14.4.1 Database First
Database First是指当数据库已经建立完成并有数据之后,在C#进行操作的方式。相比其他两种方式,它最容易理解。我们仍然使用第13章使用过的 Northwind演示数据库。
首先建立一个新的工程,然后为工程新加入一个ADO.NET实体数据模型,如图14-1所示。
在这里插入图片描述
我门将该.edmx文件命名为 Northwind。之后,进入实体数据模型向导。在这里,我们4个选项。
口自数据库的EF设计器:对应 Database First模式。
口空EF设计器模型:对应 Model First模式。
口Code First模型:对应 Code First模式。
口来自数据库 Code First:另一种 Code First模式。
这里我们选择第一项,再选择 SQL Server Database File,这样,就可以直接装载一个.mdf文件了,如图14-2所示。
在这里插入图片描述
选择演示数据库 Northwind之后(使用 Windows身份验证),IDE自动生成连接字符串和数据库上下文的名称,见图14-3。
在这里插入图片描述
之后,如果出现选择版本,请选择实体框架6.0。下一步是选择你要包含进去的表格和存储过程等。你不必在这里选择全部表格,在创建实体数据模型之后,也有机会为模型加入和删除表格,见图14-4。
在这里插入图片描述
其中,有三个选项,意义如下:
口对象名称単复数形式,例如,表的名称为 Orders,如果选择了这个选项,那么该表的记灵对应的实体将为 Order。如果不选择,则对应的实体名称和表名相同。这里我们不选。
口加入外键,通常都选择。
口加人存储过程和函数,视项目情况选择。
我们选择进项2和3,点击完成按钮之后,工程多了ー个.edmx文件,即是上面说到的三层定义文件。可以通过可视化工具观察.edmx文件的内容(图14-5显示了一部分)。
在这里插入图片描述
而 Model First,实际上就是通过IDE手动建立出这张表,而非从数据库导入。这非常烦,如果公司有数据库管理员,那么可以让数据库管理员使用SQL建立数据库,然后再用 Database First。如果使用 Model First,数据库管理员或开发者自己需要熟悉如何使用Visual Studio的可视化工具建立,edmx文件。.edmx文件内容繁多,它包括了数据库中我们选择的表格对应的所有实体,如图146所示。
在这里插入图片描述
每个实体的类型都是 Dbset。我们对实体的增删改查操作都是通过这个对象来进行的。面整个数据库则对应一个NorthwindEntities类,它是一个 Dbcontext。 Dbcontext和Dbset是 LINQ to Entities I的核心。由于 Dbsets继承了 IQueryable,所以大都分LINQ To Object的查询 Dbset也都具有(还记得IQueryable继承了 Ienumerable吗? 而Enumerable 扩展了 IEnumerable)。
现在我们就可以通过 LINQ to Entities对数据库进行操作了。注意所有的数据库都包含在数据库上下文之中,即 Northwindentities这个 Context对象。下面是一个简单的演示:

using (NorthwindEntities context =new Northwindentities())
{
	var customer =context.Customers.First();
	Console.Writeline(customer .Companyname);
}
 得到 context之后,就可以使用API层的中间层-Object Service,对数据库进行编辑。代码的第二行中,访问了 context多个表格中的一个 Customers,它在C#中是以DbSet<T>的形式存在的。 LINQ to Entities可以对 Dbset<T>进行查询。
	EF不是本书的重点,因此不会详细介绍EF的各种查询,有兴趣的读者可以自行到MSDN或其他网站学习。而且,由于EF的核心 Iqueryable继承了 IEnumerable,所以大部分 LINQ To Object 的查询仍然可以使用。

14.4.2 Model First
Model First是当还没有设计出数据库时,就在IDE中开始工作的方式,开发者通过IDE的可视化工具设计数据库编辑.edmx文件(拖拽表格并加入列和约束)。在添加新的ADO. NET实体资料模型时,选择“空EF设计器模型”,然后就可以打开一个可视化的设计面板,如图14-7所示:

在这里插入图片描述
我们以往上抱各种物体,例如实体、关联等。拖了一个实体之后,就可以往上加人列。在右边的属性列表中可以设定列的各种属性。
当全部完成后,单击右键选择“根据模型生成数据库”,就会产生一个 Context,当DBContext出现时,真正的数据库中也会相应地建立这些表格(即,将我们本来在S0L Server中做的工作转移到了Visual Studio 中来做)。我们可以保存这些建立表格的SQL脚本以便以后查阅。
和其他两种方式相比,这种方式使用的比较少。
14.4.3 Code First
对Model First模式来说,可能程序员还是觉得有些别扭,因为程序员比较习惯通过敲代码而不是拖拽控件的方式来生产数据库表。所以 Code First将产生数据库的方式从可视化的方送变成了自己写代码。
我们还是从新建ADO.NET实体数据模型开始,使用默认名称 Model。在向导中,第三个选项对应的就是Code First,如图14-8所示,如果你看不到这个选项,你需要下载对应的工具:https://www.microsoft.com/en-us/download/details.aspx?id=40762
在这里插入图片描述
选择空的 Code First 模型之后,系统将在 app.config中加入默认的资料库类型和连线字符串。系统还为我们生成了一个s文件,如图14-9所示。
在这里插入图片描述

现在我们在主程序中加入:

using (Modell context new Modell())
{
	context.Database.CreateIfNotExists():
}

即可完成数据库的创建。我们运行程序,然后查看一下 SQL Server对象资源管理,如图14-10所示。
在这里插入图片描述
我们看到一个新表 Myentities,含有两列Id和Name。而数据库创建的位置是根据连接字符串所确定的。
若要从现有数据库中产生 Code First模型,也可以在ADO.NET实体数据模型向导中选择来自数据库的 Code First.这就是将数据库中所有的数据列的属性约束以及数据表格之间的关联统统翻译成C#代码的过程。、
通过 Code First模式中丰富的API,C#代码可以表达各种各样的表格以及它们之间的关系,例如主键、外键、约束、一对多、多对多等。对 Entity Framework Code First的详细介绍超出了本书的范围,有兴趣的读者可以自行去网上寻找相关的资料。
14.5表达式树转化为SOL
在第13章中,我们解释了LNQ是如何将各种C#的语法特性结合在一起的,读者现应该可以使用原始方式对 IEnumerable进行査询了。那么, IQueryable可以手动实现吗?当然也是可以的,不过做法要复杂很多。
我们可以通过一个简单的例子一一实现一个非常简单的查询提供器,将简单的Where子句 Lambda表达式树转换为SQL。我们继续使用 Northwind做数据源。本节的代码在ExpTreeToSQL项目中。要使代码成功运行,需要手动建立数据库。
14.5.1 准备工作
我们需要明确一下我们要做的事情:
1)想办法将Where子句 Lambda表达式树转换为SQL。
2)接数据库并传人SQL,获得结果。
通过这两步,我们至少在 Where子句上可以取代LINQ to Entities。为了绕过整个EF,我们先使用SQL Server对象管理器在本地新建一个数据库testDatabase,如图14-11。
在这里插入图片描述
使用下面的SQL脚本建立一个新的表格:

CREATE TABLE [dbo].[Person](
	[Id] INT identity NOT NULL PRIMARY KEY,
	[Name] nvarchar(max) not null,
	[Age] int not null,
	[Sex] nvarchar(50) not null)
	右键点击表目录,添加新表,然后键人SQL,点击更新按钮即可执行SQL,建立表格如14-12所示。

在这里插入图片描述
表格建立好之后,从 SQL Server对象资源管理器中点击 testDatabase,查看属性、即可获得它的连接字符串。
最后,写一个简单的 Dbhelper类和对应的实体类,使用ADO.NET获取数据:

public class Dbhelper:IDisposable
{
	private SqlConnection _conn;
	public bool connect()
	{
		_conn= new SqlConnection
		{
			ConnectionString="Data Source=(localdb)\\Projects;Initial Catalog=
				TestDataBase;Integrated Security=True;Connect Timeout=30;
		};
		_conn.Open();
		return true;
	}Encrypt=False:
	public void Executesql (string sql)
	{
		var cmd =new Sqlcommand(sql, _conn);
		cmd.ExecuteNonQuery();
	}
	public List<Person> Getperson(string sql)
	{
		var person= new List<Person>();
		var cmd =new SqlCommand(sql,_conn);
		var sdr=cmd.Executereader();
		while(sdr Read())
		{
			person.Add(new Person
			{
				ID= sdr.Getint32(0),
				Name= sdr.Getstring(1),
				Age= sdr.Getint32(2),
				sex= sdr.Getstring(3)
			});
		}
		return person
	}
	public void Dispose()
	{
		_conn.Close();
		_conn=null;
	}
}
public class Person
{
	public int ID {get; set;}
	public string Name {get: set}
	public int Age {get: set:}
	public string Sex{get;set;}
}

现在我们可以连接数据库并传入SQL,获得结果。我们剩下的工作是传入表达树,并将表达式树解析为SQL。我们知道 LINQ to Entities拿回来的数据是 DbSet,它继承 IQueryable,因此,我们需要自己实现一个类继承 IQueryable,并建立一个实例,使得编译器改走我们的解析路线。

14.5.2 实现IQueryable
首选,我们自建一个类别Mylqueryable,继承 IQueryable。因为 IQueryable继承了IEnumerable,所以我们一样要实现GetEnumerator方法( IQueryable自身没有任何其他的方法需要实现,是一个空的接口)。只有当表达式需要计算时,才会调用
GetEnumerator方法(例如纯 Select就不会),这是延迟执行的特点,之前已经讨论过。然后,我门为IQueryable添加3个属性。
口 Expression:这个很好理解,就是要处理的表达式
口 Type:T的类型。
口 IQueryprovider:你自已的 Iquery Provider。在构造函数中,需要传人自己的IQueryprovider来实现自己的逻辑。
代码如下:

class Program
{
	static void Main(string[] args)
	{
		using(var context=new NorthwindEntities())
		{
			//这是 Dbset< customers>,继承了 IQueryable<Customers>
			DbSet<Customers> customer= context.Customers;
			var list =customer.Where(c=> C Address Contains("s"));
			//这是自己的 My Iqueryable<Customers
			var a= new MyIQueryable<Customers>();
			a.Where(t=>t Address Contains("s"));
		}
		
		Console.ReadKey();
	}
}
public class MyIQueryable<T>: IQueryablest<T>
{
	public IEnumerable<T> Get Enumerator()
	{
		throw new Not Implementedexception();
	}
	IEnumerator Ienumerable. GetEnumerator()
	{
		return GetEnumerator();
	}
	public Expression Expression {get: private set;}
	public Type ElementType{get, private set:}
	public IQueryProvider Provider {get: private set;}
	
/	//不知道该写什么的构造函数
	public Myiqueryable()
	{
	
	}
}

如果现在运行程序,将会报错,因为我们的 MyIQueryable的所有属性都是null,类型没有意义。我们需要实现构造函数和 Getenumerator方法,并提供属性的值。
14.5.3实现 IQueryProvider
IQueryable必须有 IQueryProvider オ能工作。所以我们要写一个 IQueryProvider ,然后在构造函数中传入。我们再次新建一个类型,继承 Iquery Provider,此时我们又需要实现4个方法,其中非泛型版本的两个方法可以暂时不用理会。代码如下:

public class MyQueryProvider: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();
	}
}

IQueryProvider将会做如下事情:
口 CreateQuery建立一个查询,但不计算,只在需要的时候才进行计算。
口 如果需要执行表达式的计算(例如调用了 ToList),此时调用 IQueryable的Getenumerator,它调用 Execute计算表达式。所以我们需要把自己的逻辑写在Execute方法中,并在 Getenumerator中进行调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值