您好,脚本专家!能保存此关系吗?

Microsoft 脚本专家

下载这篇文章的代码: HeyScriptingGuy2007_09.exe (150KB)

关系 确实很重要。但脚本专家(在所有人中)是如何知道关系重要的?答案是这样的,首先,我们看了很多电视节目,在电视上,人们经常告诉我们关系很重要。

可是关系并不仅仅出现在每周的肥皂剧或电视电影中。例如,有一个宣传自己可为人们的业务提供资助的日历贷款公司广告。首先,是不是由于他们愿意贷款给本来得不到贷款的人呢?不是。那是不是由于他们将在这笔贷款中收取您 900% 的年利息呢?当然不是。这是因为该日历贷款公司与您之间形成了个人关系。就像其广告所说的,“关系非常重要”。

注意:当然,我们在哭泣,但我们并不为此感到羞愧。毕竟,只要想一想将个人与其日历贷款公司紧紧绑在一起的牢固关系,您就必须相当冷静而不能有丝毫的激动。

当然,我们知道您在想什么:“天啊,如果关系对日历贷款公司很重要,那么关系对编写与数据库交互的脚本的人必定相当重要了。”事实上,关系应该对编写与数据库交互的脚本的人很重要。遗憾的是,尽管如此,这些脚本编写者并不总能意识到关系重要。但没有关系;我们将在本月专栏里矫正大家的观念。

我们知道,很多系统管理员往往将使用数据库作为跟踪其硬件清单的方法。问题是,其中很多数据库并不是以最佳设置方式设置的。例如,假设您需要一个数据库来跟踪连接到所有计算机上的磁盘驱动器。如果您没有数据库设计背景,这是一个很好的学习机会,您将创建包含一个表的数据库,如图 1 所示。

图 1 包含一个表的数据库

图 1** 包含一个表的数据库 **

如您所见,这是一个非常简单的设计:一个字段用来填写计算机名称,两个“是/否”字段用来确认计算机是否具有 C: 驱动器和 D: 驱动器。这就是整个设计了。干脆利落、切中主题。

这个数据库有什么问题?实话告诉您,几乎一切都有问题。如果您可以确定自己没有使用两个以上磁盘驱动器的计算机,此设计会起到作用(在某种程度上)。但是,如果计算机具有三个磁盘驱动器,会发生什么情况?在这种情况下,您将不得不添加另一字段(用来跟踪驱动器 E)。但是如果计算机具有 11 个磁盘驱动器,会发生什么情况?并且如果您希望跟踪其中每个驱动器的属性,又会发生什么情况?例如,假设您希望跟踪每个驱动器的总容量。在这种情况下,您需要 Drive_C_Size 字段和 Drive_D_Size 字段,以及 Drive_E_Size 字段,等等 — 您有点明白了吧。(另外,但愿您不会还希望跟踪可用磁盘空间、磁盘连接器类型、是否在驱动器上启用了磁盘配额等等。)

此处有一条经验法则需要记住:一个实体在任何时候都可以拥有多个事物(例如,一台计算机可以拥有多个磁盘驱动器),所以该“平面文件”(一个表格)设计不是很好。确实,您也许认为可以侥幸应付这种磁盘驱动器的设计;毕竟,连接到计算机的驱动器的数量是有限的。但是假设您希望查询数据库并获得所有 100 GB 或更大的磁盘驱动器的列表。您将如何执行此类操作?首先,您需要在 Drive_C_Size 字段查找 100 GB 的驱动器,然后在 Drive_D_Size 字段查找同样的内容,接下来在 Drive_E_Size 字段,接着在......总之,这不是切实有效的方法。但是有其他方法吗?如果您不能用平面文件数据库,那能用什么?

这很简单:关系数据库。关系数据库的设计意图之一就是处理一对多关系(例如,一台可具有多个磁盘驱动器的计算机)。您可能会问是否需要出去购买新的产品来使用关系数据库,先告诉您:不用;如果您有任何一种数据库(Microsoft® Access™、SQL Server™、Oracle 等都可以),很可能您就已经拥有关系数据库了。您不必去购买任何东西,只需知道两件事情即可:1) 如何在数据库的两个表之间建立关系;和 2) 如何编写可以利用此关系的脚本。

注意:根据电视节目,您绝对不应该利用关系。但这是不同的。

