本篇继续LINQ Operators的学习,这里我们讨论的是数据转换的两种方式:Select和SelectMany,看似简单常用的两种运算符,却也大有讲究。我们会在本篇详细介绍他们的使用方式和适用的场景,以及它们对于各种连接(Join)的支持方式。

数据转换(Projecting)

IEnumerable<TSource> → IEnumerable<TResult>

Operator

说明

SQL语义

Select

用指定的lambda表达式转换每一个输入element

SELECT

SelectMany

转换每一个输入element,并且展开和连接结果subsequences

INNER JOIN,

LEFT OUTER JOIN,

CROSS JOIN

当查询数据库时,Select和SelectMany是最灵活的连接结构;而对于本地查询,Join和Group是最有效的连接方式。

Select

参数

类型

Source sequence

IEnumerable<TSource>

结果转换表达式

TSource => TResult or (TSource,int) => TResulta

a带索引的lambda表达式在LINQ to SQL和Entity Framework中不可用。

查询表达式语法

select projection-expression

Enumerable实现

        public static IEnumerable<TResult> Select<TSource, TResult>
(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
foreach (TSource element in source)
yield return selector(element);
}

简介

对于Select,你总是得到与源sequence相同数量的elements,并且每个element都经过了lambda表达式的转换。下面的代码选择计算机上安装的所有字体:

            // using System.Drawing;
IEnumerable<string> query = from f in FontFamily.Families
select f.Name;
foreach (string name in query) Console.WriteLine(name);

在这个示例中,select子句把一个FontFamily对象转换成字体名称,下面是等价的lambda表达式:

            IEnumerable<string> query = FontFamily.Families.Select(f => f.Name);

Select语句经常使用匿名类型来保存转换结果:

            var query = from f in FontFamily.Families
select new { f.Name, LineSpacing = f.GetLineSpacing(FontStyle.Bold) };

一个不进行任何转换的select通常用在查询表达式语法中,用以满足查询必需要以select或group结尾的要求。比如下面的查询选择所有支持删除线的字体:

            // selects fonts supporting strikeout
IEnumerable<FontFamily> query =
from f in FontFamily.Families
where f.IsStyleAvailable (FontStyle.Strikeout)
select f; foreach (FontFamily ff in query) Console.WriteLine (ff.Name);

在这种情况下,编译器在把查询表达式语法翻译成方法语法时会忽略该select。

带索引的数据转换

数据转换表达式支持第二个可选参数,用以表示当前element在输入sequence中的索引位置,当然只有本地查询支持这种功能:

            string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> query = names
.Select((s, i) => i + "=" + s); // { "0=Tom", "1=Dick", ... }

Select子查询和对象层次

我们可以通过在select子句中嵌套一个子查询来创建一个对象层次。

            // 下面的示例获取一个集合,用来描述D:\Documents下的每一个子目录
// 每一项都包含一个文件集合
DirectoryInfo[] dirs = new DirectoryInfo(@"d:\Documents").GetDirectories();
var query = from d in dirs
where (d.Attributes & FileAttributes.System) ==
select new
{
DirectoryName = d.FullName,
Created = d.CreationTime,
Files = from f in d.GetFiles()
where (f.Attributes & FileAttributes.Hidden) ==
select new { FileName = f.Name, f.Length, }
}; foreach (var dirFiles in query)
{
Console.WriteLine("Directory: " + dirFiles.DirectoryName);
foreach (var file in dirFiles.Files)
Console.WriteLine("" + file.FileName + "Len: " + file.Length);
}

这个查询获取Files的部分可以被称为相关子查询。相关子查询是指它引用了外部查询的对象,本例中,它引用了正在遍历的目录对象d。Select中的子查询让我们可以把一个对象层次映射到另一个对象层次,或者把关系对象模型映射到层次对象模型。

对于本地查询,Select中的子查询会导致双重-延迟执行(double-deferred execution)。在我们上面的示例中,直到内层的foreach语句遍历dirFiles.Files时,获取文件的子查询才会被真正执行。

LINQ to SQL和EF中的子查询和连接

LINQ to SQL和EF也支持通过子查询进行数据转换,这是通过SQL中的join实现的。下面的示例获取customer的姓名以及Price大于1000的Purchases:

            var query =
from c in dataContext.Customers
select new
{
c.Name,
Purchases = from p in dataContext.Purchases
where p.CustomerID == c.ID && p.Price >
select new { p.Description, p.Price }
}; foreach (var namePurchases in query)
{
Console.WriteLine("Customer: " + namePurchases.Name);
foreach (var purchaseDetail in namePurchases.Purchases)
Console.WriteLine(" - $$$: " + purchaseDetail.Price);
}

