当前位置: 首页 > 图文教程 > 网络编程 > ASP.NET > LINQ学习笔记:选取Select
选取
Select:使用给定的Lambda表达式转换每一个输入元素,对应SQL语法也是Select
SelectMany:转换输入元素, 然后连接各个结果子序列,对应SQL语法是INNER JOIN, LEFT OUTER JOIN, CORSS JOIN
对于LINQ to SQL查询, Select和SelectMany是最常用的连接构造方法, 而对于本地查询, Join和GroupJoin才是最有效的连接构造方法.
Select
参数列表:
源序列: IEnumerable
结果选择器: TSource => TResult或者(TSource,int) => TResult(LINQ to SQL不支持)
使用Select,你总是得到开始操作前相同的元素数量, 然而每一个元素是可以被任何数量的Lambda函数转换的.
以下的操作列出了电脑中安装的所有字体名称(来自于System.Drawing)
1: IEnumerable<string> query =
2:
3: from f in FontFamily.Families
4:
5: select f.Name;
6:
7: foreach (string name in query) Console.WriteLine (name);
在这个例子当中,select从句将一个FontFamily对象转换为它的名称.以下给出了一个相应的Lamdba表达式:
1: IEnumerable<string> query =
2:
3: FontFamily.Families.Select (f => f.Name);
Select语句经常与匿名类型一起使用:
1: var query =
2: from f in FontFamily.Families
3: select new
4: {
5: f.Name,
6: LineSpacing = f.GetLineSpacing (FontStyle.Bold)
7: };
有时,一个不含任何转换的选取会用于一个复合查询,并以select或者group语句作为结尾. 以下查询选择的字体都支持strikeout:
1: IEnumerable query =
2:
3: from f in FontFamily.Families
4:
5: where f.IsStyleAvailable (FontStyle.Strikeout)
6:
7: select f;
8:
9:
10:
11: foreach (FontFamily ff in query)
12:
13: Console.WriteLine (ff.Name);
索引选取
选择表达式可以接受一个可选的整数参数,这个参数指示输入序列中的每个元素的索引位置.以下例子只能在本地查询中工作:
1: string[] names = { “James”,“Derek”,“Harry”,“Mary”,“Jack” };
2:
3: IEnumerable<string> query = names
4:
5: .Select ((s,i) => i + “=” + s);
6:
7: foreach(var s in query)
8:
9: Console.WriteLine(s); //0=James, 1=Derek…
子查询与对象层级
你可以通过在select语句中嵌套一个子查询来创建一个对象层级.以下示例返回C:\LINQ下的所有目录, 并且每个元素都拥有一个包含各自目录文件的子集合:
1: DirectoryInfo[] dirs =
2:
3: new DirectoryInfo (@”C:\LINQ”).GetDirectories( );
4:
5: var query =
6:
7: from d in dirs
8:
9: where (d.Attributes & FileAttributes.System) == 0
10:
11: select new
12:
13: {
14:
15: DirectoryName = d.FullName,
16:
17: Created = d.CreationTime,
18:
19: Files =
20:
21: from f in d.GetFiles( )
22:
23: where (f.Attributes & FileAttributes.Hidden) == 0
24:
25: select new { FileName = f.Name, f.Length, }
26:
27: };
此查询里面部分可以被称为相关子查询.当一个子查询引用了外部查询的对象我们可以称它们是相关的-在这个例子中,它引用了d,代表了被枚举的目录.
如果是本地查询, 一个select里面的子查询会引起双延迟执行. 在我们的例子中,目录下的文件不会被选取直到内部foreach表达式被枚举执行.
LINQ to SQL当中的子查询和连接
子查询在LINQ to SQL当中同样可以很好的工作,他们可以被用于SQL风格的连接查询.以下的例子查询客户的名称以及它对应的采购订单:
1: var query =
2:
3: from c in dataContext.Customers
4:
5: select new
6:
7: {
8:
9: c.Name,
10:
11: Purchases =
12:
13: from p in dataContext.Purchases
14:
15: where p.CustomerID == c.ID && p.Price > 1000
16:
17: select new { p.Description, p.Price }
18:
19: };
此查询匹配了来自两个不同集合的对象,可以想象为是”join”.与传统的数据库连接不同的是它并不会将结果集转换为一个扁平的二维数组,而是将关系型数据映射到一个层级数据对象上.
以下是一个利用Customer上的Purchase关联属性的查询,结果与上述的例子一致,但语法则更加简要一点:
1: from c in dataContext.Customers
2:
3: select new
4:
5: {
6:
7: c.Name,
8:
9: Purchases = from p in c.Purchases
10:
11: where p.Price > 1000
12:
13: select new { p.Description, p.Price }
14:
15: };
两个查询都是类似于SQL当中的LEFT OUT JOIN,即我们将选择所有的客户列表,即使他们没有任何的采购订单. 如果要将其改变为INNER JOIN,及那些不包含符合条件采购订单的客户将被排除,要做到这点我们可以在Purchase集合上增加一个过滤条件:
1: from c in dataContext.Customers
2:
3: where c.Purchases.Any (p => p.Price > 1000)
4:
5: select new {
6:
7: c.Name,
8:
9: Purchases =
10:
11: from p in c.Purchases
12:
13: where p.Price > 1000
14:
15: select new { p.Description, p.Price }
16:
17: };
这个查询稍微有点混乱,因为我们编写了两次相同的判定(Price > 1000).通过let语句我们可以去掉这个重复:
1: from c in dataContext.Customers
2:
3: let highValueP = from p in c.Purchases
4:
5: where p.Price > 1000
6:
7: select new { p.Description, p.Price }
8:
9: where highValueP.Any()
10:
11: select new { c.Name, Purchases = highValueP };
这种风格的查询的非常灵活的,例如通过将Any改为Count,我们可以改写此查询去选择那些包含至少两个采购订单的客户:
1: from c in dataContext.Customers
2:
3: let highValueP = from p in c.Purchases
4:
5: where p.Price > 1000
6:
7: select new { p.Description, p.Price }
8:
9: where highValueP.Count() >= 2
10:
11: select new { c.Name, Purchases = highValueP };
填充到具体类型
如果你想要得到一个中间结果,将数据填充到匿名类型当中是非常有用,但是如果要将结果集发送给客户端,则会有一些问题,因为匿名类型只能在一个方法内作为本地变量存在.一个代替的方法就是使用具体类型作为数据载体,例如DataSet或者自定义的实体类.一个业务实体类就是一个包含某些自定义属性的类型,类似于那些标记了[Table]注解的LINQ to SQL类型,它们被设计用于隐藏数据库的细节.我们可能在实体类中排除外键字段,例如,假设我们编写了叫做CustomerEntity和PurcahseEntity的实体类,以下示例显示我们如何运用实体类来搭载数据:
1: IQueryable query =
2:
3: from c in dataContext.Customers
4:
5: select new CustomerEntity
6:
7: {
8:
9: Name = c.Name,
10:
11: Purchases = (
12:
13: from p in c.Purchases
14:
15: where p.Price > 1000
16:
17: select new PurchaseEntity
18:
19: {
20:
21: Description = p.Description,
22:
23: Value = p.Price
24:
25: }
26:
27: ).ToList( )
28:
29: };
30:
31:
32:
33: List result = query.ToList();
到目前为止,我们并没有使用Join或者SelectMany,这是因为我们正在维护的是一个层级数据.使用LINQ,我们经常可以比避免使用传统的SQL途径来生成二维数据结果集.
SelectMany
参数列表:
源序列: IEnumerable
结果选择器(Result Selector): TSource=>IEnumerabel 或者(TSource, int)=>IEnumerable(LINQ to SQL不支持)
概要:
SelectMany将多个子序列连接成一个扁平的输出序列.
对于每一个输入元素,Select生成一个输出元素,相反的,SelectMany则生成了0…n个的输出元素.这些0…n的元素来自于处理结果或者Lambda表达式产生的子序列
SelectMany可以被用于扩展子序列,平整嵌套集合,以及联合两个集合来生成一个扁平的输出序列. 假设我们有一个名称如下的名字列表:
1: string[] fullNames =
2:
3: { “Anne Williams”, “John Fred Smith”, “Sue Green” };
我们希望能够将它转换为一个扁平的单字结果集,如下所示:
“Anne”,“Williams”,“John”,“Fred”,“Smith”,“Sue”,“Green”
SelectMany对于这个任务是非常理想的,因为对于每一个输入元素我们将会得到对应的多个输出元素 . 我们需要做的就是提供一个selector表达式来将输入元素转化为一个输出子序列.String.Split()可以很好的完成一个工作:
1: string testInputElement = “Anne Williams”;
2:
3: string[] childSequence = testInputElement.Split( );
4:
5: // childSequence = { “Anne”, “Williams” };
完整示例如下:
1: IEnumerable<string> query =
2:
3: fullNames.SelectMany (name => name.Split( ));
4:
5: foreach (string name in query)
6:
7: Console.Write (name + “,”);
8:
9: // 结果: Anne,Williams,John,Fred,Smith,Sue,Green,
如果我们使用Select的话也可以使用如下的方法得到一样的结果:
1: IEnumerable<string[]> query =
2:
3: fullNames.Select (
4:
5: name => name.Split( ));
6:
7: foreach (string[] stringArray in query)
8:
9: foreach (string name in stringArray)
10:
11: Console.Write (name + “,”);
SelectMany的好处就是它生成了单一扁平的结果序列.
通过一个额外的生成符——额外的from语句可以让复合语法支持SelectMany.这个from关键字拥有两层意思,在查询开始的那一个引出了迭代变量和输入序列,查询中其他地方出现的from关键字将被翻译成SelectMany:
1: IEnumerable<string> query =
2:
3: from fullName in fullNames
4:
5: from name in fullName.Split()
6:
7: select name;
注意,第二个from引出了一个新的查询变量——这本例中,是name.从那时起,这个新的变量将会变成迭代变量,而原始的迭代变量将会变成外部迭代变量.
外部迭代变量
在我们前面的例子中,fullName在SelectMany之后变成了一个外部迭代变量. 这个外部迭代变量依然在可用范围内直到查询结束或者到达一个into从句:
1: IEnumerable<string> query =
2:
3: from fullName in fullNames // 外部变量
4:
5: from name in fullName.Split( ) // 迭代变量
6:
7: select name + ” came from “ + fullName;
8:
9: Anne came from Anne Williams
10:
11: Williams came from Anne Williams
12:
13: John came from John Fred Smith
如果要使用Lambda语法的话,尤其在有where或者orderby的情况下,会更加复杂一点,这里有一个技巧:
1: from fullName in fullNames
2:
3: from x in
4:
5: fullName.Split( )
6:
7: .Select (name => new { name, fullName } )
8:
9: orderby x.fullName, x.name
10:
11: select x.name + ” came from “ + x.fullName;
我们使用了一个匿名类型来搭载临时数据,类似let从句的作用.最后我们可以得到一个完整的Lambda语法的查询:
1: IEnumerable<string> query = fullNames
2:
3: .SelectMany (fName =>
4:
5: fName.Split( )
6:
7: .Select (name => new { name, fName } ))
8:
9: .OrderBy (x => x.fName)
10:
11: .ThenBy (x => x.name)
12:
13: .Select (x => x.name + ” came from “ + x.fName);
复合查询的思考
正如我们前面看到的例子,如果我们需要外部迭代变量, 那么使用复合语法是一个很好的选择,因为它的语义让你可以更加直观的思考.
编写额外的生成器主要有两个基础的模式, 第一个是扩展和平整结果,为了做到这一点,我们可以通过调用已有查询变量的属性或者方法来完成,上面的例子我们就使用了这个方法.
在LINQ to SQL中一个类似的查询就是当你扩展关联的子属性,以下的实例列出了所有的客户和他们对应的采购单:
1: IEnumerable<string> query =
2:
3: from c in dataContext.Customers
4:
5: from p in c.Purchases
6:
7: select c.Name + ” bought a “ + p.Description;
8:
9: Tom bought a Bike
10:
11: Tom bought a Holiday
12:
13: Dick bought a Phone
14:
15: Harry bought a Car
另外一个模式就是执行一个交叉连接,为了做到这一点, 我们的selector表达式返回的将会是一个序列而不是一个迭代变量了:
1: int[] numbers = { 1, 2, 3 };
2:
3: string[] letters = { “a”, “b” };
4:
5: IEnumerable<string> query = from n in numbers
6:
7: from l in letters
8:
9: select n.ToString( ) + l;
10:
11: RESULT: { “1a”, “1b”, “2a”, “2b”, “3a”, “3b” }
这种风格的查询就是SelectMany风格关联的基础.
使用SelectMany做关联
我们可以使用SelectMany,并通过过滤一个交叉结果集来关联两个序列.例如, 假设我们要安排一场比赛的选手对决, 我们使用以下的查询:
1: string[] players = { “Tom”, “Jay”, “Mary” };
2:
3: IEnumerable<string> query =
4:
5: from name1 in players
6:
7: from name2 in players
8:
9: select name1 + ” vs “ + name2;
10:
11: RESULT: {“Tom vs Tom”, “Tom vs Jay”, “Tom vs Mary”,
12:
13: “Jay vs Tom”, “Jay vs Jay”, “Jay vs Mary”,
14:
15: “Mary vs Tom”, “Mary vs Jay”, “Mary vs Mary”}
但这并非我们期望的结果,很明显,选手自己并不需要与自己比赛,因此我们必须要过滤这些不符合条件的输出元素:
1: IEnumerable<string> query =
2:
3: from name1 in players
4:
5: from name2 in players
6:
7: where name1.CompareTo (name2) > 0
8:
9: orderby name1, name2
10:
11: select name1 + ” vs “ + name2;
12:
13: RESULT: { “Jay vs Mary”, “Jay vs Tom”, “Mary vs Tom” }
此过滤断言构成了连接条件,我们的查询可以被称为非等连接,因为连接条件没有使用等式操作符.
LINQ to SQL当中的SelectMany
LINQ to SQL当中的SelectMany可以被用于执行交叉连接,非等连接,inner join以及left outer join.我们可以像使用Select一样使用SelectMany,不同的地方在于SelectMany返回的是一个扁平的数据结构而不是层级机构.
交叉连接就像我们上面例举的类似,以下的查询显示了每一个客户对应的每一个采购:
1: var query =
2:
3: from c in dataContext.Customers
4:
5: from p in dataContext.Purchases
6:
7: select c.Name + ” might have bought “ + p.Description;
更通常的情况是我们只想列出客户及其对应的采购单,为此,我们可以增加一个where语句和一个连接断言,结果将会得到一个类似SQL语法的查询:
1: var query =
2:
3: from c in dataContext.Customers
4:
5: from p in dataContext.Purchases
6:
7: where c.ID == p.CustomerID
8:
9: select c.Name + ” bought a “ + p.Description;
如果在你的LINQ to SQL实体类中存在关联属性,你可以使用它们来避免交叉连接:
1: from c in dataContext.Customers
2:
3: from p in c.Purchases
4:
5: select new { c.Name, p.Description };
这样写的好处是我们消除了关联断言,但结果是一致的.
我们还可以增加一个Where语句来做进一步的过滤,例如,我们只想列出那些名字以J开头的客户:
1: from c in dataContext.Customers
2:
3: where c.Name.StartsWith (“J”)
4:
5: from p in c.Purchases
6:
7: select new { c.Name, p.Description };
你也可以将where语句往下移一行,对于这个LINQ to SQL查询,结果是一致的.然后,如果是本地查询的话,这会得到一个相对差一些的性能.对于本地查询,我们总是应该使用before joining.
我们还可以通过增加from语句引出新的表,例如,每一个采购单都有采购明细:
1: from c in dataContext.Customers
2:
3: from p in c.Purchases
4:
5: from pi in p.PurchaseItems
6:
7: select new { c.Name, p.Description, pi.DetailLine };
另外,如果通过关联属性从父表开始选择数据的话我们不需要添加from语句.
1: from c in dataContext.Customers
2:
3: select new {
4:
5: Name = c.Name,
6:
7: SalesPerson = c.SalesPerson.Name
8:
9: };
在这个例子中,我们并没有使用SelectMany因为没有子集合需要处理.关联属性返回了一个单一的项.
SelectMany和Outer join
在前面使用select的left outer join例子中:
1: from c in dataContext.Customers
2:
3: select new {
4:
5: c.Name,
6:
7: Purchases =
8:
9: from p in c.Purchases
10:
11: where p.Price > 1000
12:
13: select new { p.Description, p.Price }
14:
15: };
每一个客户都会被包括,不管它是否包含任何订单. 假设我们现在要使用SelectMany重新编写这个查询:
1: from c in dataContext.Customers
2:
3: from p in c.Purchases
4:
5: where p.Price > 1000
6:
7: select new { c.Name, p.Description, p.Price };
然而这个查询得到的是一个inner join的效果,只有包含Price>1000的采购单的客户才会被包括进来. 为了能够得到一个left outer join结果,我们可以在内部序列上使用DefaultIfEmpty查询操作符,此方法对于没有包含任何元素的输入序列返回null:
1: from c in dataContext.Customers
2:
3: from p in c.Purchases.DefaultIfEmpty()
4:
5: select new {
6:
7: c.Name,
8:
9: p.Description,
10:
11: Price = (decimal?) p.Price
12:
13: };
这个查询在LINQ to SQL当中工作良好,并且将会返回所有的客户即便他们不包含任何的采购单. 但对于本次查询,当p等于null的时候将会得到一个NullReferenceException异常, 不过我们还是可以进一步完善它:
1: from c in dataContext.Customers
2:
3: from p in c.Purchases.DefaultIfEmpty( )
4:
5: select new
6:
7: {
8:
9: c.Name,
10:
11: Descript = p == null ? null : p.Description,
12:
13: Price = p == null ? (decimal?) null : p.Price
14:
15: };
接下来让我们引出Price过滤器,我们不能简单的像以前做的那样直接加上where语句,因为它会在DefaultIfEmpty之后执行,正确的解决方案是我们需要一个子查询并且必须让Where语句在DefaultIfEmpty之前:
1: from c in dataContext.Customers
2:
3: from p in c.Purchases.Where (p => p.Price > 1000)
4:
5: .DefaultIfEmpty( )
6:
7: select new
8:
9: {
10:
11: c.Name,
12:
13: Descript = p == null ? null : p.Description,
14:
15: Price = p == null ? (decimal?) null : p.Price
16:
17: };
这个查询在LINQ to SQl当中将会被很好的转换成left outer join, 并且对于编写这类型的查询这是一个比较有效率的做法. 待续!
评论 (0) All