LINQ&Entity Framework
LinQ家族五大成员:
LinQ to Objects - 默认功能,用来实现对内存中集合对象的查询
LinQ to SQL - 针对SQL Server的查询,它是一个带有可视化的操作界面的ORM工具
LinQ to DataSet - 对强类型化或弱类型化的DataSet或独立的DataTable进行查询
LinQ to Entity - 对实体框架中EDM定义的实体集合进行查询。
LinQ to XML - 对XML文档进行查询创建等操作。
C#语法与LinQ相关的新增功能
1.隐式强类型变量
在C#3.0中可以使用var关键字隐式定义强类型局部变量。
《图1》
这里的var关键字定义变量与JavaScript定义变量看起来很像但二者有着本质的区别。
JavaScript定义的变量是弱类型的变量,也可理解为是一种通用类型的变量,它可以容纳各种类型的值,还可以在运行过程中动态修改其中的内容类型。下面这种写法在JavaScript中是正确的:
var obj = 3.14;
obj = "hello world";
C#中的var则是种强类型的变量,它在定义的时候会确定数据类型,分配内存空间。上面这两名代码在C#3.0中会报错,因为第一句已经把obj定义为double型的变量,第二句把字符串赋值给double是错误的操作。
隐式强类型并不能有效简化我的书写的代码,但当我们在用它来动态接收一些未知类型的数据的时候就显虽得很强大。在隐式强类型变量出现前,我们一般是使用object型变量来接收这种未知类型的数据的。
2.对象初始化
这个功能可以有效简化类的getter和setter部份的代码,还可以只使用一行表达式语句实现对象的实例化与初始化操作。
如定义实例类时可以使用如下代码,没有必要再把成员变量和属性分别定义了。
public class LineItem
{
public int OrderID { get; set; }
public int ProductID { get; set; }
public short Quantity { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public float Discount { get; set; }
}
实例化LineItem对象,并为它赋值
var line3 = new LineItem { OrderID = 11000, ProductID = 61, Quantity = 30, QuantityPerUnit = “12 1-kg cartons”, UnitPrice = 15.55M, Discount = 0.15F };
3.数组初始化
var LineItems = new[]
{
new LineItem {OrderID = 11000, ProductID = 11, Quantity = 10,
QuantityPerUnit = “24 500-g bottles”, UnitPrice = 15.55M, Discount = 0.0F},
new LineItem {OrderID = 11000, ProductID = 21, Quantity = 20,
QuantityPerUnit = “12 1-kg cartons”, UnitPrice = 20.2M, Discount = 0.1F},
new LineItem {OrderID = 11000, ProductID = 31, Quantity = 30,
QuantityPerUnit = “24 1-kg bags”, UnitPrice = 25.45M, Discount = 0.15F}
};
4.集合初始化
var LineItemsList = new List < LineItem >
{
new LineItem {OrderID = 11000, ProductID = 11, Quantity = 10,
QuantityPerUnit = “24 500-g bottles”, UnitPrice = 15.55M, Discount = 0.0F},
new LineItem {OrderID = 11000, ProductID = 21, Quantity = 20,
QuantityPerUnit = “12 1-kg cartons”, UnitPrice = 20.2M, Discount = 0.1F},
new LineItem {OrderID = 11000, ProductID = 31, Quantity = 30,
QuantityPerUnit = “24 1-kg bags”, UnitPrice = 25.45M, Discount = 0.15F}
};
5.匿名类型
过去我们要生成对象时,必须事先定义该对象的类,然后使用new关键字来实例化该类。匿名类型简化定义类的这个过程,我们可以使用new关键字直接把类的定义,类的实例化放在一个表达式语句中。
如:
var obj = new
{
Name = "zhangsan",
Age = 18,
URL = "http://hi.baidu.com/grayworm"
};
Console.WriteLine(obj.Name + obj.Age + obj.URL);
匿名类型主要用在LinQ to SQL中对字段的投影功能上:
var query = from i in LineItems select new { i.OrderID, i.ProductID, i.UnitPrice }
6.扩展方法
扩展方法就是为现有的类追加我们自定义的方法。在C#3.0的集成开发环境中,我们会发现带有向下箭头的方法,这些方法是我们在C#2.0中所没有见到的方法,这些方法就是我们所谓的“扩展方法”,它是C#3.0在C#2.0的基础上新增的一系列的方法,当然我们也可以为内置类添加我们自己的主扩展方法。
《图2》
例如在string中有个Length()扩展方法,它用来取得字符串的长度,但当字符串是null的时候调用该字符串的Length()时候会抛出异常。下面我们为String类添加一个自定义的方法LengthNullable(),如果字符串为null不抛出异常,而返回-1:
代码如下:
static class ExtensionMethods
{
public static int LengthNullable(this string test)
{
if (test != null)
{
return test.Length;
}
else
{
return -1
}
}
}
C#3.0的扩展方法需要单独写在一个public static的类中,并且扩展方法也应当用public static修饰。扩展方法的参数有三部份组成(this string test),第一部份是this关键字,它用来告诉编译器该方法是扩展方法;第二部份是该方法要追加到哪个类上,上面的例子代表该LengthNullable方法要追加到string类中去;第三部份是该类的实例名。
public static class ExtentionMethods
{
public static void Sleep(this Ren r)
{
Console.WriteLine(r.Name+" is sleeping.....");
}
}
public class Ren
{
public string Name { get; set; }
public int Age { get; set; }
public void Speak()
{
Console.WriteLine(Name + Age);
}
}
如果扩展方法与实例方法重名了,那在调用的时候只会调用到实例方法。
7.匿名方法
在C#2.0中就存在匿名方法,但很少有程序员去使用匿名方法,因为它的语法有些怪异。匿名方法的主要用法:使用代理来替代一些简单的方法。
大家都知道代理是指向方法的指针。如:
//声明代理
delegate void Delegate(int x);
//定义方法
void DoSomething(int y) { /* Something */ };
//把代理指向方法
Delegate d = obj.DoSomething;
这里的方法有方法名--DoSomething,而匿名方法可以让代理直接指向一个没有名子的方法。如:
//声明代理
delegate void Delegate(int x);
//把代理指向一个匿名方法。
Del d = delegate(int y) { /* Do Something */ };
匿名方法可以使代码变得更紧凑、更清晰、占用更少的资源。由于匿名方法与代理的关系很密切,所以有的人也称之为“匿名代理”。
在泛型集合List<T>中就有几个方法Exists()、Find()、FindAll()、RemoveAll()等方法,在Array类中也有类似的方法。这些方法都能够对泛型集合进行简单的查询操作,它们的方法签名如下所示:
《图3》
每个方法中都有个参数Predicate<T>,这个参数是个泛型代理,用来筛选数据。
如果不使用匿名方法,那我们得这样编写代码:
static bool HighUnitPrice(LineItem i)
{
if (i.UnitPrice > 25M)
return true;
else
return false;
}
LineItem obj = Array.Find(LineItems, HighUnitPrice);
先定义一个方法HighUnitPrice(LineItem i),然后在Array.Find()的参数Predicate<T>中调用该方法。如果有了匿名方法就不用再单独定义HighUnitPrice(LineItem i)方法了。
如:
var anon = LineItemsList.Find(
delegate(LineItem i)
{
return i.UnitPrice > = 25M;
}
);
在C#3.0中的好多地方都用到了匿名方法,匿名方法是理解Lambda表达式的基础。
8. Lambda表达式
Lambda表达式就是用很少的代码来实现匿名方法。
语法格式:
参数列表=>表达式
下面我们来看看如何把匿名方法转换为Lambda表达式:
匿名方法:
delegate(LineItem i) { return i.UnitPrice >= 25M; }
第一步:删除关键字delegate,变为(LineItem i) { return i.UnitPrice >= 25M; }
第二步:把花括号{}替的换为Lambda运算符=>,变为 (LineItem i) => return i.UnitPrice >= 25M;
第三步:去掉return和分号,语句变成表达式 (LineItem i) => i.UnitPrice >= 25M
第四步:由于编译器会自动进行类型推断,所以我们还可以把LineItem去掉。 i => i.UnitPrice >= 25M
这样就把匿名方法变成了Lambda表达式。
var anon = LineItemsList.Find( i = > i.UnitPrice > = 25M );
由此可见Lambda表达式是由Lambda运算符分割开的两部份组成。右边部分代表运算的语句块,左边部分代表的是运算需要的参数。
9.标准查询操作(Standard Query Operators SQO)
标准查询操作是对IEnumerable<T>接口追加的一系列的扩展方法。通过这些扩展方法对实现IEnumerable<T>接口的集合、数组进行一系列的查询操作。一些比较常用的扩展方法有Where()/OrderBy()/Select()等,LinQ标准查询操作我们将在后面详细阐述。
LinQ中的from关键字并不是扩展方法,因此它不是SQO。from只是为in关键字后的序列指定一个别名。
C#3.0的查询表达式是以一个或多个“from 别名 in 序列名”子句开始,以select或group子句结束。而join/let/where/orderby等子句是可选的,需要写在from和select/group子句中间。
下面是对内存中List<productList>集合进行查询的例子
var noStock = from p in productList
where p.UnitsInStock == 0
orderby p.Category, p.ProductID
select new { p.ProductID, p.Category, p.ProductName };
编译器会自动把上面的表达式语句翻译成下面的链式方法调用,在链式方法调用中使用的就是lambda表达式
var noStock = productList
.Where(p = > p.UnitsInStock == 0)
.OrderBy(p = > p.Category)
.ThenBy(p = > p.ProductID)
.Select(p = > new { p.ProductID, p.Category, p.ProductName });
10.IQueryable<T>接口
IQueryable<T>类型不是集合,他们是支持多态、优化、动态查询功能的LinQ查询序列,它能够把标准化查询操作(SQO)转换成表达式树。简而言之,IQueryable<T>接收的对象不是集合,而是查询表达式树。
我们可以使用IQueryable<T>的ToList()方法来把查询序列变成List<T>类型,使用ToArray()方法把查询序列变成数组类型。
IEnumerable<T>接口中有个AsQueryable()方法,该方法返回的是也IQueryable<T>类型。
LinQ to Objects是LinQ家庭的核心,其它的LinQ也使用了与LinQ to Objects相同的查询句法。最终编译器都是把LinQ句法翻译成扩展方法的链式表达式,同时把扩展方法中的lambda表达式转换成匿名类中的匿名方法,然后再把查询表达式编译成MSIL。
LinQ to SQL、LinQ to DataSets、LinQ to Entities和LinQ to XML则不是把查询表达式转换成MSIL,而是把查询表达式树转换成相应的特定查询语言。LinQ to SQL会生成T-SQL,LinQ to Entities会生成eSQL,LinQ to XML会生成XPath语句等。
LinQ标准查询操作符列表
《图1》
在VS2008及以后的版本中提供了LinQ的查询样例程序。
\Programe Files\Microsoft Visual Studio 2008(10.0 VS2010)\Samples\1033文件夹下有个压缩包CSharpSamples.zip,解压它,继续打开其中的文件夹
.\LinqSamples\SampleQueries\SampleQueries.sln,打开此解决方案,运行该项目,界面如下
《图2》
在左边选择示例,右侧上方会出现相应的LinQ代码,右侧下方会出现代码的运行结果。通过这个样例程序,我们可以学习LinQ的各种用法。
下面我们来学习LinQ常用操作符
一、筛选操作符Where
根据谓词对源序列的内容进行筛选,类似于SQL中的where子句。
1.简单where表达式
使用扩展方法
var query1 = CustomerList.Where(c => c.Country == “USA”);
使用查询表达式语法
query1 = from c in CustomerList where c.Country == “USA” select c;
使用扩展方法需要向where方法中传入lambda表达式。
2.复合where表达式
所谓的复合where表达式就是使用&&或||操作符对数据进行筛选
使用扩展方法
var query2 = CustomerList.Where(c => c.Country == “USA” && c.Orders.Any());
使用查询表达式语法
query2 = from c in CustomerList where c.Country == “USA” && c.Orders.Any() select c;
Any()方法相当于Count()>0,如果集合中有元素就返回true。
3.使用Index参数和IndexOf()方法
index值代表的是集合中元素的索引号,在where()中使用index可以控制返回与指定索引号相关的集合数据。泛是返回IEnumerable<T>类型的查询操作都可以使用index。
使用扩展方法
var query3 = CustomerList.Where((c, index) => c.Country == “USA” && index > 70);//返回国家是USA,索引号大于70的元素集合。
这里lambda表达式中接收的参数是(c,index),索引号做为第二个参数传入。
也可以使用IndexOf()方法来实现上面的功能,如果使用IndexOf()的话,那lambda表达式中传入的参数只需要一个就可以了。
使用扩展方法
var query3 = CustomerList.Where(c => c.Country == “USA” && CustomerList.IndexOf(c) > 70);
使用查询表达式语法
query3 = from cust in CustomerList where cust.Country == “USA”&& CustomerList.IndexOf(cust) > 70 select cust;
二、投影运算符
投影运算符对应SQL中的“select 列名”子句
(一)Select
Select操作符是从序列源返回一组指定属性
使用扩展方法
var infos = context.Infos.Where(p => p.Sex == true).Select(p => new { p.Name,p.Sex});
foreach (var c in infos)
{
Console.WriteLine(c.Name + c.Sex);
}
使用查询表达式语法
var infos = from p in context.Infos where p.Sex == true select new { p.Name, p.Sex };
foreach (var c in infos)
{
Console.WriteLine(c.Name + c.Sex);
}
如果使用扩展方法则在Select()方法中使用lambda表达式p=>new {p.Name,p.Sex}来对列进行投影;如果使用查询表达式语法的话,直接在select关键字后使用匿名类new { p.Name, p.Sex }即可。
(二)SelectMany
SelectMany操作符实际上实现的是相关数据的交叉连接操作。它根据lambda表达式从一对多的序列中返回指定的属性。
比如:
《图3》
查询男员工的所有家庭成员:
使用扩展方法
var q = context.Infos.Where(p => p.Sex == true).SelectMany(f=>f.Families);
foreach (var n in q)
{
Console.WriteLine(n.Name);
}
使用查询表达式语法
var q = from p in context.Infos where p.Sex==true from f in p.Families select f;
foreach (var n in q)
{
Console.WriteLine(n.Name);
}
三、分块操作符
(一)Skip和Take
Skip是从序列中跳过元素的个数;Take是从序列中获取元素的个数;
如:跳过集合的前2个元素,从第三个元素开始向后取4个元素。
使用扩展方法
var q = list.Skip(2).Take(4);
使用查询表达式语法
var q = (from p in list select p).Skip(2).Take(4);
(二)SkipWhile和TakeWhile
SkipWhile:条件跳过,从序列第一个元素开始依次判断,一直跳到不满足条件的元素为止,返回此元素及此元素之后的序列 ;
TakeWhile:条件抓取,从序列第一个元素开始依次判断,只要满足条件就进行下个元素判断,直到不满足条件的元素为止,返回此元素之前的序列 ;
如:取集合中第一批“性别”是“男”的元素的集合。
使用扩展方法
var q = list.SkipWhile(p => p.Sex == false).TakeWhile(p => p.Sex == true);
使用查询表达式语法
var q = (from p in list select p).SkipWhile(p => p.Sex == false).TakeWhile(p => p.Sex == true);
四、连接运算符
Join和GroupJoin操作符是把两个相互独立的对象通过关键属性关联起来。这种对象与对象的关联与SQL中的Join关联语法上有些不同。
1.LinQ的Join不支持SQL-92中的一些比较运算符,如>、<、<>等。它只支持相等运算符
2.在On子句中不能使用=来实现两个对象之间的关联,需要使用Equals运算符。
(一)Join
使用扩展方法
var list = infos.Join(works, p => p.Code, w => w.InfoCode, (p, w) => new { p.Name, w.Firm, w.Depart });
使用查询表达式语法
var list = from p in infos join w in works on p.Code equals w.InfoCode select new { p.Name, w.Firm, w.Depart };
(二)GroupJoin
可以实现外联效果
Join扩展方法与GroupJoin扩展方法签名有些不一样
《图4》
《图5》
在C#3.0查询表达式语法中没有GroupJoin语法,可以使用Join...into...来实现,它与Join不同的是,它可以实现类似于SQL外联接的效果,而Join只实现类似于SQL内联的效果。
使用查询表达式语法
var list = from p in infos join w in works on p.Code equals w.InfoCode into bo from r in bo.DefaultIfEmpty() select new { p.Name, r};
五、连接运算符
Concat运算符用来把两个序列连接到一个序列中,它类似于SQL中的关系或or运算符。
使用扩展方法
var q = infos.Select(p=>p.Name).Concat(works.Select(w=>w.Firm));
使用查询表达式语法
var q = (from p in infos select p.Name).Concat(from w in works select w.Firm);
六、排序运算符
排序运算符一共包含五个运算符OrderBy、OrderByDescending、ThenBy、ThenByDescending和Reverse
OrderBy:升序排序
OrderByDescending:降序排序
ThenBy:在OrderBy或OrderByDescending后实现多级排序中实现升序排序
ThenByDescending:在OrderBy或OrderByDescending后实现多级排序中实现降序排序
Reverse:顺序倒转
如:对所有人员先按照性别升序排序,再按照生日降序排序
使用扩展方法:
var q = infos.OrderBy(p => p.Sex).ThenByDescending(p => p.Birthday);
使用查询表达式语法:
var q = from p in infos orderby p.Sex,p.Birthday descending select p;
在查询表达式语法中实现多级排序类似于T-Sql中的方式。
七、分组操作符
分组操作符GroupBy用来按照元素的某个属性来对序列中的元素进行分组。类似于SQL中的group by 子句,但它是对象的序列,还可以获取每组中的每个元素对象。
如:按照性别对人员进行分组,并显示每组中人员的信息
使用扩展方法:
var q = infos.GroupBy(p=>p.Sex);;
使用查询表达式语法:
var q = from m in infos group m by m.Sex into g select g;
显示分组数据:
foreach (var item in q)
{
//item.Key代表分组后的关键字的值,在这里是性别Sex的值
Console.WriteLine("性别为"+item.Key+"\t共有"+item.Count()+"人");
foreach (var c in item)
{
Console.WriteLine("\t" + c.Name + "\t" + c.Sex);
}
Console.WriteLine("*********************");
}
《图6》
八、集合操作符
集合操作符包括Distinct、Union、Intersect和Except,除了Distinct之外其余的三个是用来把两个集合拼合成一个集合。
(一)Distinct
Distinct操作符用来把序列中重复的值移除掉,类似于SQL中的Distinct
如:查看Infos集合中所有
使用扩展方法:
var q = infos.Select(p => p.Nation).Distinct();
使用查询表达式语法:
var q = (from p in infos select p.Nation).Distinct();
(二)Union
Union操作符取两个具有相同结构的集合并集,如果两集合中有相同元素,则会自动滤去重复内容。而前面所讲的Concat操作符只是将两个集合进行合并,并不过滤重复元素。
如:两个集合,其中temp是infos的子集。
var infos = from p in context.Infos.ToList() select p;
var temp = infos.Where(p => p.Sex == true);
使用扩展方法:
var q = infos.Union(temp);
使用查询表达式语法:
var q = (from p in infos select p).Union(from m in temp select m);
运行结果中,子集的内容并没有重复出现
(三)Intersect
Intersect操作符是取两个具有相同结构的集合的交集部份。
如:两个集合,其中temp是infos的子集。
var infos = from p in context.Infos.ToList() select p;
var temp = infos.Where(p => p.Sex == true);
使用扩展方法:
var q = infos.Intersect(temp);
使用查询表达式语法:
var q = (from p in infos select p).Intersect(from m in temp select m);
运行结果中只显示子集中的内容。
(四)Except
Except操作符是从一个集合中取另一个集合的差集,即从集合A中取出集合B中不包含的元素。
如:两个集合,infos和temp,temp集合中包含了infos集合中不存在的元素
var infos = from p in context.Infos.ToList() select p;
var temp = infos.Where(p => p.Sex == true).ToList();
temp.Add(new Info
{
Code = "p100",
Name = "哈哈",
Sex = false,
Nation = "n004",
Birthday = DateTime.Now
});
使用扩展方法:
var q = infos.Except(temp);
使用查询表达式语法:
var q = (from p in infos select p).Except(from m in temp select m);
九、转换操作符,用来改变集合的类型
(一)ToArray
把集合转换为数组形式,不延迟
使用扩展方法:
Info[] g = infos.ToArray();
使用查询表达式语法:
Info[] g = (from p in infos select p).ToArray();
(二)ToList
把集合转换为泛型集合形式,不延迟
使用扩展方法:
List<Info> g = infos.ToList();
使用查询表达式语法:
List<Info> g = (from p in infos select p).ToList();
(三)ToDictionary
把集合转换成Dictionary<TKey,TElement>类型的集合,它每个元素的value值是原集合中的一个元素对象。
如:下面的代码把集合的内容转换为一个字典集合,字典的key值是人员代号,字典的value值是info元素对象。
使用扩展方法:
var q = infos.ToDictionary(p=>p.Code);
使用查询表达式语法:
var q = (from p in infos select p).ToDictionary(p=>p.Code);
调用语法
foreach (var item in q)
{
Console.WriteLine(item.Key+item.Value.Name);
}
从运行结果中我们可以看出,字典的第二个参数就是原集合中的元素。
(四)ToLookup
把集合转换成ILookup<TKey,TElement>类型的集合,ILookup<TKey,TElement>集合与Dictionary<TKey,TElement>集合不同的是:Dictionary<TKey,TElement>中Key和Value值一一对应,而ILookup<TKey,TElement>集合中Key和Value值是一对多的对应关系。
如:使用ILookup<TKey,TElement>把集合中的元素分组显示
使用扩展方法:
var q = infos.ToLookup(p=>p.Nation);
使用查询表达式语法:
var q = (from p in infos select p).ToLookup(p => p.Nation);
显示数据的代码:
foreach (var item in q)
{
Console.WriteLine(item.Key);
foreach (var single in item)
{
Console.WriteLine("\t" + single.Name);
}
}
显示效果:
《图7》
(原创:灰灰虫的家http://hi.baidu.com/grayworm)
十、相等操作符:SequenceEqual
用来对两个序列进行对比。如果所有元素的值相等,并且元素个数相等,并且元素的次序相等,那SequenceEqual操作符返回的是True,否则返回False
var s1 = infos.OrderBy(p => p.Code);
var s2 = infos.OrderByDescending(p=>p.Code);
var s3 = infos.OrderBy(p => p.Code);
Console.WriteLine(s1.SequenceEqual(s2)); //结果是False
Console.WriteLine(s1.SequenceEqual(s3)); //结果是True
十一、元素操作符:
元素操作符的作用是从IEnumerable<T>集合序列中返回一个指定的元素。
如果没有找到指定的元素,所有的XXXDefault操作符返回空对象,并不会产生异常。而First、Last、Single和ElementAt操作符则会产生异常。
(一)First和FirstOrDefault
如果序列中包含一个或多个元素,这两个操作符返回序列中的第一个元素。如果序列不包含任何元素,则FirstOrDefault操作符返回null值(引用类型)或默认值(值类型),而First操作符则产生异常信息。
如:查找第一个女生
使用扩展方法:
var item = infos.First(p=>p.Sex == false);
var item = infos.Where(p => p.Sex == false).First();
使用查询表达式语法:
var item = (from p in infos where p.Sex == false select p).First();
(二)Last和LastOrdefault
如果序列中包含一个或多个元素,这两个操作符返回序列中的最后一个元素。如果序列不包含任何元素,则LastOrDefault操作符返回null值(引用类型)或默认值(值类型),而Last操作符则产生异常信息。
如:查找最后一个女生
使用扩展方法:
var item = infos.Last(p=>p.Sex == false);
var item = infos.Where(p => p.Sex == false).Last();
使用查询表达式语法:
var item = (from p in infos where p.Sex == false select p).Last();
(三)Single和SingleOrDefault
如果序列中有且只有一个元素,则这两个操作符返回该元素
如果序列中没有任何元素,则Single会产生异常,而SingleOrDefault则会返回null值(引用类型)或默认值(值类型)
如果序列中包含多个元素,则这两个操作符都会产生异常。
(四)ElementAt和ElementAtOrDefault
这两个操作符是根据索引号从序列中返回指定的元素,如果未找到元素ElementAt()会产生异常,而ElementAtOrDefault()则会返回默认实例。
如:取出集合中的第二个女生
使用扩展方法:
var item = infos.Where(p=>p.Sex == false).ElementAt(2);
使用查询表达式语法:
var item = (from p in infos where p.Sex == false select p).ElementAt(2);
十二、元素数量操作符
判断序列中元素是否满足指定的条件返回bool型的值。带有该操作符的话句不能实现延迟查询,语句会被立即执行。
(一)Any
如果序列中存在任一个满足条件的元素,就返回true
如:判断是否存在代号为P005的人员
使用扩展方法:
var q = infos.Any(p=>p.Code == "p005");
var q = infos.Where(p => p.Code == "p005").Any();
使用查询表达式语法:
var q = (from p in infos where p.Code == "p005").Any();
(二)All
如果序列中所有元素都满足条件,就返回true
如:判断是否所有员工都是汉族
使用扩展方法:
var q = infos.All(p => p.Nation == "汉族");
使用查询表达式语法:
var q = (from p in infos select p).All(p=>p.Nation == "汉族");
(三)Contains
判断集合中是否包含指定的元素
十三、聚合操作符
聚合操作符类似于SQL中的聚合函数,所有带有聚合操作符的LinQ语句都无延迟功能,会被立即被执行。
(一)Count
取得序列中满足条件的元素的个数
使用扩展方法:
var q = infos.Count(p => p.Sex == false);
var q = infos.Where(p => p.Sex == false).Count();
使用查询表达式语法:
var q = (from p in infos where p.Sex==false select p).Count();
(二)Min、Max、Sum和Average
分别是取得序列中所有元素中某属性的最小值、最大值、总和、平均值等。
decimal? minFreightCharge = OrderList.Min(c => c.Freight);
decimal? maxFreightCharge = OrderList.Max(c => c.Freight);
decimal? sumFreightCharge = OrderList.Sum(c => c.Freight);
decimal? avgFreightCharge = OrderList.Average(c => c.Freight);
总结:
这一篇文章主要介绍了LinQ的标准查询操作符(SQO)。对每个操作符都介绍了使用扩展方法的使用和查询表达式的使用,对于这两种用法大家应当都掌握住,尤其要记住扩展方法的使用。
由于时间的原因没有对每个示例的运行结果抓图显示,朋友们可以自己测试一下结果。
上面的内容是LinQ to Object的基础语法,也是其它LinQ的基础,熟练使用这些操作符能够使我们在LinQ天地中自由驰骋。
LinQ to SQL可以用来取代传统的基于SQL语句的查询操作。在以后的数据访问层(DAL)中,我们可以使用LinQ to SQL实现数据库的CRUD操作,在执行的时候.net框架会把LinQ to SQL查询表达式转换成对应的SQL语句再去执行。使用LinQ to SQL可以借助于LinQ语法大大简化我们数据访问的代码量,并且还具有编译检查、智能感知和强类型表达式等优点。
LinQ to SQL从严格意义上来说不能算是一个ORM框架,它只对SQL Server起作用,并不能实现对各种关系型数据库进行透明的映射,所以我们通常把LinQ to SQL称为SQL Mapping框架。
LinQ to SQL都是对Table<TEntity>进行操作,对于“一对多”的关联(如:Customers.Orders)它会使用EntitySets集合类型的成员来表示,而对于“多对一”的关联(如:Order.Customer)它会使用EntityRefs类型成员来表示。LinQ to SQL并不支持“多对多”的直接关联操作。
一、LinQ to SQL的ORM
LinQ to SQL不但仅仅实现了对象/关系之间的映射,还提供了一个简单易用的图形化界面工具。通过这个工具可以为SQL200X每个表生成一个实体类,并在底层有关联的表的实体类之间生成一个实体关联,把数据库中表和表之间的“关联关系”彻底转换为对象与对象之间的“关联关系”。通过此关联可以直接访问到该对象和与该对象相关联的其它对象,不用再通过Join子句来实现多表关联查询了。这种关联实体的功能是LinQ和实体框架的重要功能。
这种把数据库中的表和外键封装成类和类之间的关联的优势在于开发人员可以把数据库中的抽象数据设计成现实生活中的对象,依照现实生活中的对象来管理代码世界中的对象数据。
使用LinQ to SQL的图形化界面快速实现表与实体之间的映射
1.在项目中添加新项,选择"LinQ to SQL Classes"
《图1》
2.打开“服务器资源管理器(Server Explorer)”选中要映射的表,并把它们拖到.dbml的设计器界面中。
《图2》
通过上面两步,不用写代码就可以实现出表与实体之间的映射。下面我们看看生成的代码结构。
《图3》
从图中我们看出,在我们这简单的拖动过程中,VS为我们生成了三个文件:MyDB.dbml、MyDB.dbml.layout、MyDB.designer.cs
这三个类的代码分别如下所示:
《图4》
MyDB.dbml-XML文件,描述了表与实体之间的映射关系。如果我们直接从“服务器资源管理器”中向.dbml拖动表来使用LINQ的话,那些XML映射文件并不会起做用。因为LinQ还有映射方式:使用Attributes来映射,当我们从“服务器资源管理器”中直接拖动表到.dbml上去的时候,就是使用Attribute来映射的。在下面的MyDB.designer.cs文件中我们可以看到类和属性的上都带有Attributes属性,它用来描述实体类与表的映射关系。这种使用Attributes来实现映射,不是一种很好的映射方式,因为映射没有与代码完全分离,修改映射的过程本还需要对源代码进行修改,但这种使用LinQ的方式最为简单,所以也就成为大多数LinQ程序员的选择。
MyDB.dbml.layout-XML文件,描述了在LinQ to SQL图形化设计界面中图示的布局位置信息
MyDB.designer.cs-CSharp源代码文件,根据数据库的表结构,生成的实体类和DataContext类。
如果只从应用的角度上来讲,我们不用分析这三个文件的代码。但对于MyDB.designer.cs其中的类我们需要大体来看一下。
MyDB.desinger.cs文件中包含了系统自动生成的类,这些类大部份是数据库中表所对应的实体类,类的名子一般是以表名进行命名,如:Info,Family,Work,Nation,Title。另外还有一个类MyDBDataContext类,从字面意思上理解,该类是“MyDB数据上下文”,所谓的“上下文”就是指“硬盘上的关系型数据库”和“内存中的实体对象”,MyDBDataContext类的作用就是在“硬盘上的关系型数据库”和“内存中的实体对象”之间起一个桥梁的作用。这个类的代码中包含一系列的Table<TEntity>型的属性,可以使用它们来把数据库中记录的集合变换成内存中对象的集合,以便操作。
在MyDB.desinger.cs文件的实体类中,包含诸多成员变量和属性的定义,这个成员变量除了对应于数据库中字段名,但还出现了EntitySet<TEntity>和EntityRef<TEntity>两种类型的成员变量,正如我们上面所讲到的,这两种类型的变量分别用于实现对象与对象之间的关联关系。EntitySet<TEntity>是实体集合的引用,它指向与当前对象相关联的其它实体集合(1:m);EntityRef<TEntity>是实体对象引用,它指向与当前对象相关联的其它实体对象的引用(m:1)。
《图5》
上面是Info表的实体类,除了_Code,_Name,_Sex,_Nation,_Birthday等数据库字段变量外,还有_Families和_Works,这两个成员变量对应Family表和Work表中,与当前对象相关联的实体对象的集合;而_Nation1则是与当前对象相关联的民族实体对象。
二、使用LinQ to SQL实现数据库的查询操作
LinQ to SQL的数据库操作是大多数程序员所钟情的功能,因为它能够把LinQ查询表达式自动转换为相应的SQL语句进行处理,这样就不用再花太多的时间去编写实体类和数据访问类了。LinQ to SQL的查询语句与LinQ to Objects语法很相似,只是LinQ to Objects是把LinQ表达式转换为中间语言,而LinQ to SQL是把LinQ表达式转换为SQL语句,送到数据库去执行。
下面以一个简单的例子来展示一下LinQ to SQL的使用,具体语法请参照上一篇文章。
示例:
在Info表中查询回族男生和汉族的女生中姓张的同学的信息
第一步:新建MyDB.dbml,并从服务器资源管理器中把相应的表拖进MyDB.dbml
第二步:添加新页面
第三步:实例化MyDBDataContext对象
MyDBDataContext context = new MyDBDataContext();
第三步:编写LinQ查询
var q = from p in context.Infos where p.Name.StartsWith("张") &&((p.Nation == "n002" && p.Sex == true) || (p.Nation == "n001" && p.Sex == false) ) select p;
或
var q = context.Infos.Where(p => p.Nation == "n002").Where(p => p.Sex == true).Concat(context.Infos.Where(p => p.Nation == "n001").Where(p => p.Sex == false)).Where(p=>p.Name.StartsWith("张"));
第四步:把查询序列转换成集合,并显示结果
var list = q.ToList();
foreach (Info item in list)
{
Console.WriteLine(item.Code + "\t" + item.Name + "\t" + (item.Sex.Value?"男":"女") + "\t" + item.Nation1.Name);
}
三、使用LinQ实现增、删、改操作
(一)添加操作
第一步:实例化DataContext对象
MyDBDataContext context = new MyDBDataContext();
第二步:生成实体对象
Info data = new Info
{
Code = "x004",
Name = "马大哈",
Sex = false,
Nation = "n001",
Birthday = new DateTime(1989, 12, 28)
};
第三步:向DataContext对象的Table<TEntity>集合中注册添加上一步中生成的实体对象
context.Infos.InsertOnSubmit(data);
也可以使用Table<TEntity>集合的InsertAllOnSubmit(IEnumberable<T>)方法一次注册添加多个新对象,以便一次性向数据库插入这些数据。
第四步:使用DataContext对象提交更改
context.SubmitChanges();
(二)修改操作
第一步:实例化DataContext对象
MyDBDataContext context = new MyDBDataContext();
第二步:使用DataContext对象查询数据库中需要修改的内容,返回对应的实体对象
Info data = context.Infos.Where(p => p.Code == "x004").First();
第三步:修改上一步中实体对象中的值
data.Name = "马也";
data.Sex = true;
data.Nation = "n002";
第四步:使用DataContext对象提交更改
context.SubmitChanges();
修改数据时,由于要修改的数据本身就是数据库中现有的数据,所以不用像插入操作那样使用context.Infos.InsertOnSubmit(data)注册数据,直接修改数据并提交更改即可,
(三)删除操作
第一步:实例化DataContext对象
MyDBDataContext context = new MyDBDataContext();
第二步:使用DataContext对象查询数据库中需要修改的内容,返回对应的实体对象
Info data = context.Infos.Where(p => p.Code == "x004").First();
第三步:向DataContext对象的Table<TEntity>集合中注册删除上一步中查出的对象
context.Infos.DeleteOnSubmit(data);
也可以使用Table<TEntity>集合的DeleteAllOnSubmit(IEnumberable<T>)方法一次注册删除多个新对象,以便一次性向数据库删除这些数据。
第四步:使用DataContext对象提交更改
context.SubmitChanges();
上面的增、删、改查代码只有在context.SubmitChanges()调用的时候才提交数据库执行对应操作。我们可以在执行context.SubmitChanges()之前编写多项数据的增、删、改代码,然后使用context.SubmitChanges()实现一次性提交。context.SubmitChanges()方法自身带有事务功能,我们不必手动编写事务实现数据库的修改。
四、使用LinQ调用存储过程
在开发人员与DBA之间总是存在一种争论:使用在程序中使用存储过程是不是一种较好的解决方案?下面我们从四方面来看一下:
1.访问控制:
如果使用存储过程,可以为数据库创建自定义的用户或角色,并授权他们访问指定的存储过程,以提高数据操作的安全性。
2.SQL注入攻击
这种攻击通常是在执行动态SQL语句的时候发生,保存动态SQL语句的变量被恶意用户利用,通过输入的内容与原有SQL语句进行组合形成新的、具有攻击性的语句来对数据库操作。LinQ to SQL使用参数化查询方式来使用动态T-SQL语句,这种参数化的T-SQL语句能够很好的避免SQL注入攻击。所以并不是只有存储过程才能够解决SQL注入攻击。
3.性能
在SQL Server6.5或更早版本中,对存储过程可以实现部份编译的功能,当调用存储过程的时候可以加快SQL代码执行速度。在SQL Server7.0和以后的版本中对于存储过程和SQL语句都具有编译功能。因此,一般说来在SQL Server7.0以后的数据库版本,存储过程的执行与参数化的SQL代码执行效率没有什么太大的区别。
4.结构独立性
如果数据库的结构发生了变化,那与之相关的存储过程也需要重新编写、测试。如果软件升级时,不但需要重新编写、测试存储过程,还需要涉及到存储过程的更新。如果使用DAL来实现数据库操作的话,可以借助于代码生成工具,来实现数据访问代码的“半自动修改”,使用DAL层可以把数据库结构改变的影响控制在一个有限的局部范围内。
(一)添加存储过程
从服务器资源管理器中把存储过程直接拖放到.dbml界面中,在界面的右侧会出现相应的存储过程。
《图6》
右击.dbml界面中的存储过程,点击“属性”,打开属性面版,可以在这里修改存储过程的属性,在这里我们修改比较多的是Return Type,它代表存储过程的返回类型。
《图7》
在这个拖动过程中,会在.designer.cs中生成两段代码:一段是实体类,用来代表存储过程用到的实体对象(这个类只在调用增、删、改的存储过程中出现),如图8;另一段是在DataContext类中,用来实现存储过程的调用,如图9。
《图8》
《图9》
下面我们来看如何使用存储过程实现CRUD的操作。
(二)使用存储过程实现查询操作
1.把存储过程拖到.dbml界面中去。
2.编写代码调用存储过程
《图10》
在调用存储过程中,可以直接使用DataContext调用对应的存储过程调用方法,如果存储过程需要参数的话,我们可以把参数值直接传递给方法。
在LinQ中存储过程调用默认返回的类型是ISingleResult<T>类型,它实现了IEnumerable<T>接口,可以使用ToList()方法把结果转换为List<T>,也可以直接绑定到ObjectDataSource控件中。ISingleResult<T>很像Table<TEntity>但比Table<TEntity>更简单一些。
(三)使用存储过程实现增、删、改操作
使用存储过程实现增、删、改操作可以有两种方式来实现:一种是使用DataContext对象直接调用存储过程调用方法。另一种是修改实体类的默认方法,把增、删、改操作默认方法指定为相当的存储过程。
第一种方法,使用DataContext对象直接调用存储过程的调用方法
1.把存储过程拖到.dbml界面中去。
2.编写代码调用存储过程
《图11》
第二种方法,修改实体类的默认方法
1.把存储过程拖到.dbml界面中去。
2.在.dbml文件中相应实体类上右击,选择“属性”,打开属性面版。
《图12》
3.在属性面版的Default Methods类别中单击相应的方法,打开对话框。
《图13》
在class下拉列表中选择要操作的实体类,在Behavior下拉列表中选择要对表进行的操作,在Customize下拉列表中选择对应的存储过程,在最下面的二维表格中选择存储过程参数与类的属性的对应关系。
4.编写代码对数据库进行增、删、改操作
《图14》
五、使LinQDataSource数据源控件
在Web应用程序中,可以使用数据源控件来向界面提供绑定的数据。LinQ to SQL能够为两种数据源对象提供数据:ObjectDataSource和LinqDataSource。
LinqDataSource控件是VS2008中的新增的数据源控件,它只能绑定到LinQ to SQL的DataContext.Table<TEntity>对象。用它向Web控件提供数据,并可以实现对数据的排序和分页操作。使用GridView、DetailsView、FormView等控件与LinqDataSource控件绑定的时候,不需要编写代码就可以实现数据的插入、修改、删除操作。
ObjectDataSource控件也可以使用LinQ to SQL获取数据,因为DataContext.Table<TEntity>对象实现了IEnumerable<T>接口。当然我们也可以把Table<TEntity>对象转换成其它集合对象实现绑定。
下面我们一起来看一下,如何使用LinQ to SQL来实现数据绑定
第一步:添加LinQ to SQL类文件.dbml,并从“服务器资源管理器”拖动表到.dbml设计界面,生成LinQ to SQL类。
第二步:从工具箱中把LinqDataSource控件拖到界面中来。
第三步:在LinqDataSource控件的智能菜单中单击“配置数据源”,打开配置向导窗口。
第四步:在"Choose your context object"下拉列表中选择***DataContext。点击“Next”。
《图15》
第五步:在Table下拉列表中选择要在界面中显示的Table集合,在下面的列的列表中选择要显示的列。点击“Next”。
《图16》
第六步:(可选)如果想获取表中一部份数据的话,点击右侧的where按钮,在弹出对话框中来配置where表达式。这个配置界面与SqlDataSource和ObjectDataSource控件很像,在此不多说了。点击“OK”。
《图17》
第七步:(可选)如果要对显示的数据进行排序的话,请在图16中点击“OrderBy”按钮,在弹出对话框中设置排序规则。点击“OK”。
《图18》
第八步:(可选)如果想让LinqDataSource控件具有增、删、改的功能,在图16中点击“Advanced”按钮,在弹出下面对话框,在对话框中有三个复选框,只要选中它们,就可以实现增、删、改的功能。
《图19》
第九步:点击“Finish”按钮完成配置工作。
第十步:从工具箱中拖GridView到界面中来,设置DataSourceID为上面的LinqDataSource控件的ID。运行页面,出现效果如下:
《图20》
从图20中我们可以看出,表中的数据都被显示出来了,但是“Nation”这一列中显示的是代号,因为这一列是外键列,如何把它显示成民族名称呢。
在上面绑定数据的基础上,继续进行如下设置
第十一步:打开GridView的编辑列对话框。把EntityRef型字段(Nation1)添加到选中列中,再删除原有的Nation绑定列。
《图21》
第十二步:把Nation1转换为模板列。
第十三步:修改模板列的绑定代码如下:
《图22》
运行页面,看到民族名称显示出来了。
《图23》
至于Sex和Birthday字段的格式化显示,请参阅另外两篇文章:GridView详解 和 ListView详解
六、延迟加载与即时加载
延迟加载(Lazy Loading):只有在我们需要数据的时候才去数据库读取加载它。
优点:较好的性能,有效节省内存资源。
缺点:会产生多次与数据库之间的交互。
即时加载(Eager Loading):在加载数据时就把该对象相关联的其它表的数据一起加载到内存对象中去。
优点:能够一次性地把数据全加载到内存,不用反复与数据库之间进行交互操作
缺点:占用服务器内存较多,在加载相关联数据的时候,会用到连接查询,会降低性能。
在默认情况下,LinQ to SQL加载数据使用的是延迟加载。比如:在我加载某个Info对象的时候,并不会立即加载该Info对象的Works、Families和Nation1三个成员对象,只有在我们访问到该对象的这三个成员的时候才会动态加载该Info对象相对应的三个对象的内容。
与延迟加载密切相关的属性有两个:DataContext.DeferredLoadingEnabled和DataContext.LoadOptions
DataContext.DeferredLoadingEnabled:是否采用延迟加载。True-(默认值)采用延迟加载;False-不采用延迟加载(也不采用即时加载,加载的时候只加载当前对象的数据。当我们访问相关联子对象的时候也不会动态加载子对象的数据)
DataContext.LoadOptions:加载选项,用来指定那些子对象采用即时加载方式。
首先,我们来看看在默认情况下,即“延迟加载”的情况下,DataContext对象的结构:
《图24》
从图24我们可以看出,在默认情况下,DataContext对象DeferredLoadingEnabled=true,即采用了延迟加载的方式。LoadOptions=null,即没有对任何子对象进行即时加载操作。
下面我们再看看DataContext中Table<TEntity>的情况:
《图25》
从图25中我们可以看出,当我们访问Info对象的Works集合时,集合中会包含对应的数据,这些数据就是在访问该Works属性时动态从数据库中提取出来的。图中虽然有个IsDeffered=false,但这不代表该Works集合没有延迟加载,它代表Works集合是否正处于延迟状态,还未被执行查询。
再从生成的SQL语句中来看
《图26》
我们看到生成的SQL语句仅仅是对Info表的数据进行查询。并没有查询Works表和Families表中的数据,这两个表中的数据是在后面被延迟加载的。
下面我们再看看“非延迟加载”的情况。
需要大家注意的是:“非延迟加载”并不一定是“即时加载”,它两个不是一个概念。
要实现“非延迟加载”需要把DataContext对象的DeferredLoadingEnabled属性设为False。
然后我们再来看DataContext中的Table<TEntity>的情况:
《图27》
从上图中我们看出在“非延迟加载”情况下,并不会自动加载子对象的数据。
它所生成的语句与 图26是一样的。不一样的是,当我们访问Works集合时并没有动态加载相应的数据。
最后我们再来看看“即时加载”,“即时加载”与DataContext.DeferredLoadingEnabled的值没有太大的关系。它主要与DataContext.LoadOptions有关。
“即时加载”就是在加载Info对象的时候不只加载该对象的基本信息,它会把该对象的相关子对象(EntitySet和EntityRef)的数据一起加载出来,而不是等到用到的时候再加载。
下面我们看看如何使用“即时加载”来加载Info对象。
《图28》
从上图中我们可以看出,虽然DataContext.DeferredLoadingEnabled=false,但由于我使用了“即时加载”来加载了“Nation1”的数据,所以Nation1是有内容的。而Works和Families依然是Count=0
下面我们再看看“即时加载”生成的SQL语句
《图29》
从图中我们看到这是一个左连接的SQL语句,在查询的时候,不再只查Info表的数据,而是把相应的Nation表的数据一起查出来了。这就是即时加载的原理。
注意:对于一个DataContext对象,DataContext.LoadOptions属性只能赋一次值,一旦赋完值,我们将不能修改这个值了。因此,虽然DataLoadOptions提供了灵活的“即时加载”功能,但使用的时候一定要考虑清楚再使用。
附:LinQ的查询管道