这种类型的查询非常适用于解释查询,外部查询和内部的子查询作为一个单元处理,这样就避免了到外部数据源的额外连接。而对于本地查询,它的效率却并不高,因为对于每一个外部查询返回的element,都会通过内部子查询去获得少量的匹配元素。这时,对于本地查询来说更好的选择是Join或GroupJoin,我们会在下一篇中介绍Join和Group Join。

上面的查询使用了两个完全不同的对象集合(Customers和Purchases),可以被看成是一种”Join”行为。它和传统意义上的数据库Join之间的区别在于:我们的查询并没有把结果平展为一个二维的数据集,而是把关系数据映射到了层次对象上。下面的示例和上面的查询实现了相同的功能,但它通过Customer实体的Purchases关联属性简化了查询:

            var query =
from c in dataContext.Customers
select new
{
c.Name,
Purchases = from p in c.Purchases // Purchases is EntitySet<Purchase>
where p.Price >
select new { p.Description, p.Price }
};

上面的两种查询都对应了SQL的left outer join(左连接),也就是说,我们在外层查询获取了所有的Customers,而不管他们是否存在Purchases。如果要模拟inner join(内连接),即不包含那些没有Purchases(Price > 1000)的Customers,我们必须在外层查询中对Customers进行过滤:

            var query =
from c in dataContext.Customers
where c.Purchases.Any (p => p.Price > )
select new {
c.Name,
Purchases = from p in c.Purchases
where p.Price >
select new { p.Description, p.Price }
};

可以看出,这个查询不够简洁,我们对同一条件 (Price > 1000)书写了两次。这时我们可以通过let子句来避免重复:

            var query =
from c in dataContext.Customers
let highValueP = from p in c.Purchases
where p.Price >
select new { p.Description, p.Price }
where highValueP.Any()
select new { c.Name, Purchases = highValueP };

这种样式的查询非常灵活,比如通过把Any改为Count,我们可以只获取那些至少有两个Purchase的Price > 1000的Customers:

                where highValueP.Count() >= 
select new { c.Name, Purchases = highValueP };

数据转换到具体类型

但我们需要获取中间结果时,把数据转换到匿名类型非常有效。但是如果我们希望把结果发送到客户端或其他方法,匿名类型就不适合了,因为匿名类型智能作为一个方法内的局部变量。替换方法是使用具体类型比如DataSets或自定义的业务实体类来保存结果。一个自定义的业务实体只是一个包含了某些属性的简单类,他们用来隐藏低层次的程序结构(比如数据库存储)。假如我们定义了CustomerEntity和PurchaseEntity业务实体,下面的代码可以使用他们来保存查询结果:

            IQueryable<CustomerEntity> query =
from c in dataContext.Customers
select new CustomerEntity
{
Name = c.Name,
Purchases = (from p in c.Purchases
where p.Price >
select new PurchaseEntity {
Description = p.Description,
Value = p.Price
}
).ToList()
}; // 要强制执行query,可以把结果转换到普通List
List<CustomerEntity> result = query.ToList();

注意,到目前为止,我们还不需要使用Join或SelectMany语句,这是因为我们在数据转换时维持了层次结构的数据,如下图所示,左边是LINQ to SQL生成的实体类,右边则是我们自定义的业务类。在LINQ中,我们通常避免SQL中把数据表平展成二维结果集的传统方式,因为上面这种层次结构的数据更易于理解和使用。

SelectMany

参数

类型

Source sequence

IEnumerable<TSource>

结果转换表达式

TSource => IEnumerable<TResult>

或者 (TSource,int)   => IEnumerable<TResult>

查询表达式语法

        from identifier1 in enumerable-expression1
from identifier2 in enumerable-expression2
...

Enumerable 实现

        public static IEnumerable<TResult> SelectMany<TSource, TResult>
(IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)
{
foreach (TSource element in source)
foreach (TResult subElement in selector(element))
yield return subElement;
}

简介

SelectMany把子查询连接合并成一个简单的输出序列。让我们回想一下,对于每个输入element,Select返回一个输出element,而对于SelectMany会返回0..n个输出elements。这0..n个elements来自lambda表达式必需生成的一个子sequence。SelectMany可以用来展开这些子sequence、水平展开嵌套的集合,并最终把他们连接到一个输出sequence。

比如,假设我们有一个名字数组如下:

string[] fullNames = { "Anne Williams", "John Fred Smith", "Sue Green" };

现在我们想把它们转换到一个平展的单词集合,即:"Anne", "Williams", "John", "Fred", "Smith", "Sue", Green"

