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

DLINQ 简介 - 第二部分(共三部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (66投票s)

2007年4月4日

CPOL

26分钟阅读

viewsIcon

232191

downloadIcon

1287

LINQ 简介

目录

引言

.NET 3.0 已经发布,所以我们现在都应该了解它了,对吧?天哪,感觉 .NET 2.0 出来还没多久。嗯,对于那些不知道的人来说,.NET 3.0 实际上包含了很多新东西,例如

  • Windows Workflow Foundation (WWF):管理对象生命周期/持久对象存储
  • Windows Communication Foundation (WCF):新的通信层
  • Windows Presentation Foundation (WPF):新的演示层 (XAML)
  • Windows Cardspace:为处理和管理各种数字身份提供基于标准的解决方案

所以你可以看到有很多东西需要学习。我正在学习 WPF/WCF,但我也对一个名为 LINQ 的小宝石感兴趣,我相信它将成为 .NET 3.5 和 Visual Studio “Orcas”(现在它的名称)的一部分。LINQ 将为 C# 和 VB.NET 添加新功能。LINQ 有三种风格

  • LINQ:用于内存对象的语言集成查询(在 2007 年 3 月的 Orcas CTP 中重命名为 LINQ to Objects)
  • DINQ:用于数据库的语言集成查询(在 2007 年 3 月的 Orcas CTP 中重命名为 LINQ to SQL)
  • XLINQ:用于 XML 的语言集成查询(在 2007 年 3 月的 Orcas CTP 中重命名为 LINQ to XML)

LINQ 很酷,我最近一直在研究它,所以我想写一篇关于我在 LINQ/DLINQ/XLINQ 领域所学知识的文章,希望能帮助到一些好心人。本文将重点介绍 SLINQ,它是拟议系列三篇文章中的第二篇。

拟议的文章系列内容将如下:

  1. 第 1 部分:将全部关于标准 LINQ,它用于查询内存中的数据对象,如 List、数组等。
  2. 第 2 部分(本文):将介绍如何使用 DLINQ,即用于数据库数据的 LINQ
  3. 第 3 部分:将介绍如何使用 XLINQ,即用于 XML 数据的 LINQ

必备组件

要运行本文随附的代码,您需要安装 2006 年 5 月 LINQ CTP,可在此处下载。有一个新的 2007 年 3 月 CTP 可用,但完整安装大约 4GB(因为它不仅是 LINQ,而且是代号为“Orcas”的下一代 Visual Studio 的全部内容),而且相当复杂,并且可能还会改变,所以 2006 年 5 月 LINQ CTP 足够用于本文演示的目的。

DLINQ 与 Entity Framework

DLINQ 本身允许开发人员查询存储在 SQL Server 数据库中的数据。DLINQ 仅支持 SQL Server。为了实现这一点,必须创建某些实体。这些实体放置在一个代码文件中,并且应该与数据库架构匹配。然后这些实体会用一些新的 DLINQ 属性进行注释。这些属性允许 DLINQ 正确识别实体,并让 DLINQ 创建适当的 SQL 命令发送到数据库。例如,有 Table、Column 和 Association 属性,它们都反映了真实的数据库对象:Table、Field(或者如果您更喜欢 Column)和 Relationship。

从我阅读的关于这个主题的资料来看,这与一种名为 O/R 映射(对象关系映射)的技术相当相似。

“对象关系映射(又称 O/RM、ORM 和 O/R 映射)是一种编程技术,用于在数据库和面向对象编程语言中不兼容的类型系统之间转换数据。实际上,这创建了一个“虚拟对象数据库”,可以从编程语言中使用。有免费和商业软件包可用于执行对象关系映射,尽管一些程序员选择创建自己的 ORM 工具。”

这就是 DLINQ 试图实现的目标;它确实提供了一个持久层,可以在其中保留更改,等待提交到底层数据库。因此,您可以将其视为数据库的内存版本。

显然,LINQ 需要某种方式来了解数据库的结构(以便它能够生成正确的 SQL 命令以提供正确的结果),这就是实体代码文件发挥作用的地方。通过使用新的 Table、Column 和 Association DLINQ 属性,我们能够正确地注释一些相当标准(尽管有一些新的 DLINQ 类,例如 EntitySet)的 .NET 代码,以使其类似于底层数据库结构。

因此,一旦这些实体存在,使用 DLINQ,就能够查询、删除、更新实体或插入新实体。因此,如果 DLINQ 被要求删除一个实体,它将(在幕后)创建适当的 SQL 命令来从请求的数据库表中删除请求的行。

据我所知,DLINQ 和实体文件是相辅相成的;您必须有实体文件才能使用 DLINQ,而实体文件实际上只在处理 DLINQ 时才使用。因此,我认为可以公平地说,有 DLINQ 的地方,就一定有实体代码,允许 DLINQ 创建底层数据库的适当内存结构。否则,DLINQ 将无法知道如何创建正确的 SQL 命令。

可能有些读者认为 O/R Mapping 已经存在一段时间了,其中一些人甚至可能以前使用过它。我不会参与这场辩论,因为我个人没有在 .NET 中使用过任何 O/R Mapping 的东西,所以无法发表评论。我确实知道 DLINQ 的方法与 Java 的 J2EE Enterprise Java Beans(我刚刚完成了一个相当大的项目)非常相似,只是 Java 支持大多数数据库,而不仅仅是 SQL Server。

本文的其余部分将重点介绍如何开始使用 DLINQ,以及为了生成实体文件以及 DLINQ 查询,您必须执行哪些步骤。

DLINQ 快速入门指南

要开始使用 DLINQ,实际上只有几个主要步骤,它们是:

  1. 定义实体代码文件
  2. 创建一个 DataContext
  3. 运行查询
  4. 修改值并提交更新

这确实就是我们需要做的全部。因此,在本文的其余部分,我将向您展示如何做到这一点。

第一步:创建实体代码文件

在使用 DLINQ 之前,我们需要创建一个实体文件。

什么是实体代码文件?

实体代码文件很简单。您将用来表示应用程序数据的对象类。

那么它们看起来像什么,这一切意味着什么?

简而言之,它们反映了您希望使用 DLINQ 交互的给定数据库中表的结构。

我很(不幸?)最近做了一些 Java J2EE Enterprise Java Bean 开发,所以如果你也有一些 Java 经验,思考这些实体代码文件的最佳方式与 Java 中的实体 bean 相同。

但是,如果您是纯粹的 Microsoft 爱好者(我也有这种倾向,别担心,四年的 Java 慢慢改变了我的想法),那么别担心,我将立即介绍什么是实体文件。

假设我们的数据库中有一个表(例如,我们使用的是2006 年 5 月 LINQ CTP 附带的常见 Northwind 示例数据库),并且我们想为其创建一个实体类,我们只需要在类声明的顶部应用一个自定义属性。DLINQ 为此定义了 Table 属性。

  [Table(Name="Customers")]
  public class Customer
  {

  public string CustomerID;
  public string City;
  }

Table 属性有一个 Name 属性,您可以使用它来指定数据库表的精确名称。如果未提供 Name 属性,DLINQ 将假定数据库表与类同名。只有声明为表的类实例才能存储在数据库中。

这些类型的类的实例被称为实体,而这些类本身被称为实体类。

除了将类与表关联起来,您还需要指定要与数据库列关联的每个字段或属性。为此,DLINQ 定义了 Column 属性。

  [Table(Name="Customers")]
  public class Customer
  {
  [Column(Id=true)]
  public string CustomerID;
  [Column]
  public string City;
  }

Column 属性有各种属性,您可以使用它们来自定义字段与数据库列之间的精确映射。值得注意的一个属性是 Id 属性。它告诉 DLINQ 数据库列是表主键的一部分。

与 Table 属性一样,您只需在 Column 属性中提供与字段或属性声明中推断出的信息不同的信息。在此示例中,您需要告诉 DLINQ CustomerID 字段是表主键的一部分,但您不必指定确切的名称或类型。

只有声明为列的字段和属性才会持久化到数据库或从数据库检索。其他的将被视为应用程序逻辑的瞬态部分。

其他列属性

还有更多的列属性,例如

属性 类型 描述
名称 字符串 表中或视图中列的名称。如果未指定,则假定列与类成员具有相同的名称。
存储 字符串 底层存储的名称。如果指定,它会告诉 DLINQ 如何绕过数据成员的公共属性访问器并与原始值本身交互。如果未指定,DLINQ 将使用公共访问器获取和设置值。
数据库类型 字符串 使用数据库类型和修饰符指定的数据库列类型。这将是用于在 T-SQL 表声明命令中定义列的精确文本。如果未指定,数据库列类型将从成员类型推断。只有在期望使用 CreateDatabase() 方法创建数据库实例时,才需要特定的数据库类型。
ID Bool 如果设置为 true,则类成员表示表主键的一部分列。如果类的多个成员被指定为 Id,则主键被认为是相关列的复合。至少一个成员必须具有此属性,并且必须映射到相应表/视图中的主键或唯一键。不支持没有唯一键的表/视图。
自动生成 布尔值 标识成员的列值由数据库自动生成。指定 AutoGen=true 的主键也应具有带有 IDENTITY 修饰符的 DBType。AutoGen 成员在数据行插入后立即同步,并在 SubmitChanges() 完成后可用。
版本 布尔值 将成员的列类型标识为数据库时间戳或版本号。版本号在每次关联行更新时递增,时间戳列更新。IsVersion=true 的成员在数据行更新后立即同步。新值在 SubmitChanges() 完成后可见。
更新检查 更新检查 确定 DLINQ 如何实现乐观并发冲突检测。如果没有成员被指定为 IsVersion=true,则通过将原始成员值与当前数据库状态进行比较来完成检测。您可以通过为每个成员提供 UpdateCheck 枚举值来控制 DLINQ 在冲突检测期间使用哪些成员。
始终 - 始终使用此列进行冲突检测
从不 - 从不使用此列进行冲突检测
更改时——仅当成员已被应用程序更改时才使用此列
是否鉴别器 布尔值 确定类成员是否保存继承层次结构的鉴别器值。

DLINQ 中还有许多其他自定义属性可以使用,例如:

  • System.Data.DLinq.DatabaseAttribute
  • System.Data.DLinq.TableAttribute (如上所述)
  • System.Data.DLinq.AssociationAttribute (SQL 中称之为关系)
  • System.Data.DLinq.StoredProcedureAttribute
  • System.Data.DLinq.FunctionAttribute
  • System.Data.DLinq.ParameterAtttribute
  • System.Data.DLinq.InheritenceMappingAttribute

我建议查看 DLINQ 概述文档,它随2006 年 5 月 LINQ CTP 下载包一同提供。

DLINQ 依赖这些属性才能正确定义一个完整的类。所以可以想象,对于一个包含大量表的整个数据库来说,这将是大量的工作。幸运的是,我们有许多作弊方法可以使用。让我们来看看吧。

技巧 1:DLINQ 设计器

好吧,当你安装2006 年 5 月 LINQ CTP 时,你会在 Visual Studio 中获得额外的项目模板以及新的项目项,你可以将它们添加到你的项目中。其中一个新项目项是 DLinqObjects 项。添加后,它会向你的项目添加一个新的 *.dlinq 文件。这个文件是一个 DLINQ 设计器文件。

Screenshot - DDes1.png

当您单击新添加的 *.dlinq 文件时,您将获得一个新的设计器表面,您可以从中将以下项目添加到工作表面:

  • 新类
  • 关联
  • 继承

一旦添加到工作台面,您可以使用 Visual Studio 交互方法(单击、右键单击)来调整工作台面上的控件。

使用设计器最主要的工作是添加一个新类(可以认为是 SQL 中的表),然后添加新属性(可以认为是表中的字段)。

一旦您对所需的结构满意,就可以构建项目,Visual Studio 将自动生成代码并将其放置在当前 *.dlinq 文件的相关代码隐藏文件(在我的情况下是 .cs)中。

没有更好的办法了吗?

嗯,实际上是有的。我们在 Visual Studio 中有一个服务器资源管理器,对吧?那么为什么不使用它来导航到正确的数据库,然后简单地将所需的表拖到 DLINQ 设计器工作台面呢?所以,让我们假设我已经导航到我自己的本地(你的将是你的本地)SQL Server(2005 Express)安装,并将其指向我安装的 Northwind(2006 年 5 月 LINQ CTP 附带的那个),并且我已将一个表拖到 DLINQ 设计器上(例如客户表)。

Screenshot - DDes2.png

然后构建项目。好的,代码将由 Visual Studio 自动生成并放置在当前 *.dlinq 文件的相关代码隐藏文件(在我的情况下是 .cs)中。

如下图所示

Screenshot - DDes3.png

那不是很好吗?然而,我确实在某个地方读到 DLINQ 设计器对于大型数据库结构来说有点bug。此外,我一般不太喜欢设计器,尽管你可能喜欢它们。但我会向你展示另一种方法。这是一个叫做 _SQLMetal.exe_ 的小命令行工具。所以我们现在来看看它。

技巧 2:SQL Metal

幸运的是,Microsoft 确实知道设计器有多慢,而且它对于大型数据库来说并不那么好,所以他们给了我们一个命令行 EXE,一旦你安装了2006 年 5 月 LINQ CTP,它就会位于(如果你接受了默认安装说明)C:\Program Files\LINQ Preview\Bin\SqlMetal.exe

SqlMetal.exe 是一个不错的小工具,我们可以用它来为数据库生成实体包装类。有许多选项,所以让我们看看这些选项:

Screenshot - SQLMetal2.png

尽管我想最常用的会是“直接从 SQL 元数据生成源代码”选项,这个选项会检查数据库并创建一个源代码包装文件。这些文件被称为实体代码文件,如前所述。

那么这个实体代码文件是如何创建的呢?

嗯,这只是向 SqlMetal.exe 提供一些详细信息的情况,例如:

  • /server
  • /database
  • /namespace
  • /code
  • /language

所以完整的命令行将构成如下:

sqlmetal /server:myserver /database:northwind /namespace:nwind /code:nwind.cs
/language:csharp

我自己的 SQLExpress 2005 家庭安装,使用2006 年 5 月 LINQ CTP 安装的 northwnd.mdf 文件的完整示例如下:

sqlmetal.exe /server:BUDHA01\SQLExpress /database:"c:\program files\LINQ 
  Preview\data\northwnd.mdf" /namespace:nwind /code:nwind.cs /language:csharp

运行此命令行后,结果是在 SqlMetal.exe 工具所在的目录中(如果您接受了默认的2006 年 5 月 LINQ CTP 安装说明,则应为 C:\Program Files\LINQ Preview\Bin\)生成一个名为 nwind.cs 的新 C# 文件。

Screenshot - SQLMetal2.png

然后我可以将这个 nwind.cs 实体代码文件复制到我当前的项目中。

一个更小的示例数据库来帮助我们理解它

在我们继续之前,让我们再尝试一下,使用一个更小、更容易的数据库,并浏览生成的实体代码文件。

首先让我们看一个经典的 SQL Server 实体关系图。我们喜欢它们。我们得到了它们。

Screenshot - DDes1.png

所以,假设我们有一个如上所述的设置,其中有两个表和一个简单的关系,那么将使用以下 SQL 来创建整个数据库。

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE 
    _object_id = OBJECT_ID(N'[dbo].[Publisher]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Publisher](
    [publisherId] [int] IDENTITY(1,1) NOT NULL,
    [publisherName] [nvarchar](50) NULL,
    [publisherEmail] [nvarchar](50) NULL,
    [publisherContact] [nvarchar](50) NULL,
 CONSTRAINT [PK_Publisher] PRIMARY KEY CLUSTERED 
(
    [publisherId] ASC
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE 
    _object_id = OBJECT_ID(N'[dbo].[Book]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Book](
    [bookId] [int] IDENTITY(1,1) NOT NULL,
    [bookName] [nvarchar](50) NULL,
    [bookAuthor] [nvarchar](50) NULL,
    [pulisherId] [int] NULL,
 CONSTRAINT [PK_Books] PRIMARY KEY CLUSTERED 
(
    [bookId] ASC
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE 
    _object_id = OBJECT_ID(N'[dbo].[FK_Book_Publisher]') 
AND parent_object_id = OBJECT_ID(N'[dbo].[Book]'))
ALTER TABLE [dbo].[Book]  WITH CHECK ADD  
    _CONSTRAINT [FK_Book_Publisher] FOREIGN KEY([pulisherId])
REFERENCES [dbo].[Publisher] ([publisherId])
ON UPDATE CASCADE
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[Book] CHECK CONSTRAINT [FK_Book_Publisher]

那是正常的 SQL 语法。现在让我们比较一下使用 SQLMetal.exe 为这个简单的两表数据库生成实体代码文件时得到的结果。

//-------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.42
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//-------------------------------------------------------------------------

namespace simpDB {
  using System;
  using System.Collections.Generic;
  using System.ComponentModel;
  using System.Reflection;
  using System.Query;
  using System.Expressions;
  using System.Data;
  using System.Data.DLinq;
  
  
  public partial class SimpleDatabase : DataContext {
    
    public Table<Book> Book;
    
    public Table<Publisher> Publisher;
    
    public SimpleDatabase(string connection) : 
        base(connection) {
    }
    
    public SimpleDatabase(System.Data.IDbConnection connection) : 
        base(connection) {
    }
    
    public SimpleDatabase(string connection, System.Data.DLinq.MappingSource 
        mappingSource) : 
        base(connection, mappingSource) {
    }
    
    public SimpleDatabase(System.Data.IDbConnection connection, 
        System.Data.DLinq.MappingSource mappingSource) : 
        base(connection, mappingSource) {
    }
  }
  
  [Table(Name="Book")]
  public partial class Book : System.Data.DLinq.INotifyPropertyChanging, 
    System.ComponentModel.INotifyPropertyChanged {
    
    private int _BookId;
    
    private string _BookName;
    
    private string _BookAuthor;
    
    private System.Nullable<int> _PulisherId;
    
    private EntityRef<Publisher> _Publisher;
    
    public Book() {
      this._Publisher = default(EntityRef<Publisher>);
    }
    
    [Column(Name="bookId", Storage="_BookId", 
        DBType="Int NOT NULL IDENTITY", Id=true, AutoGen=true)]
    public int BookId {
      get {
        return this._BookId;
      }
      set {
        if ((this._BookId != value)) {
          this.OnPropertyChanging("BookId");
          this._BookId = value;
          this.OnPropertyChanged("BookId");
        }
      }
    }
    
    [Column(Name="bookName", Storage="_BookName", 
        DBType="NVarChar(50)")]
    public string BookName {
      get {
        return this._BookName;
      }
      set {
        if ((this._BookName != value)) {
          this.OnPropertyChanging("BookName");
          this._BookName = value;
          this.OnPropertyChanged("BookName");
        }
      }
    }
    
    [Column(Name="bookAuthor", Storage="_BookAuthor", 
        DBType="NVarChar(50)")]
    public string BookAuthor {
      get {
        return this._BookAuthor;
      }
      set {
        if ((this._BookAuthor != value)) {
          this.OnPropertyChanging("BookAuthor");
          this._BookAuthor = value;
          this.OnPropertyChanged("BookAuthor");
        }
      }
    }
    
    [Column(Name="pulisherId", Storage="_PulisherId", 
        DBType="Int")]
    public System.Nullable<int> PulisherId {
      get {
        return this._PulisherId;
      }
      set {
        if ((this._PulisherId != value)) {
          this.OnPropertyChanging("PulisherId");
          this._PulisherId = value;
          this.OnPropertyChanged("PulisherId");
        }
      }
    }
    
    [Association(Name="FK_Book_Publisher", 
        Storage="_Publisher", ThisKey="PulisherId", 
        IsParent=true)]
    public Publisher Publisher {
      get {
        return this._Publisher.Entity;
      }
      set {
        Publisher v = this._Publisher.Entity;
        if ((v != value)) {
          this.OnPropertyChanging("Publisher");
          if ((v != null)) {
            this._Publisher.Entity = null;
            v.Book.Remove(this);
          }
          this._Publisher.Entity = value;
          if ((value != null)) {
            value.Book.Add(this);
          }
          this.OnPropertyChanged("Publisher");
        }
      }
    }
    
    public event System.ComponentModel.PropertyChangedEventHandler 
        PropertyChanging;
    
    public event System.ComponentModel.PropertyChangedEventHandler 
        PropertyChanged;
    
    protected virtual void OnPropertyChanging(string PropertyName) {
      if ((this.PropertyChanging != null)) {
        this.PropertyChanging(this, new 
            PropertyChangedEventArgs(PropertyName));
      }
    }
    
    protected virtual void OnPropertyChanged(string PropertyName) {
      if ((this.PropertyChanged != null)) {
        this.PropertyChanged(this, new 
            PropertyChangedEventArgs(PropertyName));
      }
    }
  }
  
  [Table(Name="Publisher")]
  public partial class Publisher : System.Data.DLinq.INotifyPropertyChanging, 
    System.ComponentModel.INotifyPropertyChanged {
    
    private int _PublisherId;
    
    private string _PublisherName;
    
    private string _PublisherEmail;
    
    private string _PublisherContact;
    
    private EntitySet<Book> _Book;
    
    public Publisher() {
      this._Book = new EntitySet<Book>(new 
          Notification<Book>(this.attach_Book), new 
          Notification<Book>(this.detach_Book));
    }
    
    [Column(Name="publisherId", Storage="_PublisherId", 
        DBType="Int NOT NULL IDENTITY", Id=true, AutoGen=true)]
    public int PublisherId {
      get {
        return this._PublisherId;
      }
      set {
        if ((this._PublisherId != value)) {
          this.OnPropertyChanging("PublisherId");
          this._PublisherId = value;
          this.OnPropertyChanged("PublisherId");
        }
      }
    }
    
    [Column(Name="publisherName", 
        Storage="_PublisherName", DBType="NVarChar(50)")]
    public string PublisherName {
      get {
        return this._PublisherName;
      }
      set {
        if ((this._PublisherName != value)) {
          this.OnPropertyChanging("PublisherName");
          this._PublisherName = value;
          this.OnPropertyChanged("PublisherName");
        }
      }
    }
    
    [Column(Name="publisherEmail", 
       Storage="_PublisherEmail", DBType="NVarChar(50)")]
    public string PublisherEmail {
      get {
        return this._PublisherEmail;
      }
      set {
        if ((this._PublisherEmail != value)) {
          this.OnPropertyChanging("PublisherEmail");
          this._PublisherEmail = value;
          this.OnPropertyChanged("PublisherEmail");
        }
      }
    }
    
    [Column(Name="publisherContact", 
         Storage="_PublisherContact", DBType=
         "NVarChar(50)")]
    public string PublisherContact {
      get {
        return this._PublisherContact;
      }
      set {
        if ((this._PublisherContact != value)) {
          this.OnPropertyChanging("PublisherContact");
          this._PublisherContact = value;
          this.OnPropertyChanged("PublisherContact");
        }
      }
    }
    
    [Association(Name="FK_Book_Publisher", 
        Storage="_Book", OtherKey="PulisherId")]
    public EntitySet<Book> Book {
      get {
        return this._Book;
      }
      set {
        this._Book.Assign(value);
      }
    }
    
    public event System.ComponentModel.PropertyChangedEventHandler 
        PropertyChanging;
    
    public event System.ComponentModel.PropertyChangedEventHandler 
        PropertyChanged;
    
    protected virtual void OnPropertyChanging(string PropertyName) {
      if ((this.PropertyChanging != null)) {
        this.PropertyChanging(this, new 
            PropertyChangedEventArgs(PropertyName));
      }
    }
    
    protected virtual void OnPropertyChanged(string PropertyName) {
      if ((this.PropertyChanged != null)) {
        this.PropertyChanged(this, new 
            PropertyChangedEventArgs(PropertyName));
      }
    }
    
    private void attach_Book(Book entity) {
      this.OnPropertyChanging(null);
      entity.Publisher = this;
      this.OnPropertyChanged(null);
    }
    
    private void detach_Book(Book entity) {
      this.OnPropertyChanging(null);
      entity.Publisher = null;
      this.OnPropertyChanged(null);
    }
  }
}

好的,代码很多,但这是您不必编写或使用 DLINQ 设计器设计的大量代码,而且您可以放心地知道其中没有错误(目前,嗯,我们稍后可能会手动修改文件。尽管我建议使用 SqlMetal.exe,因为这是它的工作)。

  public Table<Book> Book;
  ...
  [Table(Name="Book")]
  public partial class Book : System.Data.DLinq.INotifyPropertyChanging, 
        System.ComponentModel.INotifyPropertyChanged {
  
    private int _BookId;
  
    [Column(Name="bookId", Storage="_BookId", 
        DBType="Int NOT NULL IDENTITY", Id=true, AutoGen=true)]
    public int BookId {
      get {
        return this._BookId;
      }
      set {
        if ((this._BookId != value)) {
          this.OnPropertyChanging("BookId");
          this._BookId = value;
          this.OnPropertyChanged("BookId");
        }
      }
    }  
  ...    
  }
...  

所以希望您能在这个 C# 代码中看到一些常见的东西,有一个名为 Book 的新 Table 类型。还有一个名为 _BookId 的新私有字段,它与实际 SQL Server 数据库表中字段的数据类型匹配(它也是一个 int)。表中每个字段(列)都有一个带有 get/set 的属性。好的,有一些您需要学习的新 DLINQ 属性,还有一些额外的事件,例如:

    
public event System.ComponentModel.PropertyChangedEventHandler 
    PropertyChanging;

    public event System.ComponentModel.PropertyChangedEventHandler
        PropertyChanged;

但是 SqlMetal.exe 免费提供了这一切。很不错,不是吗?

注意:如果您使用的是 SQL Server Express 2005 / SQL Server 2005,并且您的数据库包含图表,那么将会生成一个额外的 sysDiagrams 表,因为 SqlMetal.exe 会为整个数据库生成代码。因此,如果您不希望在生成的实体代码文件中包含这些内容,您可以手动删除生成的 sysDiagrams 内容。

第 2 步:创建 DataContext

DataContext 是您从数据库检索对象并提交更改的主要途径。您使用它的方式与使用 ADO.NET Connection 相同。实际上,DataContext 是通过您提供的连接或连接字符串进行初始化的。

DataContext 的目的是将您对对象的请求转换为针对数据库执行的 SQL 查询,然后根据结果组装对象。DataContext 通过实现与标准查询运算符(如 WhereSelect)相同的运算符模式来启用语言集成查询。

例如,您可以使用我们通过 SQLMetal 生成的 northwnd 类 (DataContext)(我已经修改了它,以便数据库名称简单地为 db,而不是 SQLMetal 给出的默认名称)来检索所有客户对象并将其绑定到 Winforms DataGridView,如下所示:

nwind.db db;
...
try 
{
    db = new    nwind.db(@"c:\program files\LINQ 
       Preview\data\northwnd.mdf");
}
catch(Exception ex)
{
    MessageBox.Show("Error " + ex.Message);
}
...            
dataGridView1.DataSource=(from customer in custs select customer).ToBindingList();

这不是很简单吗?对于 ASP.NET 来说,这几乎是一样的,尽管在处理 ASP.NET 时不需要调用 .ToBindingList();。您只需执行以下操作:

GridView1.DataSource = from customer in db.Customers
                       select customer;

第三步:运行查询

现在您已经实例化了一个 northwnd 类(DataContext)(db),运行查询非常容易。我们可以简单地运行任何 LINQ 标准查询运算符。

以下只是其中的一小部分,有关更多标准查询运算符,您可以参阅我的第一篇文章,该文章全面介绍了标准查询运算符。

dataGridView1.DataSource=(from customer in custs select 
                          customer).ToBindingList();
...
dataGridView1.DataSource=( from customer in db.Customers
                          where customer.Country == "USA"
orderby customer.CompanyName
select customer).ToBindingList();
...
//Create a new DataSet containing only 2 columns customer.CustomerID, 
//customer.City
dataGridView1.DataSource=( from customer in db.Customers
                          where customer.Country == "USA"
orderby customer.CompanyName
select new { customer.CustomerID, 
customer.City }).ToBindingList();
...
dataGridView1.DataSource=( from customer in db.Customers
                          where customer.CustomerID.ToLower().StartsWith("a")
                          orderby customer.CompanyName
                          select customer).ToBindingList();
...
dataGridView1.DataSource=( from o in db.Orders
                          where o.OrderDate > Convert.ToDateTime("31/12/1997") &&
!(o.ShippedDate == null) 
orderby o.OrderDate
select o).Take(50).ToBindingList();
...
//query across two different tables (this is thanks to Associations within 
//the Entity class)
dataGridView1.DataSource=(from c in db.Customers
                          join o in db.Orders on c.CustomerID equals 
                          o.CustomerID
                          where o.CustomerID == "ALFKI"
select o).ToBindingList();

所以这只是 DLINQ 可以进行的一些查询类型的一个示例。随附的演示应用程序中的以下屏幕截图显示了其中一个查询绑定到 Winforms DataGridView 控件的示例。

Screenshot - Queries.png

第四步:提交更新/删除/插入

到目前为止,我只向您展示了如何从数据库中获取 (SELECT)。但我们真正想做的,是能够更改值、删除和添加新值。所以让我们继续看看这个。

插入

随附的 DEMO 应用程序提供了两个插入操作,但这应该足以让您了解情况。

插入新客户

这段代码只是向客户表添加一个新客户,然后显示 DataGridView 中所有客户。

try 
{
    Customers cust;

    string id = "SACH1";
    //is there a customer that matches
    if ( (from c in db.Customers where c.CustomerID == id select c).Count() 
        > 0) 
    {
        //there is so get it
        cust = db.Customers.Single(c => c.CustomerID == id);
        // Remove it from the table
        db.Customers.Remove(cust);
        // Ask the DataContext to save all the changes
        db.SubmitChanges();
    }

    // Create and add a new Customer and add it to the Customers table
    cust = new Customers { 
        CustomerID=id,CompanyName="SAS",ContactName="sacha 
        barber",ContactTitle="student" };
    Table<Customers> custs =  db.GetTable<Customers>();
    custs.Add(cust);
    // Ask the DataContext to save all the changes
    db.SubmitChanges(); 
    //show all customers 
    AllCustomers();
}
catch(Exception ex)
{
         MessageBox.Show("Error " + ex.Message);
}

插入新客户和新订单

这段代码只是向 Customers 表中添加一个新客户,并为该客户添加一个新订单,然后显示 DataGridView 中所有的新订单。

try 
{
    Customers cust;

    string id = "SACH2";
    //is there a customer that matches
    if ( (from c in db.Customers where c.CustomerID == id select c).Count() 
        > 0) 
    {
        //there is so get it
        cust = db.Customers.Single(c => c.CustomerID == id);
        // Remove it from the table
        db.Customers.Remove(cust);
        // Ask the DataContext to save all the changes
        db.SubmitChanges();
    }

    // Create and add a new Customer and add it to the Customers table
    cust = new Customers { 
        CustomerID=id,CompanyName="SAS",ContactName="sacha 
        barber",ContactTitle="student" };
    Table<Customers> custs =  db.GetTable<Customers>();
    custs.Add(cust);
    // Create and add a new Order and add it to the new Customer 
    Orders ord = new Orders { 
        CustomerID=id,ShipCity="Brighton",
        ShipCountry="England", ShipName="=====",
        OrderDate = DateTime.Now };
    cust.Orders.Add(ord);
    // Ask the DataContext to save all the changes
    db.SubmitChanges();   
    //show on o.OrderID, o.CustomerID, o.ShipCity,o.ShipCountry, 
    //o.ShipName, o.OrderDate within the DataGridView
    dataGridView1.DataSource=(
                                from o in cust.Orders
                                select new {    o.OrderID, o.CustomerID, 
                                                o.ShipCity,o.ShipCountry, 
                                                o.ShipName, o.OrderDate  }
                              ).ToBindingList();
}
catch(Exception ex)
{
    MessageBox.Show("Error " + ex.Message);
}

更新

随附的 DEMO 应用程序提供了一个更新,但这应该足以让您了解情况。在此示例中,系统会提示用户输入要更新的 Customer ID。然后会要求用户输入一个新的 ContactName,该名称将用于更新 Customer

try 
{
    string id = GetUserValue("Update which customer< please specify a 
        CustomerID");
    if (!string.IsNullOrEmpty(id)) 
    {
        if ( (from c in db.Customers where c.CustomerID == id select 
            c).Count() > 0) 
        {
            //valid Customer for id found
            string contactName = GetUserValue("Please enter the new 
                ContactName for CustomerID [" + id + "]");
            if (!string.IsNullOrEmpty(contactName)) 
            {
                //get the Customer there is so get it
                Customers cust = db.Customers.Single(c => c.CustomerID == 
                    id);
                // Remove it from the table
                cust.ContactName = contactName;
                // Ask the DataContext to save all the changes
                db.SubmitChanges();
            }
        }
        else 
        {
            MessageBox.Show("Couldnt find a customer with that 
                CustomerID");
        }
    }
}
catch(Exception ex)
{
    MessageBox.Show("Error " + ex.Message);
}

删除

随附的演示应用程序提供了一个删除操作,但它是一个应该影响两个表的删除操作,因为它正在删除一个拥有未结订单的 Customer。因此,需要考虑引用完整性问题。

try 
{
    string id = GetUserValue("Enter a CustomerID to delete");
    if (!string.IsNullOrEmpty(id)) 
    {
        if ( (from c in db.Customers where c.CustomerID == id select 
            c).Count() > 0) 
        {
            //valid Customer for id found

            //So, check the Orders table 1st, and Delete the Customer from 
            //there to preserve referential intergrity
            if ( (from o in db.Orders where o.CustomerID == id select 
                o).Count() > 0) 
            {
                db.Orders.Remove(db.Orders.Single(o => o.CustomerID == 
                    id));
            }
            //now delete the Customer
            Customers cust = db.Customers.Single(c => c.CustomerID == id);
            // Remove it from the table
            db.Customers.Remove(cust);
            // Ask the DataContext to save all the changes
            db.SubmitChanges();
            //show all customers
            AllCustomers();
        }
        else 
        {
            MessageBox.Show("Couldnt find a customer with that 
                 CustomerID");
        }
    }
}
catch(Exception ex)
{
    MessageBox.Show("Error " + ex.Message);
}

SQL Server 2005 Profiler

我实际上没有完整版的 SQL 2005,我使用的是 SQL SERVER EXPRESS 2005,因此无法在 DLINQ 生成的 SQL 上运行 SQL profiler。但是,如果您有幸拥有 SQL SERVER 2005 的 SQL Profiler,我建议您运行它,以便您可以看到 DLINQ 自动生成了什么。

然而,由 SQLMetal.exe 创建的实体类也包含一个很好的属性来帮助解决这个问题,它叫做 Log。只需如下设置即可:

//comment this line out if you dont want to see the generated SQL
db.Log = Console.Out;

我们可以在 Visual Studio 输出窗口中看到生成的 SQL。这省去了我们使用 SQL Profiler 的麻烦,尽管 SQL Profiler 是更好的分析工具。但这是权宜之计。

自定义更新/删除/插入逻辑

好吧,你已经看到了做一些标准 SQL 类型操作是多么容易。我认为这比 ADO.NET 容易得多。但 ADO.NET 的优点在于,如果我们不喜欢 UpdateCommandDeleteCommand(比如来自向导或通过使用 SqlCommandBuilder 类),我们可以提供自己的逻辑,无论是使用自定义 SQL 命令,还是更好地将此任务委托给 SQL Server 存储过程,并简单地为存储过程提供相关命令。

这是一个我们可能都了解和喜欢的 SQL 世界。那么 DLINQ 能做到这一点吗,还是我们被迫使用在 DataContext(数据库实体类)上调用 SubmitChanges() 方法时自动生成的 SQL 来执行更新/删除/插入操作呢?

幸运的是,LINQ 团队已经考虑到了这一点,并实际上给了我们创建自己的逻辑的选项。那么我们该怎么做呢?让我们来看看吧?

“当调用 SubmitChanges() 时,DLINQ 会生成并执行 SQL 命令来在数据库中插入、更新和删除行。这些操作可以被应用程序开发人员覆盖,并用自定义代码执行所需的动作。通过这种方式,替代功能,如数据库存储过程,可以由更改处理器自动调用。”

考虑一个用于更新 Northwind 示例数据库中 Products 表库存单位的存储过程。该过程的 SQL 声明如下:

create proc UpdateProductStock
    @id                    int,
    @originalUnits         int,
    @decrement            int
as
.....

“您可以通过在强类型 DataContext 上定义一个方法来使用存储过程,而不是普通的自动生成更新命令。即使 DataContext 类是由 DLINQ 代码生成工具自动生成的,您仍然可以在自己的分部类中指定这些方法。”

public partial class Northwind : DataContext
{
    ...

    [UpdateMethod]
    public void OnProductUpdate(Product original, Product current) {
        // Execute the stored procedure for UnitsInStock update
        if (original.UnitsInStock != current.UnitsInStock) {
            int rowCount = this.ExecuteCommand(
                "exec UpdateProductStock " +
                "@id={0}, @originalUnits={1}, @decrement={2}",
                original.ProductID,
                original.UnitsInStock,
                (original.UnitsInStock - current.UnitsInStock)
            );
            if (rowCount < 1)
                throw new OptimisticConcurrencyException();
        }
        ...
    }
}

UpdateMethod 属性告诉 DataContext 使用此方法代替生成的更新语句。原始参数和当前参数由 DLINQ 用于传入指定类型的对象的原始副本和当前副本。这两个参数可用于乐观并发冲突检测。请注意,如果您正在覆盖默认更新逻辑,则冲突检测是您的责任。

存储过程 UpdateProductStock 使用 DataContextExecuteCommand() 方法调用。它返回受影响的行数,并具有以下签名:

public int ExecuteCommand(string command, params object[] parameters);

“对象数组用于传递执行命令所需的参数。

与更新方法类似,插入和删除方法可以使用 InsertMethodDeleteMethod 属性指定。插入和 delete 方法只接受一个实体类型参数,用于更新。例如,插入和删除 Product 实例的方法可以指定如下:”

[InsertMethod]
public void OnProductInsert(Product prod) { ... }

[DeleteMethod]
public void OnProductDelete(Product prod) { ... }

“方法名称可以是任意的,但属性是必需的,并且签名必须遵循指定的模式。”

DLinq CSharp 开发人员概述文档。版权所有 © Microsoft Corporation 2006

这段引用摘自此处提及的文档,该文档随2006 年 5 月 LINQ CTP 安装一同提供。我无法比这说得更好,所以觉得截取这一部分是可以的。

动态查询

到目前为止,我们只进行了静态查询,也就是说,完整的查询是硬编码到附加的 DEMO 应用程序中的。虽然这向我们展示了如何使用 DLINQ,但这可能不是我们在日常编程工作中必须做的事情。通常,我们会使用用户指定的参数来定制查询。

那么,让我们看看如何在 DLINQ 中实现这一点。

第一种方法非常简单,我们只需引入一些变量来限制查询将获得的结果。

该变量可以放入查询的 WHERE 子句中。让我们看一个简单的例子,它返回所有居住在美国的客户表中的客户。

try 
{
    string country="USA";
    dataGridView1.DataSource=(from emp in db.Employees
                              where emp.Country.Equals(country)
                              select emp).ToBindingList();    
}
catch(Exception ex)
{
    MessageBox.Show("Error " + ex.Message);
}

Screenshot - employees.png

这足够简单,不是吗?

但还有另一种方法,在 Matt Warren 的博客The Wayward WebLog 中描述,那就是使用 IQueryable 接口。据 Matt 说,这个接口在 LINQ 的下一个版本中一直在变化,所以当 LINQ 的新版本正式发布时,可能会与本文所述有所不同。本文基于2006 年 5 月 LINQ CTP,所以我将尝试向您展示我们目前可用的功能。

首先,我们需要定义几个类实例字段。此示例使用 Northwind 数据库的 Employee 表。

/// <summary>
/// This is the query we'll be manipulating>  It's simply a projection 
/// from db>Employees into our EmployeeView type>
/// </summary>
private readonly IQueryable<EmployeeView> baseQuery;
/// <summary>
/// This holds a row to be displayed>
/// </summary>
public class EmployeeView {
    public string LastName;
    public string FirstName;
    public string City;
    public string Country;
    public string Extension;
    public DateTime? HireDate;
}
...
//and we need to populate the baseQuery before we can use it as an IQueryable 
//source for further queries
baseQuery = from e in db.Employees select new EmployeeView { e.LastName, 
    e.FirstName, e.City, e.Country, e.Extension, e.HireDate };

所以那是其中一部分。

接下来,我们需要创建一个方法来执行用户指定的动态查询(实际上是过滤器;将其视为过滤器可能更好)。

代码如下所示。我希望你们能理解。这很简单,大致是这样的:

  1. 显示所有员工
  2. 创建类型为 EmployeeView 的新 ParameterExpression
  3. 根据用户提供的过滤器字符串创建一个新表达式(好的,在这种情况下演示应用程序提供了它,但它可能来自用户输入,它只是一个字符串)
  4. 让数据库根据步骤 3 中的新表达式创建一个新查询
  5. 我们现在应该有一组与过滤器匹配的结果。
private void RunDynamicEmployeesQuery(string filter)
{
   try 
    {
        //show all employees
        dataGridView1.DataSource=( from emp in db.Employees
                       select emp).ToBindingList();

        // Retrieve the underlying expression from the IQueryable.
        var expression = baseQuery.Expression;
        
        // Passing null as the name of the parameter means we'll be able to 
        // match it as a default.
        // This allows such filters as "city = 'London'", rather 
        // than "c.City = 'London'".
        ParameterExpression expr = Expression.Parameter(typeof(EmployeeView), 
            null);

        // If we have a filter string, attempt to add a Where clause to the 
        // expression.
        if (!String.IsNullOrEmpty(filter)) {

            //where filter is a string which could be set to "city = 
            //'London'"
            expression = QueryExpression.Where(expression, 
                QueryExpression.Lambda(filter, expr));
        }
        // Finally, we create a new query based on that expression.
        var query = db.CreateQuery<EmployeeView>(expression);

        string res="";
        foreach(EmployeeView ev in query)
        {
            res+="Name : " +  ev.FirstName + " " + 
                ev.LastName + ", City : " + ev.City + ", 
                Country : " + ev.Country + ", 
                Extension : " + ev.Extension + ", 
                HireDate : " + ev.HireDate.Value.ToShortDateString() + 
                "\r\n";
        }

        //GRRR why wont this work
        //GRRR why wont this work
        //GRRR why wont this work

        //var q = from ev in query select ev;
        //MessageBox.Show("Query " + q.Count().ToString());
        //dataGridView1.DataSource=(q).ToBindingList();

        MessageBox.Show(db.GetQueryText(query).ToString() + 
            "\r\n\r\n" + "User Defined Filter : " + 
            filter + "\r\n\r\n" + "Yielded " + 
            query.Count().ToString() + " employee results\r\n\r\n" 
            + res + "\r\n\r\n" + "NOTE : May 2006 CTP (This  
            one) DOES NOT seem to allow IQueryable results to be bound to 
            DataGridView though\r\n" + "perhaps, .NET 3.5 proper 
            release or March 2007 CTP \"Orcas\" release will fix 
            this. As such the forms DataGridView\r\n" + "is 
            currently set to display all Employees, and not the result of the 
            filter. The results shown in this message box are\r\n" +
            "correct, based on the filter. I just cant get the results 
            into the DataGridView.r\n\r\n" + "So if anyone knows 
            how to do that, please let me know");

    }
    catch(Exception ex)
    {
        MessageBox.Show("Error " + ex.Message);
    }
}

最后,我们需要调用这个方法,传入一个有效的过滤字符串。这很简单。

RunDynamicEmployeesQuery(@"city = 'Redmond'");

一件不起作用的事情

你们当中眼尖的人可能会发现有些代码被注释掉了。被注释掉的代码实际上应该将现在过滤后的查询结果绑定到 DataGridView。不幸的是,我无法让它工作。它应该可以,但它就是不行。我确实在某个地方读到2006 年 5 月 LINQ CTP 提供的绑定功能比最终版本少。所以我猜 LINQ 的完整发布版本(无论何时)应该允许将 IQueryable 用作 DataGridView 的 DataSource。目前它似乎不行。对此感到抱歉。

注意事项

我在这里显然处理的是一个 Winforms 应用程序,所以使用 DataGridView 来显示任何查询的结果。我只遇到了两件事:

  1. 当尝试将 DLINQ 查询的结果绑定到 DataGridView 时,我必须在 DLINQ 查询的末尾使用未记录的 .ToBindingList() 才能将其设置为 DataGridView 的数据源。
  2. 通常,DataGridView 允许通过单击列来对绑定内容进行排序。将 DLINQ 查询绑定到 DataGridView 时尝试此操作,会导致异常“如果 DataGridView 控件绑定到不支持排序的 IBindingList,则无法对其进行排序”。
  3. IQueryable 无法用作 DataGridView 的 DataSource,正如刚才解释的那样。

一旦你知道了第一点,那就很公平了。

第二点,嗯,那很糟糕。所以我找了一会儿,发现了这篇文章:

可排序绑定列表和 DLinq,其中提到在2006 年 5 月 LINQ CTP 版本的 LINQ 中,绑定支持是有限的,并且将来会变得更好。所以我想我们只能等待 Visual Studio Orcas 正式发布了。

就这些

我希望你们中有些人已经阅读了第一部分,并且可以看到本文从第一部分开始的地方。我不想再次回顾所有标准查询运算符,因为那些错过的人可以查看第一部分

我也希望这篇文章已经表明 DLINQ(或者未来将被称为 LINQ over SQL)并不可怕,而且实际上非常容易使用。

那么你觉得呢?

我只想问,如果您喜欢这篇文章,请为它投票,因为这让我知道这篇文章的水平是否合适。

另外,如果您认为下一篇建议的文章应该包含这么多材料还是更少材料。请告诉我,毕竟我想写真正能帮助人们的文章。

结论

我非常喜欢撰写这篇文章,并且对 DLINQ 的易用性感到耳目一新。尽管我仍然非常喜欢存储过程,并且忍不住认为他们(他们/微软/好的/坏的/杰出的)只是在数据访问层添加了另一个代码层。我仍然没有 100% 决定是否使用 DLINQ,它非常易于使用。但是您必须维护这些实体类,因此您的数据库设计必须相当锁定,尽管使用 SQLMetal 可以轻松地再次生成这些实体类。我确实喜欢更新数据库是多么容易,我曾认为 DataAdaptor 很酷,dataContext 比它好太多了(基本上就是很多个 o)。

历史

v1.0 2007/03/23:初始发布

参考文献

  1. Lambda 表达式和表达式树:简介
  2. Lambda 和 Curry 笔记。来自萨塞克斯大学(我的大学)
  3. C# 3.0 语言背后的概念
  4. LINQ 项目
  5. 101 LINQ 示例
  6. 构建查询
  7. 任性博客
  8. 2006 年 5 月 LINQ CTP
  9. ScottGu 的博客
© . All rights reserved.