LINQ 教程:将表映射到对象






4.89/5 (116投票s)
一个面向初学者的LINQ教程,它将引导你学习如何将SQL Server数据库表及其关系映射到对象,以及如何通过简单的LINQ查询来检索这些数据。
注意:运行此程序需要SQL Server Express 2008和.NET 3.5。
引言
这是关于使用LINQ to SQL三部分系列的第一部分
- 第一部分:将表映射到对象
- 第二部分:添加/更新/删除数据
- 第三部分:WPF数据绑定与LINQ to SQL
这些教程描述了如何手动(而不是通过SqlMetal等自动化工具)将类映射到表,以便支持M:M关系以及对实体类的数据绑定。即使你选择自动生成类,理解这些技术的工作原理也能让你扩展代码,更好地满足应用程序的需求,并在问题出现时进行故障排除。
这篇第一篇文章和“图书目录”应用程序的目的是为初学者介绍LINQ,以及如何
- 使用LINQ to SQL将SQL Server数据库表及其关系映射到你的对象。
- 对这些对象执行一些简单的LINQ查询以检索你的数据。
本文将逐步讲解这些步骤,解释如何在自己的应用程序中完成这些操作。随附的“图书目录”应用程序包含了这里描述的所有映射,以及一个简单的WPF GUI,通过数据绑定来显示结果。
入门
LINQ to SQL是一个对象关系映射(ORM)工具,它允许你使用.NET 3.5框架将SQL Server数据库映射到你的类。
数据库
“图书目录”使用一个简单的SQL Server Express数据库,其中包含图书、作者和类别。每本书只能属于一个类别,但可以有多个作者。每位作者可以有多本书。BookAuthors表仅用于连接书籍和作者之间的M:M关系。为简化起见,除了Books.Category列之外,所有列都不允许为空值。
这组表允许我们映射每种主要的关系类型(1:M、M:1和M:M)。
本文的其余部分将描述将应用程序的表及其关系映射到你的类,然后使用LINQ检索结果的步骤。它将使用这些表来提供概念示例。
Application
要使用LINQ to SQL,请创建一个.NET 3.5项目并添加对System.Data.Linq
的引用。
将DataContext映射到你的数据库
如果你为数据库创建了一个强类型DataContext
,它会提供一个单一的入口点,方便你访问数据。这个类将负责连接数据库并声明你要连接的每个表*。
*注意:你可以从DataContext
中省略M:M连接表(例如BookAuthor),因为它们仅在后台用于连接M:M关系(本文稍后将讨论)。
1. 创建一个带有[Database]属性的类,并继承DataContext
为你的数据库创建一个类,继承DataContext
,并为其添加[Database]
属性,以表明该类映射到数据库。
如果你使用的类名与数据库名称不同,请使用属性的Name
参数指定数据库名称([Database (Name="BookCatalog")]
)。如果名称相同,如本例所示,则可以省略Name
参数。
using System.Data.Linq.Mapping;
namespace LINQDemo
{
[Database]
public class BookCatalog : DataContext{}
}
2. 添加一个带连接字符串的构造函数
添加一个调用base()
并传入数据库连接字符串的构造函数,以告诉它如何连接到你的数据库。
构造函数可能将连接字符串作为参数,从属性文件中读取,或者直接硬编码,如“图书目录”演示中所做的那样(这里使用的字符串表示连接到SQL Server Compact数据库BookCatalog.sdf,它位于可执行文件所在的目录中)。
public BookCatalog( ) : base( "Data Source=.\\SQLEXPRESS;" +
"AttachDbFilename=|DataDirectory|\\BookCatalog.mdf;" +
"Integrated Security=True;User Instance=True" ) { }
请注意,“图书目录”将它的数据库BookCatalog.sdf包含在项目本身中。这对于使用LINQ to SQL不是必需的,只是为了简化分发。
3. 声明你的表
最后,将你想要连接的每个表*表示为该表(即将创建)的类类型的Table<T>
集合。
通常,你可能先创建类,但让我们看看这是什么样子。我们将创建三个类(Author
、Book
和Category
)——每个类对应一个表。因此,我们将向每个类类型添加一个Table<T>
集合,并为集合命名有意义。请注意,类名或集合名称不必与数据库表名称匹配。我们将在下一节中看到如何指定表名。
using System.Data.Linq;
using System.Data.Linq.Mapping;
namespace LINQDemo
{
[Database]
public class BookCatalog : DataContext
{
public BookCatalog( ) : base( ... );
public Table<Author> Authors;
public Table<Book> Books;
public Table<Category> Categories;
}
}
将实体类映射到你的表
为应用程序中想要连接的每个表*创建一个类。我们将以Book
为例进行讲解。
1. 创建一个带有[Table]属性的类
创建一个Book
类,并带有Table
属性,以表明它映射到一个数据库表。
在Name
参数中指定数据库表的名称([Table( Name = "Books" )]
)。我们在这里这样做是因为表名(Books)与我们的类名(Book
)不同。如果它们相同,则可以省略Name
参数。
using System.Data.Linq.Mapping;
namespace LINQDemo
{
[Table( Name = "Books" )]
public class Book{}
}
2. 使用[Column( IsPrimaryKey = true )]属性为表的主键添加一个字段
如果你愿意,也可以为自动属性分配Column
属性,这正是“图书目录”应用程序所做的。
如果你的主键在数据库中设置为Identity,请使用参数IsDbGenerated = true
进行指示。
如果你想给你的字段/属性一个与数据库列不同的名称,请使用(Name="")
标签,就像我们为表做的那样。否则,它将从你的字段或属性名称中获取列名,就像我们在这里对Book的主键Id所做的那样。
[Column( IsPrimaryKey = true, IsDbGenerated = true )]
public int Id { get; set; }
3. 使用[Column]属性添加表中的其他非关系列
我们稍后会回到关系。现在,让我们从那些不是外键到其他表的列开始。
Book有两个这样的列:Title
和Price
。对money
列使用decimal
,对varchar
使用string
,LINQ将自动为你处理这些转换。
[Column] public string Title { get; set; }
[Column] public decimal Price { get; set; }
“图书目录”应用程序也对Author和Category执行了相同的操作,以便我们有三个类,每个类都映射到我们主要表中的一个。
using System.Data.Linq.Mapping;
namespace LINQDemo
{
[Table( Name = "Books" )]
public class Book
{
[Column( IsPrimaryKey = true, IsDbGenerated = true )] public int Id { get; set; }
[Column] public string Title { get; set; }
[Column] public decimal Price { get; set; }
}
[Table (Name="Authors")]
public class Author
{
[Column (IsPrimaryKey = true, IsDbGenerated = true )] public int Id { get; set; }
[Column] public string Name { get; set; }
}
[Table (Name="BookCategories")]
public class Category
{
[Column (IsPrimaryKey = true, IsDbGenerated = true )] public int Id { get; set; }
[Column] public string Name { get; set; }
}
}
使用LINQ检索你的数据
一旦你将表映射到类并将它们声明在映射到数据库的DataContext
中,你就可以在不编写任何数据库代码的情况下访问你的数据。
只需实例化BookCatalog
(强类型DataContext
),你就可以通过使用其中定义的集合:Authors
、Books
和Categories
来访问数据库中的每本书、作者和类别。
例如,要检索目录中的书籍,你只需遍历bookCatalog.Books
即可,LINQ将自动通过你的映射访问底层数据库,并为你填充该集合——使用Book
类为找到的每本书创建一个实例。
BookCatalog bookCatalog = new BookCatalog( );
foreach( Book book in bookCatalog.Books){
string title = book.Title;
decimal price = book.Price;
}
你也可以使用LINQ来过滤这些结果,因为很多时候你不需要*所有*的书籍,也不想为了检索它们而承受性能损失。
LINQ提供了一套完整的类似SQL的查询语法,并已集成到.NET语言(C#、Visual Basic)中。虽然描述这些语法超出了本文的范围,但让我们看几个例子来了解它是如何使用的。
由于BookCatalog
返回一个对象集合,你使用的查询语法将是针对这些对象严格类型的(意味着你将使用来自你的对象及其字段/属性的名称,而不是来自你的数据库表的名称)。因此,例如,如果你只想要价格低于30美元的书籍,你可以使用LINQ的where
子句从bookCatalog.Books
中只选择价格低于30美元的书籍。
IEnumerable<Book> cheapBooks = from book in bookCatalog.Books
where book.Price.CompareTo( 30m ) < 0
select book;
LINQ的延迟执行意味着书籍列表在直到你实际尝试*访问*结果时(例如,使用foreach
循环)才从数据库中检索,因此你可以继续应用过滤器,这将限制最终对数据库执行的查询。
例如,你可以使用LINQ的orderby
来进一步优化上述结果,按书籍标题排序。
IEnumerable<Book> sortedBooks = from book in cheapBooks
orderby book.Title
select book;
现在,如果你在执行这一系列LINQ查询后迭代sortedBooks
,将只对数据库执行一个查询。它将返回所有价格低于30美元的书籍,并按标题排序。
BookCatalog bookCatalog = new BookCatalog( );
IEnumerable<Book> cheapBooks = from book in bookCatalog.Books
where book.Price.CompareTo( 30m ) < 0
select book;
IEnumerable<Book> sortedBooks = from book in cheapBooks
orderby book.Title
select book;
foreach( Book book in sortedBooks ) { ... }
现在,让我们看看如何映射关系,以便你可以遍历你的对象(例如,book.Author.Name
)。
向你的实体类添加关系
本节介绍如何映射每种关系类型:M:1、1:M,然后是M:M。虽然它不包括1:1关系,但你可以通过与M:1相同的方式映射1:1关系。所以,让我们从那里开始。再次,我们将以Book
为例进行讲解。
映射M:1关系
Book
与Category
之间存在多对一(M:1)关系,因为每本书只能有一个类别(而每个类别可以被多本书使用)。
在数据库中,Book.catalog仅保存一个ID,该ID是Category表的外键。然而,在你的对象模型中,你可能希望book.Catalog
代表实际的Catalog
对象(而不仅仅是一个ID)。你可以通过创建几个私有字段来处理后台映射,然后创建一个公共属性来保存实际的Category
对象来实现。
1. 在其他表中添加一个私有字段以保存外键
添加一个私有字段,映射到Book.category数据库外键列。
如果字段允许为空(如本字段所示),请使用可为空的类型(例如int
?)。
我将此字段命名为categoryId
(为下面的公共Category
属性保留名称Category)。这意味着我必须设置Column
属性中的Name
参数,因为我的字段名与数据库列名不同。
[Column( Name = "Category" )] private int? categoryId;
2. 添加一个私有的EntityRef<T>后备字段,该字段引用另一个表
添加一个私有的EntityRef<T>
字段来保存对书籍实际Category
实例的*引用*。这将作为我们公共Category
属性的后备字段,但通过使用EntityRef
,它将具有延迟加载(这意味着LINQ不会在数据库中检索类别,直到我们请求它)。
将字段初始化为新的EntityRef
实例,以防止在需要关系的一侧(例如Category
)但没有另一侧(Book
)的任何实例的情况下出现NullReferenceException
。
private EntityRef<Category> _category = new EntityRef<Category>( );
3. 在关联类中添加一个公共属性,并带有[Association]属性
最后,创建公共Category
属性,它将保存实际的Category
实例。为该属性添加一个Association
属性,并设置以下参数:
Name
:数据库中两个表之间的关系名称(在本例中为FK_Books_BookCategories
)。IsForeignKey = true
:标志,表示此类的表包含外键(在下面的ThisKey
参数中指定)。- 设置此属性上的两个参数,使用你刚刚创建的字段:
ThisKey
:指定你指向另一个表的外键:categoryId
。Storage
:指定你将保存另一个类实例的EntityRef<T>
:_category
。
在属性内部,通过使用包含实际Category
实例的_category
的Entity
属性来编码getter/setter。
[Association( Name = "FK_Books_BookCategories",
IsForeignKey = true, Storage = "_category", ThisKey = "categoryId" )]
public Category Category{
get { return _category.Entity; }
set { _category.Entity = value; }
}
从M:1关系访问数据
你现在可以通过此关系以面向对象的方式访问Category
的数据——例如,book.Category.Name
。
foreach( var book in bookCatalog.Books ) {
string categoryName = book.Category.Name;
}
我们在连接表中的M:1关系
现在,让我们看看如何映射M:M连接表中的M:1关系。每当我们有一个M:M连接表时,就像我们与BookAuthors一样,连接表本身就是两个M:1关系。
例如,BookAuthor
包含一个到Book
的M:1关系和一个到Author
的M:1关系,以桥接它们之间的M:M关系。
不幸的是,你仍然需要创建一个类来映射到这个连接表。但是,与外键一样,因为它仅用于后台映射,所以你可以通过将类标记为internal
来将其排除在公共接口之外。
1. 创建一个带有[Table]属性的内部类,该类映射到连接表
像创建其他实体类一样,为BookAuthors创建一个类,但不要将其标记为public
。
using System.Data.Linq;
using System.Data.Linq.Mapping;
namespace LINQDemo
{
[Table( Name = "BookAuthors" )]
class BookAuthor{}
}
2. 映射两个表的M:1关系,并将它们标识为主键。
像上面一样,为Book:Catalog关系,创建到Book的M:1关系和到Author的M:1关系。请注意,BookAuthors表持有的数据库关系在数据库中命名如下:
- BookAuthor:Authors关系名为
FK_BookAuthors_Authors
。 - BookAuthor:Books关系名为
FK_BookAuthors_Books
。
在两个Column
属性上添加IsPrimaryKey = true
属性,以表明BookAuthors的主键由这两个值组成。
[Table( Name = "BookAuthors" )]
class BookAuthor
{
[Column( IsPrimaryKey = true, Name = "Author" )] private int authorId;
private EntityRef<Author> _author = new EntityRef<Author>( );
[Association( Name = "FK_BookAuthors_Authors", IsForeignKey = true,
Storage = "_author", ThisKey = "authorId" )]
public Author Author {
get { return _author.Entity; }
set { _author.Entity = value; }
}
Column( IsPrimaryKey = true, Name = "Book" )] private int bookId;
private EntityRef<Book> _book = new EntityRef<Book>( );
[Association( Name = "FK_BookAuthors_Books", IsForeignKey = true,
Storage = "_book", ThisKey = "bookId" )]
public Book Book {
get { return _book.Entity; }
set { _book.Entity = value; }
}
}
这涵盖了M:1关系的映射。不幸的是,你现在还不能对BookAuthor
做太多有趣的事情,但当我们在下面讨论M:M关系时,我们会回到连接表。但首先,让我们看看我们Book:Catalog关系的另一端,了解如何映射1:M关系,因为我们需要知道这一点来完成我们的M:M关系。
映射1:M关系
添加1:M关系将允许你获取某个Category
中的所有书籍列表。
1. 映射另一个类中的外键
即使你正在向Category
添加关联,它仍然需要知道如何关联回Book
。因此,你只需要确保你的Book
类已经映射了保存指向Category
的外键的列。如果你正在跟随,那么你已经将其添加到了1:M映射中,所以你没问题了——只需记下字段名:categoryId
,因为你需要它来建立关联。
[Table( Name = "Books" )]
public class Book
{
...
[Column( Name = "Category" )] private int? categoryId;
2. 映射你自己的主键
LINQ会将Book
的外键与Category
的主键进行比较,所以你需要映射Category.Id
并将其标识为主键([Column (IsPrimaryKey = true)]
)。同样,如果你正在跟随,你已经在实体类创建中完成了这个操作。所以,只需记下这个属性名:Id
,因为你也需要它来建立关联。
[Table (Name="BookCategories")]
public class Category
{
[Column ( IsPrimaryKey = true, IsDbGenerated = true )] public int Id { get; set; }
...
3. 添加一个私有的EntitySet<T>后备字段,该字段引用另一个表
添加一个私有的EntitySet<Book>
字段来保存属于该Category
的书籍集合。这将作为我们公共Books
属性的后备字段。与EntityRef
类似,EntitySet
将延迟加载Book
s,直到我们实际访问它(所以我们不必每次只查看一个类别时都获取书籍列表)。
将字段初始化为新的EntitySet
,以避免在没有关系两端的情况下出现NullReferenceException
(例如,没有书籍的类别)。
private EntitySet<Book> _books = new EntitySet<Book>();
4. 在关联类中添加一个属性
最后,创建公共Books
属性,它将保存该类别中的书籍。为该属性添加一个Association
属性,并设置数据库关系Name
(FK_Books_BookCategories
)以及该属性上的三个参数,以使用你刚刚创建的字段:
OtherKey
:指定另一个类(Book
)中保存指向我们的外键的字段:categoryId
。ThisKey
:指定你的主键(OtherKey
应该匹配的对象):Id
。Storage
:指定你将用于存储关联返回的Book
实例集合的EntitySet<T>
:_books
。
在属性内部,通过使用包含实际Book
实例集合的_books
来编码getter/setter。
[Association( Name = "FK_Books_BookCategories",
Storage = "_books", OtherKey = "categoryId", ThisKey = "Id" )]
public ICollection<Book> Books {
get { return _books; }
set { _books.Assign( value ); }
}
从1:M关系访问数据
你现在可以通过简单的使用category.Books
属性来访问每个类别中的Book
列表。
foreach( var category in bookCatalog.Categories ){
foreach( Book book in category.Books ){
string bookTitle = book.Title;
}
}
映射M:M关系
最后,添加M:M关系将允许你直接从Book
实例访问该书的所有作者,并直接从Author
实例访问作者写的所有书籍。
你已经通过创建BookAuthor
类完成了大部分工作。并且,当你创建前一节中的1:M关系时,你也已经学会了*如何*做。现在,这只是把所有东西放在一起的问题。再次,我们将以Book
为例进行讲解。
1. 从该类到连接表的私有1:M关系
Book
与BookAuthor
之间存在1:M关系。也就是说,每本书可以有多个作者,但每个书作者只引用一本书。所以,你只需要遵循上一节的四个步骤。
- 在
BookAuthor
类中映射指向Book
的外键。你已经做过了,它叫做:bookId
。 - 定义
Book
的主键。你也已经做过了,它叫做:Id
。 - 添加一个引用另一个表的
EntitySet<T>
:_bookAuthors
。 - 添加一个
BookAuthors
属性(将其设为private
,因为它只是为了帮助我们获取作者列表)并带有Association
属性,该属性设置指向前面三个字段的参数。
为步骤3和4添加到Book
中的代码如下:
[Table( Name = "Books" )]
public class Book
{
...
private EntitySet<BookAuthor> _bookAuthors = new EntitySet<BookAuthor>( );
[Association( Name = "FK_BookAuthors_Books",
Storage = "_bookAuthors", OtherKey = "bookId",
ThisKey = "Id" )]
private ICollection<BookAuthor> BookAuthors {
get { return _bookAuthors; }
set { _bookAuthors.Assign( value ); }
}
你可以在Author
中做同样的事情(因为每个作者可以有多本书),但将bookId
替换为authorId
。
[Table( Name = "Authors" )]
public class Author
{
...
private EntitySet<BookAuthor> _bookAuthors = new EntitySet<BookAuthor>( );
[Association( Name = "FK_BookAuthors_Authors",
Storage = "_bookAuthors",
OtherKey = "authorId", ThisKey = "Id" )]
private ICollection<BookAuthor> BookAuthors {
get { return _bookAuthors; }
set { _bookAuthors.Assign( value ); }
}
2. 添加一个使用LINQ通过1:M关系检索数据枚举的公共属性
最后,创建你的公共Authors
属性,让它从这本书的私有书籍作者列表中检索作者。所以,例如,如果你有一本LINQ In Action的书,它有三个作者:Fabrice Marguerie、Steve Eichert和Jim Wooley,那么LINQ In Action的Book
实例将包含一个由三个BookAuthor
组成的列表。
Book | 作者 |
---|---|
LINQ In Action | Fabrice Marguerie |
LINQ In Action | Steve Eichert |
LINQ In Action | Jim Wooley |
你想做的是检索作者列表,这样调用者甚至不需要知道这个中间表。你可以通过使用LINQ查询你的私有BookAuthors
属性并告诉它*只*返回它拥有的作者来实现。
IEnumerable<Author> authors = from ba in BookAuthors
select ba.Author;
你现在可以将它包装在Book
中的一个公共、只读属性Authors
中,并且(可选地)通过调用结果上的toList()
返回一个ICollection
,正如我在这里为了提供更一致的公共接口所做的那样。
public ICollection<Author> Authors { get {
return ( from ba in BookAuthors select ba.Author ).ToList( ); } }
当然,你也可以在Author
中做同样的事情来返回作者的书籍列表。
public ICollection<Book> Books { get {
return ( from ba in BookAuthors select ba.Book ).ToList( ); } }
从M:M关系访问数据
你现在可以像预期的那样完全遍历你的对象模型。下面是一些例子:
// Starting from Books...
foreach( var book in bookCatalog.Books ) {
string title = book.Title;
decimal price = book.Price;
string category = book.Category.Name;
ICollection<Author> authors = book.Authors;
ICollection<Book> otherBooksInCategory = book.Category.Books;
}
// Starting from Authors...
foreach( var author in bookCatalog.Authors ) {
string name = author.Name;
ICollection<Book> books = author.Books;
}
// Starting from Categories...
foreach( var category in bookCatalog.Categories ){
string name = category.Name;
foreach( var book in category.Books ){
string bookTitle = book.Title;
ICollection<Author> bookAuthors = book.Authors;
}
}
未使用的/未分配的警告
你可能会注意到的一件事是,你为LINQ创建的临时变量(那些仅作为Association
属性的参数被引用,但否则从未使用过的变量)将导致Visual Studio发出警告,说这些值从未被使用或从未被分配。它们当然既被使用也被分配,但这是在后台完成的,所以编译器无法检测到。你可以像我在附带的代码中那样,使用pragma预处理器指令来抑制这些警告,将此放在每个映射回数据库的类(或字段)的上方。
// disable never used/assigned warnings for fields that are being used by LINQ
#pragma warning disable 0169, 0649
下一步?
你可以在附加的应用程序中找到这里描述的所有映射的代码,以及示例查询(位于LinqTutorialSampleQueries.cs)。它还包括一个WPF应用程序,该应用程序显示数据并允许你通过WPF数据绑定来遍历其关系。
并且,请查看本系列的第2部分和第3部分,以将你的应用程序提升到一个新的水平。
- 第一部分:将表映射到对象
- 第二部分:添加/更新/删除数据
- 第三部分:WPF数据绑定与LINQ to SQL
历史
- 2009/12/11:添加了指向第2、3部分的链接以及对引言的解释(为什么手动 vs 自动生成)。
- 2009/12/05:更新了代码,以便更容易扩展到处理更新/插入/删除。
- 将唯一的ID列设置为Identity。
- 重命名了关系,以使用一致的命名约定。
- 将Book.Category列设置为允许为空。
- 更改了数据库上Visual Studio项目设置,使其“如果较新则复制”,这样就不会覆盖数据更改。
- 在唯一的ID
[Column]
属性和[Association]
属性中添加了参数,以提供额外信息。 - 将类属性从
IEnumerable
更改为ICollection
,以提供更灵活的接口。 - 将
Book.BookAuthors
和Author.BookAuthors
属性更改为internal
。 - 初始化
EntitySet
和EntityRef
私有字段,以避免NullReferenceException
。 - 2009/10/12:初始版本。
数据库:
代码: