什么是Linq to sql
Linq to sql(或者叫DLINQ)是LINQ(.NET语言集成查询)的一部分,全称基于关系数据的 .NET 语言集成查询,用于以对象形式管理关系数据,并提供了丰富的查询功能,它和Linq to xml、Linq to objects、Linq to dataset、Linq to entities等组成了强大的LINQ。
要学好LINQ查询语法,就不得不先理解C# 3.0的一些新特性,下面一一简单介绍。
隐含类型局部变量
var age = 26; var username = "zhuye"; var userlist = new [] {"a","b","c"}; foreach(var user in userlist) Console.WriteLine(user);
|
纯粹给懒人用的var关键字,告诉编译器(对于CLR来说,它是不会知道你是否使用了var,苦力是编译器出的),你自己推断它的类型吧,我不管了。但是既然让编译器推断类型就必须声明的时候赋值,而且不能是null值。注意,这只能用于局部变量,用于字段是不可以的。
匿名类型
var data = new {username = "zhuye",age = 26}; Console.WriteLine("username:{0} age:{1}", data.username, data.age);
|
匿名类型允许开发人员定义行内类型,无须显式定义类型。常和var配合使用,var用于声明匿名类型。定义一个临时的匿名类型在LINQ查询句法中非常常见,我们可以很方便的实现对象的转换和投影。
扩展方法
public static class helper { public static string MD5Hash(this string s) { return System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(s,"MD5"); }
public static bool In(this object o, IEnumerable b) { foreach(object obj in b) { if(obj==o) return true; } return false; } }
// 调用扩展方法 Console.WriteLine("123456".MD5Hash()); Console.WriteLine("1".In(new[]{"1","2","3"}));
|
很多时候我们需要对CLR类型进行一些操作,苦于无法扩展CLR类型的方法,只能创建一些helper方法,或者生成子类。扩展方法使得这些需求得意实现,同时也是实现LINQ的基础。定义扩展方法需要注意,只能在静态类中定义并且是静态方法,如果扩展方法名和原有方法名发生冲突,那么扩展方法将失效。
自动属性
public class Person { public string username { get; protected set; } public int age { get; set; }
public Person() { this.username = "zhuye"; } }
Person p = new Person(); //p.username = "aa"; Console.WriteLine(p.username);
|
意义不是很大,纯粹解决机械劳动。编译器自动为你生成get、set操作以及字段,并且你不能使用字段也不能自定义get、set操作,不过你可以分别定义get和set的访问级别。
DataContext
DataContext类型(数据上下文)是System.Data.Linq命名空间下的重要类型,用于把查询句法翻译成SQL语句,以及把数据从数据库返回给调用方和把实体的修改写入数据库。
DataContext提供了以下一些使用的功能:
l 以日志形式记录DataContext生成的SQL
l 执行SQL(包括查询和更新语句)
l 创建和删除数据库
DataContext是实体和数据库之间的桥梁,那么首先我们需要定义映射到数据表的实体。
定义实体类
using System.Data.Linq.Mapping;
[Table(Name = "Customers")] public class Customer { [Column(IsPrimaryKey = true)] public string CustomerID {get; set;}
[Column(Name = "ContactName")] public string Name { get; set; }
[Column] public string City {get; set;} } |
以Northwind数据库为例,上述Customers类被映射成一个表,对应数据库中的 Customers表。然后在类型中定义了三个属性,对应表中的三个字段。其中,CustomerID字段是主键,如果没有指定Column特性的Name属性,那么系统会把属性名作为数据表的字段名,也就是说实体类的属性名就需要和数据表中的字段名一致。
现在,创建一个ASP.NET页面,然后在页面上加入一个GridView控件,使用下面的代码进行绑定数据:
using System.Data.Linq;
DataContext ctx = new DataContext("server=xxx;database=Northwind;uid=xxx;pwd=xxx"); Table<Customer> Customers = ctx.GetTable<Customer>(); GridView1.DataSource = from c in Customers where c.CustomerID.StartsWith("A") select new {顾客ID=c.CustomerID, 顾客名=c.Name, 城市=c.City}; GridView1.DataBind(); |
使用DataContext类型把实体类和数据库中的数据进行关联。你可以直接在DataContext的构造方法中定义连接字符串,也可以使用IDbConnection:
using System.Data.SqlClient;
IDbConnection conn = new SqlConnection("server=xxx;database=Northwind;uid=xxx;pwd=xxx"); DataContext ctx = new DataContext(conn); |
之后,通过GetTable获取表示底层数据表的Table类型,显然,数据库中的Customers表的实体是Customer类型。随后的查询句法,即使你不懂SQL应该也能看明白。从Customers表中找出CustomerID以“A”开头的记录,并把CustomersID、Name以及City封装成新的匿名类型进行返回。
结果如下图:
示例数据库
字段名 |
字段类型 |
允许空 |
字段说明 |
ID |
uniqueidentifier |
|
表主键字段 |
UserName |
varchar(50) |
|
留言用户名 |
PostTime |
datetime |
|
留言时间 |
Message |
varchar(400) |
√ |
留言内容 |
IsReplied |
bit |
|
留言是否回复 |
Reply |
varchar(400) |
√ |
留言管理员回复 |
在数据库中创建一个名为GuestBook的数据库,在里面创建一个tbGuestBook的表,结构如上表。
生成实体类
右键点击网站项目,选择添加新项,然后选择“Linq to sql Classes”,命名为GuestBook。然后打开App_Code里面的GuestBook.dbml。设计视图上的文字提示你可以从服务器资源管理器或者攻击箱拖动项到设计界面上来创建实体类。
那么,我们就在服务器资源管理器中创建一个指向GuestBook数据库的数据连接,然后把tbGuestBook表拖动到GuestBook.dbml的设计视图上,按CTRL+S保存。打开GuestBook.designer.cs可以发现系统自动创建了GuestBook数据库中tbGuestBook表的映射,如下图:
select
描述:查询顾客的公司名、地址信息
查询句法:
var 构建匿名类型1 = from c in ctx.Customers select new { 公司名 = c.CompanyName, 地址 = c.Address }; |
对应SQL:
SELECT [t0].[CompanyName], [t0].[Address] FROM [dbo].[Customers] AS [t0] |
描述:查询职员的姓名和雇用年份
查询句法:
var 构建匿名类型2 = from emp in ctx.Employees select new { 姓名 = emp.LastName + emp.FirstName, 雇用年 = emp.HireDate.Value.Year }; |
对应SQL:
SELECT [t0].[LastName] + [t0].[FirstName] AS [value], DATEPART(Year, [t0].[HireDate]) AS [value2] FROM [dbo].[Employees] AS [t0] |
描述:查询顾客的ID以及联系信息(职位和联系人)
查询句法:
var 构建匿名类型3 = from c in ctx.Customers select new { ID = c.CustomerID, 联系信息 = new { 职位 = c.ContactTitle, 联系人 = c.ContactName } }; |
对应SQL:
SELECT [t0].[CustomerID], [t0].[ContactTitle], [t0].[ContactName] FROM [dbo].[Customers] AS [t0] |
描述:查询订单号和订单是否超重的信息
查询句法:
var select带条件 = from o in ctx.Orders select new { 订单号 = o.OrderID, 是否超重 = o.Freight > 100 ? "是" : "否" }; |
对应SQL:
SELECT [t0].[OrderID], (CASE WHEN [t0].[Freight] > @p0 THEN @p1 ELSE @p2 END) AS [value] FROM [dbo].[Orders] AS [t0] -- @p0: Input Currency (Size = 0; Prec = 19; Scale = 4) [100] -- @p1: Input String (Size = 1; Prec = 0; Scale = 0) [是] -- @p2: Input String (Size = 1; Prec = 0; Scale = 0) [否] |
普通存储过程
首先在查询分析器运行下面的代码来创建一个存储过程:
create proc sp_singleresultset as set nocount on select * from customers |
然后打开IDE的服务器资源管理器,之前我们从表中拖动表到dbml设计视图,这次我们从存储过程中找到刚才创建的存储过程,然后拖动到设计视图。在方法面板中可以看到已经创建了一个sp_singleresultset的方法,如下图:
然后打开Northwind.designer.cs,可以找到下面的代码:
[Function(Name="dbo.sp_singleresultset")] public ISingleResult<sp_singleresultsetResult> sp_singleresultset() { IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod()))); return ((ISingleResult<sp_singleresultsetResult>)(result.ReturnValue)); } |
延迟执行
IQueryable query = from c in ctx.Customers select c; |
这样的查询句法不会导致语句立即执行,它仅仅是一个描述,对应一个SQL。仅仅在需要使用的时候才会执行语句,比如:
IQueryable query = from c in ctx.Customers select c; foreach (Customer c in query) Response.Write(c.CustomerID); |
如果你执行两次foreach操作,将会捕获到两次SQL语句的执行:
IQueryable query = from c in ctx.Customers select c; foreach (Customer c in query) Response.Write(c.CustomerID); foreach (Customer c in query) Response.Write(c.ContactName); |
对应SQL:
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0]
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0] |
对于这样的需求,建议你先使用ToList()等方法把查询结果先进行保存,然后再对集合进行查询:
IEnumerable<Customer> customers = (from c in ctx.Customers select c).ToList(); foreach (Customer c in customers) Response.Write(c.CustomerID); foreach (Customer c in customers) Response.Write(c.ContactName); |
延迟执行的优点在于我们可以像拼接SQL那样拼接查询句法,然后再执行:
var query = from c in ctx.Customers select c;
var newquery = (from c in query select c).OrderBy(c => c.CustomerID);
检测并发
首先使用下面的SQL语句查询数据库的产品表:
select * from products where categoryid=1 |
查询结果如下图:
为了看起来清晰,我已经事先把所有分类为1产品的价格和库存修改为相同值了。然后执行下面的程序:
var query = from p in ctx.Products where p.CategoryID == 1 select p; foreach (var p in query) p.UnitsInStock = Convert.ToInt16(p.UnitsInStock - 1); ctx.SubmitChanges(); // 在这里设断点 |
我们使用调试方式启动,由于设置了断点,程序并没有进行更新操作。此时,我们在数据库中运行下面的语句:
update products set unitsinstock = unitsinstock -2, unitprice= unitprice + 1 where categoryid = 1 |
然 后在继续程序,会得到修改并发(乐观并发冲突)的异常,提示要修改的行不存在或者已经被改动。当客户端提交的修改对象自读取之后已经在数据库中发生改动, 就产生了修改并发。解决并发的包括两步,一是查明哪些对象发生并发,二是解决并发。如果你仅仅是希望更新时不考虑并发的话可以关闭相关列的更新验证,这样 在这些列上发生并发就不会出现异常:
[Column(Storage="_UnitsInStock", DbType="SmallInt", UpdateCheck = UpdateCheck.Never)] [Column(Storage="_UnitPrice", DbType="Money", UpdateCheck = UpdateCheck.Never)] |
为这两列标注不需要进行更新检测。假设现在产品价格和库存分别是27和32。那么,我们启动程序(设置端点),然后运行UPDATE语句,把价格+1,库存-2,然后价格和库存分别为28和30了,继续程序可以发现价格和库存分别是28和31。价格+1是之前更新的功劳,库存最终是-1是我们程序之后更新的功劳。当在同一个字段上(库存)发生并发冲突的时候,默认是最后的那次更新获胜。
论坛表结构
为了演示继承与关系,我们创建一个论坛数据库,在数据库中创建三个表:
1、 论坛版块分类表 dbo.Categories:
字段名 |
字段类型 |
可空 |
备注 |
CategoryID |
int |
not null |
identity/主键 |
CategoryName |
varchar(50) |
not null |
|
2、 论坛版块表 dbo.Boards:
字段名 |
字段类型 |
可空 |
备注 |
BoardID |
int |
not null |
identity/主键 |
BoardName |
varchar(50) |
not null |
|
BoardCategory |
int |
not null |
对应论坛版块分类表的CategoryID |
3、 论坛主题表 dbo.Topics:
字段名 |
字段类型 |
可空 |
备注 |
TopicID |
int |
not null |
identity/主键 |
TopicTitle |
varchar(50) |
not null |
|
TopicContent |
varchar(max) |
not null |
|
ParentTopic |
int |
null |
如果帖子是主题贴这个字段为null,否则就是所属主题id |
TopicType |
tinyint |
not null |
0 – 主题贴 1 – 回复帖 |
我们可以使用sqlmetal命令行工具来生成外部映射文件,使用方法如下:
1、开始菜单 -》 VS2008 -》VS工具 -》VS2008命令行提示
2、输入命令:
D:/Program Files/Microsoft Visual Studio 9.0/VC>sqlmetal /conn:server=xxx; database=Northwind;uid=xxx;pwd=xxx /map:c:/northwind.map /code:c:/northwind.cs |
3、这样,我们就可以在C盘下得到一个xml映射文件和C#的实体类代码
4、把.cs文件添加到项目中来(放到App_Code目录),然后使用下面的代码加载映射文件:
String path = @"C:/Northwind.map"; XmlMappingSource xms = XmlMappingSource.FromXml(File.ReadAllText(path)); Northwind ctx = new Northwind("server=xxx;database=Northwind;uid=xxx;pwd=xxx", xms); |
5、现在就可以照常进行其它工作了。使用sqlmetal可以很方便的同步数据库与实体和映射文件。每次修改数据库结构,从dbml设计器上删除表、存储过程然后再重新添加也是很麻烦的事情。
处理空值
var count = (from c in ctx.Customers where c.Region == null select c).Count(); Response.Write(count + "<br/>"); var query = from emp in ctx.Employees select emp.ReportsTo; foreach (Nullable<int> r in query) { Response.Write(r.HasValue ? r.Value.ToString() + "<br/>" : "没有<br/>"); } |
代码执行后捕获到下面的SQL被执行:
SELECT COUNT(*) AS [value] FROM [dbo].[Customers] AS [t0] WHERE [t0].[Region] IS NULL
SELECT [t0].[ReportsTo] FROM [dbo].[Employees] AS [t0] |
项目介绍
这节将要把《一步一步学Linq to sql(三):增删改》中留言簿的例子修改为使用WCF的多层构架。我们将会建立以下项目:
l A,网站项目 WebSite:留言簿表现层
l B,类库项目 Contract:定义数据访问服务的契约
l C,类库项目 Service:定义数据访问服务
l D,类库项目Entity:留言簿实体
l E,控制台项目Host:承载数据访问服务
项目之间的引用如下:
l A引用B和D;
l B引用D和System.ServiceModel程序集
l C引用B、D、System.ServiceModel以及System.Data.Linq程序集
l D引用System.Data.Linq程序集
l E引用C和System.ServiceModel程序集
生成映射文件和实体
打开VS2008命令行提示,执行以下命令:
sqlmetal /conn:server=xxx;database=GuestBook;uid=xxx;pwd=xxx /map:c:/guestbook.map /code:c:/guestbook.cs /serialization:Unidirectional |
注意到,这里我们使用了serialization开关,告知sqlmetal在生成实体的时候自动把它们标记为WCF数据对象。生成结束后把C:/GUESTBOOK.CS添加到Entity项目中。