12.2 LINQ to Entities

对于LINQ to Entities相信大家并不陌生,前面的章节也已经多次提到。LINQ to Entities提供语言集成查询((LNQ)支持,它允许开发人员使用Visual Basic或者C#根据实体框架概念模型来编写查询。其中,LINQ to Entities将LINQ查询转换为命令目录树查询,针对实体框架执行这些查询,并返回可同时由实体框架和LINQ使用的对象。一般情况下,创建和执行LINQ to Entities查询的过程如下:

1)从ObjectContext构造ObjectQuery实例。ObjectQuery类实现了IQueryable泛型接口,它表示一个查询,此查询返回由零个或零个以上类型化实体组成的集合。对象查询通常从现有对象上下文中构造(而不是手动构造),并且始终属于该对象上下文。此上下文提供了编写和执行查询所需的连接和元数据信息。借助于IQueryable接口的生成器方法,能够以增量方式生成LINQ查询。当然,也可以使用var关键字让编译器推断实体的类型。

2)通过使用ObjectQuery实例在C#中编写LINQ to Entities查询。构造好ObjectQuery泛型类的实例之后,该实例可充当LINQ to Entities查询的数据源。现在,就可以使用查询表达式语法和基于方法的查询语法来确切指定要从数据源中检索哪些信息。

3)将LINQ标准查询运算符和表达式转换为命令目录树。若要针对实体框架执行LINQ to Entities查询,那么必须得先将LINQ查询转换为可针对实体框架执行的命令目录树表示形式。LINQ to Entities查询由LINQ标准查询运算符[如Select()、Where()和GroupBy()]和表达式(x>10、Contact.LastName等)组成。其中,LINQ运算符并非由类定义,而是由类中的方法定义;而表达式可包含System.Linq.Expressions命名空间内的类型所允许的任何内容。通过扩展,它还可以包含可在lambda函数中表示的任何内容。这是实体框架允许的表达式的超集,这些表达式在定义上限于数据库所允许的操作,并且受到ObjectQuery支持。

在实体框架中,运算符和表达式同时由单一类型层次结构表示,这些运算符和表达式随后会放入命令目录树中。实体框架使用命令目录树来执行此查询。如果LINQ查询无法表示为命令目录树,则当转换查询时,将引发异常。

4)对数据源执行命令目录树表示形式的查询。执行过程中在数据源上引发的任何异常都将直接向上传递到客户端。

5)将查询结果返回到客户端。

12.2.1 简单的对象查询处理

上面已经阐述过,ObjectQuery泛型类表示一个查询,此查询返回由零个或零个以上类型化实体组成的集合。同时,该类属于包含编写和执行查询所必需的连接和元数据信息的ObjectContext。因此,执行查询时,可以使用new运算符构造一个ObjectQuery实例,并将查询字符串和对象上下文传递到该构造函数。

当然,上面这种方法是非常麻烦的,更加直接的方法是使用ObjectContext派生类的属性获取表示实体集的集合的ObjectQuery实例。通常,通过由实体框架工具生成的类创建ObjectContext的子类,且对象上下文中的属性返回作为ObjectSet的实体集。在类型化实体集的上下文中,ObjectSet类扩展ObjectQuery类以提供功能。例如,添加和删除对象。默认的ObjectQuery提供返回指定类型的所有实体的起始查询。通过将LINQ to Entities或查询生成器方法可以进一步优化此查询。

如下面的示例对Employee集合的对象上下文进行查询,并返回集合中“department=="市场部门"”的结果。


protected void Page_Load(object sender, EventArgs e)

{

using(DataConnectString entities=new DataConnectString())

{

ObjectSet<Employee>employee=entities.Employee;

IQueryable<Employee>result=from emp in employee

where emp.department=="市场部门"

select emp;

foreach(variin result)

{

Response.Write(i.employeeid+"  "

+i.employeename+"  "+i.department);

Response.Write("<hr/>");

}

}

}


在上面的代码中,LINQ to Entities使用了基于查询表达式语法来执行查询。当然,同样可以选用基于方法的查询语法来达到同样的查询结果。如下面的代码所示:


using(DataConnectString entities=new DataConnectString())

{

IQueryable<Employee>result=entities.Employee

.Where(dep=>dep.department=="市场部门");

foreach(variin result)

{

Response.Write(i.employeeid+"  "

+i.employeename+"  "+i.department);

Response.Write("<hr/>");

}

}


示例运行结果如图12-9所示。

figure_0440_0329

图 12-9 示例运行结果

12.2.2 排序、分组与聚合数据

与普通的LINQ查询一样,同样可以利用LINQ to Entities对返回结果进行排序处理。如下面的示例演示了如何将结果按照employeeid关键字进行descending排序。


ObjectSet<Employee>employee=entities.Employee;

IQueryable<Employee>result=from emp in employee

where emp.department=="市场部门"

orderby emp.employeeid descending

select emp;


除了简单排序功能之外,可以进一步将结果做分组处理。在下面的示例中,将结果按照department进行分组:


protected void Page_Load(object sender, EventArgs e)

{

using(DataConnectString entities=new DataConnectString())

{

ObjectSet<Employee>employee=entities.Employee;

var result=(

from emp in employee

group emp by emp.department into empGroup

select new{key=empGroup.Key, Names=empGroup}).

OrderBy(dep=>dep.key);

foreach(var re in result)

{

Response.Write("<hr/>");

Response.Write("分组:"+re.key);

Response.Write("<hr/>");

foreach(variin re.Names)

{

Response.Write(i.employeeid+"  "

+i.employeename+"  "

+i.address+"  "

+i.email+"<br/>");

}

}

}

}


分组示例运行结果如图12-10所示。

figure_0441_0330

图 12-10 分组示例运行结果

同样,还可以使用聚合函数来处理结果。如下面的代码所示:


protected void Page_Load(object sender, EventArgs e)

{

using(DataConnectString entities=new DataConnectString())

{

ObjectSet<Salary>salary=entities.Salary;

var result=from sal in salary

group sal by sal.Employee.department into g

select new

{

部门=g.Key,

平均工资=g.Average(t=>t.total),

平均税款=g.Average(st=>st.salestax),

平均实际工资=g.Average(sal=>sal.salary1)

};

foreach(variin result)

{

Response.Write(i.部门);

Response.Write("<hr/>");

Response.Write(i.平均工资+"  "

+i.平均实际工资+"  "

+i.平均税款+"<br/>");

}

}

}


聚合示例运行结果如图12-11所示。

figure_0442_0331

图 12-11 聚合示例运行结果

12.2.3 调用在数据库中定义的自定义函数

