【翻译】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=”form1runat=”server>
<div>
<asp:Button id=”btnText=”Go!” Runat=”server/>
<asp:Label id=”lblResultRunat=”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=”Head1runat=”server>
<title>Anonymous Method</title>
</head>
<body>
<form id=”form1runat=”server>
<div>
<asp:Button id=”btnText=”Go!”Runat=”server/>
<asp:Label id=”lblResultRunat=”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=”Head1runat=”server>
<title>Lambda Expressions</title>
</head>
<body>
<form id=”form1runat=”server>
<div>
<asp:Button id=”btnText=”Go!”Runat=”server/>
<asp:Label id=”lblResultRunat=”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,它是微软专门为操作数据库数据而设计的官方版本。下面就让我们开始吧。

原文地址:https://www.cnblogs.com/kirinboy/p/Data_Access_with_LINQ_to_SQL_1.html