搜索​​​​

清除过滤器
文章
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!
文章
jieliang liu · 一月 7, 2021

精华文章---在 Windows 主机上运行的 Hyper-V Ubuntu 虚拟机中配置 Docker 使用环境

这次我想谈一谈不专门针对 InterSystems IRIS 的东西,不过如果你想使用 Docker,并且你工作环境是安装了 Windows 10 专业版或企业版的 PC 或笔记本电脑,那么我认为这个很重要。 你可能知道,容器技术基本上来自于 Linux 世界,如今在 Linux 主机上发挥出最大潜能。 那些平常使用 Windows 的人会看到,Microsoft 和 Docker 在过去的几年做出了重要的努力,让我们可以在 Windows 系统上以非常简单的方式运行基于 Linux 映像的容器... 但是生产系统不支持这种方式,这是个大问题,如果我们要将持久性数据保留在主机系统中的容器之外,这样做非常不可靠... 这主要是由于 Windows 和 Linux 文件系统之间的巨大差异导致的。 最终,_Docker for Windows 自身使用了一个小型 linux 虚拟机 (_MobiLinux_) 来运行容器... 此操作对于 Windows 用户是透明的,而且效果完美,只要你不需要你的数据库比容器存活的时间更长... 好了,我们进入正题,很多时候为了避免出现问题和简化操作,我们需要一个完整的 Linux 系统,而且如果我们的服务器基于 Windows,那么唯一的方法就是通过虚拟机来实现。 至少在 Windows 中的 WSL2 发布之前是这样,但发布后就是另一回事了,不过它要变得足够强大稳定肯定还需要一些时间。 在本文中,我将一步一步告诉你如何在 Windows 服务器中的 Ubuntu 系统上安装一个能使用 Docker 容器进行工作的环境。 我们开始吧... 1. 启用 Hyper-V 如果尚未启用,则转到添加 `Windows 功能`并启用 Hyper-V。 你将需要重启(图片上的文本是西班牙语,但这就是我当前的区域设置。 如果你不懂堂吉诃德的语言,我希望加上说明能帮助你“解密”😉) ![](/sites/default/files/inline/images/images/image(424).png)   2. 在 Hyper-V 上创建一个 Ubuntu 虚拟机 我认为创建虚拟机 (VM) 没有更简单的方法了。 只需打开 `Hyper-V 管理器`的窗口,然后转到选项快速创建...(屏幕的右上角),使用已经提供的任一 Ubuntu 版本来创建你的虚拟机(你可以下载任何其他 Linux 的 iso 文件,创建不同发行版的虚拟机)。 在我的示例中,我选择了最新的 Ubuntu 版本:19.10。 不过,你在这里看到的一切内容也都适用于 18.04。 在 15 或 20 分钟内,具体取决于你下载映像花费的时间,新的虚拟机就创建完毕并准备就绪。 重要: 保持选项默认交换机 不变。这将保证你可以从主机和虚拟机访问互联网。 ![](/sites/default/files/inline/images/images/vm_ubuntu_network_start_defaultswitch_eth0.jpg) 3. 创建本地子网 使用虚拟机经常遇到的问题之一与网络配置有关... 有时有效,有时无效,或者连接 Wi-Fi 时有效,但连接网线就无效,或者是相反情况;或者如果我在 Windows 主机中建立一个 VPN,那么在虚拟机中就无法访问互联网,或者是虚拟机 (Linux) 和主机 (Windows) 之间的通信中断... 总之,非常让人抓狂! 这使得我在使用笔记本电脑进行开发、小型快速演示或展示时无法信任我的环境,而在这些场景下访问互联网很可能不如确保在主机与虚拟机之间进行可靠通信来得重要。 在 Windows 主机和虚拟机之间共享一个临时本地子网,可以解决这个问题。 要让它们互相通信,使用该子网就可以了。 你只需要为主机和虚拟机分配特定 IP 即可。 通过以下步骤可以很容易实现。 只需转到虚拟交换机管理器...,你可以在 `Hyper-V 管理器`中找到: ![](/sites/default/files/inline/images/images/image(425).png) 然后,转到选项新建虚拟交换机(之后就像虚拟机的新网卡一样): ![](/sites/default/files/inline/images/images/image(461).png) 确保将其定义为_内部网络_,选择我们想要的名称,其他选项保持默认 ![](/sites/default/files/inline/images/images/image(427).png) 现在,如果转到 _`Windows 控制面板 --> 网络和共享中心`_,我们会看到那里已经有了我们刚才创建的交换机: ![](/sites/default/files/inline/images/images/image(429).png)   4. 配置主机和虚拟机共享的本地子网 此时,你可以完成新的本地网络的配置。 为此,将光标放在连接 _Mi Nuevo Conmutador LOCAL_ 上,单击并转到属性,再转到 IPv4 协议,以便分配一个固定 IP 地址: ![](/sites/default/files/inline/images/images/image(449).png)   重要:在此处分配的 IP 将是主机 (Windows) 在该本地子网中的 IP。   5. 将新的本地网络链接并配置到虚拟机 现在回到 `Hyper-V 管理器`。 如果虚拟机正在运行,将其停止。 停止后,转到其配置并添加新的内部虚拟交换机: ![](/sites/default/files/inline/images/images/image(431).png) _(注意:在图片上可以看到另一个交换机 Hyper-V Conmutador INTERNO。 它用于我的另一个子网。 此配置中不需要它)_ 单击“添加”后,你只需选择先前创建的交换机: ![](/sites/default/files/inline/images/images/image(432).png) 好了,完成此操作后,依次单击“应用”、“接受”... 一切就绪!你只需启动并再次登录虚拟机即可完成内部连接的配置。 为此,在虚拟机启动后,单击网络图标(右上角),你将看到两个网络:_eth0_ 和 _eth1_。 _eth1_ 目前显示为断开连接: ![](/sites/default/files/inline/images/images/image(450).png) 进入以太网 (eht1) 的配置,并为此本地子网分配一个固定 IP,例如:_155.100.101.1_,子网掩码:_255.255.255.0_ ![](/sites/default/files/inline/images/images/image(452).png) 这样就完成了。 你的虚拟机标识为 IP 155.100.101.1,与主机共享同一子网。 7. 允许从虚拟机访问 Windows 10 你可能会发现 Windows 10 默认不允许其他服务器连接,对于 Windows 系统来说,你刚刚创建的虚拟机正是一个可能存在危险的外部服务器。因此,必须在防火墙中添加规则,才能从这些虚拟机连接到主机。 如何操作? 非常简单,只需在 `Windows 控制面板`中查找 `Windows Defender 防火墙`,转到高级配置,然后创建一条新的*入站规则*: ![](/sites/default/files/inline/images/images/image(451).png) 你可以设置一个端口或者一个或多个端口范围...(也可以设置针对所有端口的规则)... ![](/sites/default/files/inline/images/images/image(453).png) 我们需要的操作是_允许连接_... ![](/sites/default/files/inline/images/images/image(454).png) 用于_所有网络类型_... ![](/sites/default/files/inline/images/images/image(455).png) 为规则指定名称... ![](/sites/default/files/inline/images/images/image(456).png) **这里很重要**,指定名称后要立即再次打开新创建的规则的属性并*限制应用程序范围*,以便只应用于本地子网内的连接... ![](/sites/default/files/inline/images/images/image(457).png) 8. 就绪。 在新的 Ubuntu 虚拟机中安装 Docker 和任何其他应用程序 完成整个安装过程后,新虚拟机即就绪且为最新,并可以访问互联网等等。 你可以安装所需的应用程序... 至少要安装 Docker,这是一开始就有的想法,如果你需要连接公司网络,还可以安装 VPN 客户端,还有 VS Code、Eclipse+Atelier 等等。 具体来说,要在虚拟机中安装 Docker,可以按照以下说明进行操作: 确保 Docker 运行时正在工作,下载一些测试映像等等... 仅此而已。 这样... _**你已完成所有工作!**_,现在你将能够在 Ubuntu 虚拟机中无限制(除了硬件能力限制)运行容器,你可以从 Windows 10 主机、浏览器或应用程序连接到虚拟机,以及反过来从 Ubuntu 虚拟机连接到 Windows 10 主机。 所有使用你在共享本地子网中设置的 IP 地址的操作都将有效,无论是否建立 VPN,是通过 Wi-fi 适配器还是通过以太网电缆接入互联网。 啊... 最后一个建议。 如果要在 Windows 10 和虚拟机之间交换文件,一个非常有用且简单的选项是使用 [WinSCP](https://winscp.net/eng/download.php)。 它是免费的,而且非常好用。 当然,还有其他配置,但这是我使用的配置,已经证明是比较可靠的。 希望你也觉得它有用。 如果我帮助你避免了令人头疼的问题,这篇文章就值了。 编码愉快!     
文章
姚 鑫 · 四月 27, 2021

第七章 解释SQL查询计划

# 第七章 解释SQL查询计划 本章介绍由`ShowPlan`生成的InterSystems SQL查询访问计划中使用的语言和术语。 # 存储在映射中的表 SQL表存储为一组映射。 每个表都有一个包含表中所有数据的主映射; 表还可以有其他的映射,如索引映射和位图。 每个映射可以被描绘成一个多维全局,其中一些字段的数据在一个或多个下标中,其余字段存储在节点值中。 下标控制要访问的数据。 - 对于主映射,`RowID`或`IDKEY`字段通常用作映射下标。 - 对于索引映射,通常将其他字段用作前导下标,将`RowID/IDKEY`字段用作附加的较低级别的下标。 - 对于位图,可以将位图层视为附加的RowID下标级别。但是,位图只能用于为正整数的`RowID`。 # 发展计划 编译SQL查询会生成一组指令来访问和返回查询指定的数据。 这些指令表示为`. int`例程中的ObjectScript代码。 指令及其执行顺序受到SQL编译器中有关查询中涉及的表的结构和内容的数据的影响。 编译器尝试使用表大小和可用索引等信息,以使指令集尽可能高效。 查询访问计划(`ShowPlan`)是对结果指令集的可读翻译。 查询的作者可以使用这个查询访问计划来查看将如何访问数据。 虽然SQL编译器试图最有效地利用查询指定的数据,但有时查询的作者对存储的数据的某些方面的了解要比编译器清楚得多。 在这种情况下,作者可以利用查询计划修改原始查询,为查询编译器提供更多的信息或更多的指导。 # 阅读计划 `“ShowPlan”`的结果是一系列关于访问和显示查询中指定的数据的处理的语句。 下面提供了关于如何解释`ShowPlan`语句的信息。 ## 访问映射 一个查询计划可以访问多个表。 当访问一个表时,计划可以访问单个映射(索引或主映射)、两个映射(索引映射后面跟着主映射),或者,对于多索引计划,可以访问多个映射。 在通过映射访问数据时,计划指示使用的下标。 它还指示实际的下标值是什么:一个给定值、一组给定值、一个值范围,或该下标在表中显示的所有值。 选择哪一个取决于查询中指定的条件。 显然,访问单个或几个下标值要比访问该下标级别上的所有值快得多。 ## 条件和表达式 当查询运行时,将测试查询指定的各种条件。 除了前面提到的某些限制下标的条件外,`ShowPlan`输出没有显式地指示条件的测试。 尽早测试条件总是最好的。 测试各种条件的最佳地点可以从计划细节中推断出来。 类似地,`ShowPlan`不详细描述表达式和子表达式的计算。 除了简单之外,主要原因是在大多数数据库环境中,表和索引访问构成了处理的更重要方面; 检索表数据的成本占总体查询成本的主要地位,因为磁盘访问速度仍然比CPU处理慢几个数量级。 ## 循环 当访问一个表中的数据时,经常需要迭代地检查多个行。 这样的访问是通过一个循环来指示的。 每一次传递要执行的指令称为循环体。 它们可以通过缩进直观地显示出来。 涉及多个表的数据库访问通常需要循环中的循环。 在这种情况下,每个循环级别都通过与前一个级别相比的进一步缩进表示。 ## 临时文件 ### 定义 查询计划还可能指示需要构建和使用中间临时文件(`TEMP-FILE`)。这是本地数组中的“临时”区域。它用于保存临时结果以用于各种目的,如排序。就像映射一样,临时文件有一个或多个下标,可能还有节点数据。 ### 使用 一些临时文件包含处理单个表的数据。在这种情况下,可以将构建临时文件视为对该表中的数据进行预处理。在读取这样的临时文件之后,可以访问源表的主映射,也可以不访问源表的主映射。在其他情况下,临时文件可能包含处理多个表的结果。在其他情况下,临时文件用于存储分组的聚合值、检查DISTINCT等。 ## 模块 临时文件的构建,以及其他处理,可以委托给一个称为模块的独立工作单元。 每个模块都被命名。 当列出单独的模块时,该计划将指明调用每个模块的位置。 当模块执行结束时,处理将在模块调用之后的下一条语句中继续进行。 ## 发送给处理的查询 对于通过ODBC或JDBC网关连接链接的外部表,该计划显示发送到远程SQL gateway connection的查询文本,以从远程表检索所请求的数据。 对于并行查询处理和分片,该计划显示发送到并行处理或在分片上处理的各种查询。 还将显示用于每个查询的计划。 ## 子查询、连接和联合 给定查询中的一些子查询(和视图)也可以单独处理。 它们的计划在单独的子查询部分中指定。 在计划中没有指明子查询部分被调用的精确位置。 这是因为它们经常作为条件或表达式处理的一部分被调用。 对于指定`OUTER JOIN`的查询,如果没有找到匹配的行,该计划可能指示可能生成的`null`行,以满足外部连接语义的要求。 对于`UNION`,该计划可能指示将来自不同`UNION`子查询的结果行组合到一个单独的模块中,在该模块中可以对这些结果行进行进一步处理。 # 计划分析 在分析给定查询的计划时,应用程序开发人员有时可能会觉得不同的计划会更有效率。 应用程序开发人员有多种方法来影响计划。 首先,计划将受到在包含实际应用程序数据的环境中正确运行调优表的影响。 在类源定义中手动定义一些`Tune Table`通常计算的值——例如表`EXTENTSIZE`、字段`SELECTIVITY`和映射`BlockCount`——也可以用于实现所需的计划。 此外,分析计划可能表明对类定义的某些更改可能导致更有效的计划,例如: ## 添加一个索引 在某些情况下(尽管不总是),使用一个临时文件进行预处理可能意味着向原始表添加一个与临时文件具有相同或类似结构的索引将消除构建临时文件的需要。 从查询计划中删除这个处理步骤显然可以使查询运行得更快,但这必须与更新表时维护索引所需的工作量进行平衡。 ## 添加字段到索引数据 当计划显示正在使用的索引,然后是对主映射的访问时,这意味着将查询中使用的主映射字段添加到索引节点数据可能会为该查询生成更快的计划。 同样,这必须与额外的更新时间以及添加到处理使用该索引的其他查询的额外时间进行平衡,因为索引会更大,因此需要更多的读取时间。 ## 添加连接索引 当计划显示以特定顺序连接两个表时(例如,首先检索`t1`,然后使用连接条件`t1.a=t2.b`连接到`t2`),可能相反的表顺序会产生一个更快的计划。例如,如果`t2`有额外的条件,可以显著限制符合条件的行数。 在这种情况下,在`t1`上添加一个t1索引。 a将允许这样一个连接顺序。
文章
Michael Lei · 七月 18, 2022

翻译文章--Angular 14 新特性介绍

Hi 大家好! 我是 Sergei Sarkisian,在InterSystems 做Angular 前端7年。Angular是非常流行的框架,我们的开发人员、客户和合作伙伴经常选择它来开发他们的应用程序。 我会写一系列的文章,涵盖Angular的不同方面:概念、方法、最佳实践、高级主题等等。这个系列的文章将针对那些已经熟悉Angular的人,不会涉及基本概念。由于我正在构建文章的路线图,我想从突出最近的Angular版本中的一些重要功能开始。 ## 严格类型化表单 这可能是近几年来Angular最受欢迎的功能。有了Angular 14,开发者现在可以在Angular Reactive Forms中使用TypeScript的所有严格类型检查功能。 表单控制Formcontrol 类现在是通用的,并接受它所持有的值的类型。 ```typescript /* Before Angular 14 */ const untypedControl = new FormControl(true); untypedControl.setValue(100); // value is set, no errors // Now const strictlyTypedControl = new FormControl(true); strictlyTypedControl.setValue(100); // you will receive the type checking error message here // Also in Angular 14 const strictlyTypedControl = new FormControl(true); strictlyTypedControl.setValue(100); // you will receive the type checking error message here ``` 正如你所见,第一个和最后一个例子几乎是一样的,但有不同的结果。这是因为在Angular 14中,新的FormControl类从开发者提供的初始值中推断出类型。因此,如果提供了`true`的值,Angular就为这个FormControl设置`boolean | null`的类型。`.reset()`方法需要可置空的值,如果没有提供值,就会置空这些值。 一个旧的、没有定义类型的FormControl类被转换为`UntypedFormControl`(对`UntypedFormGroup`、`UntypedFormArray`和`UntypedFormBuilder`来说也是如此),它实际上是`FormControl`的别名。如果你从以前的Angular版本升级,你所有提到的`FormControl`类将被Angular CLI替换为`UntypedFormControl`类。 Untyped* 类通常用以实现特定目标: 1. 保持应用程序的工作方式与从以前的版本过渡之前完全一样(记住,新的FormControl将从初始值推断出类型) 2. 确保所有的`FormControl`的使用都是有意的。所以你需要自己将任何UntypedFormControl改为`FormControl`。 3. 为了给开发者提供更多的灵活性(我们将在下面介绍这个问题) 记住,如果你的初始值是 "null",那么你将需要明确指定FormControl类型。另外,在TypeScript中有一个错误,如果你的初始值是 "false",也需要这样做。 对于表单组,你也可以定义接口,并把这个接口作为表单组的类型传递。在这种情况下,TypeScript将推断出FormGroup中的所有类型。 ```typescript interface LoginForm { email: FormControl; password?: FormControl; } const login = new FormGroup({ email: new FormControl('', {nonNullable: true}), password: new FormControl('', {nonNullable: true}), }); ``` FormBuilder的方法`.group()`现在有了通用属性,可以接受你预定义的接口,就像上面的例子中我们手动创建了FormGroup。 ```typescript interface LoginForm { email: FormControl; password?: FormControl; } const fb = new FormBuilder(); const login = fb.group({ email: '', password: '', }); ``` 由于我们的接口只有原始的nonNullable类型,它可以用新的 "nonNullable "表单生成器属性来简化(它包含 "NonNullable FormBuilder表单生成器 "类实例,也可以直接创建): ```typescript const fb = new FormBuilder(); const login = fb.nonNullable.group({ email: '', password: '', }); ``` ❗ 请注意,如果你使用nonNullable的FormBuilder或者你在FormControl中设置了nonNullable的选项,那么当你调用`.reset()`方法时,它将使用初始FormControl值作为重置值。 另外,非常重要的一点是,`this.form.value`中的所有属性都将被标记为可选属性。像这样: ```typescript const fb = new FormBuilder(); const login = fb.nonNullable.group({ email: '', password: '', }); // login.value // { // email?: string; // password?: string; // } ``` 发生这种情况是因为当你禁用表单组FormGroup内的任何表单控件FromControl时,这个表单控件的值将从`form.value`中删除。 ```typescript const fb = new FormBuilder(); const login = fb.nonNullable.group({ email: '', password: '', }); login.get('email').disable(); console.log(login.value); // { // password: '' // } ``` 要获得整个表单对象,你应该使用`.getRawValue()`方法:: ```typescript const fb = new FormBuilder(); const login = fb.nonNullable.group({ email: '', password: '', }); login.get('email').disable(); console.log(login.getRawValue()); // { // email: '', // password: '' // } ``` 严格类型化表单的优势: 1. 任何返回FormControl / FormGroup值的属性和方法现在都是严格类型的。例如:`value`,`getRawValue()`,`valueChanges`. 2. 任何改变表单控件值的方法现在都是类型安全的:`setValue()`, `patchValue()`, `updateValue()` 3. 表单控件现在是严格类型化的。它也适用于表单组的`.get()`方法。这也将防止你在编译时发生访问不存在的情况. ### 新的 FormRecord 类 新的 "表单组 "类的缺点是它失去了它的动态性质。一旦定义了,你将不能在运行中添加或删除表单控件。 为了解决这个问题,Angular提出了新的类--`FormRecord'。`FormRecord`实际上与`FormGroup`相同,但它是动态的,所有的表单控件都应该有相同的类型。. ```typescript folders: new FormRecord({ home: new FormControl(true, { nonNullable: true }), music: new FormControl(false, { nonNullable: true }) }); // Add new FormContol to the group this.foldersForm.get('folders').addControl('videos', new FormControl(false, { nonNullable: true })); // This will throw compilation error as control has different type this.foldersForm.get('folders').addControl('books', new FormControl('Some string', { nonNullable: true })); ``` 正如你所看到的,这里有另一个限制 - 所有的FormControls必须是相同的类型。如果你真的需要动态和异质的FormGroup,你应该使用`UntypedFormGroup`类来定义你的表单 ## 无模块的 (独立standalone) 组件 这个特性仍然被标记为实验性的,但它是一个有趣的功能。它允许你定义组件、指令和管道,而不把它们包含在任何模块中。 这个概念还没有完全准备好,但我们已经能够在没有ngModules的情况下建立一个应用程序。 要定义一个独立的组件,你需要使用Component组件/Pipe管道/Directive Decorator指令装饰器中新的`standalone'属性: ```typescript @Component({ selector: 'app-table', standalone: true, templateUrl: './table.component.html' }) export class TableComponent { } ``` 在这种情况下,这个组件不能在任何NgModule中声明。但它可以在NgModules和其他独立组件中被导入。 每个独立的组件/管道/指令现在都有机制可以直接在Decorator装饰器中导入它的依赖项: ```typescript @Component({ standalone: true, selector: 'photo-gallery', // an existing module is imported directly into a standalone component // CommonModule imported directly to use standard Angular directives like *ngIf // the standalone component declared above also imported directly imports: [CommonModule, MatButtonModule, TableComponent], template: ` ... Next Page `, }) export class PhotoGalleryComponent { } ``` 正如我上面提到的,你可以在任何现有的ngModule中导入独立的组件。不再需要导入整个共享模块,我们可以只导入我们真正需要的东西。这也是一个开始使用新的独立组件的好策略: ```typescript @NgModule({ declarations: [AppComponent], imports: [BrowserModule, HttpClientModule, TableComponent], // import our standalone TableComponent bootstrap: [AppComponent] }) export class AppModule {} ``` 你可以通过输入Angular CLI创建独立的组件: ```bash ng g component --standalone user ``` ### Bootstrap 无模块的应用 如果你想摆脱你的应用程序中的所有ngModules,你将需要以不同的方式启动你的应用程序。Angular有新的函数,你需要在main.ts文件中调用这个函数: ```typescript bootstrapApplication(AppComponent); ``` 这个函数的第二个参数将允许你定义你在你的应用程序中需要的提供者。由于大多数提供者通常存在于模块中,Angular(目前)需要为它们使用一个新的`importProvidersFrom`提取函数: ```typescript bootstrapApplication(AppComponent, { providers: [importProvidersFrom(HttpClientModule)] }); ``` ### 懒人加载独立组件的路线: Angular有新的懒人-加载路由函数`loadComponent`,它的存在正是为了加载独立的组件: ```typescript { path: 'home', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) } ``` `loadChildren`现在不仅允许懒人加载ngModule,而且还允许直接从路由文件中加载子路由: ```typescript { path: 'home', loadChildren: () => import('./home/home.routes').then(c => c.HomeRoutes) } ``` ### 关于本文的一些注意事项 - 独立组件的功能仍处于实验阶段。它在未来会变得更好,因为它将移到Vite builder而不是Webpack,更好的工具,更快的构建时间,更强大的应用架构,更容易的测试等等。但现在这些东西都没有了,所以我们没有得到整个包,但至少我们可以开始用新的Angular范式开发我们的应用程序。 - IDE和Angular工具还没有完全准备好静态地分析新的独立实体。因为你需要在每个独立实体中导入所有的依赖关系,万一你漏掉了什么,编译器也会漏掉它,并在运行时让你失败。这一点会随着时间的推移而得到改善,但现在需要开发人员更加关注导入。 - 目前Angular中没有全局导入(例如在Vue中),所以你需要在每个独立实体中完全导入每个依赖。我希望这个问题能在未来的版本中得到解决,因为在我看来,这个功能的主要目标是减少模板,让事情变得更简单。 # 先写到这,谢谢大家!
文章
姚 鑫 · 十一月 11, 2021

第七十三章 SQL命令 SET OPTION

# 第七十三章 SQL命令 SET OPTION 设置执行选项。 # 大纲 ```java SET OPTION option_keyword = value ``` # 描述 `SET OPTION`语句用于设置执行选项,如编译模式、`SQL`配置设置和控制日期、时间和数字约定的区域设置。 每个`set option`语句只能设置一个关键字选项。 `SET OPTION`支持以下选项: - `AUTO_PARALLEL_THRESHOLD - `COMPILEMODE` - `DEFAULT_SCHEMA`` - `EXACT_DISTINCT` - `LOCK_ESCALATION_THRESHOLD` - `LOCK_TIMEOUT` - `PKEY_IS_IDKEY` - `SUPPORT_DELIMITED_IDENTIFIERS` - `Locale Options (date, time, and numeric conventions)` `SET OPTION`可以在动态SQL(包括`SQL Shell`)和嵌入式SQL中使用。 为了`SQL`兼容性,IRIS会解析其他`SET OPTION`参数(这里没有文档),但不执行任何操作。 因为`SET OPTION`的准备和执行速度很快,而且通常只运行一次,所以`IRIS`不会在`ODBC`、`JDBC`或动态SQL中为`SET OPTION`创建缓存查询。 IRIS支持下列选项: ## `AUTO_PARALLEL_THRESHOLD` `AUTO_PARALLEL_THRESHOLD`选项被设置为一个整数`n`,用于确定当启用自动并行处理时是否应该对查询应用并行处理。 由于与并行处理相关的性能成本,因此需要为并行处理的优势确定一个阈值。 `n`越高,SQL查询使用并行处理执行的可能性就越低。 默认为`3200`。 这是一个系统范围的设置。 值n大致对应于所访问的映射中发生并行处理所需的最小元组数量。 当自动并行被禁用时,`AUTO_PARALLEL_THRESHOLD`选项没有作用。 也可以使用`$SYSTEM.SQL.Util.SetOption()`方法`AutoParallelThreshold`选项设置该选项。 ## COMPILEMODE `COMPILEMODE`选项将当前名称空间的编译模式设置为`DEFERRED`、`IMMEDIATE`、`INSTALL`或`NOCHECK`。 默认为`IMMEDIATE`。 从`DEFERRED`编译模式更改为`IMMEDIATE`编译模式会导致`DEFERRED compile Queue`中的任何类立即被编译。 如果所有类编译都成功,IRIS将`SQLCODE`设置为0。 如果有任何错误,`SQLCODE`设置为`-400`。 类编译错误记录在`^mtemp2 ("Deferred Compile Mode","Error")`中。 如果将`SQLCODE`设置为`-400`,则应该查看此全局结构以获得更精确的错误消息。 `INSTALL`编译模式类似于`DEFERRED`编译模式,但它应该只用于表中没有数据的`DDL`安装。 `NOCHECK`编译模式与`IMMEDIATE`编译模式类似,只是在编译时忽略了以下约束:如果一个表被删除, IRIS不检查引用被删除表的其他表中的外键约束。 如果添加了外键约束, IRIS不会检查现有数据以确保它对这个外键有效。 如果添加了`NOT NULL`约束, IRIS不会检查现有数据是否为`NULL`,也不会指定字段的默认值。 如果删除了`UNIQUE`或`Primary Key`约束 IRIS不会检查该表或其他表中的外键是否引用了被删除的键。 也可以使用`$SYSTEM.SQL.Util.SetOption()`方法`CompileMode`选项设置该选项。 ## DEFAULT_SCHEMA `DEFAULT_SCHEMA`选项为所有名称空间设置系统范围的默认模式。 在显式更改之前,此默认值将保持有效。 默认模式名用于为所有未限定的表、视图或存储过程名提供模式名。 可以指定一个文字模式名或指定`_CURRENT_USER`。 如果指定`_CURRENT_USER`作为默认模式名, IRIS会将当前登录进程的用户名作为默认模式名。 ## EXACT_DISTINCT `EXACT_DISTINCT`布尔值选项指定是否在系统范围内使用`DISTINCT`处理`(TRUE)`或`Fast DISTINCT`处理`(FALSE)`。 系统范围的默认值是使用`Fast Distinct`处理。 当`EXACT_DISTINCT=TRUE`时,`GROUP BY`和`DISTINCT`查询生成原始值。 当`EXACT_DISTINCT=FALSE`时,启用快速`Distinct`,通过更好地使用索引(如果有索引),使涉及`Distinct`或`GROUP BY`子句的SQL查询更有效地运行。 但是,这些查询返回的值以与存储在索引中的相同的方式进行排序。 这意味着此类查询的结果可能都是大写的。 这可能对区分大小写的应用程序有影响。 这个选项也可以使用`$SYSTEM.SQL.Util.SetOption()`方法`FastDistinct boolean`选项来设置。 ## `LOCK_ESCALATION_THRESHOLD` `LOCK_ESCALATION_THRESHOLD`选项被设置为一个整数`n`,用于确定何时将行锁定升级为表锁定。 默认值是`1000`。 值`n`是单个事务中单个表的插入、更新或删除次数,当到达时将触发表级锁。 这是针对所有名称空间的系统范围设置。 例如,如果锁阈值为`1000`,并且进程启动一个事务,然后插入`2000`行,那么在插入第`1001`行之后,进程将尝试获取表级锁,而不是继续锁定各个行。 这有助于防止锁表变得太满。 这个选项也可以使用`$SYSTEM.SQL.Util.SetOption()`方法`LockThreshold`选项来设置。 ## LOCK_TIMEOUT `LOCK_TIMEOUT`数值选项允许为当前进程设置默认的锁定超时。 `LOCK_TIMEOUT`值是SQL执行期间试图建立锁时等待的秒数。 当锁定冲突阻止当前进程对`lock`、`INSERT`、`UPDATE`、`DELETE`或`SELECT`操作立即锁定一条记录、表或其他实体时,使用此锁定超时。 `SQL`继续尝试建立锁,直到超时超时,这时将生成`SQLCODE -110`或`-114`错误。 可用的值是正整数和零。 超时设置是每个进程的。 可以使用`$SYSTEM.SQL.Util.GetOption(“ProcessLockTimeout”)`方法确定当前进程的锁定超时设置。 如果没有为当前进程设置锁定超时,则默认为当前系统范围的锁定超时设置。 如果您的`ODBC`连接断开并重新连接,重新连接的进程将使用当前系统范围的锁定超时设置。 系统范围的锁定超时默认为10秒。 ## PKEY_IS_IDKEY `PKEY_IS_IDKEY boolean`选项指定主键是否也是系统范围内的ID键。 取值为`TRUE`、`FALSE`。 如果为`TRUE`,且该字段不包含数据,则将主键创建为`ID`键。 也就是说,表的主键也成为了类定义中的`IDKey`索引。 如果字段不包含数据,则没有定义`IDKey`索引。 如果将主键定义为`IDKey`索引,则数据访问将更加有效,但主键值一旦设置,就永远不能修改。 一旦设置,就不能更改分配给主键的值,也不能将其他键指定为主键。 使用此选项还将更改主键排序规则的默认值; 主键字符串值默认为`EXACT`排序规则。 如果为`FALSE`,则主键和`ID`键被定义为独立的,效率较低。 但是,主键值是可修改的,主键字符串值默认为当前排序规则类型`default`,默认为`SQLUPPER`。 要设置`PKEY_IS_IDKEY`选项,必须具有`%Admin_Manage:USE`权限。 否则,将收到一个`SQLCODE -99`错误(特权违反)。 一旦设置,该选项将在系统范围内对所有进程生效。 该选项的系统范围默认值也可以通过以下方式设置: - `$SYSTEM.SQL.Util.SetOption()`方法配置选项`DDLPKeyNotIDKey`。 要确定当前设置,调用`$SYSTEM.SQL.CurrentSettings()`,它显示通过DDL创建的是主键而不是ID键; 默认值是1。 - 管理门户配置设置。 选择系统管理,配置,SQL和对象设置,SQL。 查看或修改通过DDL创建的表的“将主键定义为ID键”的当前设置。 `PKEY_IS_IDKEY`设置保持有效,直到通过另一个SET OPTION `PKEY_IS_IDKEY`重置或直到 IRIS `Configuration`被重新激活,将该参数重置为IRIS System `Configuration`设置。 ## SUPPORT_DELIMITED_IDENTIFIERS 默认情况下,系统范围内支持分隔标识符。 `SUPPORT_DELIMITED_IDENTIFIERS`布尔选项允许您更改系统范围内对分隔标识符的支持。 取值为`TRUE`、`FALSE`。 如果为`TRUE`,用双引号分隔的字符串被认为是SQL语句中的标识符。 如果为`FALSE`,由双引号分隔的字符串被认为是SQL语句中的字符串字面值。 要设置`SUPPORT_DELIMITED_IDENTIFIERS`选项,必须具有`%Admin_Manage:USE`权限。 否则,将收到一个`SQLCODE -99`错误(特权违反)。 一旦设置,该选项将在系统范围内对所有进程生效。 `SUPPORT_DELIMITED_IDENTIFIERS`设置将保持有效,直到通过另一个设置选项`SUPPORT_DELIMITED_IDENTIFIERS`进行重置,或者直到由`$SYSTEM.SQL.Util.SetOption()方法delimitedifiers`选项在系统范围内进行更改。 ## Locale Options 区域设置选项是关键字选项,用于为当前进程的日期、时间和数字约定设置IRIS区域设置。 可选关键字有`AM、DATE_FORMAT、DATE_MAXIMUM、DATE_MINIMUM、DATE_SEPARATOR、DECIMAL_SEPARATOR、MIDNIGHT、MINUS_SIGN、MONTH_ABBR、MONTH_NAME、NOON、NUMERIC_GROUP_SEPARATOR、NUMERIC_GROUP_SIZE、PM、PLUS_SIGN、TIME_FORMAT、TIME_PRECISION、TIME_SEPARATOR、WEEKDAY_ABBR、WEEKDAY_NAME、YEAR_OPTION`。 所有这些选项都可以设置为文字,并且都采用默认值(美式英语惯例)。 `TIME_PRECISION`选项是可配置的(参见下面)。 如果将这些选项中的任何一个设置为无效值,InterSystems IRIS将发出`SQLCODE -129`错误(`set OPTION`区域设置属性的非法值)。 Date/Time Option Keyword| Description ---|--- `AM` |`String`. 默认 `'AM'` `DATE_FORMAT` |`Integer`. 默认值为`1`。取值范围为`0 ~ 15`。 `DATE_MAXIMUM`| `Integer`. 默认为`2980013(12/31/9999)`。可以设置为更早的日期,但不能设置为更晚的日期。 `DATE_MINIMUM`| `Positive Integer`. 默认为0`(12/31/1840)`。可以设置为较晚的日期,但不能设置为较早的日期。 `DATE_SEPARATOR`| Character. Default is '/' `DECIMAL_SEPARATOR`| Character. Default is '.' `MIDNIGHT`| String. Default is 'MIDNIGHT' `MINUS_SIGN`| Character. Default is '-' `MONTH_ABBR`| String. Default is ' Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'. (注意,该字符串以空格字符开始,这是默认分隔符.) `MONTH_NAME`| String. Default is ' January February March April May June ... November December'. 注意,该字符串以空格字符开始,这是默认分隔符.) `NOON`| String. Default is 'NOON' `NUMERIC_GROUP_SEPARATOR`| Character. Default is ',' `NUMERIC_GROUP_SIZE` |Integer. Default is 3.PM String. Default is 'PM' `PLUS_SIGN`| Character. Default is '+' `TIME_FORMAT`| Integer. Default is 1. 取值范围为1 ~ 4。 `TIME_PRECISION`| Integer from 0 through 9 (inclusive). Default is 0. 小数秒的位数。 `TIME_SEPARATOR`| Character. Default is ':' `WEEKDAY_ABBR`| String. Default is ' Sun Mon Tue Wed Thu Fri Sat'. (注意,该字符串以空格字符开始,这是默认分隔符.) `WEEKDAY_NAME`| String. Default is ' Sunday Monday Tuesday Wednesday Thursday Friday Saturday'. (注意,该字符串以空格字符开始,这是默认分隔符.) `YEAR_OPTION`| Integer. Default is 0. 取值范围为0 ~ 6。有关表示2位数和4位数年份的这些方法的解释,见ObjectScript $ZDATE函数。 要在系统范围内配置`TIME_PRECISION`,请进入管理门户,选择“系统管理”、“配置”、“SQL”和“对象设置”、“SQL”。 查看和编辑`GETDATE()`、`CURRENT_TIME`和`CURRENT_TIMESTAMP`的默认时间精度的当前设置。 它指定小数秒的精确位数。 默认值是`0`。 允许的值的范围是`0`到`9`位精度。 小数秒中有意义的数字的实际数目与平台有关。
文章
Michael Lei · 四月 19, 2022

用Caché ObjectScript 生成EXCEL

有很多方法可以使用Intersystems生成excel文件,其中一些是ZEN报告、IRIS报告(Logi报告或正式称为JReports),或者我们可以使用第三方Java库,可能性几乎是无限的。 但是,如果你想只用Caché ObjectScript创建一个简单的电子表格呢?(没有第三方应用程序) 在我的案例中,我需要生成包含大量原始数据的报告(财务人员喜欢这些数据),但是我的ZEN/IRIS失败了,给了我一个我想称之为 "零字节的文件",基本上说java的内存用完了,并导致报告服务器上的重载。 这可以用Office Open XML(OOXML)来完成。Office Open XML格式是由一个ZIP包内的一些XML文件组成的。因此,基本上我们需要生成这些XML文件,并将其压缩重命名为.xslx。就这么简单。 这些文件遵循一套简单的惯例,称为开放包装惯例。你需要声明各部分的内容类型,以及告诉消费应用程序应该从哪里开始。 为了创建一个简单的电子表格,我们至少需要5个文件。 workbook.xml worksheet.xml [Content_Types].xml styles.xml _rels .rels workbook.xml.rels workbook.xml工作簿是各种工作表的容器。工作簿是你可以引用样式部分、共享字符串表以及适用于整个电子表格文件的任何其他信息的地方。、 ClassMethod GenerateWorkbookXML(){ set status =$$$OK set xmlfile = tempDirectoryPath_"workbook.xml" try{ set stream = ##class(%Stream.FileCharacter).%New() set sc=stream.LinkToFile(xmlfile) do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>") do stream.WriteLine("<workbook xmlns='http://schemas.openxmlformats.org/spreadsheetml/2006/main' xmlns:r='http://schemas.openxmlformats.org/officeDocument/2006/relationships'>") do stream.WriteLine("<sheets> <sheet name='"_workSheetName_"' sheetId='1' r:id='rId1'/>") do stream.WriteLine("</sheets> </workbook>") do stream.%Save() }catch{ set status=$$$NO } kill stream return status } _rels/workbook.xml.rels 我们只需要创建一个id为rId1的关系,这样它就会与workbook.xml部分的引用相匹配 ClassMethod CreateRelsXML(){ set status =$$$OK set isunix=$zcvt($p($zv," ",3,$l($p($zv," (")," ")),"U")["UNIX" if isunix { set ext="/" }else{ set ext="\" } set xmlfile = fileDirectory_"_rels"_ext_"workbook.xml.rels" set stream = ##class(%Stream.FileCharacter).%New() set sc=stream.LinkToFile(xmlfile) do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>") do stream.WriteLine("<Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'>") do stream.WriteLine("<Relationship Id='rId1' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet' Target='worksheet.xml'/>") do stream.WriteLine("<Relationship Id='rId2' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles' Target='styles.xml' />") do stream.WriteLine("</Relationships>") try{ do stream.%Save() }catch{ set status=$$$NO } kill stream set xmlfile = fileDirectory_"_rels"_ext_".rels" set stream = ##class(%Stream.FileCharacter).%New() set sc=stream.LinkToFile(xmlfile) do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>") do stream.WriteLine("<Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'>") do stream.WriteLine("<Relationship Id='rId1' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument' Target='workbook.xml'/>") do stream.WriteLine("</Relationships>") try{ do stream.%Save() }catch{ set status=$$$NO } kill stream return status } [Content_Types].xml静态文件(目前,它应该是一个动态文件,取决于工作表的数量)将工作簿的工作表和样式链接在一起。每个Office Open XML文件必须声明ZIP包中使用的内容类型。这是用[Content_Types].xml文件完成的。 ClassMethod GenerateConntentTypesXML(){ set status =$$$OK set xmlfile = tempDirectoryPath_"[Content_Types].xml" set stream = ##class(%Stream.FileCharacter).%New() set sc=stream.LinkToFile(xmlfile) try{ do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>") do stream.WriteLine("<Types xmlns='http://schemas.openxmlformats.org/package/2006/content-types'>") do stream.WriteLine("<Default Extension='rels' ContentType='application/vnd.openxmlformats-package.relationships+xml'/>") do stream.WriteLine("<Override PartName='/workbook.xml' ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'/>") do stream.WriteLine("<Override PartName='/worksheet.xml' ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'/>") do stream.WriteLine("<Override PartName='/styles.xml' ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml' />") do stream.WriteLine("</Types>") do stream.%Save() }catch{ set status=$$$NO } kill stream return status } styles.xml所有的格式化都在这里,目前我已经添加了一些静态样式,(计划将其转换为更多的动态工作簿特定的样式)。 Excel Styles ID Style Excel Format 1 default Text 2 #;[Red]-# Number 3 #.##;[Red]-#.## Number 4 yyyy/mm/dd Date 5 hh:mm Date 6 Header and Center Aligned Text 7 Header 2 Left Aligned Text 8 Good(Green Highlight) General 9 Bad(Red Highlight) General 10 Neutral(Orange Highlight) General 11 yyyy/mm/dd hh:mm Date ClassMethod CreateStylesXML(){ set status =$$$OK set xmlfile = tempDirectoryPath_"styles.xml" try{ set stream = ##class(%Stream.FileCharacter).%New() set sc=stream.LinkToFile(xmlfile) do stream.WriteLine("<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>") do stream.WriteLine("<styleSheet xmlns=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"" xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006"" mc:Ignorable=""x14ac x16r2 xr"" xmlns:x14ac=""http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"" xmlns:x16r2=""http://schemas.microsoft.com/office/spreadsheetml/2015/02/main"" xmlns:xr=""http://schemas.microsoft.com/office/spreadsheetml/2014/revision"">") do stream.WriteLine("<numFmts count=""4"">") do stream.WriteLine("<numFmt numFmtId=""166"" formatCode=""#,##0;[Red]\-#,##0""/>") do stream.WriteLine("<numFmt numFmtId=""168"" formatCode=""#,##0.00;[Red]\-#,##0.00""/>") do stream.WriteLine("<numFmt numFmtId=""169"" formatCode=""dd\/mm\/yyyy;@""/>") do stream.WriteLine("<numFmt numFmtId=""170"" formatCode=""dd/mm/yyyy\ hh:mm""/></numFmts>") do stream.WriteLine("<fonts count=""5"" x14ac:knownFonts=""1"">") do stream.WriteLine("<font><sz val=""10""/><color theme=""1""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>") do stream.WriteLine("<font><sz val=""10""/><color rgb=""FF006100""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>") do stream.WriteLine("<font><sz val=""10""/><color rgb=""FF9C0006""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>") do stream.WriteLine("<font><sz val=""10""/><color rgb=""FF9C5700""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>") do stream.WriteLine("<font><b/><sz val=""10""/><color theme=""1""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font></fonts>") do stream.WriteLine("<fills count=""5"">") do stream.WriteLine("<fill><patternFill patternType=""none""/></fill>") do stream.WriteLine("<fill><patternFill patternType=""gray125""/></fill>") do stream.WriteLine("<fill><patternFill patternType=""solid""><fgColor rgb=""FFC6EFCE""/></patternFill></fill>") do stream.WriteLine("<fill><patternFill patternType=""solid""><fgColor rgb=""FFFFC7CE""/></patternFill></fill>") do stream.WriteLine("<fill><patternFill patternType=""solid""><fgColor rgb=""FFFFEB9C""/></patternFill></fill></fills>") do stream.WriteLine("<borders count=""1""><border><left/><right/><top/><bottom/><diagonal/></border></borders>") do stream.WriteLine("<cellStyleXfs count=""4"">") do stream.WriteLine("<xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0""/>") do stream.WriteLine("<xf numFmtId=""0"" fontId=""1"" fillId=""2"" borderId=""0"" applyNumberFormat=""0"" applyBorder=""0"" applyAlignment=""0"" applyProtection=""0""/>") do stream.WriteLine("<xf numFmtId=""0"" fontId=""2"" fillId=""3"" borderId=""0"" applyNumberFormat=""0"" applyBorder=""0"" applyAlignment=""0"" applyProtection=""0""/>") do stream.WriteLine("<xf numFmtId=""0"" fontId=""3"" fillId=""4"" borderId=""0"" applyNumberFormat=""0"" applyBorder=""0"" applyAlignment=""0"" applyProtection=""0""/></cellStyleXfs>") do stream.WriteLine("<cellXfs count=""12""><xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0""/>") do stream.WriteLine("<xf numFmtId=""49"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" quotePrefix=""1"" applyNumberFormat=""1""/>") do stream.WriteLine("<xf numFmtId=""166"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>") do stream.WriteLine("<xf numFmtId=""168"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>") do stream.WriteLine("<xf numFmtId=""169"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>") do stream.WriteLine("<xf numFmtId=""20"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>") do stream.WriteLine("<xf numFmtId=""49"" fontId=""4"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1"" applyFont=""1""/>") do stream.WriteLine("<xf numFmtId=""49"" fontId=""4"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1"" applyFont=""1"" applyAlignment=""1""><alignment horizontal=""center""/>") do stream.WriteLine("</xf>") do stream.WriteLine("<xf numFmtId=""49"" fontId=""1"" fillId=""2"" borderId=""0"" xfId=""1"" applyNumberFormat=""1""/>") do stream.WriteLine("<xf numFmtId=""0"" fontId=""2"" fillId=""3"" borderId=""0"" xfId=""2""/>") do stream.WriteLine("<xf numFmtId=""0"" fontId=""3"" fillId=""4"" borderId=""0"" xfId=""3""/>") do stream.WriteLine("<xf numFmtId=""170"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/></cellXfs>") do stream.WriteLine("<cellStyles count=""4""><cellStyle name=""Bad"" xfId=""2"" builtinId=""27""/>") do stream.WriteLine("<cellStyle name=""Good"" xfId=""1"" builtinId=""26""/><cellStyle name=""Neutral"" xfId=""3"" builtinId=""28""/>") do stream.WriteLine("<cellStyle name=""Normal"" xfId=""0"" builtinId=""0""/></cellStyles><dxfs count=""0""/>") do stream.WriteLine("<tableStyles count=""0"" defaultTableStyle=""TableStyleMedium2"" defaultPivotStyle=""PivotStyleLight16""/> ") do stream.WriteLine("<extLst><ext uri=""{EB79DEF2-80B8-43e5-95BD-54CBDDF9020C}"" xmlns:x14=""http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"">") do stream.WriteLine("<x14:slicerStyles defaultSlicerStyle=""SlicerStyleLight1""/></ext><ext uri=""{9260A510-F301-46a8-8635-F512D64BE5F5}"" xmlns:x15=""http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"">") do stream.WriteLine("<x15:timelineStyles defaultTimelineStyle=""TimeSlicerStyleLight1""/></ext></extLst>") do stream.WriteLine("</styleSheet>") do stream.%Save() }catch{ set status=$$$NO } kill stream return status } worksheet.xml这是我们的数据所在的地方。工作表的第一行将有列的标题。 接下来的行将只有第一列的数据。我们将在这里定义每一列的列宽,如果不是默认的,列将被设置为自动适应。 worksheet xml 示例 <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <worksheet xmlns="https://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="https://schemas.openxmlformats.org/officeDocument/2006/relationships"> <sheetData> <row> <c t="inlineStr"> <is> <t>Name</t> </is> </c> <c t="inlineStr"> <is> <t>Amount</t> </is> </c> </row> <row> <c t="inlineStr"> <is> <t>Jhon Smith</t> </is> </c> <c> <v>1000.74</v> </c> </row> <row> <c t="inlineStr"> <is> <t>Tracy A</t> </is> </c> <c> <v>6001.74</v> </c> </row> </sheetData> </worksheet> Excel 示例 工作表中的公式可以用函数<f>标签来完成 <c > <f>B2*0.08</f > </c > <c > <f>B2+C2</f > </c> and finally we zip them, rename it to.xlsx (using unix zip) set cmd ="cd "_fileDirectory_" && find . -type f | xargs zip .."_ext_xlsxFile 生成excel文件. 以下代码生成excel 文件. set file = "/temp/test.xlsx" set excelObj = ##class(XLSX.writer).%New(file) do excelObj.SetWorksheetName("test1") set status = excelObj.BeginWorksheet() set row = 0 set row = row+1 ;----------- excelObj.Cells(rowNumber,columnNumber,style,content) set status = excelObj.Cells(row,1,1,"Header1") set row = row+1 set status = excelObj.Cells(row,1,2,"Content 1") set status = excelObj.EndWorksheet() W !,excelObj.fileName 写Excel类请看这里 xlsx.writer.xml.zip
文章
姚 鑫 · 六月 17, 2022

第三章 锁定和并发控制(三)

# 第三章 锁定和并发控制(三) # 升级锁 使用升级锁来管理大量锁。当锁定数组的节点时,它们是相关的,特别是当将多个节点锁定在同一下标级别时。 当给定进程在同一数组中的给定下标级别创建了超过特定数量(默认为 `1000`)的升级锁时, 将删除所有单独的锁名称并用新锁替换它们。新锁位于父级,这意味着数组的整个分支被隐式锁定。示例(如下所示)演示了这一点。 应用程序应在合适的情况下尽快释放特定子节点的锁(与非升级锁完全相同)。当释放锁时, 会减少相应的锁计数。当的应用程序移除足够多的锁时,会移除父节点上的锁。第二小节显示了一个示例。 ## 锁升级示例 假设有 `1000` 个`^MyGlobal("sales","EU",salesdate)` 形式的锁,其中 `salesdate` 表示日期。锁表可能如下所示: ![image](A6C86FBB77E643A88642FDBF2A59D6E3) 注意 `Owner 19776` 的条目(这是拥有锁的进程)。 `ModeCount` 列指示这些是共享的、升级的锁。 当同一进程试图创建另一个相同形式的锁时, 会升级它们。它会移除这些锁并用名称为 `^MyGlobal("sales","EU")` 的单个锁替换它们。现在锁表可能如下所示: ![image](DE73B7B561CA4C0C912C1C3301C3CF32) `ModeCount` 列表明这是一个共享的升级锁,它的计数是 `1001`。 请注意以下关键点: - `^MyGlobal("sales","EU")` 的所有子节点现在都被隐式锁定,遵循数组锁定的基本规则。 - 锁定表不再包含有关 `^MyGlobal("sales","EU")` 的哪些子节点被特别锁定的信息。这在删除锁时具有重要意义。见下一小节。 当同一进程添加更多形式为 `^MyGlobal("sales","EU",salesdate)` 的锁名称时,锁表会增加锁名称 `^MyGlobal("sales","EU")` 的锁计数。锁定表可能如下所示: ![image](AC88A17ABE92442EA4E18CEC6699719E) `ModeCount` 列指示此锁的锁计数现在为 `1026`。 ## 移除升级锁 与非升级锁完全相同,应用程序应尽快释放特定子节点的锁。当这样做时, 会减少升级锁的锁计数。例如,假设代码删除了 `^MyGlobal("sales","EU",salesdate)` 的锁定,其中 `salesdate` 对应于 2011 年的任何日期 — 因此删除了 `365` 个锁定。锁表现在看起来像这样: ![image](D723701CA49E43C9AAD6357C9CC50076) 请注意,即使现在锁的数量低于阈值 (`1000`),锁表也不包含 `^MyGlobal("sales","EU",salesdate)`. 的锁的单独条目。 节点 `^MyGlobal("sales")`保持显式锁定,直到该过程再删除 `661` 个 `^MyGlobal("sales","EU",salesdate)` 形式的锁定。 重要提示:有一点需要考虑,与前面的讨论有关。应用程序可能会“释放”数组节点上的锁,这些节点一开始就从未锁定,从而导致升级锁的锁计数不准确 - 并且可能在需要这样做之前释放升级锁。 例如,假设进程锁定 `^MyGlobal("sales","EU",salesdate)` 中从 2010 年到现在的节点。这将创建超过 `1000` 个锁,并且此锁将按计划升级。假设应用程序中的错误删除了 `1970` 年节点的锁。 将允许此操作,即使这些节点以前没有被锁定,并且 会将锁计数减少 `365`。生成的锁计数不会是所需锁的准确计数。如果应用程序随后移除了其他年份的锁,则升级的锁可能会意外地提前移除。 # Locks, Globals, and Namespaces 锁通常用于控制对全局变量的访问。因为可以从多个命名空间访问全局, 为其锁定机制提供自动跨命名空间支持。该行为是自动的,不需要干预,但在此描述以供参考。有几种情况需要考虑: - 任何命名空间都有一个默认数据库,其中包含持久类和任何其他全局变量的数据;这是此命名空间的全局数据库。访问数据时, IRIS 会从该数据库中检索数据,除非有其他考虑。一个给定的数据库可以是多个命名空间的全局数据库。请参见方案 1。 - 命名空间可以包括提供对存储在其他数据库中的全局变量的访问的映射。请参见方案 2。 - 命名空间可以包括下标级别的全局映射,这些映射提供对部分存储在其他数据库中的全局变量的访问。请参见方案 3。 - 在一个命名空间中运行的代码可以使用扩展引用来访问在此命名空间中不可用的全局变量。请参见方案 4。 尽管锁名称本质上是任意的,但是当使用以插入符号 (`^`) 开头的锁名称时,IRIS 提供了适合这些情况的特殊行为。以下小节给出了详细信息。为简单起见,只讨论排他锁;共享锁的逻辑类似。 ## 场景 1:具有相同Global数据库的多个命名空间 如前所述,虽然进程 `A` 拥有一个具有给定锁名的独占锁,但没有其他进程可以获取任何具有相同锁名的锁。 如果锁名称以插入符号开头,则此规则适用于使用相同全局数据库的所有命名空间。 例如,假设命名空间 `ALPHA` 和 `BETA` 都配置为使用数据库 `GAMMA` 作为其全局数据库。下面显示一个草图: ![image](A7C676A9EEA5405CBE60A5E0271C35DA) 然后考虑以下场景: 1. 在命名空间 `ALPHA` 中,进程 `A` 获得一个名为 `^MyGlobal(15)` 的独占锁。 2. 在命名空间 `BETA` 中,进程 `B` 尝试获取名称为 `^MyGlobal(15)` 的锁。此 `LOCK` 命令不返回;进程被阻塞,直到进程 `A` 释放锁。 在这种情况下,锁表只包含进程 `A` 拥有的锁的条目。如果检查锁表,会注意到它指示了该锁应用到的数据库;请参阅目录列。例如: ![image](D4EB913EE5EC4D14A9B937D0A7009DEB) ## 场景 2:命名空间使用映射的Global 如果一个或多个命名空间包含全局映射,系统会自动跨适用的命名空间强制实施锁定机制。当在非默认命名空间中获得锁时, IRIS 会自动创建额外的锁表条目。 例如,假设命名空间 `ALPHA` 配置为使用数据库 `ALPHADB` 作为其全局数据库。假设命名空间 `BETA` 配置为使用不同的数据库 (`BETADB`) 作为其全局数据库。命名空间 `BETA` 还包括一个全局映射,它指定 `^MyGlobal` 存储在 `ALPHADB` 数据库中。下面显示一个草图: ![image](D00DD242E49146E8BA9F0782DED2B0A5) 然后考虑以下场景: 1. 在命名空间 `ALPHA` 中,进程 `A` 获得一个名为 `^MyGlobal(15)` 的独占锁。 与前面的场景一样,锁表仅包含进程 `A` 拥有的锁的条目。此锁适用于 `ALPHADB` 数据库: ![image](E47C71BD0CCC40FEBD2722F460BC15D4) 2. 在命名空间 `BETA` 中,进程 `B` 尝试获取名称为 `^MyGlobal(15)` 的锁。此 `LOCK` 命令不返回;进程被阻塞,直到进程 `A` 释放锁。 ## 场景 3:命名空间使用映射的`Global`下标 如果一个或多个命名空间包含使用下标级别映射的全局映射,系统会自动跨适用的命名空间强制实施锁定机制。在这种情况下,当在非默认命名空间中获取锁时,IRIS 还会自动创建额外的锁表条目。 例如,假设命名空间 `ALPHA` 配置为使用数据库 `ALPHADB` 作为其全局数据库。命名空间 `BETA` 使用 `BETADB` 数据库作为其全局数据库。 还假设命名空间 `BETA` 还包括一个下标级别的全局映射,因此 `^MyGlobal(15)` 存储在 `ALPHADB` 数据库中(而这个全局的其余部分存储在命名空间的默认位置)。下面显示一个草图: ![image](00394289AF904AFE8EC62C77BB8D5E91) 然后考虑以下场景: 1. 在命名空间 `ALPHA` 中,进程 `A` 获得一个名为 `^MyGlobal(15)` 的独占锁。 2. 与前面的场景一样,锁表仅包含进程 `A` 拥有的锁的条目。此锁适用于 `ALPHADB` 数据库(例如,`c:\InterSystems\IRIS\mgr\alphadb`)。 当非默认命名空间获得锁时,整体行为是相同的,但 IRIS 处理细节略有不同。假设在命名空间 `BETA` 中,一个进程获得了一个名为 `^MyGlobal(15)` 的锁。在这种情况下,锁表包含两个条目,一个用于 `ALPHADB` 数据库,一个用于 `BETADB` 数据库。这两个锁都归命名空间 `BETA` 中的进程所有。 ![image](A3128EA8041344DE8D186A6264F2AEE7) 当此进程释放锁名称 `^MyGlobal(15)` 时,系统会自动删除两个锁。 # 场景 4:扩展的Global引用 在一个命名空间中运行的代码可以使用扩展引用来访问在此命名空间中不可用的全局变量。在这种情况下,IRIS 将一个条目添加到影响相关数据库的锁表中。锁归创建它的进程所有。例如,考虑以下场景。为简单起见,此方案中没有全局映射。 1. 进程 `A` 在 `ALPHA` 命名空间中运行,该进程使用以下命令获取 `BETA` 命名空间中可用的全局锁: ```java lock ^["beta"]MyGlobal(15) ``` 2. 现在锁定表包括以下条目: ![image](9B63E247C2704E26BA9F4C07FE886993) 请注意,这仅显示全局名称(而不是用于访问它的引用)。此外,在这种情况下,`BETADB` 是 `BETA` 命名空间的默认数据库。 3. 在命名空间 `BETA` 中,进程 `B` 尝试获取名称为 `^MyGlobal(15)` 的锁。此 `LOCK` 命令不返回;进程被阻塞,直到进程 `A` 释放锁。 进程私有`Global`在技术上是一种扩展引用,但 IRIS 不支持使用进程私有全局名称作为锁名称;无论如何,都不需要这样的锁,因为根据定义,只有一个进程可以访问这样的全局。
文章
Lilian Huang · 十二月 29, 2023

使用 FHIR 适配器在传统系统上提供 FHIR 服务 - 阅读资源

我们继续推出有关可供 HealthShare HealthConnect 和 InterSystems IRIS 用户使用的 FHIR 适配器工具的系列文章。 在前几篇文章中,我们介绍了小型应用程序,并在此基础上建立了我们的工作,并展示了安装 FHIR 适配器后在 IRIS 实例中部署的架构。在今天的文章中,我们将看到一个示例,说明如何执行最常见的 CRUD(创建 - 读取 - 更新 - 删除)操作之一,即读取操作,我们将通过恢复资源来完成此操作。 什么是资源? FHIR 中的一个资源对应一种相关的临床信息,这种信息可以是病人(Patient)、对实验室的请求(ServiceRequest)或诊断(Condition)等。每种资源都定义了组成它的数据类型,以及对数据的限制和与其他类型资源的关系。每个资源都允许对其包含的信息进行扩展,从而满足 FHIR 80% 以外的需求(满足 80% 以上用户的需求)。 在本文的示例中,我们将使用最常见的资源 "Patient"。让我们来看看它的定义: { "resourceType" : "Patient" , // from Resource: id, meta, implicitRules, and language // from DomainResource: text, contained, extension, and modifierExtension "identifier" : [{ Identifier }], // An identifier for this patient "active" : <boolean>, // Whether this patient's record is in active use "name" : [{ HumanName }], // A name associated with the patient "telecom" : [{ ContactPoint }], // A contact detail for the individual "gender" : "<code>" , // male | female | other | unknown "birthDate" : "<date>" , // The date of birth for the individual // deceased[x]: Indicates if the individual is deceased or not. One of these 2 : "deceasedBoolean" : <boolean>, "deceasedDateTime" : "<dateTime>" , "address" : [{ Address }], // An address for the individual "maritalStatus" : { CodeableConcept }, // Marital (civil) status of a patient // multipleBirth[x]: Whether patient is part of a multiple birth. One of these 2 : "multipleBirthBoolean" : <boolean>, "multipleBirthInteger" : <integer>, "photo" : [{ Attachment }], // Image of the patient "contact" : [{ // A contact party (eg guardian, partner, friend) for the patient "relationship" : [{ CodeableConcept }], // The kind of relationship "name" : { HumanName }, // IA name associated with the contact person "telecom" : [{ ContactPoint }], // IA contact detail for the person "address" : { Address }, // I Address for the contact person "gender" : "<code>" , // male | female | other | unknown "organization" : { Reference(Organization) }, // I Organization that is associated with the contact "period" : { Period } // The period during which this contact person or organization is valid to be contacted relating to this patient }], "communication" : [{ // A language which may be used to communicate with the patient about his or her health "language" : { CodeableConcept }, // R! The language which can be used to communicate with the patient about his or her health "preferred" : <boolean> // Language preference indicator }], "generalPractitioner" : [{ Reference(Organization|Practitioner| PractitionerRole) }], // Patient's nominated primary care provider "managingOrganization" : { Reference(Organization) }, // Organization that is the custodian of the patient record "link" : [{ // Link to a Patient or RelatedPerson resource that concerns the same actual individual "other" : { Reference(Patient|RelatedPerson) }, // R! The other patient or related person resource that the link refers to "type" : "<code>" // R! replaced-by | replaces | refer | seealso }] } 正如您所看到的,它几乎涵盖了患者的所有管理信息需求。 从我们的 HIS 中恢复患者信息 如果您还记得之前的文章中我们部署了一个模拟 HIS 系统数据库的 PostgreSQL 数据库,那么让我们看一下我们特定 HIS 中的示例表。 虽然数量不多,但对于我们的例子来说已经足够了。让我们更详细地看看我们的患者表。 这里我们有 3 个示例患者,您可以看到每个患者都有一个唯一的标识符 ( ID ) 以及一系列与卫生组织相关的管理数据。我们的首要目标是为我们的一位患者获取 FHIR 资源。 患者咨询 我们如何从我们的服务器请求患者数据?根据 FHIR 制定的实现规范,我们必须通过 REST 对包含我们服务器地址、资源名称和标识符的 URL 执行 GET。我们必须调用: http://SERVER_PATH/Patient/{id} 在我们的示例中,我们将搜索 Juan López Hurtado,其 id = 1,因此我们必须调用以下 URL: http://localhost:52774/Adapter/r4/Patient/1 为了进行测试,我们将使用 Postman 作为客户端。让我们看看服务器的响应是什么: { "resourceType" : "Patient" , "address" : [ { "city" : "TERUEL" , "line" : [ "CALLE SUSPIROS 39 2ºA" ], "postalCode" : "98345" } ], "birthDate" : "1966-11-23" , "gender" : "M" , "id" : "1" , "identifier" : [ { "type" : { "text" : "ID" }, "value" : "1" }, { "type" : { "text" : "NHC" }, "value" : "588392" }, { "type" : { "text" : "DNI" }, "value" : "12345678X" } ], "name" : [ { "family" : "LÓPEZ HURTADO" , "given" : [ "JUAN" ] } ], "telecom" : [ { "system" : "phone" , "value" : "844324239" }, { "system" : "email" , "value" : "juanitomaravilla@terra.es" } ] } 现在让我们分析一下我们的请求在生产中所采取的路径: 这里我们有路径: 请求到达我们的 BS InteropService。 将请求转发到我们已配置为 BS 目的地的 BP,在该 BP 中将恢复所接收呼叫的患者标识符。 从我们的 BO FromAdapterToHIS 查询到我们的 HIS 数据库。 将患者数据转发到我们的 BP,并将其转换为 FHIR 患者资源。 将响应转发给BS。 让我们看一下我们在 BP ProcessFHIRBP中收到的消息类型: 让我们看一下三个属性,它们对于识别客户端请求的操作类型至关重要: Request.RequestMethod:它告诉我们要执行什么类型的操作。在此示例中,搜索病人将采用 GET 方式。 Request.RequestPath:该属性包含到达服务器的请求路径,该属性将指示我们要处理的资源,在本例中,它将包括恢复资源的特定标识符。 Quick.StreamId: FHIR 适配器会将收到的每条 FHIR 消息转换为流,并为其分配一个标识符,该标识符将保存在此属性中。在本例中,我们不需要它,因为我们执行的是 GET,并没有发送任何 FHIR 对象。 让我们深入分析负责处理的 GLP,继续我们的消息之旅。 流程FHIRBP: 我们在生产中实施了 BPL,它将管理我们从业务服务收到的 FHIR 消息传递。让我们看看它是如何实现的: 让我们看看每个步骤中将执行的操作: 管理 FHIR 对象: 我们将调用负责连接到 HIS 数据库并负责数据库查询的 BO FromAdapterToHIS。 Method ManageFHIR(requestData As HS.FHIRServer.Interop.Request, response As Adapter.Message.FHIRResponse) As %Status { set sc = $$$OK set response = ##class (Adapter.Message.FHIRResponse). %New () if (requestData.Request.RequestPath = "Bundle" ) { If requestData.QuickStreamId '= "" { Set quickStreamIn = ##class (HS.SDA3.QuickStream). %OpenId (requestData.QuickStreamId,, .tSC) set dynamicBundle = ##class ( %DynamicAbstractObject ). %FromJSON (quickStreamIn) set sc = ..GetBundle (dynamicBundle, .response) } } elseif (requestData.Request.RequestPath [ "Patient" ) { if (requestData.Request.RequestMethod = "POST" ) { If requestData.QuickStreamId '= "" { Set quickStreamIn = ##class (HS.SDA3.QuickStream). %OpenId (requestData.QuickStreamId,, .tSC) set dynamicPatient = ##class ( %DynamicAbstractObject ). %FromJSON (quickStreamIn) set sc = ..InsertPatient (dynamicPatient, .response) } } elseif (requestData.Request.RequestMethod = "GET" ) { set patientId = $Piece (requestData.Request.RequestPath, "/" , 2 ) set sc = ..GetPatient (patientId, .response) } } Return sc } 我们的 BO 将检查收到的HS.FHIRServer.Interop.Request类型的消息,在本例中,通过设置 GET 并在与患者资源对应的路径中指示将调用GetPatient方法,我们将在下面看到: Method GetPatient(patientId As %String , Output patient As Adapter.Message.FHIRResponse) As %Status { Set tSC = $$$OK set sql= "SELECT id, name, lastname, phone, address, city, email, nhc, postal_code, birth_date, dni, gender FROM his.patient WHERE id = ?" //perform the Select set tSC = ..Adapter .ExecuteQuery(.resultSet, sql, patientId) If resultSet.Next() { set personResult = { "id" :(resultSet.GetData( 1 )), "name" : (resultSet.GetData( 2 )), "lastname" : (resultSet.GetData( 3 )), "phone" : (resultSet.GetData( 4 )), "address" : (resultSet.GetData( 5 )), "city" : (resultSet.GetData( 6 )), "email" : (resultSet.GetData( 7 )), "nhc" : (resultSet.GetData( 8 )), "postalCode" : (resultSet.GetData( 9 )), "birthDate" : (resultSet.GetData( 10 )), "dni" : (resultSet.GetData( 11 )), "gender" : (resultSet.GetData( 12 )), "type" : ( "Patient" )} } else { set personResult = {} } //create the response message do patient.Resource.Insert(personResult. %ToJSON ()) Return tSC } 正如您所看到的,此方法仅在我们的 HIS 数据库上启动查询并恢复所有患者信息,然后生成一个 DynamicObject,随后将其转换为 String 并存储在Adapter.Message.FHIRResponse类型的变量中。我们已将 Resource 属性定义为字符串列表,以便能够稍后在跟踪中显示响应。您可以直接将其定义为 DynamicObjects,从而节省后续转换。 检查是否捆绑: 根据 BO 的响应,我们检查它是否是 Bundle 类型(我们将在以后的文章中解释)或者它是否只是一个 Resource。 创建动态对象: 我们将 BO 响应转换为 DynamicObject 并将其分配给临时上下文变量 (context.temporalDO)。用于转换的函数如下: ##class ( %DynamicAbstractObject ). %FromJSON (context.FHIRObject.Resource.GetAt( 1 )) FHIR 变换: 使用 DynamicObject 类型的临时变量,我们将其转换为HS.FHIR.DTL.vR4.Model.Resource.Patient类的对象。如果我们想寻找其他类型的资源,我们必须为每种类型定义特定的转换。让我们看看我们的转变: 这种转换使我们能够拥有 BS InteropService 可以解释的对象。我们将结果存储在变量context.PatientResponse中。 将资源分配给 Stream : 我们将FHIR变换中获得的变量context.PatientResponse转换为Stream。 转换为 QuickStream: 我们将必须返回给客户端的所有数据分配给响应变量: set qs= ##class (HS.SDA3.QuickStream). %New () set response.QuickStreamId = qs. %Id () set copyStatus = qs.CopyFrom(context.JSONPayloadStream) set response.Response.ResponseFormatCode= "JSON" set response.Response.Status= 200 set response.ContentType= "application/fhir+json" set response.CharSet = "utf8" 在这种情况下,我们总是返回 200 响应。在生产环境中,我们应该检查是否已正确恢复搜索到的资源,如果没有,请将响应状态从 200 修改为对应“未找到”的 404。正如您在此代码片段中看到的,对象HS.FHIR.DTL.vR4.Model.Resource.Patient转换为 Stream 并存储为HS.SDA3.QuickStream ,将所述对象的标识符添加到QuickStreamID属性,随后我们的 InteropService 服务将以 JSON 形式正确返回结果。 结论: 让我们总结一下我们所做的事情: 我们发送了一个 GET 类型的请求,以搜索具有定义 ID 的患者资源。 BS InteropService已将请求转发至配置的BP。 BP 调用了负责与 HIS 数据库交互的 BO。 已配置的 BO 已从 HIS 数据库检索患者数据。 业务处理程序将结果转换为默认互操作服务创建的 BS 可理解的对象。 BS已收到响应并将其转发给客户端。 如您所见,操作相对简单,如果我们想在服务器中添加更多类型的资源,只需在 BO 中添加对数据库中与要恢复的新资源相对应的表的查询,并在 BP 中将 BO 的结果转换为与之相对应的 HS.FHIR.DTL.vR4.Model.Resource.* 类型的对象。 在下一篇文章中,我们将回顾如何将患者类型的新 FHIR 资源添加到我们的 HIS 数据库中。 感谢大家的关注!
文章
姚 鑫 · 二月 9, 2021

第二十九章 Caché 变量大全 $ZERROR 变量

# 第二十九章 Caché 变量大全 $ZERROR 变量 包含上一个错误的名称和位置。 # 大纲 ``` $ZERROR $ZE ``` # 描述 `$ZERROR`包含最新错误的名称,最新错误的位置(在适用的情况下)以及(对于某些错误代码而言)有关导致错误的原因的其他信息。 `$ZERROR`始终包含相应语言模式的最新错误。 `$ZERROR`值旨在错误后立即使用。由于`$ZERROR`值可能不会在例程调用中保留,因此希望保留`$ZERROR`值以供以后使用的用户应将其复制到变量中。**强烈建议用户在使用后立即将`$ZERROR`设置为空字符串(“”)。** $ZERROR中包含的字符串可以是以下任何一种形式: ```java entryref info entryref info ``` - `` 错误名称。错误名称始终以全部大写字母返回,并用尖括号括起来。它可能包含空格。 - `entryref` 对发生错误的代码行的引用。它由标签名称和距该标签的行偏移量组成,后跟`^`和程序名称。此`entryre`f紧跟在错误名称的右尖括号之后。从终端调用`$ZERROR`时,此`entryref`信息没有意义,因此不会返回。对最近使用`ZLOAD`加载到例程缓冲区中的例程的引用。 - `info` 特定于某些错误类型的附加信息(见下表)。此信息与``或`entryref`之间用空格分隔。如果有多个组件要提供信息,则用逗号分隔。 例如,一个程序(名为`zerrortest`)包含以下例程(名为`ZerrorMain`),该例程试图写入`fred`(一个未定义的局部变量)的内容: ```java /// d ##class(PHA.TEST.SpecialVariables).ZERROR() ClassMethod ZERROR() { ZerrorMain TRY { SET $ZERROR="" WRITE "$ZERROR = ",$ZERROR,! WRITE fred } CATCH { WRITE "$ZERROR = ",$ZCVT($ZERROR,"O","HTML") } } ``` ```java DHC-APP> d ##class(PHA.TEST.SpecialVariables).ZERROR() $ZERROR = $ZERROR = <UNDEFINED>zZERROR+5^PHA.TEST.SpecialVariables.1 *fred ``` 在上面的示例中,第一个`$ZERROR`包含一个空字符串(`“”`),因为自从`$ZERROR`重置为空字符串以来没有发生任何错误。尝试写入未定义的变量会设置`$ZERROR`并将其抛给`CATCH`块。此`$ZERROR`包含`ZerrorMain+4^zerrortest*fred`,指定错误的名称、位置和特定于该类型错误的附加信息。在本例中,附加信息是未定义的局部变量`fred`的名称;星号前缀表示它是局部变量。(请注意,本例中使用`$ZCVT($ZERROR,“O”,“HTML”)`,因为Caché错误名称用尖括号括起来,并且本例从Web浏览器运行。) `Entryref`可能如下所示: - `ZerrorMain+4^zerrortest`--程序`zerrortest`中标签`ZerrorMain`的4行偏移量 - `ZerrorMain^zerrortest`--在程序`zerrortest`中没有与标签`ZerrorMain`的偏移量;标签行中出现错误 - `+3^zerrortest`--从程序`zerrortest`开始的3行偏移量;错误行前面没有标签 `$ZERROR`值的最大长度为512个字符。超过该长度的值将被截断为512个字符。 ## AsSystemError() Method `%Exception.SystemException`类的`AsSystemError()`方法返回与`$ZERROR`相同的值。下面的示例显示了这一点: ```java /// d ##class(PHA.TEST.SpecialVariables).ZERROR1() ClassMethod ZERROR1() { TRY { KILL mylocal WRITE mylocal } CATCH myerr { WRITE "AsSystemError is: ",myerr.AsSystemError(),! WRITE "$ZERROR is: ",$ZERROR } } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZERROR1() AsSystemError is: zZERROR1+3^PHA.TEST.SpecialVariables.1 *mylocal $ZERROR is: zZERROR1+3^PHA.TEST.SpecialVariables.1 *mylocal ``` **在`Try/Catch`异常处理块结构中,`AsSystemError()`比`$ZERROR`更可取,因为`$ZERROR`可能会被异常处理期间发生的错误覆盖。** ## 有关某些错误的其他信息 当发生某些类型的错误时,`$ZERROR`将以以下格式返回错误: ```java entryref info ``` `INFO`组件包含有关错误原因的附加信息。下表列出了错误列表,其中包括附加信息和该信息的格式。错误代码与`INFO`组件之间用空格字符分隔。 错误代码 |信息组件 ---|--- `` | 未定义变量的名称(包括使用的任何下标)。这可以是局部变量、进程私有全局属性、全局属性或多维类属性。局部变量名称以星号作为前缀。多维属性名以句点开头,以区别于本地变量名。通过设置`%SYSTEM.Process.Unfined()`方法,可以更改Caché行为,以便在引用未定义的变量时不会生成``错误。 `` | 错误的下标引用:生成错误的行引用(例程和行偏移)、下标变量以及错误的下标级别。对于结构化系统变量(SSVN),仅提供行引用(例程和行偏移量)。通过设置`%SYSTEM.Process.NullSubscript()`方法,可以更改默认行为,以便在引用字符串下标为空的全局变量时不会生成错误。局部变量不允许使用空字符串下标。 `` |前缀为星号,即引用的例程名称。 `` | 前缀为星号,即引用的类名。 `` | 前缀为星号(引用属性的名称),后跟逗号分隔符和应该在其中的类名。 `` |前缀是星号,即调用的方法的名称,后跟逗号分隔符和应该在其中的类名。 `` | 全局引用的名称和包含全局引用的目录的名称,用逗号分隔。 `` |前缀为星号、对象名称,后跟`DisplayString()`方法返回的值。 `` | 当不在事务中调用`TCOMMIT`时,`INFO`组件为`*NoTransaction`。当调用不返回值的用户定义函数时,`INFO`组件是一条消息,其中包含本应返回值的命令的位置。 `` |以星号为前缀的无效目录的完整路径名。 `` | 当``错误终止进程时,带有附加信息的``错误将作为消息写入`mgr/cconsole.log`。信息性消息显示已终止进程的进程ID(PID)和产生错误的行引用(例程和行偏移量)。例如:`(PID)0at+13^|“user\|mytest` 例程(或方法)本地变量的名称以及未定义例程、类、属性和方法的名称都以星号(`*`)为前缀。进程-专用全局变量由其`^||`前缀标识。全局变量由它们的`^`(插入符号)前缀标识。类名以其`%`前缀形式表示。 以下示例显示了指定错误原因的其他错误信息。在每种情况下,指定的项都不存在。请注意,生成的错误的`INFO`组件与错误名称之间用空格分隔。星号(`*`)表示局部变量、类、属性或方法。插入符号(`^`)表示全局,`^||`表示进程私有全局。 ``错误示例: ```java /// d ##class(PHA.TEST.SpecialVariables).ZERROR2() ClassMethod ZERROR2() { UndefTest ; SET $NAMESPACE="SAMPLES" KILL x,abc(2) KILL ^xyz(1,1),^|"USER"|xyz(1,2) KILL ^||ppg(1),^||ppg(2) TRY { WRITE x } // 未定义的局部变量 CATCH { WRITE $ZERROR,! } TRY { WRITE abc(2) } // 未定义的下标局部变量 CATCH { WRITE $ZERROR,! } TRY { WRITE ^xyz(1,1) } // 未定义的全局变量 CATCH { WRITE $ZERROR,! } TRY { WRITE ^|"USER"|xyz(1,2) } // 另一个命名空间中未定义的全局变量 CATCH { WRITE $ZERROR,! } TRY { WRITE ^||ppg(1) } // 未定义的进程专用全局变量 CATCH { WRITE $ZERROR,! } TRY { WRITE ^|"^"|ppg(2) } // 未定义的进程专用全局变量 CATCH { WRITE $ZERROR,! } } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZERROR2() zZERROR2+7^PHA.TEST.SpecialVariables.1 *x zZERROR2+13^PHA.TEST.SpecialVariables.1 *abc(2) zZERROR2+19^PHA.TEST.SpecialVariables.1 ^xyz(1,1) zZERROR2+25^PHA.TEST.SpecialVariables.1 ^xyz(1,2) zZERROR2+31^PHA.TEST.SpecialVariables.1 ^||ppg(1) zZERROR2+37^PHA.TEST.SpecialVariables.1 ^||ppg(2) ``` ``错误的示例: ```java /// d ##class(PHA.TEST.SpecialVariables).ZERROR3() ClassMethod ZERROR3() { SubscriptTest ; DO $SYSTEM.Process.NullSubscripts(0) KILL abc,xyz TRY { SET abc(1,2,3,"")=123 } CATCH { WRITE $ZERROR,! } TRY { SET xyz(1,$JUSTIFY(1,1000))=1 } CATCH { WRITE $ZERROR,! } } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZERROR3() zZERROR3+5^PHA.TEST.SpecialVariables.1 *abc() Subscript 4 is "" zZERROR3+11^PHA.TEST.SpecialVariables.1 *xyz() Subscript 2 > 511 chars ``` ``错误的示例: ```java /// d ##class(PHA.TEST.SpecialVariables).ZERROR4() ClassMethod ZERROR4() { NoRoutineTest ; KILL ^NotThere TRY { DO ^NotThere } CATCH { WRITE $ZERROR,! } TRY { JOB ^NotThere } CATCH { WRITE $ZERROR,! } TRY { GOTO ^NotThere } CATCH { WRITE $ZERROR,! } } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZERROR4() zZERROR4+4^PHA.TEST.SpecialVariables.1 *NotThere zZERROR4+10^PHA.TEST.SpecialVariables.1 *NotThere zZERROR4+16^PHA.TEST.SpecialVariables.1 *NotThere ``` 对象错误的示例: ```java DHC-APP>DO $SYSTEM.SQL.MyMethod() DO $SYSTEM.SQL.MyMethod() ^ *MyMethod,%SYSTEM.SQL DHC-APP>WRITE $SYSTEM.XXQL.MyMethod() WRITE $SYSTEM.XXQL.MyMethod() ^ *%SYSTEM.XXQL DHC-APP>SET x=##class(%SQL.Statement).%New() DHC-APP>WRITE x.MyProp WRITE x.MyProp ^ *MyProp,%SQL.Statement ``` ``错误的示例(在Windows上): ```java // 用户没有%SYS名称空间的访问权限 SET x=^|"%SYS"|var ^var,c:\intersystems\cache\mgr\ ``` 调用用户定义函数时的``错误示例。在本例中,`MyFunc Quit`命令不返回值。这将生成一个``错误,其中`entryref`指定`$$MyFunc`调用的位置,`INFO`消息指定`QUIT`命令的位置: ```java /// d ##class(PHA.TEST.SpecialVariables).ZERROR5() ClassMethod ZERROR5() { Main TRY { KILL x SET x=$$MyFunc(7,10) WRITE "returned value is ",x,! RETURN } CATCH { WRITE "$ZERROR = ",$ZCVT($ZERROR,"O","HTML"),! } MyFunc(a,b) SET c=a+b QUIT } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZERROR5() $ZERROR = <COMMAND>zZERROR5+4^PHA.TEST.SpecialVariables.1 *Function must return a value at zZERROR5+13^PHA.TEST.SpecialVariables.1 ``` 使用`PUBLIC`关键字将函数作为过程调用时,出现相同的``错误: ```java Main TRY { KILL x SET x=$$MyFunc(7,10) WRITE "returned value is ",x,! RETURN } CATCH { WRITE "$ZERROR = ",$ZCVT($ZERROR,"O","HTML"),! } MyFunc(a,b) PUBLIC { SET c=a+b QUIT } ``` ``错误示例(在Windows上): ```java /// d ##class(PHA.TEST.SpecialVariables).ZERROR6() ClassMethod ZERROR6() { TRY { SET prev=$SYSTEM.Process.CurrentDirectory("bogusdir") WRITE "previous directory: ",prev,! RETURN } CATCH { WRITE "$ZERROR = ",$ZCVT($ZERROR,"O","HTML"),! QUIT } } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZERROR6() $ZERROR = <DIRECTORY>zCurrentDirectory+2^%SYSTEM.Process.1 *e:\dthealth\db\dthis\data\bogusdir\ ``` ## 5.1版本之前的错误处理代码 在Caché5.1和后续版本的这些错误代码中添加`INFO`组件的结果是,假设`$ZERROR`中的字符串格式的5.1版本之前的错误处理例程可能需要重新设计才能像以前一样工作。例如,以下内容在5.1版中将不再有效: ```java WRITE "Error line: ", $PIECE($ZERROR, ">", 2) ``` 并应更改为类似以下内容: ```java WRITE "Error line: ", $PIECE($PIECE($ZERROR, ">", 2), " ", 1) ``` # 注意 ## ZLOAD和错误消息 在`ZLOAD`操作之后,加载到例程缓冲区中的例程的名称出现在后续错误消息的`entryref`部分。这将在整个过程中持续存在,或者直到使用`ZREMOVE`删除,或者被另一个`ZLOAD`删除或替换。以下终端示例显示例程缓冲区内容的此显示: ```java SAMPLES>ZLOAD Sample.Person.1 SAMPLES>WRITE 6/0 ^Sample.Person.1 SAMPLES>WRITE fred ^Sample.Person.1 *fred SAMPLES>WRITE ^fred ^Sample.Person.1 ^fred SAMPLES>ZNAME "USER" USER>WRITE 7/0 ^Sample.Person.1 USER>ZREMOVE USER>WRITE ^fred ^fred ``` ## $ZERROR和程序栈 `$ZERROR`字符串的``部分包含最新的错误消息。`$ZERROR`字符串的`entryref`部分的内容反映了最近错误的堆栈级别。以下终端会话试图调用无意义的命令`gobbledegook`,导致``错误。它还运行`ZerrorMain`(上面指定),产生`$ZERROR`值``。此终端会话期间的后续`$ZERROR`值反映了此程序调用,如下所示: ```java SAMPLES>gobbledegook SAMPLES>WRITE $ZERROR SAMPLES>DO ^zerrortest SAMPLES>WRITE $ZERROR ZerrorMain+2^zerrortest *FRED SAMPLES 2d0>gobbledegook SAMPLES 2d0>WRITE $ZERROR ^zerrortest SAMPLES 2d0>QUIT SAMPLES>WRITE $ZERROR ^zerrortest SAMPLES>gobbledegook SAMPLES>WRITE $ZERROR ``` ## 设置`$ZTRAP`时的`$ZERROR`操作 发生错误并设置`$ZTRAP`时,Caché在`$ZERROR`中返回错误消息,并分支到为`$ZTRAP`指定的错误陷阱处理程序 ## 设置`$ZERROR` 只有在Caché模式下,才能使用`set`命令将`$ZERROR`设置为最多512个字符的值。长度超过512个字符的值将被截断为512。 **强烈建议在错误处理后将`$ZERROR`重置为空字符串(`“”`)。**
文章
Hao Ma · 五月 26, 2023

IRIS镜像配置(4)_配置后的步骤

题外话:我刚刚翻译了InterSystems专家Bob Binstock的[Caché Mirroring 101:简要指南和常见问题解答](https://cn.community.intersystems.com/post/cach%C3%A9-mirroring-101%EF%BC%9A%E7%AE%80%E8%A6%81%E6%8C%87%E5%8D%97%E5%92%8C%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E8%A7%A3%E7%AD%94)。 尽管题目是Caché Mirror 101, 而且是写于2016年,但因为讲解的都是Mirror的基本原理,所以在大量使用IRIS的今天也完全适用。 前面的3篇文章,包括了配置Mirror的各个方面。如果您照着操作,现在已经有了一个工作的mirror环境,并加入了您的数据库。然而,还没完,这篇我来讨论一下后面的工作,首先的问题是: **Mirror不复制什么** 简单说,Caché/IRIS镜像是**数据库复制(Database Replication)**。在Caché/IRIS里什么是数据库?也就是**Cache.dat和iris.dat**文件。数据库的修改日志,也就是journal,从主机被传送到其他镜像成员。而除此之外的内容,需要维护人员来分别的个个处理, 解决这些内容在各个镜像成员间的拷贝。需要很多的计划和细心。 >系统数据库, 包括IRISSYS, IRISTEMP, IRISLIB等等, 这些Caché/IRIS本身的数据库不应该被加入Mirror,在大多数Caché/IRIS版本里也都设置成不可以加入入MIRROR。 > >例外的HealthCare产品, HSSYS需要做Mirror, HSCustom可以做Mirror, 而HSLIB不可以Mirror 我们可以把问题转换成下面的题目: ## 需要人工在镜像成员中同步的项目 ### 命名空间(namespace)和Mapping 命名空间是应用开发的概念,它使用数据库。命名空间定义了3种映射关系:Package Mapping, Routing Mapping, Global Mapping。这样在一个命名空间可以使用多个数据库的内容。 通常情况下,用户会在主机创建命名空间的同时,创建一个新的带有mirror属性的数据库,然后会在其他mirror成员中手工一个个的创建命名空间,加入镜像的数据库。之后,管理员无需考虑更多的操作。 然而,对命名空间的修改,比如要添加或者删除命名空间的某些mapping,这偶尔会需要,尤其是应用迭代和系统扩容的情况下,那么,管理员/实施人员,必须清楚Mirror无法同步这个修改,您必须手工同步修改到其他机器去。 如果配置的mapping比较多, 我建议使用Manifest来操作。Mainfest是一个xml的文本,用来安装或者修改Caché/IRIS的配置,你可以参考[在线文档: Using a Manifest](https://docs.intersystems.com/iris20231/csp/docbook/DocBook.UI.Page.cls?KEY=GCI_manifest), 或者社区文章[使用Manifest](https://cn.community.intersystems.com/post/%E4%BD%BF%E7%94%A8manifest)。 这里给一个配置mapping的例子: ```xml ``` 如果是资深的Caché维护工程师,懂得如果修改CPF文件并在不重启实例的情况下应用修改后的内容,可以考虑把主机上的CPF中的mapping部分复制粘贴到其他机器。如果您没有这方面的经验,我不建议这种方式。 另外,在IRIS 2022后的版本中有了一个新工具,Configuration Merge。 文档在[这里](https://docs.intersystems.com/iris20231/csp/docbook/Doc.View.cls?KEY=ACMF)。可惜只有最新版的IRIS或者Health Connect 用户有的用。 ### 数据库的修改 数据库的内容会通过Journal从主机同步到其他成员,但修改不会,一般会遇到的是**压缩和截断**。 由于某种错误操作,某个数据库,会扩展到不正常的大,而当错误修正后,用户可能需要对该数据库进行压缩和截断,以释放被错误占用的空闲的磁盘空间。 由于除主机外,其他镜像成员的数据库都是只读的,这个操作的顺序应该是这样: 1. 在主机A执行压缩和截断 2. 切换到备机B, 再次执行压缩和截断。 3. 异步成员DR。 一种方案是吧DR提升到备机。这时当前的备机A会将为灾备,然后再切换DR为主机,再进行压缩和截断。 还有一个选择,就是重新配置DR上的这个数据库,这需要从主机到DR的数据库备份和恢复。 ### IRIS实例的配置 从最常用的内存的配置,Service的配置, **用户,权限,资源**的配置等等。它们都不会被MIRROR同步。如果您在MIRROR主机里做了修改了缩表的大小,或者启动了一个,比如TELNET服务, 您需要人工在其他机器上做相同操作。 像上面的mapping配置一样,这里还是建议使用Manifest人工同步IRIS得修改。注意的是,Mainfest不保证能支持所有的配置。比如在Caché的版本下, 比如您在主机上启动了TELNET服务, Manifest没有相应的标签。这种情况下, 如果您熟悉ObjectScript语言,可以把ObjectScript实现加入执行Manifest的方法,比如说: ```java ClassMethod main(){ //执行Manifest修改命名空间 Set pVars("Namespace")="MYNAMESPACE" $$$ThrowOnError(..ModifyNamespace(.pVars)) //启动IRIS的TELNET服务 set properties("Enabled")=1 // 有効 set sts=##class(Security.Services).Modify("%Service_Telnet",.properties) } ``` 当然,如果您缺乏开发实施的知识,在用户界面上一个个机器的操作是最省心的办法。 问题是,打开一个服务,修改一个配置参数操作都很简单,但是如果要添加大量的用户和权限怎么办? 用Manifest管理是一个办法。但根本上,如果您经常有大量的用户管理的工作,其实使用Kerberos或者LDAP管理用户身份认证和授权的工作, 在有多个镜像成员的情况下,尤其的合适。 关于这部分内容,请参考[在线文档:Authentication and Authorization](https://docs.intersystems.com/iris20231/csp/docbook/DocBook.UI.Page.cls?KEY=PAGE_security_authentication_authorization) ### 定时任务(TASK) 在主机上创建的定时任务, 您需要人工在其他机器上做相同操作。这里有2个步骤: 1. 在主机上创建新任务的时候,要选择”**应如何为镜像运行任务**“。 这是个下拉菜单,选项有*”仅在主镜像成员上运行“,“仅在非主镜像成员上运行“ ,“在任何镜像成员上运行"。* 选择的出发点是:非主镜像成员的数据库是只读的。因此,比如一个Ensemble的镜像配置中, 删除Ensemble消息的定时任务, 一定是”仅在主镜像成员上运行“。 2. 把新的定时任务从主机同步到其他成员。 ​ 如果是一个或者少量几个TASK, 那么手工在其他各个镜像成员上添加是最简单直接的做法。而如果是有很长 的任务列表,尤其在配置Mirror得时候可以需要同步一个长长的列表时, 您可以考虑**从主机导出Task到其 他机器导入**,我只知道使用ObjectScript命令的方法, 使用`%SYS.Task.ExportTask()`和 `%SYS.Task.ImportTasks()`。 文档在[这里](https://docs.intersystems.com/iris20231/csp/documatic/%25CSP.Documatic.cls?LIBRARY=%25SYS&CLASSNAME=%25SYS.Task)。 ### Web Application 主机上配置的Web Applicaiton 也要同步到其他镜像成员。如果要同步的Web Application比较多,推荐的方式依然是Manifest, 下面是一个例子。 ```xml ``` 麻烦的是不同的版本Caché/IRIS使用的标签上会略有不同,要稍微仔细的查看一下您的版本的文档。 如果您对ZPM, 现在称为IPM熟悉的话, 用ZPM做同步也是个好选择。关于zpm, 您可以参考这个帖子[zpm介绍](https://cn.community.intersystems.com/post/zpm%E4%BB%8B%E7%BB%8D1)。提醒一下的是,程序因为是存在数据库里面的,如果该数据库是被镜像的,您其实不需要用ZPM把程序代码拷贝到其他镜像成员。 ### Gateway 一般用到的有**SQL Gateway**和**External Language Gateway**,它们分别用于连接其他的数据库和使用其他语音的代码包。 SQL Gateway 记录保存在%SYS命名空间的*%Library.sys_SQLConnection*数据表里。简单的方法是使用工具把表记录导入导出。 External Language Gateway(外部语言网关) 新版的IRIS系统内嵌了外部语言服务器,包括%Python Server, %Java Server, %Dotnet Server等。如果您使用的是默认配置,各个镜像成员是一致的,无需操心。如果只是IP端口的修改,手工同步一下也很容易,毕竟工作量有限,只是您需要清楚的记得,这个也是不被Mirror自动同步的。 ### 文件 我把文件分为两类, 一类是“固定文件”,包括一下几个部分, - CSP文件,js文件,css文件,html文件等 - XSLT文件 - 其他语言的程序代码,Java文件,python文件, .Net文件 这类文件上传到主机的时候, 也必须上传到其他镜像成员,这是个简单的操作,别忘了就行。 麻烦的是**流文件**。在ObjectScript里如果使用了%Stream.FileBinary, %Stream.FileCharacter等类,那么数据不是保存到Cache.Dat或者IRIS.data, 而是保存在和.Dat同目录的一个stream的子目录下,而这个目录是不会被镜像同步的。 而且,因为这是实时数据,你也不可能手工的把它拷来拷去。 如果您的应用里用到了文件流,我任务您需要一个文件服务器保证流文件在各个各个镜像成员间的同步。 ### Ensemble Production Consideration 对于Ensemble和Health Connect用户,您需要阅读这部分在线文档: [Production Considerations for Mirroring](https://docs.intersystems.com/iris20223/csp/docbook/DocBook.UI.Page.cls?KEY=GHA_mirror_manage#GHA_mirror_set_ensemble) , 简单总结一下: - 创建的带有ensemble或者Inteoprability的命名空间,数据库要创建为Mirror的数据库。 - **"production是否自动启动“**应该在主机和备机上,甚至DR上都配置为“自动启动”。 在Mirror配置下的Production会先检查这个实例是不是主机,如果不是,“自动启动”的配置也不会生效,这样保证了Production只在主机上运行,而切换后也不需要人工干预。 上面的这些并不是完整的内容,尽管在大多少情况下这些内容差不多够了。如果您想要确保Mirror的主机的工作内容完全同步到了备机和DR, 请仔细阅读在线文档的这一部分:[Mirror Configuration Guidelines](https://docs.intersystems.com/iris20223/csp/docbook/DocBook.UI.Page.cls?KEY=GHA_mirror_set_config#GHA_mirror_set_config_guidelines) 另外,对于各种需要人工同步的内容的操作,还建议阅读[在线文档:Server Migration](https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=AMIG#AMIG_migration_external)。 如果是最新的IRIS用户,请参考[在线文档:Deploy Mirrors Using Configuration Merge](https://docs.intersystems.com/iris20223/csp/docbook/DocBook.UI.Page.cls?KEY=GHA_mirror_set_config#GHA_mirror_set_config_auto_merge)
文章
Hao Ma · 九月 17, 2022

IRIS镜像配置(3)

# 把数据库添加进Mirror 以往的经验里, 用户在把数据库添加到镜像时遇到过各种各样的问题,以致必须请求外部帮助才能解决。除了步骤本身比较繁琐,很大的原因是阅读文档不细致。还有一个,就是对英文水平不太高的用户,有些英文句式并不是很好懂,比如说,文档中有这一句其实非常关键: > If you attempt to add a new database to the mirror on a nonprimary member that was not created as a mirrored database on the primary, but rather added to the mirror after it was created, an error message notes this and you cannot complete the operation. 我用最好的翻译器DeepL翻译后的中文是: **如果你试图在一个非主要成员上向镜像添加一个新的数据库,而这个数据库并不是在主要成员上作为镜像数据库创建的,而是在创建后添加到镜像中的,那么就会出现错误信息提示,你无法完成操作。** 很讨厌的是它没用说明错误信息是什么,以致于很多用户, 当他们在Backup成员中把一个数据库添加到镜像时,遇到相关的错误时,没有把问题和这句话关联起来,这个错误提示是这样的: >“错误 #2105: 与成员 SERVERA/IRIS 中的相匹配的数据库 :mirror:AUGEST:DEMO 未被创建为镜像数据库”。 或者用英文, > ERROR #2105: Matching mirrored DB :mirror:AUGEST:DEMO in member SERVERA/IRIS was not created as mirrored DB 我来解释一下这句话,它说的是: ”嘿, 你在本机要添加的:mirror:AUGEST:DEMO数据库, 它在主镜像成员SERVERA/IRIS里, 未被创建为镜像数据库。“ 如果您看了我的解释, 还觉得莫名其秒,我相信您其实是没懂这个关键点: ​ **“一个数据库创建成镜像数据库,和创建成普通数据库后面后再添加到镜像里,它们是不同的。”** 关于这一点,其实文档也有说明,啰嗦,但说明了原因。直接上翻译: > 创建镜像数据库(即添加一个不含数据的新数据库)的过程与向镜像添加现有数据库的过程不同。作为镜像数据库创建的数据库上的Global操作从一开始就被记录在镜像Journal中,因此镜像可以访问它所需要的所有数据,以便在镜像成员之间同步数据库。但现有数据库在被添加到镜像之前的Global操作包含在非镜像Journal文件中,镜像不能访问这些文件。由于这个原因,一个现有的数据库在被添加到镜像后,必须在主故障转移成员上进行备份,并在备份故障转移成员和它要所在的任何异步成员上进行恢复。一旦这样做了,你必须激活并赶上数据库,使其与主数据库保持同步。 清楚了这个关键, 您才能理解为什么安装步骤分为下面的两个类型, - **创建新的镜像数据库** - **将已有的数据库加入镜像** > TIP: 另外,还有一个值得提醒的:只有用户自己的数据库可以被加入镜像。系统本身的数据库, 比如IRISSYS, IRISLIB, IRISTEMP等等,都不能加入镜像。早期有些版本可以,NOMORE! ## 创建新的镜像数据库 - 在**主镜像成员**的系统维护界面上,选择System Administration – Configuration – System Configuration – Local Databases , 选择Create New Database. **在数据库创建向导窗口,在“镜像数据库?”下拉菜框,选择'是‘(Yes)**。 SQL"页面, 确认表Persons同步到了所有的镜像成员。 ​ > 这里如果您遇到上面提到的“Error 2105“, 那就是这个数据库在Primary上先是创建成一般数据库,然后加入的镜像,那您应该按下面的步骤操作了。 > > 如果有人好奇:在Primary上的这种区别,Backup是怎么知道的,它不是还没加入到镜像吗? 故事是这样的: 镜像日志中同步的不是只有镜像数据库的数据的修改, 还包括IRISSYS, IRISAUDIT,等库的内容。NEWDB在主成员中是怎么加入到镜像的, IRISSYS里的Global Set是不一样的,而这个set, 是同步给备用成员backup的。 又一个没用的知识。 ## 将已有的数据库加入镜像 **已有的(Existing)数据库是指原本在主成员里按普通数据库创建的,然后加入镜像的数据库。** 这样的情况,哪怕同样名称,配置的数据库在其他成员上已经有了。能直接加入镜像吗?比如你主成员上有个User, 备用成员上也有,您能在主机, 备机直接把它们加入镜像吗? 答案是肯定不行。系统根本没法保证这两个库里面已有的数据是一样的。**您要在主成员上备份数据库,在其他成员恢复, 而恢复操作成功后,在其他成员上,这个数据库自动变成了“镜像数据库”**, 也就是加入了镜像。 这个同名的数据库要先在其他成员上创建。创建成普通数据库。如果其他成员上已经有了,也不用删除,就直接用主机的备份文件覆盖就好。 以下是详细的步骤: - 在主机的“系统>配置>本地数据库“页面, 点击**添加到镜像**按钮。然后在跳出窗口中选中您要添加的数据库,可以一次选多个。 数据库很大或者多个数据库同时加入是,可以选中”在后台运行“。通常这个添加动作是在秒级时候内完成的,无所谓是否后台运行。 - 到镜像监视器查看添加的结果。被添加的数据库状态这时候应该是"一般"(Normal) 。 - 到其他镜像成员的镜像监视器查看, 您会看到主机来的通知引发的提醒: - 在备机检查自己的数据库状态。如果没有DEMO或者USER数据库,那么创建它们,创建时下拉框”是否镜像?”选择否或者NO。之后在本地数据库列表中它们应该是这样,注意没有在镜像里。 - 在Primary做数据库的在线备份, 用于后面步骤里到其他成员上去做数据库恢复。 以下过程仅供参考: ```sh # 在主成员备份,并发送给备份成员serverb %SYS>do ^BACKUP 1) Backup 2) Restore ALL 3) Restore Selected or Renamed Directories 4) Edit/Display List of Directories for Backups 5) Abort Backup 6) Display Backup volume information 7) Monitor progress of backup or restore Option? 1 *** The time is: 2022-09-17 15:27:48 *** InterSystems IRIS Backup Utility -------------------------- What kind of backup: 1. Full backup of all in-use blocks 2. Incremental since last backup 3. Cumulative incremental since last full backup 4. Exit the backup program 1 => 1 Specify output device (type STOP to exit) Device: /isc/FullDBList_user.cbk => /isc/setmirror.cbk Backing up to device: /isc/setmirror.cbk Description: Backing up the following directories: /isc/data/demo/ /isc/iris/mgr/user/ Start the Backup (y/n)? => y Journal file switched to: /isc/jrnpri/MIRROR-AUGEST-20220917.011 Starting backup pass 1 Backing up /isc/data/demo/ at 09/17/2022 15:28:26 Copied 82 blocks in 0.004 seconds Finished this pass of copying /isc/data/demo/ Backing up /isc/iris/mgr/user/ at 09/17/2022 15:28:28 Copied 908 blocks in 0.475 seconds Finished this pass of copying /isc/iris/mgr/user/ Backup pass 1 complete at 09/17/2022 15:28:29 Starting backup pass 2 Backing up /isc/data/demo/ at 09/17/2022 15:28:31 Copied 2 blocks in 0.000 seconds Finished this pass of copying /isc/data/demo/ Backing up /isc/iris/mgr/user/ at 09/17/2022 15:28:33 Copied 2 blocks in 0.000 seconds Finished this pass of copying /isc/iris/mgr/user/ Backup pass 2 complete at 09/17/2022 15:28:33 Starting backup pass 3 Journal file '/isc/jrnpri/MIRROR-AUGEST-20220917.010' and the subsequent ones are required for recovery purpose if the backup were to be restored Journal marker set at offset 197572 of /isc/jrnpri/MIRROR-AUGEST-20220917.011 - This is the last pass - Suspending write daemon Backing up /isc/data/demo/ at 09/17/2022 15:28:35 Copied 2 blocks in 0.000 seconds Finished this pass of copying /isc/data/demo/ Backing up /isc/iris/mgr/user/ at 09/17/2022 15:28:35 Copied 2 blocks in 0.001 seconds Finished this pass of copying /isc/iris/mgr/user/ Backup pass 3 complete at 09/17/2022 15:28:35 ***FINISHED BACKUP*** Global references are enabled. Backup complete. 1) Backup 2) Restore ALL 3) Restore Selected or Renamed Directories 4) Edit/Display List of Directories for Backups 5) Abort Backup 6) Display Backup volume information 7) Monitor progress of backup or restore Option? %SYS>!scp /isc/setmirror.cbk root@172.16.58.102:/isc Enter passphrase for key '/root/.ssh/id_rsa': root@172.16.58.102's password: setmirror.cbk 100% 8448KB 49.4MB/s 00:00 %SYS> ``` - 在其他成员上恢复数据库,这里分两种情况: - 其他成员上没有这个数据库: 比如我的serverb没有DEMO数据库,要做的是:创建一个DEMO数据库,使用和servera一样的设置,除了**在下拉框“镜像数据库?“,回答”NO“** - 其他成员上有这个库,比如备机serverb里有User, 不用管它,下面我们就可以直接把它覆盖掉。 请参考下面的数据库恢复过程。 **提醒一点:不要使用第一个选项“All Directories", 该选项不能用其他机器的备份文件恢复本机。** ```sh # 在Backup成员serverb上执行,恢复用源文件拷贝自servera %SYS>do ^DBREST Cache DBREST Utility Restore database directories from a backup archive Restore: 1. All directories 2. Selected and/or renamed directories 3. Display backup volume information 4. Exit the restore program 1 => 2 Do you want to set switch 10 so that other processes will be prevented from running during the restore? Yes => Specify input file for volume 1 of backup 1 (Type STOP to exit) Device: /isc/setmirror.cbk This backup volume was created by: IRIS for UNIX (Red Hat Enterprise Linux 7 for x86-64) 2022.1 The volume label contains: Volume number 1 Volume backup SEP 17 2022 03:28PM Full Previous backup SEP 16 2022 09:11AM Full Last FULL backup SEP 16 2022 09:11AM Description Buffer Count 0 Mirror name AUGEST Failover Member SERVERA/IRIS Is this the backup you want to start restoring? Yes => This backup was made on the other mirror member. Limit restore to mirrored databases? yes For each database included in the backup file, you can: -- press RETURN to restore it to its original directory; -- type X, then press RETURN to skip it and not restore it at all. -- type a different directory name. It will be restored to the directory you specify. (If you specify a directory that already contains a database, the data it contains will be lost). /isc/data/demo/ (:mirror:AUGEST:DEMO) => /isc/iris/mgr/user/ (:mirror:AUGEST:USER) => Do you want to change this list of directories? No => Restore will overwrite the data in the old database. Confirm Restore? No => Yes ***Restoring /isc/data/demo/ at 15:47:09 82 blocks restored in 0.0 seconds for this pass, 82 total restored. Expanding /isc/iris/mgr/user/ ... Expanding /isc/iris/mgr/user/ from 1 MB to 654 MB ***Restoring /isc/iris/mgr/user/ at 15:47:12 908 blocks restored in 0.0 seconds for this pass, 908 total restored. ***Restoring /isc/data/demo/ at 15:47:12 2 blocks restored in 0.0 seconds for this pass, 84 total restored. ***Restoring /isc/iris/mgr/user/ at 15:47:12 2 blocks restored in 0.0 seconds for this pass, 910 total restored. ***Restoring /isc/data/demo/ at 15:47:12 2 blocks restored in 0.0 seconds for this pass, 86 total restored. ***Restoring /isc/iris/mgr/user/ at 15:47:12 2 blocks restored in 0.0 seconds for this pass, 912 total restored. Specify input file for volume 1 of backup following SEP 17 2022 03:28PM (Type STOP to exit) Device: Do you have any more backups to restore? Yes => no Mounting /isc/data/demo/ which is a mirrored DB /isc/data/demo/ ... (Mounted) Mounting /isc/iris/mgr/user/ which is a mirrored DB /isc/iris/mgr/user/ ... (Mounted) Journal records for mirrored DBs were restored successfully. %SYS> ``` - 检查数据库列表中的状态,注意它们已经成了AUGEST的镜像数据库了, **而且它们是只读模式**。 - 在serverb上查看镜像监视器,确认它们的状态是Dejournaling 后面您可以像上面提到的,在主机上操作数据, 确认数据修改同步给了备机。到此这部分工作才算结束。 > 如果只有外部备份文件: > > 按照文档上的说法,如果用外部备份在非主成员恢复,恢复后需要在镜像监视器的”镜像数据库列表里“点击"ACtiviate", 直到看到状态为Caaught up为至。请参考文档,我不是很清楚细节。 # 其他的镜像操作 这里我说说怎么删除镜像, 以及其他的一些常用操作的要点, 比如什么时候使用“SET NO FAILOVER”等等。 TO BE CONTINUED...
文章
Qiao Peng · 十二月 4, 2023

通用RESTful 业务服务和业务操作

1. 通用RESTful业务服务和业务操作 InterSystems IRIS 提供了一组通用的RESTful 业务服务和业务操作类,用户无需开发自定义的业务服务和业务操作类,就可以直接向外提供RESTful服务和调用外部的RESTful API。 BS EnsLib.REST.GenericService 通用REST业务服务 BS EnsLib.REST.SAMLGenericService 检查SAML令牌的签名和时间戳的REST业务服务 BO EnsLib.REST.GenericOperation 通用REST业务操作 BO EnsLib.REST.GenericOperationInProc 用于透传模式的通用REST业务操作 2. 通用RESTful 消息 通用的RESTful 业务服务和业务操作类使用一个通用的RESTful消息类 - EnsLib.REST.GenericMessage,它是EnsLib.HTTP.GenericMessage的子类,二者数据结构都是 HTTPHeaders 记录http头的数组 Stream 记录http体的数据流 Type 数据流类型,例如是字符流还是二进制流。自动赋值,无需设置 Attributes 记录属性的数组 OriginalFilename 无需使用 OutputFolder 无需使用 OutputFilename 无需使用 因此EnsLib.REST.GenericMessage和EnsLib.HTTP.GenericMessage都可以被通用RESTful业务操作和业务服务所使用。 3. 通用RESTful 业务操作 使用通用的RESTful业务操作,可以连接到任何第三方的RESTful服务器,调用其RESTful API。 3.1 向production中加入通用RESTful业务操作 增加通用RESTful业务操作,只需要在Production配置页面的操作中添加EnsLib.REST.GenericOperation。 建议加入Production时,给业务操作起一个名字,用于代表具体的业务,例如是连接到LIS的RESTful 服务,可以命名为RESTtoLIS(可以考虑的命名规则 - 接口方式+业务系统)。如果未命名,默认会使用类名作为业务操作名。 3.2 配置通用RESTful业务操作 主要的设置项是以下3个: 1. HTTP服务器:目标RESTful服务器的服务器名或IP地址 2. HTTP端口:目标RESTful服务器提供RESTful API的端口号 3. URL:RESTful API的服务端点 启用该业务操作后,既可以访问外部RESTful API了。 3.3 测试通用RESTful业务操作 启用后,加入的通用的RESTful业务操作即可测试了。因为EnsLib.HTTP.GenericMessage的REST消息体是一个流类型的属性,为了测试时方便输入这个数据,我们增加一个业务流程。 1. 创建一个新的业务流程,设置其请求消息为Ens.StringRequest,用于测试时传入REST body数据。并为其上下文增加一个名为DataBody、类型为%Stream.GlobalCharacter(可持久化的字符流类型)的属性: 2. 在业务流程中增加一个代码流程(<code>),将请求消息的字符串数据写入上下文的DataBody字符流: Do context.DataBody.Write(request.StringValue) 注意行首加空格。 3. 然后在业务流程中再加入一个调用流程(<call>),调用上面已经加入production的业务操作,例如RESTtoLIS,并设置请求和响应消息为EnsLib.REST.GenericMessage或EnsLib.HTTP.GenericMessage。 4. 配置RESTtoLIS业务操作的请求消息(Request) 可以直接点击构建请求消息(Request Builder)按钮,使用图形化拖拽建立请求消息: 4.1 将左边上下文context里的DataBody拖拽到callrequest的Stream属性上; 4.2 对callrequest的HTTPHeaders赋值,它是一个元素类型为字符串的数组,代表HTTP请求的头。以下3个HTTP头是必须要填写的: HTTP头属性说明 下标 值 HTTP方法 "httprequest" 例如"POST" HTTP消息体的内容类型 "content-type" 例如"application/json" 客户端希望接收的内容类型 "Accept" 例如"*/*" 这3个数组元素赋值,可以通过在添加操作下拉列表中设置(Set)进行赋值。 5. 将业务流程加入Production,并测试 确保Production的设置是允许调试。在Production配置页面中选中这个业务流程,在右侧的操作标签页中选择测试按钮,并在弹出的测试消息页面里填入测试用的数据,并点击调用测试服务: 然后可以检查测试的消息处理流程,并确认REST消息体和HTTP消息头被正确地传递到目标REST API 4. 通用RESTful 业务服务 使用通用的RESTful业务服务,可以向外发布能处理任何RESTful API调用请求的RESTful服务端。 4.1 将通用RESTful业务服务加入Production 在Production配置页面,点击服务后面的加号。弹出的向导页面,服务类选择EnsLib.REST.GenericService;输入服务名,建议写一个能代表组件功能的名字,例如向HIS系统开放的REST服务,可以起名RESTforHIS;选中立即启用。 RESTful通用业务服务可以通过2种方式向外提供RESTful API服务:第一种通过Web服务器向外提供服务,第二种使用IRIS服务器的特定TCP端口向外提供服务。第二种方式不依赖于独立的Web服务器,但推荐使用Web服务器,从而得到更好的性能和安全性。 这里我们使用Web服务器提供REST服务,因此在业务服务的端口配置中,保持空白。在接受消息的目标名称中,选择接收RESTful API请求的业务流程或业务操作,这里我们测试使用一个空的业务流程。点击应用激活这些设置。 4.2 建立一个向外提供RESTful API的Web应用 向外发布RESTful服务,不仅涉及到服务发布的URL,还涉及到安全。我们通过创建一个专用的Web应用来进行管理和控制。 在IRIS系统管理门户>系统管理>安全>应用程序>Web应用程序 中,点击新建Web应用程序按钮,新建一个Web应用程序,并做以下配置: 1. 名称,填写一个计划发布的服务端点,例如/IRISRESTServer。注意前面的/ 2. NameSpace,选择Production所在的命名空间 3. 选中启用 REST,并设置分派类为EnsLib.REST.GenericService 4. 根据安全需要,配置安全设置部分。这里方便测试起见,允许的身份验证方法选择了未验证(无需验证)。如果是生产环境,或者您在做性能压力测试,都应该选择密码或Kerberos安全的身份验证方式! 注意,请保证同一个命名空间下,仅有一个分派类为EnsLib.REST.GenericService的REST类型的Web应用。 4.3 测试RESTful业务服务 现在就可以测试这个RESTful业务服务了。这个RESTful服务可以响应任何REST API的请求,如何响应则是后续业务流程/业务操作的事。 它的完整的RESTful URL是:[Web服务器地址]:[Web服务器端口]/[Web应用的名称]/[通用REST服务在production中的配置名]/[API名称和参数],例如我在IRIS本机的私有Apache的52773端口上访问上面创建的REST通用业务服务,调用PlaceLabOrder的API (注意,这里我们并没有实现过PlaceLabOrder这个API,但我们依然可以响应,而不会报404错误),那么完整的REST 调用地址是: 127.0.0.1:52773/IRISRESTServer/RESTforHIS/PlaceLabOrder 打开POSTMAN,用POST方法,发起上面REST API的调用: 在IRIS里会得到类似这样的消息追踪结果,如果你没有实现过处理REST API请求的业务流程,会得到一个500错,但依然可以查看IRIS产生的EnsLib.HTTP.GenericMessage消息内容: 这个通用RESTful业务服务会把REST请求转换为EnsLib.HTTP.GenericMessage消息,向目标业务操作/业务流程发送。因此,通过解析它的消息内容,就知道REST API请求的全部信息: 1. Stream里是POST的数据 2. HTTPHeaders 的下标"HttpRequest"是HTTP的方法 3. HTTPHeaders 的下标"URL"是完整的API路径,包括了服务端点(在"CSPApplication"下标下)、REST业务服务名称(在"EnsConfigName"下标下)和API 后续业务流程可以通过这些数据对REST API请求进行响应。 4.4 使用业务流程对REST API调用进行路由 有了通用RESTful业务服务生成的EnsLib.HTTP.GenericMessage消息,我们就可以使用消息路由规则或业务流程对REST API请求进行路由。这里我使用业务流程方法对REST API请求进行路由演示。 构建一个新的业务流程,请求消息和响应消息都是EnsLib.REST.GenericMessage或EnsLib.HTTP.GenericMessage,同时为context增加一个名为ReturnMsg的字符串类型的属性,并设置它默认值为:"{""Code"":-100,""Msg"":""未实现的API""}"。 在业务流程里增加一个<switch>流程,然后在<switch>下增加2个条件分支,分别为: 名称:下达检验医嘱,条件:判断是否http头的URL为PlaceLabOrder,且http头的HttpRequest为POST: (request.HTTPHeaders.GetAt("URL")="/IRISRESTServer/RESTforHIS/PlaceLabOrder") && (request.HTTPHeaders.GetAt("HttpRequest")="POST") 名称:查询检验项目,条件:判断是否http头的URL为GetLabItems,且http头的HttpRequest为GET: (request.HTTPHeaders.GetAt("URL")="/IRISRESTServer/RESTforHIS/GetLabItems") && (request.HTTPHeaders.GetAt("HttpRequest")="GET") 在两个分支里,分别增加<code>, 产生返回的REST消息内容: Set context.ReturnMsg="{""Code"":200,""Msg"":""检验医嘱下达成功""}" Set context.ReturnMsg="{""Code"":200,""Msg"":""查询检验项目成功""}" 最后在<switch>后增加一个<code>,构建响应消息: // 初始化响应消息 set response = ##class(EnsLib.REST.GenericMessage).%New() // 初始化响应消息的流数据 Set response.Stream = ##class(%Stream.GlobalCharacter).%New() // 将REST返回数据写入流 Do response.Stream.Write(context.ReturnMsg) 编译这个业务流程,并将其加入Production。 之后修改通用RESTful业务服务的设置,将接收消息的目标名称改为这个新建的业务流程。 现在再通过POSTMAN测试一下各种API,并查看返回REST响应: 在真实项目中,根据实际情况,将上面<switch>流程分支的<code>替换为API响应业务流程或业务操作即可。 总结:使用通用RESTful业务操作和业务服务,无需创建自定义的RESTful 业务组件类,就可以调用外部RESTful API和向外提供RESTful API服务,降低开发和实施成本,实现低代码开发。 后记:关于EnsLib.REST.GenericService对CORS(跨域资源共享)的支持 CORS是一种基于 HTTP 头的机制,通过允许服务器标示除了它自己以外的其它origin(域、协议和端口)等信息,让浏览器可以访问加载这些资源。所以要让EnsLib.REST.GenericService支持CORS,需要让它的响应消息增加对于CORS支持的HTTP头的信息,这里不详细介绍这些头含义了,大家可以去W3C的网站或者搜索引擎查询具体定义,最简单可以使用以下代码替代上面4.4中的初始化响应消息代码: // 设置HTTP响应的头信息 set tHttpRes=##class(%Net.HttpResponse).%New() set tHttpRes.Headers("Access-Control-Allow-Origin")="*" set tHttpRes.Headers("Access-Control-Allow-Headers")="*" set tHttpRes.Headers("Access-Control-Allow-Methods")="*" // 初始化响应消息 set response = ##class(EnsLib.REST.GenericMessage).%New(,,tHttpRes)
文章
Claire Zheng · 六月 12

CHIMA访谈:青岛大学附属医院医院信息系统升级记

“一根筷子易折断,十根筷子抱成团”,这句话在青岛大学附属医院(以下简称“青大附院”)的医院信息系统升级换代中得到了淋漓尽致的体现。 创建于1898年的青大附院是一家山东省省属的综合性三甲医院,拥有市南院区、崂山院区、西海岸院区、市北院区和平度院区共五个院区。在医院发展的过程中,青大附院按照国家、山东省、青岛市的要求,结合医院特色,进行信息化建设的统一规划。“五个院区的规划、业务流程和管理理念是一致的,秉承着多院区集中管理的理念,采取的是集中开发、分院区部署的方式。”青大附院信息管理部主任辛海燕介绍。 多年来在医院信息化上深耕细作,令青大附院的五个院区实现了医务、护理、医疗设备、人事、后勤等的一体化管理,系统实现了互联互通,达到了信息共享的目标,让医院业务不中断。五个院区的信息化底层是打通的,围绕医疗和管理两条线进行,遵循的原则是在符合国家政策的前提下,以用户需求为出发点进行建设,可以向医院管理层和患者提供一体化服务。 软技能铸造团队精神:加强信息团队内部沟通与协作 目前青大附院信息管理部共有三个业务科室:计算机中心,负责日常硬件和软件的管理运维;项目开发中心,主要负责所有信息系统的软件实施;网络管理中心,主要负责信息网络安全、机房运维和所有终端的准入。 辛海燕介绍,青大附院的信息化团队重视技术和沟通这两大基础能力:信息管理部每位成员都必须掌握自己负责的技术,要用一个开放、学习、严谨的心态对待这份工作;要会沟通,在与临床科室、合作伙伴沟通时,要擅长“翻译”,这就需要到临床和管理中学习流程。 除此之外,青大附院的信息化团队非常重视内部的信息同步建设共享,方便整个团队在技术和工作流程同步,加强团队成员的团结和协作。 这些软技能,铸造了迎考信息系统大升级时必不可少的团队精神。 迎考:医院信息系统大升级 这次升级是十年一遇的大升级。 青大附院原有系统是2012年上线的,随着时代发展和需求的增长,对新功能应用的支持慢慢变得力不从心,限制了信息化支撑业务发展的能力。“信息化平台应该是为医院高质量发展提供帮助的,原有的医院信息系统逐渐无法满足医院高质量发展的需求。在通过多轮评估后,经管理层批准,医院于2022年8月启动医院信息系统升级,并于2023年3月14日顺利完成升级。”辛海燕介绍道。 这是一场硬仗。为使系统升级不妨碍医疗业务开展,青大附院对升级顺序进行了排序。“3月10日,我们门、急诊实现了‘一刀切’式的升级,当天晚上,我们就启用了急诊系统。第二天医生上班后,直接启用新的门诊系统。在新的门、急诊系统稳定运行后,4天后(3月14日),我们成功切换了住院系统。”辛海燕介绍道。至此,青大附院信息系统升级第一阶段完成。 “在升级过程中,我们面临的最大压力是院区多,此次是对四个院区(市南院区、崂山院区、西海岸院区、市北院区)和两个中心(生殖中心和美容中心)同步进行升级。在整个过程中,最担心是导致业务意外中断——比如患者挂不上号、看不上诊、住不上院。因此,在整个系统升级过程中,我们首先要确保的是系统可以有效支撑业务的连续性运转。”辛海燕强调,医院信息系统升级不能中断医疗业务,信息部与临床科室、合作伙伴团结协作,“在各方的支持下,我们最终顺利、稳定地完成了这次升级。”辛海燕回忆道。 在信息系统升级过程中,有一个小插曲。按照最初的规划,青大附院系统升级应该在2022年年底完成,方便新旧年度财务数据交接。由于疫情的原因,系统升级延续到了2023年春节后。在这个过程中,医院计算机中心工作人员到门诊、住院和管理部门统计硬件数量,经过半年多时间,更新了科室工作站的环境,确保每台工作站兼容新旧系统。摸清了每一个科室的每一台电脑、每一台打印机,统计发现医院内网终端数量共有6685台,保障升级后正常访问。 “这是一个细活,不能有疏漏。” 辛海燕指出,在系统升级期间,项目开发团队每天与合作伙伴一起,评估上线各类需求,网络安全管理团队为升级提供硬件环境和网络安全加固,计算机中心对终端工作站进行全面梳理,确保新旧系统的运行环境能兼容。“这段时间我们承受的是成倍的工作压力。我要特别感谢团队,因为这是一项系统性工程,事情本身是有些复杂,整个过程必须齐心协力,哪个环节掉链子都不行。”辛海燕说。 辛苦拼搏获得回报,团结协作推动事情顺利进展。经过近半年的努力,青大附院的信息系统升级工作圆满完成! 借助坚实的数字化底座,助力医院高质量发展 此次进行的信息系统升级,对底层数据库和业务系统平台进行了升级,在数据临床质量和流程方面更加精细,比如可在系统里加上闭环管理等很精细化的流程。信息系统升级换代后,医院很多业务流程都得到了更好的优化,比如门诊医生工作站、护士工作站、手术麻醉信息系统等,都变得更为友好。 “系统升级后数据库系统运行效率大大提高,在使用操作上更贴近实际业务流程,工作站的界面做了优化,更符合操作习惯,医护人员反馈说新系统用起来更加顺手,常用功能也比以前增加很多,还可以随时查看诊疗活动状态和历史诊疗数据。”辛海燕举例,任何一个院区的医生在工作界面写病历时,都可以随时查看患者的检查检验报告的进度,这不需要切换页面,实现了医疗流程和业务的无缝对接,使业务和系统的结合更紧密。 随着行业对“数据二十条”探讨的深入,越来越多的共识是:医疗数据正在成为医院的核心资产。“如何把数据保护好、利用好,让数据成为资产,助力医院高质量发展,这是我们系统升级后努力的方向。”辛海燕强调。 在这次升级中,医院数据库升级到了InterSystems IRIS医疗版数据平台。“IRIS医疗版数据平台包括数据库、中间件等等,是一个很全面、集成的平台,效率很高,非常适合医疗领域。它的建模方式和医院所需要数据库的架构是一样的。非常稳定,扩展性也很强。”辛海燕指出,“由于对FHIR等多个医疗数据标准的深度支持,IRIS医疗版数据平台在数据资产利用上有很大优势,我们看重的是依托这个平台可以‘长’很多应用,比如医疗质控、人工智能、临床决策支持等等。这些都是助力医院实现高质量发展必不可少的能力。在用数据说话这件事上,这个平台的优势非常大,能够高效、稳定地支撑业务运转。” 辛海燕以一个非常普遍的业务场景为例进行了说明。医生在多个院区出诊,数据调用是最常见的业务场景。为了更好地利用好临床数据,青大附院建设了临床数据中心,将五个院区所有患者病历数据汇集到该中心,底层用一个“容器”将所有数据都加载到一起。医生在工作站可查看到患者在五个院区和两个中心的所有门诊和住院就诊情况,包括挂号时间、检查检验报告、手术时间、治疗计划等,实现了扁平化阅读,对其后续治疗做出合理规划。不论患者在哪个院区就诊,其他院区的医生都可以即时调阅其检查、检验报告,不需要任何等待,立等可取。 据了解,青大附院此次信息系统升级只是完成了信息系统建设规划的第一步。“医院下一步信息化建设重点是以HRP为主的管理平台建设,打破管理部门信息系统‘烟囱’现状,建立以HRP为主的一体化管理平台,借助临床生产出的数据给管理做抓手,实现医院向管理要效益的目标,助力医院实现高质量发展。”辛海燕十分有信心。 附:青大附院信息系统选型标准 在进行系统选型时,青大附院认为医院和合作伙伴之间是合作共赢的关系,会考量合作伙伴的技术实力和先进性,以及它在大型三甲医院的案例是否足够有说服力。同时,医院非常看重合作伙伴的技术团队稳定性,能否有长效机制留住技术人才是医院在系统选型时着重考量的一个因素。“首先要稳定,稳定之后,再去谋发展。”辛海燕表示。 (本文原载于CHIMA微信公众号)
文章
姚 鑫 · 十一月 4, 2021

第六十六章 SQL命令 REVOKE

# 第六十六章 SQL命令 REVOKE 从用户或角色中删除特权。 # 大纲 ```sql REVOKE admin-privilege FROM grantee REVOKE role FROM grantee REVOKE [GRANT OPTION FOR] object-privilege ON object-list FROM grantee [CASCADE | RESTRICT] [AS grantor] REVOKE [GRANT OPTION FOR] SELECT ON CUBE[S] object-list FROM grantee REVOKE column-privilege (column-list) ON table FROM grantee [CASCADE | RESTRICT] ``` ## 参数 - `admin-privilege` - 管理员级特权或以前授予要撤销的管理员级特权的以逗号分隔的列表。 可用的`syspriv`选项包括`16`个对象定义权限和`4`个数据修改权限。对象定义权限为:`%CREATE_FUNCTION`, `%DROP_FUNCTION`, `%CREATE_METHOD`, `%DROP_METHOD`, `%CREATE_PROCEDURE`, `%DROP_PROCEDURE`, `%CREATE_QUERY`, `%DROP_QUERY`, `%CREATE_TABLE`, `%ALTER_TABLE`, `%DROP_TABLE`, `%CREATE_VIEW`, `%ALTER_VIEW`, `%DROP_VIEW`, `%CREATE_TRIGGER`, `%DROP_TRIGGER`。 或者,可以指定`%DB_OBJECT_DEFINITION`,这将撤销所有`16`个对象定义特权。数据修改权限为`INSERT`、`UPDATE`、`DELETE`操作的`%NOCHECK`、`%NOINDEX`、`%NOLOCK`、`%NOTRIGGER`权限。 - `grantee` - 拥有SQL系统权限、`SQL`对象权限或角色的一个或多个用户的列表。 有效值是一个以逗号分隔的用户或角色列表,或`“*”`。 星号(`*`)指定当前定义的所有没有`%all`角色的用户。 - `AS grantor` - 此子句允许通过指定原始授予者的名称来撤销另一个用户授予的特权。 有效的授予者值是用户名、以逗号分隔的用户名列表或`“*”`。 星号(`*`)指定当前定义的所有授予者。 要使用`AS`授予器子句,必须具有`%All`角色或`%Admin_Secure`资源。 - `role` - 一个角色或以逗号分隔的角色列表,这些角色的权限将从用户被撤销。 - `object-privilege` - 基本级别特权或先前授予要撤销的基本级别特权的逗号分隔列表。 该列表可以包含以下一个或多个:`%ALTER`、`DELETE`、`SELECT`、`INSERT`、`UPDATE`、`EXECUTE`和`REFERENCES`。 要撤销所有特权,可以使用`“all [privileges]”`或`“*”`作为此参数的值。 注意,您只能从多维数据集撤销`SELECT`特权,因为这是惟一可授予的多维数据集特权。 - `object-list` - 一个以逗号分隔的列表,其中包含一个或多个正在撤销对象特权的表、视图、存储过程或多维数据集。 可以使用`SCHEMA`关键字指定从指定模式中的所有对象撤销对象特权。 可以使用`" * "`指定从当前命名空间中的所有对象撤销对象特权。 - `column-privilege` - 从一个或多个列列表列出的列撤销基本权限。 可用选项有`SELECT`、`INSERT`、`UPDATE`和`REFERENCES`。 - `column-list` - 由一个或多个列名组成的列表,用逗号分隔,用括号括起来。 - `table` - 包含列列表列的表或视图的名称。 # 描述 `REVOKE`语句撤销允许用户或角色在指定的表、视图、列或其他实体上执行指定任务的权限。 `REVOKE`还可以撤销用户分配的角色。 `REVOKE`撤销`GRANT`命令的操作; 特权只能由授予特权的用户撤消,或者通过`CASCADE`操作(如下所述)。 可以从指定用户、用户列表或所有用户(使用`*`语法)撤销角色或特权。 因为`REVOKE`的准备和执行速度很快,而且通常只运行一次,所以`IRIS`不会在`ODBC`、`JDBC`或动态SQL中为`REVOKE`创建缓存查询。 即使不能执行实际的撤销(例如,指定的特权从未被授予或已经被撤销),`REVOKE`也会成功地完成。 但是,如果在`REVOKE`操作期间发生错误,`SQLCODE`将被设置为负数。 ## 撤销的角色 角色可以通过`SQL GRANT`和`REVOKE`命令授予或撤销,也可以通过`^SECURITY IRIS System SECURITY`命令授予或撤销。 可以使用`REVOKE`命令从某个用户撤消一个角色,也可以从另一个角色撤消一个角色。 不能使用`IRIS System Security`将角色授予或撤销给其他角色。 特殊变量`$ROLES`不显示授予角色的角色。 `REVOKE`可以指定单个角色,也可以指定要撤销的角色列表,以逗号分隔。 `REVOKE`可以从指定的用户(或角色)、用户(或角色)列表或所有用户(使用*语法)中撤销一个或多个角色。 `GRANT`命令可以将一个不存在的角色授予用户。 可以使用`REVOKE`命令从现有用户撤销不存在的角色。 但是,角色名必须使用与授予角色时相同的字母大小写来指定。 如果试图从不存在的用户或角色撤销现有角色, IRIS将发出`SQLCODE -118`错误。 如果不是超级用户,并且试图撤销一个不拥有且没有`ADMIN OPTION`的角色,InterSystems IRIS将发出`SQLCODE -112`错误。 ## 撤销对象权限 对象特权赋予用户或角色对特定对象的某些权限。 从一个被授予者的对象列表上撤销一个对象特权。 对象列表可以在当前名称空间中指定一个或多个表、视图、存储过程或多维数据集。 通过使用逗号分隔的列表,单个`REVOKE`语句可以从多个用户和/或角色中撤销多个对象上的多个对象特权。 可以使用星号(`*`)通配符作为对象列表值,从当前名称空间中的所有对象撤销对象特权。 例如,`REVOKE SELECT ON * FROM Deborah`将撤销该用户对所有表和视图的SELECT权限。 `REVOKE EXECUTE ON * FROM Deborah`将撤销该用户对所有非隐藏存储过程的`EXECUTE`权限。 可以使用`SCHEMA SCHEMA -name`作为对象列表值,以撤销指定模式中当前名称空间中的所有表、视图和存储过程的对象特权。 例如,`REVOKE SELECT ON SCHEMA Sample FROM Deborah`将撤销该用户对`Sample`模式中所有对象的`SELECT`权限。 可以将多个模式指定为逗号分隔的列表; 例如,`REVOKE SELECT ON SCHEMA Sample,Cinema FROM Deborah`撤销`Sample`和`Cinema`模式中所有对象的`SELECT`权限。 可以从用户或角色撤消对象特权。 如果从某个角色撤销该权限,则仅通过该角色拥有该权限的用户将不再拥有该权限。 不再拥有特权的用户不能再执行需要该对象特权的现有缓存查询。 当`REVOKE`撤销对象特权时,它将成功完成并将`SQLCODE`设置为0。 如果`REVOKE`没有执行实际的撤销(例如,指定的对象权限从未被授予或已经被撤销),它将成功完成,并将`SQLCODE`设置为`100`(不再有数据)。 如果在`REVOKE`操作期间发生错误,它将`SQLCODE`设置为负数。 多维数据集是不受模式名称限制的SQL标识符。 要指定多维数据集对象列表,必须指定`CUBE`(或cubes)关键字。 因为多维数据集只能有`SELECT`权限,所以您只能从多维数据集撤销`SELECT`权限。 对象权限可以通过以下任意方式撤销: - `REVOKE command`. - `$SYSTEM.SQL.Security.RevokePrivilege()`方法。 - 通过IRIS系统安全。 转到管理门户,选择系统管理、安全、用户(或系统管理、安全、角色),为所需的用户或角色选择`Edit`,然后选择SQL表或SQL视图选项卡。 在下拉列表中选择`Namespace`。 向下滚动到所需的表,然后单击`revoke`来撤销权限。 可以通过调用`%CHECKPRIV`命令来确定当前用户是否具有指定的对象特权。 通过调用`$SYSTEM.SQL.Security.CheckPrivilege()`方法,可以确定指定的用户是否具有指定的表级对象特权。 ## 撤销对象所有者特权 如果从对象的所有者那里撤消对SQL对象的特权,那么所有者仍然隐式地拥有对对象的特权。 为了从对象的所有者完全撤销对象上的所有特权,必须更改对象以指定不同的所有者或没有所有者。 ## 撤销表级和列级特权 `REVOKE`可用于撤销表级特权或列级特权的授予。 表级特权提供对表中所有列的访问。 列级特权提供对表中每个指定列的访问。 向表中的所有列授予列级特权在功能上等同于授予表级特权。 然而,这两者在功能上并不完全相同。 列级`REVOKE`只能撤销在列级授予的权限。 不能向表授予表级特权,然后在列级为一个或多个列撤销此特权。 在这种情况下,`REVOKE`语句对已授予的权限没有影响。 ## CASCADE 或 RESTRICT IRIS支持可选的`CASCADE`和`ESTRICT关`键字来指定`REVOKE`对象特权行为。 如果没有指定关键字,则默认为`RESTRICT`。 可以使用`CASCADE`或`RESTRICT`来指定从一个用户撤销对象特权或列特权是否也会从通过`WITH GRANT OPTION`接收到该特权的任何其他用户撤销该特权。 `CASCADE`撤销所有这些关联的特权。 当检测到关联的特权时,`RESTRICT(默认值)`导致`REVOKE`失败。 相反,它设置`SQLCODE -126`错误`“REVOKE with RESTRICT failed”`。 下面的例子展示了这些关键字的使用: ```sql --UserA GRANT Select ON MyTable TO UserB WITH GRANT OPTION ``` ```sql --UserB GRANT Select ON MyTable TO UserC ``` ```sql --UserA REVOKE Select ON MyTable FROM UserB -- This REVOKE fails with SQLCODE -126 ``` ```sql --UserA REVOKE Select ON MyTable FROM UserB CASCADE -- This REVOKE succeeds -- It revokes this privilege from UserB and UserC ``` 注意,`CASCADE`和`RESTRICT`对`UserB`创建的引用`MyTable`的视图没有影响。 ## 对缓存查询的影响 当撤销特权或角色时, IRIS将更新系统上所有缓存的查询,以反映特权中的这一更改。 但是,当无法访问某个名称空间时——例如,当连接到数据库服务器的ECP连接关闭时——`REVOKE`会成功完成,但不会对该名称空间中的缓存查询执行任何操作。 这是因为`REVOKE`不能更新不可达名称空间中的缓存查询,以撤销缓存查询级别的特权。 没有发出错误。 如果数据库服务器稍后启动,则该名称空间中缓存查询的权限可能不正确。 如果某个角色或特权可能在某个名称空间不可访问时被撤销,建议清除该名称空间中的缓存查询。 ## IRIS Security REVOKE命令是一个特权操作。 在嵌入式SQL中使用`REVOKE`之前,必须以具有适当特权的用户身份登录。 如果不这样做,将导致`SQLCODE -99`错误(特权冲突)。 使用`$SYSTEM.Security.Login()`方法为用户分配适当的权限: ``` DO $SYSTEM.Security.Login("_SYSTEM","SYS") &sql( ) ``` 必须具有`%Service_Login:Use`权限才能调用`$SYSTEM.Security`。 登录方法。 # 示例 下面的嵌入式SQL示例创建两个用户,创建一个角色,并将角色分配给用户。 然后,它使用星号(`*`)语法从所有用户撤销该角色。 如果用户或角色已经存在,`CREATE`语句将发出`SQLCODE -118`错误。 如果用户不存在,`GRANT`或`REVOKE`语句将发出`SQLCODE -118`错误。 如果用户存在但角色不存在,则`GRANT`或`REVOKE`语句发出`SQLCODE 100`。 如果用户和角色存在,则`GRANT`或`REVOKE`语句发出`SQLCODE 0`。 即使已经完成了角色的授予或撤销,如果您试图撤销从未被授予的角色,也是如此。 ```java ClassMethod Revoke() { d $SYSTEM.Security.Login("_SYSTEM","SYS") &sql( CREATE USER User1 IDENTIFY BY fredpw ) &sql( CREATE USER User2 IDENTIFY BY barneypw ) w !,"CREATE USER error code: ",SQLCODE &sql( CREATE ROLE workerbee ) w !,"CREATE ROLE error code: ",SQLCODE &sql( GRANT workerbee TO User1,User2 ) w !,"GRANT role error code: ",SQLCODE &sql( REVOKE workerbee FROM * ) w !,"REVOKE role error code: ",SQLCODE } ``` 在下面的示例中,使用`AS`授予子句,一个用户(`Joe`)授予一个特权,另一个用户(`John`)撤销该特权: ```sql /* User Joe */ GRANT SELECT ON Sample.Person TO Michael ``` ```sql /* User John */ REVOKE SELECT ON Sample.Person FROM Michael AS Joe ``` 注意,`John`必须具有`%All`角色或`%Admin_Secure`资源。
文章
Nicky Zhu · 一月 6, 2023

《数据二十条》的号角声

国务院于2022年12月19日发布了《中共中央 国务院关于构建数据基础制度更好发挥数据要素作用的意见》(后简称《数据二十条》),如何有效利用数据已经成为下一步的趋势。另一方面,无论是基于数据中台还是数据编织理念,两者也都对如何利用数据提出了构想。因此医疗行业数字化建设的目标已不能再局限于如何收集数据,建立医疗行业数据的流通机制将会是为越来越普遍的需求。 时钟拨回几年前,数据中台概念开始火爆。人们对数据中台的定义、诠释尽管有诸多差异,通过数据中台降低数据共享和利用的成本则是共同的期望。但经过这几年的探索之后,中台已死的观点也在涌现。究其原因,除去中台概念在技术上的不确定,数据流通过程中的责权益的不清晰也是严重的制约因素。毕竟,数据中台自身作为一套技术框架并不能代替法律法规与市场自动将数据转变为商品从而创造出流通价值。 那么,如何能够使数据的流通合规合法,使数据能够如货币和商品一般自由流动,则是我们需要思考和探索的主题,这次《数据二十条》的出现,无疑为医疗信息技术工作者提供了一个明确的思考方向。 政策利好与约束 鉴于《数据二十条》对数据行业生态的覆盖范围之广,涉及数据权属界定、数据产品流通、数据收益分配和数据市场有效监管等各方面,本文将无法全面展开每一条政策进行解读和思考,因此将聚焦于与每个从业人员都息息相关的数据产权和数据产品流通两方面进行。 产权与使用权的破与立 还记得数年前与信息科同事谈及基于医疗数据的统计与分析时,医院的同事对于数据被第三方访问的恐惧远多于期待。对数据要素的权属及其确立规则的不清晰使得每个从业人员都无法在具备法律法规保障的前提下运用数据。本次《数据二十条》对于个人数据、企业数据和公共数据进行了产权定义,还提出了数据资源持有权、数据加工使用权、数据产品经营权等分置的产权运行机制,从而打破了这样无法可依的尴尬局面。 可以预见的是,通过对数据的产权与使用权进行分离,在取得数据所有者(如个人或企业)授权的前提下,对数据进行加工处理,通过数据洞察进行盈利将成为合理合法的业务形态。 数据供应链的建立 《数据二十条》第三章对数据供应链体系做了一系列的规划,包括数据流通过程中参与方的角色,如数据商和第三方专业服务机构;包括流通场所,如数据交易所以及对应的流程合规与监管规则体系的远景。这样一个体系的构建,其规模和复杂性并不亚于为汽车工业组织零部件生产和消费的供应链。 特别需要注意的是,正如《数据二十条》中明确指出的,数据供应链的建立必将依托数据质量标准化体系,推进对数据采集和接口的标准化,依赖于数据整合互通和互操作。 这些概念和体系对于医疗信息技术工作者来说并不陌生。然而在既往的工作中,跨企业、跨区域医疗行业数据共享的产业规模并未对标准化产生强劲的推力。尽管近年来随着互联互通标准化评测工作的开展,医疗信息互操作在标准化方面得到了极大的进展,但是医疗行业数据与上下游生态企业(如药企、保险、养老机构等)间进行数据流通所需的统一语义和标准还未确立和应用,势必将在不远的未来对医疗信息技术工作者提出更高的挑战。 另一方面,在鼓励数据交易所发挥作用的同时,《数据二十条》也倡导在数据流程合规与受规则体系监管的前提下,培育一批数据商和第三方专业服务机构,依法依规在场内和场外采取开放、共享、交换、交易等方式流通数据,也为创建数据供应、数据托管和数据服务代理等多种模式的数据经济形态创造了条件。 医疗行业数据流通案例 医疗数据产业并不是一个已经成熟的规模化产业,即使对于美国、英国这些在医疗信息化方面较早起步的国家,医疗数据产品和流通也仍然处于初步的市场探索阶段。我们可以看到一个案例。 Epic COSMOS数据集 美国最大的电子病历厂商Epic于2019年推出了数据集产品COSMOS(https://COSMOS.epic.com/)。所有Epic电子病历系统的用户都可以自愿与Epic签约成为COSMOS合作伙伴,在开放自己的医疗健康数据的同时共享同样加入了COSMOS网络其他用户的数据。时至今日,COSMOS已经收录了1亿6千7百万患者的数据,覆盖一千余家医院和两万余家诊所。 图 1 COSMOS数据流 如上图所示,Epic采用了非常传统的前置机+中心化存储方案构建。在置于院端的前置机中,以批量上传和事件触发上传两种方式加载数据集,在前置机一侧对数据进行标准化和匿名化,并通过HL7 CDA标准以文档的形式将数据传到数据中心。置于AWS云端的数据中心将负责对数据进行去重及合并。其中,数据在云端将以非结构化的Global形态存储于InterSystems的Caché中,并利用Caché自带的后结构化能力将非结构化的Global转换并存储为关系型数据对外提供SQL访问能力。 在这个过程中,COSMOS进行的若干细节处理非常值得即将面临数据开放的医疗信息技术工作者参考和借鉴。读者可参考相关论文查阅(如https://www.thieme-connect.com/products/ejournals/pdf/10.1055/s-0041-1731004.pdf)。 真实数据的可访问性:COSMOS本质上只解决了分散的,真实的医疗数据的可访问性问题,还没有运用任何颠覆性的BI、AI技术。作为美国最大的电子病历厂商,手握上亿人的医疗数据的Epic,需要从基础数据的准备切入市场,这从侧面反映了当前医疗行业所面临的客观现实,即供应链底层的数据原料并不存在稳定的供给,从而阻碍了其他技术的演进。这同样是我们面临的现状。 非常传统的数据采集:COSMOS只采集EHR中的结构化数据,并不收纳任何影像、视频和除实验室检测结果外的文本等多媒体数据,也未采用实时数据流进行采集。这并不意味着多媒体数据没有价值,也不意味着实时数据流没有价值,而是意味着半静态的,结构化的数据中的价值并没有得以充分提炼和发挥,仅通过收集整理结构化数据形成规模一项工作已足以支撑起庞大的价值链条,尽管这些静态数据并不是唯一的价值来源。 去识别化与个人数据授权:作为对患者信息进行隐私保护的首要手段,COSMOS及与之相似的数据集产品Cerner Real-World Data(CRWD)均遵循美国自1996年通过的HIPPA法案,只开放法案允许开放的数据集,并按照HIPPA的要求对可能暴露患者隐私或反向识别个人的数据字段进行匿名化处理。需要注意的是,尽管CRWD相关的论文中声明,由于对数据进行了匿名化处理,对个人医疗数据的使用不需要患者本人的授权(https://www.sciencedirect.com/science/article/pii/S2352340922003304),但COSMOS仍然提供了供患者撤回数据授权,将本人的数据从COSMOS网络中退出的工作流。因此,即使在美国,数据所有者和数据使用者之间的权益平衡仍保留了相当的灵活性,我国在制定相关法规时也会对基于所有权和使用权定义相应的细则。 数据访问控制:在前置机与云端数据中心通信过程中,CDA文档将被加密并通过专网传输,避免在公网传输并被截获和解析的可能。另一方面,尽管COSMOS收集了诊断、药嘱、手术史、社会史和家族史等患者个人的明细数据,但它并不对最终用户开放这些数据;COSMOS用户可以通过查询门户,制定条件,查询基于这些明细的统计数据,例如在一定行政区域内罹患新冠的患者数量及其年龄分布等,但无法查询到对应的个人,因此经过认证的科研机构在COSMOS中发起查询并不再需要特定的审查委员会审核;同时COSMOS也不提供将数据从COSMOS网络中导出的渠道,避免数据流出网络。从这些控制手段上来看,COSMOS选择的技术路线和服务模式与《新二十条》中“原始数据不出域、数据可用不可见”的要求和“以模型、核验等产品和服务等形式向社会提供”的倡导高度吻合,值得借鉴。 数据标准化:COSMOS在前置机上收集一家医院的数据时已落实了数据的标准化,采用固定的数据结构和术语集。医院需要先完成对数据和术语的标准化映射,才能接入COSMOS网络。而在云端存储中,原始数据也是以标准化的CDA文档形式保存,进一步巩固了数据标准。也正是在标准化数据存储的基础上,最终用户才能够通过统一的查询构建器,在同一种语义环境下同时访问来自于不同医院,采用了不同术语标准的医疗数据。因此通过数据标准术语标准达成语义一致性的重要性不言而喻,这是医疗数据的利用迅速得到规模效应的客观需求。 医疗数据产品发展前景 如前所述,基于数据所有权与数据使用权分离的假设,很难想象未来医疗数据产品的发展方向以生数据产品的形态,开放对个人数据的(即使经过了匿名化)访问。相反的,基于医疗数据需求的多样性以及个人、企业、公共数据管理规则的差异性,以生数据为基础,以对药企、保险等企业提供潜在可招募患者的区域锁定或针对患者的年龄、诊断、家族史的普遍特征与医疗支出进行精算为例,针对人群展开的数据洞察和数据分析服务,更可能得到业界的认可并在数据价值利用和数据隐私保护间取得平衡,有很大概率成为率先得以实现的商业模式。同时,作为一个新兴产业,生产者(数据工程师)群体的培养和储备,以及与之配套的生产资料的制造和积累,则是医疗数据产业能够成型的前提,值得医疗信息技术工作者关注和投入。 因此,在未来相当长的一段时间内,对医疗行业数据的利用,将以各医院、集团和企业建立的数据中心为基础,通过对真实数据进行洞察分析和价值挖掘的形态,以数据服务的形式对外提供,从而迅速释放这些被积累了很久的数据的价值。 后续我们还会继续阐述和分析在医疗数据流通领域中的生产者和生产资料的特征,欢迎大家与我们交流,谢谢。