这时,SelectMany就非常适用于这项任务,因为我们需要把每个输入元素映射到多个输出元素。我们需要做的就是 写出一个把单个元素转换到一个子sequence的lambda表达式。string.Split方法正好可以完成此项工作,所以,我们的SelectMany查询如下:

            string[] fullNames = { "Anne Williams", "John Fred Smith", "Sue Green" };
IEnumerable<string> query = fullNames.SelectMany(name => name.Split());
foreach (string name in query)
Console.Write(name + "|"); // Anne|Williams|John|Fred|Smith|Sue|Green|

如果我们用Select来替换SelectMany,我们会获得层次结构形式的相同数据,这时结果是一个sequence,但其中的每个element是一个string array,我们需要嵌套的foreach来遍历结果:

            IEnumerable<string[]> query = fullNames.Select(name => name.Split());
foreach (string[] stringArray in query)
foreach (string name in stringArray)
Console.Write(name + "|"); // Anne|Williams|John|Fred|Smith|Sue|Green|

使用SelectMany的好处是 它会返回一个简单和平展的sequence。查询表达式语法也支持SelectMany,不过它是通过一个额外的from子句来实现的。查询表达式中的from关键字有两种意义:查询开始处的from引入初始范围变量和输入sequence,任何其他位置的from会被翻译成SelectMany。下面就使用查询表达式语法来重写上面的示例:

            IEnumerable<string> query = from fullName in fullNames
from name in fullName.Split() // Translates to SelectMany
select name;

上面查询的第二个from子句引入了一个新的查询变量name,从这里开始我们就可以在查询中使用这个新的范围变量,而老的范围变量fullName也就成了查询的外部范围变量。

外部范围变量

在上面的例子中,fullName在SelectMany之后就成了一个外部范围变量。外部范围变量的作用域会一直保持到查询结束或到达一个into子句为止。这种需要使用外部范围变量的场景,查询表达式语法就优于方法语法了。假如上面的例子,我们需要在结果中加入fullName:

            IEnumerable<string> query =
from fullName in fullNames // fullName = outer variable
from name in fullName.Split() // name = range variable
select name + " came from " + fullName; Anne came from Anne Williams
Williams came from Anne Williams
John came from John Fred Smith
...

那么在后台,编译器是如何来解析外部范围变量的引用的呢?换句话说,它该如何把上面的查询表达式翻译成等价的方法语法呢?因为在方法语法中,SelectMany返回一个平展的sequence,所以它来自哪个外部范围变量fullName已经“丢失”了。其实,编译器使用了一种策略来解决这个问题:在一个临时的匿名类型中,同时保存外部元素和每一个内部子元素。即编译器会把上面的查询表达式转换成如下的方法语法:

            IEnumerable<string> query = fullNames
.SelectMany(fName => fName.Split().Select(name => new { name, fName }))
.Select(x => x.name + " came from " + x.fName);

使用SelectMany连接

我们可以使用SelectMany来join两个sequences,得到一个元素之间的交叉结果集,比如:

            string[] players = { "Tom", "Jay", "Mary" };
IEnumerable<string> query = from name1 in players
from name2 in players
select name1 + " vs " + name2; // RESULT: { "Tom vs Tom", "Tom vs Jay", "Tom vs Mary",
// "Jay vs Tom", "Jay vs Jay", "Jay vs Mary",
// "Mary vs Tom", "Mary vs "Jay", "Mary vs Mary" }

尽管我们通过上面的查询实现一个cross join,但在让其结果有意义之前我们必须要加上一个过滤条件,该过滤语句构成了join的条件:

            IEnumerable<string> query = from name1 in players
from name2 in players
where name1.CompareTo (name2) <
orderby name1, name2
select name1 + " vs " + name2; //RESULT: { "Jay vs Mary", "Jay vs Tom", "Mary vs Tom" }

LINQ to SQL 和 EF中的SelectMany

LINQ to SQL和EF中的SelectMany可以用来做cross joins、inner joins、和left outer joins。 我们可以像Select语句那样,使用已经定义好的关联。一个LINQ-to-db 交叉连接(cross join)就是我们上节示例中的方法:

            // 下面的查询匹配每个Customer和每个Purhcase(a cross join):
var query = from c in dataContext.Customers
from p in dataContext.Purchases
select c.Name + " might have bought a " + p.Description;

通常情况下,我们希望Customers只匹配他们自己的Purchases。我们可以通过Where子句来增加一个join条件,其结果是一个标准的SQL样式相等连接(equi-join):

            var query = from c in dataContext.Customers
from p in dataContext.Purchases
where c.ID == p.CustomerID
select c.Name + " bought a " + p.Description;

