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

LINQ 教程:添加/更新/删除数据

2009 年 12 月 11 日

CPOL

17分钟阅读

viewsIcon

420961

downloadIcon

10787

本教程将逐步讲解如何使用 LINQ to SQL 在数据库中添加/更新/删除数据,同时保持类关系(多对多、一对多和多对一)的同步。

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

Book Catalog Application

引言

这是关于使用 LINQ to SQL 的三部分系列文章的第二部分。

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

本文的目的是继续介绍 LINQ to SQL,通过展示如何:

  1. 使用LINQ to SQL通过类在数据库中添加/更新/删除数据。
  2. 确保在执行这些添加/更新/删除操作时,您的多对多、一对多和多对一关系保持同步*

本文将逐步讲解这些步骤,解释如何在自己的应用程序中实现。随附的“图书目录”应用程序提供了此处描述的所有内容的示例,以及一个简单的 WPF GUI,允许您浏览数据。第三部分将扩展此内容,展示如何通过 WPF 数据绑定通过 GUI 更新数据。

*处理一对多和多对一关系的技术是MSDN 的 LINQ to SQL 白皮书中描述的那些。处理多对多关系的技术是我对这些技术的扩展。

入门

本文建立在LINQ 教程:将表映射到对象的基础上,通过 LINQ to SQL 增加了数据持久化功能。请参阅该文章的最新版本,了解应用程序的设置方式以及其类如何映射到相应的数据库表。

“图书目录”示例使用相同的数据库。也就是说,一个简单的SQL Server Express 2008数据库,其中包含图书作者类别

Database Diagram

它有一个从 Books 到 BookAuthors 的DELETE CASCADE,因此删除一本书会删除其作者,但没有其他CASCADE,因此如果作者或类别有任何书籍,则无法删除它们。所有字段都需要值,除了Books.Category,它可以是null

将更改持久化到数据库

让我们从持久化只影响单个类/表的更改开始。

注意:应用程序设置为仅在“较新”时将数据库复制到bin目录。当您对数据库进行更改时,如果您想恢复到初始数据库,只需进行一次“清理生成”即可。

LINQ 中的数据更改通过您的DataContext进行持久化。

LINQ 教程(第一部分)中,我们创建了一个强类型DataContext,名为BookCatalog,它知道如何连接到我们的数据库,并将我们所有公共的“实体”类作为Table集合。

[Database]
public class BookCatalog : DataContext
{
    public BookCatalog( ) : base( ... );
 
    public Table<Author> Authors;
    public Table<Book> Books;
    public Table<Category> Categories;
}

更新现有数据

当您从DataContext检索对象时,LINQ to SQL 会跟踪对它们进行的所有更改,您可以通过调用SubmitChanges()来持久化这些更改。

我们将以Category为例,将其“编程实践”类别重命名为“技术实践”进行更新。

1. 从 DataContext 中检索要更改的对象

使用 LINQ 从您的DataContext中检索您要更新的类别。

LINQ 的Single方法提供了一种简单的方法,通过接受一个返回单个对象的 Lambda 表达式来实现。在本例中,即Name == "Programming Practices"的类别(c)。

BookCatalog bookCatalog = new BookCatalog( );
Category category = 
  bookCatalog.Categories.Single( c => c.Name == "Programming Practices" );

2. 更新对象

category.Name = "Technical Practices";

3. 将更改提交到 DataContext

bookCatalog.SubmitChanges( );

您的更新现在已持久化到数据库。

添加新数据

您可以通过创建新对象并将它们添加到DataContext中相应的Table集合来向数据库添加新记录。

如果您的表使用标识列作为其主键(如Category),LINQ to SQL 会在对象添加到数据库后自动使用该 ID 更新您的对象。

举例来说,我们将为 Java 添加一个新的Category

1. 创建新对象

如果您的主 ID 列是标识列,请将其留空,让数据库为您设置。

Category category = new Category( ) { Name = "Java" };

2. 将对象添加到 DataContext 中相应的 Table 集合

这通过您的Table集合上的InsertOnSubmit()方法完成。

BookCatalog bookCatalog = new BookCatalog( );
bookCatalog.Categories.InsertOnSubmit( category );

3. 将更改提交到 DataContext

数据将存在于您的DataContext中,但在您调用SubmitChanges()之前不会持久化到数据库。

