65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ 教程:将表映射到对象

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (116投票s)

2009年10月12日

CPOL

17分钟阅读

viewsIcon

550815

downloadIcon

14457

一个面向初学者的LINQ教程,它将引导你学习如何将SQL Server数据库表及其关系映射到对象,以及如何通过简单的LINQ查询来检索这些数据。

注意:运行此程序需要SQL Server Express 2008和.NET 3.5。

Book Catalog Application

引言

这是关于使用LINQ to SQL三部分系列的第一部分

这些教程描述了如何手动(而不是通过SqlMetal等自动化工具)将类映射到表,以便支持M:M关系以及对实体类的数据绑定。即使你选择自动生成类,理解这些技术的工作原理也能让你扩展代码,更好地满足应用程序的需求,并在问题出现时进行故障排除。

这篇第一篇文章和“图书目录”应用程序的目的是为初学者介绍LINQ,以及如何

  1. 使用LINQ to SQL将SQL Server数据库表及其关系映射到你的对象。
  2. 对这些对象执行一些简单的LINQ查询以检索你的数据。

本文将逐步讲解这些步骤,解释如何在自己的应用程序中完成这些操作。随附的“图书目录”应用程序包含了这里描述的所有映射,以及一个简单的WPF GUI,通过数据绑定来显示结果。

入门

LINQ to SQL是一个对象关系映射(ORM)工具,它允许你使用.NET 3.5框架将SQL Server数据库映射到你的类。

数据库

“图书目录”使用一个简单的SQL Server Express数据库,其中包含图书作者类别。每本书只能属于一个类别,但可以有多个作者。每位作者可以有多本书。BookAuthors表仅用于连接书籍和作者之间的M:M关系。为简化起见,除了Books.Category列之外,所有列都不允许为空值。

Database Diagram

这组表允许我们映射每种主要的关系类型(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>集合。

通常,你可能先创建类,但让我们看看这是什么样子。我们将创建三个类(AuthorBookCategory)——每个类对应一个表。因此,我们将向每个类类型添加一个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有两个这样的列:TitlePrice。对money列使用decimal,对varchar使用string,LINQ将自动为你处理这些转换。

[Column] public string Title { get; set; }
[Column] public decimal Price { get; set; }

“图书目录”应用程序也对AuthorCategory执行了相同的操作,以便我们有三个类,每个类都映射到我们主要表中的一个。

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),你就可以通过使用其中定义的集合:AuthorsBooksCategories来访问数据库中的每本书、作者和类别。

BookCatalog contains each of our tables

例如,要检索目录中的书籍,你只需遍历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关系

BookCategory之间存在多对一(M:1)关系,因为每本书只能有一个类别(而每个类别可以被多本书使用)。

Book to Catalog Relationship

在数据库中,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参数中指定)。
  • 设置此属性上的两个参数,使用你刚刚创建的字段:
    1. ThisKey:指定你指向另一个表的外键:categoryId
    2. Storage:指定你将保存另一个类实例的EntityRef<T>_category

在属性内部,通过使用包含实际Category实例的_categoryEntity属性来编码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关系。

BookAuthors Relationships

不幸的是,你仍然需要创建一个类来映射到这个连接表。但是,与外键一样,因为它仅用于后台映射,所以你可以通过将类标记为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将延迟加载Books,直到我们实际访问它(所以我们不必每次只查看一个类别时都获取书籍列表)。

将字段初始化为新的EntitySet,以避免在没有关系两端的情况下出现NullReferenceException(例如,没有书籍的类别)。

private EntitySet<Book> _books = new EntitySet<Book>();

4. 在关联类中添加一个属性

最后,创建公共Books属性,它将保存该类别中的书籍。为该属性添加一个Association属性,并设置数据库关系NameFK_Books_BookCategories)以及该属性上的三个参数,以使用你刚刚创建的字段:

  1. OtherKey:指定另一个类(Book)中保存指向我们的外键的字段:categoryId
  2. ThisKey:指定你的主键(OtherKey应该匹配的对象):Id
  3. 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关系

BookBookAuthor之间存在1:M关系。也就是说,每本书可以有多个作者,但每个书作者只引用一本书。所以,你只需要遵循上一节的四个步骤。

  1. BookAuthor类中映射指向Book的外键。你已经做过了,它叫做:bookId
  2. 定义Book的主键。你也已经做过了,它叫做:Id
  3. 添加一个引用另一个表的EntitySet<T>_bookAuthors
  4. 添加一个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 ActionBook实例将包含一个由三个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部分,以将你的应用程序提升到一个新的水平。

历史

  • 2009/12/11:添加了指向第2、3部分的链接以及对引言的解释(为什么手动 vs 自动生成)。
  • 2009/12/05:更新了代码,以便更容易扩展到处理更新/插入/删除。
  • 数据库:

    • 将唯一的ID列设置为Identity。
    • 重命名了关系,以使用一致的命名约定。
    • Book.Category列设置为允许为空。
    • 更改了数据库上Visual Studio项目设置,使其“如果较新则复制”,这样就不会覆盖数据更改。

    代码:

    • 在唯一的ID[Column]属性和[Association]属性中添加了参数,以提供额外信息。
    • 将类属性从IEnumerable更改为ICollection,以提供更灵活的接口。
    • Book.BookAuthorsAuthor.BookAuthors属性更改为internal
    • 初始化EntitySetEntityRef私有字段,以避免NullReferenceException
  • 2009/10/12:初始版本。
© . All rights reserved.