如果我们的实体中有相关的关联属性,我们就可以在查询中直接使用该关联属性来取代交叉连接并获得相同的结果:

            var query = from c in dataContext.Customers
from p in c.Purchases
select c.Name + " bought a " + p.Description;

使用关联属性的好处是我们去除了连接条件,不管怎样,两种查询最终生成相同的SQL脚本。

我们可以在这种查询中使用where子句添加额外的过滤条件,比如如果只想找出那些姓名以”t”开始的Customers:

            var query = from c in dataContext.Customers
where c.Name.StartsWith ("T")
from p in c.Purchases
select new { c.Name, p.Description };

对于LINQ-to-db查询来讲,我们可以把where子句放到第二个from的下面,其结果完全一样,因为整个查询作为一个Unit来处理并生成相同的SQL脚本。但如果这时一个本地查询,把where放到第二个from后面会导致查询效率的降低,所以对于本地查询,我们应该在 连接之前过滤。

我们还可以继续添加from子句来引入新的子table,如果每个Purchase还有多个PurchaseItems,则我们可以写出下面的查询:

            var query = from c in dataContext.Customers
from p in c.Purchases
from pi in p.PurchaseItems
select new { c.Name, p.Description, pi.DetailLine };

如果我们需要包含父表里的数据,我们就不再是添加from子句,而只需简单的导航到该属性即可,假如每个Customer都有一个SalesPerson:

            var query = from c in dataContext.Customers
select new { Name = c.Name, SalesPerson = c.SalesPerson.Name };

在这里我们不使用SelectMany是因为这里没有子集合需要平展,父关联属性返回单个元素。

SelectMany中的Outer joins

我们在前面已经看到一个Select中的子查询返回的结果类似于左连接left outer join:

            var query = from c in dataContext.Customers
select new
{
c.Name,
Purchases = from p in c.Purchases
where p.Price >
select new { p.Description, p.Price }
};

在这个示例中,所有的外层元素(customer)都被包含在结果集中,而不管该Customer有没有Purchases。但如果我们使用SelectMany来重写这个查询以获得一个简单的平展集合时:

            var query = from c in dataContext.Customers
from p in c.Purchases
where p.Price >
select new { c.Name, p.Description, p.Price };

上面的查询转到了一个内连接inner join,现在只有那些包含了Purchase Price>1000的customers才会被包含在结果集中。如果我们需要在平展的结果集中实现left outer join,我们必须在内层sequence上面应用DefaultIfEmpty查询运算符。如果输入sequence没有任何elements,这个方法返回null。请看下面的示例:

            var query = from c in dataContext.Customers
from p in c.Purchases.DefaultIfEmpty()
select new { c.Name, p.Description, Price = (decimal?)p.Price };

上面的查询在LINQ to SQL和EF中工作得很好,返回了所有的Customers,即使他们没有任何Purchases。但是如果我们在一个本地查询中这么做,程序就会崩溃,因为当p为null时,p.Description和p.Price会抛出NullReferenceException。我们可以通过改写该查询让其在两种场景中都能完美地工作:

            var query = from c in dataContext.Customers
from p in c.Purchases.DefaultIfEmpty()
select new
{
c.Name,
Descript = p == null ? null : p.Description,
Price = p == null ? (decimal?)null : p.Price
}

如果我们还需要像之前那样引入price过滤的话,我们不能像前面那样直接在from后面添加Where,因为它会在DefaultIfEmpty之后执行:

            var query = from c in dataContext.Customers
from p in c.Purchases.DefaultIfEmpty()
where p.Price > // 错误的做法
...

正确的做法是把Where条件以子查询的形式添加在DefaultIfEmpty前面:

            var query = from c in dataContext.Customers
from p in c.Purchases.Where(p => p.Price > ).DefaultIfEmpty()
select new
{
c.Name,
Descript = p == null ? null : p.Description,
Price = p == null ? (decimal?)null : p.Price
};

LINQ to SQL和EF把这种查询翻译成left outer join,上面是书写此类查询非常有效的模式。

这里介绍了两种书写外连接的查询方法,尽管使用SelectMany来获取平展的结果集更加类似于SQL的处理方式,但在很多情况下,在Select中使用子查询获得层次结果集的方式更好一些,因为我们我们不需要额外的空值处理。

