搜索​​​​

清除过滤器
文章
姚 鑫 · 八月 9, 2021

方法关键字SoapRequestMessage,SoapTypeNameSpace,SqlName,SqlProc

# 第八十一章 方法关键字 - SoapRequestMessage 当多个`web方法`具有相同的`SoapAction`时使用此方法。 在默认场景中,该关键字指定请求消息的`SOAP`正文中的顶级元素的名称。 仅适用于定义为`web服务`或`web客户端`的类。 # 用法 要在请求消息的`SOAP`体中指定顶级元素的名称,请使用以下语法: ```java Method name(formal_spec) As returnclass [ WebMethod, SoapAction = "MyAct", SoapRequestMessage="MyReqMessage" ] { //implementation } ``` 其中`soaprequestmessage`是有效的XML标识符。 # 详解 注意:此关键字仅对包装的文档/文字`document/literal`消息有效。 对于包装的文档/文字消息,该关键字指定请求消息的`SOAP`主体中的顶部元素的名称。(默认情况下,包装文档/文字消息。 如果对同一`web服务`中的多个`web方法`使用相同的`SoapAction`值,请指定此关键字。否则,一般不需要这个关键字。 # 与WSDL的关系 `SoapRequestMessage`关键字影响`web服务`的`WSDL`的``部分。例如,考虑以下web方法: ```java Method Add(a as %Numeric,b as %Numeric) As %Numeric [ SoapAction = MyAct,SoapRequestMessage=MyReqMessage, WebMethod ] { Quit a + b } ``` 对于这个web服务,WSDL包含以下内容: ```xml ``` 这些元素在``部分中相应地定义。 默认情况下,如果方法没有指定`SoapRequestMessage`关键字,``部分将改为如下所示: ```xml ``` 如果使用`SOAP`向导从`WSDL` IRIS `web服务`或客户端, IRIS将此关键字设置为适合该WSDL的。 # 对Message的影响 对于前面显示的`web方法`,`web服务`需要以下形式的请求消息: ```xml 12 ``` 相反,如果该方法没有指定`SoapRequestMessage`关键字,则该消息将如下所示: ```xml 12 ``` # 第八十二章 方法关键字 - SoapTypeNameSpace 为此`web方法`使用的类型指定`XML`命名空间。仅适用于定义为`web服务`或`web客户端`的类。 # 用法 若要重写类型的默认`XML`命名空间(当该方法用作web方法时),请使用以下语法: ```java Method name(formal_spec) As returnclass [ SoapTypeNameSpace = "soapnamespace", SoapBindingStyle = document, WebMethod ] { //implementation } ``` 其中`soapnamespace`是命名空间`URI`。请注意,如果`URI`包含冒号(`:`),则该字符串必须加引号。也就是说,可以使用以下内容: ```java Method MyMethod() [ SoapTypeNameSpace = "http://www.mynamespace.org", SoapBindingStyle = document, WebMethod ] ``` 或以下内容: ```java Method MyMethod() [ SoapTypeNameSpace = othervalue, SoapBindingStyle = document, WebMethod ] ``` 但不包括以下内容: ```java Method MyMethod() [ SoapTypeNameSpace = http://www.mynamespace.org, SoapBindingStyle = document, WebMethod ] ``` 重要提示:对于手动创建的`web服务`,该关键字的默认值通常是合适的。当使用SOAP向导从`WSDL`生成`web客户端`或服务时,InterSystems IRIS会将该关键字设置为适合该`WSDL`;如果修改该值,`web客户端`或服务可能不再工作。 # 详解 此关键字指定此`web方法`使用的类型的XML命名空间。 注意:只有当方法使用文档样式绑定时,此关键字才有作用。也就是说,方法(或包含它的类)必须用等于`document`的`SoapBindingStyle`标记。(对于使用`rpc-style`绑定的方法,指定这个关键字是没有意义的。) # 默认 如果省略此关键字,则此方法的类型位于由`web服务`或`客户端`类的`TYPENAMESPACE`参数指定的命名空间中。如果未指定`TYPENAMESPACE`,则类型将位于由`web服务`或客户端的`are`参数指定的命名空间中。 # 与WSDL的关系 `SoapTypeNameSpace`关键字影响`WSDL`的以下部分: ``元素中的命名空间声明。指定的命名空间(例如,`http://www.customtypes.org`)将添加到这里。例如: ```xml ... xmlns:ns2="http://www.customtypes.org" xmlns:s0="http://www.wbns.org" xmlns:s1="http://webservicetypesns.org" ... targetNamespace="http://www.wbns.org" ``` 在本例中,`http://www.customtypes.org`命名空间被分配给前缀`ns2`。 请注意,`WSDL`还像往常一样声明了以下名称空间: - `Web服务`的命名空间(`http://www.wsns.org`),在本例中,它被分配给前缀`s0`,也用作`Web服务`的目标命名空间。 - 网络服务的类型命名空间`http://www.webservicetypesns.org`),在本例中它被分配给`前缀s1`。 如果在`web服务`类中没有指定类型命名空间,则该命名空间不包含在`WSDL`中。 - ``元素,它包含一个``元素,该元素的`targetNamespace`属性等于为`SoapTypeNameSpace`指定的命名空间: ```xml ... ... ``` 相反,如果没有指定`SoapTypeNameSpace`,那么`WSDL`的这一部分将如下所示。请注意,``元素的`targetNamespace`是`web服务`类型的命名空间: ```xml ... ... ``` (此外,如果在`web服务类`中没有指定类型命名空间,则`targetNamespace`将改为`web服务`的命名空间。) # 对消息的影响 `SOAP`消息可能如下所示(为了可读性,添加了换行符和空格): ```xml 3 ``` 请注意,``元素位于`“http://www.customtypes.org”`命名空间中。 相反,如果没有指定`SoapTypeNameSpace`关键字,则消息可以如下所示: ```xml 3 ``` # 第八十三章 方法关键字 - SqlName 覆盖投影`SQL`存储过程的默认名称。 仅当此方法被投影为`SQL`存储过程时应用。 # 用法 要覆盖方法投射为`SQL`存储过程时使用的默认名称,请使用以下语法: ```java ClassMethod name(formal_spec) As returnclass [ SqlProc, SqlName = sqlname ] { //implementation } ``` 其中`sqlname`是`SQL`标识符。 # 详解 如果将此方法投影为`SQL`存储过程,则使用此名称作为存储过程的名称。 # 默认 如果忽略这个关键字, IRIS确定`SQL`名称如下: ```java CLASSNAME_METHODNAME ``` 默认使用大写字母。 但是,在调用存储过程时可以使用任何情况,因为SQL是不区分大小写的。 因此,在下面的示例中,默认的`SQL name`值是`TEST1_PROC1`。 这个默认值是在`SELECT`语句中指定的: ```java Class User.Test1 Extends %Persistent { ClassMethod Proc1(BO,SUM) As %INTEGER [ SqlProc ] { ///definition not shown } Query Q1(KD As %String,P1 As %String,P2 As %String) As %SqlQuery { SELECT SUM(SQLUser.TEST1_PROC1(1,2)) AS Sumd FROM SQLUser.Test1 } } ``` # 第八十四章 方法关键字 - SqlProc 指定是否可以作为`SQL`存储过程调用该方法。 只有类方法(而不是实例方法)可以作为SQL存储过程调用。 # 用法 要指定该方法可以作为`SQL`存储过程调用,请使用以下语法: ```java ClassMethod name(formal_spec) As returnclass [ SqlProc ] { //implementation } ``` 否则,忽略该关键字或将`Not`放在该关键字之前。 # 详解 该关键字指定可以作为`SQL`存储过程调用该方法。 只有类方法(而不是实例方法)可以作为`SQL`存储过程调用。 存储过程由子类继承。 # 默认 如果忽略此关键字,则该方法作为`SQL`存储过程不可用。
文章
姚 鑫 · 五月 15, 2021

第一章 单元测试概述

# 第一章 单元测试概述 本教程的第一部分概述了单元测试。完成本教程的这一部分后,将能够: - 定义单元测试并区分单元测试和集成测试 - 列出单元测试的几个好处 - 描述InterSystems IRIS `%UnitTest`包和`xUnit`测试框架之间的相似性。 - 列出软件开发中测试优先方法经常声称的几个好处。 # 什么是单元测试? 单元测试是对单个代码模块的正确性的测试,例如,方法或类的测试。通常,开发人员在开发代码时为其代码创建单元测试。典型的单元测试是一种执行方法的方法,该方法测试并验证该方法是否为给定的一组输入生成了正确的输出。 单元测试不同于集成测试。集成测试验证了一组代码模块交互的正确性。单元测试仅单独验证代码模块的正确性。一组代码模块的集成测试可能会失败,即使每个模块都通过了单元测试。 # 为什么要进行单元测试? 单元测试提供了许多好处,包括: - 提供代码模块是否正确的验证。这是单元测试的主要原因。 - 提供自动回归测试。更改代码模块后,应重新运行单元测试,以确保代码模块仍然正确。也就是说,应该使用单元测试来确保更改没有破坏代码模块。理想情况下,所有代码模块的单元测试都应该在更改任何一个模块之后运行。 - 提供文档。通常,代码模块的单元测试与代码模块一起交付。检查单元测试提供了大量有关代码模块如何工作的信息。 # XUnit测试框架 单元测试框架是为开发和执行单元测试提供支持的类包。它们可以很容易地扩展以支持更具体或专门化类型的单元测试。 XUnit系列测试框架基于原始的`Sunit`框架(用于单元测试`SmallTalk`代码),包括以下框架: - `JUnit-Java`代码的单元测试框架。 - `NUnit-C#`、`VB.NET`和其他`.NET`语言代码的单元测试框架。 - `CppUnit-C++`代码的单元测试框架。 - `PyUnit-Python`代码的单元测试框架。 # %UnitTest和xUnit框架的结构 `%UnitTest`包和`xUnit`框架共享相同的基本结构。熟悉任何`Unit`框架的开发人员都可以毫不费力地学习使用`%UnitTest`包。`%UnitTest`和`xUnit`框架都围绕以下基本测试结构组织: - 测试装置-为一个测试或一组测试做准备和清理工作的代码。准备测试可能包括创建数据库连接,或使用测试数据初始化数据库。清理可能包括关闭数据库连接或恢复数据库状态。 - 测试用例-测试的最小单元。验证特定的一组输入是否会产生给定模块的特定输出。 - 测试套件-设计为一起执行的测试和测试套件的集合。 - Test Runner-用于执行测试并显示其结果的实用程序。 # 测试自动化 `%UnitTest`包和`xUnit`框架都支持测试自动化。当单元测试完成执行时,它会报告测试是通过还是失败。不需要解释测试结果。这是非常重要的。可以为每个代码更改执行大量单元测试。如果必须不断地阅读和解释结果,这个过程很快就会变得非常乏味和容易出错。 许多`xUnit`框架提供了汇总测试结果的图形用户界面(GUI)。`%UnitTest`会生成一个显示测试结果的网页。它以绿色显示有关通过的测试的信息,以红色显示有关失败的测试的信息。开发人员可以一目了然地判断是否有任何测试失败。 这是由`%UnitTest`单元测试生成的测试报告。用户可以通过单击页面上的超链接深入查看提供有关测试的更多详细信息的页面。 ![image](6F95C9D03A024D2CB690E5213CF37A4B) # 测试优先方法论 敏捷软件方法论,例如测试驱动开发(TDD)和极限编程,特别强调单元测试。事实上,这些方法使用单元测试来驱动开发过程。他们提倡“测试优先”的软件开发方法。在这种方法中,开发人员在编写代码模块的一行代码之前设计并编写代码模块的单元测试。然后,开发人员创建代码模块,目标是通过单元测试。 `Test First`方法的倡导者声称该方法具有以下好处: - 它迫使开发人员在开发任何模块之前很久就决定代码模块的正确输入和输出。 - 它集中了开发人员在创建代码模块时的注意力。开发人员关注的是在创建模块时通过单元测试的具体目标。 - 它可以防止单元测试成为事后的想法。如果首先创建单元测试,则在项目结束之前不能忽略单元测试。 - 它确保了代码的高度测试覆盖率。 注意:测试优先开发的支持者通常主张在代码模块之前执行单元测试,而不仅仅是创建单元测试。当然,在这一点上测试应该会失败。他们甚至可能不会编译。 # Red – Green – Refactor `XUnit`和`%UnitTest`测试报告GUI报告以绿色表示通过测试,以红色表示未通过测试。下面是使用测试优先开发方法的开发节奏: 1. 红色 - 编写一个不起作用的小测试,也许一开始不会编译。 2. 绿色 - 让测试快速运行,在测试过程中犯下所有必要的错误。 3. 重构 - 消除仅在使测试正常工作时产生的所有重复。 > Kent Beck,《测试驱动的设计》
文章
姚 鑫 · 六月 15, 2021

第七章 控制命名空间分配的外观

# 第七章 控制命名空间分配的外观 # 控制命名空间分配的外观 除了控制命名空间分配外,还可以控制命名空间分配在XML输出中的显示方式。具体地说,可以控制以下内容: ## 显式名称空间分配与隐式名称空间分配 将元素和属性分配给命名空间时,XML中有两种等效的表示形式,由编写器实例的`SuppressXmlns`属性控制。 为一个名为`Person`的对象生成XML输出,该对象被分配给名称空间`“http://www.person.org”`(通过前面讨论的`namespace`类参数)。 使用缺省输出(`SuppressXmlns`等于0)的示例如下: ```java Uberoth,Amanda Q. 1952-01-13 ``` 另一种可能的形式完全相同,如下所示。 这是使用`SuppressXmlns`等于1生成的,它确保显式分配给名称空间的每个元素都显示为该名称空间的前缀。 ```java Uberoth,Amanda Q. 1952-01-13 ``` 请注意,此属性仅影响命名空间分配的显示方式;它不控制如何分配任何命名空间。如果不使用命名空间,则此参数无效。 ## 为命名空间指定自定义前缀 当为对象生成XML输出时,系统会根据需要生成命名空间前缀。第一个名称空间前缀是`s01`,下一个是`s02`,依此类推。可以指定不同的前缀。为此,请在启用XML的对象本身的类定义中设置`XMLPREFIX`参数。此参数有两个效果: - 它确保在XML输出中声明指定的前缀。也就是说,即使没有必要这样做,它也会被声明。 - 它使用该前缀,而不是在其他情况下会看到的自动生成的前缀。 # 控制空字符串(`""`)的导出方式 为对象启用XML时,需要指定将空值和空字符串投影到XML的方式 其中一个选项是在支持xml的类中将`XMLIGNORENULL`设置为`“RUNTIME”`(不区分大小写)。 在这种情况下,当使用%XML.Write的`RuntimeIgnoreNull`属性的值来确定如何处理任何等于`""`的属性,如下所示: - **如果编写器的`RuntimeIgnoreNull`属性为0(默认值),则`XMLNIL`参数控制如何导出该属性。`XMLNIL`是一个类参数和一个属性参数;属性参数优先。** - 如果`XMLNIL`为0(默认值),则不投影特性。也就是说,它不包含在XML文档中。 - 如果`XMLNIL`为1,并且该属性用作元素,则该属性将按如下方式导出: ``` ``` - 如果`XMLNIL`为1并且特性用作属性,则不会输出特性。 - 如果编写器的`RuntimeIgnoreNull`属性为1,则该属性将导出为空元素或空属性(其导出方式与值`$char(0)`相同,后者始终导出为空元素或空导出)。 除非`XMLIGNORENULL`在启用xml的类中是`“RUNTIME”`,否则编写器的`RuntimeIgnoreNull`属性是无效的。 ## 示例:`RuntimeIgnoreNull`为0(默认值) ```java Class EmptyStrings.Export Extends (%Persistent, %XML.Adaptor) { Parameter XMLNAME="Test"; Parameter XMLIGNORENULL = "RUNTIME"; ///把这个作为一个元素 ///XMLNIL is 0, the default Property Property1 As %String; ///把这个作为一个元素 ///XMLNIL is 0, the default Property Property2 As %String(XMLPROJECTION = "ATTRIBUTE"); ///将其作为XMLNIL=1的元素 Property Property3 As %String(XMLNIL = 1); ///将其作为XMLNIL=1的属性进行项目 Property Property4 As %String(XMLNIL=1,XMLPROJECTION="ATTRIBUTE"); } ``` 如果创建了这个类的一个新实例(并且没有设置任何属性的值),然后使用 `%XML.Writer`输出,如下所示: ```java ``` ## 例如:`RuntimeIgnoreNull`为1 ```java ``` **在本例中,因为`RuntimeIgnoreNull`为1,所以没有使用`XMLNIL`参数。 相反,`""`被导出为空属性或空元素。** # 导出类型信息 默认情况下,XML编写器不写入类型信息。有两个选项可用于在输出中包括类型信息: - 编写器的`OutputTypeAttribute`属性。如果此属性为1,则编写器包括其写入的对象内所有元素的XML类型信息(但不包括对象本身)。例如: ```java Petersburg,Greta U. 1949-05-15 ``` 请注意,相应的命名空间将添加到XML文档的根。 - `Object()`和`RootObject()`方法的`className`参数。此参数用于指定对象的预期`ObjectScript`类型(类名)。 如果参数与实际类型相同,则编写器不包括对象的类型信息。 如果参数与实际类型不同,编写器将包括对象的实际XML类型(默认为类名)。例如,假设`Test2.PersonWithAddress`实例编写输出,并将`className`参数指定为`MyPackage.MyClass`。因为`MyPackage.MyClass`与实际的类名不同,所以编写器生成以下输出: ```java Avery,Robert H. Ukiah 82281 ``` # 生成SOAP编码的XML 对于`%XML.Writer`,`Format`属性控制输出的整体格式。这是以下选项之一: - `“literal”`,即默认值,在本书的大多数例子中都使用了它。 - `“encoded”`,按照SOAP 1.1标准中的描述进行编码。 - `“encoded12”`,按照SOAP 1.2标准中的描述进行编码。 ## 创建内联引用 在编码格式中,任何对象值属性都被作为引用包含,被引用的对象被导出为单独的元素。 要将这些属性内联导出,而不是作为单独的元素,请将`ReferencesInline`属性设置为1。 如果格式是`“literal”`,`ReferencesInline`属性没有效果。 # 导出后控制unswizling 当导出一个支持xml的持久对象时,系统会像往常一样自动将所有需要的信息混合到内存中;该信息包括对象值属性。导出对象后,InterSystems IRIS将消除任何对象列表,但(默认情况下)不会消除单个对象引用。 对于大对象,这可能导致``错误。 在这种情况下,要使任何单个对象引用不被混合,请在支持xml的类中设置`XMLUNSWIZZLE`参数,如下所示: ```java Parameter XMLUNSWIZZLE = 1; ``` 该参数默认值为0。 # 控制元素的关闭 只包含属性的元素可以用以下任一方式表示: ```java ``` `Object()`方法始终使用第一个语法导出元素。如果需要使用此处显示的第二种语法关闭元素,请手动编写对象,如本章前面的“手动构造元素”中所述。 咨询一下,我也是初学,有没有类似于oracle的exp远程备份数据库的方法?
文章
姚 鑫 · 六月 14, 2022

第九章 其他参考资料(二)