因为此专栏名称不是“您好,数据库设计专家!”,所以我们不会花很多时间讨论数据库设计。我们仅是向您演示一个非常简单的关系数据库设计,我们将使用该设计讨论如何编写关系查询。在该数据库中,我们有两个表。第一个表命名为 Computers,含有两个字段:ComputerName 和 SerialNumber。ComputerName 字段用来存储计算机名称,SerialNumber 字段用来存储 — 呵,您猜对了 — 序列号。

那么,在何处存放磁盘驱动器信息呢?那就要用到第二个表了,该表名为 DiskDrives。它包含三个容易理解的字段:SerialNumber、DriveLetter 和 DriveSize。

此处的关键字段是 SerialNumber。这个字段名称听起来熟悉,确实,因为它与 Computers 表中的一个字段完全相同。这并不是意外或巧合,我们是专门这样设计的。为了在这两个表之间建立关系,我们需要某一个字段在两个表中同时出现,这样,我们就可以分辨哪些磁盘驱动器属于哪些计算机了。

为什么用序列号而不用计算机名称链接两个表?实际上,这是因为有一个充分的理由:计算机名称会(并且经常)更改。而序列号往往不会更改。

我们说得够多了。让我们看看图 2,一个脚本可以搜索这两个表并检索属于各台计算机的磁盘驱动器的列表。图 3 显示了该脚本返回的此类数据。

Figure 3 搜索结果

atl-ws-01
C:

atl-ws-01
D:

atl-ws-01
E:

atl-ws-02
C:

atl-ws-02
D:

Figure 2 搜索表

Const adOpenStatic = 3
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
 "Provider = Microsoft.Jet.OLEDB.4.0; " & _
 "Data Source = c:\scripts\test.mdb" 

objRecordSet.Open "SELECT Computers.*, DiskDrives.* FROM Computers " & _
 "INNER JOIN DiskDrives ON Computers.SerialNumber = DiskDrives.SerialNumber " & _
 "ORDER BY Computers.ComputerName, DiskDrives.Drive", objConnection, adOpenStatic, _
  adLockOptimistic

objRecordSet.MoveFirst

Do Until objRecordset.EOF
 Wscript.Echo objRecordset.Fields.Item("ComputerName")
 Wscript.Echo objRecordset.Fields.Item("Drive")
 Wscript.Echo
 objRecordset.MoveNext
Loop

objRecordSet.Close
objConnection.Close

我们不打算在本月的专栏文章中论述连接数据库和使用数据库的基础知识。如果您是编写数据库脚本的初学者,您可能想查看“脚本专家”网络广播“系统管理员的数据库脚本”(go.microsoft.com/fwlink/?LinkId=22089)。我们所要说的是我们正要连接到一个 Access 数据库 (C:\Scripts\Test.mdb),并与名为 Computers 和 DiskDrives 的关系表一起使用。这从脚本来看应该相当清楚。

我们要说的另一件事是您需要做一点改进以便与 Access 2007 一起使用:很重要的一点是提供程序需要从 Microsoft.Jet.OLEDB.4.0 更改为 Microsoft.ACE.OLEDB.12.0,代码如下:

objConnection.Open _
 "Provider = Microsoft.ACE.OLEDB.12.0; " & _
 "Data Source = c:\scripts\test.accdb" 

对,就是这样。我们下面要执行的操作重点在于从两个表中检索数据的 SQL 查询:

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "INNER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "ORDER BY ComputerName, Drive", _
 objConnection, adOpenStatic, adLockOptimistic 

复杂吗?也许有一点。这样,我们看看是否可以将其分解以便于理解。

查询的第一部分实际上非常简单。我们要选择两个表中的所有字段:Computers 和 DiskDrives。因而就采用了下面这段 SQL 代码:

SELECT Computers.*, DiskDrives.* 

确实不错;无需说明,星号只是表示“任何内容”的通配符。

如果您以前编写过 SQL 查询(或者使用 SQL 查询语言的子集编写过 Windows<SUP>®</SUP> Management Instrumentation 查询或 WMI 查询),您应该知道相应的例程:在指定了要选择的项目后,接下来指定从哪里选择这些项目。以下是下一条查询,比较难:

FROM Computers INNER JOIN DiskDrives ON Computers.SerialNumber = DiskDrives.SerialNumber

不要让这些把您搞晕。当然,这比典型的 FROM 语句复杂得多,但使用它是有充分理由的。毕竟,在典型的 SQL 查询中,我们只从一个表中选择数据;而这次我们同时从两个表中选择数据。

让我们进一步看看该语法以及它的工作方式。正如您所看到的一样,我们要求脚本从 Computers 表和 DiskDrives 表选择数据。不过,请注意,我们不使用单词“and”,相反,我们用术语 INNER JOIN。这个术语定义了关系的类型,并说明了如何将两个单独表中的信息连接到一个记录集。(是的,存在其他类型的联接;稍后我们将讨论这些联接。)

正如前面指出的,指定两个表之间的关系类型以及这两个表到底如何进行关联由我们自己决定。下面这段代码将为我们实现上述目的:

ON Computers.SerialNumber = DiskDrives.SerialNumber

我们现在要做的是定义如何联接表。只要 Computers 表中的 SerialNumber 字段与 DiskDrives 表中的 SerialNumber 字段相匹配,随时都会对记录进行组合。如果我们将其他字段(例如 ComputerName)用作联接字段,会怎样?此时所用的代码将类似如下内容:

ON Computers.ComputerName = DiskDrives.ComputerName

为了防止您会产生疑惑,我事先说明一下,是的,这两个字段不必名称相同;它们只需包含相同数据。使用相同的名称可更容易地识别关系字段。这里只需记住一个问题:由于两个字段具有同一名称,所以在引用其中一个字段时需要始终使用语法 TableName.FieldName。这表示要使用类似以下的代码:Wscript.Echo obj- Recordset.Fields.Item("Computers.SerialNumber")。

这实际上就是我们所需的一切;查询的其余部分将仅对数据进行排序,首先按计算机名称排序,然后按驱动器排序:

ORDER BY ComputerName, Drive

看,这种方法很不错,不是吗?我们现在需要暂停片刻,说一下联接。为什么在查询中使用内部联接?很简单。内部联接仅返回在每个表中具有匹配值的记录。换句话说,假设我们有序列号 121989。如果该序列号在 Computers 表和 DiskDrives 表中同时出现,则会返回相应的记录。(当然,并不是在任何地方显示的序列号都有效;它必须显示在 SerialNumber 字段中。)明白了吗?很好。

现在,假设我们有一台具有序列号 121989 的计算机;但是,没有使用该序列号的磁盘驱动器。在这种情况下,将不返回具有序列号 121989 的计算机的数据。这是因为内部联接仅当记录在每一个联接的表中具有匹配值时才返回信息。

换句话说,内部联接查询返回所有具有硬盘的计算机;它不返回任何不具有硬盘的计算机,或者未在计算机上安装的硬盘。很好;这通常是我们需要的。但是,如果您确实想要不具有硬盘的计算机列表或未安装在计算机上的硬盘列表,怎么办?然后呢?

这就是外部联接的用途。(哦,所以这就是外部联接的用途!)现在,我们将谈谈外部联接的两种不同类型:左和右。我们的数据库中有两个表:Computers(视为“左”表,因为它是主表)和 DiskDrives(视为“右”表,因为它不是主表)。假设我们希望返回的记录集包含所有计算机,甚至包含那些没有安装磁盘驱动器的计算机。在这种情况下,我们使用语法 LEFT OUTER JOIN 以及与以下代码类似的查询:

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "LEFT OUTER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "ORDER BY ComputerName, Drive", _
 objConnection, adOpenStatic, adLockOptimistic

您可能已经明白了,即使在右表中没有相应的记录,左外部联接也会返回左表中的所有记录。这样,我们可以收到所有计算机的记录,但从右表(磁盘驱动器)只能返回与计算机关联的记录。

或者,也许我们想要所有磁盘驱动器(包括那些没有安装在计算机上的磁盘驱动器)的列表。因为 DiskDrives 表在关系中是右表,所以我们使用语法 RIGHT OUTER JOIN,类似如下代码:

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "RIGHT OUTER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "ORDER BY ComputerName, Drive", _
 objConnection, adOpenStatic, adLockOptimistic

现在我们考虑一下,如果 DiskDrives 在关系中是右表,那么不管怎么样 Computers 在关系中应该被认为是 husband 表而不是左表。正如某些脚本专家经过痛苦的经历了解到的,husband 在关系中绝不会是右实体。

