作者 :斯科特·米切尔
使用数据库时,通常请求跨多个表分布的数据。 若要从两个不同的表中检索数据,可以使用相关子查询或 JOIN 操作。 在本教程中,我们将比较相关的子查询和 JOIN 语法,然后再了解如何在其主查询中创建包含 JOIN 的 TableAdapter。
介绍
使用关系数据库时,我们感兴趣的数据通常分布在多个表中。 例如,在显示产品信息时,我们可能需要列出每个产品的相应类别和供应商名称。 表 Products
具有 CategoryID
和 SupplierID
值,但实际类别和供应商名称分别位于 Categories
表和 Suppliers
表中。
若要从另一个相关表检索信息,可以使用 相关子查询 或 JOIN
s。 嵌套SELECT
查询是一个关联的子查询,它引用了外部查询中的列。 例如,在 “创建数据访问层 ”教程中,我们在主查询中使用了两个相关子 ProductsTableAdapter
查询来返回每个产品的类别和供应商名称。 一 JOIN
个 SQL 构造,用于合并两个不同表中的相关行。 我们在通过 SqlDataSource 控件查询数据教程中使用了JOIN
来展示每个产品的类别信息。
由于 TableAdapter 向导存在无法自动生成相应的INSERT
、UPDATE
和DELETE
语句的局限性,我们放弃了使用 TableAdapter 的JOIN
。 更具体地说,如果 TableAdapter 的主查询包含任何 JOIN
s,则 TableAdapter 无法为其InsertCommand
UpdateCommand
和DeleteCommand
属性自动创建即席 SQL 语句或存储过程。
在本教程中,我们将简要地比较和对比相关的子查询与 JOIN
,然后探索如何创建一个包含 JOIN
的 TableAdapter 在其主查询中。
比较和对比相关子查询和JOIN
s
回忆一下,我们在 DataSet 的第一个教程中创建的ProductsTableAdapter
使用了相关子查询,来返回每个产品对应的类别和供应商名称。 主要 ProductsTableAdapter
查询如下所示。
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder,
ReorderLevel, Discontinued,
(SELECT CategoryName FROM Categories WHERE Categories.CategoryID =
Products.CategoryID) as CategoryName,
(SELECT CompanyName FROM Suppliers WHERE Suppliers.SupplierID =
Products.SupplierID) as SupplierName
FROM Products
两个相关的子查询 - (SELECT CategoryName FROM Categories WHERE Categories.CategoryID = Products.CategoryID)
和 (SELECT CompanyName FROM Suppliers WHERE Suppliers.SupplierID = Products.SupplierID)
- 是 SELECT
作为外部 SELECT
语句列列表中的附加列返回每个产品的单个值的查询。
或者,可以使用 a JOIN
来返回每个产品的供应商和类别名称。 以下查询返回与上述查询相同的输出,但使用 JOIN
代替子查询:
SELECT ProductID, ProductName, Products.SupplierID, Products.CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder,
ReorderLevel, Discontinued,
Categories.CategoryName,
Suppliers.CompanyName as SupplierName
FROM Products
LEFT JOIN Categories ON
Categories.CategoryID = Products.CategoryID
LEFT JOIN Suppliers ON
Suppliers.SupplierID = Products.SupplierID
根据某些条件,JOIN
将一个表的记录与另一个表的记录合并。 例如,在上述查询中, LEFT JOIN Categories ON Categories.CategoryID = Products.CategoryID
指示 SQL Server 将每个产品记录与其 CategoryID
值与产品值匹配的 CategoryID
类别记录合并。 合并的结果使我们能够处理每个产品的相应类别字段(例如 CategoryName
)。
注释
JOIN
从关系数据库查询数据时,通常使用 s。 如果你不熟悉 JOIN
语法或者需要去复习一下其用法,我建议你在 W3 Schools 上学习一个 SQL 联接教程。 同样值得阅读的是 JOIN
基础知识和子查询基础知识部分,这些都是SQL 联机丛书的一部分。
由于 JOIN
和相关子查询都可用于从其他表中检索相关数据,因此许多开发人员抓破头皮,困惑于选用哪种方式。 我交谈过的所有 SQL 专家都有类似的看法,都认为这在性能上并不重要,因为 SQL Server 将生成大致相同的执行计划。 然后,他们的建议是使用你和你的团队最熟悉的技术。 值得指出的是,在提出这一建议后,这些专家立即表达他们对JOIN
的偏好,而不是对相关子查询的偏好。
使用类型化数据集生成数据访问层时,工具在使用子查询时效果更好。 具体而言,如果主查询包含任何JOIN
,TableAdapter向导将不会自动生成相应的INSERT
、UPDATE
和DELETE
语句,但当使用相关子查询时,会自动生成这些语句。
若要探索此缺点,请在 ~/App_Code/DAL
文件夹中创建临时类型化数据集。 在 TableAdapter 配置向导中,选择使用即席 SQL 语句并输入以下 SELECT
查询(请参阅图 1):
SELECT ProductID, ProductName, Products.SupplierID, Products.CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder,
ReorderLevel, Discontinued,
Categories.CategoryName,
Suppliers.CompanyName as SupplierName
FROM Products
LEFT JOIN Categories ON
Categories.CategoryID = Products.CategoryID
LEFT JOIN Suppliers ON
Suppliers.SupplierID = Products.SupplierID
图 1:输入包含 JOIN
s 的主查询(单击以查看全尺寸图像)
默认情况下,TableAdapter 将根据主查询自动创建INSERT
UPDATE
和DELETE
语句。 如果单击“高级”按钮,可以看到此功能已启用。 尽管进行了此设置,但由于主查询包含JOIN
,TableAdapter 将无法创建INSERT
、UPDATE
和DELETE
语句。
图 2:输入包含 JOIN
s 的主查询
单击“完成”以完成向导。 此时,您的数据集设计器将包含一个单一的 TableAdapter,以及一个包含查询列列表中每个字段的 DataTable。 如图 3 所示,这包括CategoryName
和SupplierName
。
图 3:DataTable 的每个字段在列列表中都有对应的列
虽然 DataTable 具有相应的列,但 TableAdapter 缺少其 InsertCommand
、UpdateCommand
值和 DeleteCommand
属性。 若要确认这一点,请单击设计器中的 TableAdapter,然后转到“属性”窗口。 在那里,你会看到属性InsertCommand
、UpdateCommand
和DeleteCommand
都设置为(None)。
图 4:“InsertCommand
UpdateCommand
属性DeleteCommand
”设置为“(无)(单击以查看全尺寸图像)
为了解决此问题,我们可以通过“属性”窗口手动提供 SQL 语句和参数InsertCommand
UpdateCommand
以及DeleteCommand
属性。 或者,我们可以首先将 TableAdapter 的主查询配置为 不包含 任何 JOIN
查询。 这将允许自动为我们生成INSERT
、UPDATE
和DELETE
语句。 完成向导后,我们可以从“属性”窗口手动更新 TableAdapter, SelectCommand
使其包含 JOIN
语法。
虽然这种方法有效,但在使用即席 SQL 查询时非常脆弱,因为每次通过向导重新配置 TableAdapter 的主查询时,自动生成的INSERT
、UPDATE
和DELETE
语句都会被重新创建。 这意味着,如果我们右键单击 TableAdapter,从上下文菜单中选择“配置”,然后再次完成向导,我们稍后所做的所有自定义项都将丢失。
TableAdapter 自动生成的INSERT
、UPDATE
和DELETE
语句的脆弱性很幸运地仅限于即席 SQL 语句。 如果 TableAdapter 使用存储过程,则可以自定义SelectCommand
、InsertCommand
UpdateCommand
或DeleteCommand
存储过程并重新运行 TableAdapter 配置向导,而无需担心存储过程将被修改。
在接下来的几个步骤中,我们将创建一个 TableAdapter,该 TableAdapter 初始使用不包含任何 JOIN
项的主查询,以便自动生成相应的插入、更新和删除存储过程。 然后,我们将更新 SelectCommand
,以便使用一个 JOIN
,该 JOIN
从相关表中返回额外的列。 最后,我们将创建相应的业务逻辑层类,并演示如何在 ASP.NET 网页中使用 TableAdapter。
步骤 1:使用简化的主查询创建 TableAdapter
在本教程中,我们将为 NorthwindWithSprocs
数据集中的 Employees
表添加 TableAdapter 和 强类型的数据表。 该 Employees
表包含一个 ReportsTo
字段,用来指定员工的经理 EmployeeID
。 例如,员工安妮·多兹沃思的ReportTo
值为5,而这是史蒂文·布坎南的EmployeeID
值。 因此,安妮向她的经理史蒂文报告。 除了报告每个员工 ReportsTo
的值之外,我们还可能想要检索其经理的名称。 这可以使用JOIN
来实现。 使用JOIN
最初创建 TableAdapter 时,会阻止向导自动生成相应的插入、更新和删除功能。 因此,我们将首先创建主查询不包含任何 JOIN
项的 TableAdapter。 然后,在步骤 2 中,我们将更新主查询存储过程,以使用 JOIN
获取经理的姓名。
首先,在 NorthwindWithSprocs
文件夹中打开数据集 ~/App_Code/DAL
。 右键单击设计器,从上下文菜单中选择“添加”选项,然后选择 TableAdapter 菜单项。 这将启动 TableAdapter 配置向导。 如图 5 所示,让向导创建新的存储过程,然后单击“下一步”。 有关如何通过 TableAdapter 向导创建新存储过程的指南,请参阅 Typed DataSet 的 TableAdapters 教程中的“创建新存储过程”章节。
图 5:选择“创建新存储过程”选项(单击以查看全尺寸图像)
对 SELECT
TableAdapter 主查询使用以下语句:
SELECT EmployeeID, LastName, FirstName, Title, HireDate, ReportsTo, Country
FROM Employees
由于此查询不包含任何 JOIN
项,TableAdapter 向导将自动创建包含相应 INSERT
、 UPDATE
语句和 DELETE
语句的存储过程,以及用于执行主查询的存储过程。
以下步骤允许我们命名 TableAdapter 的存储过程。 使用名称Employees_Select
、Employees_Insert
、Employees_Update
和Employees_Delete
,如图 6 所示。
图 6:将 TableAdapter s 存储过程命名(单击以查看全尺寸图像)
最后一步提示我们命名 TableAdapter 方法。 使用 Fill
并 GetEmployees
用作方法名称。 此外,请务必保留 Create 方法以将更新直接发送到数据库(GenerateDBDirectMethods)复选框。
图 7:命名 TableAdapter 的方法 Fill
和 GetEmployees
(单击以查看全尺寸图像)
完成向导后,花点时间检查数据库中的存储过程。 应会看到四个新项: Employees_Select
、 Employees_Insert
、 Employees_Update
和 Employees_Delete
。 接下来,检查刚刚创建的 EmployeesDataTable
和 EmployeesTableAdapter
。 DataTable 包含主查询返回的每个字段的列。 单击 TableAdapter,然后转到“属性”窗口。 你将在那里看到属性InsertCommand
、UpdateCommand
和DeleteCommand
已正确配置,以调用相应的存储过程。
图 8:TableAdapter 包括插入、更新和删除功能(单击以查看全尺寸图像)
自动创建了插入、更新和删除存储过程,并正确配置了InsertCommand
、UpdateCommand
和DeleteCommand
属性后,我们就可以自定义SelectCommand
存储过程,以返回关于每个员工经理的额外信息。 具体而言,我们需要更新Employees_Select
存储过程以使用JOIN
并返回管理器的FirstName
和LastName
值。 更新存储过程后,我们需要更新 DataTable,使其包含这些附加列。 我们将在步骤 2 和 3 中处理这两个任务。
步骤 2:将存储过程自定义以包含某个元素JOIN
首先转到服务器资源管理器,向下钻取到 Northwind 数据库的“存储过程”文件夹,然后打开 Employees_Select
存储过程。 如果未看到此存储过程,请右键单击“存储过程”文件夹,然后选择“刷新”。 更新存储过程,以便它使用 LEFT JOIN
返回经理的名字和姓氏。
SELECT Employees.EmployeeID, Employees.LastName,
Employees.FirstName, Employees.Title,
Employees.HireDate, Employees.ReportsTo,
Employees.Country,
Manager.FirstName as ManagerFirstName,
Manager.LastName as ManagerLastName
FROM Employees
LEFT JOIN Employees AS Manager ON
Employees.ReportsTo = Manager.EmployeeID
更新 SELECT
语句后,转到“文件”菜单并选择“保存” Employees_Select
保存更改。 或者,可以单击工具栏中的“保存”图标或按 Ctrl+S。 保存更改后,右键单击 Employees_Select
服务器资源管理器中的存储过程,然后选择“执行”。 这将运行存储过程,并在“输出”窗口中显示其结果(请参阅图 9)。
图 9:存储过程结果显示在输出窗口中(单击以查看全尺寸图像)
步骤 3:更新数据表(DataTable)的列
此时, Employees_Select
存储过程返回 ManagerFirstName
和 ManagerLastName
值,但 EmployeesDataTable
缺少这些列。 可以通过以下两种方式之一将这些缺失列添加到 DataTable:
- 手动 - 右键单击数据集设计器中的 DataTable,然后从“添加”菜单中选择“列”。 然后,可以命名列并相应地设置其属性。
-
自动 - TableAdapter 配置向导将更新 DataTable 中的列,以反映存储过程所返回的字段。 使用即席 SQL 语句时,由于
SelectCommand
现在包含JOIN
,向导还将删除InsertCommand
、UpdateCommand
和DeleteCommand
属性。 但是,使用存储过程时,这些命令属性保持不变。
我们在之前的教程中探索过如何手动添加 DataTable 列,包括使用项目符号列表呈现主记录和详细信息 DataList及上传文件。我们将在下一期教程中更详细地研究这一过程。 但是,对于本教程,让我们通过 TableAdapter 配置向导使用自动方法。
首先,右键单击 EmployeesTableAdapter
并从上下文菜单中选择“配置”。 此时会显示 TableAdapter 配置向导,其中列出了用于选择、插入、更新和删除的存储过程及其返回值和参数(如果有)。 此向导如图 10 所示。 在这里,我们可以看到 Employees_Select
存储过程现在返回 ManagerFirstName
和 ManagerLastName
字段。
图 10:向导显示存储过程的 Employees_Select
更新列列表(单击以查看全尺寸图像)
单击“完成”完成向导。 返回数据集设计器后,EmployeesDataTable
包含了两个新增的列:ManagerFirstName
和 ManagerLastName
。
图 11: EmployeesDataTable
包含两个新列(单击可查看全尺寸图像)
为了说明更新 Employees_Select
的存储过程有效,并且 TableAdapter 的插入、更新和删除功能仍然有效,让我们创建一个网页,允许用户查看和删除员工。 但是,在创建此类页面之前,我们需要先在业务逻辑层中创建新类,以便与 NorthwindWithSprocs
数据集中的员工合作。 在步骤 4 中,我们将创建一个 EmployeesBLLWithSprocs
类。 在步骤 5 中,我们将从 ASP.NET 页面使用此类。
步骤 4:实现业务逻辑层
在~/App_Code/BLL
文件夹中创建一个名为EmployeesBLLWithSprocs.vb
的新类文件。 此类模拟现有 EmployeesBLL
类的语义,仅此新类提供更少的方法并使用 NorthwindWithSprocs
数据集(而不是 Northwind
DataSet)。 将以下代码添加到 EmployeesBLLWithSprocs
类。
Imports NorthwindWithSprocsTableAdapters
<System.ComponentModel.DataObject()> _
Public Class EmployeesBLLWithSprocs
Private _employeesAdapter As EmployeesTableAdapter = Nothing
Protected ReadOnly Property Adapter() As EmployeesTableAdapter
Get
If _employeesAdapter Is Nothing Then
_employeesAdapter = New EmployeesTableAdapter()
End If
Return _employeesAdapter
End Get
End Property
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Select, True)> _
Public Function GetEmployees() As NorthwindWithSprocs.EmployeesDataTable
Return Adapter.GetEmployees()
End Function
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteEmployee(ByVal employeeID As Integer) As Boolean
Dim rowsAffected = Adapter.Delete(employeeID)
'Return true if precisely one row was deleted, otherwise false
Return rowsAffected = 1
End Function
End Class
EmployeesBLLWithSprocs
类的Adapter
属性返回NorthwindWithSprocs
数据集的EmployeesTableAdapter
实例。 这由类 GetEmployees
和 DeleteEmployee
方法使用。 使用 GetEmployees
的方法调用 EmployeesTableAdapter
的相应 GetEmployees
方法,该方法将调用 Employees_Select
存储过程,并将结果填入 EmployeeDataTable
。
DeleteEmployee
方法同样调用 EmployeesTableAdapter
的 Delete
方法,该方法再调用 Employees_Delete
存储过程。
步骤 5:在展示层中处理数据
完成EmployeesBLLWithSprocs
课程后,我们便可以通过ASP.NET页面处理员工数据。 打开JOINs.aspx
文件夹中的AdvancedDAL
页面,并将 GridView 从工具箱拖到设计器上,将其ID
属性设置为 Employees
。 接下来,在 GridView 的快捷标签中,将网格绑定到新的名为 EmployeesDataSource
的 ObjectDataSource 控件。
将 ObjectDataSource 配置为使用 EmployeesBLLWithSprocs
类,并确保在 SELECT 和 DELETE 选项卡中从下拉列表中选择了 GetEmployees
和 DeleteEmployee
方法。 单击“完成”以完成 ObjectDataSource 的配置。
图 12:将 ObjectDataSource 配置为使用 EmployeesBLLWithSprocs
类(单击以查看全尺寸图像)
图 13:让 ObjectDataSource 使用 GetEmployees
和 DeleteEmployee
方法(单击以查看全尺寸图像)
Visual Studio 会为 EmployeesDataTable
的每个列在 GridView 中添加一个 BoundField。 删除除 Title
、LastName
、FirstName
、ManagerFirstName
和 ManagerLastName
之外的所有 BoundField,并将最后四个 BoundField 的属性重命名为姓氏、名字、经理的名字和经理的姓氏。
若要允许用户从此页面中删除员工,我们需要执行两项作。 首先,选择 GridView 的智能标签中的“启用删除”选项,以指示 GridView 提供删除功能。 其次,将 ObjectDataSource 的 OldValuesParameterFormatString
属性从 ObjectDataSource 向导(original_{0}
)设置的值更改为其默认值({0}
)。 进行这些更改后,GridView 和 ObjectDataSource 声明性标记应如下所示:
<asp:GridView ID="Employees" runat="server" AutoGenerateColumns="False"
DataKeyNames="EmployeeID" DataSourceID="EmployeesDataSource">
<Columns>
<asp:CommandField ShowDeleteButton="True" />
<asp:BoundField DataField="Title"
HeaderText="Title"
SortExpression="Title" />
<asp:BoundField DataField="LastName"
HeaderText="Last Name"
SortExpression="LastName" />
<asp:BoundField DataField="FirstName"
HeaderText="First Name"
SortExpression="FirstName" />
<asp:BoundField DataField="ManagerFirstName"
HeaderText="Manager's First Name"
SortExpression="ManagerFirstName" />
<asp:BoundField DataField="ManagerLastName"
HeaderText="Manager's Last Name"
SortExpression="ManagerLastName" />
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="EmployeesDataSource" runat="server"
DeleteMethod="DeleteEmployee" OldValuesParameterFormatString="{0}"
SelectMethod="GetEmployees" TypeName="EmployeesBLLWithSprocs">
<DeleteParameters>
<asp:Parameter Name="employeeID" Type="Int32" />
</DeleteParameters>
</asp:ObjectDataSource>
通过浏览器访问来测试页面。 如图 14 所示,页面将列出每个员工及其经理的姓名(假设他们有一个)。
图 14:JOIN
存储过程中的Employees_Select
返回经理的名称(单击以查看全尺寸图像)
单击“删除”按钮将启动删除工作流,最终会执行存储过程 Employees_Delete
。 存储过程中的 DELETE
语句尝试失败,因为违反了外键约束(请参阅图 15)。 具体而言,每个员工在 Orders
表中都有一个或多个记录,导致删除失败。
图 15:删除具有相应订单的员工会导致外键约束冲突(单击可查看全尺寸图像)
若要允许删除员工,可以:
- 更新外键约束以实现级联删除。
- 请从
Orders
表中手动删除您想要删除的员工记录,或者 - 更新
Employees_Delete
存储过程,在删除Employees
记录之前先从Orders
表中删除相关记录。 我们在 Typed DataSet s TableAdapters 教程的“使用现有存储过程” 中讨论了此方法。
我把这留给读者的练习。
概要
使用关系数据库时,查询通常会从多个相关表拉取其数据。 关联子查询和 JOIN
s 提供了两种不同的技术来访问查询中相关表中的数据。 在前面的教程中,我们最常使用相关子查询,因为 TableAdapter 无法自动生成INSERT
和UPDATE
DELETE
语句用于涉及 JOIN
s 的查询。 尽管可以手动提供这些值,但是当使用即席 SQL 语句时,任何自定义内容将在完成 TableAdapter 配置向导时被覆盖。
幸运的是,使用存储过程创建的 TableAdapters 不会像使用即席 SQL 语句创建的那样脆弱。 因此,在使用存储过程时,创建一个主查询使用 JOIN
的 TableAdapter 是可行的。 本教程介绍了如何创建此类 TableAdapter。 我们首先对 TableAdapter 的主查询使用不含JOIN
的SELECT
查询,以便自动创建相应的插入、更新和删除存储过程。 完成 TableAdapter 的初始配置后,我们扩充了 SelectCommand
存储过程,以便使用 JOIN
并重新运行 TableAdapter 配置向导来更新 EmployeesDataTable
这些列。
重新运行 TableAdapter 配置向导会自动更新 EmployeesDataTable
列,以反映存储过程返回 Employees_Select
的数据字段。 或者,我们可以手动将这些列添加到 DataTable。 我们将在下一教程中了解如何手动将列添加到 DataTable。
快乐编程!
关于作者
斯科特·米切尔,七本 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