# 第九章 其他参考资料(二) # 特殊变量 (SQL) 系统提供的变量。 ```sql $HOROLOG $JOB $NAMESPACE $TLEVEL $USERNAME $ZHOROLOG $ZJOB $ZPI $ZTIMESTAMP $ZTIMEZONE $ZVERSION ``` SQL直接支持许多对象脚本特殊变量。这些变量包含系统提供的值。只要可以在SQL中指定文字值,就可以使用它们。 SQL特殊变量名不区分大小写。大多数可以使用缩写来指定。 Variable| Name| Abbreviation| Data Type Returned Use ---|---|---|--- $HOROLOG| $H| %String/VARCHAR |当前进程的本地日期和时间 $JOB| $J| %String/VARCHAR |当前进程的 job ID $NAMESPACE| none| %String/VARCHAR |当前命名空间名称 $TLEVEL| $TL| %Integer/INTEGER|| 当前事务嵌套级别 $USERNAME| none| %String/VARCHAR|当前进程的用户名 $ZHOROLOG| $ZH |%Numeric/NUMERIC(21,6)|自InterSystems IRIS启动后经过的秒数 $ZJOB| $ZJ| %Integer/INTEGER|当前进程的job状态 $ZPI| none| %Numeric/NUMERIC(21,18) |数值常量PI $ZTIMESTAMP |$ZTS| %String/VARCHAR |协调世界时间格式的当前日期和时间 $ZTIMEZONE| $ZTZ| %Integer/INTEGER| 当地时区与GMT的偏移量 $ZVERSION| $ZV| %String/VARCHAR| IRIS的当前版本 # 示例 ```sql SELECT TOP 5 Name,$H FROM Sample.Person ``` 以下示例仅在时区位于大陆内时才返回结果集: ```sql SELECT TOP 5 Name,Home_State FROM Sample.Person WHERE $ZTIMEZONE BETWEEN -480 AND 480 ``` # 字符串操作(SQL) 字符串操作函数和运算符。 SQL 支持多种类型的字符串操作: - 字符串可以通过长度、字符位置或子字符串值进行操作。 - 字符串可以通过指定的分隔符或分隔符字符串来操作。 - 字符串可以通过模式匹配和单词感知搜索来测试。 - 特殊编码的字符串(称为列表)包含嵌入的子字符串标识符,而不使用分隔符。各种 `$LIST` 函数对这些与标准字符串不兼容的编码字符串进行操作。唯一的例外是 `$LISTGET` 函数和 `$LIST` 的单参数和双参数形式,它们将编码字符串作为输入,但将单个元素值作为标准字符串输出。 SQL 支持字符串函数、字符串条件表达式和字符串运算符。 ObjectScript 字符串操作区分大小写。字符串中的字母可以转换为大写、小写或混合大小写。字符串排序规则可以区分大小写,也可以不区分大小写;默认情况下,SQL 字符串排序规则是不区分大小写的 `SQLUPPER`。 SQL 提供了许多字母大小写和排序规则函数和运算符。 当为数字参数指定字符串时,大多数 SQL 函数执行以下字符串到数字的转换: 非数字字符串转换为数字 0;将数字字符串转换为规范数字;并且混合数字字符串在第一个非数字字符处被截断,然后转换为规范数字。 # 字符串连接 以下函数将子字符串连接成字符串: - `CONCAT`:连接两个子字符串,返回一个字符串。 - `STRING`:连接两个或多个子字符串,返回单个字符串。 - `XMLAGG`:连接列的所有值,返回单个字符串。 - `LIST`:连接列的所有值,包括逗号分隔符,返回单个字符串。 - 连接运算符 (`||`) 也可用于连接两个字符串。 # 字符串长度 以下函数可用于确定字符串的长度: - `CHARACTER_LENGTH` 和 `CHAR_LENGTH`:返回字符串中的字符数,包括尾随空格。 `NULL` 返回 `NULL`。 - `LENGTH`:返回字符串中的字符数,不包括尾随空格。 `NULL` 返回 NULL。 - `$LENGTH`:返回字符串中的字符数,包括尾随空格。 `NULL` 返回为 0。 # Truncation and Trim 以下函数可用于截断或修剪字符串。截断限制字符串的长度,删除超出指定长度的所有字符。`Trim`从字符串中删除前导和/或尾随空格。 - `Truncation`: `CONVERT`, `%SQLSTRING`, and `%SQLUPPER`. - `Trimming`: `TRIM`, `LTRIM`, and `RTRIM`. # 子串搜索 以下函数在字符串中搜索子字符串并返回字符串位置: - `POSITION`:按子字符串值搜索,找到第一个匹配项,返回子字符串开始的位置。 - `CHARINDEX`:按子字符串值搜索,找到第一个匹配项,返回子字符串开始的位置。可以指定起点。 - `$FIND`:按子串值搜索,找到第一个匹配项,返回子串结束的位置。可以指定起点。 - `INSTR`:按子字符串值搜索,找到第一个匹配项,返回子字符串开始的位置。可以指定起点和子串出现。 以下函数在字符串中按位置或分隔符搜索子字符串并返回子字符串: - `$EXTRACT`:按字符串位置搜索,返回由开始位置或开始和结束位置指定的子字符串。从字符串的开头搜索。 - `SUBSTRING`:按字符串位置搜索,返回由开始位置或开始和长度指定的子字符串。从字符串的开头搜索。 - `SUBSTR`:按字符串位置搜索,返回由起始位置或起始和长度指定的子字符串。从字符串的开头或结尾搜索。 - `$PIECE`:按分隔符搜索,返回第一个分隔的子字符串。可以指定起点或默认为字符串的开头。 - `$LENGTH`:按分隔符搜索,返回分隔子串的数量。从字符串的开头搜索。 - `$LIST`:在特殊编码的列表字符串上按子字符串计数搜索。它通过子串计数定位子串并返回子串值。从字符串的开头搜索。 - 包含运算符 (`[`) 也可用于确定子字符串是否出现在字符串中。 - `%STARTSWITH` 比较运算符将指定的字符与字符串的开头进行匹配。 # 子串搜索和替换 以下函数在字符串中搜索子字符串并将其替换为另一个子字符串。 - `REPLACE`:按字符串值搜索,用新的子字符串替换子字符串。从字符串的开头搜索。 - `STUFF`:按字符串位置和长度搜索,用新的子字符串替换子字符串。从字符串的开头搜索。 # 字符类型和 Word-Aware 比较 `%PATTERN` 比较运算符将字符串与指定的字符类型模式匹配。
文章
姚 鑫 · 八月 15, 2022

第二章 使用管理门户(二)

# 第二章 使用管理门户(二) # 管理门户概述 本节介绍管理门户页面的一些常见布局元素。 注意:在管理门户中的任何位置,将光标移到菜单项上都会显示该项目的描述。 ## 管理门户主页 管理门户主页的标题是 `Welcome, `。在标题旁边,功能区包含以下选项: - 两个视图按钮,可让指定如何在菜单列中显示链接。 - 搜索栏,位于功能区的右侧。当指定一个词并按 Enter 键时,将显示包含该词的所有页面的列表;然后,可以单击要显示的目标页面,而无需浏览子菜单。 以下部分描述了主页的区域: ### 管理门户菜单栏 位于主页左边缘的菜单栏是导航门户的主要方法。 ### 管理门户欢迎窗格 欢迎窗格位于主页的中心,包括经常访问的页面的快捷方式。它包含以下字段: - 收藏夹`Favorites` — 列出选择为收藏夹的管理门户页面(请参阅操作窗格);可以单击每个页面标题直接转到该页面。 - 最近`Recent` — 列出自上次启动 IRIS 以来最近显示的页面。 - `Did you know?` — 显示提示。 - 链接 `Links` - 指向可能想要访问的页面的链接。 ### 管理门户消息窗格 位于主页右侧边缘的消息窗格显示一般系统信息并提供指向系统仪表板的链接。 如果实例是镜像成员,则消息窗格还显示它所属的镜像、其状态和成员类型以及指向镜像监视器的链接。 ## 管理门户标题 页眉位于管理门户中每个页面的顶部,可用于快速导航门户。 标题包含以下链接: - 主页`Home` — 显示管理门户主页。 - 关于`About` — 显示系统概览信息。 - 帮助`Help` — 显示正在查看的页面/主题的在线文档(帮助)。 - 联系方式`Contact` — 显示全球响应中心 (WRC) 的联系方式页面。 - 注销`Logout` — 注销您并带您进入管理门户的登录页面。 - 菜单`Menu` — 根据用户担任的角色显示常见任务列表。 标头还包含有用的信息,例如: - 服务器`Server` — 运行 IRIS 的服务器的名称。 - 命名空间`Namespace` — 当前使用的命名空间的名称。要在不同的命名空间中工作,请单击切换并选择所需的命名空间。 - 用户`User` — 登录到管理门户的用户的名称。要更改用户的密码,请单击名称。 - 许可证`Licensed to` — 出现在许可证密钥信息中的客户名称。 - 实例`Instance` — 服务器上运行的 `IRIS` 实例的名称。 此外,可以显示系统模式标签(例如,测试系统); 管理门户标题的左侧显示正在使用的产品的名称。 ## 管理门户功能区 功能区位于标题正下方,并显示特定于每个页面的不同内容。例如,数据库页面(`System Explorer` > `Databases`)的功能区如下图所示: 功能区的典型内容是: - 正在显示的管理门户页面的标题。 - 当前页面的面包屑,直接列在页面标题上方。路径中列出的每个页面都是一个活动链接,可以使用它返回到先前显示的子菜单/列表。当在页面上进行未保存的更改时,会在面包屑中附加一个星号,例如系统 > 配置 >内存和启动 —(配置设置)*。在离开未保存的更改之前,系统始终会提示进行确认。 注意:页签不会列出路径中的每个页面,并且页签中的页面并不总是与导航菜单中的页面匹配。始终可以通过单击主页返回到管理门户主页并使用搜索工具导航到特定页面,本节稍后将对此进行介绍。 - 允许在页面上执行操作的几个按钮。例如,`Databases` 页面包含按钮 `Integrity Check` 和 `Integrity Log`。 - 刷新按钮,包含有关页面上次更新时间的信息。 ## 系统概述信息 单击管理门户标题上的关于时,将显示一个表格,其中包含以下信息: - 版本 — 此 `IRIS` 实例的特定构建信息,包括平台、构建号和构建日期。 - 配置 - 此实例正在使用的配置 (`.cpf`) 文件的名称和位置。 - 数据库缓存 (`MB`) — 为数据库分配的空间例程缓存 (`MB`) — 为例程分配的空间。 - 日志文件 - 当前日志文件的名称和位置。 - `SuperServer` 端口 — 运行 `IRIS` 服务器的端口号。 - `Web`服务器端口 — 运行私有 `IRIS Web` 服务器的端口号。 - 许可证服务器地址/端口 — `IRIS` 许可证服务器的 `IP` 地址和运行它的端口号。 - 许可给 — 出现在许可密钥信息中的客户名称。 - 集群支持 - 指示此实例是否是集群的一部分。 - 镜像 — 指示此实例是否是镜像的成员。 - `Time System Started` — 上次启动 `InterSystems IRIS` 实例的日期和时间。 - 加密密钥标识符 — 如果激活加密,则为加密密钥的 `GUID`(全局唯一 `ID`)。 - `NLS` 区域设置 — 国家语言支持区域设置。 - 此会话的首选语言 - 管理门户已本地化并可显示的语言的下拉列表。可以通过从下拉列表中选择新的语言来更改显示语言。最初,浏览会话的首选语言是为浏览器指定的语言,如果不支持浏览器语言,则为英语;在特定浏览器中选择首选语言后,即使更改了浏览器语言,该浏览器中的管理门户也会使用该语言。
文章
王喆 👀 · 九月 13, 2022

IRIS快速查询服务思路分享