LINQ之路12:LINQ Operators之数据转换(Projecting)的更多相关文章

  1. LINQ之路15:LINQ Operators之元素运算符、集合方法、量词方法

    本篇继续LINQ Operators的介绍,包括元素运算符/Element Operators.集合方法/Aggregation.量词/Quantifiers Methods.元素运算符从一个sequ ...

  2. LINQ之路 7:子查询、创建策略和数据转换

    在前面的系列中,我们已经讨论了LINQ简单查询的大部分特性,了解了LINQ的支持计术和语法形式.至此,我们应该可以创建出大部分相对简单的LINQ查询.在本篇中,除了对前面的知识做个简单的总结,还会介绍 ...

  3. LINQ之路16:LINQ Operators之集合运算符、Zip操作符、转换方法、生成器方法

    本篇将是关于LINQ Operators的最后一篇,包括:集合运算符(Set Operators).Zip操作符.转换方法(Conversion Methods).生成器方法(Generation M ...

  4. LINQ之路11:LINQ Operators之过滤(Filtering)

    在本系列博客前面的篇章中,已经对LINQ的作用.C# 3.0为LINQ提供的新特性,还有几种典型的LINQ技术:LINQ to Objects.LINQ to SQL.Entity Framework ...

  5. LINQ之路10:LINQ to SQL 和 Entity Framework(下)

    在本篇中,我们将接着上一篇“LINQ to SQL 和 Entity Framework(上)”的内容,继续使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术 ...

  6. LINQ之路(3):LINQ扩展

    本篇文章将从三个方面来进行LINQ扩展的阐述:扩展查询操作符.自定义查询操作符和简单模拟LINQ to SQL. 1.扩展查询操作符 在实际的使用过程中,Enumerable或Queryable中的扩 ...

  7. [转]LINQ之路系列博客导航

    分享一个学习Linq的好博客:Linq之路

  8. LINQ之路 4:LINQ方法语法

    书写LINQ查询时又两种语法可供选择:方法语法(Fluent Syntax)和查询语法(Query Expression). LINQ方法语法是非常灵活和重要的,我们在这里将描述使用链接查询运算符的方 ...

  9. LINQ之路(2):LINQ to SQL本质

    LINQ之路(2):LINQ to SQL本质 在前面一篇文章中回顾了LINQ基本语法规则,在本文将介绍LINQ to SQL的本质.LINQ to SQL是microsoft针对SQL Server ...

随机推荐

  1. jenkins启动java项目的jar包总是退出

    参考文档: https://www.cnblogs.com/DFX339/p/8241253.htmlhttps://blog.csdn.net/windanchaos/article/details ...

  2. Herriott池的设计

    0.矩阵法计算光路 1.谐振腔和透镜组的等效,计算x和x’ 2.近轴光路的近似计算和矩阵法. 3.相邻光线的角度 4.为啥分模式 5.椭圆模式 6.要考虑的其他问题,相邻光斑的干涉

  3. Codeforces 1136D - Nastya Is Buying Lunch - [贪心+链表+map]

    题目链接:https://codeforces.com/problemset/problem/1136/D 题意: 给出 $1 \sim n$ 的某个排列 $p$,再给出若干 $(x,y)$ 表示当序 ...

  4. PE就是市盈率的缩写 PB是平均市净率的缩写

    PE就是市盈率的缩写 PB是平均市净率的缩写 以下为百科知识 PE Price/Earnings 市盈率 也有叫做PER的,Price/Earnings Ratio 本益比,价格收益比,市盈率 市盈率 ...

  5. etcd集群的搭建

    由于最近在学习kubernetes,etcd作为kubernetes集群的主数据库,必须先启动. etcds实例名称 IP地址 Hostname etcd 1 192.168.142.161 kube ...

  6. java框架之SpringBoot(12)-消息及整合RabbitMQ

    前言 概述 大多数应用中,可通过消息服务中间件来提升系统异步通信.扩展解耦的能力. 消息服务中两个重要概念:消息代理(message broker)和目的地(destination).当消息发送者发送 ...

  7. 搭建sftp

    参考:https://www.cnblogs.com/heyanan/p/8178480.html 需要注意两点:1.链接时候选择sftp不要选择ftp 2.目录权限严格按照参考文章设置

  8. [LeetCode] 90.Subsets II tag: backtracking

    Given a collection of integers that might contain duplicates, nums, return all possible subsets (the ...

  9. 非CI执行Allure2 trends空白问题

    问题描述 未使用CI工具集成Aluure运行测试用例并生成Allure报告,多次执行后,trends是空白的,未展示出期望的趋势图 问题原因非CI工具,是通过命令 allure serve 展示报告 ...

  10. flask 定义数据关系(多对一)

    多对一 一对多关系反过来就是多对一关系,这两种关系模式分别从不同的视角出发.一个作者拥有多篇文章,反过来就是多篇文章属于同一个作者.为了便于区分,我们使用居民和城市来演示多对一关系:多个居民住在同一个 ...