在实体框架中,通过在.edmx文件的存储架构定义语言((SDL)中声明一个相关的自定义函数,就可以在LINQ to Entities查询中调用在数据库中定义的自定义函数。

下面先来创建一个数据库的自定义函数ConvertString,该函数接受一个英文字符串,并返回每个单词首字母大写的英文字符串。如下面的代码所示:


create function[dbo].[ConvertString]

——定义输入参数

@inputString varchar(2000)

——定义函数返回的类型

returns varchar(2000)

as

begin

——转换为小写字母

set@inputString=lower(@inputString)

——设置第一个字母大写

set@inputString=

stuff(@inputString,1,1,upper(substring(@inputString,1,1)))

——定义临时变量i,用做循环

declare@i int

set@i=1

——循环处理输入字符串

while@i<len(@inputString)

begin

——检查是否为单词的开始

if substring(@inputString,@i,1)=''

begin

——设置第一个字母大写

set@inputString=

stuff(@inputString,@i+1,1,

upper(substring(@inputString,@i+1,1)))

end

——循环变量增1

set@i=@i+1

end

——返回修改后的字符串

return@inputString

end


在数据库中定义好自定义函数ConvertString之后,需要在.edmx文件的存储架构定义语言((SDL)中声明刚刚定义好的函数ConvertString。如下面的代码所示:


<Function Name="ConvertString"ReturnType="varchar"

Aggregate="false"BuiltIn="false"NiladicFunction="false"

IsComposable="true"

ParameterTypeSemantics="AllowImplicitConversion"Schema="dbo">

<Parameter Name="inputString"Type="varchar"Mode="In"/>

</Function>


接下来,还需要在代码中创建一个ConvertString方法,并将其映射到在SSDL中声明的ConvertString函数上。可以通过使用EdmFunctionAttribute将类中的方法映射到在SSDL中定义的函数中。如下面的代码所示:


[EdmFunction("EmployeeModel.Store","ConvertString")]

public static string ConvertString(string str)

{

throw new NotSupportedException("

Direct calls are not supported.");

}


在代码中创建好ConvertString方法之后,通过在LINQ to Entities查询中调用此方法,就可以执行数据库中相应的自定义函数。如下面的代码所示:


protected void Page_Load(object sender, EventArgs e)

{

using(DataConnectString employeeContext=

new DataConnectString())

{

var result=from emp in employeeContext.Employee

join sal in employeeContext.Salary

on emp.employeeid

equals sal.employeeid

where emp.department=="软件研发部"

select new

{

姓名=emp.employeename,

部门=emp.department,

邮箱=ConvertString(emp.email),

总工资=sal.total,

应交税款=sal.salestax,

实际工资=sal.salary1

};

GridView1.DataSource=result;

GridView1.DataBind();

}

}


在上面的示例代码中,在LINQ to Entities查询中通过调用ConvertString(emp.email)方法将email的首个字母转换成大写。其示例运行结果如图12-12所示。

figure_0443_0332

图 12-12 示例运行结果

12.2.4 调用在数据库中定义的存储过程

除了自定义函数之外,实体数据模型((EM)还支持使用存储过程来检索和修改数据。如下面的GetEmployee存储过程将根据EmployeeID来获取Employee信息。


create procedure[dbo].[GetEmployee]

@EmployeeID int

as

select*from Employee

where EmployeeID=@EmployeeID


与上面的自定义函数一样,接下来,需要在.edmx文件的存储架构定义语言((SDL)的EntityContainer标记外部声明一个函数。如下面的代码在SSDL中声明了GetEmployee函数。


<Function Name="GetEmployee"Aggregate="false"

BuiltIn="false"NiladicFunction="false"

IsComposable="false"

ParameterTypeSemantics="AllowImplicitConversion"Schema="dbo">

<Parameter Name="EmployeeID"Type="int"Mode="In"/>

</Function>


在SSDL的EntityContainer标记外部声明好GetEmployee函数之后,就可以来实现概念架构定义语言((CDL)了,即将FunctionImport添加到CSDL段的EntityContainer中。如下面的代码所示:


<FunctionImport Name="GetEmployee"EntitySet="Employee"

ReturnType="Collection(EmployeeModel.Employee)">

<Parameter Name="EmployeeID"Type="Int32"Mode="In">

</Parameter>

</FunctionImport>


到目前为止,在CSDL中定义了方法、方法返回的实体类型以及返回的实体所属的EntitySet;而SSDL定义了存储过程。接下来,需要将CSDL映射到SSDL,以使概念方法了解要执行何种存储过程。要实现这种影射,通过将FunctionImportMapping插入映射规范语言((ML)的EntityContainerMapping部分即可,即在MSL中添加如下代码:


<FunctionImportMapping FunctionImportName="GetEmployee"

FunctionName="EmployeeModel.Store.GetEmployee"/>


现在,就可以在代码里使用这个GetEmployee存储过程了。使用示例如下面的代码所示:


protected void Page_Load(object sender, EventArgs e)

{

using(DataConnectString employeeContext=

new DataConnectString())

{

int employeeId=1;

foreach(Employee emp in

employeeContext.GetEmployee(employeeId))

{

Response.Write("EmployeeID:"+employeeId+"<br/>"

+emp.employeename+"  "

+emp.department+"  "

+emp.address+"  "

+emp.email);

}

}

}


示例运行结果如图12-13所示。

figure_0445_0333

图 12-13 示例运行结果