浙江恒生平易科技有限公司 苟安廷
打我们立志成为码农开始,就和数据库打交道了,没有数据库的软件感觉就是不可思议的,要存点啥东西,首先就想到了数据库,所以我们对数据库也特别熟悉,操作也特别顺手,当然,也特别依赖。随着互联网发展,大家都要用“平台”思想来构架系统,这时,为减少模块之间的耦合性、提高并发性能,数据库本身的功能在不断弱化,尤其是考虑到数据库的移植、兼容已经MVC大行其道等问题,过去我们引以为傲、功能强大、健壮高效的数据库编程日渐沦落为就是一堆带索引的表而已,或者说得高大上一点,叫数据仓库,而数据本身的操作,都放后台服务软件里面去了,微软C#的Linq就是目前比较流行的“类数据库”编程方式(C#3.0以后的版本支持),把我们过去习惯的直接在数据库里面通过写存储过程等思维放到开发语言里面去了,看来,大量(当然不是全部,资深大牛忽视本句话)数据库程序编写人员不得不另谋出路,转向前端或后台服务开发了,对于习惯了数据库开发的人来说,这个角色转换是痛苦的,我们尝试着从一个SQLer转到Linqer,为区别Sql和Linq,本文例子中,关键字大写的为Sql语句,小写的(也必须小写)为Linq语句。
一、 初识Linq
我们先在数据库中创建一个表,表名为MemberInfo,包含下面几个字段,并输入一些数据,表的结构和内容如下:
id |
MemberName |
Tel |
1 |
张三 |
123456 |
2 |
李四 |
54321 |
3 |
王五 |
87654 |
字段名比较容易理解,来看一个简单的sql语句,就是查询id大于0的全部数据,这个太简单了:
SELECT *
FROM MemberInfo
WHERE id>0
作为一个数据库软件开发者,上面的操作简直是太简单了,简单得让人觉得天经地义,“SELECT”表示我们需要返回的字段内容,其中“*”表示全部字段,“FROM”表示我们从哪个表里面查询,“WHERE”表示过滤条件,一切都顺理成章。我们再来看看如果在C#里面,对应的Linq该怎么写呢?
var result =from m in MemberInfos
where m.id > 0
select m;
大多数SQLer和我一样,第一次看到这样的写法,心里头一定有一万头草泥马飞奔而过,这是哪个脑袋被门夹扁了的人设计的?完全是脑残嘛,顺序搞反了不说,中间还一堆莫名其妙的东西,至少,你应该参考一下SQL语句标准写法该怎么写吧,这种山寨水平也好拿来丢人现眼?不过,心里爽不爽是一回事,人家已经做好了,且用了这么多年了,你除了牢骚外,还能怎样?还不得硬着头皮学下去,正如一句名言:工作就象被强奸,如果无力反抗,就要学会享受……
现在静下心来看看上面的Linq代码,虽然比较BT,但至少还是有很多熟悉的东西,不是全新的嘛,如果让你丢掉现有经验、知识,全新学一门语言呢,你会不会说,想当年,我拿着SQL利器,单枪匹马在长坂坡杀了个七进七出,又在当阳桥一声断喝,吓退数万敌兵,而今让我改行去做弼马温?所以,我们应该知足了,至少,是从熟悉的地方转行,老的经验也是可以用起来的,古人云,温故而知新,转行从熟悉的地方开始,谈恋爱从熟悉的人下手,难度就小了很多。
先丢掉怨气,想想为什么设计者会搞这么奇奇怪怪的语法呢?这恐怕得从智能感知说起了,所谓的智能感知,就是我们在编写代码时,输入部分内容时,相关内容会自动弹出来供我们选择,避免一个字一个字敲,参见下图:
当我们输入“strName.”之后,系统会自动提示可用的方法、属性等供我们选择,试想一下,如果我们没有Visual Studio这类强大的开发工具,而是像过去一样,每个字都自己敲出来,会是什么样子的?对于一些长、怪癖的名称,我们除了祭出Ctrl+C、Ctrl+V大法之外,似乎没有更好的办法了,想象一下,让你用记事本来开发项目,没有提示、没有语法着色、没有自动排版、没有变量名重构,会不会觉得不可思议?其实,我们过去在DOS下开发,就是这么做的,连鼠标都不支持呢,看一下当年的开发界面,感受一下现在的幸福吧:
回过头来想想,VS为什么能自动提示呢?显然,我们首先定义了变量的类型“stringstrName = "张三"”,在变量名“strName”后面输入“.”时,系统就根据string类型知道有哪些公共的东西可供使用了,然后列出来让我们选择,从而极大减轻了我们的记忆量,还可以有效避免输入错误。试想一下,如果我们先不定义,而是直接输入“strName.”会怎么样?显然,系统因为不知道类型,也就无法智能感知,当然,这种语法也是不允许的。
相对于C#良好的开发环境,数据库开发就可怜多了,尤其是智能感知更是可怜,这几年微软在sql server里面增加了部分智能感知功能,但总体还是非常弱,第三方插件SQL Prompt则强大得多,我们希望输入SELECT后,能自动提示字段名啥的。现场测试一下,当我们输入SELECT后,看看会不会提示MemberInfo表中的Tel字段名:
显然,虽然我们输入了“te”,但系统并没有提示字段名“tel”,只是提示了一个毫不相干的Test数据库名,这也好理解,那么多表、视图、函数啥的,系统怎么知道从哪个表里面去取啊,那么,我们再测试一下,先输入后面的FROM部分,再倒过来输入前面的SELECT部分,看看有什么效果:
漂亮,系统自动列出了字段名供我们快速输入了,为什么这次系统能“智能感知”了呢?稍微观察一下就明白了,因为后面有了“FROMdbo.MemberInfo”部分,系统根据该表就能把字段列出来供选择了。
这时,我们发现平时习以为常的语法有缺陷了,如果按“标准”语顺序法写,无法实现“智能感知”,要实现智能感知,就必须倒过来先写后面再写前面,不符合大家的书写顺序,纠结啊,要是现在有一个机会,让你去改进传统sql语法,要求既能实现智能感知,又符合书写顺序,你会怎么设计呢?估计你马上就想到了,既然智能感知必须要提前知道对象的类型,那么,就应该参照C#的用法,先申明对象再使用嘛,对于上面的查询语句,先写FROM部分,再写SELECT部分不就可以了,如下图(当然,实际这么写是错,下图是PS出来的):
当我们输入“FROM”时,系统会自动把所有表列出来供选择,然后,当我们输入“SELECT”时,系统会根据上面FROM后面的表,把字段列出来供选择,既可以从前往后写代码,又可以实现智能感知,两全其美!
当我们为自己的聪明才智沾沾自喜时,回头再看一下Linq的语法,你还会诅咒他们吗?估计会为他们能跳出传统框架束缚与时俱进而佩服得五体投地吧,所以放心,设计Linq的人不是蠢材,也不是脑子进水,都是天才!跟着天才走,一定少走弯路。
现在,估计你对Linq语法已经不那么憎恨了,甚至,有一点点佩服这些人了,那么接下来,我们再尝试亲近一点,当然,为平稳过渡,我们还是要从熟悉的地方开始。
为了简化SQL语法,我们在查询时,都习惯给表一个别名,尤其是关联查询时,我们看看下面的代码:
SELECT m.*
FROM MemberInfo AS m
WHERE m.id>0
这里的AS关键字可以省略,现在有没有一种把SELECT放最后的冲动?为了满足这种自我陶醉,我们人为地交换一下吧:
FROM MemberInfo AS m
WHERE m.id>0
SELECT m.*
现在是不是看起来没有那么怪怪的感觉了?这里,我们给表取了个别名“m”,接下来的WHERE、SELECT等就可以直接用别名“m”代替表名了,省时省力,我们再来看Linq:
var result =from m in MemberInfos
where m.id > 0
select m;
没错,这里的范围变量“m”和SQL里面的表别名异曲同工,你就认为是表的别名吧,只不过语法顺序有点点差异而已,不过差异不大,in关键字相当于SQL里面的AS关键字,甚至你可以认为Linq里面的select m是SQL里面的SELECTm.*的简写。对于范围变量m,实际上和数据库的别名还是不一样的,事实上是数据集中的一个元素,用下面的代码更能反映本质:
传统写法 |
Linq写法 |
List<MemberInfo> result = newList<MemberInfo>(); |
var result = |
foreach(MemberInfo m in arrMemberInfos) { |
from m in MemberInfos |
if (m.id > 0) |
where m.id > 0 |
result.Add(m); } |
select m; |
也就是说,我们在写代码时,应该记住m(当然你可以随便取其他名称)是数据源中的一个具体条目,本质上是通过遍历数据源,每次遍历时,把当前元素存放在m里面供使用。
现在看来,Linq和SQL语法越来越接近了,亲和力进一步得到提升。
查询的数据,总要有个来源吧,也就是我们总要指定一个数据源,在SELECT * FROM MemberInfo中,MemberInfo是数据库中的一个表,也是我们的数据源,表包含列(字段)和行(记录)两部分,Linq中,也需要数据源,前面的示例中,我们直接使用了MemberInfos变量,该变量自然就是Linq的数据源,但我们并没有说明该数据源到底是什么,显然,不是数据库中的一个表,通常来说,数据源也是一个二维表,应该包含字段名、记录,我们自然想到了C#中的DataTable类型,简直和数据库的表一模一样啊,但DataTable的列名往往是未知的,只有在运行状态才能列举出来,开发阶段是不直观的,所以,并不是最合适的,虽然和数据库最接近。最理想的,当然是已知字段名的数据源,这样,才有利于“智能感知”嘛。为构建这样的数据源,我们首先得构建“字段”部分,通常是创建一个类,里面设置公共“属性”,当成字段处理,如:
public classMemberInfo
{
public int id {get;set; }
public string MemberName {get;set; }
public string Tel {get;set; }
}
我们定义了一个类,里面有3个字段,和数据库里面的定义刚好对应起来。
有了字段,就需要定义记录了,记录的数量可以是0,也可以是很多,自然的,我们想到了数组,因此,我们通过下面的方式就创建了Linq的数据源:
List<MemberInfo>MemberInfos = newList<MemberInfo>();
MemberInfos.Add(new MemberInfo() {id=1,MemberName="张三",Tel="12345" });
……
现在,一个完美的数据源就有了,只不过,数据库是一个表,而Linq是一个List数组(当然,直接使用数组也是一样的,如MemberInfo[]MemberInfos),因为字段名是已知的,所以,智能感知就是分内之事了,如:
一切是那么顺理成章水到渠成,事实上,凡是实现了IEnumerable的对象都可以做Linq数据源。
在SQL操作中,我们辛辛苦苦查询的结果,总不能随便扔掉吧,比如SELECT * FROM MemberInfo得到的结果,一般就直接被软件取得了,数据库本身似乎不关心,而Linq本身就是在软件中啊,不能不管,咋办呢?当然是存在变量里面供以后使用了,因此,我们第一句就是“var result= from……”,可以这么简单粗暴地理解为,把查询结果保存到了result变量中(实际上,这一步仅仅是定义了查询规则,并没有真正查询,只有在后面用到时,才会真正执行)。
我们前面都是通过“SELECT*”返回了全部列,列名就是原来的字段名,当然,也可返回部分字段名,并可以对返回的字段名指定别名,同样,在Linq中,直接“select m”返回全部列的信息,当我们不需要返回全部字段时,也可以部分返回,并指定别名。在SQL中,我们这么指定别名:SELECT id AS MemberID,MemberName AS Name FROM dbo.MemberInfo,也可以这么写:SELECT MemberID=id, Name=MemberName FROMMemberInfo,在Linq中,可以通过匿名对象返回部分字段,指定别名的方法和SQL第二种方式非常相像:
var result =from m in MemberInfos
select new { MemberID = m.id,Name = m.MemberName};
这里,我们返回两列,并取了别名,实际上,我们通过“new”关键字,返回了一个匿名的类,该类包含MemberID和Name两个公共方法。我们还可以显示定义一个类:
private classMyMemberInfo
{
public intMemberID {get;set; }
public stringName {get;set; }
public MyMemberInfo(int memberid,string membername)
{
this.MemberID = memberid;
this.Name = MemberName;
}
}
然后返回:
var result =from m in MemberInfos
select new MyMemberInfo(m.id,m.MemberName);
这样,返回的每条记录,都会对应一个MyMemberInfo对象。
上面的用法都是查询数据源中部分列,顶多换一个新名称而已,实际上,我们还可以“构建”新的列,比如Sql里面:
SELECTNOWTIME= GETDATE(),ID
FROM XXX
同样,Linq里面也可以这么写:
select new { NowTime=DateTime.Now,id=n};
其他的,如数学函数啥的,尽管放进来,C#本身认的,一般都认,这可比SQL本身强大多了。
到此为止,我们将一个通用的、标准的SQL语句成功转换成Linq语句,以后其他语句都是在这个基础上进行不断扩展而已。
大部分教程中,都会直接使用数组作为例子,前面说了只要实现了IEnumerable
int[] nums = { 1, 2, 3, 4, 5, 6, -1, -2 };
var posNum =from n in nums
where n > 0
select n;
上面的where语句只用了一个条件,如wheren>0,如果需要多个条件进行限定呢?显然,我们首先会想到用条件表达式组合起来,比如:
where n > 0 && n<10
没错,就是这么简单,当然,还可以加括号啥的,就不赘述了,不过,Linq还有一个“BT”的写法,每个条件单独用一个where关键字:
where n > 0
wheren < 10
确实,这样写太逆天,我们了解一下就可以了,实际工作中基本上也用不到。
前面辛辛苦苦写了一堆查询语句,总要用起来才行,查询是手段,使用结果才是目的,使用很简单,通常都是foreach遍历结果变量,如:
//定义查询规则
int[] nums = { 1, 2, 3, 4, 5, 6, -1, -2 };
var posNum =from n in nums
where n > 0
select n;
//遍历查询结果
foreach (int i inposNum)
Console.Write(i);
这是对普通数组的遍历,因为我们知道结果是int型,所有可以直接inti,更多的时候,我们使用var更方便,对于多列,也是一样的:
var result =from minMemberInfos
select new { MemberID = m.id,MemberName = m.MemberName};
foreach (var member in result)
Console.Write(member.MemberID.ToString() +" " + member.MemberName);
需要特别强调的是,前面定义的Linq语句仅仅是定义,并没有马上执行,只有在后面foreach时,才真正执行,换句话说,我们定义后,如果修改了数据源,遍历的结果也就变了,一个Linq语句可以反复被执行,如上例中,第一次foreach后,修改数据的值,再遍历时,得到的结果就变了,如:
//定义Linq语句
var result =from m in MemberInfos
select new { MemberID = m.id,MemberName = m.MemberName};
//开始执行
foreach (var member in result)
Console.Write(member.MemberID.ToString() +" " + member.MemberName);
//修改数据源
MemberInfos[0].Tel ="13612345";
//再次执行
foreach (var member in result)
Console.Write(member.MemberID.ToString() +" " + member.MemberName);
Linq语句只定义了一次,第一次执行后,修改数据源,再次执行,虽然两次执行的Linq都是同一个变量,但结果却不一样,相当于在数据库执行下面的语句:
DECLARE @SqlNVARCHAR(100)
--定义查询条件
SELECT @Sql='SELECT * FROM dbo.MemberInfo'
--开始执行
EXEC (@Sql)
--修改数据源
UPDATE dbo.MemberInfo SET Tel='13612345'WHERE id=1
--再次执行
EXEC(@Sql)
执行结果如下:
前面的查询是通用的Linq语法,直观(相对的),但并不紧凑,看起来也不是那么专业,很容易让人摸透自己的真实水平,不要以为穿上大学校服就显得受过高等教育,还记得当年学校流行过的“大一娇、大二俏、大三急得跳、大四没人要”的口头禅么?你的一举一动可能就暴露了你的实力,要想逼格高一些,就得学习一些贵族的生活方式,我们换一种方式,把自己打扮得洋气起来,那就是使用查询方法,每个支持IEnumerable的类都可以用这种方法查询,我们先来回忆一下前面的查询语法:
var result =from m in arrGoods
where m.GoodsType =="衣服"
select m;
不陌生吧,换成查询方法如下:
var result = arrGoods.Where(m=>m.GoodsType=="衣服").
Select(m => m);
是不是简洁很多?但好像有一点点看不懂,是什么呢?对了,好像是Lambda表达式,如果是,你得抽点时间学习一下了,顺便学习一下什么是委托,要知道委托和Lambda表达式可是有血缘关系的,同样的,查询方法和查询语法也是有血缘关系的,看不懂不要紧,我们先简单介绍一下什么是Lambda表达式。
“m=>m.GoodsType=="衣服"”是一个Lambda表达式,看起来怪怪的,中间出现了一个新的运算符“=>”,这是什么鬼,其实,你不用管他是什么鬼,你当成一个分隔符看就可以了,也就是说,这个表达式由两部分构成,前面的“m”和后面的“m.GoodsType=="衣服"”构成,拆开后是不是看起来亲切一些了,也大概看出点名堂了,再分析一下,其实,Lambda表达式是一个简写的“方法”(传统习惯上叫“函数”),由参数和方法体构成,Where是过滤用的,需要返回true或false,因此,我们写一个下面的方法你就明白了:
privatebool Where(GoodsRecorde m)
{
if (m.GoodsType =="衣服")
return true;
else
return false;
}
怎么样,现在还难理解吗?如果我们把方法体浓缩一下,变成:
privatebool Where(GoodsRecorde m)
{
return m.GoodsType =="衣服";
}
把上面的方法参数、方法体用“=>”分开,就是Lambda表达式了。里面的“m”是哪里来的?还记得前面说的表“别名”吗?这个就是别名,你可以用m,也可以n或者你喜欢的任何变量名。同样,后面的Select里面也是Lambda表达式,和查询方法对照起来看,不难理解,只不过,查询语法的m是显示指定的,而查询方法里面,是自己随便取的,只要“=>”前后的变量名对应起来就可以了。
查询方法除了前面的Where、Select外,还有OrderBy、GroupBy、Join,都用到Lambda表达式,除Join复杂一点外,其他的都差不多。
查询语法里面,可以返回匿名对象、特定的对象,查询方法也完全一样,如:
var result = arrGoods.Where(m => m.GoodsType =="衣服").
OrderBy(p => p.GoodsType).OrderByDescending(p => p.GoodsType).
Select(m =>new { NewName = m.GoodsName, NewType = m.GoodsType, NewPrice = m.Price });
注意例中多列排序的用法(一个升序、一个降序)。
以上针对的是一个表查询,相对还是比较简单的,多表关联查询相对麻烦一点点,但也不是想象的那么复杂,我们还是从熟悉的地方开始,看看查询语法怎么写的:
var result =from A in arrStudent
join B in arrBook on A.Id equals B.StudentId
select new { id = A.Id, Name = A.Name, BookName = B.BookName };
这个还记得吗?如果不记得了,回头去看看,看明白后,我们看一下对应的查询方法怎么写的:
var result = arrStudent.Join(
arrBook,
A => A.Id,
B => B.StudentId,
(A, B) =>new { id = A.Id, Name = A.Name, BookName = B.BookName });
Join需要4个参数,(1)被关联的表,(2)根据自己的别名得到的关联字段,(3)根据被关联表的别名得到关联字段,(4)根据自己、被关联表别名得到的返回字段列表。这样看来,也没有什么难度嘛。
除了语法更紧凑,看起来更专业外,根本原因是查询方法才是亲生的,当我们用查询语法编写的代码,编译后,会被自动转换成查询方法,既然如此,我们何必绕一圈呢?
到目前为止,基本查询我们已经不惧怕了,但要完成一些特殊业务,靠这么简单的查询是远远不够的,必须把数据库中更多的用法搬进来才行。
对于查询结果,我们通常需要进行排序,排序分为升序和降序,可以根据一列排,也可以根据多列排,升序通常可以省略选项:
SELECT * FROMdbo.MemberInfo
ORDER BY MemberName ASC,Tel DESC
上面的查询,先按MemberName升序排序,如果相同,则按Tel降序排列,而ASC是可以省略的,同样,在Linq里面也可以这么操作,方法如出一辙,只不过关键字要写全,而不是简单的DESC缩写,有了智能感知,是否写全已经不重要了,不管是完整的写法,还是缩写,其实都是敲入第一个字母,然后回车解决。
var result =from m in MemberInfos
orderby m.MemberName ascending,m.Tel descending
select m;
上面的语法非常容易理解,同样,ascending可以省略。
在Sql里面,如果查询很复杂,我们可以“分解”查询,先查询出一部分结果,放入临时表,然后再对临时表进行二次加工,如:
--生成临时表
SELECT * INTO #temp
FROM dbo.GoodsRecorde
WHERE Numbers>1
--从临时表里面再次查询
SELECT * FROM #temp
WHERE GoodsName='A'
对于Linq,你完全可以把result当成临时表,在后面的foreach遍历时,再使用Linq进行二次查询,除此之外,还可以在一个Linq里面完成,比如上面的Sql语句,Linq可以这么写:
var result =from m in SaleRecords
where m.Numbers> 1
select m into temp
//相当于二次查询
wheret emp.GoodsName=="A"
select temp;
以上仅仅是为了演示二次查询的用法,这种情况通常都是合并在一起的,into表示子句的延续,不仅仅适用于select,也适用于group,group的用法在后面将单独说明:
var result =from m in SaleRecords
where m.Numbers> 1
group m by m.GoodsName
into temp
//相当于二次查询
where temp.Count()>1
select temp;
into关键字针对的是前面的范围变量,也就是数据源中的一个具体的元素,因此temp的内容仅仅针对当前具体的元素,更直观的理解参照下面“let”关键后面传统写法和Linq写法的对比。
使用into关键字创建的“临时表”数据源是原来数据源的子集,有时候,我们需要根据查询条件对当前元素“分解”出现新的数据源,这时,可以有let关键字实现,如:
string[] strs = {"alpha","beta","gama" };
var result =from m in strs
//创建新的数据源
let newSource = m.ToArray()
//从新的数据源查询(又一个Linq查询)
from n in newSource
select n;
前面说过了,范围变量(这里有两个,m和n)本质上是遍历过程中的每一个具体元素,上面看起来如果觉得费劲,我们用传统写法写一遍,对照起来看就一目了然了:
传统写法 |
Linq写法 |
List<char> result = newList<char>(); |
var result = |
foreach (string m in strs) { |
from m in strs |
char[] newSource = m.ToArray(); |
let newSource = m.ToArray() |
foreach (char n in newSource) |
from n in newSource |
result.Add(n); } |
select n |
let可以使用多次,以便生成多个中间变量。
实际工作中,数据源往往不止一个,需要在多个数据源中进行关联查询,常用的有内连接INNER JOIN、左连接LEFT JOIN、右连接RIGHT JOIN、全连接FULL JOIN、交叉连接CROSS JOIN ,下面我们尝试看看Linq里面有没有对应的方式。
内连接表示返回左右两边的数据源里都有的数据,例如两个表,学生信息、借书信息,用Sql查询如下:
如果学生没有借过书,则不会在查询结果中,如果借了多本书,显然会出现多条记录(张三出现两次),在Linq里面,表示方法与之类似,不过,“ON”后面的“=”用“equals”代替,我就不明白了,用“=”不是更简单直观吗?
先定义两个类,模拟学生和借书记录两张表结构:
//学生类
private class Student
{
public int Id {get;set; }
public string Name {get;set; }
}
//借书类
private classBooks
{
public int StudentId {get;set; }//和学生ID关联
public string BookName {get;set; }
}
接下来,我们创建列表作为数据源:
List<Student> arrStudent = newList<Student>()
{
new Student() { Id=1,Name="张三"},
new Student() { Id=2,Name="李四"}
};
List<Books>arrBook = newList<Books>()
{
new Books() {StudentId=1,BookName="大话西游" },
new Books() {StudentId=1,BookName="武林歪转" },
new Books() {StudentId=2,BookName="熊出没" },
//为了测试后面的右连接,故意添加一条不存在学生记录的借书信息,
//当然,实际工作中不应该出现这样的记录
new Books() {StudentId=100,BookName="未来水世界" }
};
查询语句为:
var result =from AinarrStudent
join B in arrBook on A.Id equals B.StudentId
select new { id = A.Id, Name = A.Name, BookName = B.BookName };
和普通Sql几乎没有区别,要是能把“equals”换成“=”就完美了。
左连接就是以左边的表为准,右边的记录存在,则显示,如果右边有多条,则左边的对应记录会重复,如果右边没有符合条件的,则为NULL。参照SQL如下:
王五没有借过书,右边是NULL,张三借了两本,故出现两条记录。
Linq的左连接就没有那么方便了,查询相关代码如下:
我们先看一下执行情况:
张三:大话西游
张三:武林歪转
李四:熊出没
王五:无
这段代码相当费解,要反复看才弄得明白,不得不说这个设计让人不可思议,就象用“equals”而不用“=”一样让人莫名其妙,且容易搞错,下面逐一分解。
(1) 先连接,并生成一个临时表
var result =from A in arrStudent
join B in arrBook on A.Id equals B.StudentId
//生成临时表
into temp
前面部分和内连接完全一样,唯一多了一个temp“临时表”,那么,temp包含哪些字段呢?一开始我想当然地认为,应该是A、B两个表的全部字段,但仔细一想好像又不对,万一两个字段名有相同的咋办呢?经实测(测试方法很简单,后面的语句中看智能感知即可),temp只有B表一个表的字段内容,也就是只有B表的内容,真是很坑爹,如果就像select一样自己可以指定列名多好。
(2) 二次查询并使用默认值
对临时表进行查询:
from t in temp.DefaultIfEmpty()
这里和前面不同的是,多了一个DefaultIfEmpty()方法,顾名思义,就是如果B表为空时,使用的默认值,看起来还不错,实际又是一个坑,如果B为空,压根就不能直接使用(可以指定默认值,后文将说明)。
(3) 设置查询结果集
接下来,就可以设置查询结果需要的字段列表了,前面说过了,临时表其实是B表,要用A表的字段咋办呢?其实,A表是可以直接使用的,这里直接用A,相当于数据库里面A.*了:
select new { A, BookName = (t ==null ?"无" : t.BookName) };
临时表temp在二次查询时,用了别名t,因此上面的语句时,B表必须使用变量t代替,这里,我们返回了A表全部内容,B表的BookName,前面说了,如果B表对应的记录为空,不允许直接调用,要不然会产生异常,故我们用了BookName = (t == null ?"无" : t.BookName)进行判断。
除了判断t是否为null外,还可以创建一个默认的对象数组,因为左连接可能一对多,故必须是默认的对象数组,我们改造一下临时表的用法,改造前:
from tintemp.DefaultIfEmpty()
select new { A, BookName = (t ==null ?"无" : t.BookName) };
改造后:
from tintemp.DefaultIfEmpty(newBooks() { BookName ="无", StudentId = A.Id })
select new { A, t.BookName };
意思是,如果B表没有对应的内容,那么,手工创建一个记录数组,这样,下面的select里面就不会为null了,效果也是一样的,不过本人还是相对倾向于第一种写法。
左连接是以左边的表为准,右边如果没有,则标记为NULL,而右连接则是以右边的记录为准,左边的如果没有,用NULL标记,换句话说,把左右表交换一下,就可以用左连接代替右连接了,因此,没有专门的右连接。
全连接是左连接和右连接的组合,任何一边有,另外一边如果没有,则标记为NULL,Linq没有全连接,通用做法是先A表左连接B得到一个结果,再B表左连接A得到一个结果,然后把二者组合起来取唯一值,Sql查询如下:
对应的Linq一般这么写:
//第一个表左连接第二个表
var result1 =from A in arrStudent
join B in arrBook on A.Id equals B.StudentId
into temp
from t in temp.DefaultIfEmpty()
select new { A.Name, BookName = t ==null ?"" :t.BookName };
//第二个表左连接第一个表
var result2 =from B in arrBook
join A in arrStudent on B.StudentId equals A.Id
into temp
from t in temp.DefaultIfEmpty()
select new { Name = t ==null ?"" :t.Name, B.BookName };
//将两个查询组起来,取唯一值
var result = result1.Concat(result2).Distinct();
//显示查询结果
StringBuildersb =newStringBuilder();
foreach (var r in result)
sb.AppendLine(r.Name +":" + r.BookName);
textBox2.AppendText(sb.ToString());
结果参考如下:
张三:大话西游
张三:武林歪转
李四:熊出没
王五:
:未来水世界
交叉连接表示,第一个数据源的每条记录,都和第二个数据源的每条记录组合成新的记录,也就是得到一个m×n的笛卡尔积,因为两个表无须关联,故无需通过on设置关联条件,用两个from分别表示两个表即可(还记得前面多个条件可以用多个where表示吗?该思维这里又出现了,十分不爽):
char[] ch1 = {'A','B','C' };
char[] ch2 = {'1','2','3' };
var result =from m in ch1
from n in ch2
select new { r = m.ToString() + n.ToString() };
得到的结果如下:
A1
A2
A3
B1
B2
B3
C1
C2
C3
前面的查询,都是A表的一个字段和B表的一个字段关联,有时候因业务需要,可能是多个字段关联,比如这里需要ID和名称都一样(当然,实际不会发生学生姓名和书名一样的查询,这里仅仅是举个例子),SQL里面这么写:
SELECT *
FROM dbo.Student A INNER JOIN dbo.Books B ON A.id=b.StudentId AND A.Name=B.BookName
自然的,我们想到在Linq里面也应该这么写:
var result=from A in arrStudent
join B in arrBook
//关联部分
onA.Idequals && A.Name equals B.BookName
select new { A.Name, B.BookName };
看起来一切顺理成章,但事实上这样写是会报错的,将关联部分换一种方式就比较接近了:
on new { A.Id, A.Name } equals new { B.StudentId, B.BookName }
也就是说,将关联部分做成一个匿名对象,且列数相等,这样看起来对,实际还是会报错,原来,还需要字段名也完全一样,因此,必须取一个别名:
on new { StudentId=A.Id, BookName=A.Name }equals new { B.StudentId, B.BookName }
现在查询就正常了,既然要求字段名相同,那么两个字段前后顺序应该没关系吧,要不然岂不是多此一举?实际上如果顺序不一致,也会报错,换句话说,多字段关联时,关联字段必须做成匿名对象,且字段名名称、个数、顺序都必须完全一致。
上面讲的都是两个表关联,有时候需要用到多个表关联,以SQL为例,写法如下:
SELECT*
FROM dbo.Student ALEFT JOIN dbo.Books B ON A.id=b.StudentId
LEFT JOIN GoodsRecorde CON B.BookName=C.GoodsName
这样,再多几个表关联也无所谓,一路关联下去就OK了,Linq里面也一样:
var result =from ain arrBook
join b in arrStudent on a.StudentI dequals b.Id
join c in arrBaseInfo on a.BookId equals c.Id
select new { c.BaseName, b.Name,a.BookName };
仔细看其实也不难,再多几个表,顶多再多几个临时表中转而已,只是比普通SQL语句麻烦得不是一点点。
有时需要将几个查询结果拼成一个,如:
SELECT * FROM 销售记录 WHERE销售时间>'2001-01-01'
UNION ALL
SELECT * FROM 历史销售记录 WHERE 销售时间>'2001-01-01'
当然,可以一直UNION下去,表示将查询结果组合起来,去掉重复的,而UNIONALL表示全部的,包含重复的,UNION对应的Linq这么写:
var q = ( from c in db.Customers select c.Country ).Union
( from e in db.Employees select e.Country );
UNION ALL对应的可以这么写:
var q = ( from c in db.Customers select c.Phone ).Concat
( from c in db.Customers select c.Fax ).Concat
( from e in db.Employees select e.HomePhone );
前面的关联,都是以左边的为准,如果右边有多条记录,则左边的会通过重复来配对,最终返回一个“二维表”,有时,我们希望左边的不重复,类似“树”控件一样,采用一对多的方式返回,这种查询就需要用到“组连接”了,使用组连接时,返回的是一个键值对应结果集,键就是左边的表内容,值就是右边表记录列表,示例如下:
var result =from A in arrStudent
join B in arrBook on A.Id equals B.StudentId
into temp
select new {Name=A.Name,BookNames=temp };
StringBuilder sb =newStringBuilder();
foreach (var rin result)
{
sb.AppendLine(r.Name);
foreach (var m in r.BookNames)
sb.AppendLine(" "+m.BookName);
}
textBox1.Text = sb.ToString();
执行结果为:
张三
大话西游
武林歪转
李四
熊出没
示例中,直接把“临时表”作为字段名返回了,故可以在里面遍历,从而产生一对多的树形对象,对于使用主从关系对象时,该方法特别有用。
在SQL里面,如果对查询结果进行分类汇总,对非汇总的列必须使用GROUP BY分类汇总,也就是这些列相同的字段放一起,结果为一个二维表,假如原始表信息如下:
现在根据商品名称,对数量和金额进行分类汇总:
Linq中也可以用group by分组,但使用方法差别很大,Linq的分组功能得到的不是一个可遍历的数组,而是一个键值对应的可遍历对象(类似上面的组连接),“键”就是分组的依据,而“值”是该键下面的原始记录列表,我们来看一个实际例子。
首先定义一个类,模拟一条记录的定义:
private classGoodsRecorde
{
publicDateTime SaleDate {get;set; }
public string GoodsName {get;set; }
public string GoodsType {get;set; }
public decimal Price {get;set; }
public int Numbers {get;set; }
public decimal money {get;set; }
}
然后创建数据源,并分类汇总:
为避免图片中文字太小,上图关于创建数据源部分截图不完整,未截屏部分意义也不大。
示例中,按GoodsName分组,得到的结果中,GoodsName相同的记录归于一个键下面,最终的结果显示如下:
A
衣服,100,2
衣服,110,3
B
鞋子,50,2
鞋子,80,1
从本例不难看出,该分组和Sql差别很大,但更强大。
不仅仅可以直接使用字段名分组,还可以根据计算结果分组,上例中,假如销售额每100元为一个区间分组,我们可以这么写:
var result =from m inSaleRecords
group m by ((int)m.money) /100*100;
这时,得到的结果是:
200
衣服,100,2
300
衣服,110,3
100
鞋子,50,2
0
鞋子,80,1
当然,还可以使用where语句进行过滤。
上面的汇总总有一些遗憾,无法替代数据库本身的GROUP BY功能,其实借助临时表和Lambda表达式也能轻松实现,我们看下面一个例子:
var result =from m in arrGoods
group m bynew { m.GoodsName, m.GoodsType}
into g
select new {
g.Key.GoodsName,
g.Key.GoodsType,
Money=g.Sum(p=>p.money),
Count=g.Count(),
Avg=g.Average(p=>p.Price*p.Numbers)
};
StringBuilder sb =new StringBuilder();
foreach(var r inresult)
sb.AppendLine(r.GoodsName+":"+
r.GoodsType+":"+
r.Money+":"+
r.Count+":"
+r.Avg);
textBox2.Text = sb.ToString();
这里我们模拟普通Sql语句,按两列进行分类汇总,执行结果如下:
阿尔卡迪:衣服:500:1:500
老人头:衣服:400:2:200
耐克:鞋子:1200:2:600
现在的结果和我们普通查询就完全一样了。
重要提示:
回过头来,我们看看上面的分组语句group m bynew { m.GoodsName, m.GoodsType},m是范围变量,也就是前面说的“表的别名”,表示“可以使用”的字段列表,by后面的,表示分组的字段,前面例子中,都是直接使用了m.GoodsName等,本例中,创建了一个匿名对象,起到“按多列”分组的效果,当然,我们可以取个别名,如:group m bynew {NewName= m.GoodsName, NewType=m.GoodsType},同样的,范围变量m也可以是匿名对象,过滤出一些字段供后面使用(这在多表查询时避免字段名相同时特别有效),示例如下:
var result = from min arrGoods
group new { m.Numbers, m.money, NewPrice = m.Price }
by new {NewName = m.GoodsName, NewType = m.GoodsType }
into g
select new {
g.Key.NewName,
g.Key.NewType,
Money = g.Sum(p => p.money),
Count = g.Count(),
Avg = g.Average(p => p.NewPrice * p.Numbers)
};
上面已经模拟了数据库的分类汇总,但数据库往往是多个表联合查询,并对指定的列分类汇总,下面这段代码是网上抄的,不难理解。
var result =from a in TableA
join b in TableB on a.Id equals b.aId
where ((b.Type == 1 || b.Type == 2 || b.Type == 5) && b.State == 1)
group new { a.Id, b.Name, b.CreateDate }by new { a.Id, b.Name } into g
select new { Id = g.Key.Id, Name = g.Key.Name ?? "", Money = g.Sum(p => p.money) };
上面是多表联合查询,多列分组,并对部分计算列进行求和,COPY过去,改改就能完全胜任普通SQL的分组查询了。
在数据库开发中,为了让代码更直观简洁,我们经常会用到嵌套查询,也就是将内层的查询的结果作为外层查询的数据源。
string[] strs = {"alpha","beta","gama" };
var result =from min (
from n in strs
where n.Contains('a')
select n
)
where m.Length > 1
select m;
以上代码不难理解,先过滤出包含字母“a”的,再过滤出长度大于1的,用Lambda写估计更优雅。
var result = strs.Where(n => n.Contains('a')).
Where(m => m.Length > 1);
查询条件只需要返回true或false,因此,对于是否是嵌套还是普通计算表达式、语句块都无所谓。
string[] names = {"Tom","Dick","Harry","Mary","Jay" };
var result = names.Where(n => n.Length ==
names.OrderBy(n2 => n2.Length)
.Select(n2 => n2.Length).First()
);
以上是本人对Linq的一些粗浅理解,可以作为入门的参考,真正的大牛一定是理论+实践出来的,更多更深奥的知识需要在工作中不断学习和深入,百度Linq相关知识时,无意中发现的这个网站有非常全面的学习资料,感谢博主的无私奉献,需要系统学习的不妨去看看:
http://www.cnblogs.com/lifepoem/archive/2011/12/16/2288017.html