|
Andre Guirard
EI 产品开发人员, IBM
2005 年 7 月 13 日
本文介绍了如何在 Lotus Connectors LotusScript Extension (LCLSX) 中使用文件附件,说明了 LCLSX 如何处理文件附件,并通过一个示例应用程序介绍了文件附件的处理。
Lotus Connectors LotusScript Extension (LCLSX) 很容易实现 Notes 数据库和关系数据库之间简单字段的同步,文本、日期和数值字段都没有问题。甚至可以把富文本作为普通文本存储,或者保存在关系数据库中的 BLOB 字段(虽然富文本字段的二进制编码只对 Lotus Notes/Domino 有意义)中。
但是,文件附件更复杂一些。无论 Notes 原生类还是 LCLSX 都不支持直接访问文件附件中的数据。从 Lotus Notes 中提取文件附件的唯一方式就是将其分离到磁盘上,也只能从磁盘文件创建文件附件。不过在处理 Notes 数据时,LCLSX 可以自动完成文件附件的分离和附加,从而使事情更容易一些。
本文详细介绍了一个示例应用程序,向关系数据库复制或者从中取出 Notes 文件附件。我们假设您非常熟悉 LCLSX 的使用和术语。否则请参阅 IBM 红皮书 Implementing IBM Lotus Enterprise Integrator 6,该书中有一章介绍 LCLSX,通过很多例子详细介绍了这一技术。
LCLSX 中文件附件的一般原理
LCLSX 的 Notes 连接程序包含以下用于处理文件附件的属性:
- LoadFile 是一个 Boolean 属性,设为 True 时将在 Select 方法返回的结果集中增加一个特殊的字段 FILE。此外,对 Notes 文档执行 Fetch 操作时,该文档的所有文件附件都被复制到本地文件目录中,而 FILE 字段包含其文件名(是一个多值列表)。更新或插入 Notes 文档时,FILE 字段中列出的所有文件都将附加到 Notes 文档中。
- FilePath 是一个字符串值,如果 LoadFile 为 True,它定义了提取 Notes 文档时分离出来的附件所在目录的文件路径。如果保留为空值,则使用当前目录。不要将该字段设为空值!当前目录通常就是 Notes 程序目录,包含大量文件,可不希望意外地被同名的附件覆盖。
- CopyFile 是一个 Boolean 属性,若为 True,则在 Select 方法返回的结果集字段列表中增加一个特殊字段 LCXFILE。如果使用该字段列表插入或者更新另一个 Notes 文档,将把所有附件复制到新文档中。
本文中将使用前两个属性。CopyFile 仅用于在两个 Notes 文档之间传递附件,而非用于 Lotus Notes 和关系数据库。
由此可得到将 Notes 附件数据存入文件的一种方法。下一步是获得保存在关系数据库二进制字段中的文件内容。为此,必须将文件数据放入一个 LCField 对象,可以使用工具 File 连接程序。File 连接程序可以从文件系统中访问数据,就像是来自关系数据库。每个文件用结果集中的一行表示,每个目录表,无论使用什么样的操作系统都包含以下四列:
- Filename 是不包括完整文件路径的文件名。
- Contents 是文件中的数据,可以是二进制形式也可以是文本,取决于连接程序的 Binary 属性设置。这里我们希望使用二进制格式,从而能够保留附件的原始内容。
- Timestamp 表示文件最后修改的时间。
- Size 是文件大小,以字节为单位。
从 Lotus Notes 导出的主要过程就是使用 Notes 连接程序从 Notes 文档中 Fetch 数据,连接程序自动将每个附件分离成一个文件。使用 File 链接程序将这些文件读入内存和某种关系连接程序,将文件数据保存到二进制列中。另一方面,也可以向 File 连接程序插入记录,后者自动创建文件。
无论导入还是导出,建立连接的语句基本上是一样的。下面就是建立 Notes 连接程序的语句(读写都是一样的)。
Dim lcconNotes As New LCConnection("notes")
lcconNotes.server = db.server
lcconNotes.database = db.filepath
lcconNotes.metadata = "Villain"
lcconNotes.FilePath = containing_folder + "\" + foldername
lcconNotes.LoadFile = True
|
假设您使用 Windows 操作系统,因为我们使用“\”作为目录分隔符。接下来,按以下方式设置 File 连接程序:
Dim lcconFiles As New LCConnection("file")
lcconFiles.Database = containing_folder包含脚本
lcconFiles.Metadata = foldername
lcconFiles.Binary = True
|
最后一行规定文件数据是二进制形式,File 连接程序不再尝试将字节解释成字符。上述代码中,用户可以使用定制类创建临时目录,执行完成后自动删除。
示例应用程序
可以下载一个示例 Notes 数据库,其中包含我们讨论的代码。该应用程序叫做“Dick Tracy Crime Files”。Tracy 是一位著名的警探,Lotus 产品的长期用户。他创建这个数据库来追踪已知的罪犯。罪犯的照片作为附件以 JPEG 格式保存。我们需要将这些 Notes 数据和关系数据库结合起来,在关系数据库中 JPEG 保存在二进制字段中。
Notes 文档 Villain 包含字段 ID,ID 适用于和其他关系表联系的唯一关键字。要更新的表包括三列:
- VillainID,应该拷贝 Lotus Notes 中的 ID 字段。一个坏蛋可能有多幅照片,因此该列的值不是唯一的。
- Filename 包含 JPEG 文件的原来名称。VillainID 和 Filename 联结在一起应该是唯一的。
- Filedata 是二进制文件的内容。
该脚本中还包括其他 Notes 字段和其他表的同步,但我们只关心文件附件。
准备运行示例脚本
可以从 Notes 客户机中运行脚本。当然也需要一个关系数据库来导入导出。任何数据库都行,Microsoft Access 就足够了。如果需要的表不存在,示例脚本将自动创建,只要登录的关系数据库具有这种访问权限。
写的时候,脚本从 Domino 服务器上的 DECS(或 LEI) Administrator 数据库(decsadm.nsf)读取连接信息。如果愿意,该数据库可以是本地的。不需要运行 DECS 或 LEI,但是必须有 decsadm.nsf 数据库。可以使用 DECS Administrator 模板(decsadm.ntf)创建该数据库,如果愿意的话也可以在脚本库中硬编码连接信息。无论哪种情况,如果需要编辑这个脚本库,其名称为 Customize Connections。在 (Options) 段中,可以找到几个 Const 声明,适当调整这些参数值告诉脚本到哪里寻找连接信息:
- YOUR_CONNECTION_DOC 包含了脚本将在 DECS Administrator 数据库中搜索的 Connection 文档名。
- VILLAINS_MAIN、VILLAINS_CRIMES 和 VILLAINS_ATTACHMENTS 包含导出数据的三个表的名称。
- YOUR_DECS_SERVER 包含 DECS Administrator 数据库所在的服务名。本地数据库可以将该参数设为空值。
如果希望硬编码关系数据库类型、名称、ID 和口令,请参阅脚本中的各个模块看看如何定制。
定制该库之后,创建在 YOUR_CONNECTION_DOC 中输入的 Connection 文档,这样就准备好了。
运行示例脚本
在 Notes 客户机中打开数据库,然后打开 Villains,按 ID 查看。其中包括四个示例文档。Actions 菜单列出了带有编号的代理,应该按照顺序运行。
1:创建/清除表 这个代理使用 Action 方法和参数 LCACTION_TRUNCATE 删除关系表中的所有数据。如果表不存在,则该脚本创建它。如果感兴趣,可以观察该脚本看看如何用 LCLSX 创建表,但这不是本文的目的,不再详细介绍。
2:导出 Villains 到 RDB 这个代理实现了前面讨论的导出过程。该脚本中性能不是大问题,因为只有四个文档,但是作为一个好的例子(您的应用程序中性能可能很重要),我们遵循了最佳实践:首先建立要复制的字段之间的关系,这样复制就能自动进行,不需要在主程序循环中把数据从一个字段移到另一个字段。
这一步是通过创建包含对同一些字段对象的引用的字段列表实现的。LCField 是一个单独的对象,可以被多个字段列表引用,可以使用不同的名称。图 1 中,矩形代表一个 字段列表元素,椭圆则表示一个 LCField 对象。中间的连线说明字段列表元素引用了哪些 LCField 对象。
图 1. 字段列表