背景 作为集成平台厂商,在医院同其它系统联调的时候所做的事情中,多的不是开发代码而是查消息。我先演示一下目前我正在使用的IRIS查消息的方式: 例子1: 需要看【个人信息注册服务】 我只需要在框中输入【个人信息注册】回车 点击【查看消息】,显示的是消息里面的内容,如图所示: 点击【查看流程】,显示的是IRIS消息可视化的页面,如图所示: 例子2: 需要查询患者ID为【2874621】这患者在集成平台调用了哪些服务 我只需要选择下拉选择【患者ID】,然后输入【2874621】,回车 这个是我们以用户需求的角度直观的查询到指定的消息,IRIS也有这个功能—消息查看器,它是如何使用的呢?首先,我们得知道这条消息使用了哪些Production组件,其次我们需要了解这个消息使用的实体类的结构。比如同样查这个人的消息,我需要输入request.Body.PatientID=“2874621”,选择消息类。如果我需要查多个服务的我还需要多选消息类 …… 本文不是来介绍如何使用消息查看器的,各位大概知道就好。 程序分析与设计思路 原始消息查看器的使用 我们先使用IRIS自带的【消息查看器】查患者ID为【2874621】个人信息注册服务,如图所示: 选时间,输入图中所示的条件去检索。步骤上好像也挺简单的,但是这里有两个前提条件,一是我得知道每个服务对应的消息类是哪个。二是我知道这个消息类里面患者ID对应的字段是哪个,而且这个字段不能在循环里面(好像说了3个条件 ~ 哈哈),如何处理摆在了我们的眼前。 SQL分析 使用IRIS自带的【显示查询】功能(这个如何开启使用可以看我的另外一篇文章 https://cn.community.intersystems.com/node/525741 ),如图所示: 找个位置格式化一下: 可以看到从这段SQL可以看出3点: (1)IRIS消息记录的表是【Ens.MessageHeader】 (2)IRIS保存消息是给实体本身建立了一个表 比如上述的例子: 【BKIP_PatientInfo_MSG.PatientInfoRegister】 (3)head.MessageBodyId = BKIP_PatientInfo_MSG.PatientInfoRegister.%ID 通过这3条,我们可以得到下面这张图的信息: 倘若我们把SQL按照如上图所示的方式处理,然后反应在页面上供用户选择;患者ID也由用户选择或者输入;这样确实直观的解决了。但是,效率,上述消息查看器在用户选择时间的时候首先查到两个ID让查询就在这两个主键ID之间查找,增加了效率,我们当然可以使用。那么它是必须的么,去掉的话我们速度一定变慢?这是其一。第二点,如果患者ID是在循环中,甚至是在循环的循环中…… 消息查看器好像就没办法了(当然如果有大佬可以解决请接受我的膝盖)。 需要解决的问题 对照表,服务和消息类的对照表 如何把循环中的字段作为关键字进行查询 第一个问题就是解决方案,新建一张表维护服务和消息类的关系,第二个问题:我们要去看一下Ens.MessageHeader这张表: 好像看不出什么。。。 大家可以看看这个图 在图中红框框住的部分是一样的,同时我们进入消息可视化,如图: 我们可以不可以说这个会话ID就是代表了这个服务的这条消息呢?我们要查指定服务的指定人的消息可以理解为查这个会话ID也就是SessionID。我们把思路换一下,如果我把循环中的患者ID遍历出来存入一张表中,这一行的数据我有sessionID、患者ID、服务名。当我想查患者ID为【2874621】的个人信息注册服务得到sessionID之后,直接跳到可视化追踪,如果把患者ID换成医嘱ID也是一样的处理,其它关键字段也是一样,问题是不是迎刃而解? 程序设计(思路) 新建一个索引表 字段为 SessionID、服务名、属性名、属性值、创建时间,如图所示: 然后在每次服务被调用的时候取出我们需要的属性名、属性值和SessionID存入这张表中,如图所示: 我们在页面上进行查询的时候只需要编写如图所示的SQL,省略号代表and后面的条件。 后续的REST接口和前端的页面在此就【略】,大家可以参考我的另外一篇文章提供的思路编写提供出来Restful接口(https://cn.community.intersystems.com/node/525561 )。 总结 总的来说,我们思路是我们把原本数据量变小,把原本的多重循环的问题维护在一张单表的多行数据中去。这样把原本的多表联合查询改成了单表查询。这速度不快的飞起?目前这边只做思路分享,相信程序设计上大家肯定有自己的一套方式,我这边暂时不做过多展示。都看到这里了,给我点一个赞吧!!! 可以考虑使用ElasticSearch做全文搜索 是的,用ES是最好的。但是ES中有一个很重要的概念叫倒排索引,就是拆分词语存入倒排索引的库,方便在检索的时候分析,和我这边的把一个消息关键字段拆分成索引去检索是否有一点异曲同工捏。当然还是那句话ES是最好的,只不过我这边不需要那么高性能,而且也没试过IRIS结合ES去使用,这里只做分享哈。 还可以考虑用iknow(现在叫InterSystems NLP)实现全文检索
文章
Louis Lu · 五月 30, 2021

如何保存、查询 List 类型数据

本文主要总结了在InterSystems IRIS 中如何保存、查询List类型数据 假设我们设计的对象中包含姓名,同时每个姓名下可以包含多个电话。我们可以使用下面方法进行处理。 1. 传统方式 我们可以把每一个姓名和电话放在不同列中。 Class Test.Person Extends %Persistent { Property Name As %String; Property Phone As %String; } 我们使用SQL语句插入数据: insert into Test.Person values ('a','111-111-1111'); insert into Test.Person values ('b','222-111-1111'); insert into Test.Person values ('a','111-222-1111'); insert into Test.Person values ('c','333-111-1111'); insert into Test.Person values ('b','222-222-1111'); 数据在表中是这样的: Name Phone a 111-111-1111 b 222-111-1111 a 111-222-1111 c 333-111-1111 b 222-222-1111 这种情况下,我们可以使用下面的sql语句将结果返回: SELECT distinct %exact(Name) Name, LIST(phone %foreach(Name)) Phonestr FROM test.person Name Phonestr a 111-111-1111,111-222-1111 b 222-111-1111,222-222-1111 c 333-111-1111 我们可以为电话号码创建索引,以提高搜索速度,如下: Index IdxP On Phone; 使用这种方式保存list数据比较简单,当是当list数据非常多时,这种方法会使表格臃肿。 2. 保存在一个字符串字段中,使用分隔符区分 这里我们将所有电话号码保存在一个字符串字段中,每个号码之间用逗号区分 Class Test.Person2 Extends %Persistent { Property Name As %String; Property PhoneStr As %String; } 填充数据后,类似于这样 Name PhoneStr a 111-111-1111,111-222-1111 b 222-111-1111,222-222-1111 c 333-111-1111 d 333-111-1111,222-222-1111 这种情况下我们可以用下面方法实现对每个电话的索引 Index idxP On PhoneStr(ELEMENTS); ClassMethod PhoneStrBuildValueArray(value, ByRef array) As %Status { if value="" { s array(0)=value }else{ s list=$lfs(value,","),ptr=0 while $listnext(list,ptr,item){ s array(ptr)=item } } q $$$OK } 这里用到了一个函数 ClassMethod propertynameBuildValueArray(value, ByRef valueArray) As %Status 其中: value – 需要拆分的内容; valueArray – 返回array类型的值,其中包含拆分的内容,格式为 array(key1)=value1, array(key2)=value2... 这时候我们的数据是这样: USER>zw ^Test.Person2D ^Test.Person2D=4 ^Test.Person2D(1)=$lb("","a","111-111-1111,111-222-1111") ^Test.Person2D(2)=$lb("","b","222-111-1111,222-222-1111") ^Test.Person2D(3)=$lb("","c","333-111-1111") ^Test.Person2D(4)=$lb("","d","333-111-1111,222-222-1111") 索引是这样: USER>zw ^Test.Person2I ^Test.Person2I("idxP"," 111-111-1111",1)="" ^Test.Person2I("idxP"," 111-222-1111",1)="" ^Test.Person2I("idxP"," 222-111-1111",2)="" ^Test.Person2I("idxP"," 222-222-1111",2)="" ^Test.Person2I("idxP"," 222-222-1111",4)="" ^Test.Person2I("idxP"," 333-111-1111",3)="" ^Test.Person2I("idxP"," 333-111-1111",4)="" 这种情况下我们可以通过下面的SQL 语句查找 包含电话号码 333-111-1111 的姓名 select Name from test.person2 where phonestr ['333-111-1111' select Name from test.person2 where phonestr like '%333-111-1111%' 但是当你检查查询计划的时候,却发现它并没有使用任何索引 我们只能通过类似于下面SQL语句的写法才能使用该索引 select Name from test.person2 where for some %element(Phonestr) (%value = '333-111-1111') 类似的还有下面的写法 (%Value %STARTSWITH 'а') (%Value [ 'a' and %Value [ 'b') (%Value in ('c','d')) (%Value is null) 3. 使用 %List 类型 Class Test.Person3 Extends %Persistent { Property Name As %String; Property PhoneList As %List; Index idxP On PhoneList(ELEMENTS); ClassMethod PhoneListBuildValueArray(value, ByRef array) As %Status { if value="" { s array(0)=value }else{ s ptr=0 while $listnext(value,ptr,item){ s array(ptr)=item } } q $$$OK } } 插入数据 insert into Test.Person3 (Name,PhoneList) select 'a', $LISTBUILD('111-111-1111','111-222-1111') insert into Test.Person3 (Name,PhoneList) select 'b', $LISTBUILD('222-111-1111','222-222-1111') insert into Test.Person3 (Name,PhoneList) select 'c', $LISTBUILD('333-111-1111') insert into Test.Person3 (Name,PhoneList) select 'd', $LISTBUILD('333-111-1111','222-222-1111') 数据和索引保存为 USER>zw ^Test.Person3D ^Test.Person3D=4 ^Test.Person3D(1)=$lb("","a",$lb("111-111-1111","111-222-1111")) ^Test.Person3D(2)=$lb("","b",$lb("222-111-1111","222-222-1111")) ^Test.Person3D(3)=$lb("","c",$lb("333-111-1111")) ^Test.Person3D(4)=$lb("","d",$lb("333-111-1111","222-222-1111")) USER>zw ^Test.Person3I ^Test.Person3I("idxP","111-111-1111",1)="" ^Test.Person3I("idxP","111-222-1111",1)="" ^Test.Person3I("idxP","222-111-1111",2)="" ^Test.Person3I("idxP","222-222-1111",2)="" ^Test.Person3I("idxP","222-222-1111",4)="" ^Test.Person3I("idxP","333-111-1111",3)="" ^Test.Person3I("idxP","333-111-1111",4)="" 同样可以使用下面的SQL语句查找包含电话333-111-1111的姓名 select Name from test.person2 where for some %element(phonelist) (%value = '333-111-1111') 4 使用 List Of、Array Of 保存 不需要定义propertynameBuildValueArray函数 Class Test.Person4 Extends %Persistent { Property Name As %String; Property PhoneList As list Of %String; Index idxP On PhoneList(ELEMENTS); } 使用同样的方式插入数据 insert into Test.Person4 (Name,PhoneList) select 'a', $LISTBUILD('111-111-1111','111-222-1111') insert into Test.Person4 (Name,PhoneList) select 'b', $LISTBUILD('222-111-1111','222-222-1111') insert into Test.Person4 (Name,PhoneList) select 'c', $LISTBUILD('333-111-1111') insert into Test.Person4 (Name,PhoneList) select 'd', $LISTBUILD('333-111-1111','222-222-1111') 数据和索引保存为 USER>zw ^Test.Person4D ^Test.Person4D=4 ^Test.Person4D(1)=$lb("","a",$lb("111-111-1111","111-222-1111")) ^Test.Person4D(2)=$lb("","b",$lb("222-111-1111","222-222-1111")) ^Test.Person4D(3)=$lb("","c",$lb("333-111-1111")) ^Test.Person4D(4)=$lb("","d",$lb("333-111-1111","222-222-1111")) USER>zw ^Test.Person4I ^Test.Person4I("idxP"," 111-111-1111",1)="" ^Test.Person4I("idxP"," 111-222-1111",1)="" ^Test.Person4I("idxP"," 222-111-1111",2)="" ^Test.Person4I("idxP"," 222-222-1111",2)="" ^Test.Person4I("idxP"," 222-222-1111",4)="" ^Test.Person4I("idxP"," 333-111-1111",3)="" ^Test.Person4I("idxP"," 333-111-1111",4)="" 使用同样的SQL查询可以得到结果 select Name from test.person4 where for some %element(Phonelist) (%value = '333-111-1111') 引申话题:针对日期字段的索引 日期格式通常是yyyy-mm-dd,我们经常要求按照某年或者某月查询数据,我们可以使用propertynameBuildValueArray函数设定保存的索引方式实现这个目的 Class Test.Person5 Extends %Persistent { Property Name As %String; Property DOB As %Date; Index idxD On (DOB(KEYS), DOB(ELEMENTS)); ClassMethod DOBBuildValueArray(value, ByRef array) As %Status { if value="" { s array(0)=value }else{ s d=$zd(value,3) s array("yy")=+$p(d,"-",1) s array("mm")=+$p(d,"-",2) s array("dd")=+$p(d,"-",3) } q $$$OK } } 插入数据 insert into Test.Person5 (Name,DOB) select 'a', {d '2000-01-01'} union all select 'b', {d '2000-01-02'} union all select 'c', {d '2000-02-01'} union all select 'd', {d '2001-01-02'} union all select 'e', {d '2001-01-01'} union all select 'f', {d '2001-02-01'} 查看数据以及索引保存的内容 USER>zw ^Test.Person5D ^Test.Person5D=6 ^Test.Person5D(1)=$lb("","a",58074) ^Test.Person5D(2)=$lb("","b",58075) ^Test.Person5D(3)=$lb("","c",58105) ^Test.Person5D(4)=$lb("","d",58441) ^Test.Person5D(5)=$lb("","e",58440) ^Test.Person5D(6)=$lb("","f",58471) USER>zw ^Test.Person5I ^Test.Person5I("idxD","dd",1,1)="" ^Test.Person5I("idxD","dd",1,3)="" ^Test.Person5I("idxD","dd",1,5)="" ^Test.Person5I("idxD","dd",1,6)="" ^Test.Person5I("idxD","dd",2,2)="" ^Test.Person5I("idxD","dd",2,4)="" ^Test.Person5I("idxD","mm",1,1)="" ^Test.Person5I("idxD","mm",1,2)="" ^Test.Person5I("idxD","mm",1,4)="" ^Test.Person5I("idxD","mm",1,5)="" ^Test.Person5I("idxD","mm",2,3)="" ^Test.Person5I("idxD","mm",2,6)="" ^Test.Person5I("idxD","yy",2000,1)="" ^Test.Person5I("idxD","yy",2000,2)="" ^Test.Person5I("idxD","yy",2000,3)="" ^Test.Person5I("idxD","yy",2001,4)="" ^Test.Person5I("idxD","yy",2001,5)="" ^Test.Person5I("idxD","yy",2001,6)="" 执行下面 SQL 可以显示所有2月出生的信息 select * from Test.Person5 where for some %element(DOB) (%key='mm' and %value = 2) 这篇文章源自 这里,作者 Vitaliy Serdtsev
文章
Guangliang Zhang · 十月 21, 2022

基于cconsole.log的cache数据库的实时监控

cache数据库自身带有系统监控Portal界面,但需要运维人员定期主动查看才能获取监控信息。当系统故障发生时,容易出现由于没有及时获取故障信息而不能及时处理,从而导致造成的影响扩大。本文研究通过解析cache数据库控制台日志(cconsole.log)进行监控信息获取并主动推送微信或短信实现cache数据库主动实时监控。 cache数据库在运行时会将所有控制台消息包括一般消息、系统错误、某些操作系统错误和网络错误都会发送到控制台日志文件,通过操作员控制台工具从其他系统远程启动的作业的成功或失败等信息也会写入控制台日志,因此通过对控制台日志的解析即可获取所需要监控信息。具体步骤方法如下: 解析控制台日志 控制台日志默认存储在install-dir\mgr路径下。 根据cache版本不同,使用的读取方法也不同。对于cache2016版本以上,系统提供了EnsLib.SQL.Snapshot类,可以直接获取日志的行和列信息,非常方便。对于cache2010及以下版本则无此方法,需要使用%File文件读取方法。 随着系统运行时间增加,控制台日志也会不断增大,造成每次检索时间加大,且不容易找出最近的监控信息,作出有效监控。较好的方式是每次解析时均从上次的节点继续进行,以便获取新的有效的监控信息。可以使用指定global来记录每次执行的最后一行ID,下次执行时从ID+1行开始解析来达到目的。对于控制台日志,EnsLib.SQL.Snapshot方法里提供了方法以获取当前行,对于cache2010及以下版本,则需要记录最后一次处理的内容,再次处理的时先找到本条内容的位置,从此位置继续处理。代码示例如下。 cache2016及以上版本代码示例: ClassMethod GetAlertInfo() As %String{ //cconsole.log存储路径 set FilePath="D:\InterSystems\HealthShare\mgr\cconsole.log" //使用EnsLib.SQL.Snapshot读取日志 set snap=##class(EnsLib.SQL.Snapshot).%New() set snap.MaxRowsToGet=-1 do snap.ImportFile(FilePath,," ") do Consolelogshow do snap.Clean() quit $$$OK //处理日志内容Consolelogshow set colsCount=snap.ColCount //获取上一次处理的最后一行行号 set snap.%CurrentRow=$g(^AlertCurrentRow("Consolelog","CurrentRow")) k ^AlertCurrentRow("Consolelog","PID") while snap.Next() { set alertLevel=snap.GetData(3) set pid=snap.GetData(2) set pid=$p(pid,"(",2) set pid=$p(pid,")",1) continue:pid="" //0级别信息不处理 continue:((alertLevel=0)) //进程号已存在不处理,即只获取每次每个进程的第一条告警/错误信息 continue:($d(^AlertCurrentRow("Consolelog","PID",pid))) //记录已处理的进程号 set ^AlertCurrentRow("Consolelog","PID",pid)=1 //定义并组装监控消息内容 set alertInfo="" i ((alertLevel=1)||(alertLevel=2)||(alertLevel=3)){ f i=4:1:colsCount{ set alertInfo=alertInfo_" "_snap.GetData(i) } set alertInfo=snap.GetData(1)_":"_alertLevel_":"_alertInfo continue:((alertLevel=1)&&(alertInfo'["Warning")) //发送监控信息至信息推送平台,如微信或短信等 set rt=..sendAlertInfo(alertInfo) } //记录当前处理的行 set ^AlertCurrentRow("Consolelog","CurrentRow")=snap.%CurrentRow } quit $$$OK} cache2010及以下版本代码示例: ClassMethod GetAlertInfo() As %String{ //使用%File读取日志 set file=##class(%File).%New("/cachesys/mgr/cconsole.log") set Status= file.Open("R") k ^AlertCurrentRow("Consolelog","PID") //获取上次处理的信息内容 set LastPosition=$g(^AlertCurrentRow("Consolelog","LastPosition")) s:LastPosition="" LastPosition=0 set LastPosition=LastPosition+1 set int=0 while 'file.AtEnd{ i int=0{ //跳转到上次处理的最后位置 do file.MoveTo(LastPosition) } set file.LineTerminator=$c(10) set text= file.ReadLine(,.sc) set int=int+1 //解析日志 set pid=$p(text," ",2) set pid=$p(pid,"(",2) set pid=$p(pid,")",1) set alertLevel=$p(text," ",3) s:text'="" textLast=text //定义并组装监控消息内容 i ((alertLevel=1)&&(alertLevel["Warning"))||(alertLevel=2)||(alertLevel=3){ set length=$length(text," ") set alert="",alertInfo="" f i=4:1:length{ set alert=$p(text," ",i) i i=4{ set alertInfo=alert }else{ set alertInfo=alertInfo_" "_alert } } set alertInfo=$p(text," ",1)_": "_alertLevel_": "_pid_":"_alertInfo set alertInfo=$zcvt(alertInfo,"O","UTF8") i '$d(^AlertCurrentRow("Consolelog","PID",pid)){ //记录已处理的进程号 set ^AlertCurrentRow("Consolelog","PID",pid)=1 //发送监控信息至信息推送平台,如微信或短信等 set rt=..sendAlertInfo(alertInfo) } } } //记录最后处理的行内容 set ^AlertCurrentRow("Consolelog","LastPosition")=file.FindAt(1,textLast) do file.Close() quit $$$OK} 控制台日志在记录信息时,经常会记录一系列问题。记录模式为时间+进程ID+信息级别+详细信息。如图1所示。信息级别从0级到3级,问题严重程度依次增加。为0的一般为正常运行信息,也是占日志主要部分的内容。问题级别为1的为警告信息。问题级别为2的为错误信息。问题级别为3的为严重错误信息。因此信息级别为1以上均是监控需要关注的信息。需要将此部分信息及时发出。 图1 cconsole.log普通记录示例 有时候控制台日志也会有其它格式的信息出现,如图2所示。此类信息说明有比较严重的异常行为出现,需要重点关注,因为一般将其全部发出。 图2 cconsole.log特殊记录示例 监控信息推送 控制台日志会对一个进程的多个操作进行记录,如果将所有信息发出,则会造成信息量过大而找不到重点。一般将每个进程的第一个告警或错误信息发出即可。处理方式见上述示例代码。 将获取到所需要发送的监控信息调取微信(如图3)或短信(如图4)或其它信息推送终端的接口即可进行信息推送。 图3 微信告警示例 图4 短信告警示例 任务部署 将程序部署成定时任务,按照所需自定义监控频率即可实现cache数据库的主动实时监控。如图5所示。 图5 监控任务示例 确实升级后的Cache强了很多 这个极实用,推荐大伙收藏。 实用,收藏
文章
姚 鑫 · 六月 17, 2023

第六十章 镜像中断程序 - 使用主 ISCAgent 的日志数据进行 DR 提升和手动故障转移

# 第六十章 镜像中断程序 - 使用主 `ISCAgent` 的日志数据进行 `DR` 提升和手动故障转移 ## 使用主 `ISCAgent` 的日志数据进行 `DR` 提升和手动故障转移 如果 `IRIS A` 的主机系统正在运行,但 `IRIS` 实例没有且无法重新启动,您可以使用以下过程在通过升级后使用来自 `IRIS A` 的最新日志数据更新升级的 `IRIS C IRIS A` 的 `ISCAgent`。 1. 推广 `IRIS C`,选择 `IRIS A` 作为故障转移伙伴。 `IRIS C` 被提升为故障转移成员,从 `IRIS A` 的代理获取最新的日志数据,并成为主要成员。 2. 重新启动 `IRIS A` 上的 `IRIS` 实例,它作为备份重新加入镜像。 3. 在 `IRIS A` 重新加入镜像并变为活动状态后,可以使用使用升级的 DR 异步临时替换故障转移成员中描述的过程,将所有成员返回到它们以前的角色,首先是正常关闭 `IRIS C` ,然后在 `IRIS B` 的配置参数文件的 `[MirrorMember]` 部分中设置 `ValidatedMember=0`(请参阅配置参数文件参考中的 `[MirrorMember]`),将 `IRIS B` 重新启动为 `DR` 异步,将 `IRIS B` 提升为备份,并以 `DR` 异步方式重新启动 `IRIS C`。 注意:如果 `IRIS A` 的主机系统已关闭,但 `IRIS B` 的主机系统已启动,尽管其 `IRIS` 实例未运行,请按照手动故障转移到活动备份中所述在 `IRIS B` 上运行 `^MIRROR` 例程以确定 是否`IRIS B` 在发生故障时是一个活动备份。如果是这样,使用前面的过程,但在升级期间选择 `IRIS B` 作为故障转移伙伴,允许 `IRIS C` 从 `IRIS B` 的 `ISCAgent` 获取最新的日志数据。 ## 使用来自日志文件的日志数据进行 DR 提升和手动故障转移 如果 `IRIS A` 和 `IRIS B` 的主机系统都已关闭,但可以访问 `IRIS A` 的日志文件,或者 `IRIS B` 的日志文件和消息日志可用,您可以使用最新的日志数据更新 `IRIS C`从升级前的初级开始,使用以下过程。 1. 使用 `IRIS A` 或 `IRIS B` 的最新日志文件更新 `IRIS C`,如下所示: - 如果 `IRIS A` 的日志文件可用,则将最新的镜像日志文件从 `IRIS A` 复制到 `IRIS C`,从 `IRIS C` 上的最新日志文件开始,并包括来自 `IRIS A` 的任何后续文件。例如,如果 `MIRROR -MIRRORA-20180220.001` 是 `IRIS C` 上的最新文件,复制 `MIRROR-MIRRORA-20180220.001` 和 `IRIS A` 上的任何更新文件。 - 如果 `IRIS A` 的日志文件不可用但 `IRIS B` 的日志文件和消息日志可用: 1. 确认`IRIS B`很可能已被捕获,如下所示: a. 确认当`A`及其代理不可用时,`B`同时断开与 A的连接。可以通过在`Messages.log`文件中搜索类似于以下内容的消息来检查 `IRIS B`断开连接的时间: ```java MirrorClient: Primary AckDaemon failed to answer status request ``` b. 通过在其 `messages.log` 文件中搜索类似于以下内容的消息,确认 IRIS B 在断开连接时是活动备份: ```java Failed to contact agent on former primary, can't take over ``` 注意:`messages.log` 文件中的如下消息表明 `IRIS B` 在断开连接时未处于活动状态: ```java nonactive Backup is down ``` 当无法确认它是否已被追上时强制提升的 `DR` 异步成为主数据库可能会导致它成为主数据库而没有镜像生成的所有日志数据。因此,一些全局更新操作可能会丢失,而其他镜像成员可能需要从备份中重建。 2. 如果可以确认 `IRIS B` 处于活动状态,请将最新的镜像日志文件从 `IRIS B` 复制到 `IRIS C`,从 `IRIS C` 上的最新日志文件开始,然后包括来自 `IRIS B` 的所有后续文件。例如,如果 `MIRROR-MIRRORA-20180220.001` 是 `InterSystems IRIS C` 上的最新文件,请从 `IRIS C` 复制 `MIRROR-MIRRORA-20180220.001` 和任何更新的文件。检查文件的权限和所有权,并在必要时更改它们以匹配现有日志文件。 2. 在不选择故障转移合作伙伴的情况下将 `IRIS C` 提升为故障转移成员。 `IRIS C` 成为主要的。 3. 当 `IRIS A` 和 `IRIS B` 的问题得到修复时,尽早并在重新启动 `IRIS` 之前,在每个成员上的 `IRIS` 实例的配置参数文件的 `[MirrorMember]` 部分中设置 `ValidatedMember = 0`(参见 `[ MirrorMember]` 在配置参数文件参考)。说明指出,此更改是必需的。完成此操作后,在每个成员上重新启动 `IRIS`,从 `IRIS A`(最近成为主成员的成员)开始。 1. 如果成员在 `IRIS` 重新启动时作为备份或 `DR` 异步加入镜像,则不需要进一步的步骤。任何在故障成员上但不在当前主成员上的日志数据都已被丢弃。 2. 如果在 `IRIS` 实例重新启动时成员无法加入镜像,如重建镜像成员中描述的引用不一致数据的消息日志消息所示,则成员上的最新数据库更改晚于存在于上的最新日志数据 `IRIS C` 成为主要时。要解决此问题,请按照该部分中的描述重建成员。 4. 在大多数情况下,`DR` 异步系统不是主要故障转移成员的合适永久主机。在 `IRIS A` 和 `IRIS B` 重新加入镜像后,使用使用升级的 `DR` 异步临时替换故障转移成员中描述的过程将所有成员返回到它们以前的角色。如果 `IRIS A` 或 `IRIS B` 作为备份重新启动,则在备份处于活动状态时从正常关闭 `IRIS C` 开始,以故障转移到备份;如果 `IRIS A` 或 `IRIS B` 都重新启动为 `DR` 异步,将其中一个提升为备份,然后在 `IRIS C` 上执行正常关闭。将另一个以前的故障转移成员提升为备份,然后将 `IRIS C` 作为 `DR` 异步重启。
文章
姚 鑫 · 三月 23, 2023

中高级开发者教程:通用查询解决方案

# 简介 ### 什么是`Query` > `Query`是一种查询方法,用于查找满足条件的数据,将结果以数据集的形式展现出来。 ### `Query`类别 - `SQL Query`,使用类 `%SQLQuery`和 `SQL SELECT` 语句。 - 自定义`Query`,使用类 `%Query` 和自定义逻辑生成查询数据。 **说明:在讲通用`Query`解决方案之前,我们先了解一下`Query`的基础和基础使用,有助于理解实现原理。如果读者了解`Query`基本使用,可跳过此章节,直接阅读“现状”。** ## `Query`基本使用 ### `SQL Query`基本使用 ```java Query QueryPersonByName(name As %String = "") As %SQLQuery(COMPILEMODE = "IMMEDIATE", CONTAINID = 1, ROWSPEC = "id:%Integer:ID,MT_Name:%String:name,age:%String,no:%String", SELECTMODE = "RUNTIME") [ SqlName = QueryPersonByName, SqlProc ] { SELECT top 10 ID, MT_Age, MT_Name, MT_No FROM M_T.Person WHERE (MT_Name %STARTSWITH :name) ORDER BY id } ``` 说明: - `Query` - 声明`Query`方法关键字。 - `QueryPersonByName` - `Query`方法的名称。 - `name As %String = ""` - `Query`方法的参数。 - `%SQLQuery` - `Query`类型为`%SQLQuery`。 - `%SQLQuery`为`%Query`的子类,使用`Query`的简单的形式,可在方法体内直接编写`Select SQL`语句。 - `COMPILEMODE` - 为`%SQLQuery`的参数,表示编译方式。 - `IMMEDIATE` - 立即编译,当检测当前`SQL`语句是否正确。 - `DYNAMIC` - 动态编译 ,在运行时在编译`SQL`语句。 - `CONTAINID` - 置为返回 `ID` 的列的编号。 - `1` - 返回`ID`列。 - `0` - 不返回。 - `SELECTMODE` - 表示显示方式。 - `RUNTIME` - 无 - `ODBC` - 以`ODBC`方式显示数据。 - `DISPLAY` - 以显示方式显示数据。 - `LOGICAL` - 以逻辑方式显示数据。 - `ROWSPEC` - 提供数据列名称、数据类型、描述。用引号和逗号分隔的变量名和数据类型列表。格式如下: - ```java ROWSPEC = "id:%Integer:ID,age:%String,MT_Name:%String:name,no:%String" ``` - `id` - 表示数据列名称。 - `%Integer` - 表示数据类型。 - `ID` - 数据描述。 - `SqlProc` - 表示该方法可作为存储过程调用。 - `SqlName` - 调用的存储过程名称。 - 无声明调用方式 - `call M.Query_QueryPersonByName()` - 声明调用方式 - `call M.QueryPersonByName()` ![image](/sites/default/files/inline/images/1_64.png) - 运行`Query`方法 - `d ##class(%ResultSet).RunQuery(className, queryName, arg...)` ```java USER>d ##class(%ResultSet).RunQuery("M.Query", "QueryPersonByName") ID:age:name:no: 1:21:yaoxin:314629: 2:29:yx:685381: 3:18:Umansky,Josephine Q.:419268: 4:27:Pape,Ted F.:241661: 5:25:Russell,Howard T.:873214: 6:30:Xenia,Ashley U.:420471: 7:24:Rotterman,Martin O.:578867: 8:18:Drabek,Hannah X.:662167: 9:19:Eno,Mark U.:913628: 11:18:Tsatsulin,Dan Z.:920134: ``` ### 自定义`Query`基本使用 在使用自定义`Query`时,一般都遵循固定的模版。在同一个类中定义以下类方法: - `QueryName` - 在Query方法类型指定 `%Query`。 - `QueryNameExecute `— 此方法主要编写获取数据的业务逻辑,得到数据集。 - `QueryNameFetch` — 此方法遍历数据集。 - `QueryNameClose` — 此方法删除临时数据或对象。 **说明:下面示例展示常用“套路”的自定义`Query`模版,此模版仅仅是常用的一种,并非是固定写法。** --- **定义`QueryName`** ```java Query QueryPersonByAge(pAge As %String = "", count As %Integer = "10") As %Query(ROWSPEC = "id:%Integer:ID,MT_Name:%String:name,age:%String,no:%String") { } ``` 定义名为`QueryPersonByAge`的`Query`类型指定为`%Query`。并将查询定义的主体留空。 --- **定义`QueryNameExecute`** ```java ClassMethod QueryPersonByAgeExecute(ByRef qHandle As %Binary, pAge As %String = "", count As %Integer = "10") As %Status { s pid = $i(^CacheTemp) // 注释1 s qHandle = $lb(0, pid, 0) // 注释2 s index = 1 // 注释3 /* 业务逻辑代码 注释4 */ s id = "" for { s id = $o(^M.T.PersonD(id)) q:(id = "") q:(id > count) s data = ^M.T.PersonD(id) s i = 1 s name = $lg(data, $i(i)) s age = $lg(data, $i(i)) continue:(age < pAge) s no = $lg(data, $i(i)) d output } /* 业务逻辑代码 */ q $$$OK output s ^CacheTemp(pid, index) = $lb(id, age, name, no) // 注释6 s index = index + 1 // 注释7 } ``` `QueryNameExecute()` 方法提供所有需要的业务逻辑。方法的名称必须是 `QueryNameExecute()` ,其中 `QueryName`是定义`Query`的名称。 其中: - `qHandle` - 用于与实现此查询的其他方法进行通信。`qHandle` 可以为任何类型。默认为`%Binary`。 - `pAge As %String = "", count As %Integer = "10"`为`Query`传入参数,可作为业务逻辑的条件使用。 - 注释`1`处代码,`s pid = $i(^CacheTemp)` - 获取`pid`。 - 注释`2`处代码,`s qHandle = $lb(0, pid, 0)` - 数组内第一个元素0表示循环的开始,第二个元素`pid`用于获取`^CacheTemp`数据,第三个元素`0`用于遍历`^CacheTemp`起始节点。 - 业务逻辑代码 - 为获取数据集的主要实现逻辑。 - 注释`3`处代码与注释`7`处代码,为`^CacheTemp`增加索引节点。 - 注释`6`处代码,`s ^CacheTemp(pid, index) = $lb(id, name, age, no)` - 为`^CacheTemp`赋值为后续遍历使用。 - **这里数据格式为`%Library.List`形式,这样`Fetch`方法就不用转类型了,否则`Fetch`方法还需要将数据转为内部列表格式。** --- **定义`QueryNameFetch`** ```java ClassMethod QueryPersonByAgeFetch(ByRef qHandle As %Binary, ByRef row As %List, ByRef end As %Integer = 0) As %Status [ PlaceAfter = QueryPersonByAgeExecute ] { s end = $li(qHandle, 1) // 注释1 s pid = $li(qHandle, 2) s index = $li(qHandle, 3) s index = $o(^CacheTemp(pid, index)) // 注释2 if index = "" { // 注释3 s end = 1 s row = "" } else { s row = ^CacheTemp(pid, index) } s qHandle = $lb(end, pid, index) // 注释4 q $$$OK } ``` `QueryNameFetch()` 方法必须以 `%Library.List` 格式返回单行数据。方法的名称必须是 `QueryNameFetch`,其中 `QueryName`是定义`Query`的名称。 其中: - `qHandle` - 用于与实现此查询的其他方法进行通信。它的值应该是`Execute`定义的值。 - `row` - 表示要返回的一行数据的值类型为` %Library.List`,如果没有返回数据则为空字符串。 - `end` - 当到达最后一行数据时,`end`必须为 `1`。如果不指定为`1`,则会无限循环。 - `PlaceAfter` - `PlaceAfter`方法关键字控制此方法在生成代码中顺序。这里表示在方法`QueryPersonByAgeExecute`生成之后在生成`QueryPersonByAgeFetch`方法。 - 注释`1`处代码, `1~3` 行,解析`qHandle`数组的值获取`end`、`pid`、`index`。 - 注释`2`处代码,`s index = $o(^CacheTemp(pid, index)) ` 根据解析到的`pid`,`index`开始遍历。 - 注释`3`处代码,将遍历的`^CacheTemp(pid, index)`每行属于赋值给`row`,如果`index`为空,则一定要将`end`赋值为`1`。 - 注释`4`处代码,`s qHandle = $lb(end, pid, index)`将取到的`end`、`index`重新复制给`qHandle`为取下一行数据做准备。 **注:`Fetch`方法为多次执行,有多少行数据就遍历多少遍。`Execute`、`Close`方法为一次执行。** --- **定义`QueryNameClose`** ```java ClassMethod QueryPersonByAgeClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = QueryPersonByAgeExecute ] { s pid = $li(qHandle, 2) // 注释1 k ^CacheTemp(pid) // 注释2 q $$$OK } ``` `QueryNameClose() `方法在数据检索完成后删除清理临时数据或对象等结束收尾工作。方法的名称必须是 `QueryNameClose()` ,其中 `QueryName`是定义`Query`的名称。 - `qHandle` - 用于与实现此查询的其他方法进行通信。 - 注释`1`处代码,获取`qHandle` 保存的`pid`。 - 注释`2`处代码,清除临时生成的`^CacheTemp`。 --- **调用自定义Query** ```java USER> d ##class(%ResultSet).RunQuery("M.Query", "QueryPersonByAge","20") ID:name:age:no: 1:yaoxin:21:314629: 2:yx:29:685381: 4:Pape,Ted F.:27:241661: 5:Russell,Howard T.:25:873214: 6:Xenia,Ashley U.:30:420471: 7:Rotterman,Martin O.:24:578867: ``` - 这里查询是年龄大于`20`岁并且`id`小于`10`的所有人员信息。 # 现状 上面`2`个`Query`的基本使用示例,可能是大家最常用的两种方式。 但是经常写查询或者写报表的同学可能会面临如下几个问题: 1. 每次写`Query`都需要定义列头`ROWSPEC`很麻烦,是否可以自己指定列头`ROWSPEC`? 2. 现在很多方法返回的值是`JSON`,如何将`JSON`方法快速转成`Query`? 3. 是否可以写一个通用`Query`,只需要写`Execute`主要逻辑即可? 4. 是否可以优化现在的模版,例如`^CacheTemp`替换成`^||CacheTemp`? 以上的问题,都是可以解决的,请继续阅读下面文章部分。 # 方案 如果想实现通用`Query`还得需要知道一个回调方法`QueryNameGetInfo`。 ```java ClassMethod Json2QueryGetInfo(ByRef colinfo As %List, ByRef parminfo As %List, ByRef idinfo As %List, ByRef qHandle As %Binary, extoption As %Integer = 0, extinfo As %List) As %Status { q $$$OK } ``` 其中: - `colinfo` - 此参数最关键用于定义`ROWSPEC`列头部分。为在 `ROWSPEC` 中声明的每一列包含一个列表元素。形式为 `name:exttype:caption `。 - `name` - 为列头名称。 - `exttype` - 为数据类型。 - `caption` - 为描述说明。 - `colinfo` 类型必须是`%Library.List`,定义的列头的类型也是`%Library.List`。 例如: ```java ClassMethod QueryPersonByAgeGetInfo(ByRef colinfo As %List, ByRef parminfo As %List, ByRef idinfo As %List, ByRef %qHandle As %Binary, extoption As %Integer = 0, extinfo As %List) As %Status { s colinfo = $lb($lb("id", "%Integer", "ID"), $lb("age", "%String", ""), $lb("MT_Name", "%String", "name"), $lb("no", "%String", "")) s parminfo = "" s idinfo = "" q $$$OK } ``` **说明:`Query`执行顺序 `Execute` -> `GetInfo` -> `Fetch(n)` -> `Close`。** 下面分别描述以下几种解决方案: - 通过`Json`数据或方法动态生成`Query ` - 通过`Select Sql`语句动态生成`Query` - 通过`Query`动态生成`Query` - 支持传统的Query并通过参数形式生成Query列 - 定义通用`Query`,只需要实现`Exceute`方法 ## 通过`Json`数据或方法动态生成`Query ` --- **定义`Json`方法** - `Json`方法可任意定义,此示例仅为了测试使用。如下方法:查询当前电脑盘符以`Json`结果输出。 ```json /// desc:查询盘符 ClassMethod QueryDrives(fullyQualified = 1, type = "D") { s array = [] s rs = ##class(%ResultSet).%New() s rs.ClassName = "%File" s rs.QueryName = "DriveList" d rs.Execute(fullyQualified) while (rs.Next()) { s drive = rs.Get("Drive") s drive = $zcvt(drive, "U") s obj = {} s obj.type = "D" continue:(type '= "D") s obj.drive = drive d array.%Push(obj) } q array } ``` 运行: ```json USER> w ##class(M.Query).QueryDrives().%ToJSON() [{"type":"D","drive":"C:\\"},{"type":"D","drive":"D:\\"},{"type":"D","drive":"E:\\"},{"type":"D","drive":"F:\\"},{"type":"D","drive":"G:\\"}] ``` --- **定义`QueryName`** ```java Query Json2Query(className As %String, methodName As %String, arg...) As %Query { } ``` 其中: - `className` - 类名。 - `methodName` - 需要执行的`Json`方法名称。 - `arg..`. - 需要执行的方法参数。 --- **定义`QueryNameExecute`** ```java ClassMethod Json2QueryExecute(ByRef qHandle As %Binary, className As %String, methodName As %String, arg...) As %Status { s array = $classmethod(className, methodName, arg...) // 注释1 if ('$isobject(array)) { // 注释2 s array = [].%FromJSON(array) } q:('array.%IsA("%Library.DynamicArray")) $$$ERROR($$$GeneralError, "不是数组对象") // 注释3 q:(array.%Size() = 0) $$$ERROR($$$GeneralError, "没有数据") // 注释4 s qHandle = array // 注释5 q $$$OK } ``` - 注释`1`代码,利用反射机制调用目标方法并获取返回值。 - 注释`2`代码,判断如果返回的字符串则转成`Json`对象。 - 注释`3`代码,判断该对象不是`%Library.DynamicArray`抛出错误信息。 - 注释`4`代码,`Json`数组长度为`0`抛出错误信息。 - 注释`5`代码,获取数组对象。 --- **定义`QueryNameGetInfo`** ```java ClassMethod Json2QueryGetInfo(ByRef colinfo As %List, ByRef parminfo As %List, ByRef idinfo As %List, ByRef qHandle As %Binary, extoption As %Integer = 0, extinfo As %List) As %Status { s colinfo = $lb() // 注释1 s count = 1 s obj = qHandle.%GetIterator() if obj.%GetNext(.key, .value) { s obj = value.%GetIterator() while obj.%GetNext(.objKey, .objValue) { // 注释2 s $li(colinfo, count) = $lb(objKey) s count = $i(count) } } s parminfo = "" // 注释3 s idinfo = "" // 注释4 s qHandle = qHandle.%GetIterator() // 注释5 q $$$OK } ``` - 注释`1`代码,初始化`colinfo`数组,将`obj`赋值`qHandle.%GetIterator()`迭代器对象。 - 注释`2`代码,遍历`Json`对象获取`Key`,并通过`$li`给`colinfo`赋值。 - 注释`3`代码,初始化`parminfo`,否则报错。 - 注释`4`代码,初始化`idinfo`,否则报错。 - 注释`5`代码,获取的迭代器对象 --- **定义`QueryNameFetch`** ```java ClassMethod Json2QueryFetch(ByRef qHandle As %Binary, ByRef row As %List, ByRef end As %Integer = 0) As %Status [ PlaceAfter = Json2QueryExecute ] { s iter = qHandle q:($g(iter) = "") $$$OK if iter.%GetNext(.key, .value) { // 注释1 s row = "" s obj = value.%GetIterator() while obj.%GetNext(.objKey, .objValue) { // 注释2 if ( $g(row) = "" ) { s row = $lb(objValue) } else { s row = row _ $lb(objValue) } } s end = 0 } else { s row = "" s end = 1 // 注释3 } q $$$OK } ``` - 注释`1`代码,获取当前迭代器`Json`数据行。 - 注释`2`代码,遍历当前`Json`对象并把`value`与`row`进行`$lb`串联。 - 注释`3`代码,如果没有数据设置`end`为`1`表示遍历结束。 --- **定义`QueryNameClose`** ``` ClassMethod Json2QueryClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = Json2QueryFetch ] { s qHandle = "" // 注释1 q $$$OK } ``` - 注释`1`代码,将对象`qHandle`清空。 **注:其实`M`有相关回收机制,实际上`Close`方法不声明也可以。** --- **调用`Json2Query`方法** ```java USER>d ##class(%ResultSet).RunQuery("M.Query","Json2Query","M.Query","QueryDrives","0","D") type:drive: D:D:: D:E:: D:F:: D:G:: ``` ```java USER>d ##class(%ResultSet).RunQuery("M.Query","Json2Query","M.Query","QueryDrives","1","D") type:drive: D:D:\: D:E:\: D:F:\: D:G:\: ``` --- ## 通过`Select Sql`语句动态生成`Query` --- **定义`QueryName`** ```java Query Sql2Query(sql As %String, mode As %String = 1) As %Query { } ``` - `sql` - 表述需要写入`SQL`语句的变量。 - `mode` - 显示数据格式类型。 - 0 - 逻辑格式 - 1 - `OBDC `格式 - 2 - 显示格式 --- **定义`QueryNameExecute`** ```java ClassMethod Sql2QueryExecute(ByRef qHandle As %Binary, sql As %String, mode As %String = 1) As %Status { s sqlStatement = ##class(%SQL.Statement).%New() s sqlStatement.%SelectMode = mode // 注释1 s sqlStatus = sqlStatement.%Prepare(.sql) // 注释2 q:$$$ISERR(sqlStatus) sqlStatus s sqlResult = sqlStatement.%Execute() s stateType = sqlStatement.%Metadata.statementType q:('stateType = 1 ) $$$ERROR($$$GeneralError, "不是select语句") // 注释3 s qHandle = {} s qHandle.sqlResult = sqlResult // 注释4 s qHandle.sqlStatement = sqlStatement q $$$OK } ``` - 注释`1`代码,设置`SQL`的数据显示格式。 - 注释`2`代码,传入`SQL`语句得到`sqlStatement`与`sqlResult`对象。 - 注释`3`代码,传入的`SQL`非`Select`语句,抛出错误信息。 - 注释`4`代码,将`qHandle`传入两个对象分别是`sqlResult`、`sqlStatement`。 - `sqlResult`用于遍历数据使用。 - `sqlStatement`用于得到数据列头信息。 --- **定义`QueryNameGetInfo`** ```java ClassMethod Sql2QueryGetInfo(ByRef colinfo As %List, ByRef parminfo As %List, ByRef idinfo As %List, ByRef qHandle As %Binary, extoption As %Integer = 0, extinfo As %List) As %Status { s colinfo = $lb() s sqlStatement = qHandle.sqlStatement // 注释1 s count = 1 for i = 1 : 1 : sqlStatement.%Metadata.columnCount { s data = sqlStatement.%Metadata.columns.GetAt(i).label s $li(colinfo, count) = $lb(data) // 注释2 s count = $i(count) } s parminfo = "" s idinfo = "" q $$$OK } ``` - 注释`1`代码,通过`qHandle`得到`sqlStatement`对象。 - 注释`2`代码,给`colinfo`列表进行循环赋值列头信息, --- **定义`QueryNameFetch`** ```java ClassMethod Sql2QueryFetch(ByRef qHandle As %Binary, ByRef row As %List, ByRef end As %Integer = 0) As %Status [ PlaceAfter = Sql2QueryExecute ] { s sqlStatement = qHandle.sqlStatement // 注释1 s sqlResult = qHandle.sqlResult s colCount = sqlResult.%ResultColumnCount // 注释2 if (sqlResult.%Next()) { for i = 1 : 1 : colCount{ s val = sqlResult.%GetData(i) if ( $g(row) = "" ) { // 注释3 s row = $lb(val) } else { s row = row _ $lb(val) } } s end = 0 } else { s row = "" s end = 1 } s qHandle.sqlResult = sqlResult // 注释4 q $$$OK } ``` - 注释`1`代码,通过`qHandle`得到`sqlStatement`、`sqlResult`对象。 - 注释`2`代码,得到列数,相当于得到一行数据有多少项。 - 注释`3`代码,遍历数据给`row`赋值。 - 注释`4`代码,将`qHandle.sqlResult`对象,赋值给循环当前对象。 --- **定义`QueryNameClose`** 此处省略。 **注:其实`M`有相关回收机制,实际上`Close`方法不声明也可以。** --- **调用`Sql2Query`方法** ```java USER>d ##class(%ResultSet).RunQuery("M.Query","Sql2Query","select * from M_T.Person", 1) id:MT_Age:MT_Name:MT_No: 1:21:yaoxin:314629: 2:29:yx:685381: 3:18:Umansky,Josephine Q.:419268: 4:27:Pape,Ted F.:241661: 5:25:Russell,Howard T.:873214: 6:30:Xenia,Ashley U.:420471: 7:24:Rotterman,Martin O.:578867: 8:18:Drabek,Hannah X.:662167: 9:19:Eno,Mark U.:913628: ... 100:24:Nathanson,Jocelyn A.:147578: ``` ```java USER>d ##class(%ResultSet).RunQuery("M.Query","Sql2Query","select ID,MT_Name from M_T.Person") id:MT_Name: 1:yaoxin: 2:yx: 3:Umansky,Josephine Q.: 4:Pape,Ted F.: 5:Russell,Howard T.: 6:Xenia,Ashley U.: 7:Rotterman,Martin O.: ... 100:Nathanson,Jocelyn A.: ``` ```java USER>d ##class(%ResultSet).RunQuery("M.Query","Sql2Query","select top 10 ID as id from M_T.Person") id: 1: 2: 3: 4: 5: 6: 7: 8: 9: 11: ``` ## 通过`Query`生成动态`Query` --- **定义`QueryName`** ```java Query Query2Query(className As %String, queryName As %String, arg...) As %Query { } ``` - `className` - 类名。 - `queryName` - 需要执行的`Query`方法名称。 - `arg..`. - 需要执行的`Query`方法参数。 --- **定义`QueryNameExecute`** ```java ClassMethod Query2QueryExecute(ByRef qHandle As %Binary, className As %String, queryName As %String, arg...) As %Status { s sqlStatement = ##class(%SQL.Statement).%New() s sqlStatus = sqlStatement.%PrepareClassQuery(className, queryName) q:$$$ISERR(sqlStatus) sqlStatus s sqlResult = sqlStatement.%Execute() s qHandle = {} s qHandle.sqlResult = sqlResult s qHandle.sqlStatement = sqlStatement q $$$OK } ``` - 与`Sql2Query`类似。 --- **定义`QueryNameGetInfo`** ```java ClassMethod Query2QueryGetInfo(ByRef colinfo As %List, ByRef parminfo As %List, ByRef idinfo As %List, ByRef qHandle As %Binary, extoption As %Integer = 0, extinfo As %List) As %Status { s colinfo = $lb() s sqlStatement = qHandle.sqlStatement s count = 1 s column = "" for { s column = $o(sqlStatement.%Metadata.columnIndex(column)) q:(column = "") s data = sqlStatement.%Metadata.columnIndex(column) s $li(colinfo, count) = $lb($lg(data, 2)) s count = $i(count) } s parminfo = "" s idinfo = "" q $$$OK } ``` - 与`Sql2Query`类似。 --- **定义`QueryNameFetch`** ```java ClassMethod Query2QueryFetch(ByRef qHandle As %Binary, ByRef row As %List, ByRef end As %Integer = 0) As %Status [ PlaceAfter = Query2QueryExecute ] { s sqlStatement = qHandle.sqlStatement s sqlResult = qHandle.sqlResult s colCount = sqlResult.%ResultColumnCount if (sqlResult.%Next()) { for i = 1 : 1 : colCount{ s val = sqlResult.%GetData(i) if ( $g(row) = "" ) { s row = $lb(val) } else { s row = row _ $lb(val) } } s end = 0 } else { s row = "" s end = 1 } s qHandle.sqlResult = sqlResult q $$$OK } ``` - 与`Sql2Query`类似。 --- **调用`Query2Query`** ```java USER>d ##class(%ResultSet).RunQuery("M.Query","Query2Query","M.Query","QueryPersonByName") age:id:MT_Name:no: 1:21:yaoxin:314629: 2:29:yx:685381: 3:18:Umansky,Josephine Q.:419268: 4:27:Pape,Ted F.:241661: 5:25:Russell,Howard T.:873214: 6:30:Xenia,Ashley U.:420471: 7:24:Rotterman,Martin O.:578867: 8:18:Drabek,Hannah X.:662167: 9:19:Eno,Mark U.:913628: 11:18:Tsatsulin,Dan Z.:920134: ``` --- ## 支持传统的`Query`并通过参数形式生成`Query`列 - 支持传统的`Query`形式。 - 支持通过参数形式定义列,不需要指定`ROWSPEC`参数。 - 优化将`^CacheTemp`为`^||CacheTemp。` **定义`M.CommonQuery`** ```java Class M.CommonQuery Extends %Query { ClassMethod Close(ByRef qHandle As %Binary) As %Status [ CodeMode = generator, PlaceAfter = Execute, ProcedureBlock = 1, ServerOnly = 1 ] { s %code($i(%code))= (" s pid = $li(qHandle, 2)") s %code($i(%code))= (" k ^||GlobalTemp(pid)") s %code($i(%code))= (" q $$$OK") q $$$OK } ClassMethod Fetch(ByRef qHandle As %Binary, ByRef row As %List, ByRef end As %Integer = 0) As %Status [ CodeMode = generator, PlaceAfter = Execute, ProcedureBlock = 1, ServerOnly = 1 ] { s %code($i(%code))= (" s end = $li(qHandle, 1)") s %code($i(%code))= (" s pid = $li(qHandle, 2)") s %code($i(%code))= (" s ind = $li(qHandle, 3)") s %code($i(%code))= (" s ind = $o(^||GlobalTemp(pid, ind))") s %code($i(%code))= (" if (ind = """") { ") s %code($i(%code))= (" s end = 1") s %code($i(%code))= (" s row = """"") s %code($i(%code))= (" } else { ") s %code($i(%code))= (" s row = ^||GlobalTemp(pid, ind)") s %code($i(%code))= (" }") s %code($i(%code))= (" s qHandle = $lb(end, pid, ind)") s %code($i(%code))= (" q $$$OK") q $$$OK } ClassMethod GetInfo(ByRef colinfo As %List, ByRef parminfo As %List, ByRef idinfo As %List, ByRef qHandle As %Binary, extoption As %Integer = 0, ByRef extinfo As %List) As %Status [ CodeMode = generator, ServerOnly = 1 ] { s %code($i(%code))= (" s colinfo = $lb()") s %code($i(%code))= (" s column = $lg(qHandle, 4)") s %code($i(%code))= (" if ($lv(column)) {") s %code($i(%code))= (" for i = 1 : 1 : $ll(column) {") s %code($i(%code))= (" s $li(colinfo, i) = $lb(""Column"" _ i )") s %code($i(%code))= (" } ") s %code($i(%code))= (" } else {") s %code($i(%code))= (" s len = $l(column, "","")") s %code($i(%code))= (" for i = 1 : 1 : len {") s %code($i(%code))= (" s $li(colinfo, i) = $lb($p(column, "","", i))") s %code($i(%code))= (" }") s %code($i(%code))= (" }") s %code($i(%code))= (" s parminfo = """"") s %code($i(%code))= (" s idinfo = """"") s %code($i(%code))= (" q $$$OK") q $$$OK } } ``` --- **定义`QueryName`** ```java Query CustomColumnQuery(column As %String = "") As M.CommonQuery { } ``` - `column` - 表示要自定义参数列的变量。 - `M.CommonQuery` - 自定义Query类型,不需要写`GetInfo`、`Fetch`、`Close`方法。 --- **定义`QueryNameExecute`** `QueryNameExecute` 支持三种定义列头方式: 1. 通过`column`参数传入列头,实现如下: ```java ClassMethod CustomColumnQueryExecute(ByRef qHandle As %Binary, column As %List) As %Status { s pid = $i(^||GlobalTemp) s qHandle = $lb(0, pid, 0) s $li(qHandle, 4) = column // 方式1此位置必填 s ind = 1 s id = "" for { s id = $o(^M.T.PersonD(id)) q:(id = "") s data = ^M.T.PersonD(id) s i = 1 s name = $lg(data, $i(i)) s age = $lg(data, $i(i)) s no = $lg(data, $i(i)) d output } q $$$OK output s data = $lb(id, name) s ^||GlobalTemp(pid, ind)=data s ind = ind + 1 } ``` ```java USER> d ##class(%ResultSet).RunQuery("M.Query","CustomColumnQuery","ID,Name") ID:Name: 1:yaoxin: 2:yx: 3:Umansky,Josephine Q.: 4:Pape,Ted F.: 5:Russell,Howard T.: ``` 2. 不传入`column`参数,自动根据列表数据数量生成列头,实现如下: ```java ClassMethod CustomColumnQueryExecute(ByRef qHandle As %Binary, column As %String = "") As %Status { s pid = $i(^||GlobalTemp) s qHandle = $lb(0, pid, 0) s ind = 1 s id = "" for { s id = $o(^M.T.PersonD(id)) q:(id = "") s data = ^M.T.PersonD(id) s i = 1 s name = $lg(data, $i(i)) s age = $lg(data, $i(i)) s no = $lg(data, $i(i)) s data = $lb(id, name, no) q:(id > 5) d output } s $li(qHandle, 4) = data // 方式2此位置必填 q $$$OK output s ^||GlobalTemp(pid, ind)=data s ind = ind + 1 } ``` ```java USER>d ##class(%ResultSet).RunQuery("M.Query","CustomColumnQuery") Column1:Column2:Column3: 1:yaoxin:314629: 2:yx:685381: 3:Umansky,Josephine Q.:419268: 4:Pape,Ted F.:241661: 5:Russell,Howard T.:873214: ``` 3. 不传入`column`参数,通过`Execute`方法自定义列头信息,实现如下: ```java ClassMethod CustomColumnQueryExecute0(ByRef qHandle As %Binary, column As %String = "") As %Status { s pid = $i(^||GlobalTemp) s qHandle = $lb(0, pid, 0) s ind = 1 s id = "" for { s id = $o(^M.T.PersonD(id)) q:(id = "") s data = ^M.T.PersonD(id) s i = 1 s name = $lg(data, $i(i)) s age = $lg(data, $i(i)) s no = $lg(data, $i(i)) s data = $lb(id, name, no) q:(id > 5) d output } s $li(qHandle, 4) = "id,name,age" // 方式3此位置必填 q $$$OK output s ^||GlobalTemp(pid, ind)=data s ind = ind + 1 } ``` ```java USER>d ##class(%ResultSet).RunQuery("M.Query","CustomColumnQuery") id:name:age: 1:yaoxin:314629: 2:yx:685381: 3:Umansky,Josephine Q.:419268: 4:Pape,Ted F.:241661: 5:Russell,Howard T.:873214: ``` --- ## 定义通用`Query`,只需要实现`Exceute`方法 实现通用`Query`,需要通过抽象方法,子类去重写的方式去实现。所以首先定义父类。 **定义`M.CommonQuery`** ```java Class M.BaseQuery Extends %RegisteredObject { /// d ##class(%ResultSet).RunQuery("M.BaseQuery","CustomQuery","id,name") Query CustomQuery(column As %List, arg...) As %Query { } ClassMethod CustomQueryExecute(ByRef qHandle As %Binary, column As %List, arg...) As %Status { s qHandle = $lb(0, 0) // 注释1 s $li(qHandle, 3) = column // 注释2 d ..QueryLogic(arg...) // 注释3 q $$$OK } ClassMethod CustomQueryGetInfo(ByRef colinfo As %List, ByRef parminfo As %List, ByRef idinfo As %List, ByRef qHandle As %Binary, extoption As %Integer = 0, ByRef extinfo As %List) As %Status { s colinfo = $lb() s column = $lg(qHandle ,3) s len = $l(column, ",") for i = 1 : 1 : len { s $li(colinfo, i) = $lb($p(column, ",", i)) // 注释5 } s parminfo = "" s idinfo = "" q $$$OK } ClassMethod CustomQueryClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = CustomQueryExecute ] { k %zQueryList // 注释7 q $$$OK } ClassMethod CustomQueryFetch(ByRef qHandle As %Binary, ByRef row As %List, ByRef end As %Integer = 0) As %Status [ PlaceAfter = CustomQueryExecute ] { s end = $li(qHandle,1) s index = $li(qHandle,2) s index = $o(%zQueryList(index)) if index = "" { // 注释6 s end = 1 s row = "" } else { s row = %zQueryList(index) } s qHandle = $lb(end, index) Quit $$$OK } ClassMethod QueryLogic(arg...) [ Abstract ] { // 注释4 } } ``` - `column` - 表示要自定义参数列的变量。 - `arg...` - 传入的参数。 - 注释`1`代码,这里做了一些改变,`qHandle`只记录了`end`与`index`。因为这里如果是全局变量或者进程私有`Global`只针对当前进程有效,所以`pid`可省略。 - 注释`2`代码,将`qHandle`第三个位置传入列头名称。 - 注释`3`代码,调用待实现的业务逻辑方法,此方法为抽象方法,需要子类去实现。 - 注释`4`代码,子类需要实现的具体业务逻辑,得到数据集。 - 注释`5`代码,获取到`column`动态设置列头。 - 注释`6`代码,遍历全局变量。 - 注释`7`代码,遍历结束后,将全局变量清空。 --- **定义子类`M.PersonQuery`继承`M.BaseQuery`实现`QueryLogic`方法** - 这里只需要给`%zQueryList($i(count))`全局变量赋值即可。固定模版已经抽象到父类。 ```java ClassMethod QueryLogic(arg...) { s pName = arg(1) s id = "" for { s id = $o(^M.T.PersonD(id)) q:(id = "") s data = ^M.T.PersonD(id) s i = 1 s name = $lg(data, $i(i)) continue:(pName '= "")&&(name '= pName) s age = $lg(data, $i(i)) s no = $lg(data, $i(i)) s %zQueryList($i(count)) = $lb(id, name, age) } } ``` --- **调用`CustomQuery`方法** ```java USER>d ##class(%ResultSet).RunQuery("M.PersonQuery","CustomQuery","ID,Name,Age", "yaoxin") ID:Name:Age: 1:yaoxin:21: ``` **注:这里是用的是全局变量作为数据传递,如果数据过大,则可能会出现内存泄漏问题。改成进程私有`Global`即可。由读者基于此逻辑自行实现。** **注:这种方式一个类只能声明一个Query,如果想一个类声明多个Query,则考虑换成支持传统的Query方式。** --- ## 通过`Query`生成`Json` ```java ClassMethod Query2Json(className, queryName, arg...) { s array = [] s rs = ##class(%ResultSet).%New() s rs.ClassName = className s rs.QueryName = queryName d rs.Execute(arg...) s array = [] #; 属性值 while (rs.Next()) { s valStr = "" s obj = {} for i = 1 : 1 : rs.GetColumnCount(){ s columnName = rs.GetColumnName(i) s val = rs.Data(columnName) d obj.%Set(columnName, val) } d array.%Push(obj) } q array.%ToJSON() } ``` ```java USER>w ##class(Util.JsonUtils).Query2Json("%SYSTEM.License","Summary") [{"LicenseUnitUse":"当前使用的软件许可单元 ","Local":"1","Distributed":"1"},{"Li censeUnitUse":"使用的最大软件许可单元数 ","Local":"15","Distributed":"15"},{"Lic enseUnitUse":"授权的软件许可单元 ","Local":"300","Distributed":"300"},{"LicenseU nitUse":"当前连接 ","Local":"3","Distributed":"3"},{"LicenseUnitUse":"最大连接数 ","Local":"17","Distributed":"17"}] ``` --- ## 通过`Query`生成`Csv` ```java ClassMethod Query2Csv(className, queryName, filePath, arg...) { s file = ##class(%FileCharacterStream).%New() s file.Filename = filePath s array = [] s rs = ##class(%ResultSet).%New() s rs.ClassName = className s rs.QueryName = queryName d rs.Execute(arg...) #; 列名 s colStr = "" for i = 1 : 1 : rs.GetColumnCount(){ s columnName = rs.GetColumnName(i) s colStr = $s(colStr = "" : columnName, 1 : colStr _ "," _ columnName) } d file.Write(colStr) #; 属性值 while (rs.Next()) { s valStr = "" for i = 1 : 1 : rs.GetColumnCount(){ s columnName = rs.GetColumnName(i) s val = rs.Data(columnName) s valStr = $s(valStr = "" : val, 1 : valStr _ "," _ val) } d file.Write($c(10) _ valStr) } d file.%Save() q $$$OK } ``` ```java USER>w ##class(Util.FileUtils).Query2Csv("%SYSTEM.License","Summary","E:\m\CsvFile2.csv") 1 ``` ![请添加图片描述](https://img-blog.csdnimg.cn/b29f587a069b4a96896d1372471b5c0f.png) ![请添加图片描述](https://img-blog.csdnimg.cn/c6e34623b0654b128ba2fb2cff1d68e3.png) --- # 总结 - 理解`qHandle`参数与`GetInfo`方法是实现通用`Query`的关键。 - 使用通用`Query`可以提升开发效率。 - 使用通用`Query`可以解决数据适配问题。 **以上是个人对基于`Query`的一些理解,由于个人能力有限,欢迎大家提出意见,共同交流。** > 如果一个好点子,只是因为某个人先到想到就禁止后人使用,这会让整个人类社会多走很多弯路,这也是自由软件精神一直以来所表达的内容。 > - 理查德·马修·斯托曼 #
文章
姚 鑫 · 四月 21, 2021

第四章 缓存查询(二)

# 第四章 缓存查询(二) # 运行时计划选择 运行时计划选择(`RTPC`)是一个配置选项,它允许SQL优化器利用运行时(查询执行时)的离群值信息。运行时计划选择是系统范围的SQL配置选项。 当`RTPC`被激活时,准备查询包括检测查询是否包含具有离群值的字段上的条件。如果`PREPARE`检测到一个或多个异常值字段条件,则不会将查询发送到优化器。相反,SQL会生成一个运行时计划选择存根。在执行时,优化器使用此存根选择要执行的查询计划:忽略离群值状态的标准查询计划,或针对离群值状态进行优化的替代查询计划。如果有多个异常值条件,优化器可以从多个备选运行时查询计划中进行选择。 - 准备查询时,SQL将确定它是否包含离群值字段条件。如果是这样,它将推迟选择查询计划,直到执行查询。在准备时,它创建一条标准SQL语句和(对于动态SQL)相应的缓存查询,但将选择是使用此查询计划还是创建不同的查询计划,直到查询执行。在准备时,它创建看起来像是标准SQL语句的内容,如下所示:`DECLARE QRS CURSOR FOR SELECT Top ? Name,HaveContactInfo FROM Sample.MyTest WHERE HaveContactInfo=?`,用问号表示文字替代变量。但是,如果查看SQL语句详细资料,则查询计划在准备时包含语句“执行可能导致创建不同的计划”,动态SQL查询还会创建看似标准的缓存查询;但是,缓存查询显示计划选项使用`SELECT %NORUNTIME`关键字显示查询文本,表明这是不使用`RTPC`的查询计划。 - 执行查询(在嵌入式SQL中打开)时,SQL将创建第二个SQL语句和相应的缓存查询。SQL语句具有散列生成的名称并生成RTPC存根,如下所示: `DECLARE C CURSOR FOR %NORUNTIME SELECT Top :%CallArgs(1) Name,HaveContactInfo FROM Sample.MyTest WHERE HaveContactInfo=:%CallArgs(2)`.然后,优化器使用它来生成相应的缓存查询。如果优化器确定离群值信息没有提供性能优势,它将创建一个与准备时创建的缓存查询相同的缓存查询,并执行该缓存查询。但是,如果优化器确定使用离群值信息可提供性能优势,则它会创建一个缓存查询,以禁止对缓存查询中的离群值字段进行文字替换。例如,如果`HaveContactInfo`字段是异常值字段(绝大多数记录的值为‘Yes’),查询`SELECT Name,HaveContactInfo FROM t1 WHERE HaveContactInfo=?`将导致缓存查询:`SELECT Name,HaveContactInfo FROM t1 WHERE HaveContactInfo=(('Yes')).` 请注意,`RTPC`查询计划的显示根据SQL代码的源代码而有所不同: 管理门户SQL界面显示计划按钮可能会显示另一个运行时查询计划,因为此显示计划从SQL界面文本框中获取其SQL代码。 选中该SQL语句后,将显示包括查询计划的语句详细资料。此查询计划不显示替代运行时查询计划,而是包含文本“执行可能导致创建不同的计划”,因为它从语句索引中获取其SQL代码。 如果`RTPC`未激活,或者查询不包含适当的离群值字段条件,优化器将创建标准SQL语句和相应的缓存查询。 如果一个`RTPC`存根被冻结,那么所有相关的备用运行时查询计划也会被冻结。 即使关闭了`RTPC`配置选项,对于冻结的查询,`RTPC`处理仍然是活动的。 在写查询时,可以通过指定圆括号来手动抑制文字替换: `SELECT Name,HaveContactInfo FROM t1 WHERE HaveContactInfo=(('Yes'))`.如果在条件中抑制离群值字段的文字替换,则`RTPC`不会应用于查询。 优化器创建一个标准的缓存查询。 ## 激活RTPC 可以使用管理门户或类方法在系统范围内配置`RTPC`。 注意,更改`RTPC`配置设置将清除所有缓存的查询。 使用管理门户,根据参数值SQL设置配置系统范围的优化查询。 该选项将运行时计划选择(`RTPC`)优化和作为离群值(`BQO`)优化的偏差查询设置为合适的组合。 选择系统管理、配置、SQL和对象设置、SQL来查看和更改此选项。 可用的选择有: - 假设查询参数值不是字段离群值(`BQO=OFF`, `RTPC=OFF`,初始默认值) - 假设查询参数值经常匹配字段离群值(`BQO=ON`, `RTPC=OFF`) - 在运行时优化实际查询参数值(`BQO=OFF`, `RTPC=ON`) 要确定当前设置,调用`$SYSTEM.SQL.CurrentSettings()`。 `$SYSTEM.SQL.Util.SetOption()`方法可以在系统范围内激活所有进程的`RTPC`,如下所示:`SET status=$SYSTEM.SQL.Util.SetOption("RTPC",flag,.oldval)`。 `flag`参数是一个布尔值,用于设置(1)或取消设置(0)RTPC。 `oldvalue`参数以布尔值的形式返回之前的RTPC设置。 ## 应用RTPC 系统对`SELECT`和`CALL`语句应用`RTPC`。 它不应用`RTPC`插入、更新或删除语句。 当在以下查询上下文中指定了一个离群值时,系统将`RTPC`应用于调优表确定的任何字段。 在与文字比较的条件中指定离群值字段。 这个比较条件可以是: - 使用相等(`=`)、非相等(`!=`)、`IN`或`%INLIST`谓词的`WHERE`子句条件。 - 具有相等(`=`)、非相等(`!=`)、`IN`或`%INLIST`谓词的`ON`子句连接条件。 如果应用了`RTPC`,优化器将在运行时确定是应用标准查询计划还是备选查询计划。 如果查询中包含`unresolved ?` 输入参数。 如果查询指定了用双括号括起来的文字值,则不应用`RTPC`,从而抑制了文字替换。 如果文字是由子查询提供给离群字段条件的,则`RTPC`不会被应用。 但是,如果子查询中存在离群字段条件,则应用`RTPC`。 ## Overriding RTPC 通过指定`%NORUNTIME` `restrict`关键字,可以覆盖特定查询的`RTPC`。如果查询`SELECT Name,HaveContactInfo FROM t1 WHERE HaveContactInfo=?` 会导致`RTPC`处理,查询 `SELECT %NORUNTIME Name,HaveContactInfo FROM t1 WHERE HaveContactInfo=?`将覆盖`RTPC`,从而产生一个标准的查询计划。 # 缓存查询结果集 当执行缓存的查询时,它会创建一个结果集。 缓存的查询结果集是一个对象实例。 这意味着为文字替换输入参数指定的值被存储为对象属性。 这些对象属性使用`i%PropName`语法引用。 # List缓存查询 ## 计算缓存查询 通过调用`%Library.SQLCatalog类的GetCachedQueryTableCount()`方法,可以确定表的当前缓存查询数。下面的示例显示了这一点: ```java /// w ##class(PHA.TEST.SQL).CountingCachedQueries() ClassMethod CountingCachedQueries() { SET tbl="Sample.Person" SET num=##class(%Library.SQLCatalog).GetCachedQueryTableCount(tbl) IF num=0 { WRITE "没有缓存的查询 ",tbl } ELSE { WRITE tbl," 与以下内容相关联 ",num," 缓存查询" } q "" } ``` ```java DHC-APP>w ##class(PHA.TEST.SQL).CountingCachedQueries() Sample.Person 与以下内容相关联 2 缓存查询 ``` 请注意,引用多个表的查询将创建单个缓存查询。但是,这些表中的每一个都单独计算该缓存查询的数量。因此,按表计数的缓存查询数可能大于实际缓存查询数。 ## 显示缓存的查询 可以使用IRIS管理门户查看(和管理)查询缓存的内容。从系统资源管理器中,选择SQL。使用页面顶部的切换选项选择一个命名空间;这将显示可用命名空间的列表。在屏幕左侧打开`Cached Queries`文件夹。选择其中一个缓存查询将显示详细信息。 查询类型可以是下列值之一: - `%SQL.Statement Dynamic SQL`:使用`%SQL.Statement`的动态SQL查询。 - `Embedded cached SQL` :嵌入式缓存SQL - `ODBC/JDBC Statement`:来自ODBC或JDBC的动态查询。 成功准备SQL语句后,系统会生成一个实现该语句的新类。如果已经设置了Retention Cached Query Source-System-wide配置选项,那么这个生成的类的源代码将被保留,并且可以使用Studio打开以供检查。要执行此操作,请转到IRIS管理门户。从系统管理中,依次选择配置、SQL和对象设置、SQL。在此屏幕上,可以设置保留缓存的查询源选项。如果未设置此选项(默认设置),系统将生成并部署类,并且不保存源代码。 也可以使用`$SYSTEM.SQL.Util.SetOption()`方法设置这个系统范围的选项,如下所示:`SET status=$SYSTEM.SQL.Util.SetOption("CachedQuerySaveSource",flag,.oldval)`。`Flag`参数是一个布尔值,用于在编译缓存查询后保留(1)或不保留(0)查询源代码;默认值为0。要确定当前设置,请调用`$SYSTEM.SQL.CurrentSettings()`。 ## 使用^rINDEXSQL列出缓存查询 ```java ZWRITE ^rINDEXSQL("sqlidx",2) ``` 此列表中的典型全局变量如下所示: ```java ^rINDEXSQL("sqlidx",2,"%sqlcq.USER.cls4.1","oRuYrsuQDz72Q6dBJHa8QtWT/rQ=")="". ``` 第三个下标是位置。例如,`"%sqlcq.USER.cls4.1"`是用户名称空间中的缓存查询;`"Sample.MyTable.1"`是一条SQL语句。第四个下标是语句散列。 ## 将缓存查询导出到文件 以下实用程序将当前名称空间的所有缓存查询列出到文本文件中。 ```java ExportSQL^%qarDDLExport(file,fileOpenParam,eos,cachedQueries,classQueries,classMethods,routines,display) ``` - `file` 要列出缓存查询的文件路径名。指定为带引号的字符串。如果该文件不存在,系统将创建该文件。如果该文件已存在,则InterSystems IRIS会覆盖该文件。 - `fileOpenParam` 可选-文件的打开模式参数。指定为带引号的字符串。默认值为`“WNS”`。`“W”`指定正在打开文件以进行写入。`“N”`指定如果该文件不存在,则使用此名称创建一个新的顺序文件。`“S”`指定以回车符、换行符或换页符作为默认终止符的流格式。 - `eos` 可选-用于分隔清单中各个缓存查询的语句结尾分隔符。指定为带引号的字符串。默认值为`“GO”`。 - `cachedQueries` 可选—从查询缓存导出所有SQL查询到文件。一个布尔标志。默认值为1。 - `classQueries` 可选-从SQL类查询导出所有SQL查询到文件。一个布尔标志。默认值为1。 - `classMethods` 可选-从类方法导出嵌入式SQL查询到文件。一个布尔标志。默认值为1。 - `routines` 可选-从MAC例程导出嵌入式SQL查询到文件。这个清单不包括系统例程、缓存查询或生成的例程。一个布尔标志。默认值为1。 - `display` 可选-在终端屏幕上显示导出进度。一个布尔标志。默认值为0。 下面是一个调用这个缓存查询导出工具的示例: ```java DO ExportSQL^%qarDDLExport("C:\temp\test\qcache.txt","WNS","GO",1,1,1,1,1) ``` 当在终端命令行中执行`display=1`时,导出进度显示在终端屏幕上,示例如下: ```sql Export SQL Text for Cached Query: %sqlcq.USER.cls14.. Done Export SQL Text for Cached Query: %sqlcq.USER.cls16.. Done Export SQL Text for Cached Query: %sqlcq.USER.cls17.. Done Export SQL Text for Cached Query: %sqlcq.USER.cls18.. Done Export SQL Text for Cached Query: %sqlcq.USER.cls19.. Done Export SQL statement for Class Query: Cinema.Film.TopCategory... Done Export SQL statement for Class Query: Cinema.Film.TopFilms... Done Export SQL statement for Class Query: Cinema.FilmCategory.CategoryName...Done Export SQL statement for Class Query: Cinema.Show.ShowTimes... Done 20 SQL statements exported to script file C:\temp\test\qcache.txt ``` 创建的导出文件包含如下条目: ```sql -- SQL statement from Cached Query %sqlcq.USER.cls30 SELECT TOP ? Name , Home_State , Age , AVG ( Age ) AS AvgAge FROM Sample . Person ORDER BY Home_State GO ``` ``` -- SQL statement from Class Query Cinema.Film.TopCategory #import Cinema SELECT TOP 3 ID, Description, Length, Rating, Title, Category->CategoryName FROM Film WHERE (PlayingNow = 1) AND (Category = :P1) ORDER BY TicketsSold DESC GO ``` ``` -- SQL statement(s) from Class Method Aviation.EventCube.Fact.%Count #import Aviation.EventCube SELECT COUNT(*) INTO :tCount FROM Aviation_EventCube.Fact GO ``` 这个缓存的查询列表可以用作查询优化计划实用程序的输入。 # 执行缓存查询 - 从动态SQL:`%SQL.Statement`准备操作(`%PrepareClassQuery()`或`%ExecDirect()`)创建缓存查询。使用同一实例的动态`SQL%Execute()`方法执行最近准备的缓存查询。 - 从终端:可以使用`$SYSTEM.SQL`类的`ExecuteCachedQuery()`方法直接执行缓存查询。此方法允许指定输入参数值并限制要输出的行数。可以从终端命令行执行动态SQL`%SQL.Statement`缓存查询或xDBC缓存查询。此方法主要用于测试有限数据子集上的现有缓存查询。 - 在管理门户SQL界面中:按照上面的“显示缓存的查询”说明进行操作。从所选缓存查询的目录详细资料选项卡中,单击执行链接。 # 缓存查询锁 在更新缓存的查询元数据时,发出`PREPARE`或`PURCESS`语句会自动请求独占的系统范围锁。SQL支持`$SYSTEM.SQL.Util.SetOption()`方法的系统范围`CachedQueryLockTimeout`选项。此选项控制在尝试获取对缓存查询元数据的锁定时的锁定超时。默认值为120秒。这比标准的SQL锁定超时(默认为10秒)要长得多。系统管理员可能需要在具有大量并发准备和清除操作的系统上修改此缓存查询锁定超时,尤其是在执行涉及大量(数千)缓存查询的批量清除的系统上。 `SET status=$SYSTEM.SQL.Util.SetOption("CachedQueryLockTimeout",seconds,.oldval)`方法设置系统范围的超时值: ```java SetCQTimeout SET status=$SYSTEM.SQL.Util.SetOption("CachedQueryLockTimeout",150,.oldval) WRITE oldval," initial value cached query seconds",!! SetCQTimeoutAgain SET status=$SYSTEM.SQL.Util.SetOption("CachedQueryLockTimeout",180,.oldval2) WRITE oldval2," prior value cached query seconds",!! ResetCQTimeoutToDefault SET status=$SYSTEM.SQL.Util.SetOption("CachedQueryLockTimeout",oldval,.oldval3) ``` `CachedQueryLockTimeout`设置系统范围内所有新进程的缓存查询锁定超时。它不会更改现有进程的缓存查询锁定超时。 # 清除缓存的查询 每当修改(更改或删除)表定义时,基于该表的任何查询都会自动从本地系统上的查询缓存中清除。如果重新编译持久类,则使用该类的任何查询都会自动从本地系统上的查询缓存中清除。 可以使用清除缓存查询选项之一通过管理门户显式清除缓存查询。可以使用SQL命令`PURGE Cached Queries`显式清除缓存查询。可以使用SQL Shell清除命令显式清除缓存查询。 可以使用`$SYSTEM.SQL.Push(N)`方法显式清除最近未使用的缓存查询。指定`n`天数将清除当前命名空间中在过去n天内未使用(准备)的所有缓存查询。将`n`值指定为`0`或`“”`将清除当前命名空间中的所有缓存查询。例如,如果在2018年5月11日发出`$SYSTEM.SQL.Push(30)`方法,则它将仅清除在2018年4月11日之前最后准备的缓存查询。不会清除恰好在30天前(在本例中为4月11日)上次准备的缓存查询。 还可以使用以下方法清除缓存的查询: - `$SYSTEM.SQL.PurgeCQClass()`按名称清除当前命名空间中的一个或多个缓存查询。可以将缓存的查询名称指定为逗号分隔的列表。缓存查询名称区分大小写;命名空间名称必须以全大写字母指定。指定的缓存查询名称或缓存查询名称列表必须用引号引起来。 - `$SYSTEM.SQL.PurgeForTable()`清除当前命名空间中引用指定表的所有缓存查询。架构和表名称不区分大小写。 - `$SYSTEM.SQL.PurgeAllNamespaces()`清除当前系统上所有名称空间中的所有缓存查询。请注意,删除命名空间时,不会清除与其关联的缓存查询。执行`PurgeAllNamespaces()`检查是否有任何与不再存在的名称空间相关联的缓存查询;如果有,则清除这些缓存查询。 要清除当前命名空间中的所有缓存查询,请使用管理门户清除此命名空间的所有查询选项。 清除缓存的查询还会清除相关的查询性能统计信息。 清除缓存的查询还会清除相关的SQL语句列表条目。管理门户中列出的SQL语句可能不会立即清除,可能需要按清除陈旧按钮才能从SQL语句列表中清除这些条目。 **注意:当您更改系统范围的默认架构名称时,系统会自动清除系统上所有名称空间中的所有缓存查询。** ## 远程系统 在本地系统上清除缓存的查询不会清除该缓存查询在镜像系统上的副本。 必须手动清除远程系统上已清除的缓存查询的副本。 当修改和重新编译持久性类时,基于该类的本地缓存查询将被自动清除。 IRIS不会自动清除远程系统上缓存的查询的副本。 这可能意味着远程系统上缓存的一些查询是“过时的”(不再有效)。 但是,当远程系统尝试使用缓存的查询时,远程系统会检查查询引用的任何持久类是否已重新编译。 如果重新编译了本地系统上的持久化类,则远程系统在尝试使用它之前会自动清除并重新创建过时的缓存查询。 # 没有缓存的SQL命令 以下非查询SQL命令不会缓存;它们在使用后会立即清除: - 数据定义语言(DDL):`CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`, `CREATE VIEW`, `ALTER VIEW`, `DROP VIEW`, `CREATE INDEX`, `DROP INDEX`, `CREATE FUNCTION`, `CREATE METHOD`, `CREATE PROCEDURE`, `CREATE QUERY`, `DROP FUNCTION`, `DROP METHOD`, `DROP PROCEDURE`, `DROP QUERY`, `CREATE TRIGGER`, `DROP TRIGGER`, `CREATE DATABASE`, `USE DATABASE`, `DROP DATABASE` - 用户、角色和权限:`CREATE USER`, `ALTER USER`, `DROP USER`, `CREATE ROLE`, `DROP ROLE`, `GRANT`, `REVOKE`, `%CHECKPRIV` - 锁 :`LOCK TABLE`, `UNLOCK TABLE` - 其他: `SAVEPOINT`, `SET OPTION` 请注意,如果从管理门户执行查询界面发出这些SQL命令之一,性能信息将包括如下文本:缓存查询:`%sqlcq.USER.cls16`。这将显示在中,表示已分配缓存的查询名称。但是,此缓存查询名称不是链接。未创建缓存查询,并且未保留增量缓存查询编号`.cls16`。 SQL将此缓存的查询号分配给下一个发出的SQL命令。
文章
Michael Lei · 五月 10, 2021

通过深度学习解释和研究 Covid-19 X 射线分类器

关键字:深度学习,Grad-CAM,X 射线,Covid-19,HealthShare,IRIS ## **目的** 在复活节周末,我谈到了[一些针对 Covid-19 肺的深度学习分类器](https://community.intersystems.com/post/run-some-covid-19-lung-x-ray-classification-and-ct-detection-demos)。  演示结果还算不错,似乎与当时有关该主题的一些[学术研究刊物](https://arxiv.org/ftp/arxiv/papers/2004/2004.02731.pdf)相吻合。 但它真的足够“好”吗?  最近,我偶然收听了一个关于“机器学习中的可解释性”的在线午餐网络讲座,Don 在演讲的最后谈到了这个分类结果: ![](/sites/default/files/inline/images/images/wrong_classifier.png) 上图也出现在 [“Why Should I Trust You?” Explaining the Predictions of Any Classifier](https://arxiv.org/pdf/1602.04938.pdf) 这篇研究论文中。   我们可以看到,分类器实际上经过训练,以背景像素(如雪等野生环境)作为主要输入,对宠物狗和野狼进行分类。  这关乎我过去的兴趣,现在也激起一些好奇: * 我们如何“观察”这些通常以“黑盒”形式表示的 Covid-19 分类器,了解哪些像素实际上促成了“Covid-19 肺”结果? * 在这种情况下,我们可以利用的最简单的形式或工具是什么?  这也是篇简单的 10 分钟笔记。 最后,我会谈到为什么它也与我们即将推出的全新 IRIS 和 HealthShare 功能有关。 ## **范围** 幸运的是,过去几年中,各种 CNN 衍生分类器都有了方便的工具: * [CAM(类激活图)](https://arxiv.org/abs/1512.04150):其应用在[这里](http://cnnlocalization.csail.mit.edu/)和[这里](https://towardsdatascience.com/demystifying-convolutional-neural-networks-using-class-activation-maps-fe94eda4cef1)得到了充分解释。   * [Grad-CAM(梯度加权类激活)](https://arxiv.org/pdf/1610.02391.pdf):这是 [CAM 更通用的版本,让我们能够观察整个模型中的任何 CNN 层](https://towardsdatascience.com/demystifying-convolutional-neural-networks-using-gradcam-554a85dd4e48)。 我们将使用 **Grad-CAM** 对我们上一篇帖子中的 Covid-19 肺分类器进行快速演示。 **"Tensorflow 2.2.0rc + Jupyter"** Docker 在配备 Nvidia T4 GPU 的 AWS Ubuntu 16.04 服务器上使用。   TensorFlow 2 提供了简单的**梯度带**实现。  这是我在 Ubuntu 服务器上启动的快速笔记:   docker run -itd --runtime=nvidia  -v /zhong/tf/:/tf  -p 8896:8888 -p 6026:6006 --name tf-gpu2 tensorflow/tensorflow:2.2.0rc2-gpu-py3-jupyter   ## **方法** 您可以放心地在此处忽略以上 Grad-CAM 研究出版物中引用的数字。 这里引用这些数字只是为了对后面使用的 Python 代码进行连续的[原始提案(第 4 页和第 5 页)](https://arxiv.org/pdf/1610.02391.pdf)交叉检查,也希望能提供更好的结果透明度。 ![](/sites/default/files/inline/images/images/gradient_cam1.png)                                                              ![](/sites/default/files/inline/images/images/gradient_cam2.png) (1):为了得到任意类 c 的宽度 u 和高度 v 的类判别定位图,我们首先计算类 c 的得分相对于卷积层的特征图 Ak 的梯度 yc(softmax 前)。 这些回流的梯度被全局平均池化,得到目标类的神经元重要性权重 ak。 (2):计算出目标类 c 的 ak 后,我们对激活图进行加权组合,然后执行 ReLU。  这就得到了与卷积特征图大小相同的粗略热图。     ## **测试** 现在,我们来尝试一下目前能找到的最简单的编码: **1. 导入软件包** import tensorflow as tf;print(tf.__version__) 2.2.0-rc2 import tensorflow as tfimport tensorflow.keras.backend as Kfrom tensorflow.keras.applications.inception_v3 import InceptionV3from tensorflow.keras.preprocessing import imagefrom tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictionsimport numpy as npimport osimport imutilsimport matplotlib.pyplot as pltimport cv2   **2. 加载我们[之前训练和保存的模型](https://community.intersystems.com/post/run-some-covid-19-lung-x-ray-classification-and-ct-detection-demos)** new_model = tf.keras.models.load_model('saved_model/inceptionV3')<br>new_model.summary() 可以看到,在最终的全局平均池化之前,模型中 4D 的最后一个 CNN 层被称为“mixed10”。    **3. 计算 Grad-CAM 热图**   下面是一个实现了上述 Grad-CAM 公式 (1) 和 (2) 的简单版本热图。  这篇帖子对其做出了解释。  with tf.GradientTape() as tape:<br>  last_conv_layer = model.get_layer('mixed10') <br>  iterate = tf.keras.models.Model([model.inputs], [model.output, last_conv_layer.output])<br>  model_out, last_conv_layer = iterate(testX)<br>  class_out = model_out[:, np.argmax(model_out[0])]<br>  grads = tape.gradient(class_out, last_conv_layer)<br>  pooled_grads = K.mean(grads, axis=(0, 1, 2))  heatmap = tf.reduce_mean(tf.multiply(pooled_grads, last_conv_layer), axis=-1)  在我们的示例中,它将生成一个热图 NumPy 数组 (27, 6, 6)。 然后,我们可以将它重新调整为原始 X 射线图像尺寸并叠加在 X 射线图像上方。 不过,在这种情况下,我们将使用略详细的版本,这篇帖子对此也有很好的解释。 它组成了一个函数,Grad-CAM 热图已调整为原始 X 射线图的大小: # import the necessary packages<br>from tensorflow.keras.models import Model<br>import tensorflow as tf<br>import numpy as np<br>import cv2 class GradCAM:<br>    def __init__(self, model, classIdx, layerName=None):<br>        self.model = model<br>        self.classIdx = classIdx<br>        self.layerName = layerName<br>        if self.layerName is None:<br>            self.layerName = self.find_target_layer()     def find_target_layer(self):<br>        for layer in reversed(self.model.layers):<br>            # check to see if the layer has a 4D output<br>            if len(layer.output_shape) == 4:<br>                return layer.name        raise ValueError("Could not find 4D layer. Cannot apply GradCAM.")     def compute_heatmap(self, image, eps=1e-8):<br>        gradModel = Model(<br>            inputs=[self.model.inputs],<br>            outputs=[self.model.get_layer(self.layerName).output,<br>                self.model.output])        # record operations for automatic differentiation<br><strong>        with tf.GradientTape() as tape:<br>            inputs = tf.cast(image, tf.float32)<br>            (convOutputs, predictions) = gradModel(inputs)<br>            loss = predictions[:, self.classIdx]</strong>        # use automatic differentiation to compute the gradients<br>        grads = tape.gradient(loss, convOutputs)        # compute the guided gradients<br>        castConvOutputs = tf.cast(convOutputs > 0, "float32")<br>        castGrads = tf.cast(grads > 0, "float32")<br>        guidedGrads = castConvOutputs * castGrads * grads        convOutputs = convOutputs[0]<br>        guidedGrads = guidedGrads[0]        weights = tf.reduce_mean(guidedGrads, axis=(0, 1))<br>        cam = tf.reduce_sum(tf.multiply(weights, convOutputs), axis=-1)         # resize the heatmap to oringnal X-Ray image size<br>        (w, h) = (image.shape[2], image.shape[1])<br>        heatmap = cv2.resize(cam.numpy(), (w, h))         # normalize the heatmap<br>        numer = heatmap - np.min(heatmap)<br>        denom = (heatmap.max() - heatmap.min()) + eps<br>        heatmap = numer / denom<br>        heatmap = (heatmap * 255).astype("uint8")         # return the resulting heatmap to the calling function<br>        return heatmap   **4. 加载 Covid-19 肺部 X 射线图** 现在,加载一个从未在模型训练和验证过程中使用过的测试 X 射线图。 (也已上传到上一篇帖子中) filename = './test/nejmoa2001191_f1-PA.jpeg'orignal = cv2.imread(filename)plt.imshow(orignal)plt.show() 调整为 256 x 256,归一化为像素值在 0.0 到 1.0 之间的 numpy 数组“dataXG”。 orig = cv2.cvtColor(orignal, cv2.COLOR_BGR2RGB)<br>resized = cv2.resize(orig, (256, 256))<br>dataXG = np.array(resized) / 255.0<br>dataXG = np.expand_dims(dataXG, axis=0)   **5. 进行快速分类 **  现在可以调用上面新加载的模型进行快速预测: preds = new_model.predict(dataXG)<br>i = np.argmax(preds[0])<br>print(i, preds) 0     [[0.9171522  0.06534185 0.01750595]] 因此它被归类为 0 型 - Covid-19 肺,概率为 0.9171522。 **6. 计算 Grad-CAM 热图** # Compute the heatmap based on step 3<br>cam = GradCAM(model=new_model, classIdx=i, layerName='mixed10') # find the last 4d shape "mixed10" in this case<br>heatmap = cam.compute_heatmap(dataXG) #show the calculated heatmapplt.imshow(heatmap)plt.show() **7. 在原始 X 射线图上显示热图** # Old fashioned way to overlay a transparent heatmap onto original image, the same as above<br>heatmapY = cv2.resize(heatmap, (orig.shape[1], orig.shape[0]))<br>heatmapY = cv2.applyColorMap(heatmapY, cv2.COLORMAP_HOT)  # COLORMAP_JET, COLORMAP_VIRIDIS, COLORMAP_HOT<br>imageY = cv2.addWeighted(heatmapY, 0.5, orignal, 1.0, 0)<br>print(heatmapY.shape, orig.shape) # draw the orignal x-ray, the heatmap, and the overlay together<br>output = np.hstack([orig, heatmapY, imageY])<br>fig, ax = plt.subplots(figsize=(20, 18))<br>ax.imshow(np.random.rand(1, 99), interpolation='nearest')<br>plt.imshow(output)<br>plt.show() (842, 1090, 3) (842, 1090, 3) 这似乎表明我们的 Covid-19 演示分类器“相信”患者的“右侧气管旁带”周围出现了一些“浑浊”问题? 我不是很明白,这要请教真正的放射科医生。 那么,接下来再尝试一些从现实世界案例提交到 GitHub 仓库中的测试图像: filename = './test/1-s2.0-S0929664620300449-gr2_lrg-b.jpg' 0    [[9.9799889e-01 3.8319459e-04 1.6178709e-03]] 这似乎也是合理的 Covid-19 解释,表明问题更多地发生在左心线区域?   再试试另一个随机测试 X 射线图: filename = '../Covid_M/all/test/covid/radiol.2020200490.fig3.jpeg' 0      [[0.9317619  0.0169084  0.05132957]] 没想到这并不完全正确,但看起来好像也不算太离谱?  它显示了两个问题区域,左侧为主要问题,右侧为部分问题,这与放射科医师的标记应该是有些对应的? (希望它不是在人类标记上训练 - 这是可解释性问题的另一个层面)。 我要在这里打住了,我猜不会有太多人对 X 射线感兴趣。 ##   ## **原因** 我个人对“可解释性”和“可理解性”以及相关技术方法的重要性有着深切的体会。  在此领域的任何尝试都是值得的,无论它有多么微不足道。 最终,“数据公平”、“数据公正”和“数据信任”将建立在其数字经济过程透明化的基础之上。  另外,它现在开始能为人所用了。     25 年前,当年轻的我在 1995 年的夏天忙着写博士论文的时候,我甚至不指望对被广泛用作黑盒的所谓“神经网络”有任何了解。 当时的 AI 更像是逻辑推理机“专家系统”,“神经网络”只是被称为“神经网络”,而“深度学习”尚未诞生。 现在,[越来越多的研究和工具](https://towardsdatascience.com/an-overview-of-model-explainability-in-modern-machine-learning-fc0f22c8c29a)不断涌现,可供 AI 开发者轻松使用。  最后,具体到这个演示,我很欣赏这种工具的一点是,它甚至不需要以像素级的标记作为起点,而是试图自动生成肺部病变区域,实现一种类似半自动标记的效果。 这在实际应用中是有意义的。   我记得去年,我的一位放射科医生朋友为了帮助我获取一些骨折数据,一遍又一遍地生成 U-Net 训练的像素标签,这很伤眼睛。    ## **未来计划** 回到正题。 得益于过去 10 多年深度学习的快速发展,医学影像已成为 AI 领域比较成熟的方向。 这是值得我们深入研究的。 不过,接下来如果有时间,我希望我们能在 NLP 方面多做一些尝试。      ## **致谢** 所有来源均已根据需要插入上述文本。 如有其他需要,我还将提供更多引用。     ### **免责声明**:  再次说明,我现在写这篇快速笔记是为了防止相关信息随时间遗失。 本文完全是出自“开发者”角度的个人观点。 内容和文本随时可能会被更改或完善。 以上内容主要是展示技术思想和方法,而不是临床解释。临床解释需要放射科专家根据大量优质数据建立黄金规则。  
文章
姚 鑫 · 十一月 9, 2021

第七十一章 SQL命令 SELECT(三)

# 第七十一章 SQL命令 SELECT(三) ## 列别名 指定`SELECT-ITEM`时,可以使用AS关键字指定列名的别名: ```sql SELECT Name AS PersonName, DOB AS BirthDate, ... ``` 列别名在结果集中显示为列标题。指定列别名是可选的;始终提供默认值。列别名以指定的字母大小写显示;但是,当在`ORDER BY`子句中引用时,它不区分大小写。`C`别名必须是有效的标识符。`C`别名可以是分隔的标识符。使用带分隔符的标识符允许列别名包含空格、其他标点符号或作为`SQL`保留名称。例如,`SELECT Name AS "Customer Name" or SELECT Home_State AS "From"`。 As关键字不是必需的,但使查询文本更易于阅读。因此,以下也是有效的语法: ```sql SELECT Name PersonName, DOB BirthDate, ... ``` SQL不执行列别名的惟一性检查。 字段列和列别名可能具有相同的名称(尽管不可取),或者两个列别名相同。 当`ORDER by`子句引用此类非惟一列别名时,可能会导致`SQLCODE -24“Ambiguous sort column”`错误。 列别名与所有SQL标识符一样,不区分大小写。 其他`SELECT`子句中列别名的使用由查询语义处理顺序控制。 可以通过`ORDER by`子句中的列别名引用列。 不能在选择列表中的另一个选择项、`DISTINCT BY`子句、`WHERE`子句、`GROUP BY`子句或`HAVING`子句中引用列别名。 不能在`JOIN`操作的`ON`子句或`USING`子句中引用列别名。 但是,可以使用子查询使列别名可用来供其他这些其他`SELECT`子句使用。 ## 字段列别名 选择项字段名不区分大小写。 但是,除非提供列别名,否则结果集中的字段列的名称应遵循与列属性相关联的`SqlFieldName`的字母大小写。 `SqlFieldName`的大小写对应于表定义中指定的字段名,而不是选择项列表中指定的字段名。 因此,`SELECT name FROM Sample.Person`返回字段列标签为`Name`。 使用字段列别名可以指定要显示的字母大小写,示例如下: ```sql SELECT name,name AS NAME FROM Sample.Person ``` 字母大小写解析需要时间。 为了最大化`SELECT`性能,您可以指定字段名的确切字母大小写,如表定义中所指定的那样。 但是,在表定义中确定字段的确切字母大小写通常很不方便,而且容易出错。 相反,可以使用字段列别名来避免字母大小写问题。 注意,对字段列别名的所有引用必须以字母大小写匹配。 下面的动态SQL示例需要字母大小写解析(`SqlFieldNames`为`" Latitude "`和`" Longitude "`): ```java ClassMethod Select() { s myquery = "SELECT latitude,longitude FROM Sample.USZipCode" s tStatement = ##class(%SQL.Statement).%New() s qStatus = tStatement.%Prepare(myquery) if qStatus '= 1 { w "%Prepare failed:" d $System.Status.DisplayError(qStatus) q } s rset = tStatement.%Execute() while rset.%Next() { w rset.latitude," ",rset.longitude,! } } ``` 下面的动态SQL示例不需要区分大小写,因此执行得更快: ```java ClassMethod Select1() { s myquery = "SELECT latitude AS northsouth,longitude AS eastwest FROM Sample.USZipCode" s tStatement = ##class(%SQL.Statement).%New() s qStatus = tStatement.%Prepare(myquery) if qStatus '= 1 { w "%Prepare failed:" d $System.Status.DisplayError(qStatus) q } s rset = tStatement.%Execute() while rset.%Next() { w rset.northsouth," ",rset.eastwest,! } } ``` 列名中不包含`t-alias`表别名前缀。 因此,在下面的示例中,两列都被标记为`Name`: ```sql SELECT p.Name,e.Name FROM Sample.Person AS p LEFT JOIN Sample.Employee AS e ON p.Name=e.Name ``` 要区分指定多个表的查询中的列,您应该指定列别名: ```sql SELECT p.Name AS PersonName,e.Name AS EmployeeName FROM Sample.Person AS p LEFT JOIN Sample.Employee AS e ON p.Name=e.Name ``` 提供列别名以使数据更容易理解。 以表中“Home_State”列为例,将其重命名为“US_State_Abbrev”。 ```sql SELECT Name,Home_State AS US_State_Abbrev FROM Sample.Person ``` 请注意,`%ID`引用特定的列,因此返回字段名(默认为`ID`)或指定的列别名,如下面的示例所示: ```sql SELECT %ID,%ID AS Ident,Name FROM Sample.Person ``` ## Non-Field列别名 非字段列将自动分配一个列名。 如果没有为这些字段提供别名, SQL将提供一个惟一的列名,如`“Expression_1”`或`“Aggregate_3”`。 整数后缀指`SELECT`语句中指定的选择项位置(选择项列号)。 它们不是该类型字段的计数。 下面是自动分配的列名(n是一个整数)。 这些内容的顺序越来越广泛。 例如,在数字上添加加号或减号将其从`HostVar`提升为表达式; 连接`HostVar`和`Literal`将其提升为表达式; 在子查询中指定`Literal`、`HostVar`、`Aggregate`或`Expression`将其提升为子查询: - `Literal_n`:一个伪字段变量,比如`%TABLENAME`,或者`NULL`说明符。 注意`%ID`不是`Literal_n`; 它得到实际`RowID`字段的列名。 - `HostVar_n`:主机变量。 这可能是一个字面量,如`' text '`, `123`,或空字符串(`"`),一个输入变量(`:myvar`),或? 由文字替换的输入参数。 请注意,任何对字面量的表达式求值,如在数字后附加符号、字符串连接或算术操作,都使其成为`Expression_n`。 提供给? 参数不受表达式求值影响而返回。 例如,提供`5+7`将返回字符串`'5+7'`作为`HostVar_n`。 - `Aggregate_n`:聚合函数,如`AVG(Age)`、`COUNT(*)`。 如果最外层的操作是聚合函数,那么列就被命名为`Aggregate_n`,即使这个聚合包含一个表达式。 例如,`COUNT(Name)+COUNT(Spouse)`是`Expression_n`,而`MAX(COUNT(Name)+COUNT(Spouse))`是`Aggregate_n`, -`AVG(Age)`是`Expression_n`,而`AVG(-Age)`是`Aggregate_n`。 - `Expression_n`:在文本、字段或`Aggregate_n`、`HostVar_n`、`Literal_n`或`Subquery_n`选择项列表中的任何操作都会将其列名更改为`Expression_n`。 这包括对数字的一元操作(`-Age`),算术操作(`Age+5`),连接(`'USA:'||Home_State`),数据类型`CAST`操作,SQL排序函数(`%SQLUPPER(Name)`或`%SQLUPPER Name)`, SQL标量函数(`$LENGTH(Name)`),用户定义的类方法,`CASE`表达式,和特殊变量(如`CURRENT_DATE`或`$ZPI`)。 - `Window_n`:窗口函数的结果。 在`OVER`关键字的右括号之后指定列别名。 - `Subquery_n`:指定单个选择项的子查询的结果。 选择项可以是字段、聚合函数、表达式或文字。 在子查询之后而不是在子查询中指定列别名。 在下面的例子中,`AVG`函数创建的聚合字段列的别名是`“AvgAge”`; 它的默认名称是`“Aggregate_3”`(一个在`SELECT`列表中位置3的聚合字段)。 ```sql SELECT Name, Age, AVG(Age) AS AvgAge FROM Sample.Person ``` 下面的示例与上一个示例相同,只是此处省略了AS关键字。 建议使用该关键字,但不是必需的。 ```sql SELECT Name, Age, AVG(Age) AvgAge FROM Sample.Person ``` 下面的示例演示如何为选择项子查询指定列别名: ```sql SELECT Name AS PersonName, (SELECT Name FROM Sample.Employee) AS EmpName, Age AS YearsOld FROM Sample.Person ``` # FROM子句 `FROM table-ref`子句指定一个或多个表、视图、表值函数或子查询。 可以将这些`table-ref`类型的任意组合指定为逗号分隔列表或使用`JOIN`语法。 如果指定单个`table-ref`,则从该表或视图检索指定的数据。 如果指定多个表引用,SQL将对这些表执行连接操作,将它们的数据合并到一个结果表中,从这个结果表中检索指定的数据。 如果指定了多个`table-ref`,可以用逗号或显式连接语法关键字分隔这些表名。 可以使用`$SYSTEM.SQL.Schema.TableExists("schema.tname")`或`$SYSTEM.SQL.Schema.ViewExists("schema.vname")`方法来确定当前名称空间中是否存在表或视图。 可以使用`$SYSTEM.SQL.Security.CheckPrivilege()`方法来确定是否对该表或视图具有`SELECT`权限。 ## 表的别名 当指定`table-ref`时,可以使用AS关键字指定该表名或视图名的别名: ```sql FROM Sample.Person AS P ``` `AS`关键字不是必需的,但使查询文本更容易阅读。 下面是有效的等价语法: ```sql FROM Sample.Person P ``` `t-alias`名称必须是有效的标识符。 别名可以是分隔的标识符。 `t-alias`在查询中的表别名之间必须是唯一的。 与所有标识符一样,`t-alias`不区分大小写。 因此,不能指定两个只有字母大小写不同的`t-alias`名称。 这将导致`SQLCODE -20`“名称冲突”错误。 表别名用作字段名的前缀(带句点),以指示字段所属的表。 例如: ```sql SELECT P.Name, E.Name FROM Sample.Person AS P, Sample.Employee AS E ``` 当查询指定多个具有相同字段名的表时,必须使用表引用前缀。 表引用前缀可以是`t-alias`如上所示),也可以是全限定表名,如下面的等价示例所示: ```sql SELECT Sample.Person.Name, Sample.Employee.Name FROM Sample.Person, Sample.Employee ``` 但是,如果已为该表名分配了`t-alias`,则不能将完整表名作为该选择项的一部分。 尝试这样做会导致`SQLCODE -23`错误。 当查询仅引用一个表(或视图)时,可选择指定表别名。 当查询引用多个表(和/或视图)且引用的字段名对每个表都是唯一的时,指定表别名是可选的(但推荐)。 当查询引用多个表(和/或视图),并且在不同的表中引用的字段名相同时,需要指定表别名。 没有指定`t-alias`(或完全限定的表名)前缀将导致`SQLCODE -27`“字段`%1D`在适用的表中不明确”错误。 当指定如下子查询时,可以使用t-alias,但不是必需的: ```sql SELECT Name,(SELECT Name FROM Sample.Vendor) FROM Sample.Person ``` `t-alias`仅唯一标识查询执行的字段; 要惟一地标识用于显示查询结果集的字段,还必须使用列别名(`c-alias`)。 下面的示例使用了表别名(`Per`和`Emp`)和列别名(`PName`和`Ename`): ```sql SELECT Per.Name AS PName, Emp.Name AS EName FROM Sample.Person AS Per, Sample.Employee AS Emp WHERE Per.Name %STARTSWITH 'G' ``` 可以为字段、列别名和/或表别名使用相同的名称,而不会产生命名冲突。 如果需要区分引用的是哪个表,则使用`t-alias`前缀。 以下是一些例子: ```sql SELECT P.%ID As PersonID, AVG(P.Age) AS AvgAge, Z.%TABLENAME||'=' AS Tablename, Z.* FROM Sample.Person AS P, Sample.USZipCode AS Z WHERE P.Home_City = Z.City GROUP BY P.Home_City ORDER BY Z.City ``` ## Sharding Transparent to SELECT Queries 分片对SQL查询是透明的; 不需要特殊的查询语法。 查询不需要知道`FROM`子句中指定的表是分片的还是非分片的。 同一个查询可以访问分片表和非分片表。 查询可以包括分片表和非分片表之间的连接。 分片表使用`CREATE table`命令定义。 它必须在分片主数据服务器上的主命名空间中定义。 这个主命名空间还可以包括非分片表。
文章
Michael Lei · 四月 24, 2022

基于Docker的Apache Web Gateway

# 基于Docker的Apache Web Gateway Hi 社区 在本文中,我们将基于Docker程序化地配置一个Apache Web Gateway,使用。: * HTTPS protocol. * TLS\SSL to secure the communication between the Web Gateway and the IRIS instance. ![image](/sites/default/files/inline/images/net-schema-01.png) 我们将使用两个镜像:一个用于Web网关,第二个用于IRIS实例。 所有必需的文件都在这 [GitHub repository](https://github.com/lscalese/docker-webgateway-sample). 我们从git clone开始: ```bash git clone https://github.com/lscalese/docker-webgateway-sample.git cd docker-webgateway-sample ``` ## 准备系统 为了避免权限方面的问题,你的系统需要一个用户和一个组: * www-data * irisowner 需要与容器共享证书文件。 如果你的系统中不存在这些文件,只需执行: ```bash sudo useradd --uid 51773 --user-group irisowner sudo groupmod --gid 51773 irisowner sudo useradd –user-group www-data ``` ## 生成证书 在这个示例中,我们使用以下三个证书: 1. HTTPS web server usage. 2. TLS\SSL encryption on Web Gateway client. 3. TLS\SSL encryption on IRIS Instance. 有一个随时可用的脚本来生成它们。. 然而,你应该自定义证书的主题;只需编辑这个文件 [gen-certificates.sh](https://github.com/lscalese/docker-webgateway-sample/blob/master/gen-certificates.sh) . 这是 OpenSSL `subj` argument的结构: 1. **C**: Country code 2. **ST**: State 3. **L**: Location 4. **O**: Organization 5. **OU**: Organization Unit 6. **CN**: Common name (basically the domain name or the hostname) 可以随意改动这些值. ```bash # sudo is needed due chown, chgrp, chmod ... sudo ./gen-certificates.sh ``` 如果一切都OK,应该能看到两个带证书的新目录 `./certificates/` and `~/webgateway-apache-certificates/` with certificates: | File | Container | Description | |--- |--- |--- | | ./certificates/CA_Server.cer | webgateway,iris | Authority server certificate| | ./certificates/iris_server.cer | iris | Certificate for IRIS instance (used for mirror and wegateway communication encryption) | | ./certificates/iris_server.key | iris | Related private key | | ~/webgateway-apache-certificates/apache_webgateway.cer | webgateway | Certificate for apache webserver | | ~/webgateway-apache-certificates/apache_webgateway.key | webgateway | Related private key | | ./certificates/webgateway_client.cer | webgateway | Certificate to encrypt communication between webgateway and IRIS | | ./certificates/webgateway_client.key | webgateway | Related private key | 请记住,如果有自签名的证书,浏览器会显示安全警报。 显然,如果你有一个由认证机构交付的证书,你可以用它来代替自签的证书(尤其是Apache服务器证书) ## Web Gateway 配置文件 让我们来看看配置文件. ### CSP.INI 你能看到在 `webgateway-config-files` 目录下 CSP.INI 文件. 将被推到镜像里, 但内容可以在runtime被修改. 可以把这个文件作为模版. 在这个示例中,以下参数将在容器启动时被覆盖: * Ip_Address * TCP_Port * System_Manager 更多细节请参考 [startUpScript.sh](https://github.com/lscalese/docker-webgateway-sample/blob/master/startUpScript.sh) . 大致上,替换是通过`sed`命令行进行的. 同时, 这个文件包含 SSL\TLS 配置来确保与 IRIS 实例的通信: ``` SSLCC_Certificate_File=/opt/webgateway/bin/webgateway_client.cer SSLCC_Certificate_Key_File=/opt/webgateway/bin/webgateway_client.key SSLCC_CA_Certificate_File=/opt/webgateway/bin/CA_Server.cer ``` 这些语句都比较重要. 我们必需确保证书文件可用. 我们稍后将在`docker-compose`文件中用一个卷来做这件事. ### 000-default.conf 这是一个Apache 配置文件. 允许使用HTTPS协议并将HTTP请求重定向到HTTPS. 证书和私钥文件在这个文件里设置: ``` SSLCertificateFile /etc/apache2/certificate/apache_webgateway.cer SSLCertificateKeyFile /etc/apache2/certificate/apache_webgateway.key ``` ## IRIS 实例 对我们 IRIS实例, 我们仅仅配置最低要求来允许SSL\TLS 和Web Gateway 之间的通信; 这涉及到: 1. `%SuperServer` SSL Config. 2. Enable SSLSuperServer security setting. 3. Restrict the list of IPs that can use the Web Gateway service. 为简化配置, config-api 用一个简单的JSON 配置文件. ```json { "Security.SSLConfigs": { "%SuperServer": { "CAFile": "/usr/irissys/mgr/CA_Server.cer", "CertificateFile": "/usr/irissys/mgr/iris_server.cer", "Name": "%SuperServer", "PrivateKeyFile": "/usr/irissys/mgr/iris_server.key", "Type": "1", "VerifyPeer": 3 } }, "Security.System": { "SSLSuperServer":1 }, "Security.Services": { "%Service_WebGateway": { "ClientSystems": "172.16.238.50;127.0.0.1;172.16.238.20" } } } ``` 不需要做任何动作. 在容器启动时这个配置会自动加载. ## tls-ssl-webgateway 镜像 ### dockerfile ``` ARG IMAGEWEBGTW=containers.intersystems.com/intersystems/webgateway:2021.1.0.215.0 FROM ${IMAGEWEBGTW} ADD webgateway-config-files /webgateway-config-files ADD buildWebGateway.sh / ADD startUpScript.sh / RUN chmod +x buildWebGateway.sh startUpScript.sh && /buildWebGateway.sh ENTRYPOINT ["/startUpScript.sh"] ``` 默认的 entry point是 `/startWebGateway`, 但是在启动webserver前需要执行一些操作. 记住我们的 CSP.ini 文件只是个 `模版`, 并且我们需要在启动时改变一些参数 (IP, port, system manager) . `startUpScript.sh` 将执行这些变化并启动初始 entry point 脚本 `/startWebGateway`. ## 启动容器 ### docker-compose 文件 启动容器之前, 必须修改好`docker-compose.yml` 文件: * `**SYSTEM_MANAGER**` 必须配好授权的IP来访问 **Web Gateway Management** https://localhost/csp/bin/Systems/Module.cxw 基本就是你自己的IP地址 (可以是一个用逗号分开的列表). * `**IRIS_WEBAPPS**` 必须配好 CSP 应用列表. 这个表用空格隔开, 例如: `IRIS_WEBAPPS=/csp/sys /swagger-ui`. 默认, 只有 `/csp/sys` 被暴露. * 80和 443 端口映射好. 如果你的系统中已经使用了这些端口,请将调整为其他端口. ``` version: '3.6' services: webgateway: image: tls-ssl-webgateway container_name: tls-ssl-webgateway networks: app_net: ipv4_address: 172.16.238.50 ports: # change the local port already used on your system. - "80:80" - "443:443" environment: - IRIS_HOST=172.16.238.20 - IRIS_PORT=1972 # Replace by the list of ip address allowed to open the CSP system manager # https://localhost/csp/bin/Systems/Module.cxw # see .env file to set environement variable. - "SYSTEM_MANAGER=${LOCAL_IP}" # the list of web apps # /csp allow to the webgateway to redirect all request starting by /csp to the iris instance # You can specify a list separate by a space : "IRIS_WEBAPPS=/csp /api /isc /swagger-ui" - "IRIS_WEBAPPS=/csp/sys" volumes: # Mount certificates files. - ./volume-apache/webgateway_client.cer:/opt/webgateway/bin/webgateway_client.cer - ./volume-apache/webgateway_client.key:/opt/webgateway/bin/webgateway_client.key - ./volume-apache/CA_Server.cer:/opt/webgateway/bin/CA_Server.cer - ./volume-apache/apache_webgateway.cer:/etc/apache2/certificate/apache_webgateway.cer - ./volume-apache/apache_webgateway.key:/etc/apache2/certificate/apache_webgateway.key hostname: webgateway command: ["--ssl"] iris: image: intersystemsdc/iris-community:latest container_name: tls-ssl-iris networks: app_net: ipv4_address: 172.16.238.20 volumes: - ./iris-config-files:/opt/config-files # Mount certificates files. - ./volume-iris/CA_Server.cer:/usr/irissys/mgr/CA_Server.cer - ./volume-iris/iris_server.cer:/usr/irissys/mgr/iris_server.cer - ./volume-iris/iris_server.key:/usr/irissys/mgr/iris_server.key hostname: iris # Load the IRIS configuration file ./iris-config-files/iris-config.json command: ["-a","sh /opt/config-files/configureIris.sh"] networks: app_net: ipam: driver: default config: - subnet: "172.16.238.0/24" ``` Build and start: ```bash docker-compose up -d --build ``` `tls-ssl-iris 和 tls-ssl-webgateway 容器应该启动好了.` ## 测试 Web Access ### Apache 默认页 打开网页 [http://localhost](http://localhost). 你将自动被重定向到[https://localhost](https://localhost). 浏览器显示安全警告. 如果是自签署的证书,这是正常的,接受并继续. ![image](/sites/default/files/inline/images/apache-web-gateway-with-docker-02.png) ### Web Gateway 管理页面 打开 [https://localhost/csp/bin/Systems/Module.cxw](https://localhost/csp/bin/Systems/Module.cxw) 并测试服务器连接. ![image](/sites/default/files/inline/images/apache-web-gateway-with-docker-03.png) ### 管理门户 打开 [https://localhost/csp/sys/utilhome.csp](https://localhost/csp/sys/utilhome.csp) ![image](/sites/default/files/inline/images/apache-web-gateway-with-docker-04.png) 赞! Web Gateway 例子跑起来了! ## IRIS Mirror 与vWeb Gateway 在上一篇文章中,我们建立了一个镜像环境,但网络网关是一个缺失的部分。 现在,我们可以改进这一点。 一个包括Web Gateway和一些更多改进的资源库就可以用了 [iris-miroring-with-webgateway](https://github.com/lscalese/iris-mirroring-with-webgateway) : 1. 证书不再是即时生成的,而是在一个单独的过程中生成的. 2. IP地址被docker-compose和JSON配置文件中的环境变量所取代, 变量被定义在'.env'文件中. 3. 这个repository 可以作为一个模板来使用. 查看 repository文件 [README.md](https://github.com/lscalese/iris-mirroring-with-webgateway) 来运行以下环境: ![image](https://github.com/lscalese/iris-mirroring-with-webgateway/blob/master/img/network-schema-01.png?raw=true)
文章
Jingwei Wang · 七月 21, 2022

DeepSee 的开发 - 第四部分 - 创建主题区

一个主题区是一个子立方体,可以选择覆盖项目的名称。你定义一个主题区是为了使用户能够关注较小的数据集,出于安全原因或其他原因。本章讨论了以下主题。 简介 在本教程中,我们创建了两个主题区域,按邮政编码划分患者: Patient Set A: 居住在邮政编码为32006, 32007, or 36711区域的患者 Patient Set B: 居住在邮政编码为34577 or 38928区域的患者 创建主题领域 要创建主题区域,请做以下工作。 在模型中,点击 "新建"。 选中 "主题区域"。 对于主题区名称,键入Patient Set A 对于主题区的类名,输入 Tutorial.SubjectA 对于基础立方体,点击浏览并选择 Tutorial。 单击 OK。 在一个单独的浏览器标签或窗口中,访问分析器,然后做以下工作。 展开HomeD。 把ZIP Code放到过滤器框中。这就在数据透视表的正上方增加了一个过滤框。 在该过滤框中,点击搜索按钮,然后选择 32006, 32007, 和 36711。 然后点击'为透视表显示当前查询'按钮(笔记本带一个笔的图标) 系统会显示一个对话框,显示分析器所使用的MDX查询。 SELECT FROM [PPatients] %FILTER %OR({[HOMED].[H1].[ZIP CODE].&[32006],[HOMED].[H1].[ZIP CODE].&[32007],[HOMED].[H1].[ZIP CODE].&[36711]}) 将%FILTER后面的文本复制到系统剪贴板上。 点击确定。 在模型中,点击标有Patient Set A的一行。 在详细信息栏中,将复制的文本粘贴到 过滤器 中。 %OR({[HOMED].[H1].[ZIP Code].&[32006],[HOMED].[H1].[ZIP Code].&[32007],[HOMED].[H1].[ZIP Code].&[36711]}) 点击保存,然后点击确定。 编译该主题区。 对于第二个主题区,重复前面的步骤,并作如下改动。 对于课题区名称,键入Patient Set B 对于主题区的类名,键入Tutorial.SubjectB 对另外两个邮政编码重复前面的步骤。因此,对于Filter,使用以下内容。 %OR({[HOMED].[H1].[ZIP Code].&[34577],[HOMED].[H1].[ZIP Code].&[38928]}) 检查主题领域 现在我们检查一下我们所创建的主题领域。 在分析器中,点击左上角立方体按钮,选择Patient Set A。 单击 "确定"。然后分析器显示所选主题区的内容。 注意,总的记录数没有你的Tutorial基本立方体那么高。 在模型内容区,展开HomeD维度,ZIP Code级别,以及City级别。都没有之前的Tutorial基本立方体数据那么多。 对患者组B重复前面的步骤。 当您展开HomeD维度、ZIP Code级别,以及City级别。也没之前的Tutorial基本立方体数据那么多。 常见的过滤器表达式 在这一节中,我们在分析器中试验常见的过滤器,看看它们对生成的查询的影响。 在分析器中,打开Tutorial立方体。 分析器把立方体和主题区都称为主题区。它们之间的正式区别只有在你创建它们时才有意义。 点击新建。 分析器显示计数(记录的计数)。 在添加过滤器之前,让我们看看当前的查询是如何定义的,以便我们有一个比较的基础。 展开ColorD和Favorite Color。 把Orange拖到过滤器。 分析器现在只使用最喜欢的颜色是Orange的患者。 点击‘为透视表显示当前查询’按钮(笔记本加一个笔图标)。然后系统显示以下查询。 SELECT FROM [TUTORIAL] %FILTER [ColorD].[H1].[Favourite Color].&[Orange] %FILTER关键字限制了查询。%FILTER后面的片段是一个过滤表达式。 点击确定。 给过滤器添加另一种颜色。点击过滤器中橙色旁边的X。这样就可以删除该过滤器。 把 "Favourite Color "拖到过滤器中。这就在数据透视表的正上方增加了一个过滤器框。 在该过滤框中,点击搜索按钮(放大镜图标),然后选择橙色和紫色。 系统现在只使用最喜欢的颜色是橙色或最喜欢的颜色是紫色的患者(注意,计数比单独的橙色要高)。 再次显示查询文本。现在你应该看到以下内容。 SELECT FROM [TUTORIAL] %FILTER %OR({[COLORD].[H1].[FAVOURITE COLOR].&[Orange],[COLORD].[H1].[FAVOURITE COLOR].&[Purple]}) 在这种情况下,过滤器的表达式如下。 %FILTER %OR({[COLORD].[H1].[FAVOURITE COLOR].&[Orange],[COLORD].[H1].[FAVOURITE COLOR].&[Purple]}) %OR函数是InterSystems公司的一项优化;该函数的参数是一个集合。 这个集合被大括号{}所包围,由一个逗号分隔的元素列表组成。在这种情况下,该集合包含两个成员表达式。一个集合表达式指的是由该集合的元素所表示的所有记录。在本例中,该集合指的是所有最喜欢的颜色是橙色的患者和所有最喜欢的颜色是紫色的患者。 点击确定。 使用过滤器下拉列表,清除紫色旁边的复选框。现在分析器只使用最喜欢的颜色是橙色的患者。 展开AllerD和Allergies。将模具拖到过滤器,在最喜欢的颜色的下面。这个透视表只显示最喜欢的颜色是橙色和对霉菌过敏的患者。 再次显示查询文本。现在你应该看到以下内容。 SELECT FROM [TUTORIAL] %FILTER NONEMPTYCROSSJOIN([AllerD].[H1].[Allergies].&[mold],[COLORD].[H1].[FAVOURITE COLOR].&[Orange]) MDX函数NONEMPTYCROSSJOIN结合了两个成员,并返回结果元组。该元组只访问属于两个给定成员的记录。 现在你已经看到了三种最常见的过滤表达式。 当你使用一个成员表达式作为过滤器时,系统只访问属于这个成员的记录。你可以写一个成员表达式,如下所示。 [dimension name].[hierarchy name].[level name].&[member key] 或者。 [dimension name].[hierarchy name].[level name].[member name] dimension name是一个维度的名称。 hierarchy name是一个层次结构的名称。您可以省略层次结构的名称。如果你这样做,查询会使用在这个维度中定义的具有给定名称的第一层。 level name是该层次结构中的一个层次的名称。你可以省略层次名称。如果你这样做,查询会使用在这个维度中定义的具有给定名称的第一个成员。 member key是给定层次中成员的键。这通常与成员名称相同。 member name是给定级别中成员的名称。 关于更多过滤规则,请看用DeepSee使用MDX和DeepSee MDX参考。