创建适用于类型化数据集的 TableAdapter 的新存储过程 (C#)

作者 :斯科特·米切尔

下载 PDF

在前面的教程中,我们已在代码中创建 SQL 语句,并将这些语句传递给要执行的数据库。 另一种方法是使用存储过程,其中 SQL 语句在数据库中预定义。 本教程介绍如何让 TableAdapter 向导为我们生成新的存储过程。

介绍

这些教程的数据访问层 (DAL) 使用类型化数据集。 如 “创建数据访问层 ”教程中所述,类型化数据集由强类型数据表和 TableAdapters 组成。 DataTable 表示系统中的逻辑实体,而 TableAdapters 与基础数据库接口来执行数据访问工作。 这包括使用数据填充 DataTable、执行返回标量数据的查询,以及插入、更新和删除数据库中的记录。

TableAdapters 执行的 SQL 命令可以是即席 SQL 语句,例如 SELECT columnList FROM TableName,或存储过程。 我们的体系结构中的 TableAdapters 使用即席 SQL 语句。 但是,许多开发人员和数据库管理员更喜欢存储过程而不是临时 SQL 语句,因为出于安全性、可维护性和可更新性的原因。 另一些人则热衷于选择临时 SQL 语句,因为灵活性更高。 在我自己的工作中,我更喜欢存储过程而非即席 SQL 语句,但选择使用即席 SQL 语句来简化前面的教程。

定义 TableAdapter 或添加新方法时,TableAdapter 向导使创建新存储过程或使用现有存储过程变得非常简单,就像使用即席 SQL 语句一样。 本教程将介绍如何让 TableAdapter 向导自动生成存储过程。 在下一教程中,我们将了解如何配置 TableAdapter 方法以使用现有或手动创建的存储过程。

注释

请参阅 Rob Howard 的博客文章《还没有使用存储过程?》和 Frans Bouma 的博客文章《存储过程是坏的,M Kay?》,以了解关于存储过程和临时 SQL 的利弊的生动辩论。

存储过程基础知识

函数是所有编程语言通用的构造。 函数是调用函数时执行的语句的集合。 函数可以接受输入参数,并且可以选择性地返回值。 存储过程 是数据库构造,与编程语言中的函数共享许多相似之处。 存储过程由调用存储过程时执行的一组 T-SQL 语句组成。 存储过程可以接受零到多个输入参数,并且可以从查询返回标量值、输出参数或最常见的结果集 SELECT

注释

存储过程通常简称为sprocs或SPs。

存储过程是使用 CREATE PROCEDURE T-SQL 语句创建的。 例如,以下 T-SQL 脚本创建一个名为GetProductsByCategoryID的存储过程,该过程接受一个参数@CategoryID,并返回ProductID表中字段ProductNameUnitPriceDiscontinuedProducts,这些字段属于具有匹配CategoryID值的列。

CREATE PROCEDURE GetProductsByCategoryID
(
    @CategoryID int
)
AS
SELECT ProductID, ProductName, UnitPrice, Discontinued
FROM Products
WHERE CategoryID = @CategoryID

创建此存储过程后,可以使用以下语法调用它:

EXEC GetProductsByCategory categoryID

注释

在下一教程中,我们将检查如何通过 Visual Studio IDE 创建存储过程。 但是,在本教程中,我们将让 TableAdapter 向导自动生成存储过程。

除了仅返回数据之外,存储过程还通常用于在单个事务的范围内执行多个数据库命令。 例如,名为 DeleteCategory 的存储过程可能会接收一个 @CategoryID 参数,并执行两条 DELETE 语句:第一条用于删除相关产品,第二条用于删除指定类别。 存储过程中的多个语句 不会 自动封装到事务中。 需要发出其他 T-SQL 命令,以确保存储过程的多个命令被视为原子作。 我们将在后续教程中了解如何在事务的范围内封装存储过程的命令。

在体系结构中使用存储过程时,数据访问层的方法调用特定的存储过程,而不是发出即席 SQL 语句。 这会集中执行(在数据库上)的 SQL 语句的位置,而不是在应用程序体系结构中定义它。 这种集中化可以说可以更轻松地查找、分析和优化查询,并提供更清晰的数据库使用位置和方式。

有关存储过程基础知识的详细信息,请参阅本教程末尾的“进一步阅读”部分中的资源。

步骤 1:创建高级数据访问层场景网页

在开始讨论如何使用存储过程创建 DAL 之前,让我们先花点时间在网站项目中创建 ASP.NET 页面,我们将需要此页面和接下来的几个教程。 首先添加名为 <a0/a0> 的新文件夹。 接下来,将以下 ASP.NET 页添加到该文件夹,确保将每个页面与 Site.master 母版页相关联:

  • Default.aspx
  • NewSprocs.aspx
  • ExistingSprocs.aspx
  • JOINs.aspx
  • AddingColumns.aspx
  • ComputedColumns.aspx
  • EncryptingConfigSections.aspx
  • ManagedFunctionsAndSprocs.aspx

为高级数据访问层方案教程添加 ASP.NET 页

图 1:为高级数据访问层方案教程添加 ASP.NET 页

与其他文件夹中一样, Default.aspxAdvancedDAL 文件夹中将列出其部分中的教程。 回想一下, SectionLevelTutorialListing.ascx 用户控件提供了此功能。 因此,通过将此用户控件从解决方案资源管理器拖动到页面的设计视图中,将它添加到Default.aspx

将 SectionLevelTutorialListing.ascx 用户控件添加到 Default.aspx

图 2:将用户控件SectionLevelTutorialListing.ascx添加到 Default.aspx单击以查看全尺寸图像

最后,将这些页面作为条目添加到Web.sitemap文件中。 具体而言,在“处理批处理数据 <siteMapNode>”后添加以下标记:

<siteMapNode url="~/AdvancedDAL/Default.aspx" 
    title="Advanced DAL Scenarios" 
    description="Explore a number of advanced Data Access Layer scenarios.">
    
    <siteMapNode url="~/AdvancedDAL/NewSprocs.aspx" 
        title="Creating New Stored Procedures for TableAdapters" 
        description="Learn how to have the TableAdapter wizard automatically 
            create and use stored procedures." />
    <siteMapNode url="~/AdvancedDAL/ExistingSprocs.aspx" 
        title="Using Existing Stored Procedures for TableAdapters" 
        description="See how to plug existing stored procedures into a 
            TableAdapter." />
    <siteMapNode url="~/AdvancedDAL/JOINs.aspx" 
        title="Returning Data Using JOINs" 
        description="Learn how to augment your DataTables to work with data 
            returned from multiple tables via a JOIN query." />
    <siteMapNode url="~/AdvancedDAL/AddingColumns.aspx" 
        title="Adding DataColumns to a DataTable" 
        description="Master adding new columns to an existing DataTable." />
    <siteMapNode url="~/AdvancedDAL/ComputedColumns.aspx" 
        title="Working with Computed Columns" 
        description="Explore how to work with computed columns when using 
            Typed DataSets." />
    <siteMapNode url="~/AdvancedDAL/EncryptingConfigSections.aspx" 
        title="Protected Connection Strings in Web.config" 
        description="Protect your connection string information in 
            Web.config using encryption." />
    <siteMapNode url="~/AdvancedDAL/ManagedFunctionsAndSprocs.aspx" 
        title="Creating Managed SQL Functions and Stored Procedures" 
        description="See how to create SQL functions and stored procedures 
            using managed code." />
</siteMapNode>

更新 Web.sitemap后,请花点时间通过浏览器查看教程网站。 左侧菜单现在包括高级 DAL 场景教程的条目。

站点地图现在包括高级 DAL 场景教程的相关条目

图 3:网站地图现在包含有关高级 DAL 场景教程的条目

步骤 2:配置 TableAdapter 以创建新的存储过程

为了演示如何创建使用存储过程而不是即席 SQL 语句的数据访问层,让我们在~/App_Code/DAL文件夹中创建一个名为NorthwindWithSprocs.xsd的新类型化数据集。 由于我们在前面的教程中详细介绍了此过程,因此我们将快速完成此处的步骤。 如果在创建和配置类型化数据集时遇到问题或需要进一步的分步说明,请参阅 “创建数据访问层 ”教程。

右键单击 DAL 文件夹,选择“添加新项”,然后选择数据集模板,如图 4 所示,向项目添加新的数据集。

将新类型化数据集添加到名为 NorthwindWithSprocs.xsd 的项目

图 4:向命名 NorthwindWithSprocs.xsd 项目添加新类型化数据集(单击以查看全尺寸图像

这将创建新的类型化数据集,打开其设计器,创建新的 TableAdapter,并启动 TableAdapter 配置向导。 TableAdapter 配置向导的第一步要求我们选择要使用的数据库。 Northwind 数据库的连接字符串应列在下拉列表中。 选择此项并单击“下一步”。

在下一个屏幕中,我们可以选择 TableAdapter 应如何访问数据库。 在前面的教程中,我们选择了第一个选项“使用 SQL 语句”。 对于本教程,请选择第二个选项,创建新的存储过程,然后单击“下一步”。

指示 TableAdapter 创建新的存储过程

图 5:指示 TableAdapter 创建新的存储过程(单击以查看全尺寸图像

与使用即席 SQL 语句一样,在以下步骤中,我们要求提供 SELECT TableAdapter 主查询的语句。 但是,TableAdapter 向导不会使用 SELECT 此处输入的语句直接执行临时查询,而是创建包含此 SELECT 查询的存储过程。

请对该 SELECT TableAdapter 使用以下查询:

SELECT ProductID, ProductName, SupplierID, CategoryID, 
       QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
       ReorderLevel, Discontinued
FROM Products

输入 SELECT 查询

图 6:输入 SELECT 查询(单击以查看全尺寸图像

注释

ProductsTableAdapter类型化数据集中,以上查询与Northwind的主查询略有不同。 请记住,ProductsTableAdapterNorthwind 类型化数据集中包含两个相关子查询,用于提取每个产品的类别名称和供应商的公司名称。 在即将到来的 更新 TableAdapter 以使用 JOINs 教程中,我们将探讨如何将这些相关数据添加到 TableAdapter 中。

花点时间单击“高级选项”按钮。 在这里,我们可以指定向导是否还应为 TableAdapter 生成插入、更新和删除语句、是否使用乐观并发,以及是否应在插入和更新后刷新数据表。 默认情况下会选中“生成插入”、“更新”和“删除”语句选项。 保留选中。 对于本教程,请取消选中“使用乐观并发”选项。

当 TableAdapter 向导自动创建存储过程时,似乎忽略“刷新数据表”选项。 无论选中此复选框,生成的插入和更新存储过程都将检索刚刚插入的或刚刚更新的记录,如我们将在步骤 3 中看到的那样。

保持“生成插入、更新和删除语句”选项处于选中状态

图 7:保持“生成插入、更新和删除语句的选项”勾选状态

注释

如果选中了“使用乐观并发”选项,向导将在WHERE条件子句中添加其他条件,以防止在其他字段中发生更改时更新数据。 有关使用 TableAdapter 内置乐观并发控制功能的详细信息,请参阅 实现乐观并发 教程。

输入 SELECT 查询并确认选中了“生成插入、更新和删除语句”选项后,单击“下一步”。 下图 8 中显示的下一个屏幕提示向导将创建的存储过程的名称,以便选择、插入、更新和删除数据。 将这些存储过程名称更改为Products_SelectProducts_InsertProducts_UpdateProducts_Delete

重命名存储过程

图 8:重命名存储过程(单击以查看全尺寸图像

若要查看 TableAdapter 向导将用于创建四个存储过程的 T-SQL,请单击“预览 SQL 脚本”按钮。 在“预览 SQL 脚本”对话框中,可以将脚本保存到文件或将其复制到剪贴板。

预览用于生成存储过程的 SQL 脚本

图 9:预览用于生成存储过程的 SQL 脚本

命名存储过程后,单击“下一步”以命名 TableAdapter 的相应方法。 就像使用即席 SQL 语句一样,我们可以创建填充现有 DataTable 或返回新数据表的方法。 还可以指定 TableAdapter 是否应包含用于插入、更新和删除记录的 DB-Direct 模式。 选中所有三个复选框,但将“返回数据表”方法重命名为 GetProducts (如图 10 所示)。

命名方法加入和获取产品

图 10:方法命名 FillGetProducts单击以查看全尺寸图像

单击“下一步”以查看向导将执行的步骤摘要。 单击“完成”按钮完成向导。 向导完成后,您将返回到数据集设计器,现在应包含 ProductsDataTable

数据集设计器显示新添加的产品数据表

图 11:数据集设计器显示新添加 ProductsDataTable 的图像(单击以查看全尺寸图像

步骤 3:检查新创建的存储过程

步骤 2 中使用的 TableAdapter 向导会自动创建用于选择、插入、更新和删除数据的存储过程。 可以通过 Visual Studio 查看或修改这些存储过程,方法是转到服务器资源管理器并向下钻取到数据库的“存储过程”文件夹。 如图 12 所示,Northwind 数据库包含四个新的存储过程:Products_DeleteProducts_InsertProducts_SelectProducts_Update

可以在数据库存储过程文件夹中找到在步骤 2 中创建的四个存储过程

图 12:可以在数据库存储过程文件夹中找到步骤 2 中创建的四个存储过程

注释

如果未看到服务器资源管理器,请转到“视图”菜单,然后选择“服务器资源管理器”选项。 如果未看到从步骤 2 中添加的产品相关存储过程,请尝试右键单击“存储过程”文件夹并选择“刷新”。

若要查看或修改存储过程,请在服务器资源管理器中双击其名称,或者右键单击存储过程并选择“打开”。 图 13 显示了打开后的Products_Delete存储过程。

可以从 Visual Studio 中打开和修改存储过程

图 13:可以从 Visual Studio 中打开和修改存储过程(单击以查看全尺寸图像

Step 2: Products_Delete存储过程和Products_Select存储过程的内容非常简单。 存储过程Products_InsertProducts_Update需要更加仔细地检查,因为它们在语句SELECTINSERT之后均执行UPDATE语句。 例如,以下 SQL 构成 Products_Insert 存储过程:

ALTER PROCEDURE dbo.Products_Insert
(
    @ProductName nvarchar(40),
    @SupplierID int,
    @CategoryID int,
    @QuantityPerUnit nvarchar(20),
    @UnitPrice money,
    @UnitsInStock smallint,
    @UnitsOnOrder smallint,
    @ReorderLevel smallint,
    @Discontinued bit
)
AS
    SET NOCOUNT OFF;
INSERT INTO [Products] ([ProductName], [SupplierID], [CategoryID], [QuantityPerUnit], 
    [UnitPrice], [UnitsInStock], [UnitsOnOrder], [ReorderLevel], [Discontinued]) 
VALUES (@ProductName, @SupplierID, @CategoryID, @QuantityPerUnit, @UnitPrice, 
    @UnitsInStock, @UnitsOnOrder, @ReorderLevel, @Discontinued);
    
SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, 
    UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued 
FROM Products 
WHERE (ProductID = SCOPE_IDENTITY())

存储过程接受作为输入参数的 Products 列,这些列是由 TableAdapter 向导中指定的 SELECT 查询返回的,这些值被用于 INSERT 语句中。 在 INSERT 语句之后,使用 SELECT 查询来返回新添加记录的 Products 列值(包括 ProductID)。 使用 Batch 更新模式添加新记录时,此刷新功能非常有用,因为它使用数据库分配的自动递增值自动更新新添加 ProductRow 的实例 ProductID 属性。

以下代码演示了此功能。 它包含为ProductsTableAdapter类型化数据集创建的ProductsDataTableNorthwindWithSprocs。 通过创建 ProductsRow 实例、提供其值并调用 TableAdapter 方法 Update (传入 ProductsDataTable)来向数据库添加新产品。 在内部,TableAdapter 的 Update 方法枚举传入的 DataTable 中的实例(在此示例中只有一个——即我们刚刚添加的实例),然后执行相应的插入、更新或删除命令。 在这种情况下, Products_Insert 将执行存储过程,该存储过程将新记录添加到 Products 表中,并返回新添加的记录的详细信息。 然后更新 ProductsRow 实例 ProductID 的值。 方法Update完成后,可以通过 s ProductID 属性访问新添加的记录值ProductsRowProductID

// Create the ProductsTableAdapter and ProductsDataTable
NorthwindWithSprocsTableAdapters.ProductsTableAdapter productsAPI = 
    new NorthwindWithSprocsTableAdapters.ProductsTableAdapter();
NorthwindWithSprocs.ProductsDataTable products = 
    new NorthwindWithSprocs.ProductsDataTable();
// Create a new ProductsRow instance and set its properties
NorthwindWithSprocs.ProductsRow product = products.NewProductsRow();
product.ProductName = "New Product";
product.CategoryID = 1;  // Beverages
product.Discontinued = false;
// Add the ProductsRow instance to the DataTable
products.AddProductsRow(product);
// Update the DataTable using the Batch Update pattern
productsAPI.Update(products);
// At this point, we can determine the value of the newly-added record's ProductID
int newlyAddedProductIDValue = product.ProductID;

存储过程Products_Update在其SELECT语句后同样包括一个UPDATE语句。

ALTER PROCEDURE dbo.Products_Update
(
    @ProductName nvarchar(40),
    @SupplierID int,
    @CategoryID int,
    @QuantityPerUnit nvarchar(20),
    @UnitPrice money,
    @UnitsInStock smallint,
    @UnitsOnOrder smallint,
    @ReorderLevel smallint,
    @Discontinued bit,
    @Original_ProductID int,
    @ProductID int
)
AS
    SET NOCOUNT OFF;
UPDATE [Products] 
SET [ProductName] = @ProductName, [SupplierID] = @SupplierID, 
    [CategoryID] = @CategoryID, [QuantityPerUnit] = @QuantityPerUnit, 
    [UnitPrice] = @UnitPrice, [UnitsInStock] = @UnitsInStock, 
    [UnitsOnOrder] = @UnitsOnOrder, [ReorderLevel] = @ReorderLevel, 
    [Discontinued] = @Discontinued 
WHERE (([ProductID] = @Original_ProductID));
    
SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, 
    UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued 
FROM Products 
WHERE (ProductID = @ProductID)

请注意,此存储过程包含两个输入参数:ProductID@Original_ProductID@ProductID。 此功能允许在主键可能更改的情况下进行操作。 例如,在员工数据库中,每个员工记录都可以使用员工的社会保险号码作为其主键。 若要更改现有员工的社会保险号码,必须提供新的社会保障号码和原始号码。 Products对于表,不需要此类功能,因为ProductID列是列IDENTITY,不应更改。 事实上, UPDATE 存储过程中的 Products_Update 语句在其列列表中不包括 ProductID 该列。 因此,虽然在@Original_ProductID语句的UPDATE子句中使用WHERE,但对于Products表来说,这是多余的,可以用@ProductID参数来代替。 修改存储过程的参数时,还必须更新使用该存储过程的 TableAdapter 方法(s)。

步骤 4:修改存储过程的参数并更新 TableAdapter

@Original_ProductID由于参数是多余的,因此让我们将其完全从Products_Update存储过程中删除。 打开Products_Update存储过程,删除@Original_ProductID参数,并在WHERE语句的UPDATE子句中,将使用的参数名称从@Original_ProductID更改为@ProductID。 进行这些更改后,存储过程中的 T-SQL 应如下所示:

ALTER PROCEDURE dbo.Products_Update
(
    @ProductName nvarchar(40),
    @SupplierID int,
    @CategoryID int,
    @QuantityPerUnit nvarchar(20),
    @UnitPrice money,
    @UnitsInStock smallint,
    @UnitsOnOrder smallint,
    @ReorderLevel smallint,
    @Discontinued bit,
    @ProductID int
)
AS
    SET NOCOUNT OFF;
UPDATE [Products] SET [ProductName] = @ProductName, [SupplierID] = @SupplierID, 
    [CategoryID] = @CategoryID, [QuantityPerUnit] = @QuantityPerUnit, 
    [UnitPrice] = @UnitPrice, [UnitsInStock] = @UnitsInStock, 
    [UnitsOnOrder] = @UnitsOnOrder, [ReorderLevel] = @ReorderLevel, 
    [Discontinued] = @Discontinued 
WHERE (([ProductID] = @ProductID));
    
SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, 
    UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued 
FROM Products 
WHERE (ProductID = @ProductID)

若要将这些更改保存到数据库,请单击工具栏中的“保存”图标或按 Ctrl+S。 此时, Products_Update 存储过程不需要 @Original_ProductID 输入参数,但 TableAdapter 配置为传递此类参数。 在数据集设计器中选择 TableAdapter 后,转到“属性”窗口,点击 Products_Update 集合中的 UpdateCommand 省略号,即可看到 TableAdapter 将发送到 Parameters 存储过程的参数。 此时会显示图 14 中显示的“参数集合编辑器”对话框。

参数集合编辑器列出了用于传递给Products_Update存储过程的参数

图 14:参数集合编辑器列出了传递给存储过程的参数Products_Update

您只需从成员列表中选择@Original_ProductID 参数,然后点击“删除”按钮,就可以将其从此处删除。

或者,可以通过右键单击设计器中的 TableAdapter 并选择“配置”来刷新用于所有方法的参数。 这将启动 TableAdapter 配置向导,其中列出了用于选择、插入、更新和删除的存储过程以及存储过程预期接收的参数。 如果单击“更新”下拉列表,可以看到 Products_Update 存储过程预期的输入参数,这些参数现在不再包含 @Original_ProductID (请参阅图 15)。 只需单击“完成”即可自动更新 TableAdapter 使用的参数集合。

或者,可以使用 TableAdapter 的配置向导刷新其方法参数集合

图 15:也可以使用 TableAdapter 的配置向导刷新其方法参数集合(单击以查看全尺寸图像

步骤 5:添加其他 TableAdapter 方法

如步骤 2 所示,创建新的 TableAdapter 时,可以轻松自动生成相应的存储过程。 将其他方法添加到 TableAdapter 时也是如此。 为了说明这一点,让我们在步骤 2 中创建的 GetProductByProductID(productID) 中添加一个 ProductsTableAdapter 方法。 此方法将接收 ProductID 作为输入,并返回有关指定产品的详细信息。

首先右键单击 TableAdapter,然后从上下文菜单中选择“添加查询”。

向 TableAdapter 添加新查询

图 16:向 TableAdapter 添加新查询

这将启动 TableAdapter 查询配置向导,该向导会首先提示 TableAdapter 如何访问数据库。 若要创建新的存储过程,请选择“创建新存储过程”选项,然后单击“下一步”。

选择“创建新存储过程选项”

图 17:选择“创建新存储过程选项”(单击以查看全尺寸图像

下一个屏幕要求我们确定要执行的查询类型,无论是返回一组行还是单个标量值,还是执行一个 UPDATEINSERTDELETE 语句。 GetProductByProductID(productID)由于该方法将返回行,请保留 SELECT 以返回所选行选项并点击“下一步”。

选择返回行选项的 SELECT

图 18:选择返回行选项的 SELECT(单击以查看全尺寸图像

下一个屏幕显示 TableAdapter 的主查询,该查询只列出存储过程的名称(dbo.Products_Select)。 将存储过程名称替换为以下 SELECT 语句,该语句返回指定产品的所有产品字段:

SELECT ProductID, ProductName, SupplierID, CategoryID, 
       QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
       ReorderLevel, Discontinued
FROM Products
WHERE ProductID = @ProductID

将存储过程名称替换为 SELECT 查询

图 19:将存储过程名称替换为 SELECT 查询(单击以查看全尺寸图像

后续屏幕要求你命名将要创建的存储过程。 输入名称 Products_SelectByProductID ,然后单击“下一步”。

将新存储过程命名为Products_SelectByProductID

图 20:命名新存储过程 Products_SelectByProductID单击以查看全尺寸图像

向导的最后一步允许我们更改生成的方法名称,以及指示是使用“填充数据表”模式、返回 DataTable 模式还是同时使用两者。 对于此方法,请选中这两个选项,但将方法重命名为 FillByProductIDGetProductByProductID。 单击“下一步”以查看向导将执行的步骤摘要,然后单击“完成”以完成向导。

将 TableAdapter 方法重命名为 FillByProductID 和 GetProductByProductID

图 21:将 TableAdapter 方法重命名为 FillByProductIDGetProductByProductID单击以查看全尺寸图像

完成向导后,TableAdapter 具有可用的新方法, GetProductByProductID(productID) 调用后,将执行 Products_SelectByProductID 刚刚创建的存储过程。 花点时间查看服务器资源管理器中的此新存储过程,方法是钻取存储过程文件夹并打开 Products_SelectByProductID (如果未看到存储过程文件夹,请右键单击“存储过程”文件夹,然后选择“刷新”。

请注意,SelectByProductID 存储过程以 @ProductID 作为输入参数,并执行我们在向导中输入的 SELECT 语句。

ALTER PROCEDURE dbo.Products_SelectByProductID
(
    @ProductID int
)
AS
    SET NOCOUNT ON;
SELECT ProductID, ProductName, SupplierID, CategoryID, 
       QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
       ReorderLevel, Discontinued
FROM Products
WHERE ProductID = @ProductID

步骤 6:创建业务逻辑层类

在整个教程系列中,我们一直努力维护分层体系结构,其中表示层对其业务逻辑层(BLL)进行了所有调用。 为了遵守此设计决策,我们首先需要为新的类型化数据集创建 BLL 类,然后才能从表示层访问产品数据。

ProductsBLLWithSprocs.cs文件夹中创建一个名为~/App_Code/BLL的新类文件,并在其中添加以下代码:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindWithSprocsTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsBLLWithSprocs
{
    private ProductsTableAdapter _productsAdapter = null;
    protected ProductsTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindWithSprocs.ProductsDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, false)]
    public NorthwindWithSprocs.ProductsDataTable GetProductByProductID(int productID)
    {
        return Adapter.GetProductByProductID(productID);
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Insert, true)]
    public bool AddProduct
        (string productName, int? supplierID, int? categoryID, 
         string quantityPerUnit, decimal? unitPrice, short? unitsInStock, 
         short? unitsOnOrder, short? reorderLevel, bool discontinued)
    {
        // Create a new ProductRow instance
        NorthwindWithSprocs.ProductsDataTable products = 
            new NorthwindWithSprocs.ProductsDataTable();
        NorthwindWithSprocs.ProductsRow product = products.NewProductsRow();
        product.ProductName = productName;
        if (supplierID == null) 
            product.SetSupplierIDNull(); 
        else 
            product.SupplierID = supplierID.Value;
        if (categoryID == null) 
            product.SetCategoryIDNull(); 
        else 
            product.CategoryID = categoryID.Value;
        if (quantityPerUnit == null) 
            product.SetQuantityPerUnitNull(); 
        else 
            product.QuantityPerUnit = quantityPerUnit;
        if (unitPrice == null) 
            product.SetUnitPriceNull(); 
        else 
            product.UnitPrice = unitPrice.Value;
        if (unitsInStock == null) 
            product.SetUnitsInStockNull(); 
        else 
            product.UnitsInStock = unitsInStock.Value;
        if (unitsOnOrder == null) 
            product.SetUnitsOnOrderNull(); 
        else 
            product.UnitsOnOrder = unitsOnOrder.Value;
        if (reorderLevel == null)
            product.SetReorderLevelNull(); 
        else 
            product.ReorderLevel = reorderLevel.Value;
        product.Discontinued = discontinued;
        // Add the new product
        products.AddProductsRow(product);
        int rowsAffected = Adapter.Update(products);
        // Return true if precisely one row was inserted, otherwise false
        return rowsAffected == 1;
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Update, true)]
    public bool UpdateProduct
        (string productName, int? supplierID, int? categoryID, string quantityPerUnit,
        decimal? unitPrice, short? unitsInStock, short? unitsOnOrder, 
        short? reorderLevel, bool discontinued, int productID)
    {
        NorthwindWithSprocs.ProductsDataTable products = 
            Adapter.GetProductByProductID(productID);
        if (products.Count == 0)
            // no matching record found, return false
            return false;
        NorthwindWithSprocs.ProductsRow product = products[0];
        product.ProductName = productName;
        if (supplierID == null) 
            product.SetSupplierIDNull(); 
        else 
            product.SupplierID = supplierID.Value;
        if (categoryID == null) 
            product.SetCategoryIDNull(); 
        else 
            product.CategoryID = categoryID.Value;
        if (quantityPerUnit == null) 
            product.SetQuantityPerUnitNull(); 
        else 
            product.QuantityPerUnit = quantityPerUnit;
        if (unitPrice == null) 
            product.SetUnitPriceNull(); 
        else 
            product.UnitPrice = unitPrice.Value;
        if (unitsInStock == null) 
            product.SetUnitsInStockNull(); 
        else 
            product.UnitsInStock = unitsInStock.Value;
        if (unitsOnOrder == null) 
            product.SetUnitsOnOrderNull(); 
        else 
            product.UnitsOnOrder = unitsOnOrder.Value;
        if (reorderLevel == null) 
            product.SetReorderLevelNull(); 
        else 
            product.ReorderLevel = reorderLevel.Value;
        product.Discontinued = discontinued;
        // Update the product record
        int rowsAffected = Adapter.Update(product);
        // Return true if precisely one row was updated, otherwise false
        return rowsAffected == 1;
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Delete, true)]
    public bool DeleteProduct(int productID)
    {
        int rowsAffected = Adapter.Delete(productID);
        // Return true if precisely one row was deleted, otherwise false
        return rowsAffected == 1;
    }
}

此类模仿前面教程中的ProductsBLL类语义,但使用ProductsTableAdapter数据集中的ProductsDataTableNorthwindWithSprocs对象。 例如,using NorthwindTableAdapters类没有像ProductsBLL那样在类文件的开头用ProductsBLLWithSprocs语句,而是使用using NorthwindWithSprocsTableAdapters。 同样,此类中使用的 ProductsDataTableProductsRow 对象以 NorthwindWithSprocs 命名空间为前缀。 该 ProductsBLLWithSprocs 类提供两种数据访问方法, GetProducts 以及 GetProductByProductID用于添加、更新和删除单个产品实例的方法。

步骤 7:使用NorthwindWithSprocs展示层中的数据集

此时,我们创建了一个 DAL,它使用存储过程来访问和修改基础数据库数据。 我们还构建了一个基本的 BLL,其中包含检索所有产品或特定产品的方法以及添加、更新和删除产品的方法。 为了总结本教程,我们创建一个使用 BLL 的 ProductsBLLWithSprocs 类来显示、更新和删除记录的 ASP.NET 页面。

打开 NewSprocs.aspx 文件夹中的 AdvancedDAL 页面,将 GridView 从工具箱拖到设计器上,然后将其命名为 Products。 从 GridView 智能标签中选择将其绑定到名为 ProductsDataSource 的新 ObjectDataSource。 将 ObjectDataSource 配置为使用该 ProductsBLLWithSprocs 类,如图 22 所示。

将 ObjectDataSource 配置为使用 ProductsBLLWithSprocs 类

图 22:将 ObjectDataSource 配置为使用 ProductsBLLWithSprocs 类(单击以查看全尺寸图像

SELECT 选项卡中的下拉列表有两个选项, GetProducts 还有 GetProductByProductID两个选项。 由于我们想要在 GridView 中显示所有产品,因此请选择该方法 GetProducts 。 UPDATE、INSERT 和 DELETE 选项卡中的下拉列表中,每个选项卡只有一种方法。 确保其中每个下拉列表都选择了其适当的方法,然后单击“完成”。

ObjectDataSource 向导完成后,Visual Studio 会将 BoundFields 和 CheckBoxField 添加到 GridView 中的产品数据字段。 通过选中智能标记中存在的“启用编辑”和“启用删除”选项,打开 GridView 的内置编辑和删除功能。

页面包含启用了编辑和删除支持的 GridView

图 23:页面包含启用了编辑和删除支持的 GridView(单击以查看全尺寸图像

正如我们在前面的教程中所述,在 ObjectDataSource 向导完成时,Visual Studio 会将 OldValuesParameterFormatString 属性设置为original_{0}。 这需要还原为其默认值 {0} ,以便数据修改功能在 BLL 中按预期参数正常工作。 因此,请务必将属性OldValuesParameterFormatString设置为{0}或从声明性语法中删除该属性。

完成“配置数据源”向导后,打开 GridView 中的编辑和删除支持,并将 ObjectDataSource s OldValuesParameterFormatString 属性返回到其默认值,页面的声明性标记应如下所示:

<asp:GridView ID="Products" runat="server" AutoGenerateColumns="False" 
    DataKeyNames="ProductID" DataSourceID="ProductsDataSource">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:BoundField DataField="ProductID" HeaderText="ProductID" 
            InsertVisible="False" ReadOnly="True" 
            SortExpression="ProductID" />
        <asp:BoundField DataField="ProductName" HeaderText="ProductName" 
            SortExpression="ProductName" />
        <asp:BoundField DataField="SupplierID" HeaderText="SupplierID" 
            SortExpression="SupplierID" />
        <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" 
            SortExpression="CategoryID" />
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="QuantityPerUnit" 
            SortExpression="QuantityPerUnit" />
        <asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice" 
            SortExpression="UnitPrice" />
        <asp:BoundField DataField="UnitsInStock" HeaderText="UnitsInStock" 
            SortExpression="UnitsInStock" />
        <asp:BoundField DataField="UnitsOnOrder" HeaderText="UnitsOnOrder" 
            SortExpression="UnitsOnOrder" />
        <asp:BoundField DataField="ReorderLevel" HeaderText="ReorderLevel" 
            SortExpression="ReorderLevel" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" 
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsDataSource" runat="server" 
    DeleteMethod="DeleteProduct" InsertMethod="AddProduct" 
    SelectMethod="GetProducts" TypeName="ProductsBLLWithSprocs" 
    UpdateMethod="UpdateProduct">
    <DeleteParameters>
        <asp:Parameter Name="productID" Type="Int32" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="supplierID" Type="Int32" />
        <asp:Parameter Name="categoryID" Type="Int32" />
        <asp:Parameter Name="quantityPerUnit" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="unitsInStock" Type="Int16" />
        <asp:Parameter Name="unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="reorderLevel" Type="Int16" />
        <asp:Parameter Name="discontinued" Type="Boolean" />
        <asp:Parameter Name="productID" Type="Int32" />
    </UpdateParameters>
    <InsertParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="supplierID" Type="Int32" />
        <asp:Parameter Name="categoryID" Type="Int32" />
        <asp:Parameter Name="quantityPerUnit" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="unitsInStock" Type="Int16" />
        <asp:Parameter Name="unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="reorderLevel" Type="Int16" />
        <asp:Parameter Name="discontinued" Type="Boolean" />
    </InsertParameters>
</asp:ObjectDataSource>

此时,我们可以通过自定义编辑界面来整理 GridView, 以便包括验证功能,并让 CategoryID 列和 SupplierID 列呈现为下拉列表等。 我们还可以将客户端确认添加到“删除”按钮,并鼓励你花点时间实现这些增强功能。 但是,由于这些主题已在前面的教程中介绍,因此我们不会在此处再次介绍这些主题。

无论是否增强 GridView,都可以在浏览器中测试页面的核心功能。 如图 24 所示,页面列出了 GridView 中提供每行编辑和删除功能的产品。

可以从 GridView 查看、编辑和删除产品

图 24:可从 GridView 查看、编辑和删除产品(单击以查看全尺寸图像

概要

类型化数据集中的 TableAdapters 可以使用即席 SQL 语句或通过存储过程访问数据库中的数据。 使用存储过程时,可以使用现有的存储过程,也可以指示 TableAdapter 向导基于 SELECT 查询创建新的存储过程。 在本教程中,我们探讨了如何自动创建存储过程。

虽然自动生成存储过程有助于节省时间,但在某些情况下,向导创建的存储过程与我们自己创建的存储过程不一致。 一个例子是Products_Update存储过程,它期望输入参数@Original_ProductID@ProductID,尽管@Original_ProductID参数是多余的。

在许多情况下,存储过程可能已经创建,或者我们可能需要手动生成存储过程,以便更好地控制存储过程的命令。 无论哪种情况,我们都希望指示 TableAdapter 对其方法使用现有的存储过程。 我们将在下一教程中了解如何完成此作。

快乐编程!

深入阅读

有关本教程中讨论的主题的详细信息,请参阅以下资源:

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams Teach Yourself ASP.NET 2.0 in 24 Hours》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。

特别致谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是希尔顿·盖塞诺。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请给我写信。mitchell@4GuysFromRolla.com