10.2 LINQ to DataSet

LINQ to DataSet主要是提供对离线数据的支持,当用数据填充完DataSet对象后,便可以开始查询该对象了。与LINQ的其他实现一样,可以用两种不同形式创建LINQ to DataSet查询:即查询表达式语法和基于方法的查询语法。

但这里还需要注意的是,在对DataSet对象使用LINQ查询时,所查询的是DataRow对象的枚举,而不是自定义类型的枚举。这意味着可以在LINQ查询中使用DataRow类的任意成员,它允许创建丰富而复杂的查询。

10.2.1 LINQ to DataSet概述

简单地讲,一个LINQ to DataSet查询包括以下几个步骤:

1)获取DataSet或者DataTable数据源。LINQ to DataSet是通过LINQ来查询DataSet或者DataTable中的数据,所以,首先就要准备好DataSet或者DataTable数据源。可以通过ADO.NET技术从数据库获取,可以通过XML技术从XML文件获取,也可以从其他任何形式的数据源获取,甚至可以在内存中直接创建并填充DataSet或者DataTable对象。

2)将DataTable转换成IEnumerable<T>类型。前面的章节已经阐述过,LINQ只能够在IEnumerable<T>或IQueryable<T>接口对象上执行查询操作,而DataTable并没有实现这两个接口,所以不能直接查询。在LINQ to DataSet中,可以通过DataTableExtensions扩展的AsEnumerable()方法从DataTable获取一个等价的IEnumerable<T>对象。

3)使用LINQ语法编写查询。LINQ to DataSet中查询的编写可以使用查询表达式语法和基于方法的查询语法,可以对它执行任何IEnumerable<T>允许的查询操作。

4)使用查询结果。查询结果产生后,就可以使用这些查询结果(一个IEnumerable<T>对象)。例如,用foreach遍历所有元素,用Max()等进行数值计算,将它作为数据源进行二次查询,等等。

下面的示例演示了一个简单的LINQ to DataSet查询。


protected void Page_Load(object sender, EventArgs e)

{

string sql="select*from employee";

DataTable dt=

DbHelper.Instance.CreateDataTable(CommandType.Text, sql);

IEnumerable<DataRow>result=

from emp in dt.AsEnumerable()

select emp;

foreach(DataRow dr in result)

{

Response.Write(dr.Field<string>("employeename"));

}

}


这里需要注意的是,由于DataSet本身是DataTable的集合,它可以包含一个或多个DataTable及它们之间的关系,LINQ to DataSet实际是对DataTable进行数据查询,并非对DataSet进行查询。

10.2.2 单表查询

我们知道,一个DataSet通常包含一个或多个DataTable,同时也包括它们之间的关系集合等。而在实际开发中,可以把DataSet看成是一个缩影的数据库。LINQ to DataSet也是对一个或多个DataTable进行查询,这些DataTable可以来自单个DataSet,也可以来自多个DataSet。

上面已经阐述过,LINQ查询只适用于实现IEnumerable<T>接口或IQueryable<T>接口的数据源。而DataTable类不实现任何一个接口,所以如果要使用DataTable作为LINQ查询的from子句中的源,则必须调用AsEnumerable方法。AsEnumerable方法将DataTable转换成一个类型为IEnumerable<DataRow>的可枚举数据集合。该方法的定义如下:


public static EnumerableRowCollection<DataRow>AsEnumerable(

this DataTable source)


然而,要从DataTable中获取的元素类型为DataRow,就需要进一步访问数据表的记录的具体字段数据,这时就需要使用DataRow的一个扩展泛型方法Field<T>,并通过Field<T>方法来获取DataRow的某字段的数据。Field<T>方法的定义如下:


public staticTField<T>((tis DataRow row, DataColumn column)

public staticTField<T>((tis DataRow row, int columnIndex)

public staticTField<T>((tis DataRow row, string columnName)

public staticTField<T>((tis DataRow row,

DataColumn column, DataRowVersion version)

public staticTField<T>((tis DataRow row,

int columnIndex, DataRowVersion version)

public staticTField<T>((tis DataRow row,

string columnName, DataRowVersion version)


其中,参数columnIndex表示从0开始的索引列索引,而columnName表示要返回数据的字段的名称。演示示例如下面的代码所示:


protected void Page_Load(object sender, EventArgs e)

{

string sql="select*from employee";

DataTable dt=

DbHelper.Instance.CreateDataTable(CommandType.Text, sql);

var result=from emp in dt.AsEnumerable()

where emp.Field<string>("department")=="软件研发部"

select new

{

employeename=emp.Field<string>("employeename"),

email=emp.Field<string>("email")

};

foreach(variin result)

{

Response.Write(i.employeename+"-"+i.email+"<hr/>");

}

}


在上面的代码中,首先通过CreateDataTable方法创建了一个DataTable。然后在查询result中通过dt.AsEnumerable()方法将DataTable转换成IEnumerable<T>类型的数据集合,并在查询中使用where子句来查询所有“软件研发部”的员工。示例运行结果如图10-17所示。

figure_0392_0290

图 10-17 单表查询示例运行结果

10.2.3 交叉表查询

除了单表查询之外,也可以在LINQ to DataSet中执行交叉表查询。它可以通过使用连接来完成,连接就是将一个数据源中的对象与另一个数据源中具有相同公共属性的对象相关联。在面向对象的编程中,由于每个对象都有引用另一个对象的成员,所以对象间的关系相对较容易导航。但在外部数据库表中,导航关系不像这样简单。数据库表不包含内置关系。在这些情况下,可以通过连接操作来匹配每个源中的元素。

LINQ提供两个连接运算符:Join和GroupJoin。这些运算符执行同等连接,即仅在键相等时匹配两个数据源的连接。其中:

1)Join实现内部连接,内部连接是仅返回在相对数据集中具有匹配对象的那些对象的一种连接类型。

2)GroupJoin运算符没有直接等效项,它们实现内部连接和左外部连接的超集。左外部连接是一种即使在第二个集合中没有关联元素的情况下也会返回第一个(左侧)集合中每个元素的连接。

演示示例如下面的代码所示:


protected void Page_Load(object sender, EventArgs e)

{

string employeeSql="select*from employee";

string salarySql="select*from salary";

DataTable employeedt=

DbHelper.Instance.CreateDataTable(CommandType.Text,

employeeSql);

DataTable salarydt=

DbHelper.Instance.CreateDataTable(CommandType.Text,

salarySql);

var result=from emp in employeedt.AsEnumerable()

join sal in salarydt.AsEnumerable()

on emp.Field<decimal>("employeeid")equals

sal.Field<decimal>("employeeid")

where emp.Field<string>("department")=="软件研发部"

select new

{

employeename=emp.Field<string>("employeename"),

email=emp.Field<string>("email"),

salary=sal.Field<decimal>("salary")

};

foreach(variin result)

{

Response.Write(i.employeename+"-"+i.email+"-"

+i.salary+"<hr/>");

}

}


在上面的代码中,使用了Join连接运算符将employee表与salary表的employeeid字段进行连接。示例运行结果如图10-18所示。

figure_0393_0291

图 10-18 交叉表查询示例运行结果

除此之外,同样可以使用下面的LINQ to DataSet方法来轻松地查询多个数据表中的数据。这通常需要使用多个from子句进行复合查询,同时通过where子句来进行多个表之间的关系判断。如下面的示例代码所示:


protected void Page_Load(object sender,

EventArgs e)

{

string employeeSql="select*from employee";

string salarySql="select*from salary";

DataTable employeedt=

DbHelper.Instance.CreateDataTable(CommandType.Text,

employeeSql);

DataTable salarydt=

DbHelper.Instance.CreateDataTable(CommandType.Text,

salarySql);

var result=from emp in employeedt.AsEnumerable()

from sal in salarydt.AsEnumerable()

where emp.Field<decimal>("employeeid")==

sal.Field<decimal>("employeeid")

&&emp.Field<string>("department")=="软件研发部"

select new

{

employeename=emp.Field<string>("employeename"),

email=emp.Field<string>("email"),

salary=sal.Field<decimal>("salary")

};

foreach(variin result)

{

Response.Write(i.employeename+"-"+i.email+"-"

+i.salary+"<hr/>");

}

}


其运行结果与图10-18所示相同。

10.2.4 用查询创建数据表

LINQ to DataSet通过DataTableExtensions类提供的扩展方法CopyToDataTable将从数据表中获取到的查询结果(类型为IEnumerable<DataRow>)直接复制到一个新的数据表((DtaTable)中,从而可以将查询结果绑定到界面控件(如GridView等),也可以使用一些DataTable特有的特性。在执行数据操作后,新的DataTable将合并回源DataTable。

CopyToDataTable方法使用下面的过程来通过查询创建DataTable:

1)CopyToDataTable方法克隆源表中的DataTable(实现IQueryable<T>接口的DataTable对象)。IEnumerable源通常来源于LINQ to DataSet表达式或方法查询。

2)克隆的DataTable的架构从源表中枚举的第一个DataRow对象的列生成,克隆表的名称是源表的名称后面追加单词“query”。

3)对于源表中的每一行,会将行内容复制到新DataRow对象中,然后将该对象插入到克隆表中。RowState和RowError属性在整个复制操作过程中保留。如果源中的ArgumentException对象来自不同的表,则会引发DataRow。

4)复制完可查询的输入表中的所有DataRow对象后,将返回克隆的DataTable。如果源序列不包含任何DataRow对象,则该方法将返回一个空DataTable。

当CopyToDataTable方法在源表的行中遇到空引用或可以为null的值类型时,它将用Value替换该值。这样可以在返回的DataTable中正确处理Null值。

如下面的示例代码所示:


protected void Page_Load(object sender, EventArgs e)

{

string employeeSql="select*from employee";

DataTable employeedt=

DbHelper.Instance.CreateDataTable(CommandType.Text,

employeeSql);

var query1=from emp in employeedt.AsEnumerable()

where emp.Field<string>("department")=="软件研发部"

select emp;

DataTable newDt=query1.CopyToDataTable<DataRow>();

GridView1.DataSource=newDt;

GridView1.DataBind();

}


示例运行结果如图10-19所示。

figure_0395_0292

图 10-19 示例运行结果

最后还需要说明的是,CopyToDataTable方法接受可从多个DataTable或DataSet对象返回行的查询作为输入。CopyToDataTable方法将数据(不包括属性)从源DataTable或DataSet对象复制到返回的DataTable。你将需要显式设置返回的DataTable的属性,如Locale和TableName。

10.2.5 修改表中字段数据

在前面的示例代码中,只使用了DataRowExtensions类的Field方法来获取数据表中字段的数据。其实在实际开发中,LINQ to DataSet有时也需要对数据表中的数据进行修改,这时就需要使用DataRowExtensions类的SetField方法来修改数据。SetField方法主要用于设置数据表中指定列的数据,并且指定明确的数据类型。该方法的定义如下所示:


public static void SetField<T>((tis DataRow row,

DataColumn column, T value)

public static void SetField<T>((tis DataRow row,

int columnIndex, T value)

public static void SetField<T>((tis DataRow row,

string columnName, T value)


其中,column是表示要设置数据的列对象((DtaColumn类型);columnIndex是从0开始的要设置数据的列索引;columnName是要设置数据的列名称。通常,一个数据表的列名是固定不变的,所以建议尽可能使用列名指定要设置数据的列,这样的代码更具扩展性与可读性。

示例如下面的代码所示:


protected void Page_Load(object sender, EventArgs e)

{

string employeeSql="select*from employee";

DataTable employeedt=

DbHelper.Instance.CreateDataTable(CommandType.Text,

employeeSql);

foreach(var row in employeedt.AsEnumerable())

{

decimal emp=row.Field<decimal>("employeeid");

row.SetField<decimal>("employeeid",emp+100);

}

GridView1.DataSource=employeedt;

GridView1.DataBind();

}


示例运行结果如图10-20所示。

figure_0396_0293

图 10-20 示例运行结果