脚本的故事

2005 年 5 月

发布日期: 2005年05月19日
*

伙计,我的打印机在哪里:使用脚本对 Active Directory 执行搜索(第 2 部分,共 2 部分)

和答案本身相比,探求某个问题的答案但是却没有找到答案能让我们学习到更多知识。

-- Lloyd Alexander

Lloyd Alexander 撰写过许多面向儿童的幻想书籍,所以,很容易理解他的这番话不是针对通常的 IT 专业人员或系统管理员的。当然,在一个幻想世界里,寻求某个问题的答案但是并没有找到答案这种情况具有很大的价值可能是正确的。但对于系统管理员,情况一般不是这样的:

“您是否可以列出财务部门拥有的所有打印机?”

“不,我找不出。但是我真的认为可以在寻找打印机列表的过程中学习到更多宝贵的经验。”

我们想,这个系统管理员可能学习到的宝贵经验是:在现实世界中,找到答案通常要比找不到答案更好。(我们这些脚本专家对此都有着痛苦的经验和体会。)

幸运的是,我们需要的许多答案在 Active Directory 中都可以找到:我们需要做的就是提出问题,然后 Active Directory 将很乐意为我们提供帮助。而这就是本月的脚本的故事的目的所在(当然,与流行的信仰相比,有时本专栏绝对仅有一个目的)。此外,这个分为两部分的系列专栏文章的第一部分 向您介绍了对 Active Directory 执行搜索所需了解的一些基本原则。在第 2 部分中,我们将讨论对 Active Directory 执行搜索所需的一些实际的东西:我们将展示如何编写有助于精确地返回所需信息的 SQL 查询。返回的数据将不多不少,恰到好处。

注意:实际上,也可以使用另外一种类型的语法来对 Active Directory 执行搜索:即 LDAP 语法。我们发现,SQL 语法要更易于理解,所以我们将主要介绍这种语法。如果您对 LDAP 语法感兴趣,可以在您繁忙的工作中抽出一小时的时间来观看有关搜索 Active Directory 的“脚本专家”Web 广播。当然也可以不这样做,毕竟,有的时候,不观看 Web 广播比观看 Web 广播能学习到更多的东西。否则,他们也会这么说。

返回页首返回页首

SQL 查询的基本组成部分

某位脚本专家朦朦胧胧地记得在某个电影中,一个年轻人走进一个年轻的姑娘,并且问她是否可以吻她。她立刻给了他一巴掌。这个年轻小伙子的朋友也走到这个姑娘的旁边并要求吻她,姑娘立刻同意了。为什么他成功了,而第一个小伙子失败了呢?“关键不在于您所要求的东西,”,成功的小伙子说。“关键在于你如何提出你的要求。”

对于 Active Directory 也同样如此。在您以错误的方式提出一个问题时,Active Directory 不会给你一巴掌(虽然在未来的版本中,我们考虑将这样做);但是,如果您问了一个错误的问题,Active Directory 肯定会返回错误的信息(如果它能返回一些信息的话)。这是什么意思呢?这表示,能够成功地对 Active Directory 进行搜索的秘诀不仅在于提出正确的问题,而且需要以正确的方式提出问题。而以正确的方式提出问题意味着仔细构造您的 SQL 查询。

在本月的专栏中,我们将解释编写用于搜索 Active Directory 的 SQL 查询背后所隐藏的秘密和诀窍。我们把典型的 SQL 查询划分为三个部分,如下图所示:

SQL Query


若要查看完整大小的图片,请单击此处

现在,我们将为您介绍所有这些项目:

指定要返回的属性

指定开始进行搜索的位置

指定可选的 WHERE 子句

下面,让我们开始逐个进行介绍。

返回页首返回页首

指定要返回的属性

在我们的 SQL 查询中,第 1 部分是非常简单明了的:您需要做的就是指定要返回的属性的名称。例如,您是否要返回 Active Directory 中所有对象的名称?(不用担心,我们在后面将介绍如何将返回的数据限制到一个特定的类别 - 例如用户帐户。)那么,可以使用以下查询:

"SELECT NAME FROM 'LDAP://dc=fabrikam,dc=com'"

想知道 Active Directory 中所有对象的 ADsPath?可以使用如下语句:

"SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com'"

非常容易,是吗?但是,如果想要返回多个属性应该怎样做呢?或者,如果想返回所有属性又该怎样做呢?在这种情况下,您可以使用类似如下的查询:

"SELECT * FROM 'LDAP://dc=fabrikam,dc=com'"

对吗,脚本专家?我可以使用 SELECT * FROM 返回多个属性吗?嘿,脚本专家?

听着,我们需要谈一谈…。

返回页首返回页首

返回多个属性:SELECT * FROM 的奇怪情况

如果您有编写 SQL 查询的经验(包括使用 WMI Query Language 编写查询,它是 SQL 的一个子集),那么您肯定熟悉使用 SELECT * FROM 这种快捷方式选择记录集中的所有记录或者某个对象的所有属性。例如,在 WMI 中,以下查询可以选择 Win32_Process 类的所有实例的所有属性:

"SELECT * FROM Win32_Process"

如果您熟悉 SELECT * FROM 的使用,那么您迟早会想编写一个类似下面的 Active Directory 脚本,指出这是一种获得某个对象或对象组的所有属性的简便、快捷的方法:

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection") Set objCommand =   CreateObject("ADODB.Command") objConnection.Provider = "ADsDSOObject" objConnection.Open "Active Directory Provider" Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000 objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE 

objCommand.CommandText = _ "SELECT * FROM 'LDAP://dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user'" Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst Do Until objRecordSet.EOF Wscript.Echo objRecordSet.Fields("Name").Value objRecordSet.MoveNext Loop

我们期望这个脚本返回域中所有用户帐户的所有属性;然后,我们希望该脚本如我们所愿地返回每个用户的 Name 属性的值。但是,这里有一个问题:并不会返回所有用户的名称,您将得到下面这样的错误信息:

ADODB.Recordset:Item cannot be found in the collection corresponding to the requested name or ordinal(无法在与所请求名称或序号对应的集合中找到项)。

嗯?我们要得到的东西仅仅是 Name。难道说 Name 不是一个有效的 Active Directory 属性吗?

听着,不要惊慌失措:Name 绝对是一个有效属性。问题在于,我们的查询不会返回 Name 属性;这就是为什么无法在记录集中找到 Name 的原因。但是,为什么我们的查询不会返回 Name 属性呢?我们不能使用 SELECT * FROM 吗?

是的,不能,这就是问题所在。在 ADSI 中,SELECT * FROM 不会按照您期望的方式工作。在 WMI 中,SELECT * FROM 会返回对象的所有属性。但是,在查询 Active Directory 时,SELECT * FROM 将仅仅返回 ADsPath 属性。原因就在这里。下面这个脚本工作正常,它用于返回所有用户帐户的 ADsPath 属性:

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection") Set objCommand =   CreateObject("ADODB.Command") objConnection.Provider = "ADsDSOObject" objConnection.Open "Active Directory Provider" Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000 objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE 

objCommand.CommandText = _ "SELECT * FROM 'LDAP://dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user'" Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst Do Until objRecordSet.EOF Wscript.Echo objRecordSet.Fields("ADsPath").Value objRecordSet.MoveNext Loop

如果用其他任何属性替换 Echo 语句中的 ADsPath,则此脚本将失败。我保证会这样。

现在,我们还不能肯定地说出 SELECT * 为何会具有这种行为,虽然这似乎是因为您通常不会希望返回所有对象的所有属性这一事实。如果您安装了 Microsoft Exchange(谁会不安装呢?),那么每个用户帐户将拥有大约 300 个属性。那么,如果将 300 个属性乘以 10,000 个用户,您可以完成下面两件事情:为域控制器施加极大的压力(毕竟,它会占用系统资源来处理这些信息),而且您还会发送成吨的数据,导致网络上出现数据风暴。那么,应该怎样来显示每个用户的 Name 属性呢?改变 SELECT * FROM 的行为真的是一件事情;它让您无法使用一种简单的方法来干扰网络的运行。

的确是一件好事情。真的是这样,虽然我们不能利用 ADsPath 完成任何事情,但是我们能够通过搜索返回的唯一属性就是它。例如,如果我们有一个电话目录,我们需要每个用户的“姓”、“名”和“电话号码”这些属性(最起码的属性)。我们已经知道了如何在 Active Directory 中查询单个属性。如果我们想返回多个属性,而且不能使用 SELECT * FROM,那么是否意味着必须为每个属性单独进行一次查询呢?

稍安勿躁,我们不会让您做这样具有难度的事情的。如果您想在查询中返回多个属性,只需简单地在您的 SELECT 语句中列出这些属性,并使用逗号分隔每个属性。例如,这里有一个能够返回 fabrikam.com 域中的所有用户的三个属性(givenName、SN 和 telephoneNumber)的查询:

"SELECT givenName, SN, telephoneNumber " _ & "FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"

注意.喂,脚本专家,请稍等一下!我们需要的是“姓”(last name)和“名”(first name),你的查询为何使用的是 givenName 和 SN 呢?(SN?)这是什么东西?

好的,让我来告诉你:Active Directory 使用的术语和我们使用的并不总是相同。例如,虽然我们将某个东西称作用户的“姓” (last name),但是 Active Directory 将同样的东西称作用户的 SN (surname)。那么用户的“名”呢?对于 Active Directory,这是他或她的 givenName。那么,你是怎么知道这些的?嗯,确定 Active Directory 为属性使用的名称的一种方法便是检查 Active Directory 架构;在进行查询的时候,请确信您使用的是每个属性的 LDAP 显示名称 (Display Name)(类的 ldapDisplayName 代表我们可以在脚本中使用的名称)。例如:

ldapDisplayName


好,我们刚才说到哪了?哦,知道了:在查询中检索多个属性。想获取用户的职务 (Title)、部门 (Department) 以及他们的姓、名和电话号码?只需将这些属性添加到 SELECT 语句中即可(顺序并不重要,可以按照想要的顺序放置它们):

"SELECT givenName, SN, telephoneNumber, title, Department " _ & "FROM 'LDAP://dc=fabrikam,dc=com' WHERE objectCategory='user'"

看见这是多么简单的事情了吗?如果想对某个属性执行一些操作(例如显示它的值),请确保该属性包括在 SELECT 查询中。

OK,想法很好:如果您只需要几个属性,这很容易。但是如果您需要 47 个属性,或 93 个属性,或者(天理难容啊!)真的需要所有用户的所有属性又该怎样呢?您必须在奇长无比的 SELECT 语句中输入 300 个属性的名称吗?

不需要。一个远胜于此的方法是仅返回每个用户的 ADsPath。在有了 ADsPath 之后,可以单独绑定到每个用户帐户,然后显示想要的任意多个属性的值。理论上,这种方法要比同时返回所有值要慢一些;但是在实际应用中,通常会发现它要更快一些,因为这种方法所需耗用的资源更少。例如,这里有一个示例脚本,用来返回 fabrikam.com 域中的所有用户的 ADsPath。然后,该脚本使用 ADsPath 的值单独绑定到每个用户帐户并显示不同属性的值,这些属性都没有在搜索条件中指定:

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection") Set objCommand =   CreateObject("ADODB.Command") objConnection.Provider = "ADsDSOObject" objConnection.Open "Active Directory Provider" Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000 objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE 

objCommand.CommandText = _ "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user'" Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst Do Until objRecordSet.EOF strPath = objRecordSet.Fields("ADsPath").Value Set objUser = GetObject(strPath) Wscript.Echo objUser.givenName Wscript.Echo objUser.initials Wscript.Echo objUser.SN Wscript.Echo objUser.organization Wscript.Echo objUser.department Wscript.Echo objUser.title Wscript.Echo objUser.telephoneNumber objRecordSet.MoveNext Loop

在我们发出这个查询的时候,我们会取得一个包含 fabrikam.com 中每个最终用户的 ADsPath 的记录集。我们遍历此记录集,并获取第一个用户的 ADsPath,将其保存在一个名为 strPath 的变量中;然后,我们使用变量的值绑定到该用户的用户帐户。这就是我们在以下两行代码中完成的工作:

strPath = objRecordSet.Fields("ADsPath").Value Set objUser = GetObject(strPath)

在我们绑定到用户帐户后,我们可以显示该帐户的所有属性的值。然后,我们执行循环,对其 ADsPath 包含在记录集中的下一位用户重复此过程。

顺便说一下,如果您要使用搜索脚本修改一个值,也需要使用上述过程。针对 Active Directory 的 ADO 提供程序是只读的;它不支持 UPDATE 查询或者其他任何会修改数据的查询。因此,在搜索到某个特定用户后,您必须绑定到该用户帐户来执行任何更新或修改操作。例如,下面这个脚本能够取得 Accounting(会计)部门的所有用户,并将其部门属性修改为 Finance(财务)。注意:此查询仅返回 ADsPath 属性;然后,该脚本取得属性的值,连接到返回的每个用户帐户,然后逐个更改部门的名称。

Const ADS_SCOPE_SUBTREE = 2

Set objConnection = CreateObject("ADODB.Connection") Set objCommand =   CreateObject("ADODB.Command") objConnection.Provider = "ADsDSOObject" objConnection.Open "Active Directory Provider" Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000 objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE 

objCommand.CommandText = _ "SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user' AND Department='Accounting'" Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst Do Until objRecordSet.EOF strPath = objRecordSet.Fields("ADsPath").Value Set objUser = GetObject(strPath) objUser.Department = "Finance" objUser.SetInfo objRecordSet.MoveNext Loop
返回页首返回页首

指定开始进行搜索的位置

Active Directory 搜索的最大好处之一就是它允许您搜索所有 Active Directory。需要所有用户、所有计算机或是所有已发布打印机的列表?只需搜索 Active Directory 并将您所在域的根指定为搜索的起点,即可获得这些信息。

熟悉 WMI 的人经常编写类似下面这样的查询,从 WMI 类 (Win32_Process) 中选取一个属性 (Name):

"SELECT Name FROM Win32_Process"

在对 Active Directory 执行搜索时,可以使用类似的语法形式:只需简单地用某个 ADSI 属性替换 WMI 属性,然后用起始搜索位置的 ADsPath 替换该 WMI 类即可。例如,下面的查询将返回 fabrikam.com 中所有对象的 Name 属性(我们假定搜索范围已经设为 ADS_SCOPE_SUBTREE 的默认值):

"SELECT Name FROM 'LDAP://dc=fabrikam,dc=com'"

我们怎样才能知道此查询会搜索整个域?这很简单:可以将域根的 ADsPath 作为起点:

'LDAP://dc=fabrikam,dc=com'

因为使用默认搜索范围,我们的脚本不仅会搜索域根,而且将搜索在域根中找到的任何其他 OU 或容器,以及它们的子容器。实际上,这表示包含了整个域。

顺便提一下,请特别注意 ADsPath 两边的单引号标记,如果没有这些引号,脚本将无法正常运行。

注意:那么,如果想搜索所有域应该怎么办呢?好的,假定您要查询的属性被复制到了全局编录中,以便您只需连接到 GC(全局编录)提供程序,而不是 LDAP 提供程序:

'GC://dc=fabrikam,dc=com'

搜索整个域非常有用。但是,在更多情况下,您需要进行更具针对性的搜索。例如,您可能想将返回的数据限制为 Finance OU(以及 Finance OU 内的所有子 OU)的用户。在这种情况下,您需要做的所有事情就是为搜索指定一个不同的起点位置:不再将域根作为搜索的起点,而使用相关的 OU 作为起始位置。例如,下面的 SQL 语句可以返回 Finance OU(以及 Finance OU 的所有子 OU)中的所有用户的 ADsPath。注意:我们这次没有指定域根,而是指定了 Finance OU 的 ADsPath:

"SELECT ADsPath FROM 'LDAP://ou=Finance,dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user'"

假定 Finance OU 没有任何子 OU。在这种情况下,您不需要使用搜索脚本来获取所有用户的列表;您可以直接绑定到该 OU 并列出所有用户帐户。如果 Finance OU 子 OU,搜索工作就变得太幸运了:否则,您必须写一个递归函数来获得所有的子 OU(以及子 OU 包含的所有子子 OU)。看看刚才我们讨论的东西简直像绕口令一样。而编写脚本的工作则还要更糟糕一些。

但是,如果您真的仅希望搜索一个 OU 并忽略所有子 OU,应该怎么办呢?例如,可能您想列出 Finance OU 中实际上不属于财务部门的所有用户。与绑定每个用户帐户并检查 Department 属性的值相比,对该 OU 进行搜索可能要更快和更容易一些。

在像这样的情况下,您仍然应该指定 LDAP://ou=Finance,dc=fabrikam,dc=com.除此之外,您需要将搜索范围设置为 ADS_SCOPE_BASE;指示脚本仅仅搜索目标容器(Finance OU)并忽略所有子容器(即所有子 OU)。下面是一个完整的脚本,它仅仅搜索 Finance OU:

Const ADS_SCOPE_BASE = 0

Set objConnection = CreateObject("ADODB.Connection") Set objCommand =   CreateObject("ADODB.Command") objConnection.Provider = "ADsDSOObject" objConnection.Open "Active Directory Provider" Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000 objCommand.Properties("Searchscope") = ADS_SCOPE_BASE 

objCommand.CommandText = _ "SELECT Name FROM 'LDAP://ou=Finance,dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user'" Set objRecordSet = objCommand.Execute

objRecordSet.MoveFirst Do Until objRecordSet.EOF Wscript.Echo objRecordSet.Fields("Name").Value objRecordSet.MoveNext Loop

脚本专家提示.如果您决定执行有针对性的搜索,应记住 Users 容器和 Computers 容器都不是 OU;若要将这两个位置指定为搜索起点,需要编写一个类似以下的查询,在 ADsPath 中使用 cn 而不是使用 ou

"SELECT Name FROM 'LDAP://cn=Users,dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user'"
返回页首返回页首

指定可选的 WHERE 子句

没错:假如说 WHERE 子句是可选的,就如同说水对人类是可有可无的东西一样。虽然没有水您也可以生存(尽管时间很短暂),但是这个“可选”的附加物品变为“必需品”所需的时间却不会太长。Active Directory 中的 WHERE 子句也同样如此。例如,下面是一个完全有效的 SQL 语句,它返回 Active Directory 中所有对象的名称:

"SELECT ADsPath FROM 'LDAP://dc=fabrikam,dc=com'"

看到了吗?没有 WHERE 子句,但它是一个绝对有效的 SQL 查询。可选。

当然,很快您就会厌倦取回 Active Directory 中所有对象的列表这种做法,尤其是在您希望列出财务部门的用户或具有彩色打印能力的打印机的时侯。马上,您就会想创建更有效率和更具针对性的搜索;这就是必须使用 WHERE 子句的时侯了。那么,我们也许应该花上几分钟时间来研究一下 WHERE 子句以及它的使用方法。

返回页首返回页首

搜索对象类和对象类别

因为我们已经在本系列专栏文章的第 1 部分中讨论了对象类和对象类别 所以,我们在此不再花费更多的时间来讨论这些概念。我敢说,在大多数 Active Directory 搜索脚本中,您可能都希望包括一两个对象类或对象类别(最有可能的是一个对象类别)。毕竟,在搜索中包括类或类别能使您执行仅返回属于财务部门的打印机这样的事情,而不是同时返回打印机、用户、联系人以及计算机等等…。(提示:如果您不知道我们在说什么,请阅读第 1 部分。)

那么,应如何返回已在域中发布的打印机呢?您可以使用类似如下的查询:

"SELECT Name FROM 'LDAP://cn=Users,dc=fabrikam,dc=com' WHERE " _ & "objectCategory='printQueue'"

我们在此需要做的工作就是指定 objectCategory - 即,要返回的对象的类型 - 应被限制为 printQueue 对象(已发布打印机的 Active Directory 名称)。以下是一些更常见的对象类别,您在执行搜索时可能会用到它们:

computer

contact

group

inetOrgPerson

organizationalUnit

printQueue(Active Directory 中发布的打印机)

site

siteLink

subnet

user

volume(Active Directory 中发布的共享文件夹)

返回页首返回页首

在查询中使用 AND 和 OR

老实说,没有任何理由一定要搜索 Active Directory。例如,假定您希望获得与 sAMAccountName 为 kenmyer 的某个用户有关的更多信息。您不必搜索 Active Directory;可以编写一个递归脚本,系统地检索 Active Directory 中所有容器中的所有用户名称,并检查每个帐户,看看其 sAMAccountName 属性是否恰巧为 kenmyer。这种方法不仅非常慢,而且非常烦琐,但是它的确很有效。

但是谁喜欢既缓慢又烦琐的方法呢?(我们没有说我们不是缓慢和烦琐的,只是说我们不希望这样。)我们想要更快和更有效率。而实现这一点的一个好方法就是在搜索 Active Directory 时编写非常明确的查询。例如,你想列出 sAMAccountName 为 kenmyer 的所有用户吗?那么可使用以下代码:

"SELECT Name FROM 'LDAP://cn=Users,dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user' AND sAMAccountName = 'kenmyer'"

请注意,我们在这里做的工作:我们在 WHERE 子句中指定了两个不同的条件:我们希望得到一个对象列表,对象的 objectCategory 等于用户,并且 sAMAccountName 等于 kenmyer。只有同时满足这两个条件的数据才会被返回;即对象必须为用户并且它的 sAMAccountName 必须为 kenmyer。这就是我们为何在 WHERE 子句中使用 AND 的原因。如果我们想要列出财务部门担任会计职务的所有用户会怎样呢?在这种情况下,我们的查询看起来可能如下所示:

"SELECT Name FROM 'LDAP://cn=Users,dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user' AND department = 'Finance' AND title = 'Accountant'"

知道它是怎么工作的了吗?我们可以继续在该子句中添加其他字符串项目,但是请确保用 AND 分隔每个项。

当然,有时我们会想在其中包含稍微多一点的东西。比如,假定我们想返回属于 Accounts Payable(可支付帐户)部门或者 Accounts Receivable(可接受帐户)部门的用户。在这种情况下,我们不能使用 AND,因为用 AND 表示所有条件必须为真,这意味着我们的用户需要同时属于 Accounts Payable 和 Accounts Receivable 部门。所以,我们希望返回的数据仅满足其中的一个条件。因此,我们改用 OR:

"SELECT Name FROM 'LDAP://cn=Users,dc=fabrikam,dc=com' WHERE " _ & "department='Accounts Payable' OR department='Accounts Receivable'"

使用这个查询,我们可以获得属于 Accounts Payable Accounts Receivable 部门的所有对象的信息。

为什么我们说“所有对象”,而不说“所有用户”呢?为了让第一个例子尽可能简单,我们删除了查询中的 objectCategory=’user’ 部分。我们现在将它重新添加进来,但是请认真检查 WHERE 子句的结构;毕竟,这一次我们要同时使用 AND OR:

"SELECT Name FROM 'LDAP://cn=Users,dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user' AND " _ & "(department='Accounts Payable' OR department='Accounts Receivable')"

希望您能够注意到,我们使用了圆括号将这两个条件分开:我们只需要作为用户并且属于 Accounts Payable 或 Accounts Receivable 部门的对象。如果您在一个 WHERE 子句同时使用了 AND 和 OR,那么圆括号有助于避免出现意料之外的结果。例如,如果我们去除圆括号,上面这个查询则很难理解:

"SELECT Name FROM 'LDAP://cn=Users,dc=fabrikam,dc=com' WHERE " _ & "objectCategory='user' AND " _ & "department='Accounts Payable' OR department='Accounts Receivable'"

因为,该查询还可以像下面这样理解:选择属于 Accounts Payable 部门的所有用户或者选择 Accounts Receivable 部门的所有对象(不仅仅是用户)。这可能不是我们想要的结果,防止错误理解查询(不论是对于我们还是对于运行该脚本的计算机均是如此)的最好办法就是使用圆括号。

下面是另外一个例子:它会返回属于 Accounts Payable 或 Accounts Receivable 部门的用户计算机。再一次地,请注意这里使用的圆括号:

"SELECT Name FROM 'LDAP://cn=Users,dc=fabrikam,dc=com' WHERE " _ & "(objectCategory='user' OR objectCategory='computer') AND " _ & "(department='Accounts Payable' OR department='Accounts Receivable')"
返回页首返回页首

在 WHERE 子句中指定值

在任何时侯,只要您写一个 WHERE 子句,就需要知道下面(最起码的)两件事情:属性的名称(非常显而易见的事情)以及属性的数据类型。Active Directory 属性有各种不同的表现形式:一些为字符串型的值,一些为数值型的值,还有一些为布尔类型的值。您需要知道属性的数据类型,因为数据类型决定了 WHERE 子句的语法。

为什么这样说呢?让我们看一个简单的 WHERE 子句:

WHERE objectCategory='user' AND department='Finance'

注意,为 objectCategory 和 department 指定的值分别为“‘user’”和“‘Finance'”,它们都包含在一对单引号内;这是因为 objectCategory 和 department 都是字符串类型的属性。只要使用字符串类型的属性,都需要用一对单引号标记将指定的值括起来。想返回 surname (SN) 为 Smith 的所有用户的列表吗?那么 WHERE 子句看起来应如下所示:

WHERE objectCategory='user' AND SN='Smith'

在这里,属性仍然不是数值或布尔数据类型。例如,假定要搜索域中的所有全局组。全局组的 groupType 值为 2;因此您的 WHERE 子句应如下所示:

WHERE objectCategory='group' AND groupType=2

看明白了吗?2 这个值的两边没有单引号。对于布尔数据类型的属性也是如此。需要列出彩色打印机吗?对于任何能够执行彩色打印工作的打印机,其 printColor 属性的值均为 TRUE:

WHERE objectCategory='printQueue' AND printColor=TRUE

我们可再次注意到这里的 TRUE 值没有用单引号括起来。单引号仅用于字符串数据。

供出真相。好了,我们一直隐瞒了下面这个事实:在处理具有位掩码属性的日期时,事情就变得有些不那么清楚了。但是,由于本文是一篇介绍性的文章,所以我们假装某些其他数据类型不存在就好了。我们将在以后的文章中讨论这些其他的数据类型。

返回页首返回页首

在 WHERE 子句中使用变量

我们感觉到有些人肯定会问这个问题。我们的所有例子都使用了下面这样的硬编码值,也就是说在脚本中显式地包含了部门的名称:

WHERE objectCategory='user' AND department='Finance'

对于教学目的来说这很有帮助,但是正如我们知道的,什么时侯教学活动对现实生活有过帮助呢?我们所做的就是创建一个脚本,仅仅搜索以下一项内容:属于 Finance 部门的用户。在现实生活中,您可能想要一个能够搜索任何部门中的用户的脚本,并且可在命令行参数中指定要搜索的部门。我们现在不会讨论命令行参数;如果您想了解有关它的更多信息,请参见这篇脚本故事专栏。我们假设您用某种方法成功地将要搜索的部门保存在一个名为 strDepartment 的变量之中。现在,您应该如何在 WHERE 子句中使用该变量呢?

不,很抱歉,以下语句不能工作:

WHERE objectCategory='user' AND department='strDepartment'

为什么呢?因为在这种情况下,脚本会搜索 name 等于 strDepartment 的部门,所以可能不会返回任何数据。下面的语句同样不能工作:

WHERE objectCategory='user' AND department=strDepartment

为什么呢?应当记住,department 为字符串数据类型;因此它需要一个字符串值 - 由单引号括起来的数据。这里没有单引号,因此会得到下面这样的含义模糊的错误信息:

提供程序:在处理命令时发生了一个或多个错误。

是的,这个信息相当有帮助,不是吗?它告诉我们的是这样一个事实:脚本不知道 department=strDepartment 是什么意思。所以,它会简单地放弃执行。

我们不再和您捉迷藏了;下面的 WHERE 子句可以正常工作:

WHERE objectCategory='user' AND department='" & strDepartment & "'

不管您信还是不信,它的确管用。我们需要将一个字符串值传递给 department,并且需要该字符串值以一个单引号标记开始。这就是我们实际拥有的东西:

WHERE objectCategory='user' AND department='

如果我们用硬编码的方式写入一个值,我们可以在这个单引号后面紧跟上这个值。不过,我们不是要用硬编码的方式添加值;我们要使用一个变量。因此,我们临时中止硬编码文本;这就是双引号的作用。然后,我们使用 & 符号将 strDepartment 变量的值添加到子句。在处理脚本的时侯(假定 strDepartment 等于 Finance),计算机会按如下方式读取 WHERE 子句:

WHERE objectCategory='user' AND department='Finance

为什么?因为它已经用实际值 Finance 替换了变量 strDepartment。随后,我们只需添加结束的单引号标记即可;这就是第二个 & 符号、第二个双引号(它表示:“OK,现在我们已经用变量的内容进行了替换,接下来继续使用硬编码文本”)以及第二个单引号的作用。

如果您不太明白上述逻辑,不用太过担心;您可以获得更为详细一些的解释,方法是单击这里。 只需遵照上面的模式就可以得到满意的结果。

如果是使用布尔值或数值,也照此办理,但是不应该使用单引号。因此为:

WHERE objectCategory='printQueue' AND printColor=" & blnColor & "
返回页首返回页首

在 WHERE 子句中使用运算符

迄今为止,我们仅仅为您介绍了在 WHERE 子句中使用等号运算符的 SQL 查询;例如:

WHERE objectCategory='user' AND department='Finance'

如果要获取 Finance 部门中所有用户的列表,这种方法非常方便。但是,如果要获取不属于 Finance 部门的用户的列表应该怎样做呢?可以写一个不使用等号运算符的查询吗?

没错:如果我们没有问题的答案,那么是不会向您提出问题的。您可以这样做;事实上,WHERE 子句中可以使用以下任意运算符:

运算符

描述

=

等于

<>

不等于

>

大于

<

小于

>=

大于或等于

<=

小于或等于

想获取不属于 Finance 部门的用户的列表吗?只要使用一个查询,在查询中说明“为我提供所有 department 属性不等于 Finance 的用户”就可以了。该查询的 WHERE 子句大致如下所示:

WHERE objectCategory='user' AND department<>'Finance'
返回页首返回页首

在 WHERE 子句中使用通配符

如果您正在想,“嘿,如果他们继续为我展示有关 Active Directory 查询的更多内容,我简直要发疯了!”,您可能想就此停止读下去了。但是,听着,您没有任何理由发疯:在 WHERE 子句中使用通配符是我们今天要讨论的最后一项内容。

对,我们知道:我们已经在本专栏文章里涉及了太多的内容。幸运的是,本部分很简短,因为实际上只有一个通配符需要对付:星号 (*) 代表“所有内容”。例如,假设您所搜索的用户的姓为...(老实说,您实际上根本不记得该用户的姓是什么)。它好像是 Smith 或 Smythe 或 Smithson,或与它们类似的姓。在这种情况下应该怎么办?

好的,在这种情况下,可以使用通配符搜索 SN 以字母 SM 开头的所有用户。下面的查询将返回其 SN 以字母 SM 开头的所有用户:

WHERE objectCategory='user' AND SN='Sm*'

请注意通配符的位置。如果我们想将返回的数据限制到 Smith、Smithson 或其他以 SMITH 开头的任何名称,可以使用如下的查询:

WHERE objectCategory='user' AND SN='Smith*'

它会返回我们想要的所有用户。您还可以在字符串的开头使用通配符。下面的查询会返回 Smithson、Johnson、Thompson 以及姓以 SON 结尾的所有其他用户:

WHERE objectCategory='user' AND SN='*son'

您甚至还可以在字符串的中间使用通配符;但是,这样会显著降低域控制器的性能,所以我们不建议这样做。

在试图找出是否存在某些东西的时侯,通配符特别有用。例如,以下查询将返回具有电话号码的所有用户的列表,而不管其电话号码究竟为多少:

WHERE objectCategory='user' AND telephoneNumber='*'

像这样简单指定了 * 的搜索非常方便,尤其是在您审核 Active Directory 并且需要列出所有没有部门、职务或其他属性的用户的时侯。例如,下面的查询将返回没有工作职务的所有用户的集合:

WHERE objectCategory='user' AND title<>'*'

请注意,我们这一次使用了 <>(不等于)运算符。

看,情况并不算太坏,是吗?

返回页首返回页首

那么,我们现在已经了解了有关 Active Directory 搜索的所有知识,对吗?

是的,不过这么说并不准确。您现在已经有了足够的起步知识,可以开始执行大多数搜索了;但是,这并不意味着我们已经掌握了所有内容。我们还没有讨论如何搜索具有位掩码的属性或具有多值的属性以及如何执行引用跟踪 - 好的,您知道有这些东西就行了。如果您对这些额外的话题感兴趣,请写信给我们,地址为 scripter@microsoft.com如果大家都对这些话题感兴趣,我们就会考虑做点什么了。不过,要是用 Lloyd Alexander 的话来说,则应该是:在有些时侯,不学习所有东西比学习所有东西能够学习到更多东西。

好了,其实我们也不知道这话究竟是什么意思。但是,在本专栏结束的时侯,我们必须有某些容易让人记住的口号,上面这个是我们能够想出来的最好的口号了。不过,这并不是我们的问题:如果 Lloyd Alexander 能够说一些更聪明的话,那么我们也就不会被逼无奈想出这样一个口号了!


返回页首返回页首