利用右外部联接,我们可以收到右表中的所有记录(所有磁盘驱动器),但只能收到与磁盘驱动器关联的计算机(左表中的记录)记录。

目前来看其意义可能不大,但您只要用用它,研究一下,就会明白它起的作用。如果您需要使用数据库,请访问 microsoft.com/technet/scriptcenter/resources/tnmag/archive.mspx,我们在其中发布了一个示例供您使用。

无论何时使用外部联接,至少也要使 On Error Resume Next 处于脚本上的第一行,这至关重要。为什么?假设我们执行左外部联接;在这种情况下,我们有可能收到一些未安装硬盘的计算机记录。这很好(毕竟,这正是我们希望收到的),至少在我们遇到下面这行代码之前是这样的,这行代码回显驱动器号:

Wscript.Echo objRecordset.Fields.Item("Drive")

由于特定计算机上不存在“驱动器”字段,所以脚本将崩溃停止:

C:\Scripts\database.vbs(22, 9) Microsoft VBScript runtime error: Type mismatch

不过,如果我们实现了 On Error Resume Next,则脚本可以忽略计算机不具有磁盘驱动器的事实并且能继续顺利执行。或者,您可能使用如下代码来检查“驱动器”字段的值,然后执行相应的操作:

If IsNull(objRecordset.Fields.Item("Drive")) _Then
    Wscript.Echo "No disk drives installed."
Else
    Wscript.Echo _
      objRecordset.Fields.Item("Drive")
End If

使用此代码,我们将检查是否收到了空值而非实际驱动器号。如果返回空值,我们将回显“未安装磁盘驱动器”消息。如果未返回空值,我们只回显“驱动器”字段的值。最终的结果输出如图 4 所示。

Figure 4 显示相应结果

atl-ws-01
C:

atl-ws-01
D:

atl-ws-02
C:

atl-ws-02
D:

alt-ws-03
No disk drives installed.

无需过多解释,让我们看看另外两个查询。例如,下面是一个内部联接查询,返回了计算机及其安装的磁盘驱动器的列表,并假定这些磁盘驱动器在 50 GB 以上(我们假想磁盘容量大小用 GB 来计算):

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "INNER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "WHERE DriveSize > 50 " & _
 "ORDER BY Computers.ComputerName, " & _
 "DiskDrives.Drive", objConnection, _
 adOpenStatic, adLockOptimistic

您可以清楚地看到,我们所做的全部工作就是将标准的 Where 子句添加到原始查询:

WHERE DriveSize > 50

如果我们只需要计算机上 E 驱动器的相关信息,怎么办?没问题 — 只需添加相应的 Where 子句:

WHERE Drive = 'E:'

以下是一个稍显复杂的查询,它返回未安装磁盘驱动器的计算机集合:

objRecordSet.Open _
 "SELECT Computers.ComputerName, " & _
 "DiskDrives.Drive " & _
 "FROM Computers LEFT JOIN DiskDrives " & _
 "ON Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "GROUP BY Computers.ComputerName, " & _
 "DiskDrives.Drive " & _
 "HAVING (DiskDrives.Drive) Is Null " & _
 "ORDER BY Computers.ComputerName", _
 objConnection, adOpenStatic, adLockOptimistic

如我们所说,这个查询有些复杂,由于篇幅所限,我们不解释它的工作原理了。但是它确实奏效,而这是最重要的事情。

还有,也许这是第二重要的事情:正如我们曾经多次指出的,关系确实很重要。这并不表示关系不会出现偏差。正如 Woody Allen 在“Annie 会议”结束前所说,“关系就像是一条鲨鱼,它必须不断前进,否则就会死亡。而我想我们获得的是一条死鲨鱼”。要是 Woody 已了解内部联接和外部联接,就不会这样说了。利用这些查询技巧,您可以始终拥有成功的关系!

Microsoft 脚本专家为 Microsoft 工作,也就是受雇于 Microsoft。在玩、教或看棒球(以及各种其他活动)的闲暇之余,他们还负责维护 TechNet 脚本中心。您可以光顾他们的网站 www.scriptingguys.com,看看他们在做些什么。

© 2008 Microsoft Corporation 与 CMP Media, LLC.保留所有权利;不得对全文或部分内容进行复制.