第10章 LINQ to ADO.NET
简单地讲,可以把LINQ to ADO.NET分成三种独立的ADO.NET LINQ技术,即LINQ to DataSet、LINQ to SQL和LINQ to Entities。其中,LINQ to DataSet提供对DataSet的更为丰富的优化查询;使用LINQ to SQL可以直接查询SQL Server数据库架构;而使用LINQ to Entities可以查询Entity Data Model。总之,通过LINQ to ADO.NET,可以在ADO.NET中使用LINQ编程模型来查询任何可枚举对象。
10.1 LINQ to SQL
LINQ to SQL提供用于将关系数据作为对象进行管理的运行时基础结构。在LINQ to SQL中,关系数据库的数据模型映射到用开发人员所用的编程语言表示的对象模型。当执行应用程序时,LINQ to SQL会将对象模型中的语言集成查询转换为SQL,然后将它们发送到数据库进行执行。当数据库返回结果时,LINQ to SQL会将它们转换回可以操作的对象。
除此之外,LINQ to SQL还包括对数据库中存储过程和用户定义的函数的支持,以及对对象模型中继承的支持。
10.1.1 DataContext类
DataContext又称为数据上下文,它为LINQ to SQL提供操作数据库的入口。如果使用LINQ to SQL操作数据库,则首先需要为该数据库创建一个继承于DataContext类的自定义的数据上下文类,并在该类中定义表,以及操作数据的方法等。
1.DataContext类概述
DataContext类是一个LINQ to SQL类,它充当SQL Server数据库与映射到该数据库的LINQ to SQL实体类之间的管道,它包含用于连接数据库以及操作数据库数据的连接字符串信息和方法。DataContext类能够通过数据库连接或连接字符串来映射数据库中的所有实体的源,并跟踪和标识用户对数据库的更改。用户可以调用其SubmitChanges()方法将所有更改提交到数据库。DataContext类提供了4个构造函数。具体说明如下:
1)public DataContext(IDbConnection connection)。使用连接对象创建DataContext类的实例。如果提供了已关闭的连接或连接字符串,则DataContext会根据需要打开和关闭数据库连接。通常情况下,决不要对DataContext调用Dispose。如果提供打开的连接,则DataContext将不关闭该连接。因此,不要实例化具有打开的连接的Dat aCo ntext,除非有充分的理由执行该操作。在System.Transactions事务中,DataContext将不打开或关闭连接以免提升。
2)public DataContext(string fileOrServerOrConnection)。使用连接字符串、数据库所在的服务器的名称(将使用默认数据库)或数据库所在文件的名称创建DataContext类的实例。
3)public DataContext(IDbConnection connection, MappingSource mapping)。使用连接对象和映射源创建DataContext类的实例。
4)public DataContext(string fileOrServerOrConnection, MappingSource mapping)。使用连接字符串、数据库所在的服务器的名称(将使用默认数据库)或数据库所在文件的名称和映射源创建DataContext类的实例。
下面的示例演示了如何使用DataContext类编写自己的EmployeeDataContext类。
前面已经讲过,利用LINQ to SQL从数据库中获取信息时,这些信息被从表中的一组记录转换为内存中的一组对象,这些转换步骤是LINQ to SQL的核心。因此,要使用LINQ to SQL,首先就需要创建一个数据实体类,如代码清单10-1所示。
代码清单10-1 EmployeeEntity.cs
using System;
using System.Data.Linq.Mapping;
namespace Test
{
[TableAttribute(Name="dbo.Employee")]
public partial class EmployeeEntity
{
private decimal_employeeid;
private string_employeename;
private string_department;
private string_address;
private string_email;
private System.Nullable<System.DateTime>_workdate;
[ColumnAttribute(Storage="_employeeid",
IsPrimaryKey=true,
DbType="Decimal(18,0)NOT NULL")]
public decimal employeeid
{
get{return this._employeeid;}
set
{
if(((tis._employeeid!=value))
{
this._employeeid=value;
}
}
}
[ColumnAttribute(Storage="_employeename",
DbType="VarChar(100)NOT NULL",CanBeNull=false)]
public string employeename
{
get{return this._employeename;}
set
{
if(((tis._employeename!=value))
{
this._employeename=value;
}
}
}
[ColumnAttribute(Storage="_department",
DbType="VarChar(100)")]
public string department
{
get{return this._department;}
set
{
if(((tis._department!=value))
{
this._department=value;
}
}
}
[ColumnAttribute(Storage="_address",
DbType="VarChar(200)")]
public string address
{
get{return this._address;}
set
{
if(((tis._address!=value))
{
this._address=value;
}
}
}
[ColumnAttribute(Storage="_email",
DbType="VarChar(200)")]
public string email
{
get{return this._email;}
set
{
if(((tis._email!=value))
{
this._email=value;
}
}}
[ColumnAttribute(Storage="_workdate",
DbType="DateTime")]
public System.Nullable<System.DateTime>workdate
{
get{return this._workdate;}
set
{
if(((tis._workdate!=value))
{
this._workdate=value;
}
}
}
public EmployeeEntity()
{
}
}
}
在代码清单10-1中,System.Data.Linq.Mapping命名空间包含用于生成表示关系数据库的结构和内容的LINQ to SQL对象模型的类。其中,TableAttribute类可以将某个类指定为与数据库表相关联的实体类。它有两个属性,Name属性用于获取或设置表或视图的名称,而TypeId属性用于当在派生类中实现时,获取该Attribute的唯一标识符。
ColumnAttribute类可以将类与数据库表中的列相关联,它的常用属性如表10-1所示。
创建数据实体类EmployeeEntity之后,接下来还需要创建一个EmployeeDataContext类。该类继承于DataContext类,如代码清单10-2所示。
代码清单10-2 EmployeeDataContext.cs
using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Data;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.Linq.Expressions;
using System.ComponentModel;
namespace Test
{
[DatabaseAttribute(Name="ASPNET4")]
public partial class EmployeeDataContext:DataContext
{
private static MappingSource mappingSource=new
AttributeMappingSource();
partial void OnCreated();
public EmployeeDataContext():base(global:System.
Configuration.ConfigurationManager.ConnectionStrings
["ConnectString"].ConnectionString, mappingSource)
{
OnCreated();
}
public EmployeeDataContext(string connection):
base(connection, mappingSource)
{
OnCreated();
}
public EmployeeDataContext(IDbConnection connection):
base(connection, mappingSource)
{
OnCreated();
}
public EmployeeDataContext(string connection,
MappingSource mappingSource):base(connection, mappingSource)
{
OnCreated();
}
public EmployeeDataContext(IDbConnection connection,
MappingSource mappingSource):base(connection, mappingSource)
{
OnCreated();
}
public Table<EmployeeEntity>Employee
{
get{return this.GetTable<EmployeeEntity>();}
}
}
}
最后,它的数据库连接字符串与ADO.NET的数据库连接字符串的配置格式一样。如下面的代码所示:
<connectionStrings>
<add name="ConnectString"connectionString="
server=.;database=ASPNET4;uid=sa;pwd=mawei;"
providerName="System.Data.SqlClient"/>
</connectionStrings>
2.DataContext类的属性
上面创建好DataContext类的派生类EmployeeDataContext之后,接下来继续阐述如何操作该类的常用属性。
(1)连接属性Connection
Connection属性可以获取DataContext类的实例的连接(类型为DbConnection)。值得注意的是,用户获取该属性的值(即连接对象)之后,该连接对象的默认状态是关闭的。因此,用户如果要使用该连接对象,则需要显式打开该连接对象的状态。
下面的示例代码获取了EmployeeDataContext类的实例db的Connection属性的值。然后通过调用Open()方法打开该连接对象的状态,并显示该连接对象的属性(如数据库、数据源、服务器版本、状态等)的值:
protected void Page_Load(object sender, EventArgs e)
{
EmployeeDataContext db=new EmployeeDataContext();
using(DbConnection con=db.Connection)
{
con.Open();
//显示连接的信息
Response.Write("ConnectionString:"
+con.ConnectionString+"<br>");
Response.Write("ConnectionTimeout:"
+con.ConnectionTimeout.ToString()+"<br>");
Response.Write("Database:"+con.Database+"<br>");
Response.Write("DataSource:"+con.DataSource+"<br>");
Response.Write("ServerVersion:"
+con.ServerVersion+"<br>");
Response.Write("State:"+con.State.ToString()+"<br>");
}
}
示例运行结果如图10-1所示。
图 10-1 示例运行结果
(2)事务属性Transaction
Transaction属性为DataContext类的实例设置访问数据库的事务。其中,LINQ to SQL支持以下3种事务:
1)显式本地事务。调用Submit-Changes时,如果Transaction属性设置为((IbTransaction)事务,则在同一事务的上下文中执行SubmitChanges调用。成功执行事务后,要由你来提交或回滚事务。与事务对应的连接必须与用于构造DataContext的连接匹配。如果使用其他连接,则会引发异常。
2)隐式事务。当你调用SubmitChanges时,LINQ to SQL会检查此调用是否在Transaction的作用域内或者Transaction属性((IbTransaction)是否设置为由用户启动的本地事务。如果这两个事务均未找到,则LINQ to SQL启动本地事务((IbTransaction),并使用此事务执行所生成的SQL命令。当所有SQL命令均已成功执行完毕时,LINQ to SQL提交本地事务并返回。
3)显式可分发事务。可以在活动Transaction的作用域中调用LINQ to SQL API(包括但不限于SubmitChanges)。LINQ to SQL检测到调用是在事务的作用域内,因而不会创建新的事务。在这种情况下,LINQ to SQL还会避免关闭连接。可以在此类事务的上下文中执行查询和SubmitChanges操作。
除此之外,还可以在LINQ to SQL中使用TransactionScope类封闭提交到数据库的数据。前面已经讲过,TransactionScope类可以将普通代码创建为事务性代码。如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
using(TransactionScope ts=new TransactionScope())
{
……
}
(3)执行命令的最大时间属性CommandTimeout
CommandTimeout属性可以设置或获取DataContext类的实例的查询数据库操作的超时期限,该时间的单位为秒。未设置此属性时,会使用CommandTimeout的默认值执行查询命令,默认值为30秒。
但有时查询数据库操作可能需要很长的时间。此时,则需要增大该属性的值,以保证查询数据库的操作能够完成。设置示例如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
db.CommandTimeout=120;
(4)冲突对象集合属性ChangeConflicts
ChangeConflicts属性返回DataContext类的实例调用SubmitChanges()方法时导致并发冲突的对象的集合。如果要检测并发操作发生冲突,则在调用SubmitChanges()方法时需要设置报告冲突的方式。该方式由ConflictMode枚举指定,它包含以下两个枚举值:
❑FailOnFirstConflict:当检测到第一个并发冲突错误时,则立即中止更新数据库的操作;
❑ContinueOnConflict:当检测到并发冲突错误时,不立即中止更新数据库的操作,而是执行所有更新数据库的操作之后,最终返回该更新过程中所有并发冲突。
值得注意的是,只有在并发操作发生冲突时,ChangeConflicts属性的值才不为空。其中,ChangeConflicts属性返回并发冲突的对象的集合,每一个冲突对象的类型为ObjectChangeConflict。该类型包含以下4个属性:
❑Object:发生冲突的对象;
❑MemberConflicts:导致更新失败的所有成员冲突的集合;
❑IsDeleted:表示是否已从数据库中删除发生冲突的对象的值;
❑IsResolved:表示是否已解决此对象的冲突的值。
一旦检测到并发冲突的对象之后,可以把该对象转换为数据库中对应的表,并读取该表的相应信息。如下面的示例代码所示:
protected void Page_Load(object sender, EventArgs e)
{
EmployeeDataContext db=new EmployeeDataContext();
//添加一个新员工
EmployeeEntity emp=new EmployeeEntity();
emp.employeeid=12;
emp.employeename="mawei";
emp.department="软件研发部";
emp.address="陕西西安";
emp.email="mawei@hotmail.com";
emp.workdate=System.DateTime.Now;
db.Employee.InsertOnSubmit(emp);
try
{//提交更改到数据库
db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch(ChangeConflictException ex)
{
Response.Write("提交数据库时发生错误,原因如下:<br/>"
+ex.Message);
//显示冲突信息
foreach(ObjectChangeConflict occ in db.ChangeConflicts)
{
MetaTable mt=db.Mapping.GetTable(occ.Object.GetType());
EmployeeEntity employee=((EployeeEntity)occ.Object;
Response.Write("表名称:"+mt.TableName+"<br/>");
Response.Write("员工名称:"+employee.employeename
+"<br/>");
}
}
}
(5)是否延时加载关系属性DeferredLoadingEnabled
DeferredLoadingEnabled属性可以设置或获取DataContext类的实例是否延时加载关系。有时,DataContext类的实例的查询操作可能要一次性加载多个表(这些表之间可能存在一对一或一对多的关系)的数据。如果DataContext类的实例一次性加载所有表的数据可能需要很长时间,为了减少用户的等待时间,可以把DataContext类的实例的DeferredLoadingEnabled属性的值设置为false,从而延时加载关系表的数据。如下面的示例代码所示:
EmployeeDataContext db=new EmployeeDataContext();
db.DeferredLoadingEnabled=false;
(6)数据导入选项属性LoadOptions
LoadOptions属性可以获取或设置与此DataContext关联的DataLoadOptions。DataLoadOptions类提供相关数据的立即加载和筛选的方式,即它提供两种方法以立即加载指定的相关数据。其中,LoadWith方法允许立即加载与主目标相关的数据;而AssociateWith方法允许筛选相关对象。更加详细的示例请参考10.1.2节。
(7)日志属性Log
Log属性可以将DataContext类的实例的SQL查询或命令显示在网页或控制台中。示例如下面的代码所示:
protected void Page_Load(object sender, EventArgs e)
{
EmployeeDataContext db=new EmployeeDataContext();
//输出查询日志
db.Log=Response.Output;
var result=from emp in db.Employee
where emp.employeeid<=4
select emp;
foreach(EmployeeEntity employee in result)
{
Response.Write("<hr/>员工名称:"
+employee.employeename+"<br/>");
}
}
示例运行结果如图10-2所示。
图 10-2 示例运行结果
3.DataContext类的方法
前文阐述了DataContext类的相关属性及其应用方法。接下来,继续阐述DataContext类的常用方法的使用。
(1)SubmitChanges()方法
前面已经提到,SubmitChanges()方法能够计算要插入、更新或删除的已修改对象的集,并执行相应的修改提交到数据库,并修改数据库。该方法的原型如下:
public void SubmitChanges()
public virtual void SubmitChanges(ConflictMode failureMode)
其中,failureMode参数指定提交失败时要采取的操作,有效参数包括FailOnFirstConflict与ContinueOnConflict。默认失败模式为FailOnFirstConflict。
(2)DatabaseExists()方法
DatabaseExists()方法可以检测指定的数据库是否存在,如果存在,则返回true,否则返回false。其实,它在检测指定的数据库时,将尝试打开DataContext类的实例指定的数据库的连接。如果打开连接成功,则返回true,否则返回false。示例如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
if(db.DatabaseExists())
{
Response.Write(db.Connection.Database+"数据库已经存在。");
}
else
{
db.CreateDatabase();
}
(3)CreateDatabase()方法
CreateDatabase()方法可以在DataContext类的实例的连接字符串指定的服务器上创建数据库。在创建数据库时,CreateDatabase()方法可以使用以下两种方法设置数据库的名称:
❑如果在连接字符串中已经标识了数据库的名称,则使用该连接字符串标识的名称作为数据库的名称;
❑如果DataContext类使用DatabaseAttribute属性通过Name属性指定了数据库的名称,则使用Name属性的值作为数据库的名称。
(4)DeleteDatabase()方法
DeleteDatabase()方法可以删除DataContext类的实例的连接字符串标识的数据库。示例如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
if(db.DatabaseExists())
{
db.DeleteDatabase();
}
(5)ExecuteCommand()方法
ExecuteCommand()方法能够执行指定的SQL语句,并通过该SQL语句来操作数据库。该方法的原型如下:
public int ExecuteCommand(string command,
params Object[]parameters)
其中,command参数表示要执行的SQL语句;而parameters参数指定SQL语句中参数的值,且参数的数量与SQL语句中的数量相等。在传递给命令的参数数组时,需要注意下面的行为:
❑如果数组中的对象的数目小于命令字符串中已标识的最大数,则会引发异常;
❑如果数组包含未在命令字符串中引用的对象,则不会引发异常;
❑如果任一参数为null,则该参数会转换为DBNull.Value。
ExecuteCommand()方法返回一个整数值,即该SQL语句修改记录的数量。下面的示例演示了一个带参数的SQL语句的执行情况。
EmployeeDataContext db=new EmployeeDataContext();
string sql="
update employee set employeename={0}where employeeid=1";
int result=db.ExecuteCommand(sql, new object[]{"mawei"});
Response.Write(result.ToString()+"条数据被修改。");
(6)ExecuteQuery()方法
ExecuteQuery()方法可以执行指定的SQL查询语句,并通过SQL查询语句检索数据,查询结果保存数据类型为IEnumerable或IEnumerable<TResult>的对象。该方法的原型如下:
public IEnumerable ExecuteQuery(
Type elementType,
string query,
params Object[]parameters
)
public IEnumerable<TResult>ExecuteQuery<TResult>(
string query,
params Object[]parameters
)
其中,query参数指定SQL查询语句;parameters参数指定SQL查询语句的参数,且参数的数量与SQL查询语句中的数量相等;elementType参数指定元素的数据类型。
ExecuteQuery()方法返回数据类型为IEnumerable或IEnumerable<TResult>的对象,TResult参数指定元素的数据类型。示例如下面的代码所示:
protected void Page_Load(object sender, EventArgs e)
{
EmployeeDataContext db=new EmployeeDataContext();
string sql="select*from employee";
//执行SQL语句,并获取数据
IEnumerable<EmployeeEntity>result=
db.ExecuteQuery<EmployeeEntity>((sl);
GridView1.DataSource=result;
GridView1.DataBind();
}
示例运行结果如图10-3所示。
图 10-3 示例运行结果
(7)GetCommand()方法
GetCommand()方法用于获取指定查询的执行命令的信息。示例如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
var result=from emp in db.Employee
where emp.employeeid<=4
select emp;
DbCommand cmd=db.GetCommand(result);
Response.Write("CommandText:<br/>"+cmd.CommandText+"<br/>");
Response.Write("CommandType:<br/>"+cmd.CommandType+"<br/>");
Response.Write("Connection:<br/>"+cmd.Connection+"<br/>");
Response.Write("CommandTimeout:<br/>"+cmd.CommandTimeout);
示例运行结果如图10-4所示。
图 10-4 示例运行结果
(8)GetTable()方法
GetTable()方法是一个非常重要的方法,它用于获取DataContext类的实例的表的集合。该方法的原型如下:
public ITable GetTable(Type type)
public Table<TEntity>GetTable<TEntity>()
where TEntity:class
其中,type参数指定返回对象的数据类型;而TEntity参数指定返回的表中元素的数据类型。
其实,早在EmployeeDataContext类里面就使用GetTable()方法来获取该类的实例的表的集合。如下面的代码所示:
public Table<EmployeeEntity>Employee
{
get
{
return this.GetTable<EmployeeEntity>();
}
}
除此之外,还可以像下面这样来调用它。示例代码如下所示:
EmployeeDataContext db=new EmployeeDataContext();
Table<EmployeeEntity>Emp=db.GetTable<EmployeeEntity>();
var result=from emp in Emp
where emp.employeeid<=4
select emp;
foreach(EmployeeEntity employee in result)
{
Response.Write("员工名称:"+employee.employeename+"<br/>");
}
10.1.2 延迟执行
在LINQ to SQL中,默认采用的模式就是延迟执行。所谓延迟执行,其实就是在获取对象本身时,并不会获取和其关联的其他对象,只有在访问其关联对象的时候,程序才会去加载关联对象的数据到内存中。这样的好处是程序不会在初次访问的时候,就加载大批量的数据,而是以一种延迟加载的方式进行处理。相对而言,对于系统和网络的性能开支会减小很多。因此。对于一个默认的LINQ to SQL查询,延迟加载就是其默认的设置。不过,在某些情况下,延迟加载并非完全“智能”,不但没有实现其本意,反而增大了网络流量和性能开支。
为了演示延迟执行,接下来,看这样一个例子。如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
db.Log=Response.Output;
var result=from emp in db.Employee
where emp.employeeid<=2
select emp;
foreach(EmployeeEntity employee in result)
{
Response.Write("<br/>员工名称:"+employee.employeename);
}
示例运行结果如图10-5所示。
如图10-5所示,输出的SQL看来还比较正常。下面再来改一下程序,即再增加一个foreach语句。如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
db.Log=Response.Output;
var result=from emp in db.Employee
where emp.employeeid<=2
select emp;
foreach(EmployeeEntity employee in result)
{
Response.Write("<br/>员工名称:"+employee.employeename);
}
Response.Write("<br/>");
foreach(EmployeeEntity employee1 in result)
{
Response.Write("<br/>员工邮箱:"+employee1.email);
}
示例运行结果如图10-6所示。
图 10-5 一个foreach语句示例运行结果
图 10-6 两个foreach语句示例运行结果
如图10-6所示,两个foreach语句会导致查询被执行两次,从而生成两个SQL语句。因此,这样的延迟执行如果使用不当,反而会导致各种程序效率问题,如果大量的数据绑定使得LINQ延迟执行,程序效率将会大大降低。
这时你或许会问,面对这样的问题我们应该怎样来处理呢?
其实,最简单的办法就是通过调用ToList、ToArray等方法直接执行LINQ查询,将查询结果缓冲在list变量中,从而可以避免LINQ延迟执行的效率问题。如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
db.Log=Response.Output;
var result=from emp in db.Employee
where emp.employeeid<=2
select emp;
var list=result.ToList<EmployeeEntity>();
foreach(EmployeeEntity employee in list)
{
Response.Write("<br/>员工名称:"+employee.employeename);
}
Response.Write("<br/>");
foreach(EmployeeEntity employee1 in list)
{
Response.Write("<br/>员工邮箱:"+employee1.email);
}
示例运行结果如图10-7所示。
图 10-7 使用ToList示例运行结果
当然,也可以通过设置DataContext类的DeferredLoadingEnabled属性为false,来显式地关闭默认的延迟加载方式。
上面的这些方法虽然比较方便,但是也有一定的局限性。例如,简单地使用ToList只能解决一些简单的查询问题,而对于复杂的查询需求,ToList还是不能解决延迟取得子对象所引发的多次查询问题。并且,在大量数据被加载到内存中的时候,对内存的需求也是很大的。这时,就需要采用另外一种方法,即使用DataLoadOptions实现对加载对象的优化。
其中,使用DataLoadOptions的LoadWith方法指定应同时检索与主目标相关的哪些数据。例如,如果知道所需要的有关员工信息,则可以使用LoadWith来确保在检索员工信息的同时检索员工工资信息。使用此方法可仅访问一次数据库,但同时获取两组信息。
如下面的示例中,通过设置DataLoadOptions,来指示DataContext在加载Employee的同时把对应的Salary一起加载,在执行查询时会检索“Empolyeeid<=3”的所有Employee的所有Salary。这样,连续访问Employee对象的Salaries属性不会触发新的数据库查询。在执行时生成的SQL语句使用了左连接。如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
db.Log=Response.Output;
DataLoadOptions dl=new DataLoadOptions();
dl.LoadWith<Employee>((ep=>emp.Salaries);
db.LoadOptions=dl;
var result=((fomcin db.Employees
where c.employeeid<=3
select c);
foreach(var employee in result)
{
Response.Write("<hr/>");
foreach(var sal in employee.Salaries)
{
Response.Write("员工姓名:"+employee.employeename
+"——工资:"+sal.salary+"<br/>");
}
}
示例运行结果如图10-8所示。
图 10-8 LoadWith示例运行结果
而使用DataLoadOptions的AssociateWith方法可以指定子查询以限制检索的数据量,但它会生成很多SQL语句。如下面的示例代码所示:
EmployeeDataContext db=new EmployeeDataContext();
db.Log=Response.Output;
DataLoadOptions dl=new DataLoadOptions();
dl.AssociateWith<Employee>
((ep=>emp.Salaries.Where(s=>s.salary<4000));
db.LoadOptions=dl;
var result=((fomcin db.Employees
where c.employeeid<=3
select c);
foreach(var employee in result)
{
Response.Write("<hr/>");
foreach(var sal in employee.Salaries)
{
Response.Write("<hr/>");
Response.Write("员工姓名:"+employee.employeename
+"——工资:"+sal.salary+"<br/>");
}
}
示例运行结果如图10-9所示。
图 10-9 AssociateWith示例运行结果
10.1.3 自动生成数据类
其实,对于上面的数据类与DataContext派生类,Visual Studio提供了自动生成的功能。为了在Visual Studio里生成数据类,首先需要为应用程序添加一个DBML(DataBase Markup Language,数据库标记语言)文件。其添加方法很简单,用鼠标右击项目,选择“Add”/“New Item”,然后选择“LINQ to SQL Classes”,并在Name文本框里设置好名字(如Employee.dbml),最后单击“Add”按钮即可。
创建好Employee.dbml文件之后,Visual Studio会自动生成另外两个文件。如下所示:
❑Employee.dbml:该XML文件定义数据库某部分的架构;
❑Employee.dbml.layout:该XML文件定义每个表在数据库图表设计界面的布局;
❑Employee.designer.cs:这个C#代码文件包含了自动生成的数据类。
如图10-10所示,在Employee.dbml设计界面里,可以手动添加类或者直接从“服务器资源管理器”里将表拖入设计器来自动创建数据类。
同时,在这里也可以设置表与表之间的依赖关系。通过“属性”窗口,还可以修改属性的一些细节问题,如Name、Access、Read Only与Type等。
做好这些设置之后,打开Employee.designer.cs文件,会发现Visual Studio已经自动生成好了数据类与DataContext派生类,如图10-11所示。
1.数据类
打开Employee.designer.cs文件,会发现Visual Studio生成的数据类与上面自己创建的数据类大致相同。如下面的示例代码所示:
图 10-10 为DBML文件构建图表
图 10-11 自动生成的数据类
[global:System.Data.Linq.Mapping.TableAttribute(
Name="dbo.Employee")]
public partial class Employee:INotifyPropertyChanging,
INotifyPropertyChanged
{
private static PropertyChangingEventArgs
emptyChangingEventArgs=new
PropertyChangingEventArgs(String.Empty);
private decimal_employeeid;
private string_employeename;
……
private EntitySet<Salary>_Salaries;
region Extensibility Method Definitions
partial void OnLoaded();
partial void OnValidate(System.Data.Linq.ChangeAction action);
partial void OnCreated();
……
endregion
public Employee()
{
this._Salaries=new EntitySet<Salary>(
new Action<Salary>((tis.attach_Salaries),
new Action<Salary>((tis.detach_Salaries));
OnCreated();
}
[global:System.Data.Linq.Mapping.ColumnAttribute(
Storage="_employeeid",DbType="Decimal(18,0)NOT NULL",
IsPrimaryKey=true)]
public decimal employeeid
{
get{return this._employeeid;}
set
{
if(((tis._employeeid!=value))
{
this.OnemployeeidChanging(value);
this.SendPropertyChanging();
this._employeeid=value;
this.SendPropertyChanged("employeeid");
this.OnemployeeidChanged();
}
}
}
……
[global:System.Data.Linq.Mapping.AssociationAttribute(
Name="Employee_Salary",Storage="_Salaries",
ThisKey="employeeid",OtherKey="employeeid")]
public EntitySet<Salary>Salaries
{
get{return this._Salaries;}
set{this._Salaries.Assign(value);}
}
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void SendPropertyChanging()
{
if(((tis.PropertyChanging!=null))
{
this.PropertyChanging(this, emptyChangingEventArgs);
}
}
……
}
如上面的示例代码所示,相比之下,Visual Studio生成的数据类比编写的数据类在一些细节上更加详细。其中:
1)局部类声明。生成的数据类总是用一个partial关键字声明。这样,它们才可以和其他文件里相近的类定义合并。其实,如果要往数据类里添加自定义代码,就应该把这部分代码放到单独的文件里。只要在类的定义里包含partial关键字,代码就会和自动生成的代码合并为一个完整的类声明。这种方式确保了即使重新生成了数据类,字定义代码也不会被改动。
2)变更追踪。自动生成的数据类继承了INotifyPropertyChanging与INotifyPropertyChanged接口以支持变更追踪。当属性值发生变化时,相应的PropertyChanging与PropertyChanged事件就会通过这些接口触发。
2.派生的DataContext类
除了数据类Employee与Salary之外,Visual Studio还自动生成了DataContext类的派生类EmployeeDataContext。如下面的示例代码所示:
[global:System.Data.Linq.Mapping.DatabaseAttribute(
Name="ASPNET4")]
public partial class EmployeeDataContext:
System.Data.Linq.DataContext
{
private static System.Data.Linq.Mapping.MappingSource
mappingSource=new AttributeMappingSource();
region Extensibility Method Definitions
partial void OnCreated();
partial void InsertEmployee(Employee instance);
partial void UpdateEmployee(Employee instance);
partial void DeleteEmployee(Employee instance);
partial void InsertSalary(Salary instance);
partial void UpdateSalary(Salary instance);
partial void DeleteSalary(Salary instance);
endregion
public EmployeeDataContext():base(global:
System.Configuration.ConfigurationManager.ConnectionStrings
["ASPNET4ConnectionString"].ConnectionString, mappingSource)
{
OnCreated();
}
public EmployeeDataContext(string connection):
base(connection, mappingSource)
{
OnCreated();
}
public EmployeeDataContext(System.Data.IDbConnection
connection):base(connection, mappingSource)
{
OnCreated();
}
public EmployeeDataContext(string connection,
System.Data.Linq.Mapping.MappingSource mappingSource):
base(connection, mappingSource)
{
OnCreated();
}
public EmployeeDataContext(System.Data.IDbConnection
connection, System.Data.Linq.Mapping.MappingSource
mappingSource):base(connection, mappingSource)
{
OnCreated();
}
public System.Data.Linq.Table<Employee>Employees
{
Get{return this.GetTable<Employee>();}
}
public System.Data.Linq.Table<Salary>Salaries
{
get{return this.GetTable<Salary>();}
}
}
相比于编写的DataContext派生类,EmployeeDataContext类为了提高扩展性,还额外定义了一些没有代码的局部方法。如:
partial void InsertEmployee(Employee instance);
partial void UpdateEmployee(Employee instance);
partial void DeleteEmployee(Employee instance);
partial void InsertSalary(Salary instance);
partial void UpdateSalary(Salary instance);
partial void DeleteSalary(Salary instance);
可以在自己的局部类声明中定义这些方法以便于插入各类操作。例如,可以在记录被插入、更新或者删除时加入自己的代码。
10.1.4 处理关系
如上面的Employee.dbml文件所示,LINQ to SQL通过EntitySet集合与AssociationAttribute属性来处理表与表之间的关系。如下面的示例代码所示:
private EntitySet<Salary>_Salaries;
public Employee()
{
this._Salaries=new EntitySet<Salary>(
new Action<Salary>((tis.attach_Salaries),
new Action<Salary>((tis.detach_Salaries));
OnCreated();
}
[global:System.Data.Linq.Mapping.AssociationAttribute(
Name="Employee_Salary",Storage="_Salaries",
ThisKey="employeeid",OtherKey="employeeid")]
public EntitySet<Salary>Salaries
{
get
{
return this._Salaries;
}
set
{
this._Salaries.Assign(value);
}
}
其中,EntitySet为LINQ to SQL应用程序中的一对多关系和一对一关系的集合方提供延迟加载和关系维护。即如果希望Employee对象保持Salary对象的集合,那么该集合必须是EntitySet类的实例。
而LINQ to SQL定义了AssociationAttribute属性来帮助表示此类关系。AssociationAttribute属性与EntitySet<TEntity>和EntityRef<TEntity>类型一起使用,来表示将作为数据库中的外键关系的内容。AssociationAttribute的属性如表10-2所示。
定义好表与表之间的关系之后,就可以这样来进行访问了。如下面的代码所示:
protected void Page_Load(object sender, EventArgs e)
{
EmployeeDataContext db=new EmployeeDataContext();
StringBuilder str=new StringBuilder();
foreach(Employee emp in db.Employees)
{
str.Append(emp.employeename);
foreach(Salarysin emp.Salaries)
{
str.Append("-");
str.Append(s.salary);
}
str.Append("<hr/>");
}
Response.Write(str);
}
在上面的代码中,需要说明的是db.Employees只能够获取Employee的信息,并不能够获取Salary的信息。相反,因为LINQ toSQ L使用了延迟加载技术,每当访问emp.Salaries属性的时候,LINQ to SQL才会执行一个新查询去获取当前Employee相关的Salary信息。示例运行结果如图10-12所示。
图 10-12 示例运行结果
当然,也可以通过DataLoadOptions来预加载Employee相关的Salary信息。如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
DataLoadOptions dl=new DataLoadOptions();
dl.LoadWith<Employee>((ep=>emp.Salaries);
db.LoadOptions=dl;
除此之外,还可以使用EntityRef来实现从子记录导航到父记录。如下面的代码所示:
private EntityRef<Employee>_Employee;
public Salary()
{
this._Employee=default(EntityRef<Employee>);
OnCreated();
}
[global:System.Data.Linq.Mapping.AssociationAttribute(
Name="Employee_Salary",Storage="_Employee",
ThisKey="employeeid",OtherKey="employeeid",
IsForeignKey=true)]
public Employee Employee
{
get
{
return this._Employee.Entity;
}
set
{
Employee previousValue=this._Employee.Entity;
if((((peviousValue!=value)
||((tis._Employee.HasLoadedOrAssignedValue==false)))
{
this.SendPropertyChanging();
if(((peviousValue!=null))
{
this._Employee.Entity=null;
previousValue.Salaries.Remove(this);
}
this._Employee.Entity=value;
if(((vlue!=null))
{
value.Salaries.Add(this);
this._employeeid=value.employeeid;
}
else
{
this._employeeid=default(decimal);
}
this.SendPropertyChanged("Employee");
}
}
}
现在,就可以这样来加载它了。如下面的代码所示:
EmployeeDataContext db=new EmployeeDataContext();
DataLoadOptions dl=new DataLoadOptions();
dl.LoadWith<Salary>((sl=>sal.Employee);
db.LoadOptions=dl;
10.1.5 使用存储过程
其实,利用DBML设计器还可以很方便地生成调用数据库存储过程的方法。其添加方法很简单,如图10-13所示。
图 10-13 添加AddEmployee存储过程
在图10-13中,只需要将相关的存储过程(如AddEmployee)拖入设计器中,Visual Studio便会自动在DataContext派生类里面生成相关的存储过程调用方法。如下面的代码所示:
[global:System.Data.Linq.Mapping.FunctionAttribute(
Name="dbo.AddEmployee")]
public int AddEmployee(
[global:System.Data.Linq.Mapping.ParameterAttribute(
DbType="VarChar(100)")]string employeename,
[global:System.Data.Linq.Mapping.ParameterAttribute(
DbType="VarChar(100)")]string department,
[global:System.Data.Linq.Mapping.ParameterAttribute(
DbType="VarChar(200)")]string address,
[global:System.Data.Linq.Mapping.ParameterAttribute(
DbType="VarChar(200)")]string email,
[global:System.Data.Linq.Mapping.ParameterAttribute(
DbType="Int")]ref System.Nullable<int>getsucceed)
{
IExecuteResult result=this.ExecuteMethodCall(this,
(((MthodInfo)((MthodInfo.GetCurrentMethod())),
employeename, department, address, email, getsucceed);
getsucceed=
(((Sstem.Nullable<int>)((rsult.GetParameterValue(4)));
return(((it)((rsult.ReturnValue));
}
10.1.6 插入、更新与删除操作
其实,与ADO.NET相比,使用DataContext对象进行数据库操作更加方便和简单。使用LINQ to SQL类进行数据插入的操作步骤如下:
1)创建一个新的数据类对象,并赋值。
2)将这个新对象通过InsertOnSubmit方法添加到与数据库中的目标表关联的LINQ to SQL Table集合。
3)使用SubmitChanges方法将更改提交到数据库。
插入示例如下面的代码所示:
public void InsertEmployee()
{
Employee emp=new Employee();
emp.employeeid=15;
emp.employeename="马伟15";
emp.department="软件研发部";
emp.workdate=System.DateTime.Now;
emp.email="madengwei@163.com";
EmployeeDataContext dc=new EmployeeDataContext();
//执行插入数据操作
dc.Employees.InsertOnSubmit(emp);
//执行更新操作
dc.SubmitChanges();
}
protected void Page_Load(object sender, EventArgs e)
{
InsertEmployee();
EmployeeDataContext db=new EmployeeDataContext();
GridView1.DataSource=db.Employees;
GridView1.DataBind();
}
示例运行结果如图10-14所示。
图 10-14 插入示例运行结果
与插入操作一样,LINQ to SQL对数据库中数据的修改也是非常简便的。其基本步骤如下所示:
1)查询数据库中要更新的行。
2)对得到的LINQ to SQL对象中的成员值进行所需的更改。
3)使用SubmitChanges方法将更改提交到数据库。
修改示例如下面的代码所示:
public void UpdateEmployee()
{
EmployeeDataContext dc=new EmployeeDataContext();
var result=from emp in dc.Employees
where emp.employeeid>=8 select emp;
foreach(varein result)
{
e.department="市场部门";
}
dc.SubmitChanges();
}
示例运行结果如图10-15所示。
类似地,若要删除记录,可以通过DeleteOnSubmit与DeleteAllOnSubmit方法把需要删除的记录从Table集合中删除。同样,最后还需要使用SubmitChanges方法将更改提交到数据库。其中,DeleteOnSubmit只接受一个对象,而DeleteAllOnSubmit可以接受多个对象。
删除示例如下面的代码所示:
public void DeleteEmployee()
{
EmployeeDataContext dc=new EmployeeDataContext();
var result=from emp in dc.Employees
where emp.employeeid>=12
select emp;
foreach(varein result)
{
//执行删除操作
dc.Employees.DeleteOnSubmit(e);
dc.SubmitChanges();
}
}
示例运行结果如图10-16所示。
图 10-15 修改示例运行结果
图 10-16 删除示例运行结果