搜索​​​​

清除过滤器
文章
jieliang liu · 一月 27, 2021

在Caché中使用正则表达式

1.关于本文 就像Caché模式匹配一样,正则表达式也可以在Caché中用来识别文本数据中的模式--只是表达能力更强。本文简要介绍了正则表达式,以及在Caché中如何使用它。这里提供的信息基于各种来源,最值得拜读的是Jeffrey Friedl的《掌握正则表达式》一书,当然还有Caché在线文档。本文无意讨论正则表达式的所有可能性和细节。如果你想了解更多,请参考第5章中列出的信息来源。 使用模式进行文本处理有时会变得很复杂。在处理正则表达式时,我们通常有几种实体:我们正在搜索模式的文本、模式本身(正则表达式)和匹配(文本中与模式匹配的部分)。为了便于区分这些实体,本文档中使用了以下约定。 文本样本以单色字体单独列出,不加引号。 This is a "text string" in which we want to find "something". 除非不明确,否则正文中的正则表达式会以灰色背景显示,如本例。\".*?\". 需要时用不同的颜色突出显示匹配。 这是一个"text string",我们要在其中找到"something"。 代码样本会显示在如下的文本框里: set t=" This is a ""text string"" in which we want to find ""something " set r="\"".*?\""" w $locate(t,r,,tMatch) 2.一些历史(和一些小事)。 在20世纪40年代初,神经生理学家开发了人类神经系统的模型。几年后,一位数学家用一种代数来描述这些模型,他称之为"正则集"。这种代数的符号被命名为"正则表达式"。 1965年,正则表达式第一次在计算机的范畴内被提及。随着qed,一个作为UNIX操作系统一部分的编辑器,正则表达式开始传播。该编辑器后来的版本提供了一个命令序列g/正则表达式/p(全局、正则表达式、打印),在所有文本行中搜索匹配的正则表达式并输出结果。这个命令序列最终成为独立的UNIX命令行程序"grep"。 今天,许多编程语言都存在正则表达式(RegEx)的各种实现(见3.3节)。 3.Regex 101 就像Caché模式匹配一样,正则表达式也可以用来识别文本数据中的模式--只是表达能力更强。下面的章节概述了正则表达式的组成部分,它们的评估和一些可用的引擎,然后在第4章中详细介绍如何使用。 3.1.正则表达式的组成部分 3.1.1.Regex元字符 以下字符在正则表达式中具有特殊意义。 . ( ) [ ] \ ^ $ | 如果你需要将它们作为字面数使用,你需要使用反斜杠来转义。你也可以使用 \Q <literal sequence> \E显式指定字面序列。 3.1.2.文字 普通文本和转义字符被视为字面,例如:。 abc abc \f 换页 \n 换行 \r 回车 \v 标签 \0+三位数(如:0101) 八进制数Caché (ICU)中使用的regex引擎支持八进制数,最高可达\0377(十进制系统为255)。当你从另一个引擎迁移正则表达式时,请确保你了解它如何处理八进制数。 \x+两位数(如:x41) 十六进制数Caché库确实提供了更多处理十六进制数的选项,请参考文档(链接可以在5.8节找到)。 3.1.3.锚 使用锚点,你可以匹配文本/字符串中的位置,例如:。 \A 字符串的开始 \Z 字符串的末端 ^ 文本或行的开始 $ 文末或行末 \b 字词边界 \B 不字界 < 词的开头 #> 词尾 和一些RegEx引擎的行为有所不同,例如,对构成单词的确切定义以及哪些字符被视为单词定界符。 3.1.4.量词 使用正则表达式量词,你可以指定前面的元素可能出现的频率来进行匹配。 {x}正好出现x次 {x,y}最小x,最大y的出现次数。 * 0或更多;相当于{0,}。 +1或更多;相当于{1,}。 ? 0或1 量词很"贪婪",它们会尽可能多地抓取字符。假设我们有下面的文本字符串,想找到带引号的文本。 This is "a text" with "four quotes". 由于选择器的贪婪性质,正则表达式"/".*/"会找到太多的文本。 This is "a text" with "four quotes". 在这个例子中,正则表达式.*试图捕捉尽可能多的位于一对引号之间的字符。然而,由于点选择器 ( .) 也匹配引号,我们没有得到我们想要的结果。 通过一些regex引擎(包括Caché使用的引擎),你可以通过添加一个问号来控制量化符的贪婪程度。因此,正则表达式"\".*?\"现在可以匹配引号中的两部分文本--这正是我们要找的。 This is "a text" with "four quotes". 3.1.5.字符类(范围) 方括号用于指定字符的范围或字符集,例如 [a-zA-Z0-9] 或 [abcd] - 在 regex 中,这被称为字符类。一个范围可以匹配单个字符,所以范围定义中的字符顺序无关紧要--[dbac]返回的匹配结果与[abcd]相同。 要排除一个字符范围,只需在字符范围定义前面加上^(在方括号内!)。[^abc] 匹配除了a, b或c以外的任何字符. 一些regex引擎确实提供了预先定义的字符类(POSIX),例如。 [:alnum:] [a-zA-z0-9] [:alpha:] [a-zA-Z] [:blank:] [\t] … 3.1.6.Groups (组) 正则表达式的部分内容可以使用对括号进行分组。这对于将量化符应用于一组选择符,以及从同一regex内(反向引用)和从调用正则表达式的Caché对象脚本代码(捕获缓冲区)中引用分组都很有用。组可以被嵌套。 下面的regex匹配由一个三位数组成的字符串,后面是一个破折号,然后是3对大写字母和一个数字,后面是一个破折号,然后是与第一部分相同的三位数。 ([0-9]{3})-([A-Z][[0-9]){3}-\1 这个例子展示了如何使用反向引用(见下文)不仅匹配结构,而且匹配内容:反向引用(紫色)告诉引擎在结尾处寻找与开头处相同的三位数数字(黄色)。它还演示了如何将量词应用于更复杂的结构(绿色)。 上面的regex将匹配以下字符串。 123-D1E2F3-123 在这些上面是不匹配的。 123-D1E2F3-456(最后三位数与前三位数不同) 123-1DE2F3-123(中间部分不包含三个字母/数字对) 123-D1E2-123(中间部分只包含两个字母/数字对) 组也会填充所谓的捕获缓冲区(见4.5.1节)。这是一个非常强大的功能,它允许同时匹配和提取信息。 3.1.7. Alternations(交替) 使用管道字符来指定alternations,例如skyfall|done。这允许匹配更复杂的表达式,如3.1.5节中描述的字符类。 3.1.8.回溯引用 后面的引用允许您引用以前定义的组(括号中的选择器)。下面的例子显示了一个正则表达式,它匹配三个必须相等的连续字符。 ([a-zA-Z])/1/1 后面的引用由\x指定,而x代表第x个括号中的表达式。 3.1.9.优先规则 []在()之前 +和? 在序列前:ab等于a(b*),而不是(ab)*。 序列在alternation前:ab|c等于(ab)|c,而不是a(b|c) 3.2.一些理论 正则表达式的评估通常采用以下两种方法之一来实现(这里描述是简化的,请参考第5章中提到的文献进行深入讨论)。 文本驱动(DFA - Deterministic Finite Automaton) 引擎逐字逐句地检查输入文本,并尝试匹配它目前所拥有的内容。当它真正到达输入文本的结尾时,它宣布成功。 Regex-driven (NFA - Non-deterministic Finite Automaton) 引擎会逐一检查正则表达式,并尝试将其应用到文本中。当它真正到达(并匹配)最后一个标记时,它宣布成功。 方法1是确定性的,执行时间只取决于输入文本的长度。正则表达式中选择符的顺序不影响执行时间。 方法2是非决定性的,引擎会遍历正则表达式中选择符的所有组合,直到找到匹配或遇到错误。因此,当它没有找到匹配项时,这种方法特别慢(因为它必须遍历所有可能的组合)。选择符的顺序确实对执行时间有影响。但是,这种方法允许回溯和捕获缓冲区。 3.3.Regex引擎 目前有很多不同的regex引擎,有些是编程语言或操作系统的内置部分,有些是几乎可以在任何地方使用的库。以下是一些regex引擎,按评估方法分组。 DFA: grep, awk, lex. NFA:Perl、Tcl、Python、Emacs、sed、vi、ICU。 下表是各种编程语言和库中可用的regex功能的比较。 详情请点击这里:https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines 4.RegEx和Caché InterSystems Caché使用ICU库来处理正则表达式,Caché在线文档描述了它的许多功能。请参考ICU库的在线文档以了解完整的细节(包括诸如回溯引用等)--ICU的链接可以在5.8节中找到。以下章节旨在为您快速介绍如何使用它。 4.4.$match()和$locate() 在Caché ObjectScript (COS)中,两个函数$match()和$locate()提供了对ICU库提供的大部分regex功能的直接访问。$match(String, Regex) 在输入的字符串中搜索指定的Regex模式。当它找到一个匹配的模式时,它返回1,否则它返回0。 例如: w $match("baaacd",".*(a)\1/1.*") 返回1。 w $match("baacd",".*(a)\1/1.*") 返回0。 $locate(String,Regex,Start,End,Value) 就像$match()一样,以指定的regex模式搜索输入字符串。然而,$locate()给你更多的控制权,它返回更多的信息。在Start中,你可以告诉$locate应该在哪个位置开始搜索输入字符串中的模式。当$locate()找到一个匹配时,它会返回匹配的第一个字符的位置,并将End设置为匹配后的下一个字符位置。匹配的内容会在Value中返回。 如果$locate()没有找到匹配的对象,它将返回0,并且不触及End和Value(如果指定)的内容。End和Value是以引用的形式传递的,所以如果你重复使用它(例如在循环中)要小心。 例如: w $locate("abcdexyz",".d.",1,e,x) 返回3,e设为6,x设为"cde" $locate()执行模式匹配,并且可以同时返回第一个匹配的内容,如果需要提取所有匹配的内容,可以在循环中反复调用$locate(),也可以使用%Regex.Matcher提供的方法。 如果需要提取所有匹配的内容,你可以在一个循环中重复调用$locate(),或者你可以使用%Regex.Matcher提供的方法(见下一节)。 4.5.%Regex.Matcher. %Regex.Matcher提供了ICU库的regex功能,就像$match()和$locate()一样。然而,%Regex.Matcher还提供了一些高级功能,使更复杂的任务变得非常容易使用。下面的章节将重新审视捕获缓冲区,看看用正则表达式替换字符串的可能性以及控制运行时行为的方法。 4.5.1.捕获缓冲区(Buffers) 正如我们在关于组、回溯引用和$locate()的章节中已经看到的,正则表达式允许你同时搜索文本中的模式并返回匹配的内容。它的工作原理是将您想要提取的模式的部分放在一对括号中(组)。匹配成功后,捕获缓冲区包含所有匹配的组的内容。请注意,这与$locate()通过其值参数提供的内容略有不同:$locate()返回整个匹配本身的内容,而捕获缓冲区则让您访问匹配的部分内容(组)。 要使用它,你需要创建一个 %Regex.Matcher 类的对象,并将正则表达式和输入字符串传递给它。然后你可以调用%Regex.Matcher提供的方法来执行实际工作。 例1(简单组): set m=##class(%Regex.Matcher).%New("(a|b).*(de)", "abcdeabcde") w m.Locate()返回1 w m.Group(1) 返回 a w m.Group(2) 返回 de 例2(嵌套组和回溯引用)。 set m=##class(%Regex.Matcher).%New("((a|b).*?(de))(\1)", "abcdeabcde") w m.Match()返回1 w m.GroupCount返回4 w m.Group(1) 返回 abcde。 w m.Group(2) 返回 a w m.Group(3) 返回 de w m.Group(4) 返回 abcde。 (注意嵌套组的顺序--因为开头的括号标志着一个组的开始,所以内部组的索引号比外部组的索引号高) 如前所述,捕获缓冲区是一个非常强大的功能,因为它们允许您同时匹配模式和提取匹配的内容。如果没有正则表达式,您必须在第一步中找到匹配的内容(例如使用模式匹配操作符),并在第二步中根据一些标准提取匹配的内容(或部分内容)。 如果您需要对模式中的部分进行分组(例如对该部分应用量化符),但又不想用匹配部分的内容来填充捕获缓冲区,您可以通过在组前加上问号和冒号的方式将组定义为"非捕获"或"害羞",如下面的例子3。 例3。 set m=##class(%Regex.Matcher).%New("((a|b).*?(?:de))(\1)","abcdeabcde") w m.Match()返回1 w m.Group(1) 返回 abcde。 w m.Group(2) 返回 a w m.Group(3) 返回 abcde。 w m.Group(4) 返回 <REGULAR EXPRESSION>zGroupGet+3^%Regex.Matcher.1。 4.5.2.替换 %Regex.Matcher也提供了立即替换匹配内容的方法。ReplaceAll()和ReplaceFirst()。 set m=##class(%Regex.Matcher).%New(".c.","abcdeabcde") w m.ReplaceAll("xxxx") 返回 axxxxeaxxxxe。 w m.ReplaceFirst("xxxx") 返回 axxxxeabcde。 你也可以在替换字符串中引用组。如果我们在上一个例子的模式中添加一个组,我们可以通过在替换字符串中包含$1来引用它的内容。 set m=##class(%Regex.Matcher).%New(".(c).","abcdeabcde").","abcdeabcde") w m.ReplaceFirst("xx$1xx") 返回 axxcxxeabcde。 使用$0在替换字符串中包含匹配的全部内容。 w m.ReplaceFirst("xx$0xx") 返回 axxbcdxxeabcde。 4.5.3.操作限制(OperationLimit) 在3.2节中,我们了解了评估正则表达式的两种方法(DFA和NFA)。Caché中使用的正则表达式引擎是一个非确定性的有限自动机(NFA)。因此,对给定输入字符串评估各种正则表达式的持续时间可能会有所不同。[1] 您可以使用%Regex.Matcher对象的OperationLimit属性来限制执行单元的数量(所谓的簇)。执行一个簇的确切持续时间取决于你的环境。通常情况下,一个簇的执行持续时间是非常少的几毫秒。默认情况下,OperationLimit被设置为0(无限制)。 4.6.真实世界的例子:从Perl到Caché的迁移。 本节介绍了从Perl迁移到Caché的过程中与正则表达式有关的部分。Perl 脚本实际上由几十个或多或少复杂的正则表达式组成,这些正则表达式被用来匹配和提取内容。 如果Caché中没有regex功能,迁移项目就会变成一项重大工作。然而,Caché中的regex功能是可用的,Perl脚本中的正则表达式几乎可以在Caché中使用,而不需要任何改变。 下面是Perl脚本的一部分。 将正则表达式从 Perl 移到 Caché 的唯一改动是 /i 修饰符(使 regex 不区分大小写)--这必须从 regex 的结尾移到开头。 在Perl中,捕获缓冲区的内容被复制到特殊的变量中(在上面的Perl代码中是$1和$2)。在Perl项目中,几乎所有的正则表达式都使用了这种机制。为了类似于这种机制,我们在Caché对象脚本中写了一个简单的包装方法。它使用 %Regex.Matcher 对文本字符串评估正则表达式,并将捕获缓冲区的内容以列表的形式返回($lb())。 由此产生的Caché对象脚本代码如下。 如果...RegexMatch( tVCSFullName。 "(?i)[\\\/]([^\\^\/]+)[\\\/]ProjectDB[\\\/](.+)[\\\/]archives[\\\/]", .tCaptureBufferList) { set tDomainPrefix=$zcvt($lg(tCaptureBufferList,1), "U") set tDomain=$zcvt($lg(tCaptureBufferList,2), "U") } … Classmethod RegexMatch(pString as %String, pRegex as %String, Output pCaptureBuffer="") { #Dim tRetVal as %Boolean=0。 set m=##class(%Regex.Matcher).%New(pRegex,pString) set m.Locate() { set tRetVal=1 for i=1:1:m.GroupCount { set pCaptureBuffer=pCaptureBuffer_$lb(m.Group(i)) } } quit tRetVal } 5.参考资料 5.7.一般资料 概括信息和教程。 http://www.regular-expressions.info/engine.html 教程和实例。 http://www.sitepoint.com/demystifying-regex-with-practical-examples/ 几种regex引擎的比较。 https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines 常用表达式cheat sheet。 https://www.cheatography.com/davechild/cheat-sheets/regular-expressions/pdf/ 书。 Jeffrey E. F. Friedl:"掌握正则表达式"(见http://regex.info/book.html) 5.8.Caché在线文件 关于Caché中正则表达式的用法概述。 http://docs.intersystems.com/latest/csp/docbook/ DocBook.UI.Page.cls?KEY=GCOS_regexp。 $match()的文档。 http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fmatch $locate()的文档。 http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_flocate %Regex.Matcher的类引用。 http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=%25SYS&CLASSNAME=%25Regex.Matcher 5.9.ICU 如上所述,InterSystems Caché使用ICU引擎。全面的文档可在网上查阅。 http://userguide.icu-project.org/strings/regexp http://userguide.icu-project.org/strings/regexp#TOC-Regular-Expression-Metacharacters http://userguide.icu-project.org/strings/regexp#TOC-Regular-Expression-Operators http://userguide.icu-project.org/strings/regexp#TOC-Replacement-Text http://userguide.icu-project.org/strings/regexp#TOC-Flag-Options 5.10.工具 有许多工具支持开发人员创建正则表达式--其中一些是免费的,另一些则有商业许可。我个人的选择是RegexBuddy(http://www.regexbuddy.com/)--它提供了一套全面的交互式和可视化功能,可以创建和测试不同风味的正则表达式。 thanks for sharing!
文章
Michael Lei · 五月 30, 2022

部分IRIS 2022 年度编程大奖赛作品展示——FHIR 匿名化代理软件(实现FHIR数据动态匿名化)

你好,我很高兴地宣布向OpenExchange和目前的比赛提交的一个作品,即FHIR匿名化代理。FHIR匿名化代理为任何现有的FHIR服务器增加了一个透明的匿名化层,使客户能够在FHIR服务器上进行查询--其中可能包含个人识别信息--并收到一个即时的匿名化数据版本。 代理机制是通过互操作性Production、BPLs和DTLs以及FHIR互操作性适配器在IRIS for Health平台上实现的。匿名化包括所有身份ID和个人数据,并可通过DTLs进行配置。 OpenExchange的演示应用程序带有一个内置的FHIR endpoint,并以会填充一些示例数据。试一下,或者将你自己的FHIR服务器配置为代理目标亲自测试一下
文章
姚 鑫 · 三月 16, 2021

第十一章 SQL隐式联接(箭头语法)

# 第十一章 SQL隐式联接(箭头语法) **InterSystems SQL提供了一个特殊的`–>`运算符,作为从相关表中获取值的快捷方式,而在某些常见情况下无需指定显式的`JOIN`即可。可以使用此箭头语法代替显式联接语法,也可以将其与显式联接语法结合使用。箭头语法执行左外部联接。** **箭头语法可用于类的属性或父表的关系属性的引用。其他类型的关系和外键不支持箭头语法。不能在`ON`子句中使用箭头语法(`–>`)。** # 属性引用 可以使用`- >`操作符作为从`“引用表”`获取值的简写。 例如,假设定义了两个类:`Company`: ```java Class Sample.Company Extends %Persistent [DdlAllowed] { /// The Company name Property Name As %String; } ``` `Employee`: ```java Class Sample.Employee Extends %Persistent [DdlAllowed] { /// The Employee name Property Name As %String; /// The Company this Employee works for Property Company As Company; } ``` `Employee`类包含一个属性,该属性是对`Company`对象的引用。 在基于对象的应用程序中,可以使用点语法遵循此引用。 例如,要查找`Employee`工作的`Company`名称: ```java Set name = employee.Company.Name ``` 可以使用使用外部连接来连接`Employee`和`Company`表的SQL语句来执行相同的任务: ```sql SELECT Sample.Employee.Name, Sample.Company.Name AS CompName FROM Sample.Employee LEFT OUTER JOIN Sample.Company ON Sample.Employee.Company = Sample.Company.ID ``` ![image](/sites/default/files/inline/images/1_27.png) 使用`- >`操作符,可以更简洁地执行相同的外连接操作: ```sql SELECT Name, Company->Name AS CompName FROM Sample.Employee ``` ![image](/sites/default/files/inline/images/2_16.png) 只要在表中有引用列,就可以使用`–>`运算符;也就是说,其列的值是被引用表的ID(本质上是外键的特殊情况)。在这种情况下,`Sample.Employee`的`Company`字段包含`Sample.Company`表中记录的`ID`。可以在可以在查询中使用列表达式的任何地方使用`–>`运算符。例如,在`WHERE`子句中: ```sql SELECT Name,Company AS CompID,Company->Name AS CompName FROM Sample.Employee WHERE Company->Name %STARTSWITH 'G' ``` ![image](/sites/default/files/inline/images/3_15.png) 使用`–>`运算符,可以更简洁地执行相同的`OUTER JOIN`操作: 这等效于: ```sql SELECT E.Name,E.Company AS CompID,C.Name AS CompName FROM Sample.Employee AS E, Sample.Company AS C WHERE E.Company = C.ID AND C.Name %STARTSWITH 'G' ``` **请注意,在这种情况下,此等效查询使用`INNER JOIN`。** 以下示例使用箭头语法访问`Sample.Person`中的`“Spouse”`字段。如示例所示,`Sample.Employee`中的`Spouse`字段包含`Sample.Person`中记录的`ID`。本示例返回`Employee`与其`Spouse`的`Home_State`相同的`Home_State`或`Office_State`的那些记录: ```sql SELECT Name,Spouse,Home_State,Office_State,Spouse->Home_State AS SpouseState FROM Sample.Employee WHERE Home_State=Spouse->Home_State OR Office_State=Spouse->Home_State ``` ![image](/sites/default/files/inline/images/4_10.png) 可以在`GROUP BY`子句中使用–>运算符: ```sql SELECT Name,Company->Name AS CompName FROM Sample.Employee GROUP BY Company->Name ``` ![image](/sites/default/files/inline/images/5_4.png) 可以在`ORDER BY`子句中使用`–>`运算符: ```sql SELECT Name,Company->Name AS CompName FROM Sample.Employee ORDER BY Company->Name ``` ![image](/sites/default/files/inline/images/6_3.png) 或在`ORDER BY`子句中为`–>`运算符列引用列别名: ```sql SELECT Name,Company->Name AS CompName FROM Sample.Employee ORDER BY CompName ``` 支持复合箭头语法,如以下示例所示。在此示例中,`Cinema.Review`表包含`“Film”`字段,其中包含`Cinema.Film`表的行`ID`。 `Cinema.Film`表包含`Category`字段,其中包含`Cinema.Category`表的行`ID`。因此,`Film-> Category-> CategoryName`访问以下三个表,以返回具有`ReviewScore`的每部电影的`CategoryName`: ```sql SELECT ReviewScore,Film,Film->Title,Film->Category,Film->Category->CategoryName FROM Cinema.Review ORDER BY ReviewScore ``` # 子表引用 可以使用`–>`运算符来引用子表。例如,如果`LineItems`是`Orders`表的子表,则可以指定: ```sql SELECT LineItems->amount FROM Orders ``` 请注意,在`Orders`中没有称为`LineItems`的属性。 `LineItems`是包含数量字段的子表的名称。该查询在结果集中为每个`Order`行生成多个行。它等效于: ```sql SELECT L.amount FROM Orders O LEFT JOIN LineItems L ON O.id=L.custorder ``` 其中`ustust`是`LineItems`表的父引用字段。 # 箭头语法权限 使用箭头语法时,必须对两个表中的引用数据都具有`SELECT`权限。必须在被引用的列上具有表级`SELECT`权限或列级`SELECT`权限。使用列级权限,需要对被引用表以及被引用列的ID具有`SELECT`权限。 以下示例演示了所需的列级权限: ```sql SELECT Name,Company->Name AS CompanyName FROM Sample.Employee GROUP BY Company->Name ORDER BY Company->Name ``` 在上面的示例中,必须对`Sample.Employee.Name`,`Sample.Company.Name`和`Sample.Company.ID`具有列级`SELECT`权限: ```java // d ##class(PHA.TEST.SQL).arrow() ClassMethod arrow() { SET tStatement = ##class(%SQL.Statement).%New() SET privchk1="%CHECKPRIV SELECT (Name,ID) ON Sample.Company" SET privchk2="%CHECKPRIV SELECT (Name) ON Sample.Employee" CompanyPrivTest SET qStatus = tStatement.%Prepare(privchk1) IF qStatus'=1 { WRITE "%Prepare 失败:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() IF rset.%SQLCODE=0 { WRITE !,"拥有Company权限",! } ELSE { WRITE !,"无权限: SQLCODE=",rset.%SQLCODE,! } EmployeePrivTest SET qStatus = tStatement.%Prepare(privchk2) IF qStatus'=1 { WRITE "%Prepare 失败:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() IF rset.%SQLCODE=0 { WRITE !,"拥有Employee权限",! } ELSE { WRITE !,"无权限: SQLCODE=",rset.%SQLCODE } } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).arrow() 拥有Company权限 拥有Employee权限 ```
文章
Lilian Huang · 七月 9, 2023

从 IRIS 嵌入式 Python 动态创建 HL7 消息

#Embedded Python #HL7 #InterSystems IRIS for Health 写在回复社区帖子《Python能否动态创建HL7消息》中。 前提条件和设置 使用一个启用了集成的命名空间。注意:USER命名空间默认不启用互操作性。如果以下建议创建一个新的互操作性命名空间来探索功能。 # 切换到ZN "[互操作性名称空间名称]" # 启动交互式Python shell:Do $SYSTEM.Python.Shell() 启动脚本 #Load dependencies import datetime as dt import uuid # Cache current time in CCYYMMDDHHMMss format hl7_datetime_now=dt.datetime.now().strftime('%Y%m%d%H%M%S') # Create HL7 Message hl7=iris.cls("EnsLib.HL7.Message")._New() # Set the doc type # 2.5.1:ORU_R01 - Unsolicited transmission of an observation message hl7.PokeDocType("2.5.1:ORU_R01") 这些信息的结构可以从管理门户中获取 创建MSH(消息头段)。 // MSH Segment hl7.SetValueAt('OutApp','MSH:SendingApplication') hl7.SetValueAt('OutFac','MSH:SendingFacility') hl7.SetValueAt('InApp','MSH:ReceivingApplication') hl7.SetValueAt('InFac','MSH:ReceivingFacility') hl7.SetValueAt(hl7_datetime_now,'MSH:DateTimeOfMessage') hl7.SetValueAt('ORU','MSH:MessageType.MessageCode') hl7.SetValueAt('R01','MSH:MessageType.TriggerEvent') hl7.SetValueAt('ORU_R01','MSH:MessageType.MessageStructure') hl7.SetValueAt(str(uuid.uuid4()),'MSH:MessageControlID') hl7.SetValueAt('2.5.1','MSH:ProcessingID') 编码和解码 HL7文件被格式化为段每个段被分隔符("|")和重复元素("~")划分为多个元素在一个元素内有"^"分界符和"&"子分界符。当定界符作为实际的文本内容出现时,它将被"\"和其他取代定界符的字符转义。通常,"&"是有问题的,因为它可能经常出现在信息中,导致接收系统读取时出现截断现象。HL7段有一个内置的方法,用于根据当前为信息选择的定界符来转义内容。一个常见的模式是获得对第一个段的引用 # Do this line the variable "msh" is used later > msh=hl7.GetSegmentAt(1) 然后可以调用Escape,例如用Python的原始字符串: > msh.Escape(r"a&b~c^d") 'a\\T\\b\\R\\c\\S\\d' The segment can also be used to Unescape back for example: > msh.Unescape('a\\T\\b\\R\\c\\S\\d') 'a&b~c^d' 因此,在设置预计包含分隔符的内容时,可以为信息转义这些分隔符 hl7.SetValueAt(msh.Escape(r"a&b~c^d"),'MSH:ReceivingFacility') 检索内容时可以不加转义 msh.Unescape(hl7.GetValueAt('MSH:ReceivingFacility')) 在这个例子中,只是将msh重新设置为以前的值 hl7.SetValueAt('InFac','MSH:ReceivingFacility') 仔细检查到目前为止的部分: > hl7.GetValueAt('MSH') 'MSH|^~\\&|OutApp|OutFac|InApp|InFac|20230610100040||ORU^R01^ORU_R01|2dfab415-51aa-4c75-a7e7-a63aedfb53cc|2.5.1' 人口统计学(PID)部分 # Virtual path prefix for PID seg='PIDgrpgrp(1).PIDgrp.PID:' hl7.SetValueAt('1',seg+'SetIDPID') hl7.SetValueAt('12345',seg+'PatientIdentifierList(1).IDNumber') hl7.SetValueAt('MRN',seg+'PatientIdentifierList(1).AssigningAuthority') hl7.SetValueAt('MR',seg+'PatientIdentifierList(1).IdentifierTypeCode') hl7.SetValueAt(msh.Escape('Redfield'), seg+'PatientName(1).FamilyName') hl7.SetValueAt(msh.Escape('Claire') ,seg+'PatientName(1).GivenName') hl7.SetValueAt('19640101',seg+'DateTimeofBirth') hl7.SetValueAt('F',seg+'AdministrativeSex') hl7.SetValueAt(msh.Escape('Umbrella Corporation') ,seg+'PatientAddress.StreetAddress') hl7.SetValueAt(msh.Escape('Umbrella Drive') ,seg+'PatientAddress.OtherDesignation') hl7.SetValueAt(msh.Escape('Raccoon City') ,seg+'PatientAddress.City') hl7.SetValueAt(msh.Escape('MO') ,seg+'PatientAddress.StateorProvince') hl7.SetValueAt(msh.Escape('63117') ,seg+'PatientAddress.ZiporPostalCode') 仔细检查PID段的内容 > hl7.GetValueAt(seg[0:-1]) 'PID|1||12345^^^MRN^MR||Redfield^Claire||19640101|F|||Umbrella Corporation^Umbrella Drive^Raccoon City^MO^63117' 订单控制部分 seg='PIDgrpgrp(1).ORCgrp(1).ORC:' hl7.SetValueAt('RE',seg+'OrderControl') hl7.SetValueAt('10003681',seg+'PlacerOrderNumber') hl7.SetValueAt('99001725',seg+'FillerOrderNumber') hl7.SetValueAt('AG104',seg+'OrderingProvider') hl7.SetValueAt('L43',seg+'EnterersLocation') 仔细检查ORC部分的内容 > hl7.GetValueAt(seg[0:-1]) 'ORC|RE|10003681|99001725|||||||||AG104|L43' 观察请求 seg='PIDgrpgrp(1).ORCgrp(1).OBR:' hl7.SetValueAt('1',seg+'SetIDOBR') hl7.SetValueAt('10003681',seg+'PlacerOrderNumber') hl7.SetValueAt('99001725',seg+'FillerOrderNumber') hl7.SetValueAt('20210428100729',seg+'ResultsRptStatusChngDateTime') hl7.SetValueAt('F',seg+'ResultStatus') hl7.SetValueAt('U',seg+'QuantityTiming.Priority') OBX 观察/结果 seg='PIDgrpgrp(1).ORCgrp(1).OBXgrp(1).OBX:' hl7.SetValueAt('1',seg+'SetIDOBX') hl7.SetValueAt('TX',seg+'ValueType') hl7.SetValueAt('V8132',seg+'ObservationIdentifier.Identifier') hl7.SetValueAt(msh.Escape('G-Virus') , seg+'ObservationIdentifier.Identifier') hl7.SetValueAt(msh.Escape('17.8 log10') ,seg+'ObservationValue') hl7.SetValueAt(msh.Escape('RNA copies/mL') ,seg+'Units') hl7.SetValueAt('F',seg+'ObservationResultStatus') hl7.SetValueAt('20210428100729',seg+'DateTimeoftheObservation') hl7.SetValueAt('AG001',seg+'ResponsibleObserver.IDNumber') hl7.SetValueAt('Birkin',seg+'ResponsibleObserver.FamilyName') hl7.SetValueAt('William',seg+'ResponsibleObserver.GivenName') hl7.SetValueAt('AG001',seg+'ResponsibleObserver.IDNumber') hl7.SetValueAt('UXL43',seg+'EquipmentInstanceIdentifier') NTE - 注释和评论 seg='PIDgrpgrp(1).ORCgrp(1).OBXgrp(1).NTE(1):' hl7.SetValueAt('1',seg+'SetIDNTE') hl7.SetValueAt(msh.Escape('Expected late onset Hyphema. Contain but do not approach.') ,seg+'Comment') 向终端打印全部信息 > print(hl7.OutputToString()) MSH|^~\&|OutApp|OutFac|InApp|InFac|20230610141201||ORU^R01^ORU_R01|2dfab415-51aa-4c75-a7e7-a63aedfb53cc|2.5.1 PID|1||12345^^^MRN^MR||Redfield^Claire||19640101|F|||Umbrella Corporation^Umbrella Drive^Raccoon City^MO^63117 ORC|RE|10003681|99001725|||||||||AG104|L43 OBR|1|10003681|99001725|||||||||||||||||||20210428100729|||F||^^^^^U OBX|1|TX|G-Virus||17.8 log10|RNA copies/mL|||||F|||20210428100729||AG001^Birkin^William||UXL43 NTE|1||Expected late onset Hyphema. Contain but do not approach. 陷阱 如果一个元素的内容包含一个诸如 "8@%SYS.Python "的值,很可能是需要用字符串值或字符串属性来代替。 例如,uuid在MSH结构中被 "str "包裹着。 原文请查看 来自 Alex Woodhead https://community.intersystems.com/post/dynamically-creating-hl7-message-iris-embedded-python
文章
姚 鑫 · 四月 25, 2021

第五章 优化查询性能(四)

# 第五章 优化查询性能(四) # 注释选项 可以在`SELECT`、`INSERT`、`UPDATE`、`DELETE`或`TRUNCATE`表命令中为查询优化器指定一个或多个注释选项。 注释选项指定查询优化器在编译SQL查询期间使用的选项。 通常,注释选项用于覆盖特定查询的系统范围默认配置。 ## 语法 语法`/*#OPTIONS */`(在`/*`和`#`之间没有空格)指定了一个注释选项。 注释选项不是注释; 它为查询优化器指定一个值。 注释选项使用`JSON`语法指定,通常是`“key:value”`对,例如: `/*#OPTIONS {"optionName":value} */`。 支持更复杂的JSON语法,比如嵌套值。 注释选项不是注释; 除了`JSON`语法之外,它可能不包含任何文本。 包含非`json`文本在`/* ... */`分隔符导致`SQLCODE -153`错误。 InterSystems SQL不验证`JSON`字符串的内容。 `#OPTIONS`关键字必须用大写字母指定。 `JSON`的大括号语法中不应该使用空格。 如果SQL代码用引号括起来,比如动态SQL语句,JSON语法中的引号应该是双引号。 例如:`myquery="SELECT Name FROM Sample.MyTest /*#OPTIONS {""optName"":""optValue""} */"`. 可以在SQL代码中任何可以指定注释的地方指定`/*#OPTIONS */` comment选项。 在显示的语句文本中,注释选项总是作为注释显示在语句文本的末尾。 你可以在SQL代码中指定多个`/*#OPTIONS */` comment选项。 它们按照指定的顺序显示在返回的语句文本中。 如果为同一个选项指定了多个注释选项,则使用`last`指定的选项值。 以下的注释选项被记录在案: - `/*#OPTIONS {"BiasAsOutlier":1} */` - `/*#OPTIONS {"DynamicSQLTypeList":"10,1,11"}` - `/*#OPTIONS {"NoTempFile":1} */` ## 显示 `/*#OPTIONS */` comment选项显示在SQL语句文本的末尾,而不管它们是在SQL命令中指定的位置。 一些显示的`/*#OPTIONS */` comment选项没有在SQL命令中指定,而是由编译器的预处理器生成的。 例如 `/*#OPTIONS {"DynamicSQLTypeList": ...} */` `/*#OPTIONS */` comment选项显示在`Show Plan`语句文本、缓存的查询查询文本和SQL语句语句文本中。 为仅在`/*#OPTIONS */` comment选项中不同的查询创建一个单独的缓存查询。 # 并行查询处理 并行查询提示指示系统在多处理器系统上运行时执行并行查询处理。 这可以极大地提高某些类型查询的性能。 SQL优化器确定一个特定的查询是否可以从并行处理中受益,并在适当的时候执行并行处理。 指定并行查询提示并不强制对每个查询进行并行处理,只强制那些可能从并行处理中受益的查询。 如果系统不是多处理器系统,则此选项无效。 要确定当前系统上的处理器数量,请使用 `%SYSTEM.Util.NumberOfCPUs() `方法。 可以通过两种方式指定并行查询处理: - 在系统范围内,通过设置`auto parallel`选项。 - 在每个查询的`FROM`子句中指定`%PARALLEL`关键字。 并行查询处理应用于`SELECT`查询。 它不应用于插入、更新或删除操作。 ## 系统范围的并行查询处理 可以使用以下选项之一来配置系统范围的自动并行查询处理: - 在管理门户中选择System Administration,然后选择Configuration,然后选择SQL和对象设置,最后选择SQL。 查看或更改在单个进程中执行查询复选框。 注意,该复选框的默认值是未选中的,这意味着并行处理在默认情况下是激活的。 - 调用`$SYSTEM.SQL.Util.SetOption()`方法,如下: `SET status=$SYSTEM.SQL.Util.SetOption("AutoParallel",1,.oldval)`. 默认值是1(自动并行处理激活)。 要确定当前的设置,调用`$SYSTEM.SQL.CurrentSettings()`,它会显示为`%PARALLEL`选项启用自动提示。 注意,更改此配置设置将清除所有名称空间中的所有缓存查询。 当激活时,自动并行查询提示指示SQL优化器对任何可能受益于这种处理的查询应用并行处理。 在IRIS 2019.1及其后续版本中,自动并行处理是默认激活的。 从IRIS 2018.1升级到IRIS 2019.1的用户需要明确激活自动并行处理。 SQL优化器用于决定是否对查询执行并行处理的一个选项是自动并行阈值。 如果激活了系统范围的自动并行处理(默认),可以使用`$SYSTEM.SQL.Util.SetOption()`方法将自动并行处理的优化阈值设置为整数值,如下所示: `SET status=$SYSTEM.SQL.Util.SetOption("AutoParallelThreshold",n,.oldval)`。 `n`阈值越高,将此特性应用于查询的可能性就越低。 此阈值用于复杂的优化计算,但可以将此值视为必须驻留在已访问映射中的元组的最小数量。 默认值为3200。 最小值为0。 要确定当前的设置,调用`$SYSTEM.SQL.CurrentSettings()`,它显示`%PARALLEL`选项的自动提示阈值。 当自动并行处理被激活时,在分片环境中执行的查询将始终使用并行处理执行,而不管并行阈值是多少。 ## 针对特定查询的并行查询处理 可选的`%PARALLEL`关键字在查询的`FROM`子句中指定。 它建议跨系统的IRIS使用多个处理器(如果适用的话)并行处理查询。 这可以显著提高使用一个或多个`COUNT`、`SUM`、`AVG`、`MAX`或`MIN`聚合函数和`/`或`groupby`子句的查询的性能,以及许多其他类型的查询。 这些通常是处理大量数据并返回小结果集的查询。 例如,`SELECT AVG(SaleAmt) FROM %PARALLEL User.AllSales GROUP BY Region`都可使用并行处理。 **仅指定聚合函数、表达式和子查询的“一行”查询执行并行处理,无论是否带有`GROUP BY`子句。 但是,同时指定单个字段和一个或多个聚合函数的“多行”查询不会执行并行处理,除非它包含`GROUP BY`子句。 例如,`SELECT Name,AVG(Age) FROM %PARALLEL Sample.Person`不执行并行处理,但是 `SELECT Name,AVG(Age) FROM %PARALLEL Sample.Person GROUP BY Home_State` 执行并行处理。** 如果在运行时模式下编译指定`%PARALLEL`的查询,则所有常量都被解释为ODBC格式。 指定`%PARALLEL`可能会降低某些查询的性能。 在一个有多个并发用户的系统上运行`%PARALLEL`查询可能会降低整体性能。 在查询视图时可以执行并行处理。 但是,即使显式地指定了`%parallel`关键字,也不会对指定`%VID`的查询执行并行处理。 ### `%PARALLEL`的子查询 `%PARALLEL`用于`SELECT`查询及其子查询。 插入命令子查询不能使用`%PARALLEL`。 当应用于与外围查询相关的子查询时,`%PARALLEL`将被忽略。 例如: ```sql SELECT name,age FROM Sample.Person AS p WHERE 30 e.dob.` 这是因为SQL优化将这种类型的连接转换为完整的外部连接。 对于完整的外部连接,`%PARALLEL`将被忽略。 - `%PARALLEL`和`%INORDER`优化不能同时使用; 如果两者都指定,`%PARALLEL`将被忽略。 - 查询引用一个视图并返回一个视图ID (`%VID`)。 - 如果表有`BITMAPEXTENT`索引,`COUNT(*)`不使用并行处理。 - `%PARALLEL`用于使用标准数据存储定义的表。 可能不支持将其与自定义存储格式一起使用。 `%PARALLEL`不支持全局临时表或具有扩展全局引用存储的表。 - `%PARALLEL`用于可以访问一个表的所有行的查询,使用行级安全(`ROWLEVELSECURITY`)定义的表不能执行并行处理。 - `%PARALLEL`用于存储在本地数据库中的数据。 它不支持映射到远程数据库的全局节点。 ## 共享内存的考虑 对于并行处理,IRIS支持多个进程间队列(`IPQ`)。 每个`IPQ`处理单个并行查询。 它允许并行工作单元子流程将数据行发送回主流程,这样主流程就不必等待工作单元完成。 这使得并行查询能够尽可能快地返回第一行数据,而不必等待整个查询完成。 它还改进了聚合函数的性能。 并行查询执行使用来自通用内存堆(`gmheap`)的共享内存。 如果使用并行SQL查询执行,用户可能需要增加`gmheap`大小。 一般来说,每个`IPQ`的内存需求是`4 x 64k = 256k`。 InterSystems IRIS将一个并行SQL查询拆分为可用的`CPU`核数。 因此,用户需要分配这么多额外的`gmheap`: ```java x x 256 = ``` 注意,这个公式不是100%准确的,因为一个并行查询可以产生同样是并行的子查询。 因此,明智的做法是分配比这个公式指定的更多的额外`gmheap`。 分配足够的`gmheap`失败将导致错误报告给`messages.log`。 SQL查询可能会失败。 其他子系统尝试分配`gmheap`时也可能出现其他错误。 要查看一个实例的`gmheap`使用情况,特别是`IPQ`使用情况,请在管理门户的主页上选择System Operation,然后选择System usage,然后单击Shared Memory Heap usage链接; ![image](/sites/default/files/inline/images/2_23.png) 要更改通用内存堆或`gmheap`(有时称为共享内存堆或SMH)的大小,请从管理门户的主页选择“系统管理”,然后是“配置”,然后是“附加设置”,最后是“高级内存”; ![image](/sites/default/files/inline/images/3_19.png) ![image](/sites/default/files/inline/images/4_14.png) ## 缓存查询注意事项 如果你正在运行一个缓存的SQL查询,使用`%PARALLEL`,当这个查询被初始化时,你做了一些事情来清除缓存的查询,那么这个查询可能会从一个工人作业报告一个``错误。 导致缓存查询被清除的典型情况是调用`$SYSTEM.SQL.Purge()`或重新编译该查询引用的类。 重新编译类将自动清除与该类相关的任何缓存查询。 如果发生此错误,再次运行查询可能会成功执行。 从查询中删除`%PARALLEL`可以避免出现此错误。 ## SQL语句和计划状态 使用`%PARALLEL`的SQL查询可以产生多条SQL语句。 这些SQL语句的计划状态是`Unfrozen/Parallel`。 计划状态为“已冻结”/“并行”的查询不能通过用户操作进行冻结。 # 生成报告 可以使用生成报告工具向InterSystems Worldwide Response Center (WRC) customer support提交查询性能报告,以便进行分析。 可以使用以下任意一种方式从管理门户运行生成报告工具: 1. 必须首先从WRC获得WRC跟踪号。可以使用每个管理门户页面顶部的Contact按钮从管理门户联系WRC。在WRC编号区域中输入此跟踪编号。可以使用此跟踪编号来报告单个查询或多个查询的性能。 2. 在“SQL语句”区域中,输入查询文本。右上角将显示一个X图标。可以使用此图标清除SQL语句区。查询完成后,选择保存查询按钮。系统生成查询计划并收集指定查询的运行时统计信息。无论系统范围的运行时统计信息设置如何,生成报告工具始终使用收集选项3:记录查询的所有模块级别的统计信息进行收集。由于在此级别收集统计信息可能需要时间,因此强烈建议您选中“在后台运行保存查询进程”复选框。默认情况下,此复选框处于选中状态。 当后台任务启动时,该工具显示“请等待……”,禁用页面上的所有字段,并显示一个新的视图进程按钮。 单击View Process按钮将在新选项卡中打开Process Details页面。 在流程详细信息页面,您可以查看该流程,并可以“暂停”、“恢复”或“终止”该流程。 进程的状态反映在Save查询页面上。 当流程完成时,当前保存的查询表将被刷新,View process按钮将消失,页面上的所有字段将被启用。 3. 对每个查询执行步骤2。 每个查询将被添加到当前保存的Queries表中。 注意,该表可以包含具有相同WRC跟踪号的查询,也可以包含具有不同跟踪号的查询。 完成所有查询后,继续步骤4。 对于列出的每个查询,可以选择Details链接。 该链接将打开一个单独的页面,其中显示完整的SQL语句、属性(包括WRC跟踪号和IRIS软件版本),以及包含每个模块的性能统计信息的查询计划。 - 要删除单个查询,请从“当前保存的查询”表中选中这些查询的复选框,然后单击“清除”按钮。 - 要删除与WRC跟踪编号关联的所有查询,请从当前保存的查询表中选择一行。WRC编号显示在页面顶部的WRC编号区域。如果您随后单击清除按钮,则对该WRC编号的所有查询都将被删除。 4. 使用查询复选框选择要报告给WRC的查询。要选择与WRC跟踪编号关联的所有查询,请从当前保存的查询表中选择一行,而不是使用复选框。在这两种情况下,都可以选择Generate Report按钮。生成报告工具创建一个XML文件,其中包括查询语句、具有运行时统计信息的查询计划、类定义以及与每个所选查询相关联的SQL int文件。 如果选择与单个WRC跟踪编号关联的查询,则生成的文件将具有默认名称,如`WRC12345.xml`。如果选择与多个WRC跟踪编号关联的查询,则生成的文件将具有默认名称`WRCMultiple.xml`。 将出现一个对话框,要求指定保存报告的位置。保存报告后,可以单击Mail to链接将报告发送给WRC客户支持。使用邮件客户端的附加/插入功能附加文件。
文章
Nicky Zhu · 五月 20, 2021

互操作消息统一管理系列:MessageBank

## 一. 企业信息库简介 企业信息库(MessageBank)是一个可选的远程归档设施,可以从多个来自不同实例的互操作性Production中收集信息、事件日志项目和搜索表项。如下图所示: ![image](/sites/default/files/inline/images/1_1_0.png) 这套环境由两种角色的实例构成: 企业信息库服务器,它本身也是一个Production,完全由Message Bank服务组成,接收来自任何数量的客户Production提交的消息、日志等。 客户端Operation(Message Bank Operation),将其添加到一个正在运行的Production中,并用企业信息库服务器的地址进行配置。如连接通畅,消息和日志即可自动转发到Message Bank并在其中存储。 为了使你能方便地看到信息库中的信息,InterSystems IRIS®提供了以下附加选项。 对于企业信息库实例,管理门户自动包括企业监控器页面,在那里你可以监控客户端Production的状态,浏览消息库,并对被监控客户的消息进行检索。 对于每个客户端实例,你在消息库实例中配置一个到企业监控器的链接。 如下所示: ![image](/sites/default/files/inline/images/1_2_0.png) ## 二. 常见应用场景 ### 消息归档 在使用IRIS互操作性时,对于生产环境,为保障其有充足的磁盘空间和即时查询的效率,通常会采用消息和日志过期策略。在生产环境中只保留近期(如一个月)的信息以备回溯,过期数据将定期被清除。因此,如果有长期保留消息(如在生产环境清除周期之外还需要更长时间的回溯)的需求,则可以通过Message Bank对消息和日志进行长期保存。 ### 企业消息仓库 对于集成规模较大,集成业务较多的大型企业和集团(如大型医院、医联体、医共体),往往会采用多套互操作性实例支撑数据交换和集成业务。在这种环境下,可以通过Message Bank汇聚和存储整个企业环境下的所有互操作消息和日志,为业务集中监控、跨实例业务故障分析等工作创造条件。 ### 消息和日志再利用 理想条件下,实施互操作性项目之后,消息和日志中就会包括大量的业务数据,典型的包括下达的医嘱、患者信息、医疗记录等。通过对Message Bank中的数据进行分析和挖掘,能够获得有价值的业务信息。 接下来我们会为大家介绍Message Bank的搭建过程。 ## 三. 搭建Message Bank ### 创建Message Bank 命名空间 在生产所用的实例之外,我们需要使用一台独立的实例用于安装和配置Message Bank(实例安装过程和License激活过程从略,请查看安装文档或联系您的支持工程师)。 在该实例上,创建一个命名空间安装Message Bank,如下所示: ![image](/sites/default/files/inline/images/1_3_0.png) 由于Message Bank本质上由Production实现,因此创建命名空间时要选上对互操作Producation的支持。 InterSystems为大家提供了可以套用的Production模版。因此,请按照以下步骤创建Production: 在刚才创建的命名空间MessageStore下创建类MessageBank.BankProduction,继承Ens.Enterprise.MsgBank.Production并将Ens.Enterprise.MsgBank.Production中的XData代码块拷贝到新建的类中,如下所示: ![image](/sites/default/files/inline/images/1_4_0.png) 保存和编译该类,并在Interoperability菜单中加载该Production。 ![image](/sites/default/files/inline/images/1_5_0.png) 其中已部署了两个服务: MsgBankService:该服务通过TCP连接从其他Production接收消息 注意该Service默认使用9192端口与其他客户端通信。 ![image](/sites/default/files/inline/images/1_6_0.png) MonitorService:该服务收集其他实例的其他Production的运行状态 此时,这个Production已经具备了从其他实例的Production收集消息和事件信息的能力,可直接启动。当然,我们还需要配置与客户端的连接。 ### 为客户端Production添加消息转发Operation 假设我们已经有一个可运行的的如下所示的Production ![image](/sites/default/files/inline/images/1_7_0.png) 注意这个Production与Message Bank不在同一个实例上。 ![image](/sites/default/files/inline/images/1_8_0.png) 这个Production接收XML格式的报文并根据报文类型转发到不同的BO。 要将这个Production加入Message Bank,则需要对该客户端Production添加Business Operation Ens.Enterprise.MsgBankOperation。 ![image](/sites/default/files/inline/images/1_9_0.png) 对于该Operation,需要指定要连接的Message Bank的IP地址和端口。 ![image](/sites/default/files/inline/images/1_10_0.png) 同时,建议开启这个Operation的“启用存档”开关,保证在Message Bank临时故障时挂起消息,在故障恢复后还能捕捉到故障期间的消息和日志。 配置完成后启用该Operation。 ### 在Message Bank中加入客户端信息 上述连接建立后,客户端和Message Bank间的连接已建立,还需要配置Message Bank和客户端Production之间的程序信息(相当于注册)才能正常工作。 #### 添加客户端连接凭据 ![image](/sites/default/files/inline/images/1_11_0.png) Message Bank需要通过Web请求访问客户端信息,因此,需要配置客户端凭据,即可通过管理门户访问客户端Production的用户名和密码(对访问权限的设计和配置,可参见我们之前的文章:IRIS中的权限管理) #### 在Message Bank上配置客户端信息 在Message Bank中的Interoperability菜单中找到“企业系统”项 ![image](/sites/default/files/inline/images/1_12_0.png) 在操作页面上通过“新建连接” ![image](/sites/default/files/inline/images/1_13_0.png) 新建连接添加客户端信息。 ![image](/sites/default/files/inline/images/1_14_0.png) 注意其中的服务Web应用路径为该客户端实例上Production所在的命名空间的Web Application根路径,并引用之前填写的凭据。 如配置正确,可通过企业监视器查看连接状态 ![image](/sites/default/files/inline/images/1_15_0.png) 连接成功的状态如下: ![image](/sites/default/files/inline/images/1_16_0.png) #### 在客户端上添加Message Bank连接信息(可选步骤) 如果需要在客户端上通过链接查看消息仓库的信息,则可以配置链接。 在客户端上,在被采集的Production所在的命名空间的Interoerability菜单中“消息仓库链接”配置 ![image](/sites/default/files/inline/images/1_17_0.png) 输入Message Bank所在的IP、端口和Production所在的命名空间,保存并“开始”即可跳转到Messsage Bank的企业监视器。 ![image](/sites/default/files/inline/images/1_18_0.png) 需要注意的是,该配置固定采用了/csp/[namespace]为Message Bank的Web Application路径,而在Message Bank实例上,这个Web Application默认的路径是/csp/healthshare/messagestorage。可通过在Message Bank上添加一个Web Application,拷贝/csp/healthshare/messagestorage的配置。 ![image](/sites/default/files/inline/images/1_19_0.png) ## 四. Message Bank的实施效果 ### 测试消息 在客户端的Production中触发任意流程产生消息,如下所示: ![image](/sites/default/files/inline/images/1_20_0.png) 此时通过Message Bank中的“消息仓库查看器”即可查询存储在消息仓库中的消息 ![image](/sites/default/files/inline/images/1_21_0.png) 如下: ![image](/sites/default/files/inline/images/1_22_0.png) 可以注意到该消息已被同步到消息仓库。 需要注意,使用“消息仓库查看器”时,查询的是在Message Bank中存储的消息数据,使用在Message Bank上定义的Search Table或索引进行查询;如果通过“企业消息查看器”查询,则是链接到客户端的消息查看器查询,应用的是在客户端上定义的索引。 ### 消息的存储 根据在源系统的消息类型的不同,传递到Message Bank后会以不同的形式保存消息。 #### 虚拟文档 对于HL7 V2等标准消息或基于XML虚拟文档的消息,在Message Bank这一侧也同样以虚拟文档的形式保存。 ![image](/sites/default/files/inline/images/1_23_0.png) 特别注意其中的如下属性: MessageBodyClassName:该类型为消息在Message Bank侧持久化的类型。 ClientBodyClassName:该类型为消息在客户端侧持久化的类型。 在本例中可以看到,客户端通过EnsLib.EDI.XML.Document类型传递的消息,在Message Bank中也是通过EnsLib.EDI.XML.Document保存。 MessageBodyId:消息在Message Bank中的物理主键 ClientBodyId:客户端侧持久化消息的物理主键 ClientSessionId:客户端会话Id #### 结构化消息 对于基于Ens.Request等持久化类型的消息,在Message Bank这一侧则默认使用字符流来保存。 例如,对于如下的客户端结构化消息传输 ![image](/sites/default/files/inline/images/1_24_0.png) 在Message Bank中的保存形式为: ![image](/sites/default/files/inline/images/1_25_0.png) 可见: MessageBodyClassName:消息在Message Bank中以%Stream.GlobalCharacter即字符流进行保存 因此,无论是保存为EnsLib.EDI.XML.Document或是%Stream.GlobalCharacter,在Message Bank中保存的消息本身都缺乏足够的结构化特征和索引以支持对消息体的检索,我们会在下一篇教程《[互操作消息统一管理系列:SearchTable加速检索](https://cn.community.intersystems.com/post/%E4%BA%92%E6%93%8D%E4%BD%9C%E6%B6%88%E6%81%AF%E7%BB%9F%E4%B8%80%E7%AE%A1%E7%90%86%E7%B3%BB%E5%88%97%EF%BC%9Asearchtable%E5%8A%A0%E9%80%9F%E6%A3%80%E7%B4%A2)》中介绍如何通过构建Search Table来检索这些消息。 对于Message Bank相关的内容,可参见: https://docs.intersystems.com/healthconnect20201/csp/docbook/DocBook.UI.Page.cls?KEY=EGDV_message_bank 也欢迎与我们联系获得更详细的信息。
文章
Weiwei Gu · 六月 27, 2022

Globals 是管理数据的魔剑 : 第一部分

Globals,这些存储数据的魔剑,已经存在了一段时间,但是没有多少人能够有效地使用它们,也没有多少人知道这个超级武器。 如果你把Globals的东西用在它们真正能发挥作用的地方,其结果可能是惊人的,要么是性能的提高,要么是整体解决方案的大幅简化 (1, 2). Globals提供了一种特殊的存储和处理数据的方式,它与SQL表完全不同。它们在1966年首次出现在 M(UMPS)编程语言中, 该语言最初用于医学数据库。现在它仍然以同样的方式被使用,但也被其他一些以可靠性和高性能为首要任务的行业所采用:金融、交易等。 后来M(UMPS)演变为 Caché ObjectScript (COS). COS是由InterSystems公司开发的,作为M的一个超集. 其原始语言仍然被开发者社区所接受,并在一些实现中保持活力。在网络上有几个活跃的网址,比如:MUMPS Google group, Mumps User's group), effective ISO Standard等等 现代基于Globals的数据库支持交易、日志、复制、分区等。这意味着它们可以被用来构建现代的、可靠的、快速的分布式系统。 Gloabls并不将你限制于关系模型的范围内。它们让你可以自由地创建为特定任务优化的数据结构。对于许多应用来说,合理地使用好的Globals就如一颗真正的银子弹头,它所提供的速度是传统关系型应用的开发者所梦寐以求的。 作为一种存储数据的方法,globals可以在许多现代编程语言中使用,包括高级和低级语言。因此,本文将特别关注Globals本身,而不是它们曾经来自的语言。 Globals 是如何工作的 让我们先了解一下globals是如何工作的,它们有哪些优点。 我们可以从不同的角度来看待globals。在文章的这一部分,我们可将把它们看成是树形状结构或分层的数据存储空间。 简单地说,Globals是一个持久化的数组。一个自动保存在磁盘上的数组。 很难想象有什么比这更简单的方法来存储数据。在程序代码中(用COS/M语言编写),与普通关联数组的唯一区别是站在它们名字前面的^符号。 若将数据保存为Globals, 你可以不需要知道SQL,因为所有必要的命令都非常简单,在一个小时内就可以学会。 让我们从最简单的例子开始,一个有两个分支的单层的树形结构。例子是用COS(Caché ObjectScript) 写的。 Set ^a("+7926X") = "John Sidorov" Set ^a("+7916Y") = "Sergey Smith" 当数据被插入一个Global(Set命令)时,有3件事情会自动发生: 1.将数据保存到磁盘。2.编制索引。括号里的是下标,等号右边的是а节点值。3. 排序。数据是按一个键来排序的。下一次的遍历会把 "Sergey Smith "放到第一个位置,然后是 "John Sidorov"。当从global获得一个用户列表时,数据库不会在排序上花费时间。实际上你可以请求一个从任何键开始的排序列表,甚至是一个不存在的键(输出将从这个键之后的第一个真正的键开始)。所有这些操作都以惊人的速度进行。在我的个人系统(i5-3340,16GB,HDD WD 1TB Blue)上,我设法在一个进程中达到105万次插入/秒。在多核系统上,速度可以达到几千万次/秒 的插入。 当然,记录插入速度本身并不能说明什么。例如,我们可以将数据写入文本文件--根据传言,这就是Visa的处理方式。然而,通过globals,我们得到了一个结构化和索引化的存储,你可以在工作中享受其高速和易用性。 globals最大的优势是在其中插入新节点的速度。 数据在全局中总是有索引的。单层和深入的树形遍历总是非常快的。 让我们在global中添加一些二级和三级的分支看看: Set ^a("+7926X", "city") = "Moscow" Set ^a("+7926X", "city", "street") = "Req Square" Set ^a("+7926X", "age") = 25 Set ^a("+7916Y", "city") = "London" Set ^a("+7916Y", "city", "street") = "Baker Street" Set ^a("+7916Y", "age") = 36 显然,你可以使用globals建立多层级的“树”。由于每次插入后的自动索引,因而其对任何节点的访问几乎都是即时的。任何一级的树枝都可以按一个键进行排序。 正如你所看到的,数据可以被存储在键和值中。一个键的综合长度(所有索引的长度之和)可以达到511字节,而Caché中的值可以达到3.6MB的大小。树中的层数(维数)上限为31。 还有一件很酷的事情:你可以在不定义顶级节点的值的情况下建立一棵“树”。 Set ^b("a", "b", "c", "d") = 1 Set ^b("a", "b", "c", "e") = 2 Set ^b("a", "b", "f", "g") = 3 空的圆圈是没有值的节点。 为了更好地理解globals,让我们把它们与其他树进行比较:所谓“花园树”和“文件系统名称树”。 让我们把globals与最熟悉的层次结构进行比较:如下的Orchard tree-“生长在花园和田野中的普通树”,以及File system-文件系统。 我们可以看到,叶子和果实只生长在普通树木的枝干末端。文件系统--信息也被存储在树枝的末端,也被称为全文件名。 而下面这里是一个Global的数据结构: 不同: 1.内部节点:Global中的信息可以存储在每个节点中,而不是只存储在分支末端。2.外部节点:globals必须有定义的分支末端(有值的末端),这对文件系统和"花园树"来说不是强制性的。 关于内部节点,我们可以把global的结构看作是文件系统的名字树和花园树结构的超集。所以global的结构是一个更灵活的结构。 一般来说,global是一个结构化的树,支持在每个节点中保存数据。 为了更好地理解globals是如何工作的,让我们想象一下,如果文件系统的创建者使用与globals相同的方法来存储信息,会发生什么? 1. 如果一个文件夹中的最后一个文件被删除了,那么这个文件夹本身以及所有只包含这个被删除的文件夹的高层文件夹也会被删除。 2. 这样就根本不需要文件夹了。会有带子文件的文件和不带子文件的文件。如果你把它与普通的树作比较,每个分支都会变成一个果实。 3. 像README.txt这样的东西可能就不再需要了。所有你需要说的关于文件夹的内容都可以写在文件夹文件本身。一般来说,文件名和文件夹名是没有区别的(例如,/etc/readme可以是文件夹,也可以是文件),这意味着我们只需要操作文件就可以了。 4. 带有子文件夹和文件的文件夹可以更快地被删除。网络上有一些文章讲述了删除数百万个小文件是多么的耗时和困难(1, 2, 3). 然而,如果你创建一个基于Global的假的文件系统,它将只需要几秒钟甚至几分之一秒。当我在家里的电脑上测试删除子树时,我成功地从HDD(不是SDD)上的两级树上删除了96-341万个节点。值得一提的是,我们讨论的是删除Global树的一部分,而不是删除包含Global的整个文件。 子树的移除是globals的另一个优势:你不需要递归来做这个。它的速度快得令人难以置信。 在我们的树中,这可以通过一个Kill的命令来完成。 Kill ^a("+7926X") 下面是一个小表格,它可以让你更好地了解你可以在Global上执行的操作 Cache object script中与Globals有关的关键命令和功能 Set设置 设置(初始化)分支到一个节点(如果未定义)和节点值 Merge合并 复制一棵子树 Kill 删除一棵字树 ZKill 删除一个特定节点的值。源自该节点的子树不受影响。 $Query 对树进行全面深入的遍历 $Order 返回同一级别的下一个下标 $Data 检查一个节点是否被定义 $Increment 节点值的原子递增,以避免ACID的读和写。最新的建议是使用 $Sequence 来代替 感谢你的关注,我很乐意回答你的任何问题。 免责声明:本文反映了作者的个人观点,与InterSystems的官方立场无关。 让我们期待下一篇继续 "Globals 是存储数据的魔剑-树 :第二部分 (待翻译) 你将了解到哪些类型的数据可以显示在globals中,以及它们在哪些地方效果最好。 好文!
文章
姚 鑫 · 八月 22, 2022

第九章 配置数据库(一)

# 第九章 配置数据库(一) 数据库是使用数据库向导创建的 `IRIS.DAT` 文件。 `IRIS`数据库保存称为全局变量的多维数组中的数据和称为例程的可执行内容,以及类和表定义。 全局变量和例程包括方法、类、网页、SQL、BASIC和JavaScript文件 **注意:在 `Windows` 系统上,不要对 `IRIS.DAT` 数据库文件使用文件压缩。 (通过右键单击 `Windows` 资源管理器中的文件或文件夹并选择属性,然后选择高级,然后压缩内容以节省磁盘空间来压缩文件;压缩后,文件夹名称或文件名在 `Windows` 资源管理器中呈现为蓝色。)如果压缩`IRIS.DAT` 文件,它所属的实例将无法启动,并出现误导性错误。** `IRIS` 数据库根据需要动态扩展(假设有可用空间),但可以指定最大大小。如果使用默认的 `8KB` 块大小,数据库可以增长到 `32 TB`。 可以动态更改大多数数据库配置;可以在系统运行时创建和删除数据库以及修改数据库属性。 **注意:这些主题描述了使用管理门户手动配置数据库的过程。 `IRIS` 还包含可用于自动化数据库配置的编程工具。可以使用新选项卡类中的 `Config.Databases` 来创建和配置数据库;还可以使用 `^DATABASE` 命令行实用程序配置数据库。** **配置数据库的另一种方法是将 `CreateDatabase`、`ModifyDatabase` 或 `DeleteDatabase` 操作与配置合并结合使用。配置合并允许通过应用声明性合并文件来自定义 `IRIS` 实例,该文件指定要应用于该实例的设置和操作。** # Background `IRIS` 将数据——持久多维数组(`globals`)以及可执行代码(例程)——存储在一个或多个称为数据库的物理结构中。数据库由存储在本地操作系统中的一个或多个物理文件组成。一个 `IRIS` 系统可能(并且通常确实)有多个数据库。 每个 `IRIS` 系统都维护一个数据库缓存——一个本地共享内存缓冲区,用于缓存从物理数据库中检索到的数据。这种高速缓存大大减少了访问数据所需的昂贵 `I/O` 操作的数量,并提供了 `IRIS` 的许多性能优势。 `IRIS` 应用程序通过命名空间访问数据。命名空间提供存储在一个或多个物理数据库中的数据(全局变量和例程)的逻辑视图。一个 `IRIS` 系统可能(并且通常确实)有多个命名空间。 `IRIS` 将逻辑命名空间中可见的数据映射到一个或多个物理数据库。这种映射为应用程序提供了一种强大的机制,可以在不更改应用程序逻辑的情况下更改应用程序的物理部署。 在最简单的情况下,命名空间和数据库之间存在一一对应关系,但许多系统利用定义命名空间的能力来提供对多个数据库中数据的访问。例如,一个系统可以有多个命名空间,每个命名空间提供存储在一个或多个物理数据库中的数据的不同逻辑视图。 # 数据库注意事项 ## 数据库总限制 可以在单个 `IRIS` 实例中配置的数据库数量的绝对限制(如果有足够的存储空间)是 `15,998`。其他限制如下: - 数据库的目录信息不能超过 `256 KB`。这意味着,如果数据库目录名称的平均长度较长,则实例可以拥有较少的数据库总数。以下公式描述了这种关系: ```math maximum_DBs = 258048/ (avg_DB_path_length + 3) ``` 例如,如果所有数据库目录路径的格式为 `c:\InterSystems\IRIS\mgr\DBNNNN\`,则平均长度为 `33` 个字节。因此,最大数据库数为 `7,168`,计算如下:`258048/ (33 + 3) = 7168`。 - 镜像数据库在 `15,998` 的绝对限制中计数两次。如果实例上的所有数据库都进行了镜像,则有效限制为 `7,499` 个数据库。这是因为 `IRIS` 为镜像数据库创建了两个数据库定义;一个用于目录路径 (`c:\InterSystems\IRIS\mgr\DBNNNN\`),另一个用于镜像定义 (`:mirror:MIRRORNAME:MirrorDBName`)。 - 可以同时使用的数据库数量受操作系统对打开文件数量(每个进程或系统范围)的限制的限制。 `IRIS` 将大约一半的操作系统打开文件分配留给自己和设备使用。 ## 数据库配置注意事项 以下是配置数据库时要考虑的提示: - `IRIS` 提供了一个无缝选项,可以在多个物理数据库 (`IRIS.DAT`) 文件中传播数据。因此,可以根据需要构建具有多个数据库的应用程序或通过全局或下标级映射拆分数据。 - 根据可用于管理任务(如备份、恢复、完整性检查等)的基础设施,将数据库大小保持在可管理的范围内。 - 建议将流全局变量(如果将流存储在 `IRIS.DAT` 数据库文件中)全局映射到单独的数据库,并且将流数据库配置为大 (`64 KB`) 块大小。 - 根据工作负载,考虑替代(更大)块大小可能比默认的 `8 KB` 数据库块大小更有利。 ## 大数据块大小注意事项 除了 `IRIS` 支持的 `8 KB`(默认)块大小(始终启用)之外,还可以启用以下块大小: - `16 KB (16384)` - `32 KB (32768)` - `64 KB (65536)` 但是,在创建使用大块的数据库时应该谨慎,因为使用它们会影响系统的性能。 在启用和使用大的块大小之前,请考虑以下几点: - 如果应用程序工作负载主要由顺序插入或顺序读取/查询组成,那么大的块大小可以提高性能。 - 如果应用程序工作负载主要由随机插入或随机读取/查询组成,那么大的块大小可能会降低性能。 由于对于给定的数据库缓存总大小,较大的块大小会导致缓存更少的块,为了减少对随机数据库访问的影响,还应该考虑将更多的总内存用作数据库缓存。 - 对于索引类型的数据库,默认的块大小(`8 KB`)确保最佳性能; 较大的块大小可能会降低性能。 如果正在考虑为数据设置更大的块大小,那么应该考虑将索引全局变量映射到一个单独的`8 KB`块大小的数据库。 要创建一个使用不支持的块大小的数据库,请执行以下操作: 1. 使用启动设置页面(系统管理>附加设置>启动)的设置启用块大小,在配置参数文件引用的`DBSizesAllowed`条目中描述。 2. 在启动设置页面(系统管理>附加设置>启动),按照内存和启动设置中的描述,为启用的块大小配置数据库缓存。 3. 重新启动 4. 按照创建本地数据库中的说明创建数据库。 # 数据库兼容性注意事项 如创建本地数据库过程中所述,可以通过复制或移动 `IRIS.DAT` 文件将 `IRIS` 数据库复制或移动到创建它的实例之外的实例,或临时装载在另一个实例中创建的数据库在同一个系统上。还可以将数据库的备份(请参阅数据完整性指南的“备份和恢复”一章)恢复到其原始实例以外的实例。但是,为避免数据不兼容,必须满足以下要求: - 目标(新)实例必须使用相同的字符宽度(`8`位或`Unicode`; 请参阅安装指南中的新选项卡中的字符宽度设置),并使用相同的区域设置(请参阅使用管理门户的NLS设置页面)作为创建数据库的实例。 此要求的一个例外是使用基于 `ISO 8859 Latin-1` 字符集的区域设置的 `8` 位实例与使用相应宽字符区域设置的 `Unicode` 实例兼容。例如,使用 `enu8` 语言环境在 `8` 位实例中创建的数据库可以在使用 `enuw` 语言环境的 `Unicode` 实例中使用。 - 如果源实例和目标实例位于不同字节序的系统上,则数据库必须转换为目标实例的字节序后才能使用。 根据平台的不同,多字节数据存储在最低内存地址(即首先)中的最高有效字节或最低有效字节:当最高有效字节首先存储时,称为“大端;”当首先存储最低有效字节时,它被称为“小端”。 当使用在不同端序的系统上创建的现有`IRIS.DAT`定义数据库时,请在使用数据库之前使用`cvendian`实用程。
文章
Michael Lei · 十二月 7, 2022

ECP 与 Docker

大家好! 这是关于使用 Docker 初始化 IRIS 实例的系列文章中的第三篇。 这次,我们将关注企业缓存协议(**E**nterprise **C**ache **P**rotocol,ECP)。 ECP 允许以一种非常简单的方式将某些 IRIS 实例配置为应用程序服务器,将其他实例配置为数据服务器。 有关详细的技术信息,请参阅官方文档。 本文旨在介绍: * 如何编写数据服务器的初始化脚本,以及如何编写一个或多个应用程序服务器的初始化脚本。 * 如何使用 Docker 在这些节点之间建立加密连接。 为此,我们通常使用我们在以前的 Web 网关中已经看到的一些工具,以及描述 OpenSSL、envsubst 和 Config-API 等工具的镜像文章。 ## 要求 ECP 不适用于 IRIS 社区版。 因此,需要访问全球响应中心才能下载容器许可证并连接到 containers.intersystems.com 注册表。 ## 准备系统 系统必须与容器共享一些本地文件。 需要创建特定用户和组来避免出现“访问被拒绝”错误。 ```bash sudo useradd --uid 51773 --user-group irisowner sudo useradd --uid 52773 --user-group irisuser sudo groupmod --gid 51773 irisowner sudo groupmod --gid 52773 irisuser ``` 如果您还没有“iris.key”许可证,请从 WRC 下载,并将其添加到您的主目录中。 ## 检索示例存储库 除“iris.key”许可证外,您需要的所有其他文件都可以在公共存储库中找到,因此,首先将其克隆: ```bash git clone https://github.com/lscalese/ecp-with-docker.git cd ecp-with-docker ``` ## SSL 证书 为了加密应用程序服务器与数据服务器之间的通信,我们需要 SSL 证书。 可以使用现成的脚本(“gen-certificates.sh”)。 但是,您可以随意修改脚本,使证书设置与您的位置、公司等保持一致。 执行: ```bash sh ./gen-certificates.sh ``` 生成的证书现在位于“./certificates”目录中。 | 文件 | 容器 | 描述 | | ------------------------------ | ------------- | ---------------- | | ./certificates/CA_Server.cer | 应用程序服务器和数据服务器 | 机构服务器证书 | | ./certificates/app_server.cer | 应用程序服务器 | IRIS 应用程序服务器实例证书 | | ./certificates/app_server.key | 应用程序服务器 | 相关私钥 | | ./certificates/data_server.cer | 数据服务器 | IRIS 数据服务器实例证书 | | ./certificates/data_server.key | 数据服务器 | 相关私钥 | ## 构建镜像 首先,登录 Intersystems docker 注册表。 在构建期间,将从注册表中下载基础镜像: ```bash docker login -u="YourWRCLogin" -p="YourICRToken" containers.intersystems.com ``` 如果您不知道自己的Token,请使用您的 WRC 帐户登录 https://containers.intersystems.com/。 在此构建过程中,我们将向 IRIS 基础镜像添加一些软件实用程序: * **gettext-base**:它将允许我们使用“envsubst”命令替换配置文件中的环境变量。 * **iputils-arping**:如果我们想要镜像数据服务器,则需要使用此实用程序。 * **ZPM**:ObjectScript 软件包管理器。 [Dockerfile](https://github.com/lscalese/ecp-with-docker/blob/master/Dockerfile): ``` ARG IMAGE=containers.intersystems.com/intersystems/iris:2022.2.0.281.0 # Don't need to download the image from WRC. It will be pulled from ICR at build time. FROM $IMAGE USER root # Install iputils-arping to have an arping command. It's required to configure Virtual IP. # Download the latest ZPM version (ZPM is included only with community edition). RUN apt-get update && apt-get install iputils-arping gettext-base && \ rm -rf /var/lib/apt/lists/* USER ${ISC_PACKAGE_MGRUSER} WORKDIR /home/irisowner/demo RUN --mount=type=bind,src=.,dst=. \ iris start IRIS && \ iris session IRIS < iris.script && \ iris stop IRIS quietly ``` 此 Dockerfile 中除最后一行外没有什么特别之处。 它将 IRIS 数据服务器实例配置为最多接受 3 个应用程序服务器。 请注意,此配置需要重新启动 IRIS。 我们在构建过程中分配此参数的值,以避免稍后编写重新启动脚本。 开始构建: ```bash docker-compose build –no-cache ``` ## 配置文件 在配置 IRIS 实例(应用程序服务器和数据服务器)时,我们使用 JSON config-api 文件格式。 您会注意到这些文件包含环境变量 "${variable_name}"。 它们的值在“docker-compose.yml”文件的“environment”部分定义,我们稍后将在本文档中看到。 这些变量将在使用“envsubst”实用程序加载文件之前被替换掉。 ### 数据服务器 对于数据服务器,我们将: * 启用 ECP 服务并定义授权客户端(应用程序服务器)列表。 * 创建加密通信所需的“SSL %ECPServer”配置。 * 创建“myappdata”数据库。 它将用作来自应用程序服务器的远程数据库。 (data-serer.json)[https://github.com/lscalese/ecp-with-docker/blob/master/config-files/data-server.json] ```json { "Security.Services" : { "%Service_ECP" : { "Enabled" : true, "ClientSystems":"${CLIENT_SYSTEMS}", "AutheEnabled":"1024" } }, "Security.SSLConfigs": { "%ECPServer": { "CAFile": "${CA_ROOT}", "CertificateFile": "${CA_SERVER}", "Name": "%ECPServer", "PrivateKeyFile": "${CA_PRIVATE_KEY}", "Type": "1", "VerifyPeer": 3 } }, "Security.System": { "SSLECPServer":1 }, "SYS.Databases":{ "/usr/irissys/mgr/myappdata/" : {} }, "Databases":{ "myappdata" : { "Directory" : "/usr/irissys/mgr/myappdata/" } } } ``` 此配置文件由“init_datasrv.sh”脚本在数据服务器容器启动时加载。 连接到数据服务器的所有应用程序服务器都必须可信。 此脚本将在 100 秒内自动验证所有连接,以限制管理门户中的手动操作。 当然,可以对其进行改进以提高安全性。 ### 应用程序服务器 对于应用程序服务器,我们将: * 启用 ECP 服务。 * 创建通信加密所需的 SSL 配置“%ECPClient”。 * 配置与数据服务器的连接信息。 * 创建远程数据库“myappdata”的配置。 * 在“USER”命名空间中创建到“myappdata”数据库的全局映射“demo.*”。 它可以让我们稍后测试 ECP 的运行。 [app-server.json](https://github.com/lscalese/ecp-with-docker/blob/master/config-files/app-server.json): ```json { "Security.Services" : { "%Service_ECP" : { "Enabled" : true } }, "Security.SSLConfigs": { "%ECPClient": { "CAFile": "${CA_ROOT}", "CertificateFile": "${CA_CLIENT}", "Name": "%ECPClient", "PrivateKeyFile": "${CA_PRIVATE_KEY}", "Type": "0" } }, "ECPServers" : { "${DATASERVER_NAME}" : { "Name" : "${DATASERVER_NAME}", "Address" : "${DATASERVER_IP}", "Port" : "${DATASERVER_PORT}", "SSLConfig" : "1" } }, "Databases": { "myappdata" : { "Directory" : "/usr/irissys/mgr/myappdata/", "Name" : "${REMOTE_DB_NAME}", "Server" : "${DATASERVER_NAME}" } }, "MapGlobals":{ "USER": [{ "Name" : "demo.*", "Database" : "myappdata" }] } } ``` 配置文件由“[init_appsrv.sh](https://github.com/lscalese/ecp-with-docker/blob/master/init_appsrv.sh)”脚本在应用程序服务器容器启动时加载。 ## 启动容器 现在,我们可以启动容器: * 2 个应用程序服务器。 * 1 个数据服务器。 为此,请运行: docker-compose up –scale ecp-demo-app-server=2 请参阅 [docker-compose](https://github.com/lscalese/ecp-with-docker/blob/master/docker-compose.yml) 文件以了解详情: ``` # Variables are defined in .env file # to show the resolved docker-compose file, execute # docker-compose config version: '3.7' services: ecp-demo-data-server: build: . image: ecp-demo container_name: ecp-demo-data-server hostname: data-server networks: app_net: environment: # List of allowed ECP clients (application server). - CLIENT_SYSTEMS=ecp-with-docker_ecp-demo-app-server_1;ecp-with-docker_ecp-demo-app-server_2;ecp-with-docker_ecp-demo-app-server_3 # Path authority server certificate - CA_ROOT=/certificates/CA_Server.cer # Path to data server certificate - CA_SERVER=/certificates/data_server.cer # Path to private key of the data server certificate - CA_PRIVATE_KEY=/certificates/data_server.key # Path to Config-API file to initiliaze this IRIS instance - IRIS_CONFIGAPI_FILE=/home/irisowner/demo/data-server.json ports: - "81:52773" volumes: # Post start script - data server initilization. - ./init_datasrv.sh:/home/irisowner/demo/init_datasrv.sh # Mount certificates (see gen-certificates.sh to generate certificates) - ./certificates/app_server.cer:/certificates/data_server.cer - ./certificates/app_server.key:/certificates/data_server.key - ./certificates/CA_Server.cer:/certificates/CA_Server.cer # Mount config file - ./config-files/data-server.json:/home/irisowner/demo/data-server.json # IRIS License - ~/iris.key:/usr/irissys/mgr/iris.key command: -a /home/irisowner/demo/init_datasrv.sh ecp-demo-app-server: image: ecp-demo networks: app_net: environment: # Hostname or IP of the data server. - DATASERVER_IP=data-server - DATASERVER_NAME=data-server - DATASERVER_PORT=1972 # Path authority server certificate - CA_ROOT=/certificates/CA_Server.cer - CA_CLIENT=/certificates/app_server.cer - CA_PRIVATE_KEY=/certificates/app_server.key - IRIS_CONFIGAPI_FILE=/home/irisowner/demo/app-server.json ports: - 52773 volumes: # Post start script - application server initilization. - ./init_appsrv.sh:/home/irisowner/demo/init_appsrv.sh # Mount certificates - ./certificates/CA_Server.cer:/certificates/CA_Server.cer # Path to private key of the data server certificate - ./certificates/app_server.cer:/certificates/app_server.cer # Path to private key of the data server certificate - ./certificates/app_server.key:/certificates/app_server.key # Path to Config-API file to initiliaze this IRIS instance - ./config-files/app-server.json:/home/irisowner/demo/app-server.json # IRIS License - ~/iris.key:/usr/irissys/mgr/iris.key command: -a /home/irisowner/demo/init_appsrv.sh networks: app_net: ipam: driver: default config: # APP_NET_SUBNET variable is defined in .env file - subnet: "${APP_NET_SUBNET}" ``` ## 我们来测试一下! ### 访问数据服务器管理门户 容器已启动。 我们从数据服务器中检查一下状态。 端口 52773 映射到本地端口 81,因此可以使用此地址 [http://localhost:81/csp/sys/utilhome.csp](http://localhost:81/csp/sys/utilhome.csp) 进行访问 使用默认登录名\密码登录,然后转到 System -> Configuration -> ECP Params(系统 -> 配置 -> ECP 参数)。 点击“ECP Application Servers”(ECP 应用程序服务器)。 如果一切正常,您应该会看到 2 个状态为“Normal”(正常)的应用程序服务器。 客户端名称的结构为 "数据服务器名称":"应用程序服务器主机名":"IRIS 实例名称"。 本例中,我们没有设置应用程序服务器主机名,因此我们将获得自动生成的主机名。 ![应用程序服务器列表](https://raw.githubusercontent.com/lscalese/ecp-with-docker/master/img/app-server-list-en.png) ### 访问应用程序服务器管理门户 要连接到应用程序服务器的管理门户,首先需要获取端口号。 由于我们使用了“--scale”选项,我们无法在 docker-compose 文件中设置端口。 因此,必须使用 `docker ps` 命令检索它们: ``` docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a1844f38939f ecp-demo "/tini -- /iris-main…" 25 minutes ago Up 25 minutes (unhealthy) 1972/tcp, 2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:81->52773/tcp, :::81->52773/tcp ecp-demo-data-server 4fa9623be1f8 ecp-demo "/tini -- /iris-main…" 25 minutes ago Up 25 minutes (unhealthy) 1972/tcp, 2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:49170->52773/tcp, :::49170->52773/tcp ecp-with-docker_ecp-demo-app-server_1 ecff03aa62b6 ecp-demo "/tini -- /iris-main…" 25 minutes ago Up 25 minutes (unhealthy) 1972/tcp, 2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:49169->52773/tcp, :::49169->52773/tcp ecp-with-docker_ecp-demo-app-server_2 ``` 在本示例中,端口: * 49170,用于第一个应用程序服务器 http://localhost:49170/csp/sys/utilhome.csp * 49169,用于第二个应用程序服务器 http://localhost:49169/csp/sys/utilhome.csp ![数据服务器](https://raw.githubusercontent.com/lscalese/ecp-with-docker/master/img/data-server-status-en.png) ### 远程数据库上的读/写测试 我们在终端中执行一些读/写测试。 在第一个应用程序服务器上打开一个 IRIS 终端: ``` docker exec -it ecp-with-docker_ecp-demo-app-server_1 iris session iris Set ^demo.ecp=$zdt($h,3,1) _ “ write from the first application server.” ``` 现在,在第二个应用程序服务器上打开一个终端: ``` docker exec -it ecp-with-docker_ecp-demo-app-server_2 iris session iris Set ^demo.ecp(2)=$zdt($h,3,1) _ " write from the second application server." zwrite ^demo.ecp ``` 您应该会看到两个服务器中的响应: ``` ^demo.ecp(1)="2022-07-05 23:05:10 write from the first application server." ^demo.ecp(2)="2022-07-05 23:07:44 write from the second application server." ``` 最后,在数据服务器上打开一个 IRIS 终端并执行全局 demo.ecp 读取: ``` docker exec -it ecp-demo-data-server iris session iris zwrite ^["^^/usr/irissys/mgr/myappdata/"]demo.ecp ^["^^/usr/irissys/mgr/myappdata/"]demo.ecp(1)="2022-07-05 23:05:10 write from the first application server." ^["^^/usr/irissys/mgr/myappdata/"]demo.ecp(2)="2022-07-05 23:07:44 write from the second application server." ``` 希望大家喜欢这篇文章。 欢迎您发表评论。
文章
Michael Lei · 六月 2, 2022

部分IRIS 2022 年度编程大奖赛作品展示—— 利用IRIS 互操作Production接受邮件消息

IRIS Interoperability互操作性/HealthConnect(前身是Ensemble)有许多内置的适配器。但是没有一个接收邮件的服务或适配器。我洗的了一个电子邮件服务,通过SMTP接收邮件,这些邮件可以被传递到电子邮件操作。 现在我想对一个使用电子邮件操作向外部邮件服务器发送邮件的Production进行负载测试。邮件服务器团队不希望我向他们发送成千上万的信息。 我创建了iris-mail应用程序来替代邮件服务器。我更新了电子邮件操作中的服务器和端口设置。外发的邮件被发送到替代的邮件服务器,我能够计算出iris-mail中收到的邮件数量,并将其与邮件操作发送的邮件数量进行比较。 应用程序的源代码:https://openexchange.intersystems.com/package/iris-mail
文章
姚 鑫 · 六月 20, 2021

第十三章 将XML文档表示为DOM

# 第十三章 将XML文档表示为DOM `%XML.Document`类和`%XML.Node`类使可以将任意XML文档表示为DOM(文档对象模型)。然后,可以导航此对象并对其进行修改。还可以创建一个新的DOM并将其添加到其中。 **注意:使用的任何XML文档的XML声明都应该指明该文档的字符编码,并且文档应该按照声明的方式进行编码。如果未声明字符编码,InterSystems IRIS将使用本书前面的“输入和输出的字符编码”中描述的默认值。如果这些默认值不正确,请修改XML声明,使其指定实际使用的字符集。** # 将XML文档作为DOM打开 要打开现有XML文档以用作DOM,请执行以下操作: 1. 创建`%XML.Reader`的实例。 2. 也可以指定此实例的`Format`属性,以指定要导入的文件的格式。 **默认情况下, IRIS假定XML文件为文字格式。如果文件是SOAP编码格式,则必须指明这一点,以便可以正确读取该文件。** 除非使用`Correlate()`和`Next()`,否则此属性无效。 3. 请使用`%XML.Reader`的以下方法之一。 - `OpenFile()` — 打开一个文件。 - `OpenStream()` —打开一个流。 - `OpenString()` — 打开字符串。 - `OpenURL()` — 打开URL。 在每种情况下,都可以选择为该方法指定第二个参数,以重写`Format`属性的值。 4. 访问`Document`属性,它是一个DOM。此属性是`%XML.Document`实例,它提供了可用于查找有关整个文档的信息的方法。例如,`CountNamespace()`返回DOM使用的名称空间总数。 或者,如果流包含XML文档,调用`%XML.Document`的`GetDocumentFromStream()`方法。返回`%XML.Document`的实例。 ## 示例1:将文件转换为DOM 例如,下面的方法读取一个XML文件,并在表示该文档的返回`%XML.Document`的一个实例: ```java ClassMethod GetXMLDocFromFile(file) As %XML.Document { s reader = ##class(%XML.Reader).%New() s status = reader.OpenFile(file) if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF} s document = reader.Document q document } ``` ## 示例2:将对象转换为DOM 以下方法接受`OREF`,并在表示该对象中返回`%XML.Document`的实例。该方法假定`OREF`是启用XML的类的实例: ```java ClassMethod GetXMLDoc(object) As %XML.Document { //确保这是启用XML的类的实例 if '$IsObject(object){ w "参数不是对象" q $$$NULLOREF } s classname = $CLASSNAME(object) s isxml = $CLASSMETHOD(classname,"%Extends","%XML.Adaptor") if 'isxml { w "参数不是启用XML的类的实例" q $$$NULLOREF } //步骤1-将对象作为XML写入流 s writer = ##class(%XML.Writer).%New() s stream = ##class(%GlobalCharacterStream).%New() s status = writer.OutputToStream(stream) if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF} s status = writer.RootObject(object) if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF} //步骤2-从流中提取%XML.Document s status = ##class(%XML.Document).GetDocumentFromStream(stream,.document) if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF} quit document } ``` # 获取DOM的名称空间 当 IRIS读取XML文档并创建DOM时,它会标识文档中使用的所有名称空间,并为每个名称空间分配一个索引号。 在`%XML.Document`实例提供了以下方法,可以使用这些方法查找有关文档中命名空间的信息: ### CountNamespace() 返回文档中的命名空间数。 ### FindNamespace() 返回与给定命名空间对应的索引。 ### GetNamespace() 返回给定索引的XML命名空间URI。 下面的示例方法显示一个报表,其中显示文档中使用的命名空间: ```java ClassMethod ShowNamespaces(doc As %XML.Document) { s count = doc.CountNamespace() w !, "文档中的命名空间数: "_count for i = 1 : 1 : count { w !, "Namespace "_i_" is "_doc.GetNamespace(i) } } ``` # 导航DOM的节点 要访问文档的节点,可以使用两种不同的技术: - 使用`%XML.Document`实例的`GetNode()`方法。此方法接受一个整数,它指示从1开始的节点号。 - 调用`%XML.Document`实例的`GetDocumentElement()`方法。 此方法返回`%XML.Node`的实例,提供用于访问有关根节点的信息以及移动到其他节点的属性和方法。以下小节提供了有关使用`%XML.Node`的详细信息。 ## 移动到子节点或同级节点 要移动到子节点或同级节点,请使用`%XML.Node`实例的以下方法。: - `MoveToFirstChild()` - `MoveToLastChild()` - `MoveToNextSibling()` - `MoveToPreviousSibling()` 这些方法中的每一个都移动到另一个节点(如方法名称所示)。如果是,则该方法返回TRUE。如果不是,则返回False,焦点与调用该方法之前相同。 这些方法中的每一个都有一个可选参数`skipWhitespace`。如果此参数为真,则该方法将忽略任何空格。`SkipWhitespace`的默认值为false。 ## 移动到父节点 要移动到当前节点的父节点,请使用`%XML.Node`实例的`MoveToParent()`方法。 此方法接受一个可选参数`restrictDocumentNode`。如果此参数为真,则该方法不会移动到文档节点(根)。`restrictDocumentNode`的默认值为False。 ## 移动到特定节点 要移动到特定节点,可以设置`%XML.Node`实例的`NodeId`属性。例如: ```java set saveNode = node.NodeId //..... lots of processing //... // restore position set node.NodeId=saveNode ``` ## 使用id属性 在某些情况下,XML文档可能包括名为`id`的属性,该属性用于标识文档中的不同节点。例如: ```java Jack O'Neill Samantha Carter Daniel Jackson ``` 如果(如本例所示)文档使用名为`id`的属性,则可以使用它导航到该节点。为此,可以使用文档的`GetNodeById()`方法,该方法返回`%XML.Node`的一个实例。(请注意,与大多数其他导航方法不同,此方法可从`%XML.Document`,而不是`%XML.Node`。)
文章
姚 鑫 · 六月 11, 2021

第四章 添加命名空间声明

# 第四章 添加命名空间声明 # 添加命名空间声明 ## 默认行为 在`%XML.Writer`会自动插入命名空间声明,生成命名空间前缀,并在适当的地方应用前缀。例如,以下类定义: ```java Class Sample.Person Extends (%Persistent, %Populate, %XML.Adaptor) { Parameter NAMESPACE = "http://www.yaoxin.com"; } ``` 如果导出此类的多个对象,则会看到类似以下内容: ```java DHC-APP> w ##class(Demo.XmlDemo).Obj2Xml(1) yaoxin 111-11-1117 1990-04-25 889 Clinton Drive St Louis WI 78672 9619 Ash Avenue Ukiah AL 56589 濮氶懌 111-11-1115 Red Orange Yellow Green Red Orange Yellow 31 ``` 名称空间声明会自动添加到每个`` 元素。只将其添加到文档的根目录。 ## 手动添加声明 可以控制何时将命名空间引入XML输出。以下方法都会影响所写入的下一个元素(但不会影响该元素之后的任何元素)。为方便起见,其中几种方法添加了标准的`W3`名称空间。 通常使用这些方法将命名空间声明添加到文档的根元素;也就是说,在调用`RootObject()`或`RootElement()`之前调用其中一个或多个方法。 注意:这些方法都没有将任何元素分配给名称空间,并且这些名称空间永远不会作为默认名称空间添加。在生成特定元素时,需要指明它使用的名称空间,如后面的“编写根元素”和“生成XML元素”中所述。 ### AddNamespace() ```java method AddNamespace(namespace As %String, prefix As %String, schemaLocation As %String) as %Status ``` 添加指定的命名空间。这里,`Namespace`是要添加的名称空间,`Prefix`是该名称空间的可选前缀,`schemaLocation`是指示相应架构位置的可选URI。 如果未指定前缀,则会自动生成前缀(格式为S01、S02等)。 下面的示例显示了此方法的效果。首先,假设`Person`类被分配给一个名称空间(类参数中的`NAMESPACE`)。如果在未首先调用`AddNamespace()`方法的情况下生成此类实例的输出,则可能会收到如下所示的输出: ```java Love,Bart Y. ... ``` 或者,在编写根元素之前按如下方式调用`AddNamespace()`方法: ```java set status=writer.AddNamespace("http:///www.person.org","p") ``` 如果随后生成根元素,则输出如下所示: ```java ... ``` 或者,假设在调用`AddNamespace()`方法时指定了第三个参数,该参数提供了关联架构的位置: ```java set status=writer.AddNamespace("http:///www.person.org","p","http://www.MyCompany.com/schemas/person.xsd") ``` 在这种情况下,如果随后生成Root元素,则输出如下所示: ```java ... ``` ### AddInstanceNamespace() ```java method AddInstanceNamespace(prefix As %String) as %Status ``` 添加W3架构实例命名空间。这里的前缀是用于此命名空间的可选前缀。默认前缀为`XSI`。 ```java ... ``` ### AddSchemaNamespace() ```java method AddSchemaNamespace(prefix As %String) as %Status ``` 添加`W3`架构命名空间。这里的前缀是用于此命名空间的可选前缀。默认前缀为`s`。 ```java ... ``` ### AddSOAPNamespace() ```java method AddSOAPNamespace(soapPrefix As %String, schemaPrefix As %String, xsiPrefix As %String) as %Status ``` 添加`W3 SOAP`编码命名空间、`SOAP`架构命名空间和`SOAP`架构实例命名空间。此方法有三个可选参数:用于这些命名空间的前缀。默认前缀分别为`SOAP-Enc`、`s`和`XSI`。 ```java ... ``` ### AddSOAP12Namespace() ```java method AddSOAP12Namespace(soapPrefix As %String, schemaPrefix As %String, xsiPrefix As %String) as %Status ``` 添加`W3 SOAP 1.2`编码命名空间、`SOAP`架构命名空间和`SOAP`架构实例命名空间。 ```java ... ``` 可以使用这些方法中的多个方法。如果使用其中的多个命名空间,则受影响的元素将包含所有指定命名空间的声明。 # 编写根元素 每个XML文档必须恰好包含一个根元素。有两种方法可以创建此元素: - 根元素可能直接对应于一个启用了InterSystems IRIS XML的对象。 在本例中,使用`RootObject()`方法,该方法将指定的启用XML的对象作为根元素写入。输出包括该对象中包含的所有对象引用。根元素获取该对象的结构,不能插入其他元素您可以指定根元素的名称,也可以使用由启用XML的对象定义的默认值。 前面的示例使用了此技术。 - 根元素可能只是一组元素的包装器(可能是一组支持XML的对象)。 在本例中,使用`RootElement()`方法,该方法插入具有指定名称的根级元素。如果此文档缩进,此方法还会增加后续操作的缩进级别。 然后调用其他方法为根元素内的一个或多个元素生成输出。在根目录中,可以按照选择的任何顺序或逻辑包含所需的元素。之后,调用`EndRootElement()`方法关闭根元素。 在这两种情况下,都可以指定要用于根元素的命名空间,只有在启用了`XML`的类没有`Namespace`参数值的情况下才会应用该命名空间。 请记住,如果文档包含文档类型声明,则该`DTD`的名称必须与根元素的名称相同。
文章
姚 鑫 · 六月 12, 2021

第五章 生成XML元素

# 第五章 生成XML元素 # 生成XML元素 如果使用`RootElement()`启动文档的根元素,则负责生成该根元素内的每个元素。有三个选择: ## 将对象生成为元素 可以从InterSystems IRIS对象生成输出作为元素。在本例中,使用`object()`方法,该方法写入支持XML的对象。输出包括该对象中包含的所有对象引用。可以指定此元素的名称,也可以使用在对象中定义的默认值。 只能在`RootElement()`和`EndRootElement()`方法之间使用`object()`方法。 此示例为给定启用XML的类的所有已保存实例生成输出: ```java /// desc:将表里数据输出本地文件里 /// w ##class(PHA.TEST.Xml).WriteAll("Sample.Person") ClassMethod WriteTableAllToXml(cls As %String = "", directory As %String = "E:\temp\") { if '##class(%Dictionary.CompiledClass).%ExistsId(cls) { Write !, "类不存在或未编译" Quit } s check=$classmethod(cls, "%Extends", "%XML.Adaptor") If 'check { Write !, "类不扩展%XML.Adaptor" Quit } s filename = directory_"Person"_".xml" s writer = ##class(%XML.Writer).%New() s writer.Indent=1 s status = writer.OutputToFile(filename) if $$$ISERR(status) { do $System.Status.DisplayError(status) quit } s status=writer.RootElement("SampleOutput") if $$$ISERR(status) { do $System.Status.DisplayError(status) quit } //获取给定类范围内对象的ID s stmt = ##class(%SQL.Statement).%New() s status = stmt.%PrepareClassQuery(cls,"Extent") if $$$ISERR(status) { do $System.Status.DisplayError(status) quit } s rset = stmt.%Execute() while (rset.%Next()) { //对于每个ID,写入该对象 set objid = rset.%Get("ID") set obj = $CLASSMETHOD(cls,"%OpenId",objid) set status = writer.Object(obj) if $$$ISERR(status) {Do $System.Status.DisplayError(status) Quit}} d writer.EndRootElement() d writer.EndDocument() q "" } ``` 此方法的输出包含给定类的所有已保存对象,这些对象嵌套在根元素中。对于`Sample.Person`,输出如下: ```java Tillem,Robert Y. 967-54-9687 1961-11-27 3355 First Court Reston WY 11090 4922 Main Drive Newton NM 98073 Red 47 Waters,Ed X. 361-66-2801 1957-05-29 5947 Madison Drive ... ``` ## 手动构建元素 以手动构造XML元素。在本例中,使用`element()`方法,该方法使用提供的名称写入元素的开始标记。然后,可以编写内容、属性和子元素。使用`EndElement()`方法指示元素的结束。 相关方法如下: ### Element() ```java method Element(tag, namespace As %String) as %Status ``` 写入开始标记。可以为元素提供命名空间,只有在启用了XML的类没有`Namespace`参数的值时才会应用该命名空间。 ### WriteAttribute() ```java method WriteAttribute(name As %String, value As %String = "", namespace As %String, valueNamespace As %String = "", global As %Boolean = 0) as %Status ``` 写入属性。必须指定属性名称和值。参数命名空间是属性名称的命名空间。参数`valueNamespace`是属性值的名称空间;当值在XML模式名称空间中定义时使用。 对于GLOBAL,如果属性在关联的XML架构中是全局的,因此应该有前缀,请指定TRUE。 如果使用此方法,则必须在`Element()`(或`RootElement()`)之后直接使用它。 ### WriteChars() ```java method WriteChars(text) as %Status ``` 写入字符串,执行使该字符串适合作为元素内容所需的任何必要转义。参数必须`%String`类型或`%CharacterStream`类型。 ### WriteCData() ```java method WriteCData(text) as %Status ``` 参数必须`%String`类型或`%CharacterStream`类型。 ### WriteBase64() ```java method WriteBase64(binary) as %Status ``` 将指定的二进制字节编码为`base-64`,并将结果文本写入元素的内容。该参数的类型必须为`%Binary`或`%BinaryStream`。 ### WriteBinHex() ```java method WriteBinHex(binary) as %Status ``` 将指定的二进制字节编码为二进制,并将结果文本写入元素的内容。该参数的类型必须为`%Binary`或`%BinaryStream`。 ### EndElement() ```java method EndElement() as %Status ``` 结束可以与其匹配的元素。 只能在`RootElement()`和`EndRootElement()`方法之间使用这些方法。 注意:这里描述的方法旨在使能够向XML文档编写特定的逻辑片段,但在某些情况下,可能需要更多的控制。`%XML.Writer`类提供了一个附加方法`write()`,可以使用该方法编写任意字符串。有责任确保结果是格式良好的XML文档;不提供任何验证。 示例 下面是一个示例例程: ```java /// w ##class(Demo.XmlDemo).WriteObjXml() ClassMethod WriteObjXml() { set writer=##class(%XML.Writer).%New() set writer.Indent=1 set status=writer.OutputToDevice() if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.StartDocument() if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.RootElement("root") if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.Element("SampleElement") if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.WriteAttribute("Attribute","12345") if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.Element("subelement") if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.WriteChars("yao") if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.EndElement() if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.Element("subelement") if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.WriteChars("xin") if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.EndElement() if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.EndElement() if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.EndRootElement() if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} set status=writer.EndDocument() if $$$ISERR(status) {do $System.Status.DisplayError(status) quit} q "" } ``` ```java DHC-APP>w ##class(Demo.XmlDemo).WriteObjXml() yao xin ``` ### 使用%XMLL.Element 在前一节中,我们使用了Element()并指定了要生成的元素;我们还可以指定名称空间。在某些情况下,类中使用%XML.Element的实例,而不是使用元素名称。此类具有以下属性: - Local属性指定此元素是否为其父元素的本地元素,这会影响命名空间的控制。 - Namespace属性指定此元素的命名空间。 - Tagname属性指定此元素的名称。 这里还可以使用前面描述的WriteAttribute()方法。
文章
姚 鑫 · 七月 12, 2021

第一章 查询目录和驱动器

# 第一章 查询目录和驱动器 `%Library.File`(简称`%File`)为处理文件和目录提供了广泛的API。本文将介绍该API的主要功能。有关属性、方法和查询的规范列表,请参见类参考。 注意:如果指定了部分文件名或目录名,这些方法中的大多数都引用的项相对于包含正在使用的命名空间的默认全局数据库的目录。该目录在本文中称为“默认目录”。这条规则的任何例外都在文章中注明。 此外,仅当基础操作系统将文件名和目录名视为区分大小写时,这些方法才会将文件名或目录名视为区分大小写。也就是说,文件或目录名在Unix上区分大小写,但在Windows上不区分大小写。 # 查询目录和驱动器 ## 列出目录的内容 `FileSet`类查询列出目录的内容。此查询按顺序接受以下参数: 1. `directory` — 指定要检查的目录的名称。 2. `wildcards` 通配符 — 指定要匹配的文件名模式(如果有)。 3. `sortby` 排序依据 — 指定如何对结果进行排序。使用以下值之一: - `Name` 名称—文件的名称(默认) - `Type` 类型—项目类型 - `DateCreated` 创建日期—创建文件的日期和时间 - `DateModified` 日期修改—文件上次修改的日期和时间 - `Size` 大小—文件大小 4. `includedirs` —指定如何处理给定目录中的目录。如果此参数为真(1),查询将返回任何文件之前的所有目录,并且目录名忽略通配符参数。如果此参数为false (0),通配符参数适用于文件和目录。默认值为0。 5. `delimiter` 分隔符—指定通配符参数中通配符之间的分隔符。默认值为; 此查询返回的结果集提供了以下字段: - `Name` 名称—项目的完整路径名。 - `Type` 类型—项目的类型:`F`表示文件,`D`表示目录,`S`表示符号链接。 - `Size` 大小—文件大小,以字节为单位。对于目录和符号链接,此字段为空。 - `DateCreated` 创建日期—创建项目时的日期和时间,格式为`yyyy-mm-dd hh:mm:ss`。 - `DateModified` 日期修改—上次修改项目的日期和时间,格式为`yyyy-mm-dd hh:mm:ss`。 - `ItemName` 项目名称—项目的简称。对于文件,这是单独的文件名,没有目录。对于目录,这只是目录路径的最后一部分。 注意:Windows是目前唯一跟踪实际创建日期的平台。其他平台存储最后一次文件状态更改的日期。 下面是一个使用这个类查询的简单示例: ```java /// desc:查看目标路径所有文件。 /// w ##class(Demo.FileDemo).ShowDir("C:\InterSystems\Cache\mgr", "*.log", "Size") /// w ##class(Demo.FileDemo).ShowDir("E:\temp", "*.xml", "Size") ClassMethod ShowDir(dir As %String = "", wildcard As %String = "", sort As %String = "Name") { s stmt = ##class(%SQL.Statement).%New() s status = stmt.%PrepareClassQuery("%File", "FileSet") if $$$ISERR(status) { do $system.OBJ.DisplayError(status) quit } s resultSet = stmt.%Execute(dir, wildcard, sort) while resultSet.%Next() { w !, resultSet.%Get("Name") w " ", resultSet.%Get("Type") w " ", resultSet.%Get("Size") } q "" } ``` 从终端对指定目录运行此方法,筛选日志文件,并按文件大小排序,结果如下所示: ```java DHC-APP> w ##class(Demo.FileDemo).ShowDir("E:\temp", "*.xml", "Size") E:\temp\testPerson.xml F 117 E:\temp\samplePerson.xml F 327 E:\temp\xmlnewtest.xml F 351 E:\temp\Person.xml F 259854 E:\temp\tempPerson.xml F 259854 ``` 又例如,下面的方法递归检查目录及其所有子目录,并写出它找到的每个文件的名称: ```java /// w ##class(Demo.FileDemo).ShowFilesInDir("E:\temp") ClassMethod ShowFilesInDir(directory As %String = "") { s stmt = ##class(%SQL.Statement).%New() s status = stmt.%PrepareClassQuery("%File", "FileSet") if $$$ISERR(status) { d $system.OBJ.DisplayError(status) q } s resultSet = stmt.%Execute(directory) while resultSet.%Next() { s name = resultSet.%Get("Name") s type = resultSet.%Get("Type") if (type = "F") { w !, name } elseif (type = "D"){ d ..ShowFilesInDir(name) } } q "" } ``` 在默认目录下的终端中运行此方法会产生如下结果: ```java DHC-APP>w ##class(Demo.FileDemo).ShowFilesInDir("E:\temp") E:\temp\config.txt E:\temp\game.jpg E:\temp\Person.xml E:\temp\ppg.txt E:\temp\qcache.txt E:\temp\rfc7158.html E:\temp\rfc7158.txt E:\temp\samplePerson.xml E:\temp\SecurityXml.txt E:\temp\temp1.txt E:\temp\tempPerson.xml E:\temp\test\Tests.xml E:\temp\testPerson.xml E:\temp\Testzf.dll E:\temp\textReader.txt E:\temp\xmlnewtest.xml E:\temp\xmlXpath.txt E:\temp\yaoxin.txt E:\temp\yxtest.txt E:\temp\yxtest_Errors.log E:\temp\yxtest_Unsupported.log E:\temp\汉子转拼音global.gof ``` ## 列出驱动器或装载的文件系统 `Drivelist`类查询列出可用的驱动器(在Windows上)或已装载的文件系统(在Unix上)。此查询接受一个参数: 1. `fullyqualified`-如果此参数为1,则查询在每个Windows驱动器名称上都包含一个尾随反斜杠。对其他平台没有影响。默认值为0。 此查询返回的结果集提供了一个字段: - `Drive` 驱动器—驱动器的名称(在Windows上)或装载的文件系统的名称(在Unix上)。 以下示例显示了如何使用该查询: ```java /// w ##class(Demo.FileDemo).ShowDrives() ClassMethod ShowDrives() { s stmt = ##class(%SQL.Statement).%New() s status = stmt.%PrepareClassQuery("%File","DriveList") if $$$ISERR(status) { d $system.OBJ.DisplayError(status) q } s resultSet = stmt.%Execute(1) while resultSet.%Next() { w !, resultSet.%Get("Drive") } q "" } ``` 在终端中运行该方法会得到如下结果: ```java DHC-APP>w ##class(Demo.FileDemo).ShowDrives() c:\ d:\ e:\ g:\ ```