【翻译】Data Access with LINQ to SQL (1) -- New C# and VB.NET Language Features
该系列翻译自《ASP.NET Unleashed 3.5》第18章内容
在.NET Framework 3.5的众多新特性之中,LINQ to SQL是最重要的一个。实际上,它也许意味着自SQL诞生以来,应用程序与数据库的结合方式上最重大的一次变革。
长期以来,程序员处理未持久化数据(应用程序)和持久化数据(数据库)的方式有着天壤之别:在应用程序中,我们使用对象和属性(用C#或VB.NET创建);而在大多数数据库中,我们使用表和字段。
不管我们的应用程序和数据库是否描述非常类似的数据,这都是事实。例如,你可能会拥有一个类和一个表,其名称均为Product,代表你的Web站点所销售的产品列表。尽管如此,在这些实体之间进行交互的语言(C#、VB.NET与SQL语言)却不尽相同。很多大公司都会拥有不同的开发人员,有的擅长C#或VB.NET,有的则专攻SQL。
程序员要花费惊人的时间来转换对象和关系型世界,而这项工作是机械和乏味的。每当我想起花费在声明这些类(包含数据库字段到属性的映射)上的时间时,我都会不寒而栗。而这些时间我本可以用来陪孩子们逛公园、看电影或遛狗,等等。
LINQ to SQL的诞生使得我们可以对SQL宣判死刑。或者更准确地说,它使得SQL语言走向幕后,我们再也不必使用SQL了。这是件好事。SQL已死!
本章是较难的一章。LINQ to SQL并非很容易理解的概念,它依赖于C#、VB.NET以及.NET Framework中引入的一些新特性,而这些特性是不易掌握的。所以,请保持耐心,做个深呼吸。我保证最后一切都会清晰起来。
本章分为4部分。在第一部分中,我们讨论C#、VB.NET和.NET Framework 3.5引入的支持LINQ的新特性。然后,你将学习如何使用LINQ to SQL实体描述数据库表。接下来,我解释如何使用LINQ to SQL执行标准SQL命令,如SELECT、INSERT、UPDATE和DELETE命令。在本章的最后部分,我将示范如何创建自定义实体类(包含数据有效性验证)。
C#和VB.NET的新特性
微软公司为C#和VB.NET引入了诸多新的语言特性,以支持LINQ to SQL工作。很多特性都使得C#和VB.NET的行为更像动态语言(如JavaScript)。尽管引入的主要动机是支持LINQ,这些新特性本身还是非常有趣的。
注意:要使用这些新特性,你需要使Web站点面向.NET Framework 3.5,确保项目中包含web.config文件。然后选择菜单选项WebsiteàStart OptionsàBuild,在Target Framework中选择.NET Framework 3.5。执行这些步骤将会修改你的web.config文件,使其引用必要的程序集并使用正确的C#或VB.NET版本。
理解自动属性
我们将探索的第一个新语言特性叫做自动属性(Automatic Properties)。不幸的是,只有C#支持该特性,VB.NET并不支持。
自动属性提供了定义新属性的快速方法。如代码清单18-1所示,类Product包含了Id、Description和Price属性。
代码清单 18-1 LanguageChanges\App_Code\AutomaticProperties.cs
public class AutomaticProperties { // Automatic Properties public int Id { get; set; } public string Description { get; set; } // Normal Property private decimal _Price; public decimal Price { get { return _Price; } set { _Price = value; } } }
注意前两个属性Id和Description,没有使用Getter和Setter,这与最后一个属性Price不同。C#编译器将为你自动创建Getter和Setter,以及与属性对应的私有字段。
你不能向自动属性中的Getter和Setter添加任何逻辑,也不能创建只读的自动属性。
自动属性怎么会和LINQ to SQL有关呢?在使用LINQ to SQL时,你为了得到数据的结构(类似SQL查询时使用select语句得到的列表),经常将类设计为仅包含数据库表的各个字段。因此,你肯定希望使用最小的工作量来创建属性列表,自动属性就是为此量身定做的。
注意:使用Visual Web Developer或Visual Studio可以向类或页面中快速添加自动属性,你只需输入“prop”并按Tab键两次。
理解初始化器
使用初始化器(Initializers)可以减少实例化类的工作量。例如,假设你拥有如代码清单18-2(C#)或代码清单 18-3中的类(VB.NET)。
代码清单 18-2 LanguageChanges\App_Code\Product.cs
public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } }
代码清单 18-3 LanguageChanges\App_Code\Product.vb
Public Class Product Private _Id As Integer Public Property Id() As Integer Get Return _Id End Get Set(ByVal value As Integer) _Id = value End Set End Property Private _Name As String Public Property Name() As String Get Return _Name End Get Set(ByVal value As String) _Name = value End Set End Property Private _Price As Decimal Public Property Price() As Decimal Get Return _Price End Get Set(ByVal value As Decimal) _Price = value End Set End Property End Class
Product类包含3个公共属性(由于C#有自动属性的优势,因此C#示例采用自动属性进行定义,而VB.NET示例采用普通方式进行定义)。
现在,你需要创建一个Product类的实例,在.NET Framework 2.0中,采用以下方式(C#):
Product product1 = new Product();
product1.Id = 1;
product1.Name = “Laptop Computer”;
product1.Price = 800.00m;
VB.NET采用以下方式:
Dim product1 As New Product() product1.Id = 1 product1.Name = “Laptop Computer” product1.Price = 800.0
注意,初始化一个简单的Product类使用了4行代码,这太浪费了。利用初始化器,这些工作可以用一行代码完成。用C#使用初始化器的代码如下:
Product product2 = new Product {Id=1, Name=”Laptop Computer”, Price=800.00m};
用VB.NET使用初始化器的代码如下:
Dim product2 As New Product() With {.Id = 1, .Name = “Laptop Computer”,.Price = 800.0}
在.NET Framework 2.0中,你可以声明Product类的构造函数,包含Id、Name和Price作为参数。但由于还需要在构造函数中将参数值赋给属性,因此导致了代码的膨胀。使用初始化器,你显然可以做类似的工作。并且,你还因此得到了好处:更加小巧的类,以及声明该类时使用的最少的代码。
理解类型推断
一个新特性使得C#和VB.NET看上去非常像JavaScript之类的动态语言,这个特性就是局部变量类型推断(Type Inference)。当使用类型推断时,你允许C#或VB.NET编译器在编译时确定变量类型。
下面的代码介绍了在C#中如何使用类型推断:
var message = “Hello World!”;
在VB.NET中,使用如下代码:
Dim message = “Hello World!”
注意,变量message在声明时并没有为其指定任何类型。C#和VB.NET编译器会根据你初始化变量时的值来推断变量的类型(这里为String类型)。
使用类型推断并没有性能上的损失(变量并非后期绑定)。编译器在编译时就判断出了变量的数据类型。
为了支持类型推断,C#引入了一个新的关键字:var。当你希望编译器自行判断变量的数据类型时,就可以使用var类型声明变量。
当你为局部变量提供初始值时才能使用类型推断。例如,下面的代码不会执行(C#):
var message;
message = “Hello World!”;
由于message变量在声明时没有初始化,这段代码将不能通过编译(C#编译器)。
下面的代码在VB.NET中可以执行(但它做的并不是你想要的):
Dim message
message = “Hello World!”
在这种情况下,VB.NET认为message变量为一个Object类型。由于将字符串赋给了变量,在运行时将会把变量的值转换为String类型。从性能角度看,这并不是一个好的做法。
注意:VB.NET 9.0中有一个新的Option叫做Option Infer。要想使隐含类型(implicit typing)正确工作,必须激活Option Infer。你可以在代码文件的最顶端加入“Option Infer On”来激活该类的Option Infer。
下一节将介绍类型推断与LINQ to SQL的关系。在使用LINQ to SQL时,很多情况下都无法确定变量的类型,因此你需要编译器能够进行推断。
理解匿名类型
另一个类似于动态语言中你可能比较熟悉的概念是匿名类型(Anonymous Types)。当你需要创建一个临时类型(type),却并不想创建一个类(class)时,可以使用匿名类型。
下面的代码介绍了如何在C#中创建匿名类型:
var customer = new {FirstName = “Stephen”, LastName = “Walther”};
在VB.NET中创建同样的匿名类型,可以使用下面的代码:
Dim customer = New With {.FirstName = “Stephen”, .LastName = “Walther”}
注意,customer变量并没有指定类型(这与JavaScript或VBScript非常类似)。尽管如此,customer仍然具有它的类型,你只是不知道它的名字而已:它是匿名的,理解这一点是十分重要的。
仅仅一行代码,我们既创建了一个新的类,又实例化了它的属性。其简洁性让我感动到内伤。
在使用LINQ to SQL时,匿名类型十分有用。因为你会发现你经常需要实时地(on the fly)创建一些新类型。例如,当执行一个查询时,你也许希望返回一个类,来代表一些数据库字段的集合。你将需要创建一个包含这些字段的临时类。
理解泛型
是的,我知道泛型(Generics)并不是.NET 3.5的新特性。但是,它对LINQ to SQL来说是相当重要的部分,值得我们花点篇幅来回顾。
注意:
要使用泛型,你需要引入System.Collections.Generic命名空间。
我使用泛型的绝大多数情况,是由于泛型集合。例如,如果想描述一个字符串列表,你可以这样声明(C#):
List<string> stuffToBuy = new List<string>();
stuffToBuy.Add(“socks”);
stuffToBuy.Add(“beer”);
stuffToBuy.Add(“cigars”);
使用VB.NET,则这样声明:
Dim stuffToBuy As New List(Of String)
stuffToBuy.Add(“socks”)
stuffToBuy.Add(“beer”)
stuffToBuy.Add(“cigars”)
现在,利用集合初始化器,你可以仅用一行代码就声明一个强类型的字符串列表(C#):
List<string> stuffToBuy2 = new List<string> {“socks”, “beer”, “cigars”};
注意:VB.NET并不支持集合或数组初始化器。
List类是泛型类,因为在声明的时候指定了它要包含的对象的类型。C#中,要在尖括号之间(< >)指定类型,而在VB.NET中要使用Of关键字。在上例中,我们创建了一个包含字符串的List类。同样地 ,我们也可以创建包含整型或其他自定义类型(如Product和Customer类,分别代表产品和顾客)的List类。
因为泛型是强类型的,因此泛型集合(如List)优于非泛型集合(如ArrayList)。ArrayList将所有对象都保存为object,而泛型将所有对象保存为它们特定的类型。当从ArrayList中取出一个对象时,在使用该对象前你必须将其转换为特定类型。而从泛型中取出对象则不需要这种转换。
泛型并不仅仅局限于集合。你可以创建泛型方法、泛型类以及泛型接口。
例如,当使用ADO.NET时,我喜欢将data reader转换为强类型的List集合。如Listing 18.4所示,GetListFromCommand()方法包含一个command对象,执行该对象,然后自动生成一个强类型的List。
代码清单 18-4 LanguageChanges\App_Code\GenericMethods.cs
using System; using System.Collections.Generic; using System.Data.SqlClient; public class GenericMethods { public static List<T> GetListFromCommand<T>(SqlCommand command) where T: ICreatable, new() { List<T> results = new List<T>(); using (command.Connection) { command.Connection.Open(); SqlDataReader reader = command.ExecuteReader(); while (reader.Read()) { T newThing = new T(); newThing.Create(reader); results.Add(newThing); } } return results; } } public interface ICreatable { void Create(SqlDataReader reader); }
代码清单18-4中的GetListFromCommand()方法接收一个SqlCommand对象并且返回一个泛型List<T>。where子句用来约束泛型类型。泛型约束使得类型T必须实现ICreatable接口并且可以通过new实例化(准确的意思应为,必须包含无参的构造函数。——译者注)。
代码清单18-4中同样定义了ICreatable接口,它要求类实现Create()方法。
既然我们创建了可以将data reader 转换为强类型的list的泛型方法,那么我们就可以用它处理任何实现了ICreatable接口的类,如代码清单18-5中的Movie类。
代码清单 18-5 Movie.cs
using System; using System.Data.SqlClient; public class Movie : ICreatable { public int Id { get; set; } public string Title { get; set; } public void Create(SqlDataReader reader) { Id = (int)reader[“Id”]; Title = (string)reader[“Title”]; } }
你可以通过下面的方法调用GetListFromCommand()方法(随书CD中的ShowGenericMethods.aspx页面使用了该代码):
string conString = WebConfigurationManager.ConnectionStrings[“con”].ConnectionString; SqlConnection con = new SqlConnection(conString); SqlCommand cmd = new SqlCommand(“SELECT Id, Title FROM Movie”, con); List<Movie> movies = GenericMethods.GetListFromCommand<Movie>(cmd);
在这里,泛型的出色之处在于,你不必为每个类型编写相同的代码将data reader 转换为泛型List。你只编写了一个泛型方法GetListFromCommand(),却可以用该方法转换任何复合泛型约束的类型。
理解泛型的正确方法是理解代码模板。你可以使用泛型来定义某一代码模式,而将一个特定的类应用与该模式中。
理解Lambda表达式
Lambda表达式是.NET Framework 3.5引入的另一个新特性,它提供了一种极其简洁的定义方法的方式。
假设你希望将Click事件处理器正确地绑定到button控件,代码清单18-6所示为其中一种方法。
代码清单 18-6 LanguageChanges\NormalMethod.aspx
<%@ Page Language=”C#” %> <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”> <script runat=”server”> void Page_Init() { btn.Click += new EventHandler(btn_Click); } void btn_Click(object sender, EventArgs e) { lblResult.Text = DateTime.Now.ToString(); } </script> <html xmlns=”http://www.w3.org/1999/xhtml”> <head runat=”server”> <title>Normal Method</title> </head> <body> <form id=”form1” runat=”server”> <div> <asp:Button id=”btn” Text=”Go!” Runat=”server” /> <asp:Label id=”lblResult” Runat=”server” /> </div> </form> </body> </html>
在代码清单18-6中,Page_Init()方法将btn_Click()方法绑定到Button的Click事件。当点击按钮时,执行btn_Click()方法,显示当前日期和时间。这没什么特别之处。
.NET Framework 2.0引入了匿名方法的概念,其好处是可以声明内联方法。例如,代码清单18-7的功能与上例相同,所不同的是使用了匿名方法来处理Button的Click事件。
代码清单 18-7 LanguageChanges\AnonymousMethod.aspx
<%@ Page Language=”C#” %> <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”> <script runat=”server”> void Page_Init() { btn.Click += delegate(object sender, EventArgs e) { lblResult.Text = DateTime.Now.ToString(); }; } </script> <html xmlns=”http://www.w3.org/1999/xhtml”> <head id=”Head1” runat=”server”> <title>Anonymous Method</title> </head> <body> <form id=”form1” runat=”server”> <div> <asp:Button id=”btn”Text=”Go!”Runat=”server” /> <asp:Label id=”lblResult”Runat=”server” /> </div> </form> </body> </html>
在代码清单18-7中,处理Click事件的方法是在Page_Init()方法之中声明的。
注意:VB.NET并不支持匿名方法,但它支持Lambda表达式,所以VB.NET使用者请不要跳过。
Lambda表达式将匿名方法的概念更进一步,它将声明一个方法所必需的语法数量降到了最低。代码清单18-8使用了Lambda表达式,它功能与以上两个例子相同。
代码清单 18-8 LanguageChanges\LambdaExpression.aspx
<%@ Page Language=”C#” %> <!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”> <script runat=”server”> void Page_Init() { btn.Click += (sender, e) => lblResult.Text = DateTime.Now.ToString(); } </script> <html xmlns=”http://www.w3.org/1999/xhtml”> <head id=”Head1” runat=”server”> <title>Lambda Expressions</title> </head> <body> <form id=”form1” runat=”server”> <div> <asp:Button id=”btn”Text=”Go!”Runat=”server” /> <asp:Label id=”lblResult”Runat=”server” /> </div> </form> </body> </html>
代码清单18-8中的Lambda表达式如下,
(sender, e) => lblResult.Text = DateTime.Now.ToString();
这仅仅是编写方法的简洁方式。Lambda表达式使用=>操作符(goes into操作符)来分隔方法的参数列表和方法体。编译器(通常)可以推断出参数的数据类型。尽管如此,如果你愿意,还是可以像下面的代码那样指明参数类型,
(object sender, EventArgs e) => lblResult.Text = DateTime.Now.ToString();
有必要提一下,当方法只有一个参数时,圆括号是可选的。因此,Lambda表达式可以非常简洁。
Visual Basic也支持Lambda表达式,但多了一些限制。Visual Basic中的Lambda表达式中只能包含表达式,不能包含语句。
VB中创建Lambda表达式的语法如下,
Dim AddNumbers = Function(x, y) x + y
Response.Write(AddNumbers(5, 6))
第一条语句创建了一个名为AddNumbers的变量,这即为一个Lambda表达式。VB语法Function(x, y) x + y相当于C#语法(x, y) => x + y。第二条语句使用两个参数调用Lambda表达式。
理解扩展方法
扩展方法的概念对于使用过JavaScript(考虑prototype)的人来说,也应该是非常熟悉的。
使用扩展方法,你可以向一个已有类中添加新的方法。例如,你可以创建任意一个方法,并将它添加到String类中。
由于害怕JavaScript注入攻击,我一直以来都对字符串进行HTML编码。在.NET Framework 2.0中,可以调用静态方法Server.HtmlEncode()来对字符串进行HTML编码,如下,
string evilString = “<script>alert(‘boom!’)<” + “/script>”;
ltlMessage.Text = Server.HtmlEncode(evilString);
在这段代码中,调用了Server类中的静态方法HtmlEncode()。如果我们可以向下面这样,直接调用字符串的HtmlEncode()方法,岂不妙哉
string evilString = “<script>alert(‘boom!’)<” + “/script>”;
ltlMessage.Text = evilString.HtmlEncode();
使用扩展方法,就可以这么做。我们可以向喜欢的类中添加任何方法。创建扩展方法,首先要创建一个静态类,并创建一个第一个参数为特殊参数的静态方法。代码清单18-9向String类中添加HtmlEncode()方法,以此描述了如何创建扩展方法。
代码清单 18-9 LanguageChanges\MyExtensions.vb[2]
public static class MyExtensions { public static string HtmlEncode(this string str) { return System.Web.HttpUtility.HtmlEncode(str); } }
注意,HtmlEncode()方法中仅有的参数前面多了关键字this。这样的参数指明了扩展方法所应用的类型。
在VB.NET中创建扩展方法与在C#中极为类似。代码清单18-10中的HtmlEncode()方法与上面的功能相同。
代码清单 18-10 LanguageChanges\MyExtensions.cs[3]
Imports System.Runtime.CompilerServices Public Module MyExtensions <Extension()> _ Public Function HtmlEncode(ByVal str As String) As String Return System.Web.HttpUtility.HtmlEncode(str) End Function End Module
当使用VB.NET时,必须将扩展方法声明在一个module中。另外,还必须标记为System.Runtime.CompilerServices.Extension属性。
理解LINQ
终于,我们要讨论最后一个话题LINQ了,在这之后我们就可以开始研究本章的真正内容——LINQ to SQL了。
LINQ是Language Integrated Query的简称,它由C#和VB.NET的一系列新特性组成,这些特性允许我们执行查询。LINQ使得SQL查询就像C#或VB.NET的语法一样简单。
一个简单的LINQ查询示例如下,
var words = new List<string> {“zephyr”, “apple”, “azure”};
var results = from w in words
where w.Contains(“z”)
select w;
第一条语句创建了一个泛型列表words,它包含三个字符串。第二条语句就是LINQ查询。
LINQ查询及其像反向的SQL语句。它从列表中获得所有的包含字母z的单词。执行该查询,results变量将包含一下两个单词:
zephyr
azure
你可以对所有实现了IEnumerable<T>接口的对象执行标准的LINQ查询。这些实现了该接口的对象称为sequence。常用的sequence均为泛型List类或标准Array类(因此任何可以存入数组中的类,都可以使用LINQ进行查询)。
C#语言提供了以下子句,供我们在查询中使用:
- from——指定数据源以及用来迭代数据源的变量(范围变量)。
- where——过滤查询的结果。
- select——指定查询结果中的项。
- group——通过某一关键字,对相关的值进行聚合。
- into——存储聚合中的结果,或连接到一个临时变量。
- orderby——将查询结果按升序或降序进行排序。
- join——通过一个关键字,对两个数据源进行连接。
- let——创建一个临时变量,来存储子查询的结果。
创建一个LINQ查询,类似于创建一个反向的SQL查询。LINQ查询以一个from子句开始,它指定了数据的位置。然后,指定where子句来过滤数据。最后,指定用来表示数据的select子句(决定你要返回的对象和属性)。
在内部,标准LINQ查询被翻译成调用System.Linq.Enumerable类的方法。Enumerable类包含了一些扩展方法,这些扩展方法可以应用到任何实现了IEnumerable<T>接口的类中。
因此,查询
var results = from w in words
where w.Contains(“z”)
select w;
将被C#编译器翻译成下面的查询
var results = words.Where( w => w.Contains(“z”) ).Select( w => w );
第一个查询使用了查询语法(query syntax),第二个查询使用了方法语法(method syntax)。这两种查询是相同的。
注意,使用方法语法的查询在Where()和Select()方法中允许使用Lambda表达式。Where()方法中的Lambda表达式用来过滤数据,只返回包含字母z的单词。Select()方法指明要返回的对象和属性。如果我们将Lambda表达式w=>w.Length传递给Select()方法,该查询将返回每个单词的长度,而不是单词本身。
在创建LINQ查询时,使用查询语法还是方法语法纯粹属于个人偏好。查询语法属于语言的特性(C#或VB.NET),方法语法和语言无关。
我发现我使用方法语法的时候更多一些,因为查询语法不过是方法语法的子集。也就是说,使用方法语法可以做更多的事情。然而在某些情况下,使用方法语法编写查询会显得有些冗长。例如,使用查询语法编写LINQ to SQL左外连接,要比使用方法语法简单得多。
最后,选择方法语法还是查询语法其实并不重要,因为所有的查询语法语句都将被编译器翻译成方法语法。在使用标准LINQ时,这些调用的方法都存在与Enumerable类中。
在SDK文档中查找System.Linq.Enumerable类可以浏览Enumerable支持的全部方法。这里列举了一些有趣且实用的方法,
- Aggregate()——对序列中的每一项执行同一个函数。
- Average()——返回序列中每一项的平均值。
- Count()——返回序列的总项数。
- Distinct()——返回序列中不同的项。
- Max()——返回序列中的最大值。
- Min()——返回序列中的最小值。
- Select()——返回序列中的某些项或属性。
- Single()——返回序列中的某个单一值。
- Skip()——跳过序列中指定数目的项并返回剩下的元素。
- Take()——返回序列中指定数目的元素
- Where()——过滤序列中的元素。
本节我们讨论了标准LINQ(也叫LINQ to Objects)。LINQ使用了Provider Model,它有很多不同的实现,包括LINQ to SQL、LINQ to XML、LINQ over DataSets以及LINQ to Entities。LINQ也有很多第三方实现,包括LINQ to NHibernate和LINQ to SharePoint。这些不同的实现可以用来查询不同的数据源,如XML文件、SharePoint列表等等。
在本章,我们仅讨论LINQ to SQL,它是微软专门为操作数据库数据而设计的官方版本。下面就让我们开始吧。