进行这类编程时,建议您最好画一个图。事情不会总这么简单,常常要复杂得多。这样设置字段列表后,就可以从 Notes 数据库中读取文档,需要写入其他两个表的数据已经存在于插入关系数据库的字段列表中。读取 Notes 记录时自动创建的文件可以通过 File 连接程序来读取,数据已经存在于插入关系数据库附件表的字段列表中。
下面是创建这些互相联系的字段列表的代码:
Call lcconNotes.Select(Nothing, 1, flNotes) ' select all docs, init flNotes
Call lcconFiles.Select(Nothing, 1, flFiles) ' init flFiles (no files yet)
Call flDest.MapName(flNotes, "ID,Name,Location,Body", "VillainID,Name,Hideout,Comments")
Set fldID = flDest.Lookup("VillainID")
Call flDestAtt.MapName(flFiles, "Filename,Contents", "Filename,Filedata")
Call flDestAtt.IncludeField(3, fldID, "VillainID")
Call flCrimes.MapName(flNotes, "ID,Crimes", "VillainID,Crime")
Set fldCrimes = flNotes.Lookup("Crimes")
|
两个 Select 语句创建两个源字段列表的字段对象。我们要读取所有 Notes 文档,因此第一个 Select 有双重目的。但是临时目录中还没有任何文件,因此对于 File 连接程序来说 Select 仅用于初始化字段列表。
下面这些语句创建对同一些字段的引用,将其插入其他字段列表。除此之外,导出程序的主循环非常简单:
Do While lcconNotes.Fetch(flNotes) > 0
' The Fetch has also detached all attachments into
' the temp directory.
Call lcconDest.Insert(flDest) ' create main villain record.
Call lcconDestCrimes.Insert(flCrimes) ' insert multiple crimes
' rows in one operation.
Call lcconFiles.Select(Nothing, 1, Nothing) ' see what files are
' in the temp directory.
Do While lcconFiles.Fetch(flFiles)
Call lcconDestAtt.Insert(flDestAtt) ' insert one file in attachment
' table.
Loop
Call lcconFiles.Action(LCACTION_TRUNCATE) ' get rid of the
' attachment files.
Loop
|
对字段列表 flNotes 的 Fetch 操作还要在临时目录中创建文件。脚本从 File 连接程序中选择得到这些文件的列表,并读入其内容。注意 Select 语句的第三个参数是 Nothing。通常应使用空的字段列表以便填充结果集中的字段,但是这里已经有需要从结果中提取的字段列表,因此没有必要另建一个。事实上这样要比新建一个好,因为这个字段列表已经连接到输出字段列表。
该程序中值得注意的几点:
- 有三个 LCConnection 对象指向目标数据库,对应三个表。main 和 attachment 表的连接程序直接连接到数据库,到 crimes 表的连接则通过 Collapse/Expand 元连接程序,后者自动实现 Notes 多值字段和一对多关系表中多行之间的转换。最后一点不在我们的讨论范围之内,如果有兴趣可以看看代码是如何实现的。
- Notes 字段列表包含一个 FILE 字段,其中保存了所有分离出来的文件的完全路径列表。但是,导出过程中不需要该字段。我们直接通过 File 连接程序从磁盘上获得文件名和内容,这种方法得到的信息是一样的。如果目录中还包括其他文件,情况就不同了,不过我们专门创建了一个临时目录,因此不用担心。
- 虽然提取 Notes 文档能够自动将文件附件复制到磁盘上,但是用完之后去没有办法自动删除附件。循环的最后,Action 方法通过删减包含目录内容的表,从临时目录中删除所有文件。
- 我们已经将 Notes 文档中的 Body 富文本映射到关系数据库中的二进制 BLOB 字段。这样就把富文本字段中的二进制图片复制到 BLOB 字段中。这些专有格式数据是不能用的,除非再导入 Lotus Notes。虽然编辑表单时文件附件的图标可能出现在富文本字段中,附件是和富文本分别存放的,就是说复制富文本并没有复制附件。不过这也提供了备份富文本中出现的内嵌图像和其他附件的一种方法,并且可以在以后恢复到 Lotus Notes 中。
- 内部循环检索临时目录中发现的所有附件并将其插入附件关系数据表中,使用 VillainID 和文件名的组合作为记录的唯一标识符。在内部,Notes 仅指同一文档中有重复的附件名,并对不唯一的附件名重新命名,因此重复的键不是一个问题。
- 主循环执行过程中,使用自定义函数 DebugStr 创建写入磁盘的数据的字符串描述。上面的代码中省略了调试输出的语句,因此只能看到转移数据的功能。
3. 从 RDB 导入 下一个代理的功能和上一个恰好相反:用关系数据库数据中的文件附件创建新的 Notes 文档。由于我们不希望删除源数据记录,所以创建第二个 Notes 表单(和 Villain 表单类似,但称为 Bad Guy),这也是导入脚本创建的文档类型。
由于必须从关系数据库中的数据创建文件,所以使用来自主 Villains 关系表中的键从附件关系表中选择。通过向 File 连接程序插入来使用这些数据在磁盘上创建文件。然后再插入 Notes 连接程序,该操作从磁盘上抓取文件自动创建 Notes 文件附件。
不过,使用 Notes 连接程序的 LoadFile 属性读和写有一个重要的区别。读的时候可以忽略 Notes 字段列表中的 FILE 字段,察看创建了哪些文件。但是写的时候,必须使用 FILE 字段告诉 Lotus Notes 要附加的每个文件的完整路径,而不是简单地选择目标目录的中的所有文件。
FILE 字段很特殊,除了名称之外,还有专门的虚拟字段代码与之关联,告诉 Notes 连接程序要特殊对待。这样就可以区分带有附件列表功能的字段和恰好命名为 FILE 的普通字段。Notes 连接程序使用的几个特殊字段名都有自己的虚拟标志。
注意: 不应该混淆虚拟字段标识和 DECS/LEI Virtual Field 活动,后者从关系数据库中提取实时信息。虽然名称类似,但是完全不同的概念。
在进入主循环之前,导入脚本构造了一些相互联系的字段列表,和前面的代理非常类似,包括特殊的 FILE 字段。(画出这些字段的关系图留给读者作为练习。)如前所述,建立和表匹配的字段列表最简单的办法就是使用 Select 检索表的描述。如果列表中包含特殊字段,如多值字段和虚拟字段,情况更是如此。不过,对于一个富有启发性的例子,如果能用较麻烦的办法来做,何必使用最简单的办法呢?下面是从头创建自己的 FILE 虚拟字段的代码:
Set fldFiles = flNotes.Append("FILE", LCTYPE_BINARY)
该字段属于二进制类型,但不是 BLOB 字段。二进制字段用于存储特殊的 Notes 字段值,如富文本字段和多值字段。
Call fldFiles.SetFormatStream( , , LCSTREAMFMT_TEXT_LIST)
这样就把该二进制字段类型设为一个多值 Notes 文本字段。
Call fldFiles.SetVirtualCode(lcconNotes.GetPropertyInt(LCTOKEN_CONNECTOR_CODE))
SetVirtualCode 函数需要一个参数,说明希望哪一个连接程序或者连接特殊对待该字段。不同的连接程序可能有不同的它们认为特殊的字段名,上面的语句让我们指定该字段对 Lotus Notes 是特殊的,而非其他连接程序。这是让我们访问不同后端的特殊功能的一种办法。这里设定的标志告诉 Lotus Notes 要特别注意该字段。
每个 Notes 连接的 Connector 代码属性返回相同的数值。但是 Connection 代码对于每个 LCConnection 对象是唯一的。通过设置属于连接而非连接程序的虚拟代码,可以用一个 Notes 连接从 Notes 文档加载一个名称列表(将 FILE 看作一个简单的多值字段),然后将该字段连接到和其他连接关联的字段列表,该链接将其视作特殊的 FILE 字段并从磁盘上找到那些文件。FILE 字段需要包含需要附加的每个文件的完整路径,而不仅仅是文件名。
导入函数的主循环必须为从关系数据库中读取的每个 Villain 记录构造这样一个列表,代码如下:
Do While lcconVillains.Fetch(flVillains) > 0
Call lcconCrimes.Select(flVillains, 1, Nothing) ' read thru metaconnector,
' so there's always at most one result.
If lcconCrimes.Fetch(flCrimes) = 0 Then
fldCrimes.Value = "" ' if they have no crimes, erase value left over
' from previous iteration.
End If
strFilenames = ""
Call lcconAttach.Select(flVillains, 1, Nothing)
' Loop thru list of attachment relational records associated with
' current key, and detach each file to temp dir.
Do While lcconAttach.Fetch(flAttach)
Call lcconFiles.Insert(flFiles) ' copy the file contents into a
' disk file. Build a delimited string of the full filepaths of the
' files we detach.
strFilenames = strFilenames & NEWLINE & dirTemp.fullpath &
"\" & fldFilename.Text(0)
Loop
' If there are attachments, construct a string that contains a multivalue
' where each value is the full path of one file.
If strFilenames <> "" Then
lcstrFilenames.Value = Split(Mid$(strFilenames, 2), NEWLINE)
Else
Call lcstrFilenames.Clear
End If
Call fldFiles.SetStream(1, lcstrFilenames) ' set FILE multivalue field
' to list of files to attach.
Call lcconNotes.Insert(flNotes)
Call lcconFiles.Action(LCACTION_TRUNCATE) ' get rid of temp
' attachment files.
Loop
|
变量 lcstrFilenames 是一个 LCStream 对象。LCStreams 是用于保存所有文本和二进制 LCField 对象的对象。该对象用得不多,但是如果需要直接处理二进制字段如 Notes 多值字段的数据,会非常方便。可以直接赋予多值字段的文本,但是 LCStream 类将其分解成多个值,是用逗号作为分隔符,因此只能用于不含逗号的数据值。
注意以下几点:
- 字段列表 flVillains 中的 VillainID 字段被设置成一个键(LCFIELDF_KEY)。向字段列表中加载数据的 Fetch 操作忽略该标志,但是这样可以将 flVillains 作为选择条件参数来 Select 相关的犯罪和附件记录。
- 当从两个相关的表中选择时,我们使用 Nothing 作为第三个参数。我们不需要在 Select 过程中创建结果集字段列表,因为已经建立了。而且它和其他字段列表共享字段,因此无论如何都比新建一个列表更方便。
- 表达式 dirTemp.fullpath 指向自定义类的一个属性,该类用于创建临时目录然后自动删除。这个类在其他脚本中也很有用。该属性返回临时目录的完整路径。
- 再说一次,我们使用“\”作为目录分隔符,仅适用于 Windows 操作系统。
- 指定 FILE 字段时不一定要用完整的文件路径。可以将 Notes 当前目录(Curdir)设为临时目录,然后使用文件名。不过,设置 Curdir 不是很安全,因为 (1) 同一进程中可能还有其他脚本运行, (2) 这些脚本可能也会设置 Curdir(或者认为该变量具有默认值)。
- 不幸的是,设置 Notes 连接程序的 FilePath 属性,如果 FILE 值不规定一个路径,并不能让它在该目录中查找文件。
- 该脚本也使用了 Collapse/Expand 元连接程序,用于将多个关系行转化成一个 Notes 多值字段 Crimes。
现实生活中的同步
上面的脚本都很简单。在现实生活中,应用程序常常要比把数据从一个数据库转储到另一个数据库复杂得多。更常见的情况是同步不同数据源中的数据。
当然,如果数据转移是一种办法,也可以删除目标数据库中的所有数据,然后从源数据库中将全部内容复制过去,这种办法称为懒汉法。在某些应用程序中,这种办法的确可行,但是有几方面的因素使其不那么好:
- 很多数据库逐渐地会增加各种定制的程序化的事件代码,当增加、删除或修改记录时就被触发。删除一条记录再增加一条类似的记录,可能触发不必要的处理过程,得到不满意的结果。
- 如果目标数据库是 Lotus Notes,转储和重建所有 Notes 文档还有几个不利之处:
- 今天建立的 doclinks 明天就不能用了。
- 删除会留下残余,这增加了数据库的大小,增加了复制和建立索引的实践,通常会影响性能,特别是如果其中大部分是活动文档的话。
- 未读标志丢失了。
- Domino 服务器也可能有基于事件的处理过程,如新建或修改文档时运行的 API 插件和代理。
- 其他类型的数据库也可能因为不必要的数据活动而影响性能。
为了帮助解决这一问题,实例数据库中还包含一个代理在两个 LCConnection 之间执行简单的、单向的、基于关键字的同步。代理“5. Replicate Pirates”包括一个可重用的引擎,每次从两个结果集中读取一个记录,比较其关键字和数据值决定两个记录是否存在差异,如果有的话,看看它表示的是插入、更新还是删除。然后根据情况从一个或两个结果集中读取下一个记录。如何为该引擎提供经过同样排序的两个结果集取决于用户,如果使用不同的连接程序可以需要使用 Order 元连接程序。此后处理就可以自动进行了(至少对于简单字段类型)。
如何将这种技术与需要完成的文件附件操作结合起来可以有不同的办法。不过,虽然可以使用 LCStream 对象比较两个文件附件的二进制数据是否相同,但是比较时间要快得多。
结束语
本文描述了如何在 LCLSX 中处理文件附件。简要介绍 LCLSX 如何处理文件附件之后,详细讨论了一个示例应用程序来示范文件附件的处理,最后探讨了现实世界中同步技术的一些问题。
我们希望本文对您有所帮助,如果是这样,建议您下载我们提供的例子,然后根据具体的需要和要求进行调整。祝您好运! |