bookCatalog.SubmitChanges( );

4. 数据库生成的 ID 会自动为您设置

如果您的类有一个标识列,并且在其Column属性中被标记为IsDbGenerated,就像Category一样

[Table( Name = "BookCategories" )] 
public class Category : IBookCollection
{
    [Column( IsPrimaryKey = true, IsDbGenerated = true )] public int Id { get; set; }
    ...

那么在SubmitChanges()时,LINQ to SQL 将自动为您更新该列的值为数据库设置的 ID。

删除数据

同样,您可以通过从DataContext中相应的Table集合中删除记录来将其从数据库中移除。

例如,我们将删除刚刚创建的 Java Category

1. 从 DataContext 中检索要更改的对象

从您的DataContext中检索您要删除的对象:

BookCatalog bookCatalog = new BookCatalog( );
Category category = bookCatalog.Categories.Single( c => c.Name == "Java" );

2. 从 DataContext 中相应的 Table 集合中删除对象

这通过您的Table集合上的DeleteOnSubmit()方法完成。

bookCatalog.Categories.DeleteOnSubmit( category );

3. 将更改提交到 DataContext

数据将已从您的DataContext中本地移除,但在您调用SubmitChanges()之前不会从数据库中删除。

bookCatalog.SubmitChanges( );

请注意,对于任何给定的事务,每个步骤都必须使用相同的DataContext完成。例如,您不能从一个DataContext实例检索对象,然后将其传递给来自不同DataContext实例的DeleteOnSubmit()方法。

更新关系

LINQ to SQL 不会自动管理相关对象之间的一致性。因此,您需要向类添加逻辑,以便它们知道如何自行更新其关系。

更新多对一关系

在我们的多对一类中,我们希望能够从两个方向遍历对象模型。例如,每本Book都应该包含其所属Category的一个实例,每个Category都应该包含其Book的集合。

Book to Catalog Relationship

想象一下,您注意到书籍《深入 C#》的类别被设置为Java,您想更正它。当您将《深入 C#》更新为类别C#时,会发生三件事:

  1. Book《深入 C#》的Category更新为C#
  2. CategoryC#Book列表更新为包含《深入 C#》。
  3. CategoryJavaBook列表更新为不再包含《深入 C#》。

当您调用SubmitChanges()时,LINQ to SQL 将为您处理步骤 #1,但您的类负责更新其关系的其他方面(步骤 2 和 3)。

LINQ 教程(第一部分)中,我们将BookCategory属性设置为Association,以便我们可以检索与此书关联的Category实例(而不仅仅是数据库中存储的 ID)。

[Table( Name = "Books" )]
public class Book
{
    ...

    [Column( Name = "Category" )] private int? categoryId;
    private EntityRef<Category> _category = new EntityRef<Category>( );

    [Association( Name = "FK_Books_BookCategories", 
      IsForeignKey = true, Storage = "_category", 
      ThisKey = "categoryId" )]
    public Category Category {
        get { return _category.Entity; }
        set { _category.Entity = value; }
    }

Association属性告诉 LINQ to SQL 将相应Category的实例放置在EntityRef_category中,然后我们将其用作Category属性的后备字段。

更新属性的设置方法以同步关系的其他方面

BookCategory属性更新时,我们需要做两件事:

  1. 从旧类别的书籍列表中移除该书。
  2. Category priorCategory = _category.Entity;
    if( priorCategory != null )
        priorCategory.Books.Remove( this );
  3. 将此书添加到新类别的书籍列表中。
  4. Category newCategory = value;
    if( newCategory != null )
        newCategory.Books.Add( this );

但是,这是棘手的部分,我们将指示关系的每一方在更新时通知另一方。因此,我们需要向此方法添加一些额外的检查以防止其循环:

  1. 如果新类别与旧类别相同,则不执行任何操作。
  2. 在调用priorCategory.Books.Remove()之前,将我们的类别设置为null
  3. 在调用newCategory.Books.Add()之前,将我们的类别设置为其新值。

Categoryset方法的最终版本包含这些检查:

public Category Category {
    ...
    set {
          Category priorCategory = _category.Entity;
          Category newCategory = value;

          if( newCategory != priorCategory ) {

              // remove this book from our prior category's list of books
              _category.Entity = null;
             if( priorCategory != null ){
                  priorCategory.Books.Remove( this );
             }

             // set category to the new value
              _category.Entity = newCategory;

              // add this book to the new category's list of books
             if( newCategory != null ){
                  newCategory.Books.Add( this );
             }
          }
       }
}

尝试一下:多对一更新

让我们将两本《C# 中的 LINQ 高级编程》的Category更新为C#(它们目前属于 LINQ 类别)。在我们的类中有两种查看此关系的方式:

  1. 这两个Book实例的Category都是LINQ
  2. Book Details

  3. LINQCategory在其Books属性中包含这些书(而C#category不包含)。

    C# and LINQ books

在我们的“图书目录”应用程序中,一本书只能属于一个类别,因此当我们更新书的类别时,它应该自动从LINQ类别移动到C#类别。

为此,从DataContext中检索这两本书,并将其Category设置为C#类别。然后,调用SubmitChanges()将更改持久化到数据库。

BookCatalog bookCatalog = new BookCatalog( );
Category csharpCategory = 
  bookCatalog.Categories.Single( cat => cat.Name == "C#" );

var books = bookCatalog.Books.Where(
 b => b.Title.Contains("Pro LINQ: Language Integrated Query in C#"));
foreach( var book in books ) {
     book.Category = csharpCategory;
}

bookCatalog.SubmitChanges();

尽管只更新了Book对象,但我们添加的同步代码导致Category对象也得到了更新。

  1. 这两个Book实例现在都将Category设置为C#
  2. Book Details

  3. C#Category在其Books属性中包含这两本书(而LINQCategory不包含)。
  4. C# and LINQ books

在连接表中更新多对一关系

现在,让我们同步多对多连接表中的多对一关系。每当我们有一个多对多连接表时,就像我们对BookAuthors所做的那样,连接表当然只是两个多对一关系,所以我们可以模仿我们对Book.Category所做的事情。

例如,BookAuthors包含一个到Book的多对一关系和一个到Author的多对一关系,以桥接它们的多对多关系。

BookAuthors Relationships

我们希望我们的连接表提供一个中心位置,以确保多对多关系的两边都保持同步。因此,无论某人更新Book以拥有新作者,还是更新Author以添加该人的书籍,核心类BookAuthor都将确保两边相应地同步。

在我们的示例中,我们将更新BookAuthorAuthorset()方法,就像我们更新Book.Categoryset()一样。

[Table( Name = "BookAuthors" )]
internal class BookAuthor
{
    ...

    public Author Author {
        ...
        set {
            Author priorAuthor = _author.Entity;
            Author newAuthor = value;

            if( newAuthor != priorAuthor ) {
                _author.Entity = null;
                if( priorAuthor != null )
                   priorAuthor.BookAuthors.Remove( this );

                _author.Entity = newAuthor;
                newAuthor.BookAuthors.Add( this );
            }
        }
    }

以及BookAuthorBook

public Book Book {
    ...
    set {
       Book priorBook = _book.Entity;
       Book newBook = value;

        if( newBook != priorBook ) {
            _book.Entity = null;
            if( priorBook != null )
                priorBook.BookAuthors.Remove( this );

            _book.Entity = newBook;
            newBook.BookAuthors.Add( this );
        }
    }
}

我们还不能看到它的实际效果。首先,我们必须了解如何处理多对多关系的另一端:一对多关系。

更新一对多关系

就像我们可以更新一本书的类别并让它自动为我们同步旧类别和新类别一样,我们应该能够从一个类别中添加或移除书籍,并让它们相应地更新Book实例。

本节将以category.Books为例逐步讲解这一点。

LINQ 教程(第一部分)中,我们将CategoryBooks属性设置为Association的另一端——即包含书籍集合的那一端。

[Table( Name = "BookCategories" )] 
public class Category
{
    ...

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

    [Association( Name = "FK_Books_BookCategories", 
      Storage = "_books", OtherKey = "categoryId", 
      ThisKey = "Id" )]
    public ICollection<book> Books {
        get { return _books; }
        set { _books.Assign( value ); }
    }
}

要同步此关系:

  1. 每当一本书被添加到某个类别时,将该书的Category设置为this
  2. 每当一本书从某个类别中移除时,将该书的Category设置为null

这是最酷的部分。在 LINQ to SQL 中,书籍集合由EntitySet支持。而且,EntitySet允许您指定委托,以便在项目添加到或从其集合中移除时调用。因此,创建两个委托方法来处理此同步:

  1. 添加时,将书籍的Category设置为this
  2. private void OnBookAdded(Book addedBook ){
        addedBook.Category = this;
    }
  3. 移除时,将书籍的Category设置为null
  4. private void OnBookRemoved(Book removedBook ){
        removedBook.Category = null;
    }

然后,通过传入这两个委托方法来构造EntitySet。由于这些方法是实例方法,您需要在Category的构造函数中完成此操作。

public Category( ){
    _books = new EntitySet<Book>( OnBookAdded, OnBookRemoved );
}

在一对多关系中添加数据

为了演示添加新的一对多关系,请添加几本没有类别的新书。使用InsertAllOnSubmit()一次添加多条记录,并使用SubmitChanges()将其持久化到数据库。

IEnumerable<Book> books = new List<Book>( ){
      new Book( ){ Title = 
        "Essential Windows Presentation Foundation", Price = 44.99m },
      new Book( ){ Title = "WPF In Action", Price = 40.99m } };
 
BookCatalog bookCatalog = new BookCatalog( );
bookCatalog.Books.InsertAllOnSubmit( books );
bookCatalog.SubmitChanges( );

现在,创建一个新的WPFCategory并将这两本书添加到其中。

Category category = new Category( ) { Name = "WPF" };
foreach( var wpfBook in books ){
     category.Books.Add( wpfBook );
}

bookCatalog.Categories.InsertOnSubmit( category );
bookCatalog.SubmitChanges( );

请注意,我们从未直接在书籍上设置Category,但我们添加的同步代码会为我们处理。所以现在,如果您查看书籍的详细信息,您会看到它们的Category被设置为WPF

WPF Books

在一对多关系中“移动”数据

在一对多关系中,例如Category:Books,一本书只能属于一个Category。因此,如果您将一本属于某个类别的书添加到另一个类别中,您实际上是在将其移动到该新类别(即使您没有明确要求这样做)。

例如,我们可能会决定将这两本《C# 中的 LINQ 高级编程》最终移回到 LINQ 类别。然而,我们不必设置每本书的Category属性,只需将它们添加LINQCategory中即可。

BookCatalog bookCatalog = new BookCatalog( );
var books = bookCatalog.Books.Where( 
  b => b.Title.Contains( "Pro LINQ: Language Integrated Query in C#" ) );

Category linqCategory = 
  bookCatalog.Categories.Single( cat => cat.Name == "LINQ" );
foreach( var book in books ) {
    linqCategory.Books.Add( book );
}

bookCatalog.SubmitChanges( );

现在,当您查看书的详细信息时,您会看到每本书的Category都已恢复为LINQ

Book Details

在一对多关系中删除数据

当我们从类别中移除一本书时,我们的OnBookRemoved委托会清除该Book实例的Category属性。

由于某些原因,我们有两本《编程 Ruby》,所以让我们从RubyCategory中移除《编程 Ruby 1.9》版本。

BookCatalog bookCatalog = new BookCatalog( );
Book book = bookCatalog.Books.Single( 
  b => b.Title.Contains( "Programming Ruby 1.9" ) );

Category rubyCategory = bookCatalog.Categories.Single( cat => cat.Name == "Ruby" );
rubyCategory.Books.Remove( book );

bookCatalog.SubmitChanges( );

现在,当您查看书的详细信息时,您会看到其Category为空。

Ruby Book

更新多对多关系

最后,我们可以完成多对多关系。这有点棘手,因为我们希望将连接表 (BookAuthor) 排除在公共接口之外,以便调用者可以直接使用BookAuthor,而无需关心它们是如何连接在一起的。

M:M Relationship between Books and Authors

如果您一直在编写代码,那么您已经在BookAuthor中添加了逻辑,以便在任何一方更新BookAuthor关系时,它都会为您处理BookAuthor的同步。剩下的是提供一种机制,允许用户设置Book的作者和Author的书籍。

我们将从Book端开始,逐步讲解如何实现这一点。

LINQ 教程(第一部分)中,我们将BookBookAuthors的关联设置为内部,因此它不是我们公共接口的一部分。

private EntitySet<BookAuthor> _bookAuthors = new EntitySet<BookAuthor>( );
[Association( Name = "FK_BookAuthors_Books", 
 Storage = "_bookAuthors", 
 OtherKey = "bookId", ThisKey = "Id" )]
internal ICollection<BookAuthor> BookAuthors {
    get { return _bookAuthors; }
    set { _bookAuthors.Assign( value ); }
}

并且,我们创建了一个公共Authors属性,它充当代理,从BookAuthors中提取这本书的作者。

public ICollection<Author> Authors { 
  get { return ( from ba in BookAuthors select ba.Author ).ToList( ); } }

这种方法的问题在于,我们现在无法知道调用者何时添加或移除了Authors。我们需要类似于EntityRefOnAddOnRemove委托的东西,以便在调用者添加或移除作者时执行同步。

1. 将 Authors 设为 ObservableCollection

由于我们的数据不在 EntryRef 中,我们需要另一种方式来获取作者添加或删除时的通知。我们可以通过让Authors返回一个ObservableCollection来实现这一点,它会在发生更改时通知我们。

public ICollection<author> Authors {
    get {
          var authors = new ObservableCollection<Author>( 
                  from ba in BookAuthors select ba.Author );
          authors.CollectionChanged += AuthorCollectionChanged;
          return authors;
        }
    }

第二行(authors.CollectionChanged += AuthorCollectionChanged)注册了我们的AuthorCollectionChanged方法(下面添加),以便在集合更改时接收通知。

2. 创建 OnAdd 和 OnRemove 通知方法

要将我们的关系与BookAuthor同步:

  1. 当一个作者被添加到一本书时,我们需要添加一个BookAuthor(连接)记录来存储这个关系。
  2. 当一个作者从一本书中移除时,我们需要移除BookAuthor(连接)关系记录。

创建委托方法来处理此同步:

  1. 添加时,添加一条BookAuthor记录以存储此关系。
  2. private void OnAuthorAdded( Author addedAuthor ){
        BookAuthor ba = new BookAuthor( ) { Author = addedAuthor, Book = this };
    }
  3. 删除时,删除BookAuthor记录以删除此关系(我们将在下面添加Remove()方法到BookAuthor中)。
  4. private void OnAuthorRemoved( Author removedAuthor ) {
        BookAuthor baRecord = BookAuthors.SingleOrDefault( ba => 
           ba.Book == this && ba.Author == removedAuthor );
        if( baRecord != null )
            baRecord.Remove( );
    }

3. 创建 AuthorCollectionChanged 方法以接收更新通知

这是我们注册的委托方法,用于接收对Authors集合所做所有更改的通知。

创建响应AddRemove事件的方法,并使用其NotifyCollectionChangedEventArgs来访问已添加(e.NewItems)或已删除(e.OldItems)的项目。

private void AuthorCollectionChanged( object sender, 
                NotifyCollectionChangedEventArgs e ) {
    if( NotifyCollectionChangedAction.Add == e.Action ) {
        foreach( Author addedAuthor in e.NewItems )
            OnAuthorAdded( addedAuthor );
    }

    if( NotifyCollectionChangedAction.Remove == e.Action ) {
        foreach( Author removedAuthor in e.OldItems )
            OnAuthorRemoved( removedAuthor );
    }
}

4. 将新的(已添加的)BookAuthors 记录附加到 DataContext

如果您一直都在编写代码,那么您在向BookAuthorBookset()方法添加逻辑以将新的BookAuthor实例添加到Book时,就已经完成了此操作。

internal class BookAuthor
{
   ...
   public Book Book {
      ...
      set {
             ...
             Book newBook = value;
             newBook.BookAuthors.Add( this );

其工作方式是,当您将BookAuthor实例附加(添加)到您的Book时,LINQ to SQL 会注意到此更改(因为它正在监视Book的更改),因此在您下次调用SubmitChanges() 时,它将自动将此附加的 BookAuthor 记录插入到数据库中。瞧,一个新的BookAuthor关系就建立了。

5. 为 BookAuthor 创建 Remove 方法

BookAuthor添加一个Remove()方法,它将:

  1. 从数据库中删除记录。
  2. Book中移除BookAuthor实例。
  3. Author中移除BookAuthor实例。
public void Remove( ){
    BookCatalog.RemoveRecord( this );

    Author priorAuthor = Author;
    priorAuthor.BookAuthors.Remove( this );

    Book priorBook = Book;
    priorBook.BookAuthors.Remove( this );
}

步骤 1 调用DataContext上的RemoveRecord()方法(我们将在下面添加)。之所以这样做,是因为与我们迄今为止所做的任何其他更改不同,删除记录始终需要直接访问DataContext。但是,我们不希望必须将DataContext传递给我们的实体类。

我们可以通过在DataContext上使用一个static方法来处理这个问题,该方法:

  1. 检索要使用的DataContext实例*
  2. 从该DataContext实例中删除记录。

该方法使用泛型,因此可以接受任何记录。下面的示例通过创建新的DataContext实例来处理删除,从而处理步骤 #1:*

[Database]
public class BookCatalog : DataContext
{
    public static void RemoveRecord<T>( T recordToRemove ) where T : class {
        // Retrieve BookCatalog instance in way
        // that's consistent with your DataContext strategy
        BookCatalog bookCatalog = new BookCatalog();

        Table<T> tableData = bookCatalog.GetTable<T>( );
        var deleteRecord = tableData.SingleOrDefault( record => record == recordToRemove );
        if( deleteRecord != null ) {
            tableData.DeleteOnSubmit( deleteRecord );
        }

        // If you created a new BookCatalog instance, submit it's changes here
        bookCatalog.SubmitChanges( );
    }

*在某些应用程序中,使用现有DataContext可能比创建新DataContext更合适。请参阅 Rick Strahl 撰写的LINQ to SQL DataContext 生命周期管理,了解各种DataContext处理策略,然后更新此方法检索其DataContext的方式,使其与该策略保持一致(例如,如果您使用单个全局DataContext,则让此方法使用该全局DataContext而不是创建新的)。

警告:如果您确实选择为RemoveRecord()创建一个新的DataContext实例,则必须:

  1. 在方法提交更改。这是最简单的解决方案;但是,如果您正在删除这些记录作为较大事务的一部分,它可能会出现问题,因为如果您选择取消(不Submit)该较大事务,那么此删除将已经完成。
  2. 保留DataContext实例,以便您可以在更合适的时间提交其更改。

选项 #1 显示在上面。选项 #2 可能看起来像下面这样:

public class BookCatalog : DataContext
{
  // Create static DataContext for removing M:M Join records 
  private static DataContext contextForRemovedRecords = null;

  public static void RemoveRecord<T>( T recordToRemove ) where T : class {
      // Use the static contextForRemovedRecords
      if( contextForRemovedRecords == null ) 
          contextForRemovedRecords = new BookCatalog( );

      Table<T> tableData = contextForRemovedRecords.GetTable<T>( );
      var deleteRecord = tableData.SingleOrDefault( record => record == recordToRemove );
      if( deleteRecord != null ) {
          tableData.DeleteOnSubmit( deleteRecord );
      }
  }

  // NEW method (not part of LINQ to SQL) to cancel changes
  public void CancelChanges( ) {
      contextForRemovedRecords = null;
  }

  // Override DataContext's SubmitChanges() to handle any removed records
  public new void SubmitChanges( ) {
      if( contextForRemovedRecords != null ) {
          contextForRemovedRecords.SubmitChanges( );
      }
      base.SubmitChanges( );
  }

这做了三件事:

  1. 创建一个static DataContext实例(contextForRemovedRecords)并在RemoveRecord()中使用它。
  2. contextForRemovedRecords的更改延迟提交,直到下次在BookCatalog实例上调用SubmitChanges()*
  3. 添加了一个新方法CancelChanges()(不属于标准 LINQ to SQL)以允许用户取消更改。

*显然,如果您正在处理并发更改,则需要构建此功能来处理它们。

5. 更新多对多关系的另一端

最后,更新多对多关系另一端的代码。以下是Author的显示方式:

public ICollection<Book> Books {
    get {
        var books = new ObservableCollection<Book>( from ba in BookAuthors select ba.Book );
        books.CollectionChanged += BookCollectionChanged;
        return books;
    }
}

private void BookCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) {
    if( NotifyCollectionChangedAction.Add == e.Action ) {
        foreach( Book addedBook in e.NewItems ) 
            OnBookAdded( addedBook );
    }

    if( NotifyCollectionChangedAction.Remove == e.Action ) {
        foreach( Book removedBook in e.OldItems )
            OnBookRemoved( removedBook );
    }
}

private void OnBookAdded( Book addedBook ){
    BookAuthor ba = new BookAuthor( ) {Author = this, Book = addedBook};
}

private void OnBookRemoved( Book removedBook ) {
    BookAuthor baRecord = BookAuthors.SingleOrDefault( 
        ba => ba.Author == this && ba.Book == removedBook );
    if( baRecord != null ) {
        baRecord.Remove( );
    }
}

在多对多关系中添加数据

我们现在已经完成了更新类以处理同步。让我们看看如何使用这些类执行多对多更新。

添加 Bob Martin 作为《解释极限编程 (XP)》的作者。为此,从您的DataContext中检索 Bob Martin 和该书,将 BobMartin 添加到该书的作者中,然后SubmitChanges()

BookCatalog bookCatalog = new BookCatalog( );
Author bobMartin = bookCatalog.Authors.Single( 
  author => author.Name == "Bob Martin" );
Book xpExplained = bookCatalog.Books.Single( 
  book => book.Title.Contains("Extreme Programming Explained") );

xpExplained.Authors.Add( bobMartin );
bookCatalog.SubmitChanges();

瞧,Bob Martin 现在是《XP Explained》的作者,并且它出现在他的书列表中。

Bob Martin has been added to XP Explained

在多对多关系中删除数据

当然,Bob Martin 并非真的是《XP Explained》的作者,我们也不想惹 Kent Beck 生气,所以把他移除吧。我们可以从关系另一端进行操作,以验证我们的更新在两个方向上都有效。

再次从您的DataContext中检索 Bob Martin 和该书,但这次,将该书从 Bob Martin 中移除。

BookCatalog bookCatalog = new BookCatalog( );
Author bobMartin = 
  bookCatalog.Authors.Single( author => author.Name == "Bob Martin" );
Book xpExplained = bookCatalog.Books.Single( 
  book => book.Title.Contains( "Extreme Programming Explained" ) );

bobMartin.Books.Remove( xpExplained );
bookCatalog.SubmitChanges( );

数据又恢复正常了;Kent Beck 是《XP Explained》的唯一作者,它不再出现在 Bob Martin 的书列表中。

Bob Martin removed from XP Explained

使用 DELETE CASCADE 删除多对多关系中的数据

最后,让我们删除一本关联了作者的书。

我们的 BookCatalog 应用程序在 Book 和 BookAuthors 之间设置了DELETE CASCADE,因此数据库会在 LINQ 不知情的情况下删除这些记录,但我们添加到实体类中的逻辑应该会自动为我们处理同步。

我们将删除 BookCatalog 中第二本《编程 Ruby》的副本。这本书目前有三位作者,因此删除这本书应该会自动删除所有三条BookAuthor(连接)记录,并更新每位作者的书籍列表,使其不再包含这本书。

Programming Ruby 1.9 has 3 Authors

为此,从您的DataContext中检索该书,从Books表集合中删除它,然后SubmitChanges()

BookCatalog bookCatalog = new BookCatalog( );
Book rubyBook = bookCatalog.Books.Single( 
  book => book.Title.Contains( "Programming Ruby 1.9" ) );
bookCatalog.Books.DeleteOnSubmit( rubyBook );
bookCatalog.SubmitChanges( );

这将删除该书,并相应地更新每位作者的书籍列表。

下一步?

您可以在随附的应用程序中找到此处介绍的所有代码以及示例更新查询(位于LinqTutorialSampleUpdates.cs中)。它还包含一个 WPF 应用程序,用于显示数据并允许您通过 WPF 数据绑定遍历其关系。第三部分将扩展此功能,展示如何通过 WPF 数据绑定通过 GUI 更新数据。

并且,查看本系列的其余部分,将您的应用程序提升到新的水平。

历史

  • 2009年12月11日:在引言中添加了说明(为什么手动而非自动生成)。构建了删除多对多连接记录的方法以处理更多场景(参见5. 为 BookAuthor 创建 Remove 方法)。
  • 2009年12月06日:初始版本。
© . All rights reserved.