|
AndreGuirard
EI产品开发人员,IBM
2005年7月13日
本文介绍了如何在LotusConnectorsLotusScriptExtension(LCLSX)中使用文件附件,说明了LCLSX如何处理文件附件,并通过一个示例应用程序介绍了文件附件的处理。
LotusConnectorsLotusScriptExtension(LCLSX)很容易实现Notes数据库和关系数据库之间简单字段的同步,文本、日期和数值字段都没有问题。甚至可以把富文本作为普通文本存储,或者保存在关系数据库中的BLOB字段(虽然富文本字段的二进制编码只对LotusNotes/Domino有意义)中。
但是,文件附件更复杂一些。无论Notes原生类还是LCLSX都不支持直接访问文件附件中的数据。从LotusNotes中提取文件附件的唯一方式就是将其分离到磁盘上,也只能从磁盘文件创建文件附件。不过在处理Notes数据时,LCLSX可以自动完成文件附件的分离和附加,从而使事情更容易一些。
本文详细介绍了一个示例应用程序,向关系数据库复制或者从中取出Notes文件附件。我们假设您非常熟悉LCLSX的使用和术语。否则请参阅IBM红皮书ImplementingIBMLotusEnterpriseIntegrator6,该书中有一章介绍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文档之间传递附件,而非用于LotusNotes和关系数据库。
由此可得到将Notes附件数据存入文件的一种方法。下一步是获得保存在关系数据库二进制字段中的文件内容。为此,必须将文件数据放入一个LCField对象,可以使用工具File连接程序。File连接程序可以从文件系统中访问数据,就像是来自关系数据库。每个文件用结果集中的一行表示,每个目录表,无论使用什么样的操作系统都包含以下四列:
Filename是不包括完整文件路径的文件名。
Contents是文件中的数据,可以是二进制形式也可以是文本,取决于连接程序的Binary属性设置。这里我们希望使用二进制格式,从而能够保留附件的原始内容。
Timestamp表示文件最后修改的时间。
Size是文件大小,以字节为单位。
从LotusNotes导出的主要过程就是使用Notes连接程序从Notes文档中Fetch数据,连接程序自动将每个附件分离成一个文件。使用File链接程序将这些文件读入内存和某种关系连接程序,将文件数据保存到二进制列中。另一方面,也可以向File连接程序插入记录,后者自动创建文件。
无论导入还是导出,建立连接的语句基本上是一样的。下面就是建立Notes连接程序的语句(读写都是一样的)。
DimlcconNotesAsNewLCConnection("notes")lcconNotes.server=db.serverlcconNotes.database=db.filepathlcconNotes.metadata="Villain"lcconNotes.FilePath=containing_folder "\" foldernamelcconNotes.LoadFile=True
|
假设您使用Windows操作系统,因为我们使用“\”作为目录分隔符。接下来,按以下方式设置File连接程序:
DimlcconFilesAsNewLCConnection("file")lcconFiles.Database=containing_folder包含脚本lcconFiles.Metadata=foldernamelcconFiles.Binary=True
|
最后一行规定文件数据是二进制形式,File连接程序不再尝试将字节解释成字符。上述代码中,用户可以使用定制类创建临时目录,执行完成后自动删除。
示例应用程序
可以下载一个示例Notes数据库,其中包含我们讨论的代码。该应用程序叫做“DickTracyCrimeFiles”。Tracy是一位著名的警探,Lotus产品的长期用户。他创建这个数据库来追踪已知的罪犯。罪犯的照片作为附件以JPEG格式保存。我们需要将这些Notes数据和关系数据库结合起来,在关系数据库中JPEG保存在二进制字段中。
Notes文档Villain包含字段ID,ID适用于和其他关系表联系的唯一关键字。要更新的表包括三列:
VillainID,应该拷贝LotusNotes中的ID字段。一个坏蛋可能有多幅照片,因此该列的值不是唯一的。
Filename包含JPEG文件的原来名称。VillainID和Filename联结在一起应该是唯一的。
Filedata是二进制文件的内容。
该脚本中还包括其他Notes字段和其他表的同步,但我们只关心文件附件。
准备运行示例脚本
可以从Notes客户机中运行脚本。当然也需要一个关系数据库来导入导出。任何数据库都行,MicrosoftAccess就足够了。如果需要的表不存在,示例脚本将自动创建,只要登录的关系数据库具有这种访问权限。
写的时候,脚本从Domino服务器上的DECS(或LEI)Administrator数据库(decsadm.nsf)读取连接信息。如果愿意,该数据库可以是本地的。不需要运行DECS或LEI,但是必须有decsadm.nsf数据库。可以使用DECSAdministrator模板(decsadm.ntf)创建该数据库,如果愿意的话也可以在脚本库中硬编码连接信息。无论哪种情况,如果需要编辑这个脚本库,其名称为CustomizeConnections。在(Options)段中,可以找到几个Const声明,适当调整这些参数值告诉脚本到哪里寻找连接信息:
YOUR_CONNECTION_DOC包含了脚本将在DECSAdministrator数据库中搜索的Connection文档名。
VILLAINS_MAIN、VILLAINS_CRIMES和VILLAINS_ATTACHMENTS包含导出数据的三个表的名称。
YOUR_DECS_SERVER包含DECSAdministrator数据库所在的服务名。本地数据库可以将该参数设为空值。
如果希望硬编码关系数据库类型、名称、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连接程序来读取,数据已经存在于插入关系数据库附件表的字段列表中。
下面是创建这些互相联系的字段列表的代码:
CalllcconNotes.Select(Nothing,1,flNotes)''selectalldocs,initflNotesCalllcconFiles.Select(Nothing,1,flFiles)''initflFiles(nofilesyet)CallflDest.MapName(flNotes,"ID,Name,Location,Body","VillainID,Name,Hideout,Comments")SetfldID=flDest.Lookup("VillainID")CallflDestAtt.MapName(flFiles,"Filename,Contents","Filename,Filedata")CallflDestAtt.IncludeField(3,fldID,"VillainID")CallflCrimes.MapName(flNotes,"ID,Crimes","VillainID,Crime")SetfldCrimes=flNotes.Lookup("Crimes")
|
两个Select语句创建两个源字段列表的字段对象。我们要读取所有Notes文档,因此第一个Select有双重目的。但是临时目录中还没有任何文件,因此对于File连接程序来说Select仅用于初始化字段列表。
下面这些语句创建对同一些字段的引用,将其插入其他字段列表。除此之外,导出程序的主循环非常简单:
DoWhilelcconNotes.Fetch(flNotes)>0"TheFetchhasalsodetachedallattachmentsinto"thetempdirectory.CalllcconDest.Insert(flDest)''createmainvillainrecord.CalllcconDestCrimes.Insert(flCrimes)"insertmultiplecrimes"rowsinoneoperation.CalllcconFiles.Select(Nothing,1,Nothing)"seewhatfilesare"inthetempdirectory.DoWhilelcconFiles.Fetch(flFiles)CalllcconDestAtt.Insert(flDestAtt)"insertonefileinattachment"table.LoopCalllcconFiles.Action(LCACTION_TRUNCATE)"getridofthe"attachmentfiles.Loop
|
对字段列表flNotes的Fetch操作还要在临时目录中创建文件。脚本从File连接程序中选择得到这些文件的列表,并读入其内容。注意Select语句的第三个参数是Nothing。通常应使用空的字段列表以便填充结果集中的字段,但是这里已经有需要从结果中提取的字段列表,因此没有必要另建一个。事实上这样要比新建一个好,因为这个字段列表已经连接到输出字段列表。
该程序中值得注意的几点:
有三个LCConnection对象指向目标数据库,对应三个表。main和attachment表的连接程序直接连接到数据库,到crimes表的连接则通过Collapse/Expand元连接程序,后者自动实现Notes多值字段和一对多关系表中多行之间的转换。最后一点不在我们的讨论范围之内,如果有兴趣可以看看代码是如何实现的。
Notes字段列表包含一个FILE字段,其中保存了所有分离出来的文件的完全路径列表。但是,导出过程中不需要该字段。我们直接通过File连接程序从磁盘上获得文件名和内容,这种方法得到的信息是一样的。如果目录中还包括其他文件,情况就不同了,不过我们专门创建了一个临时目录,因此不用担心。
虽然提取Notes文档能够自动将文件附件复制到磁盘上,但是用完之后去没有办法自动删除附件。循环的最后,Action方法通过删减包含目录内容的表,从临时目录中删除所有文件。
我们已经将Notes文档中的Body富文本映射到关系数据库中的二进制BLOB字段。这样就把富文本字段中的二进制图片复制到BLOB字段中。这些专有格式数据是不能用的,除非再导入LotusNotes。虽然编辑表单时文件附件的图标可能出现在富文本字段中,附件是和富文本分别存放的,就是说复制富文本并没有复制附件。不过这也提供了备份富文本中出现的内嵌图像和其他附件的一种方法,并且可以在以后恢复到LotusNotes中。
内部循环检索临时目录中发现的所有附件并将其插入附件关系数据表中,使用VillainID和文件名的组合作为记录的唯一标识符。在内部,Notes仅指同一文档中有重复的附件名,并对不唯一的附件名重新命名,因此重复的键不是一个问题。
主循环执行过程中,使用自定义函数DebugStr创建写入磁盘的数据的字符串描述。上面的代码中省略了调试输出的语句,因此只能看到转移数据的功能。
3.从RDB导入下一个代理的功能和上一个恰好相反:用关系数据库数据中的文件附件创建新的Notes文档。由于我们不希望删除源数据记录,所以创建第二个Notes表单(和Villain表单类似,但称为BadGuy),这也是导入脚本创建的文档类型。
由于必须从关系数据库中的数据创建文件,所以使用来自主Villains关系表中的键从附件关系表中选择。通过向File连接程序插入来使用这些数据在磁盘上创建文件。然后再插入Notes连接程序,该操作从磁盘上抓取文件自动创建Notes文件附件。
不过,使用Notes连接程序的LoadFile属性读和写有一个重要的区别。读的时候可以忽略Notes字段列表中的FILE字段,察看创建了哪些文件。但是写的时候,必须使用FILE字段告诉LotusNotes要附加的每个文件的完整路径,而不是简单地选择目标目录的中的所有文件。
FILE字段很特殊,除了名称之外,还有专门的虚拟字段代码与之关联,告诉Notes连接程序要特殊对待。这样就可以区分带有附件列表功能的字段和恰好命名为FILE的普通字段。Notes连接程序使用的几个特殊字段名都有自己的虚拟标志。
注意:不应该混淆虚拟字段标识和DECS/LEIVirtualField活动,后者从关系数据库中提取实时信息。虽然名称类似,但是完全不同的概念。
在进入主循环之前,导入脚本构造了一些相互联系的字段列表,和前面的代理非常类似,包括特殊的FILE字段。(画出这些字段的关系图留给读者作为练习。)如前所述,建立和表匹配的字段列表最简单的办法就是使用Select检索表的描述。如果列表中包含特殊字段,如多值字段和虚拟字段,情况更是如此。不过,对于一个富有启发性的例子,如果能用较麻烦的办法来做,何必使用最简单的办法呢?下面是从头创建自己的FILE虚拟字段的代码:
SetfldFiles=flNotes.Append("FILE",LCTYPE_BINARY)
该字段属于二进制类型,但不是BLOB字段。二进制字段用于存储特殊的Notes字段值,如富文本字段和多值字段。
CallfldFiles.SetFormatStream(,,LCSTREAMFMT_TEXT_LIST)
这样就把该二进制字段类型设为一个多值Notes文本字段。
CallfldFiles.SetVirtualCode(lcconNotes.GetPropertyInt(LCTOKEN_CONNECTOR_CODE))
SetVirtualCode函数需要一个参数,说明希望哪一个连接程序或者连接特殊对待该字段。不同的连接程序可能有不同的它们认为特殊的字段名,上面的语句让我们指定该字段对LotusNotes是特殊的,而非其他连接程序。这是让我们访问不同后端的特殊功能的一种办法。这里设定的标志告诉LotusNotes要特别注意该字段。
每个Notes连接的Connector代码属性返回相同的数值。但是Connection代码对于每个LCConnection对象是唯一的。通过设置属于连接而非连接程序的虚拟代码,可以用一个Notes连接从Notes文档加载一个名称列表(将FILE看作一个简单的多值字段),然后将该字段连接到和其他连接关联的字段列表,该链接将其视作特殊的FILE字段并从磁盘上找到那些文件。FILE字段需要包含需要附加的每个文件的完整路径,而不仅仅是文件名。
导入函数的主循环必须为从关系数据库中读取的每个Villain记录构造这样一个列表,代码如下:
DoWhilelcconVillains.Fetch(flVillains)>0CalllcconCrimes.Select(flVillains,1,Nothing)''readthrumetaconnector,"sothere"salwaysatmostoneresult.IflcconCrimes.Fetch(flCrimes)=0ThenfldCrimes.Value=""''iftheyhavenocrimes,erasevalueleftover''frompreviousiteration.EndIfstrFilenames=""CalllcconAttach.Select(flVillains,1,Nothing)"Loopthrulistofattachmentrelationalrecordsassociatedwith"currentkey,anddetacheachfiletotempdir.DoWhilelcconAttach.Fetch(flAttach)CalllcconFiles.Insert(flFiles)"copythefilecontentsintoa"diskfile.Buildadelimitedstringofthefullfilepathsofthe''fileswedetach.strFilenames=strFilenames&NEWLINE&dirTemp.fullpath&"\"&fldFilename.Text(0)Loop''Ifthereareattachments,constructastringthatcontainsamultivalue''whereeachvalueisthefullpathofonefile.IfstrFilenames<>""ThenlcstrFilenames.Value=Split(Mid$(strFilenames,2),NEWLINE)ElseCalllcstrFilenames.ClearEndIfCallfldFiles.SetStream(1,lcstrFilenames)"setFILEmultivaluefield"tolistoffilestoattach.CalllcconNotes.Insert(flNotes)CalllcconFiles.Action(LCACTION_TRUNCATE)"getridoftemp"attachmentfiles.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。
现实生活中的同步
上面的脚本都很简单。在现实生活中,应用程序常常要比把数据从一个数据库转储到另一个数据库复杂得多。更常见的情况是同步不同数据源中的数据。
当然,如果数据转移是一种办法,也可以删除目标数据库中的所有数据,然后从源数据库中将全部内容复制过去,这种办法称为懒汉法。在某些应用程序中,这种办法的确可行,但是有几方面的因素使其不那么好:
很多数据库逐渐地会增加各种定制的程序化的事件代码,当增加、删除或修改记录时就被触发。删除一条记录再增加一条类似的记录,可能触发不必要的处理过程,得到不满意的结果。
如果目标数据库是LotusNotes,转储和重建所有Notes文档还有几个不利之处:
- 今天建立的doclinks明天就不能用了。
- 删除会留下残余,这增加了数据库的大小,增加了复制和建立索引的实践,通常会影响性能,特别是如果其中大部分是活动文档的话。
- 未读标志丢失了。
- Domino服务器也可能有基于事件的处理过程,如新建或修改文档时运行的API插件和代理。
其他类型的数据库也可能因为不必要的数据活动而影响性能。
为了帮助解决这一问题,实例数据库中还包含一个代理在两个LCConnection之间执行简单的、单向的、基于关键字的同步。代理“5.ReplicatePirates”包括一个可重用的引擎,每次从两个结果集中读取一个记录,比较其关键字和数据值决定两个记录是否存在差异,如果有的话,看看它表示的是插入、更新还是删除。然后根据情况从一个或两个结果集中读取下一个记录。如何为该引擎提供经过同样排序的两个结果集取决于用户,如果使用不同的连接程序可以需要使用Order元连接程序。此后处理就可以自动进行了(至少对于简单字段类型)。
如何将这种技术与需要完成的文件附件操作结合起来可以有不同的办法。不过,虽然可以使用LCStream对象比较两个文件附件的二进制数据是否相同,但是比较时间要快得多。
结束语
本文描述了如何在LCLSX中处理文件附件。简要介绍LCLSX如何处理文件附件之后,详细讨论了一个示例应用程序来示范文件附件的处理,最后探讨了现实世界中同步技术的一些问题。
我们希望本文对您有所帮助,如果是这样,建议您下载我们提供的例子,然后根据具体的需要和要求进行调整。祝您好运!
来源:互连网