搜索​​​​

清除过滤器
文章
Nicky Zhu · 二月 3, 2021

IRIS中的权限管理

下一篇: [案例: 建立只能使用SQL的用户](https://cn.community.intersystems.com/post/%E6%A1%88%E4%BE%8B-%E5%BB%BA%E7%AB%8B%E5%8F%AA%E8%83%BD%E4%BD%BF%E7%94%A8sql%E7%9A%84%E7%94%A8%E6%88%B7) IRIS通过认证(Authentication)与授权(Authorization)两项机制控制外部用户对系统及应用、数据资源的可访问性。因此。如需要进行权限控制,则需要通过配置认证和授权进行。 ## IRIS中的认证 {#2} 认证可以验证任何试图连接到InterSystems IRIS®的用户的身份。一旦通过认证,用户就与IRIS建立了通信,从而可以使用其数据和工具。有许多不同的方法可以验证用户的身份;每种方法都称为验证机制。IRIS 通常被配置为只使用其中一种方式。 支持的认证方式 * 实例认证:通过用户名/密码对登录平台,即密码认证 * LDAP:通过第三方LDAP服务器(如Windows Active Directory )完成认证 * 操作系统认证:建立操作系统用户-平台用户映射,使用操作系统用户登录平台 * Kerberos:使用Kerberos协议进行认证 * 代理认证:使用自定义的代码实现认证过程 ### 系统服务与认证 {#2.1} 在安装时,IRIS会启动一系列系统级的服务用与控制与外部用户或系统的交互,这些服务都绑定了默认的认证机制 ![image](/sites/default/files/inline/images/2.1.png) 图中红框标出的即为系统安装后会自动启用并需经认证才可使用的系统服务,认证手段可配置。 例如,如果变更%Service_Console的身份验证方法,取消密码方法,用户就不能通过输入用户名密码登入Terminal。 通过Portal的菜单 系统管理 > 安全 > 服务 可访问该设置。 ### 账户控制参数 {#2.2} 通过系统管理 > 安全 > 系统安全 > 系统范围的安全参数中的选项可对于用户名/密码认证手段的行为进行更多的约束。 ![image](/sites/default/files/inline/images/2.2.png) * 非活动限制 - 指定用户账户不活跃的最大天数,它被定义为成功登录之间的时间。当达到此限制时,该帐户将被禁用。值为0(0)表示对登录之间的天数没有限制。[对于最低安全级别的安装,默认为0,对于正常和锁定的安装,默认为90]。 * 无效登录限制 (0-64) - 指定连续不成功的登录尝试的最大次数。在达到此限制后,要么禁用账户,要么对每次尝试进行递增的时间延迟;行动取决于如果达到登录限制字段则禁用账户的值。值为0(零)表示对无效登录的次数没有限制。[默认为5] * 如果达到登录限制,则禁用账户 - 如果选中,则指定达到无效登录次数(在前一字段中指定)将导致用户账户被禁用。 * 密码有效期天数(0-99999) - 指定密码过期的频率以及用户更改密码的频率(天数)。当初始设置时,指定密码过期的天数。0(0)表示密码永远不会过期。不会影响已设置了下次登录时更改密码字段的用户。[默认为0] 需要特别注意的是,密码有效性、过期和禁用账户等设置会影响IRIS实例的所有账户,包括IRIS超级管理员账户。如触发了控制策略,则在更新这些帐户的信息之前,可能无法进行各种操作,这可能导致意外的结果。如超级管理员账户被锁定,则需要通过紧急模式启动实例再进行修改。 对于系统可用的认证手段的配置和其他可用的安全配置,请参见[Security Administration Guide](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS "安全管理向导") ## IRIS中的授权 {#3} ### 授权模型 {#3.1} InterSystems公司的授权模式采用基于角色的访问控制。 * Users – 用户 * Roles – 角色 * Privileges – 权限 * Resources – 资源 | Permissions – 许可 在这种模式下,用户拥有与分配给各自用户身份的角色相关的权限。 ![image](/sites/default/files/inline/images/3.1.png) * 一个角色是一个命名的特权集合 * 一个用户可以拥有一个以上的角色 * 权限分配给角色,角色分配给用户 其中,Roles就是权限的集合,而权限提供对资源的特定类型的访问的许可。 * 可控资源: 数据库,服务,应用(包括Web应用 )和其他 * 可选用的许可: Read, Write or Use,其中执行代码需要数据库的读权限 ### 资源的定义 {#3.2} 资源是一项相对抽象的概念,用来指代IRIS中的数据库,服务,应用等可被访问的对象。例如,对于数据库,在建立时默认采用%DB_%DEFAULT指代,也可自定义资源(数据库资源必须以%DB_开头): ![image](/sites/default/files/inline/images/3.2.png) 对于Web应用,默认不需要通过资源控制,即所有可登录用户都可访问(但该用户进程不一定能访问到数据,还需参照是否具有对数据库的访问权限)。如通过分配资源进行控制,则登录用户还需具有资源才能访问这个Web应用: ![image](/sites/default/files/inline/images/3.3.png) 因此,一项权限实际上是指对某个资源的一些特定操作的集合。 例如,对于数据库UserDB具有读写操作许可的权限A,对于Web应用/csp/sys具有使用操作许可的权限B。如果我们将这两项权限都赋给角色RoleA,那么这个角色就同时拥有A权限和B权限,从而能够访问数据库UserDB和访问Web应用/csp/sys。 ### SQL授权 {#3.3} 除了对数据库进行授权外,IRIS作为一个数据平台,需要对外提供数据访问。因此,IRIS也提供了SQL授权对用户可执行的SQL进行细粒度的权限控制。 SQL的授权可以分配给角色或用户。但通常在企业环境中,用户数量会很多,仍然需要对SQL用户进行分组,根据分组规划角色,通过角色进行授权的控制,才能有效降低维护授权所需的工作量。 SQL的授权针对SQL类型,可分为库、表级授权。 对于Create table、drop view、truncate table这一类的DDL,使用库级授权,即用户可在特定的库中执行建表、删除视图等经过授权的操作。如下: ![image](/sites/default/files/inline/images/3.4.png) 对于Select、update等DML,则使用表级授权,使用户能够通过DML访问特定的表中的数据。如下: ![image](/sites/default/files/inline/images/3.5.png) 除通过Portal操作之外,对于SQL的授权,还可使用IRIS SQL中的额GRANT语句,例如: `GRANT * ON Schema Test TO TestRole` 这个SQL即可以将当前操作数据库下Schema Test中的所有表的所有权限都赋给TestRole这个角色。 关于GRANT语句的用法,可参见[GRANT指令](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_grant) 以上即为IRIS中进行权限控制所需掌握的概念和内容,在后续文章中,我们会结合实例向大家介绍其使用。 下一篇: [案例: 建立只能使用SQL的用户](https://cn.community.intersystems.com/post/%E6%A1%88%E4%BE%8B-%E5%BB%BA%E7%AB%8B%E5%8F%AA%E8%83%BD%E4%BD%BF%E7%94%A8sql%E7%9A%84%E7%94%A8%E6%88%B7) 推荐阅读 [Security Administration Guide](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS "安全管理向导") - https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS
文章
姚 鑫 · 二月 26, 2021

第四十八章 Caché 变量大全 ^$LOCK 变量

# 第四十八章 Caché 变量大全 ^$LOCK 变量 提供锁名信息。 # 大纲 ``` ^$|nspace|LOCK(lock_name,info_type,pid) ^$|nspace|L(lock_name,info_type,pid) ``` # 参数 - `|nspace|`或`[nspace]` [nspace]可选-扩展SSVN引用,显式名称空间名称或隐含名称空间。必须计算为带引号的字符串,该字符串括在方括号(`[“nspace”]`)或竖线(`|“nspace”|`)中。命名空间名称不区分大小写;它们以大写字母存储和显示。 - lock_name 计算结果为包含锁定变量名称(带下标或无下标)的字符串的表达式。如果是文字,则必须指定为带引号的字符串。 - info_type 可选-解析为所有大写字母指定为带引号字符串的关键字的表达式。info_type指定返回关于`lock_name`的哪种类型的信息。可用选项有“所有者”、“标志”、“模式”和“计数”。作为独立函数调用`^$LOCK`时需要。 - pid 可选-用于“计数”关键字。一个整数,指定锁所有者的进程标识。如果指定,最多为“计数”返回一个列表元素。如果省略(或指定为0),将为持有指定锁的每个所有者返回一个列表元素。`pid`对其他info_type关键字没有影响。 # 描述 `^$LOCK`结构化系统变量返回有关当前命名空间或本地系统上指定命名空间中的锁的信息。可以通过两种方式使用`^$LOCK`: - info_type作为独立函数返回指定锁的信息。 - `$DATA`、`$ORDER`或`$QUERY`函数没有info_type作为参数。 注意:`^$LOCK`从本地系统的锁表中检索锁表信息。它不会从远程服务器上的锁表中返回信息。 ## ECP环境中的`^$LOCK。` - 本地系统:为本地系统持有的锁调用`^$LOCK`时,`^$LOCK`的行为与没有ECP时相同,只有一个例外:info_type的`“FLAGS”`返回一个星号(*),表示锁处于ECP环境中。这意味着一旦锁被释放,远程系统间IRIS实例就能够持有锁。 - 应用服务器:当通过ECP(数据服务器或另一个应用服务器)在一个应用服务器上为另一个服务器上持有的锁调用`^$LOCK`时,`^$LOCK`不返回任何信息。请注意,这是与锁不存在时相同的行为。 - 数据服务器:当在数据服务器上为应用服务器持有的锁调用`^$LOCK`时,`^$LOCK`的行为与本地系统略有不同,如下所示: - “Owner”:如果锁由连接到调用`^$Lock`的数据服务器的应用程序服务器持有,则`^$Lock(LOCKNAME,“OWNER”)`为持有该锁的实例返回`“ECPConfigurationName:MachineName:InstanceName”`,但不标识持有该锁的特定进程。 - `“FLAGS”`:如果锁由连接到调用`^$LOCK`的数据服务器的应用程序服务器持有,则独占锁的`^$LOCK(lockname,“FLAGS”)`将返回`“Z”`标志。这个`“Z”`表示除了ECP环境之外,在InterSystems IRIS中不再使用的一种遗留锁。 - `“mode”`:如果锁由连接到调用`^$lock`的数据服务器的应用程序服务器持有,则独占锁的`^$lock(lockname,“mode”)`返回`“ZAX”`而不是`“X”`。`ZA`是一种遗留锁,除ECP环境外,在系统间IRIS中不再使用。对于共享锁,`^$lock(lockname,“mode”)`返回`“S”`,与本地锁相同。 # 参数 ## nspace 此可选参数允许您使用扩展的SSVN引用在另一个名称空间中指定全局变量。可以显式指定名称空间名称,将其命名为带引号的字符串文字或变量,或者通过指定隐式名称空间。命名空间名称不区分大小写。可以使用方括号语法`[“ USER”]`或环境语法`|“ USER” |`。 nspace分隔符前后不允许有空格。 可以使用以下方法测试是否定义了名称空间: ```java WRITE ##class(%SYS.Namespace).Exists("USER"),! WRITE ##class(%SYS.Namespace).Exists("LOSER") ``` 可以使用`$NAMESPACE`特殊变量来确定当前的名称空间。更改当前名称空间的首选方法是`NEW $NAMESPACE`,然后`SET $NAMESPACE =“nspacename”`。 ## lock_name 该表达式的计算结果为包含锁定变量名称(带下标或未下标)的字符串。使用`LOCK`命令定义一个锁变量(通常是全局变量)。 ## info_type 当将`^$LOCK`用作独立函数时,需要一个`info_type`关键字;当将`^$LOCK`用作另一个函数的参数时,则是一个可选参数。 info_type必须以大写字母指定为带引号的字符串。 - `“OWNER”`返回锁所有者的进程ID(pid)。如果该锁是共享锁,则以逗号分隔列表的形式返回该锁的所有所有者的进程ID。如果指定的锁不存在,则`^$LOCK`返回空字符串。 - `“FLAGS”`返回锁的状态。它可以返回以下值:`“D”`-处于挂起挂起状态; `“P”`-处于锁定挂起状态; `“N”`-这是一个节点锁定,后代未锁定; `“Z”`-此锁处于ZAX模式; `“L”`-锁丢失,服务器不再具有此锁; `“*”`-这是一个远程锁定。如果指定的锁处于正常锁状态或不存在,则`^$LOCK`返回空字符串。 - `“MODE”`返回当前节点的锁定模式。对于排他锁定模式,它返回`“X”`,对于共享锁定模式,它返回`“S”`,对于`ZALLOCATE`锁定模式,它返回`“ZAX”`。如果指定的锁不存在,则`^$LOCK`返回空字符串。 - `“COUNTS”`返回锁的锁计数,指定为二进制列表结构。对于排他锁,列表包含一个元素;对于共享锁,列表包含每个锁所有者的元素。可以使用pid参数仅返回指定锁定所有者的list元素。每个元素都包含所有者的pid,独占模式增量计数和共享模式增量计数。如果独占模式和共享模式的增量计数均为0(或`“”`),则锁定处于`“ZAX”`模式。增量计数后可以跟一个`“D”`,以指示该锁已在当前事务中解锁,但是其释放被延迟(`“D”`),直到事务被提交或回滚为止。如果指定的锁不存在,则`^$LOCK`返回空字符串。 必须使用所有大写字母指定`info_type`关键字。指定无效的`info_type`关键字会生成``错误。 ## pid 锁所有者的进程ID。仅在使用`“COUNTS”`关键字时有意义。用于将“ ”返回值限制为(最多)一个列表元素。 pid在所有平台上均指定为整数。如果pid与`lock_name ^$LOCK`的所有者的进程ID匹配,则返回该所有者的`“COUNTS”`列表元素;如果pid与`lock_name ^$LOCK`的所有者的进程ID不匹配,则返回空字符串。将pid指定为0表示与省略pid相同; `^$LOCK`返回所有`“COUNTS”`列表元素。pid参数与`“OWNER”`,`“FLAGS”`或`“MODE”`关键字一起使用,但被忽略。 # 示例 下面的示例显示由info_type关键字返回的排他锁的值: ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK() ClassMethod LOCK() { LOCK ^B(1,1) ; define lock WRITE !,"lock owner: ",^$LOCK("^B(1,1)","OWNER") WRITE !,"lock flags: ",^$LOCK("^B(1,1)","FLAGS") WRITE !,"lock mode: ",^$LOCK("^B(1,1)","MODE") WRITE !,"lock counts: " ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK -^B(1,1) ; delete lock } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK() lock owner: 17824 lock flags: lock mode: X lock counts: 0000: 0B 01 04 04 A0 45 03 04 01 02 01 ....??E..... ``` 下面的示例显示在递增和递减独占锁时,info_type `“COUNTS”`返回的值如何变化: ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK1() ClassMethod LOCK1() { LOCK ^B(1,1) ; define exclusive lock ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK +^B(1,1) ; increment lock ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK +^B(1,1) ; increment lock again ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK -^B(1,1) ; decrement lock ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK -^B(1,1) ; decrement lock again ZZDUMP ^$LOCK("^B(1,1)","COUNTS") LOCK -^B(1,1) ; delete exclusive lock } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK1() 0000: 0B 01 04 04 A0 45 03 04 01 02 01 ....??E..... 0000: 0B 01 04 04 A0 45 03 04 02 02 01 ....??E..... 0000: 0B 01 04 04 A0 45 03 04 03 02 01 ....??E..... 0000: 0B 01 04 04 A0 45 03 04 02 02 01 ....??E..... 0000: 0B 01 04 04 A0 45 03 04 01 02 01 ....??E..... ``` 下面的示例显示在递增和递减共享锁时,info_type`“COUNTS”`返回的值如何变化 ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK2() ClassMethod LOCK2() { LOCK ^S(1,1)#"S" ; define shared lock ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK +^S(1,1)#"S" ; increment lock ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK +^S(1,1)#"S" ; increment lock again ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK -^S(1,1)#"S" ; decrement lock ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK -^S(1,1)#"S" ; decrement lock again ZZDUMP ^$LOCK("^S(1,1)","COUNTS") LOCK -^S(1,1)#"S" ; delete shared lock } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK2() 0000: 0B 01 04 04 A0 45 02 01 03 04 01 ....??E..... 0000: 0B 01 04 04 A0 45 02 01 03 04 02 ....??E..... 0000: 0B 01 04 04 A0 45 02 01 03 04 03 ....??E..... 0000: 0B 01 04 04 A0 45 02 01 03 04 02 ....??E..... 0000: 0B 01 04 04 A0 45 02 01 03 04 01 ....??E..... ``` 以下示例显示如何将`^$lock`用作`$DATA`、`$ORDER`和`$QUERY`函数的参数。 ## 作为$DATA的参数 `$DATA(^$|nspace|LOCK(lock_name))` `^$lock`作为`$DATA`的参数返回一个整数值,该值指定锁定名称是否作为节点存在于`^$lock`中。下表显示了`$DATA`可以返回的整数值。 Value| Meaning ---|--- 0 |锁信息不存在 10 |锁信息存在 请注意,在此上下文中使用的`$DATA`只能返回0或10,其中10表示指定的锁存在。它不能确定锁是否有后代,也不能返回1或11。 下面的示例测试当前命名空间中是否存在锁名。第一次写入返回10(锁名存在),第二次写入返回0(锁名不存在): ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK3() ClassMethod LOCK3() { LOCK ^B(1,2) ; define lock WRITE !,$DATA(^$LOCK("^B(1,2)")) LOCK -^B(1,2) ; delete lock WRITE !,$DATA(^$LOCK("^B(1,2)")) } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK3() 10 0 ``` ## 作为`$ORDER`的参数 `$ORDER(^$|nspace|LOCK(lock_name),direction)` `^$lock`作为`$ORDER`的参数,按排序顺序将下一个或上一个`^$lock`锁名节点返回到指定的锁名。如果不存在这样的锁名作为`^$lock`节点,`$ORDER`将返回空字符串。 锁以区分大小写的字符串排序顺序返回。使用数字排序规则以下标树顺序返回命名锁的下标。 Direction参数指定是返回下一个锁名称还是返回上一个锁名称。如果不提供方向参数,InterSystems IRIS会将排序序列中的下一个锁名返回到您指定的锁名。 以下子例程在Samples名称空间中搜索锁,并将锁名称存储在名为locket的本地数组中。 ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK4() ClassMethod LOCK4() { LOCKARRAY SET lname="" FOR I=1:1 { SET lname=$ORDER(^$|"SAMPLES"|LOCK(lname)) QUIT:lname="" SET LOCKET(I)=lname WRITE !,"the lock name is: ",lname } WRITE !,"All lock names listed" QUIT } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK4() the lock name is: ^%SYS("CSP","Daemon") All lock names listed ``` ## 作为`$QUERY`的参数 `$QUERY(^$|nspace|LOCK(lock_name))` `^$lock`作为`$query`的参数,将排序序列中的下一个锁名返回到您指定的锁名。如果没有将下一个锁名定义为`^$lock`中的节点,则`$query`将返回空字符串。 锁以区分大小写的字符串排序顺序返回。使用数字排序规则以下标树顺序返回命名锁的下标。 在下面的示例中,在当前命名空间中(按随机顺序)创建了五个全局锁名称。 ```java /// d ##class(PHA.TEST.SpecialVariables).LOCK5() ClassMethod LOCK5() { LOCK (^B(1),^A,^D,^A(1,2,3),^A(1,2)) WRITE !,"lock name: ",$QUERY(^$LOCK("")) WRITE !,"lock name: ",$QUERY(^$LOCK("^C")) WRITE !,"lock name: ",$QUERY(^$LOCK("^A(1,2)")) } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK5() lock name: ^$LOCK("^%SYS(""CSP"",""Daemon"")") lock name: ^$LOCK("^D") lock name: ^$LOCK("^A(1,2,3)") ``` `$QUERY`将所有全局锁变量名(带下标或无下标)视为字符串,并按字符串排序顺序检索它们。因此,`$QUERY(^$LOCK(“”))`按排序顺序检索第一个锁名:`^$LOCK(“^A”)`或排序序列中位置较高的InterSystems IRIS定义的锁。`$QUERY(^$LOCK(“^C”))`检索排序序列中不存在的`^C`:`^$LOCK(“^D”)`之后的下一个锁名。`$QUERY(^$LOCK(“^A(1,2)”))`检索排序规则序列中它后面的`^$LOCK(“^A(1,2,3)”)`。 想知道锁产生的原因大概有哪些?应该怎么避免呢? 我有个想法,希望结合应用场景来介绍,这样能够明白使用场景。
文章
Michael Lei · 五月 12, 2021

通过 ML 与 IntegratedML 运行一些 Covid-19 ICU 预测(第一部分)

关键字:IRIS, IntegratedML, 机器学习, Covid-19, Kaggle  ## 目的 最近,我注意到一个用于预测 Covid-19 患者是否将转入 ICU 的 [Kaggle 数据集](https://www.kaggle.com/S%C3%ADrio-Libanes/covid19/kernels)。 它是一个包含 1925 条病患记录的电子表格,其中有 231 列生命体征和观察结果,最后一列“ICU”为 1(表示是)或 0(表示否)。 任务是根据已知数据预测患者是否将转入 ICU。 这个数据集看起来是所谓的“传统 ML”任务的一个好例子。数据看上去数量合适,质量也相对合适。它可能更适合在 [IntegratedML 演示](https://github.com/intersystems-community/integratedml-demo-template)套件上直接应用,那么,基于普通 ML 管道与可能的 IntegratedML 方法进行快速测试,最简单的方法是什么?   ## 范围 我们将简要地运行一些常规 ML 步骤,如: * 数据 EDA  * 特征选择 * 模型选择 * 通过网格搜索调整模型参数 与 * 通过 SQL 实现的整合 ML 方法。 它通过 Docker-compose 等方式运行于 AWS Ubuntu 16.04 服务器。   ## 环境 我们将重复使用 [integredML-demo-template](https://openexchange.intersystems.com/package/integratedml-demo-template) 的 Docker 环境: ![](https://user-images.githubusercontent.com/8899513/85151307-a0d1f280-b221-11ea-81d8-f0e11ca45d4c.PNG) 以下 notebook 文件在“tf2jupyter”上运行,带有 IntegratedML 的 IRIS 在“irismlsrv”上运行。 Docker-compose 在 AWS Ubuntu 16.04 上运行。   ## 数据和任务 该数据集包含收集自 385 名患者的 1925 条记录,每名患者正好 5 条记录。 它共有 231 个列,其中有一列“ICU”是我们的训练和预测目标,其他 230 列都以某种方式用作输入。 ICU 具有二进制值 1 或 0。 除了 2 列看上去是分类字符串(在数据框架中显示为“对象”)外,其他所有列都是数值。 import numpy as np import pandas as pd from sklearn.impute import SimpleImputer import matplotlib.pyplot as plt from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score, roc_curve import seaborn as sns sns.set(style="whitegrid") import os for dirname, _, filenames in os.walk('./input'): for filename in filenames: print(os.path.join(dirname, filename)) ./input/datasets_605991_1272346_Kaggle_Sirio_Libanes_ICU_Prediction.xlsx   df = pd.read_excel("./input/datasets_605991_1272346_Kaggle_Sirio_Libanes_ICU_Prediction.xlsx") df   PATIENT_VISIT_IDENTIFIER AGE_ABOVE65 AGE_PERCENTIL GENDER DISEASE GROUPING 1 DISEASE GROUPING 2 DISEASE GROUPING 3 DISEASE GROUPING 4 DISEASE GROUPING 5 DISEASE GROUPING 6 ... TEMPERATURE_DIFF OXYGEN_SATURATION_DIFF BLOODPRESSURE_DIASTOLIC_DIFF_REL BLOODPRESSURE_SISTOLIC_DIFF_REL HEART_RATE_DIFF_REL RESPIRATORY_RATE_DIFF_REL TEMPERATURE_DIFF_REL OXYGEN_SATURATION_DIFF_REL WINDOW ICU 1 60th 0.0 0.0 0.0 0.0 1.0 1.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 0-2 1 1 60th 0.0 0.0 0.0 0.0 1.0 1.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 2-4 2 1 60th 0.0 0.0 0.0 0.0 1.0 1.0 ... NaN NaN NaN NaN NaN NaN NaN NaN 4-6 3 1 60th 0.0 0.0 0.0 0.0 1.0 1.0 ... -1.000000 -1.000000 NaN NaN NaN NaN -1.000000 -1.000000 6-12 4 1 60th 0.0 0.0 0.0 0.0 1.0 1.0 ... -0.238095 -0.818182 -0.389967 0.407558 -0.230462 0.096774 -0.242282 -0.814433 ABOVE_12 1 ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 1920 384 50th 1 0.0 0.0 0.0 0.0 0.0 0.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 0-2 1921 384 50th 1 0.0 0.0 0.0 0.0 0.0 0.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 2-4 1922 384 50th 1 0.0 0.0 0.0 0.0 0.0 0.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 4-6 1923 384 50th 1 0.0 0.0 0.0 0.0 0.0 0.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 6-12 1924 384 50th 1 0.0 0.0 1.0 0.0 0.0 0.0 ... -0.547619 -0.838384 -0.701863 -0.585967 -0.763868 -0.612903 -0.551337 -0.835052 ABOVE_12 1925 行 × 231 列 df.dtypes PATIENT_VISIT_IDENTIFIER int64 AGE_ABOVE65 int64 AGE_PERCENTIL object GENDER int64 DISEASE GROUPING 1 float64 ... RESPIRATORY_RATE_DIFF_REL float64 TEMPERATURE_DIFF_REL float64 OXYGEN_SATURATION_DIFF_REL float64 WINDOW object ICU int64 Length: 231, dtype: object 当然,设计此问题及其方法有几个选择。 首先,第一个显而易见的选择是,这可以是一个基本的“二元分类”问题。 我们可以将全部 1925 条记录都视为“无状态”个体记录,不管它们是否来自同一患者。 如果我们将 ICU 和其他值都当作数值来处理,这也可以是一个“回归”问题。 当然还有其他可能的方法。 例如,我们可以有一个更深层的视角,即数据集有 385 个不同的短“时间序列”集,每个患者一个。 我们可以将整个数据集分解成 385 个单独的训练集/验证集/测试集,我们是否可以尝试 CNN 或 LSTM 等深度学习模型来捕获每个患者对应的每个集合中隐藏的“症状发展阶段或模式”? 可以的。 这样做的话,我们还可以通过各种方式应用一些数据增强,来丰富测试数据。 但那就是另一个话题了,不在本帖的讨论范围内。 在本帖中,我们将只测试所谓的“传统 ML”方法与 IntegratedML(一种 AutoML)方法的快速运行。    ## “传统”ML 方法? 与大多数现实案例相比,这是一个相对标准的数据集,除了缺少一些值,所以我们可以跳过特征工程部分,直接使用各个列作为特征。 那么,我们直接进入特征选择。 ### **插补缺失数据** 首先,确保所有缺失值都通过简单的插补来填充: df_cat = df.select_dtypes(include=['object']) df_numeric = df.select_dtypes(exclude=['object']) imp = SimpleImputer(missing_values=np.nan, strategy='mean') idf = pd.DataFrame(imp.fit_transform(df_numeric)) idf.columns = df_numeric.columns idf.index = df_numeric.index idf.isnull().sum() ###   ### **特征选择** 我们可以使用数据框架中内置的正态相关函数,来计算每个列的值与 ICU 的相关性。 #### 特征工程 - **相关性** {#featuring-engineering---correlation} idf.drop(["PATIENT_VISIT_IDENTIFIER"],1) idf = pd.concat([idf,df_cat ], axis=1) cor = idf.corr() cor_target = abs(cor["ICU"]) relevant_features = cor_target[cor_target>0.1] # correlation above 0.1 print(cor.shape, cor_target.shape, relevant_features.shape) #relevant_features.index #relevant_features.index.shape 这将列出 88 个特征,它们与 ICU 目标值的相关度大于 0.1。 这些列可以直接用作我们的模型输入 我还运行了其他几个在传统 ML 任务中常用的“特征选择方法”: #### 特征选择 - **卡方** {#feature-selection---Chi-squared} from sklearn.feature_selection import SelectKBest from sklearn.feature_selection import chi2 from sklearn.preprocessing import MinMaxScaler X_norm = MinMaxScaler().fit_transform(X) chi_selector = SelectKBest(chi2, k=88) chi_selector.fit(X_norm, y) chi_support = chi_selector.get_support() chi_feature = X.loc[:,chi_support].columns.tolist() print(str(len(chi_feature)), 'selected features', chi_feature) 88 selected features ['AGE_ABOVE65', 'GENDER', 'DISEASE GROUPING 1', ... ... 'P02_VENOUS_MIN', 'P02_VENOUS_MAX', ... ... RATURE_MAX', 'BLOODPRESSURE_DIASTOLIC_DIFF', ... ... 'TEMPERATURE_DIFF_REL', 'OXYGEN_SATURATION_DIFF_REL'] 特征选择 - **皮尔逊相关**  def cor_selector(X, y,num_feats): cor_list = [] feature_name = X.columns.tolist() # calculate the correlation with y for each feature for i in X.columns.tolist(): cor = np.corrcoef(X[i], y)[0, 1] cor_list.append(cor) # replace NaN with 0 cor_list = [0 if np.isnan(i) else i for i in cor_list] # feature name cor_feature = X.iloc[:,np.argsort(np.abs(cor_list))[-num_feats:]].columns.tolist() # feature selection? 0 for not select, 1 for select cor_support = [True if i in cor_feature else False for i in feature_name] return cor_support, cor_feature cor_support, cor_feature = cor_selector(X, y, 88) print(str(len(cor_feature)), 'selected features: ', cor_feature) 88 selected features: ['TEMPERATURE_MEAN', 'BLOODPRESSURE_DIASTOLIC_MAX', ... ... 'RESPIRATORY_RATE_DIFF', 'RESPIRATORY_RATE_MAX'] #### 特征选择 - **回归特征消除 (RFE)** {#feature-selection---Recursive-Feature-Elimination-(RFE)} from sklearn.feature_selection import RFE from sklearn.linear_model import LogisticRegression rfe_selector = RFE(estimator=LogisticRegression(), n_features_to_select=88, step=100, verbose=5) rfe_selector.fit(X_norm, y) rfe_support = rfe_selector.get_support() rfe_feature = X.loc[:,rfe_support].columns.tolist() print(str(len(rfe_feature)), 'selected features: ', rfe_feature) Fitting estimator with 127 features. 88 selected features: ['AGE_ABOVE65', 'GENDER', ... ... 'RESPIRATORY_RATE_DIFF_REL', 'TEMPERATURE_DIFF_REL'] 特征选择 - **Lasso** ffrom sklearn.feature_selection import SelectFromModel from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import MinMaxScaler X_norm = MinMaxScaler().fit_transform(X) embeded_lr_selector = SelectFromModel(LogisticRegression(penalty="l2"), max_features=88) embeded_lr_selector.fit(X_norm, y) embeded_lr_support = embeded_lr_selector.get_support() embeded_lr_feature = X.loc[:,embeded_lr_support].columns.tolist() print(str(len(embeded_lr_feature)), 'selected features', embeded_lr_feature) 65 selected features ['AGE_ABOVE65', 'GENDER', ... ... 'RESPIRATORY_RATE_DIFF_REL', 'TEMPERATURE_DIFF_REL'] 特征选择 - **RF 基于树**:SelectFromModel from sklearn.feature_selection import SelectFromModel from sklearn.ensemble import RandomForestClassifier embeded_rf_selector = SelectFromModel(RandomForestClassifier(n_estimators=100), max_features=227) embeded_rf_selector.fit(X, y) embeded_rf_support = embeded_rf_selector.get_support() embeded_rf_feature = X.loc[:,embeded_rf_support].columns.tolist() print(str(len(embeded_rf_feature)), 'selected features', embeded_rf_feature) 48 selected features ['AGE_ABOVE65', 'GENDER', ... ... 'TEMPERATURE_DIFF_REL', 'OXYGEN_SATURATION_DIFF_REL'] #### 特征选择 - **LightGBM** 或 **XGBoost** {#feature-selection---LightGBM-or-XGBoost} from sklearn.feature_selection import SelectFromModel from lightgbm import LGBMClassifier lgbc=LGBMClassifier(n_estimators=500, learning_rate=0.05, num_leaves=32, colsample_bytree=0.2, reg_alpha=3, reg_lambda=1, min_split_gain=0.01, min_child_weight=40) embeded_lgb_selector = SelectFromModel(lgbc, max_features=128) embeded_lgb_selector.fit(X, y) embeded_lgb_support = embeded_lgb_selector.get_support() embeded_lgb_feature = X.loc[:,embeded_lgb_support].columns.tolist() print(str(len(embeded_lgb_feature)), 'selected features: ', embeded_lgb_feature) embeded_lgb_feature.index 56 selected features: ['AGE_ABOVE65', 'GENDER', 'HTN', ... ... 'TEMPERATURE_DIFF_REL', 'OXYGEN_SATURATION_DIFF_REL'] #### 特征选择 - **全部集成** {#feature-selection---Ensemble-them-all} feature_name = X.columns.tolist() # put all selection together feature_selection_df = pd.DataFrame({'Feature':feature_name, 'Pearson':cor_support, 'Chi-2':chi_support, 'RFE':rfe_support, 'Logistics':embeded_lr_support, 'Random Forest':embeded_rf_support, 'LightGBM':embeded_lgb_support}) # count the selected times for each feature feature_selection_df['Total'] = np.sum(feature_selection_df, axis=1) # display the top 100 num_feats = 227 feature_selection_df = feature_selection_df.sort_values(['Total','Feature'] , ascending=False) feature_selection_df.index = range(1, len(feature_selection_df)+1) feature_selection_df.head(num_feats) df_selected_columns = feature_selection_df.loc[(feature_selection_df['Total'] > 3)] df_selected_columns 我们可以列出通过至少 4 种方法选择的特征: ![](/sites/default/files/inline/images/images/image(810).png) ... ... ![](/sites/default/files/inline/images/images/image(812).png) 我们当然可以选择这 58 个特征。 同时,经验告诉我们,特征选择并不一定总是“民主投票”;更多时候,它可能特定于域问题,特定于数据,有时还特定于我们稍后将采用的 ML 模型或方法。 特征选择 - **第三方工具**  有广泛使用的行业工具和 AutoML 工具,例如 DataRobot 可以提供出色的自动特征选择: ![](/sites/default/files/inline/images/images/capture_feature.png) 从上面的 DataRobot 图表中,我们不难看出,各个 RespiratoryRate 和 BloodPressure 值是与 ICU 转入最相关的特征。    特征选择 - **最终选择** 在本例中,我进行了一些快速实验,发现 LightGBM 特征选择的结果实际上更好一点,所以我们只使用这种选择方法。   df_selected_columns = embeded_lgb_feature # better than ensembled selection dataS = pd.concat([idf[df_selected_columns],idf['ICU'], df_cat['WINDOW']],1) dataS.ICU.value_counts() print(dataS.shape) (1925, 58) 我们可以看到有 58 个特征被选中;不算太少,也不算太多;对于这个特定的单一目标二元分类问题,看起来是合适的数量。   ### **数据不平衡** plt.figure(figsize=(10,5)) count = sns.countplot(x = "ICU",data=data) count.set_xticklabels(["Not Admitted","Admitted"]) plt.xlabel("ICU Admission") plt.ylabel("Patient Count") plt.show() 这说明数据不平衡,只有 26% 的记录转入了 ICU。 这会影响到结果,因此我们可以考虑常规的数据平衡方法,例如 SMOTE 等。 这里,我们可以尝试其他各种 EDA,以相应了解各种数据分布。   ### **运行基本 LR 训练** Kaggle 网站上有一些不错的快速训练 notebook,我们可以根据自己选择的特征列来快速运行。 让我们从快速运行针对训练管道的 LR 分类器开始:   data2 = pd.concat([idf[df_selected_columns],idf['ICU'], df_cat['WINDOW']],1) data2.AGE_ABOVE65 = data2.AGE_ABOVE65.astype(int) data2.ICU = data2.ICU.astype(int) X2 = data2.drop("ICU",1) y2 = data2.ICU from sklearn.preprocessing import LabelEncoder label_encoder = LabelEncoder() X2.WINDOW = label_encoder.fit_transform(np.array(X2["WINDOW"].astype(str)).reshape((-1,))) confusion_matrix2 = pd.crosstab(y2_test, y2_hat, rownames=['Actual'], colnames=['Predicted']) sns.heatmap(confusion_matrix2, annot=True, fmt = 'g', cmap = 'Reds') print("ORIGINAL") print(classification_report(y_test, y_hat)) print("AUC = ",roc_auc_score(y_test, y_hat),'\n\n') print("LABEL ENCODING") print(classification_report(y2_test, y2_hat)) print("AUC = ",roc_auc_score(y2_test, y2_hat)) y2hat_probs = LR.predict_proba(X2_test) y2hat_probs = y2hat_probs[:, 1] fpr2, tpr2, _ = roc_curve(y2_test, y2hat_probs) plt.figure(figsize=(10,7)) plt.plot([0, 1], [0, 1], 'k--') plt.plot(fpr, tpr, label="Base") plt.plot(fpr2,tpr2,label="Label Encoded") plt.xlabel('False positive rate') plt.ylabel('True positive rate') plt.title('ROC curve') plt.legend(loc="best") plt.show() ORIGINAL precision recall f1-score support 0 0.88 0.94 0.91 171 1 0.76 0.57 0.65 54 accuracy 0.85 225 macro avg 0.82 0.76 0.78 225 weighted avg 0.85 0.85 0.85 225 AUC = 0.7577972709551657 LABEL ENCODING precision recall f1-score support 0 0.88 0.93 0.90 171 1 0.73 0.59 0.65 54 accuracy 0.85 225 macro avg 0.80 0.76 0.78 225 weighted avg 0.84 0.85 0.84 225 AUC = 0.7612085769980507          看起来它达到了 76% 的 AUC,准确率为 85%,但转入 ICU 的召回率只有 59% - 似乎有太多假负例。 这当然不理想 - 我们不希望错过患者记录的实际 ICU 风险。 因此,以下所有任务都将集中在如何通过降低 FN 来提高召回率的**目标**上,希望总体准确度有所平衡。 在前面的部分中,我们提到了不平衡的数据,所以第一本能通常是对测试集进行 Stratify(分层),然后使用 SMOTE 方法使其成为更平衡的数据集。 #stratify the test data, to make sure Train and Test data have the same ratio of 1:0 X3_train,X3_test,y3_train,y3_test = train_test_split(X2,y2,test_size=225/1925,random_state=42, stratify = y2, shuffle = True) <span> </span> # train and predict LR.fit(X3_train,y3_train) y3_hat = LR.predict(X3_test) #SMOTE the data to make ICU 1:0 a balanced distribution from imblearn.over_sampling import SMOTE sm = SMOTE(random_state = 42) X_train_res, y_train_res = sm.fit_sample(X3_train,y3_train.ravel()) LR.fit(X_train_res, y_train_res) y_res_hat = LR.predict(X3_test) #draw confusion matrix etc again confusion_matrix3 = pd.crosstab(y3_test, y_res_hat, rownames=['Actual'], colnames=['Predicted']) sns.heatmap(confusion_matrix3, annot=True, fmt = 'g', cmap="YlOrBr") print("LABEL ENCODING + STRATIFY") print(classification_report(y3_test, y3_hat)) print("AUC = ",roc_auc_score(y3_test, y3_hat),'\n\n') print("SMOTE") print(classification_report(y3_test, y_res_hat)) print("AUC = ",roc_auc_score(y3_test, y_res_hat)) y_res_hat_probs = LR.predict_proba(X3_test) y_res_hat_probs = y_res_hat_probs[:, 1] fpr_res, tpr_res, _ = roc_curve(y3_test, y_res_hat_probs) plt.figure(figsize=(10,10)) #And plot the ROC curve as before.   LABEL ENCODING + STRATIFY precision recall f1-score support 0 0.87 0.99 0.92 165 1 0.95 0.58 0.72 60 accuracy 0.88 225 macro avg 0.91 0.79 0.82 225 weighted avg 0.89 0.88 0.87 225 AUC = 0.7856060606060606 SMOTE precision recall f1-score support 0 0.91 0.88 0.89 165 1 0.69 0.75 0.72 60 accuracy 0.84 225 macro avg 0.80 0.81 0.81 225 weighted avg 0.85 0.84 0.85 225 AUC = 0.8143939393939393              所以对数据进行 STRATIFY 和 SMOT 处理似乎将召回率从 0.59 提高到 0.75,总体准确率为 0.84。 现在,按照传统 ML 的惯例来说,数据处理已大致完成,我们想知道在这种情况下最佳模型是什么;它们是否可以做得更好,我们能否尝试相对全面的比较?   ### **运行各种模型的训练比较**: 让我们评估一些常用的 ML 算法,并生成箱形图形式的比较结果仪表板: # compare algorithms from matplotlib import pyplot from sklearn.model_selection import train_test_split from sklearn.model_selection import cross_val_score from sklearn.model_selection import StratifiedKFold from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.naive_bayes import GaussianNB from sklearn.svm import SVC #Import Random Forest Model from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier # List Algorithms together models = [] models.append(('LR', <strong>LogisticRegression</strong>(solver='liblinear', multi_class='ovr'))) models.append(('LDA', LinearDiscriminantAnalysis())) models.append(('KNN', <strong>KNeighborsClassifier</strong>())) models.append(('CART', <strong>DecisionTreeClassifier</strong>())) models.append(('NB', <strong>GaussianNB</strong>())) models.append(('SVM', <strong>SVC</strong>(gamma='auto'))) models.append(('RF', <strong>RandomForestClassifier</strong>(n_estimators=100))) models.append(('XGB', <strong>XGBClassifier</strong>())) #clf = XGBClassifier() # evaluate each model in turn results = [] names = [] for name, model in models: kfold = StratifiedKFold(n_splits=10, random_state=1) cv_results = cross_val_score(model, X_train_res, y_train_res, cv=kfold, scoring='f1') ## accuracy, precision,recall results.append(cv_results) names.append(name) print('%s: %f (%f)' % (name, cv_results.mean(), cv_results.std())) # Compare all model's performance. Question - would like to see a Integrated item on it? pyplot.figure(4, figsize=(12, 8)) pyplot.boxplot(results, labels=names) pyplot.title('Algorithm Comparison') pyplot.show() LR: 0.805390 (0.021905) LDA: 0.803804 (0.027671) KNN: 0.841824 (0.032945) CART: 0.845596 (0.053828) NB: 0.622540 (0.060390) SVM: 0.793754 (0.023050) RF: 0.896222 (0.033732) XGB: 0.907529 (0.040693) ![](/sites/default/files/inline/images/images/image-20200821155401-1.png) 上图看起来表明,XGB 分类器和随机森林分类器的 F1 分数好于其他模型。 让我们也比较一下它们在同一组标准化测试数据上的实际测试结果: import time from pandas import read_csv from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from sklearn.metrics import confusion_matrix from sklearn.metrics import accuracy_score from sklearn.svm import SVC for name, model in models: print(name + ':\n\r') start = time.clock() model.fit(X_train_res, y_train_res) print("Train time for ", model, " ", time.clock() - start) predictions = model.predict(X3_test) #(X_validation) # Evaluate predictions print(accuracy_score(y3_test, predictions)) # Y_validation print(confusion_matrix(y3_test, predictions)) print(classification_report(y3_test, predictions)) LR: Train time for LogisticRegression(multi_class='ovr', solver='liblinear') 0.02814499999999498 0.8444444444444444 [[145 20] [ 15 45]] precision recall f1-score support 0 0.91 0.88 0.89 165 1 0.69 0.75 0.72 60 accuracy 0.84 225 macro avg 0.80 0.81 0.81 225 weighted avg 0.85 0.84 0.85 225 LDA: Train time for LinearDiscriminantAnalysis() 0.2280070000000194 0.8488888888888889 [[147 18] [ 16 44]] precision recall f1-score support 0 0.90 0.89 0.90 165 1 0.71 0.73 0.72 60 accuracy 0.85 225 macro avg 0.81 0.81 0.81 225 weighted avg 0.85 0.85 0.85 225 KNN: Train time for KNeighborsClassifier() 0.13023699999999394 0.8355555555555556 [[145 20] [ 17 43]] precision recall f1-score support 0 0.90 0.88 0.89 165 1 0.68 0.72 0.70 60 accuracy 0.84 225 macro avg 0.79 0.80 0.79 225 weighted avg 0.84 0.84 0.84 225 CART: Train time for DecisionTreeClassifier() 0.32616000000001577 0.8266666666666667 [[147 18] [ 21 39]] precision recall f1-score support 0 0.88 0.89 0.88 165 1 0.68 0.65 0.67 60 accuracy 0.83 225 macro avg 0.78 0.77 0.77 225 weighted avg 0.82 0.83 0.83 225 NB: Train time for GaussianNB() 0.0034229999999979555 0.8355555555555556 [[154 11] [ 26 34]] precision recall f1-score support 0 0.86 0.93 0.89 165 1 0.76 0.57 0.65 60 accuracy 0.84 225 macro avg 0.81 0.75 0.77 225 weighted avg 0.83 0.84 0.83 225 SVM: Train time for SVC(gamma='auto') 0.3596520000000112 0.8977777777777778 [[157 8] [ 15 45]] precision recall f1-score support 0 0.91 0.95 0.93 165 1 0.85 0.75 0.80 60 accuracy 0.90 225 macro avg 0.88 0.85 0.86 225 weighted avg 0.90 0.90 0.90 225 RF: Train time for RandomForestClassifier() 0.50123099999999 0.9066666666666666 [[158 7] [ 14 46]] precision recall f1-score support 0 0.92 0.96 0.94 165 1 0.87 0.77 0.81 60 accuracy 0.91 225 macro avg 0.89 0.86 0.88 225 weighted avg 0.91 0.91 0.90 225 XGB: Train time for XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1, colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1, importance_type='gain', interaction_constraints='', learning_rate=0.300000012, max_delta_step=0, max_depth=6, min_child_weight=1, missing=nan, monotone_constraints='()', n_estimators=100, n_jobs=0, num_parallel_tree=1, random_state=0, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, subsample=1, tree_method='exact', validate_parameters=1, verbosity=None) 1.649520999999993 0.8844444444444445 [[155 10] [ 16 44]] precision recall f1-score support 0 0.91 0.94 0.92 165 1 0.81 0.73 0.77 60 accuracy 0.88 225 macro avg 0.86 0.84 0.85 225 weighted avg 0.88 0.88 0.88 225 结果显示,RF 实际上好于 XGB。 这意味着 XGB 可能在某种程度上有一点过度拟合。 RFC 结果也比 LR 略有改善。   ### **通过进一步的“通过网格搜索调整参数”运行所选模型** 现在假定我们选择了随机森林分类器作为模型。 我们可以对此模型执行进一步的网格搜索,以查看是否可以进一步提高结果的性能。 记住,在这种情况下,我们的目标仍然是优化召回率,这通过最大程度地减少患者可能遇到的 ICU 风险的假负例来实现,所以下面我们将“recall_score”来重新拟合网格搜索。 再次像往常一样使用 10 折交叉验证,因为上面的测试集总是设置为 2915 条记录中的 12% 左右。 from sklearn.model_selection import GridSearchCV # Create the parameter grid based on the results of random search param_grid = {'bootstrap': [True], 'ccp_alpha': [0.0], 'class_weight': [None], 'criterion': ['gini', 'entropy'], 'max_depth': [None], 'max_features': ['auto', 'log2'], 'max_leaf_nodes': [None], 'max_samples': [None], 'min_impurity_decrease': [0.0], 'min_impurity_split': [None], 'min_samples_leaf': [1, 2, 4], 'min_samples_split': [2, 4], 'min_weight_fraction_leaf': [0.0], 'n_estimators': [100, 125], #'n_jobs': [None], 'oob_score': [False], 'random_state': [None], #'verbose': 0, 'warm_start': [False] } #Fine-tune by confusion matrix from sklearn.metrics import roc_curve, precision_recall_curve, auc, make_scorer, recall_score, accuracy_score, precision_score, confusion_matrix scorers = { 'recall_score': make_scorer(recall_score), 'precision_score': make_scorer(precision_score), 'accuracy_score': make_scorer(accuracy_score) } # Create a based model rfc = RandomForestClassifier() # Instantiate the grid search model grid_search = GridSearchCV(estimator = rfc, param_grid = param_grid, scoring=scorers, refit='recall_score', cv = 10, n_jobs = -1, verbose = 2) train_features = X_train_res grid_search.fit(train_features, train_labels) rf_best_grid = grid_search.best_estimator_ rf_best_grid.fit(train_features, train_labels) rf_predictions = rf_best_grid.predict(X3_test) print(accuracy_score(y3_test, rf_predictions)) print(confusion_matrix(y3_test, rf_predictions)) print(classification_report(y3_test, rf_predictions)) 0.92 [[ 46 14] [ 4 161]] precision recall f1-score support 0 0.92 0.77 0.84 60 1 0.92 0.98 0.95 165 accuracy 0.92 225 macro avg 0.92 0.87 0.89 225 weighted avg 0.92 0.92 0.92 225 结果表明,网格搜索成功地将总体准确度提高了一些,同时保持 FN 不变。 我们同样也绘制 AUC 比较图: confusion_matrix4 = pd.crosstab(y3_test, rf_predictions, rownames=['Actual'], colnames=['Predicted']) sns.heatmap(confusion_matrix4, annot=True, fmt = 'g', cmap="YlOrBr") print("LABEL ENCODING + STRATIFY") print(classification_report(y3_test, 1-y3_hat)) print("AUC = ",roc_auc_score(y3_test, 1-y3_hat),'\n\n') print("SMOTE") print(classification_report(y3_test, 1-y_res_hat)) print("AUC = ",roc_auc_score(y3_test, 1-y_res_hat), '\n\n') print("SMOTE + LBG Selected Weights + RF Grid Search") print(classification_report(y3_test, rf_predictions)) print("AUC = ",roc_auc_score(y3_test, rf_predictions), '\n\n\n') y_res_hat_probs = LR.predict_proba(X3_test) y_res_hat_probs = y_res_hat_probs[:, 1] predictions_rf_probs = rf_best_grid.predict_proba(X3_test) #(X_validation) predictions_rf_probs = predictions_rf_probs[:, 1] fpr_res, tpr_res, _ = roc_curve(y3_test, 1-y_res_hat_probs) fpr_rf_res, tpr_rf_res, _ = roc_curve(y3_test, predictions_rf_probs) plt.figure(figsize=(10,10)) plt.plot([0, 1], [0, 1], 'k--') plt.plot(fpr, tpr, label="Base") plt.plot(fpr2,tpr2,label="Label Encoded") plt.plot(fpr3,tpr3,label="Stratify") plt.plot(fpr_res,tpr_res,label="SMOTE") plt.plot(fpr_rf_res,tpr_rf_res,label="SMOTE + RF GRID") plt.xlabel('False positive rate') plt.ylabel('True positive rate') plt.title('ROC curve') plt.legend(loc="best") plt.show() LABEL ENCODING + STRATIFY precision recall f1-score support 0 0.95 0.58 0.72 60 1 0.87 0.99 0.92 165 accuracy 0.88 225 macro avg 0.91 0.79 0.82 225 weighted avg 0.89 0.88 0.87 225 AUC = 0.7856060606060606 SMOTE precision recall f1-score support 0 0.69 0.75 0.72 60 1 0.91 0.88 0.89 165 accuracy 0.84 225 macro avg 0.80 0.81 0.81 225 weighted avg 0.85 0.84 0.85 225 AUC = 0.8143939393939394 SMOTE + LBG Selected Weights + RF Grid Search precision recall f1-score support 0 0.92 0.77 0.84 60 1 0.92 0.98 0.95 165 accuracy 0.92 225 macro avg 0.92 0.87 0.89 225 weighted avg 0.92 0.92 0.92 225 AUC = 0.8712121212121211       结果表明,经过算法比较和进一步的网格搜索后,我们将 AUC 从 78% 提高到 87%,总体准确度为 92%,召回率为 77%。   ### **“传统 ML”方法回顾** 那么,这个结果到底如何? 对于使用传统 ML 算法的基本手动处理是可以的。 在 Kaggle 竞争表中表现如何? 好吧,它不会出现在排行榜上。 我通过 DataRobot 当前的 AutoML 服务运行了原始数据集,在对排名前 43 的模型进行比较后,最好的结果是使用模型“具有无人监督学习功能的 XGB 树分类器”实现的相当于大约 90+% 的 AUC(有待使用同类数据进行进一步确认)。 如果真的想在 Kaggle 上具有竞争力,这可能是我们要考虑的底线模型。 我也会将最佳结果与模型的排行列表放在 github 中。 最后,对于特定于医护场所的现实案例,我的感觉是,我们还需要考虑具有一定程度自定义的深度学习方法,正如本贴的“数据和任务”部分所提到的。 当然,在现实情况下,在哪里收集高质量数据列也可能是一个前期问题。   ## IntegratedML 方法? 上文说明了所谓的传统 ML 流程,其中通常包括数据 EDA、特征工程、特征选择、模型选择和通过网格搜索进行性能优化等。 这是我目前能想到的最简单的适合此任务的方法,我们甚至还没有触及模型部署和服务管理生命周期 - 我们将在下一个帖子中探讨这些方面,研究如何利用 Flask/FastAPI/IRIS,并将这个基本的 ML 模型部署到 Covid-19 X-Ray 演示服务栈中。 现在,IRIS 有了 IntegratedML,它是一个优雅的 SQL 包装器,包装了 AutoML 的强大选项。 在第二部分中,我们可以研究如何以大为简化的流程来完成上述任务,这样我们就不必再为特征选择、模型选择和性能优化等问题而烦恼,同时可获得等效的 ML 结果来实现商业利益。 到这里,如果再塞入使用相同数据快速运行 integratedML 的内容,本帖可能太长了,无法在 10 分钟内读完,因此我将该内容移至[第二部分](https://community.intersystems.com/post/run-some-covid-19-icu-predictions-ml-vs-integratedml-part-i)。  
文章
Michael Lei · 六月 23, 2021

Yape - 另一个 pButtons 提取程序(自动创建图表)

> 注(2019 年 6 月):许多内容发生了变化,[最新的详细信息请参见此处](https://community.intersystems.com/post/unpacking-pbuttons-yape-update-notes-and-quick-guides) > 注(2018 年 9 月):自本帖首次发布以来,内容已经有了很大改动,我建议使用 Docker 容器版本,以容器形式运行的项目以及详细信息仍然在 [GitHub 的同一个地址发布](https://github.com/murrayo/yape),您可以下载、运行并根据需要进行修改。 与客户合作进行性能评估、容量规划和故障排除时,我经常解包和查看来自 pButtons 的 Caché 和操作系统指标。 [我不久前发布了一个帖子,介绍了一个用来解包 pButtons 指标的实用工具](https://community.intersystems.com/post/extracting-pbuttons-data-csv-file-easy-charting)(该实用工具使用 unix shell、perl 和 awk 脚本编写),而不是费力地浏览 html 文件,再将需要绘制的部分剪切并粘贴到 excel 中。 虽然这是一个*有用的省时工具*,但还不够完善... 我还使用脚本自动绘制指标图表,以便快速查看并包含在报告中。 但是,这些绘图脚本不容易维护,并且当需要站点特定的配置(例如 iostat 或 Windows perfmon 的磁盘列表)时会变得特别混乱,所以我从未公开发布过绘图实用工具。 不过我现在可以很高兴地说,已经有了简单得多的解决方案。 当我与 Fabian 一起在客户站点查看系统性能时,有了意外发现,[他向我展示了使用实用的 Python 绘图模块所做的工作](https://community.intersystems.com/post/visualizing-data-jungle-part-i-lets-make-graph)。 这是一个比我使用的脚本更灵活、更容易维护的解决方案。 集成 Python 模块进行文件管理和绘制图表的简便性,包括可以分享的交互式 html,意味着输出可以有更大用处。 以 Fabian 的帖子为基础,我编写了 __Yape__,旨在快速简单地提取客户的多种格式的 pButtons 文件,然后绘制图表。 该项目已[在 GitHub 上发布](https://github.com/murrayo/yape),您可以下载、运行并根据需要进行修改。 ## 概述 目前,此过程有_两个_步骤。 ### 步骤 1. `extract_pButtons.py` 从 pButtons 提取感兴趣的部分并写入到 .csv 文件,以便使用 Excel 打开或使用 `graph_pButtons.py` 进行绘图处理。 ### 步骤 2. `graph_pButtons.py` 绘制步骤 1 中创建的文件的图表。 目前,输出可以是 `.png` 形式的线形图或点阵图,也可以是带有平移、缩放、打印等选项的`交互式 .html`。 GitHub 上的 _Readme.md_ 详细介绍了如何设置和运行这两个 python 脚本,并且将是最新的参考。 ## 其他说明 例如:使用向输出和输入目录添加前缀的选项,可以轻松遍历包含一组(例如一个星期)pButtons html 文件的目录,并针对每个 pButtons 文件都输出到一个单独目录。 for i in `ls *.html`; do ./extract_pButtons.py $i -p ${i}_; done for i in `ls *.html`; do ./graph_pButtons.py ./${i}_metrics -p ${i}_; done 在短期内,当我继续撰写有关 [Caché 容量规划和性能](https://community.intersystems.com/post/intersystems-data-platforms-capacity-planning-and-performance-series-index)的系列文章时,我将使用由这些实用工具创建的图表。 我已经在 OSX 上进行了测试,但没有在 Windows 上测试。 您应该能够在 Windows 上安装和运行 Python,请留下您在 Windows 下的经验反馈。 例如,我猜想必须对文件路径斜杠进行更改。 > 注:直到几周前,我都没有用 Python 编写过任何东西,所以如果您是 Python 专家,那么代码中可能会有一些内容并不是最佳做法。 但是,我几乎每天都使用这些脚本,因此我将继续进行改进。 我希望我的 Python 技能会有所提高 — 但是如果您看到一些应该纠正的地方,请随意“教导”我! 如果您发现这些脚本有用,请告诉我,并不时回来看看以获取新功能和更新。
文章
姚 鑫 · 七月 11, 2022

第二章 嵌入式Python概述(二)

# 第二章 嵌入式Python概述(二) # 从 Python 调用 IRIS API 如果使用的是嵌入式 `Python` 并且需要与 `IRIS` 交互,可以使用 `Python shell` 中的 `iris` 模块,或者使用 `Python` 编写的 `IRIS` 类中的方法。要遵循本节中的示例,可以使用 `ObjectScript` 命令 `do ##class(%SYS.Python).Shell()` 从终端会话启动 `Python shell`。 当启动终端会话时,将被放置在 `IRIS` 的 `USER` 命名空间中,将看到提示 `USER>`。但是,如果从 `GitHub` 加载了示例类,则需要在 `SAMPLES` 命名空间中才能访问它们。 在终端中,更改为 `SAMPLES` 命名空间,然后启动 `Python shell`,如下所示: ```java USER>set $namespace = "SAMPLES" SAMPLES>do ##class(%SYS.Python).Shell() Python 3.9.5 (default, Jul 19 2021, 17:50:44) [MSC v.1927 64 bit (AMD64)] on win32 Type quit() or Ctrl-D to exit this shell. >>> ``` 当从终端会话启动 `Python shell` 时,`Python shell` 继承与终端相同的上下文,例如,当前命名空间和用户。局部变量不被继承。 ## 使用类 要从 `Python` 访问 `IRIS` 类,请使用 `iris` 模块来实例化要使用的类。然后,可以像访问 `Python` 类一样使用访问它的属性和方法。 注意:可能习惯于在 `Python` 中导入模块,然后再使用它,例如: ```java >>> import iris ``` 但是,在使用 `%SYS.Python` 类的 `Shell()` 方法运行 `Python shell `时,不需要显式导入 `iris` 模块。继续使用该模块。 以下示例使用系统类 `%Library.File` 的 `ManagerDirectory()` 方法打印 `IRIS` 管理器目录的路径: ```java >>> lf = iris.cls('%Library.File') >>> print(lf.ManagerDirectory()) C:\InterSystems\IRIS\mgr\ ``` 此示例使用系统类 `%SYSTEM.CPU` 的 `Dump()` 方法来显示有关正在运行 `IRIS` 实例的服务器的信息: ```java >>> cpu = iris.cls('%SYSTEM.CPU') >>> cpu.Dump() -- CPU Info for node MYSERVER ---------------------------------------------- Architecture: x86_64 Model: Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz Vendor: Intel # of threads: 4 # of cores: 2 # of chips: 1 # of threads per core: 2 # of cores per chip: 2 MT supported: 1 MT enabled: 1 MHz: 2904 ------------------------------------------------------------------------------ ``` 此示例使用 `GitHub` 上 `Samples-Data` 存储库中的 `Sample.Company` 类。虽然可以使用任何命名空间中以百分号 (`%`) 开头的类(如 `%SYS.Python` 或 `%Library.File`)来访问 `Sample.Company` 类,但如前所述,必须位于 `SAMPLES` 命名空间中。 `Sample.Company` 的类定义如下: ```java Class Sample.Company Extends (%Persistent, %Populate, %XML.Adaptor) { /// The company's name. Property Name As %String(MAXLEN = 80, POPSPEC = "Company()") [ Required ]; /// The company's mission statement. Property Mission As %String(MAXLEN = 200, POPSPEC = "Mission()"); /// The unique Tax ID number for the company. Property TaxID As %String [ Required ]; /// The last reported revenue for the company. Property Revenue As %Integer; /// The Employee objects associated with this Company. Relationship Employees As Employee [ Cardinality = many, Inverse = Company ]; } ``` 此类扩展 `%Library.Persistent`(通常缩写为 `%Persistent`),这意味着此类的对象可以持久保存在 `IRIS` 数据库中。该类还具有多个属性,包括 `Name` 和 `TaxID`,这两个属性都是保存对象所必需的。 尽管不会在类定义中看到它们,但持久类带有许多用于操作此类对象的方法,例如 `%New()`、`%Save()`、`%Id()` 和 `%OpenId()`。但是,`Python` 方法名称中不允许使用百分号 (`%`),因此请改用下划线 (`_`)。 下面的代码创建一个新的 `Company` 对象,设置所需的 `Name` 和 `TaxID` 属性,然后将公司保存在数据库中: ```java >>> myCompany = iris.cls('Sample.Company')._New() >>> myCompany.Name = 'Acme Widgets, Inc.' >>> myCompany.TaxID = '123456789' >>> status = myCompany._Save() >>> print(status) 1 >>> print(myCompany._Id()) 22 ``` 上面的代码使用 `_New()` 方法创建类的实例,并使用 `_Save()` 将实例保存在数据库中。 `_Save()` 方法返回一个状态码。在这种情况下,`1` 表示保存成功。当保存一个对象时, `IRIS` 会为其分配一个唯一 `ID`,可以在以后使用该 `ID` 从存储中检索该对象。 `_Id()` 方法返回对象的 `ID`。 使用类的 `_OpenId()` 方法将对象从持久存储中检索到内存中进行处理: ```java >>> yourCompany = iris.cls("Sample.Company")._OpenId(22) >>> print(yourCompany.Name) Acme Widgets, Inc. ``` 将以下代码添加到类定义中会创建一个 `Print()` 方法,该方法打印当前公司的 `Name` 和 `TaxID`。将 `Language` 关键字设置为 `python` 会告诉类编译器该方法是用 `Python` 编写的。 ```java Method Print() [ Language = python ] { print ('\nName: ' + self.Name + ' TaxID: ' + self.TaxID) } ``` 给定一个 `Company` 对象,可以调用它的 `Print()` 方法,如下所示: ```java >>> yourCompany.Print() Name: Acme Widgets, Inc. TaxID: 123456789 ```
文章
Nicky Zhu · 五月 7, 2022

一个新库引发的血案

我们的一位客户五一期间向使用IRIS搭建的数据流推送一家三甲医院数年的历史数据,导致实施的同事们经历了一系列噩梦,包括但不限与: 1. 由于未通知实施团队有这样规模的数据推送,数据推送过程与全库备份任务重叠。尽管实例和数据流正常运行,但备份任务与数据流争抢IO,导致备份任务不能在预期时间内完成,实施童鞋五一加班处理问题。 2. 为了节省磁盘空间,服务器上部署了定期删除IRIS备份文件的任务,原本能够保持一周的全备+增量备份,但在本次数据暴增的情况下,新的备份尚未完成而旧的全备已被删除,导致问题发生时没有可用于恢复的备份。 3. 由于这次数据推送前未进行数据质量校验,推送的数据全部不合规,但已经历了较长的数据流进行处理全部入库;同时由于备份文件已被删除,无法通过恢复数据库的方法回滚,导致实施童鞋不得不逐条从生产环境三个库的数百张表中挑出问题数据逐一删除,从五一放假结束至今还未完成善后工作。大家可以设想一下,如果备份还在,那么恢复备份就可以了。 因此,我们希望再次提醒各位在前线奋斗的亲们: 1. 善待你的备份。尽管对于大型医院或医疗集团来说,两周的全备+增量备份策略下,备份文件会占据数个TB的存储空间。但在需要回滚时,这几个T的空间能救命。 2. 保持可用的测试环境。尤其是对于可能出现随机数据需求的客户,随机产生数据需求意味着随机出现测试需求。 3. 验证新数据的合规性,永远不要假设新数据一定合规。未经测试的新数据必然毫无悬念地导致新问题。 4. 对于任何批量数据处理任务,请务必提前规划,错开资源(CPU、内存、IO)的抢占,避免抢不到资源的任务饿饭。 5. 保持与最终客户的频繁沟通,所有对于生产环境进行的改动都应该经过项目组评估。虽然客户是上帝,但命运有时很顽皮,生产环境的安全保障也需要客户的合作。 最后,大家都知道InterSystems的IRIS在多数客户的场景下都不需要搭建负载均衡集群,这家客户也不例外,数据流中的数层结点上部署的都是单实例IRIS,通过Mirror实现高可用。在这次新数据的上传过程中,IRIS的数据流自然经历了突如其来的爆发式数据压力,以其中一个实例的消息量为例: 该用户在实例上保存30天的数据,可见在经历了五一的消息暴增之后,该客户的每日平均消息量已超过3300万条每天(实际上我们已经查到其中数天单日消息增量已超过5000万条),而该客户平时的消息量不过数十万条每天。 这次IRIS经洪峰而不倒固然可喜可贺,但相信在需要在客户面前经历各种千夫所指的PM、实施、开发与测试同事一定不希望经历这种惊喜。 Good luck. 值得一篇专门的文章介绍客户应该怎么使用备份以及数据推流
文章
姚 鑫 · 七月 27, 2022

第九章 REST 服务安全

# 第九章 REST 服务安全 如果 `REST` 服务正在访问机密数据,应该对服务使用身份验证。如果需要为不同的用户提供不同级别的访问权限,还要指定端点所需的权限。 # 为 `REST` 服务设置身份验证 可以对 `IRIS REST` 服务使用以下任何形式的身份验证: - `HTTP` 身份验证标头 — 这是 `REST` 服务的推荐身份验证形式。 - `Web` 会话身份验证 — 其中用户名和密码在 URL 中的问号后面指定。 - `OAuth 2.0` 身份验证 - 请参阅以下小节。 ## `REST` 应用程序和 `OAuth 2.0` 要通过 `OAuth 2.0` 对 `REST` 应用程序进行身份验证,请执行以下所有操作: - 将包含 `REST` 应用程序的资源服务器配置为 `OAuth 2.0` 资源服务器。 - 允许对 `%Service.CSP` 进行委派身份验证。 - 确保将 `Web` 应用程序(用于 `REST` 应用程序)配置为使用委托身份验证。 - 在 `%SYS` 命名空间中创建一个名为 `ZAUTHENTICATE` 的例程。 提供了一个示例例程 `REST.ZAUTHENTICATE.mac`,可以复制和修改它。此例程是 GitHub (https://github.com/intersystems/Samples-Security) 上 Samples-Security 示例的一部分。可以按照“下载用于 IRIS 的示例”中的说明下载整个示例,但在 `GitHub` 上打开例程并复制其内容可能更方便。 在例程中,修改 `applicationName` 的值并根据需要进行其他更改。 ## 指定使用 `REST` 服务所需的权限 为了指定执行代码或访问数据所需的权限, 技术使用基于角色的访问控制 (`RBAC`)。 如果需要为不同的用户提供不同级别的访问权限,请执行以下操作来指定权限: - 修改规范类以指定使用 `REST` 服务或 `REST` 服务中的特定端点所需的权限;然后重新编译。权限是与资源名称组合的权限(例如读取或写入)。 - 使用管理门户: - 定义在规范类中引用的资源。 - 定义提供权限集的角色。例如,角色可以提供对端点的读取访问权限或对不同端点的写入访问权限。一个角色可以包含多组权限。 - 将用户置于其任务所需的所有角色中。 此外,可以使用 `%CSP.REST` 类的 `SECURITYRESOURCE` 参数来执行授权。 # 指定权限 可以为整个 `REST` 服务指定权限列表,也可以为每个端点指定权限列表。为此: 1. 要指定访问服务所需的权限,请编辑规范类中的 `OpenAPI XData` 块。对于 `info` 对象,添加一个名为 `x-ISC_RequiredResource` 的新属性,其值是以逗号分隔的已定义资源列表及其访问模式 (`resource:mode`),这是访问 `REST` 服务的任何端点所必需的。 下面显示了一个示例: ```java "swagger":"2.0", "info":{ "version":"1.0.0", "title":"Swagger Petstore", "description":"A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", "termsOfService":"http://swagger.io/terms/", "x-ISC_RequiredResource":["resource1:read","resource2:read","resource3:read"], "contact":{ "name":"Swagger API Team" }, ... ``` 2. 要指定访问特定端点所需的权限,请将 `x-ISC_RequiredResource` 属性添加到定义该端点的操作对象,如下例所示: ```java "post":{ "description":"Creates a new pet in the store. Duplicates are allowed", "operationId":"addPet", "x-ISC_RequiredResource":["resource1:read","resource2:read","resource3:read"], "produces":[ "application/json" ], ... ``` 3. 编译规范类。此操作重新生成调度类。 ## 使用 `SECURITYRESOURC` 参数 作为附加的授权工具,分派 `%CSP.REST` 子类的类具有 `SECURITYRESOURCE` 参数。 `SECURITYRESOURCE` 的值要么是资源及其权限,要么只是资源(在这种情况下,相关权限是使用)。系统检查用户是否对与 `SECURITYRESOURCE` 关联的资源具有所需的权限。 注意:如果调度类为 `SECURITYRESOURCE` 指定了一个值,并且 `CSPSystem` 用户没有足够的权限,那么这可能会导致登录尝试失败时出现意外的 `HTTP` 错误代码。为防止这种情况发生, 建议您将指定资源的权限授予 `CSPSystem` 用户。
文章
Michael Lei · 十二月 7, 2022

创建基于 FHIR 的表单

Intersystems IRIS for Health 对 FHIR 行业标准提供了出色的支持。主要特点是:1.FHIR 服务器2. FHIR数据库3. REST 和 ObjectScript API 用于 FHIR 资源(患者、问卷、疫苗等)的 CRUD 操作 本文演示了如何使用这些功能,并展示了用于创建和查看表单类型的 FHIR 资源的Angula前端。 第 1 步 - 使用 InterSystems IRIS for Health 部署您的 FHIR 服务器 要创建 FHIR 服务器,您必须将以下说明添加到 iris.script 文件中(来自:https://openexchange.intersystems.com/package/iris-fhir-template) zn "HSLIB" set namespace= "FHIRSERVER" Set appKey = "/fhir/r4" Set strategyClass = "HS.FHIRServer.Storage.Json.InteractionsStrategy" set metadataPackages = $lb ( "hl7.fhir.r4.core@4.0.1" ) set importdir= "/opt/irisapp/src" //Install a Foundation namespace and change to it Do ##class (HS.HC.Util.Installer).InstallFoundation(namespace) zn namespace // Install elements that are required for a FHIR-enabled namespace Do ##class (HS.FHIRServer.Installer).InstallNamespace() // Install an instance of a FHIR Service into the current namespace Do ##class (HS.FHIRServer.Installer).InstallInstance(appKey, strategyClass, metadataPackages) // Configure FHIR Service instance to accept unauthenticated requests set strategy = ##class (HS.FHIRServer.API.InteractionsStrategy).GetStrategyForEndpoint(appKey) set config = strategy.GetServiceConfigData() set config.DebugMode = 4 do strategy.SaveServiceConfigData(config) zw ##class (HS.FHIRServer.Tools.DataLoader).SubmitResourceFiles( "/opt/irisapp/fhirdata/" , "FHIRServer" , appKey) do $System .OBJ.LoadDir( "/opt/irisapp/src" , "ck" ,, 1 ) zn "%SYS" Do ##class (Security.Users).UnExpireUserPasswords( "*" ) zn "FHIRSERVER" zpm "load /opt/irisapp/ -v" : 1 : 1 //zpm "install fhir-portal" halt 使用实用程序类 HS.FHIRServer.Installer,您可以创建 FHIR 服务器。 第 2 步 - 使用 FHIR REST 或 ObjectScript API 读取、更新、删除和查找 FHIR 数据 我喜欢使用 ObjectScript 类 HS.FHIRServer.Service 来执行所有 CRUD 操作。 要从资源类型(表单)中获取所有 FHIR 数据: /// Retreive all the records of questionnaire ClassMethod GetAllQuestionnaire() As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" set request.RequestMethod = "GET" do fhirService.DispatchRequest(request, .pResponse) set json = pResponse.Json set resp = [] set iter = json.entry. %GetIterator () while iter. %GetNext (.key, .value) { do resp. %Push (value.resource) } write resp. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on get all questionnairies" } Quit tSC } 要从 FHIR 数据存储库中获取特定数据项(调查表): /// Retreive a questionnaire by id ClassMethod GetQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "GET" do fhirService.DispatchRequest(request, .pResponse) write pResponse.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on get the questionnaire" } Quit tSC } 要创建新的 FHIR 资源事件(新的调查表): /// Create questionnaire ClassMethod CreateQuestionnaire() As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" set request.RequestMethod = "POST" set data = {}. %FromJSON ( %request.Content ) set data.resourceType = "Questionnaire" set request.Json = data do fhirService.DispatchRequest(request, .response) write response.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on create questionnaire" } Return tSC } 要更新 FHIR 资源(表单): /// Update a questionnaire ClassMethod UpdateQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "PUT" set data = {}. %FromJSON ( %request.Content ) set data.resourceType = "Questionnaire" set request.Json = data do fhirService.DispatchRequest(request, .response) write response.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on update questionnaire" } Return tSC } 要删除 FHIR 资源事件(表单): /// Delete a questionnaire by id ClassMethod DeleteQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "DELETE" do fhirService.DispatchRequest(request, .pResponse) } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on delete the questionnaire" } Quit tSC } 如您所见,您想要创建使用 POST,更新使用 PUT,删除使用 DELETE,查询使用 GET 动词。 第 3 步 - 创建 Angular 客户端以使用您的 FHIR 服务器应用程序 我使用 PrimeNG 创建了一个角度应用程序并安装了包 npm install --save @types/fhir。此包具有映射到 TypeScript 的所有 FHIR 类型。 Angular控制器类: import { Component, OnInit, ViewEncapsulation } from '@angular/core' ; import { ActivatedRoute, Router } from '@angular/router' ; import { Period, Questionnaire } from 'fhir/r4' ; import { ConfirmationService, MessageService, SelectItem } from 'primeng/api' ; import { QuestionnaireService } from './questionnaireservice' ; const QUESTIONNAIREID = 'questionnaireId' ; @Component({ selector: 'app-questionnaire', templateUrl: './questionnaire.component.html', providers: [MessageService, ConfirmationService], styleUrls: ['./questionnaire.component.css'], encapsulation: ViewEncapsulation.None }) export class QuestionnaireComponent implements OnInit { public questionnaire: Questionnaire ; public questionnairies: Questionnaire[] ; public selectedQuestionnaire: Questionnaire ; public questionnaireId: string ; public sub: any ; public publicationStatusList: SelectItem[] ; constructor( private questionnaireService: QuestionnaireService, private router: Router, private route: ActivatedRoute, private confirmationService: ConfirmationService, private messageService: MessageService){ this.publicationStatusList = [ {label: 'Draft', value: 'draft'}, {label: 'Active', value: 'active'}, {label: 'Retired', value: 'retired'}, {label: 'Unknown', value: 'unknown'} ] } ngOnInit() { this.reset() ; this.listQuestionnaires() ; this.sub = this.route.params.subscribe(params => { this.questionnaireId = String(+params[QUESTIONNAIREID]) ; if (!Number.isNaN(this.questionnaireId)) { this.loadQuestionnaire(this.questionnaireId) ; } }) ; } private loadQuestionnaire(questionnaireId) { this.questionnaireService.load(questionnaireId).subscribe(response => { this.questionnaire = response ; this.selectedQuestionnaire = this.questionnaire ; if (!response.effectivePeriod) { this.questionnaire.effectivePeriod = <Period>{} ; } }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load questionnaire.' }) ; }) ; } public loadQuestions() { if (this.questionnaire && this.questionnaire.id) { this.router.navigate(['/question', this.questionnaire.id]) ; } else { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Choose a questionnaire.' }) ; } } private listQuestionnaires() { this.questionnaireService.list().subscribe(response => { this.questionnairies = response ; this.reset() ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load the questionnaries.' }) ; }) ; } public onChangeQuestionnaire() { if (this.selectedQuestionnaire && !this.selectedQuestionnaire.id) { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' }) ; } else { if (this.selectedQuestionnaire && this.selectedQuestionnaire.id) { this.loadQuestionnaire(this.selectedQuestionnaire.id) ; } } } public reset() { this.questionnaire = <Questionnaire>{} ; this.questionnaire.effectivePeriod = <Period>{} ; } public save() { if (this.questionnaire.id && this.questionnaire.id != "" ) { this.questionnaireService.update(this.questionnaire).subscribe( (resp) => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire saved.' }) ; this.listQuestionnaires() this.loadQuestionnaire(this.questionnaire.id) ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on save the questionnaire.' }) ; } ) ; } else { this.questionnaireService.save(this.questionnaire).subscribe( (resp) => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire saved.' }) ; this.listQuestionnaires() this.loadQuestionnaire(resp.id) ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on save the questionnaire.' }) ; } ) ; } } public delete(id: string) { if (!this.questionnaire || !this.questionnaire.id) { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' }) ; } else { this.confirmationService.confirm({ message: ' Do you confirm?', accept: () => { this.questionnaireService.delete(id).subscribe( () => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire deleted.' }) ; this.listQuestionnaires() ; this.reset() ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on delete questionnaire.' }) ; } ) ; } }) ; } } } Angular HTML 文件 <p-toast [style]= "{marginTop: '80px', width: '320px'}" ></p-toast> <p-card> <div class = "p-fluid p-formgrid grid" > <div class = "field col-12 lg:col-12 md:col-12" > <p-dropdown id= "dropquestions1" [options]= "questionnairies" [(ngModel)]= "selectedQuestionnaire" (onChange)= "onChangeQuestionnaire()" placeholder= "Select a Questionnaire" optionLabel= "title" [filter]= "true" [showClear]= "true" ></p-dropdown> </div> </div> <p-tabView> <p-tabPanel leftIcon= "fa fa-question" header= "Basic Data" > <div class = "p-fluid p-formgrid grid" > <div class = "field col-3 lg:col-3 md:col-12" > <label for = "txtname" >Name</label> <input class = "inputfield w-full" id= "txtname" required type= "text" [(ngModel)]= "questionnaire.name" pInputText placeholder= "Name" > </div> <div class = "field col-7 lg:col-7 md:col-12" > <label for = "txttitle" >Title</label> <input class = "inputfield w-full" id= "txttitle" required type= "text" [(ngModel)]= "questionnaire.title" pInputText placeholder= "Title" > </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtdate" >Date</label> <p-inputMask id= "txtdate" mask= "9999-99-99" [(ngModel)]= "questionnaire.date" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtstatus" >Status</label> <p-dropdown [options]= "publicationStatusList" [(ngModel)]= "questionnaire.status" ></p-dropdown> </div> <div class = "field col-3 lg:col-3 md:col-12" > <label for = "txtpublisher" >Publisher</label> <input class = "inputfield w-full" id= "txtpublisher" required type= "text" [(ngModel)]= "questionnaire.publisher" pInputText placeholder= "Publisher" > </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtstartperiod" >Start Period</label> <p-inputMask id= "txtstartperiod" mask= "9999-99-99" [(ngModel)]= "questionnaire.effectivePeriod.start" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtendperiod" >End Period</label> <p-inputMask id= "txtendperiod" mask= "9999-99-99" [(ngModel)]= "questionnaire.effectivePeriod.end" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-12 lg:col-12 md:col-12" > <label for = "txtcontent" >Description</label> <p-editor [(ngModel)]= "questionnaire.description" [style]= "{'height':'100px'}" ></p-editor> </div> </div> <div class = "grid justify-content-end" > <button pButton pRipple type= "button" label= "New Record" (click)= "reset()" class = "p-button-rounded p-button-success mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Save" (click)= "save()" class = "p-button-rounded p-button-info mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Delete" (click)= "delete(questionnaire.id)" class = "p-button-rounded p-button-danger mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Questions" (click)= "loadQuestions()" class = "p-button-rounded p-button-info mr-2 mb-2" ></button> </div> </p-tabPanel> </p-tabView> </p-card> <p-confirmDialog #cd header= "Atenção" icon= "pi pi-exclamation-triangle" > <p-footer> <button type= "button" pButton icon= "pi pi-times" label= "Não" (click)= "cd.reject()" ></button> <button type= "button" pButton icon= "pi pi-check" label= "Sim" (click)= "cd.accept()" ></button> </p-footer> </p-confirmDialog> 角度服务类 import { Injectable } from '@angular/core' ; import { HttpClient, HttpHeaders } from '@angular/common/http' ; import { Observable } from 'rxjs' ; import { environment } from 'src/environments/environment' ; import { take } from 'rxjs/operators' ; import { Questionnaire } from 'fhir/r4' ; @Injectable({ providedIn: 'root' }) export class QuestionnaireService { private url = environment.host2 + 'questionnaire' ; constructor(private http: HttpClient) { } public save(Questionnaire: Questionnaire): Observable<Questionnaire> { return this.http.post<Questionnaire>(this.url, Questionnaire).pipe(take( 1 )) ; } public update(Questionnaire: Questionnaire): Observable<Questionnaire> { return this.http.put<Questionnaire>(`${this.url}/${Questionnaire.id}`, Questionnaire).pipe(take( 1 )) ; } public load(id: string): Observable<Questionnaire> { return this.http.get<Questionnaire>(`${this.url}/${id}`).pipe(take( 1 )) ; } public delete(id: string): Observable<any> { return this.http.delete(`${this.url}/${id}`).pipe(take( 1 )) ; } public list(): Observable<Questionnaire[]> { return this.http.get<Questionnaire[]>(this.url).pipe(take( 1 )) ; } } 第 4 步 - 实际应用 1. 转到 https://openexchange.intersystems.com/package/FHIR-Questionnaires 应用程序。 2. clone/git pull repo 到任何本地目录 $ git clone https://github.com/yurimarx/fhir-questions.git 3、在该目录下打开终端,运行: $ docker-compose up -d 4.打开网络应用程序: http://localhost:52773/fhirquestions/index.html 截图:
文章
Michael Lei · 一月 10, 2022

翻译文章--数字孪生医疗--新医疗生态系统的伟大推动者?

正如德勤最近的一份报告所强调的,数字孪生的设计和部署是为了实现虚拟协作,吸收和处理大数据,以及协助我们以更有效和安全的方式管理物理世界。 有几个行业在使用数字孪生能力方面取得了重大进展,如汽车、飞机、能源、城市规划和医疗保健等,仅举几例。疫情的影响和对全球经济造成的破坏只会加速全球数字孪生的发展速度和采用。因此,预计到2026年,全球数字孪生市场将达到482亿美元,年复合增长率为58%。虽然目前北美市场拥有最大的份额,但预计亚太市场将在未来几年经历最快的增长。 按照Gartner的说法,数字孪生是现实世界实体或系统的数字代表。数字孪生体的实现是一个封装的软件对象或模型,它反映了一个独特的物理对象、过程、组织、人或其他抽象概念。 数字孪生的设计和部署是复杂的,与其他数字技术密切相关,如云计算、人工智能、物联网、5G网络、区块链和虚拟、增强或混合现实。预计所有这些技术的采用率将对数字孪生产业产生深远影响。迄今为止,系统数字孪生体已经在所有行业中占主导地位,但专家们一致认为,产品和流程数字孪生体类型将在未来几年内上升。 作为一个社会,我们可以通过部署这些高度通用的数字化身阿凡达获得巨大的利益,因为它们可以优化效率,降低长期成本,提高质量,并进一步提高我们的研究能力。 医疗保健是一个很好的例子,数字孪生医疗有可能解决我们在全球医疗保健领域面临的许多主要挑战。从医疗服务的角度来看,它们可以提高医疗质量和安全,改善临床决策支持系统,加速各种药物从研究到临床部署的过渡,并加快各种新的精准医疗解决方案的批准。对于医疗设备行业来说,这也可能意味着更短的审批周期和更高的安全性。在医学教育中,它们可以促进更多的互动教学方法和更有效的安全培训。对于外科、肿瘤学或精准医学等不同学科来说,拥有这些数字孪生可能是革命性的、颠覆性的。最后且很重要的是,数字孪生还可以让我们发展一个超级连接的全球医疗保健网络,并获得重要的洞察,从而推动研究和创新诊断或治疗方法的发展。 然而,为了取得成功,我们还需要克服任何新兴技术所面临的一些挑战,并减轻恶意使用的风险。具体而言,道德、网络安全问题和跨行业标准将被列入大多数希望部署数字孪生的企业的议程。 数字化旅程是漫长的,然而所有的最新研究都指向数字技术的加速采用。通过将数字孪生与所有其他新兴技术相结合的部署,我们有可能建立一个高性能的全球医学互联网,它可以成为全球医疗保健生态系统的管道。通过利用脑机接口、植入物、可穿戴设备、增强智能、区块链、5G网络速度和云计算的全部能力,我们将实现指数级的效果。医疗保健未来学家甚至会大胆设想存在着最先进的智能城市,在那里,数字医疗孪生、数字金融孪生和数字法律孪生将成为日常生活的一部分。 原文链接:https://www.bbntimes.com/science/digital-health-twins-the-great-enablers-of-new-healthcare-ecosystems?suid=SU00014&medium=li&cmp=566 作者介绍: Ingrid Vasiliu-Feltes博士, 是一位医疗高管、未来学家和全球主义者。她是福布斯商业委员会成员、数字战略家、教育家和创业生态系统建设者。在她的职业生涯中,她因在研究、教学或领导方面的卓越表现而获得多个奖项。2021年WBAF世界卓越奖社会创业奖,数字孪生技术全球20强领导人,健康技术全球50强领导人,全球生态系统50强领导人,2021年教育领域100强远见奖,2021年全球100强女性领导力奖。2021年世界女性远见奖100强--创新与技术,2021年社会企业女性100强(被提名者),全球思想家50强(被提名者),女性变革者国家奖(入围),2020年医疗保健领袖100强,2020年金融领袖100强,以及2020年加密货币领域女性100强。此外,她还担任欧盟区块链观察站论坛的专家顾问,并被任命为联合国法律和经济赋权网络的董事会成员。Vasiliu-Feltes博士是Softhread公司的首席执行官,科学、创业和投资研究所的创始人和首席执行官,目前担任WBAF美国的国家主任,WBAF的参议员,WBAF商学院-创业部的教员,并在UM商学院教授行政MBA商业技术课程。她还担任政府区块链协会的首席创新官。最近,她担任Detect Genomix的总裁,Mednax的首席质量和安全官首席和创新官,迈阿密大学UHealth系统的首席质量和安全官和合规主管 在她的学术任期内,她在医学院内教授多门课程,以及MD/PhD和MD/MPH联合课程。在她的职业生涯中,Vasiliu-Feltes博士担任过几个领导职位,是许多著名专业组织的成员。她拥有多项认证,如哈佛大学的生物伦理学、麻省理工学院斯隆商学院的人工智能和商业战略、麻省理工学院斯隆商学院的区块链技术和商业创新、哈佛商学院的金融学、哈佛法学院的谈判学、斯坦福大学商学院的创新和创业学、医疗风险管理认证专家、美国医疗主管学院院士。国际安全经理人委员会联合会的病人安全官员,精益和六西格玛管理黑带大师,全国医疗质量协会的医疗质量专业人员,美国质量协会的质量和组织卓越经理,以及美国医疗风险管理协会的认证风险管理专业人员。此外,Vasiliu-Feltes博士还是多家公司的荣誉顾问委员会成员,以及多家国际出版物的编辑委员会成员、作家和电视/媒体合作伙伴。
文章
姚 鑫 · 三月 27, 2021

第十三章 使用动态SQL(五)

# 第十三章 使用动态SQL(五) # 从结果集中返回特定的值 要从查询结果集中返回特定的值,必须一次一行遍历结果集。 要遍历结果集,请使用`%Next()`实例方法。 (对于单一值,结果对象中没有行,因此`%Next()`返回0,而不是错误。) 然后,可以使用`%Print()`方法显示整个当前行的结果,或者检索当前行的指定列的值。 `%Next()`方法获取查询结果中下一行的数据,并将该数据放入结果集对象的data属性中。 `%Next()`返回1,表示它位于查询结果中的某一行上。 `%Next()`返回0,表示它位于最后一行(结果集的末尾)之后。 每次调用`%Next()`返回1个增量`%ROWCOUNT`; 如果游标定位在最后一行之后(`%Next()`返回0),`%ROWCOUNT`表示结果集中的行数。 如果`SELECT`查询只返回聚合函数,每个`%Next()`设置`%ROWCOUNT=1`。 第一个`%Next()`返回1并设置`%SQLCODE=0`和`%ROWCOUNT=1`,即使表中没有数据; 任何随后的`%Next()`返回0,并设置`%SQLCODE=100`和`%ROWCOUNT=1`。 从结果集中获取一行后,可以使用以下任何一种方式显示该行的数据: - `rset.%Print()`返回查询结果集中当前行的所有数据值。 - `rset.%GetRow()`和`rset.getrows()`以编码列表结构的元素形式从查询结果集中返回一行的数据值。 - `rset.name`按查询结果集中的属性名称、字段名称、别名属性名称或别名字段名称返回数据值。 - `rset.%Get("fieldname")`通过字段名或别名从查询结果集中或存储的查询返回一个数据值。 - `rset.%GetData(n)`按列号从查询结果集中或存储的查询中返回一个数据值。 ## %Print()方法 `%Print()`实例方法从结果集中检索当前记录。默认情况下,`%Print()`在数据字段值之间插入空白空格分隔符。 `%Print()`不会在记录的第一个字段值之前或最后一个字段值之后插入空白; 它在记录的末尾发出一个行返回。 如果数据字段值已经包含空格,则将该字段值括在引号中,以将其与分隔符区分开来。 例如,如果`%Print()`返回城市名称,它将按如下方式返回它们: `"New York" Boston Atlanta "Los Angeles" "Salt Lake City" Washington`. 引用包含分隔符作为数据值一部分的字段值,即使从未使用过`%Print()`分隔符; 例如,如果结果集中只有一个字段。 可以选择指定`%Print()`参数,该参数提供在字段值之间放置的另一个定界符。指定其他定界符将覆盖包含空格的数据字符串的引用。此`%Print()`分隔符可以是一个或多个字符。它指定为带引号的字符串。通常,`%Print()`分隔符最好是在结果集数据中找不到的字符或字符串。但是,如果结果集中的字段值包含`%Print()`分隔符(或字符串),则该字段值将用引号引起来,以将其与分隔符区分开。 如果结果集中的字段值包含换行符,则该字段值将以引号引起来。 以下ObjectScript示例使用`%Print()`遍历查询结果集以显示每个结果集记录,并使用 `"^|^"` 定界符分隔值。请注意`%Print()`如何显示`FavoriteColors`字段中的数据,该字段是元素的编码列表: ```java /// d ##class(PHA.TEST.SQL).ROWCOUNTPrint() ClassMethod ROWCOUNTPrint() { SET q1="SELECT TOP 5 Name,DOB,Home_State,FavoriteColors " SET q2="FROM Sample.Person WHERE FavoriteColors IS NOT NULL" SET myquery = q1_q2 SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() WHILE rset.%Next() { WRITE "Row count ",rset.%ROWCOUNT,! DO rset.%Print("^|^") } WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT } ``` ```java DHC-APP> d ##class(PHA.TEST.SQL).ROWCOUNTPrint() Row count 1 yaoxin^|^54536^|^WI^|^$lb("Red","Orange","Yellow") Row count 2 姚鑫^|^^|^^|^$lb("Red","Orange","Yellow","Green") Row count 3 姚鑫^|^^|^^|^$lb("Red","Orange","Yellow","Green","Green") Row count 4 Isaacs,Roberta Z.^|^^|^^|^$lb("Red","Orange","Yellow","Green","Yellow") Row count 5 Chadwick,Zelda S.^|^50066^|^WI^|^$lb("White") End of data Total row count=5 ``` 下面的示例显示如何将包含定界符的字段值括在引号中。在此示例中,大写字母`A`用作字段定界符;因此,任何包含大写字母A的字段值(名称,街道地址或州缩写)都将以引号引起来。 ```java /// d ##class(PHA.TEST.SQL).ROWCOUNTPrint2() ClassMethod ROWCOUNTPrint2() { SET myquery = "SELECT TOP 25 Name,Home_Street,Home_State,Age FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() WHILE rset.%Next() { DO rset.%Print("A") } WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).ROWCOUNTPrint2() yaoxinA889 Clinton DriveAWIA30 xiaoliAAA 姚鑫AAA7 姚鑫AAA7 姚鑫AAA43 姚鑫AAA 姚鑫AAA Isaacs,Roberta Z.AAA Chadwick,Zelda S.A9889 Clinton DriveAWIA43 Fives,James D.A2091 Washington BlvdANDA88 Vonnegut,Jose P.A3660 Main PlaceAWIA47 Chadbourne,Barb B.A1174 Second StreetA"VA"A93 "Quigley,Barb A."A"6501 Ash Avenue"AKYA73 ``` ## %GetRow()和%GetRows()方法 `%GetRow()`实例方法从结果集中检索当前行(记录),作为字段值元素的编码列表: ```java /// d ##class(PHA.TEST.SQL).ROWCOUNTPrint3() ClassMethod ROWCOUNTPrint3() { SET myquery = "SELECT TOP 17 %ID,Name,Age FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() FOR { SET x=rset.%GetRow(.row,.status) IF x=1 { WRITE $LISTTOSTRING(row," | "),! } ELSE { WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT RETURN } } } ``` `%GetRows()`实例方法从结果集中检索指定大小的一组行(记录)。每行作为字段值元素的编码列表返回。 下面的示例返回结果集中的第1、6和11行。在此示例中,`%GetRows()`第一个参数(5)指定`%GetRows()`应该检索五行的连续组。如果成功检索到一组五行,`%GetRows()`将返回1。 `.rows`参数通过引用传递这五行的下标数组,因此,`rows(1)`返回每五组中的第一行:第1、6和11行。指定`rows(2)`将返回第2、7行和12。 ```java /// d ##class(PHA.TEST.SQL).ROWCOUNTPrint4() ClassMethod ROWCOUNTPrint4() { SET myquery = "SELECT TOP 17 %ID,Name,Age FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() FOR { SET x=rset.%GetRows(5,.rows,.status) IF x=1 { WRITE $LISTTOSTRING(rows(1)," | "),! } ELSE { WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT RETURN } } } ``` 可以使用`ZWRITE rows`命令返回检索到的数组中的所有下标,而不是按下标检索单个行。请注意,上面的示例ZWRITE行不会返回结果集中的第16行和第17行,因为在检索到最后一组五行之后,这些行是余数。 ## rset.name属性 当InterSystems IRIS生成结果集时,它将创建一个结果集类,其中包含一个与该结果集中的每个字段名称和字段名称别名相对应的唯一属性。 可以使用`rset.name`属性按属性名称,字段名称,属性名称别名或字段名称别名返回数据值。 - 属性名称:如果未定义字段别名,则将字段属性名称指定为`rset.PropName`。结果集字段属性名称取自表定义类中的相应属性名称。 - 字段名称:如果没有定义字段别名,请将字段名称(或属性名称)指定为`rset。“fieldname”`。这是表定义中指定的`SQLFIELDNAME`。 Intersystems Iris使用此字段名称来查找相应的属性名称。在许多情况下,属性名称和字段名称(`SQLFieldName`)是相同的。 - 别名属性名称:如果定义了字段别名,则将别名属性名称指定为`rset.AliasProp`。别名属性名称是根据`SELECT`语句中的列名称别名生成的。不能为具有已定义别名的字段指定字段属性名称。 - 别名:如果定义了字段别名,则将此别名(或别名属性名称)指定为`rset。“ alias”`。这是`SELECT`语句中的列名别名。您不能为具有已定义别名的字段指定字段名称。 - 集合,表达式或子查询:InterSystems IRIS为这些选择项分配一个字段名称`Aggregate_n`,`Expression_n`或`Subquery_n`(其中整数`n`对应于查询中指定的选择项列表的顺序)。可以使用字段名称(`rset。“ SubQuery_7”`不区分大小写),相应的属性名称(`rset.Subquery7`区分大小写)或用户定义的字段名称别名来检索这些select-item值。也可以只使用`rset。%GetData(n)`指定选择项的序列号。 指定属性名称时,必须使用正确的字母大小写;指定字段名称时,不需要正确的字母大小写。 使用属性名称对`rset.name`的调用具有以下后果: - 字母大小写:属性名称区分大小写。字段名称不区分大小写。 Dynamic SQL可以自动解决指定字段或别名与相应属性名称之间的字母大小写差异。但是,解决字母大小写需要时间。为了最大限度地提高性能,应该指定属性名称或别名的确切字母大小写。 - 非字母数字字符:属性名称只能包含字母数字字符(起始的`%`字符除外)。如果相应的SQL字段名称或字段名称别名包含非字母数字字符(例如`Last_Name`),则可以执行以下任一操作: - 指定用引号分隔的字段名称。例如,`rset。“ Last_Name”`)。分隔符的这种使用不需要启用分隔符。执行大写字母解析。 - 指定相应的属性名称,以消除非字母数字字符。例如,`rset.LastName`(或`rset。“ LastName”`)。必须为属性名称指定正确的字母大小写。 - `%`属性名称:通常,以`%`字符开头的属性名称保留供系统使用。如果字段属性名称或别名以`%`字符开头,并且该名称与系统定义的属性冲突,则返回系统定义的属性。例如,对于`SELECT Notes AS%Message`,调用`rset。%Message`将不返回`Notes`字段值。它返回为语句结果类定义的`%Message`属性。可以使用`rset。%Get(“%Message”)`返回字段值。 - 列别名:如果指定了别名,则Dynamic SQL始终匹配该别名,而不匹配字段名称或字段属性名称。例如,对于`SELECT Name AS Last_Name`,只能使用`rset.LastName`或`rset。“ Last_Name”`来检索数据,而不能使用`rset.Name`。 - 重复名称:如果名称解析为相同的属性名称,则它们是重复的。重复名称可以是对表中同一字段的多个引用,对表中不同字段的别名引用或对不同表中字段的引用。例如,`SELECT p.DOB,e.DOB`指定两个重复的名称,即使这些名称引用了不同表中的字段。 如果`SELECT`语句包含相同字段名称或字段名称别名的多个实例,则`rset.propname`或`rset。“fieldname”`始终返回`SELECT`语句中指定的第一个。例如,对于`SELECT C.NAME,P.NAME`来自`Sample.person as p,sample.company`使用`rset.name`检索公司名称字段数据;选择`C.Name,P.Name`作为来自`Sample.person`的名称,`As P,Sample.com`本文使用`RSET。“name”`还检索公司名称字段数据。如果查询中存在重复的名称字段,则字段名称(名称)的最后一个字符由字符(或字符)替换为创建唯一属性名称。因此,查询中的重复名称字段名称具有相应的唯一属性名称,以`NAM0`(第一个重复)通过`NAM9`开始,并通过`NAMZ`继续大写字母`NAMA`。 对于使用`%Prepare()`准备的用户指定的查询,可以单独使用属性名称。对于使用`%PrepareClassQuery()`准备的存储查询,必须使用`%Get(“ fieldname”)`方法。 下面的示例返回由属性名称指定的三个字段的值:两个属性值分别由属性名称和第三个属性值由别名属性名称。在这些情况下,指定的属性名称与字段名称或字段别名相同: ```java /// d ##class(PHA.TEST.SQL).PropSQL() ClassMethod PropSQL() { SET myquery = "SELECT TOP 5 Name,DOB AS bdate,FavoriteColors FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New(1) SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() WHILE rset.%Next() { WRITE "Row count ",rset.%ROWCOUNT,! WRITE rset.Name WRITE " prefers ",rset.FavoriteColors WRITE " birth date ",rset.bdate,!! } WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).PropSQL() Row count 1 yaoxin prefers Red,Orange,Yellow birth date 1990-04-25 Row count 2 xiaoli prefers birth date Row count 3 姚鑫 prefers birth date 2014-01-02 Row count 4 姚鑫 prefers birth date 2014-01-02 Row count 5 姚鑫 prefers birth date 1978-01-28 End of data Total row count=5 ``` 在上面的示例中,返回的字段之一是`FavoriteColors`字段,其中包含`%List`数据。若要显示此数据,`%New(1)`类方法将`%SelectMode`属性参数设置为1(ODBC),从而导致该程序将`%List`数据显示为逗号分隔的字符串,并以ODBC格式显示出生日期: 下面的示例返回`Home_State`字段。因为属性名称不能包含下划线字符,所以本示例指定用引号`(“ Home_State”)`分隔的字段名称`(SqlFieldName)`。还可以指定不带引号的相应生成的属性名称`(HomeState)`。请注意,定界字段名称`(“ Home_State”)`不区分大小写,但是生成的属性名称`(HomeState)`是区分大小写的: ```java /// d ##class(PHA.TEST.SQL).PropSQL1() ClassMethod PropSQL1() { SET myquery = "SELECT TOP 5 Name,Home_State FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New(2) SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT} SET rset = tStatement.%Execute() WHILE rset.%Next() { WRITE "Row count ",rset.%ROWCOUNT,! WRITE rset.Name WRITE " lives in ",rset."Home_State",! } WRITE !,"End of data" WRITE !,"Total row count=",rset.%ROWCOUNT } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).PropSQL1() Row count 1 yaoxin lives in WI Row count 2 xiaoli lives in Row count 3 姚鑫 lives in Row count 4 姚鑫 lives in Row count 5 姚鑫 lives in End of data Total row count=5 ``` %GetRows 这个方法在IRIS 2019里有吗?
文章
姚 鑫 · 五月 31, 2021

第十二章 IBM WebSphere MQ检索邮件

# 第十二章 IBM WebSphere MQ检索邮件 # 检索邮件 要检索邮件,请执行以下操作: 1. 按照“创建连接对象”中的说明创建连接对象。在这种情况下,请创建`%Net.MQRecv`的实例。`Connection`对象有一个消息队列,可以从中检索消息。 2. 根据需要调用以下方法: - `%Get()`-通过引用返回字符串消息作为第一个参数。 - `%GetStream()`-给定初始化的文件字符流,此方法从队列中检索消息,并将其放入与该流关联的文件中。请注意,必须设置流的`Filename`属性才能对其进行初始化。不支持二进制流。 3. 检查调用的方法返回的值。请参阅“获取错误代码”。请记住,当队列为空时,IBM `WebSphere MQ`返回`2033`。 4. 检索完消息后,调用`Connection`对象的`%Close()`方法以释放动态链接库的句柄。 示例1:`ReceiveString()` 下面的类方法从`mqtest`队列检索消息。 ```java ///Method returns string or null or error message ClassMethod ReceiveString() As %String { Set recv=##class(%Net.MQRecv).%New() Set queue="mqtest" Set qm="QM_antigua" Set chan="S_antigua/TCP/antigua(1414)" Set logfile="c:\mq-recv-log.txt" Set check=recv.%Init(queue,qm,chan,logfile) If 'check Quit recv.%GetLastError() Set check=recv.%Get(.msg) If 'check { Set reasoncode=recv.%GetLastError() If reasoncode=2033 Quit "" Quit "ERROR: "_reasoncode } Quit msg } ``` 示例2:`ReceiveCharacterStream()` 以下方法可以检索更长的消息,因为它使用`%GetStream()`: ```java /// Method returns reason code from IBM WebSphere MQ ClassMethod ReceiveCharacterStream() As %Integer { Set recv=##class(%Net.MQRecv).%New() Set queue="mqtest" Set qm="QM_antigua" Set chan="S_antigua/TCP/antigua(1414)" Set logfile="c:\mq-recv-log.txt" Set check=recv.%Init(queue,qm,chan,logfile) If 'check Quit recv.%GetLastError() //initialize the stream and tell it what file to use //make sure filename is unique we can tell what we received Set longmsg=##class(%FileCharacterStream).%New() Set longmsg.Filename="c:\mq-received"_$h_".txt" Set check=recv.%GetStream(longmsg) If 'check Quit recv.%GetLastError() Quit check } ``` # 更新消息信息 `%Net.MQSend`和`%Net.MQRecv`类还提供以下方法: ### %CorId() (通过引用)更新上次读取的邮件的关联ID。 ### %ReplyQMgrName() (通过引用)更新上次读取的消息的回复队列管理器名称。 ### %ReplyQName() (通过引用)更新上次读取的消息的回复队列名称。 # Troubleshooting 如果在使用IBM `WebSphere MQ`的InterSystems IRIS接口时遇到问题,应该首先确定客户端是否安装正确并且可以与服务器通信。要执行这样的测试,可以使用IBM `WebSphere MQ`提供的示例程序。可执行文件位于IBM `WebSphere MQ`客户端的bin目录中。 以下步骤介绍如何在`Windows`上使用这些示例程序。在其他操作系统上,细节可能会有所不同;请参考IBM文档并检查您的客户端中存在的文件的名称。 1. 创建一个名为`MQSERVER`的环境变量。它的值的格式应该是`channel_name/Transport/server`,其中`channel_name`是要使用的通道的名称,`Transport`是指示要使用的传输的字符串,而`server`是服务器的名称。例如:`S_Antigua/TCP/Antigua` 2. 在命令行中,输入以下命令: ```java amqsputc queue_name queue_manager_name ``` 其中,`QUEUE_NAME`是要使用的队列的名称,`QUEUE_MANAGER_NAME`是队列管理器的名称。例如: ```java amqsputc mqtest QM_antigua ``` 如果`amqsputc`命令无法识别,请确保已更新`PATH`环境变量以包括IBM `WebSphere MQ`客户端的bin目录。 3. 应该会看到几行代码,如下所示: ```java Sample AMQSPUT0 start target queue is mqtest ``` 4. 现在可以发送消息了。只需键入每条消息,然后在每条消息后按Enter键即可。例如: ```java sample message 1 sample message 2 ``` 5. 发送完邮件后,按两次Enter键。然后,将看到如下所示的行: ```java Sample AMQSPUT0 end ``` 6. 要完成此测试,我们将检索发送到队列的消息。在命令行中键入以下命令: ```java amqsgetc queue_name queue_manager_name ``` 其中,`QUEUE_NAME`是要使用的队列的名称,`QUEUE_MANAGER_NAME`是队列管理器的名称。例如: 7. 然后,应该看到一个起始行,后跟之前发送的消息,如下所示: ```java Sample AMQSGET0 start message message ``` 8. 此示例程序短暂等待接收任何其他消息,然后显示以下内容: ```java no more messages Sample AMQSGET0 end ``` 如果测试失败,请参考IBM文档。问题的可能原因包括以下几个方面: - 安全问题 - 队列定义不正确 - 队列管理器未启动
文章
姚 鑫 · 六月 22, 2021

第十五章 XML检查属性

# 第十五章 XML检查属性 # 检查属性的基本方法 可以使用`%XML.Node`的以下方法。以检查当前节点的属性。 - `AttributeDefined()` 如果当前元素具有具有给定名称的属性,则返回非零(TRUE)。 - `FirstAttributeName()` 返回当前元素的第一个属性的属性名称。 - `GetAttributeValue()` 返回给定属性的值。如果元素没有该属性,则该方法返回NULL。 - `GetNumberAttributes()` 返回当前元素的属性数。 - `LastAttributeName()` 返回当前元素的最后一个属性的属性名称。 - `NextAttributeName()` 在给定属性名称的情况下,无论指定的属性是否有效,此方法都会按排序顺序返回下一个属性的名称。 - `PreviousAttributeName()` 在给定属性名称的情况下,无论指定的属性是否有效,此方法都会按排序顺序返回上一个属性的名称。 下面的示例遍历给定节点中的属性并编写一个简单报表: ```java /// d ##class(Demo.XmlDemo).ShowAttributes("David Marston") /// David Marston ClassMethod ShowAttributes(string) { set reader=##class(%XML.Reader).%New() set status=reader.OpenString(string) if $$$ISERR(status) {do $System.Status.DisplayError(status)} s node = reader.Document.GetDocumentElement() b s count = node.GetNumberAttributes() w !, "属性数量: ", count s first = node.FirstAttributeName() w !, "第一个属性是: ", first w !, " 值是: ",node.GetAttributeValue(first) s next = node.NextAttributeName(first) for i = 1 : 1 : count - 2 { w !, "下一个属性是: ", next w !, " 值是: ",node.GetAttributeValue(next) s next = node.NextAttributeName(next) } s last = node.LastAttributeName() w !, "最后一个属性是: ", last w !, " 值是: ",node.GetAttributeValue(last) } ``` 示例XML文档: ```xml David Marston ``` 如果将此文档的第一个节点传递给示例方法,则会看到以下输出: ```java Number of attributes: 5 First attribute is: attr1 Its value is: first Next attribute is: attr2 Its value is: second Next attribute is: attr3 Its value is: third Next attribute is: attr4 Its value is: fourth Last attribute is: attr5 Its value is: fifth ``` # 检查属性的其他方法 本节讨论可用于获取任何属性的名称、值、命名空间、`QName`和值命名空间的方法。这些方法分为以下几组: - 仅使用属性名称的方法 - 使用属性名称和命名空间的方法 注意:在XML标准中,一个元素可以包含多个同名的属性,每个属性位于不同的名称空间中。但是,在InterSystems IRIS XML中,这是不受支持的。 ## 仅使用属性名称的方法 使用以下方法获取有关属性的信息。 ### GetAttribute() ```java method GetAttribute(attributeName As %String, ByRef namespace As %String, ByRef value As %String, ByRef valueNamespace As %String) ``` 返回给定属性的数据。此方法通过引用返回下列值: - `Namespace`是来自属性QName的命名空间URI - `value` 是属性值。 - `valueNamespace` 值所属的命名空间URI。例如,以下属性: ``` xsi:type="s:string" ``` 此属性的值为字符串,并且此值位于使用前缀s在其他位置声明的命名空间中。假设本文档的较早部分包含以下命名空间声明: ```java xmlns:s="http://www.w3.org/2001/XMLSchema" ``` 在本例中,`valueNamespace`将为`“http://www.w3.org/2001/XMLSchema”`. ### GetAttributeNamespace() ```java method GetAttributeNamespace(attributeName As %String) as %String ``` 从当前元素的名为`AttributeName`的属性的`QName`返回命名空间URI。 ### GetAttributeQName() ```java method GetAttributeQName(attributeName As %String) as %String ``` 返回给定属性的`QName`。 ### GetAttributeValue() ```java method GetAttributeValue(attributeName As %String) as %String ``` 返回给定属性的值。 ### GetAttributeValueNamespace() ```java method GetAttributeValueNamespace(attributeName As %String) as %String ``` 返回给定属性的值的命名空间。 ## 使用属性名和命名空间的方法 要同时使用属性名称及其命名空间来获取有关属性的信息,请使用以下方法: ### GetAttributeNS() ```java method GetAttributeNS(attributeName As %String, namespace As %String, ByRef value As %String, ByRef valueNamespace As %String) ``` 返回给定属性的数据,其中`AttributeName`和`Namespace`指定感兴趣的属性。此方法通过引用返回以下数据: - `value` 是属性值。 - `valueNamespace` 值所属的命名空间URI。例如,以下属性: ```java xsi:type="s:string" ``` 此属性的值为字符串,并且此值位于使用前缀s在其他位置声明的命名空间中。假设本文档的较早部分包含以下命名空间声明: ```java xmlns:s="http://www.w3.org/2001/XMLSchema" ``` ### GetAttributeQNameNS() ```java method GetAttributeQNameNS(attributeName As %String, namespace As %String) as %String ``` 返回给定属性的`QName`,其中`AttributeName`和`Namespace`指定感兴趣的属性。 ### GetAttributeValueNS() ```java method GetAttributeValueNS(attributeName As %String, namespace As %String) as %String ``` 返回给定属性的值,其中`AttributeName`和`Namespace`指定感兴趣的属性。 ### GetAttributeValueNamespaceNS ```java method GetAttributeValueNamespaceNS(attributeName As %String, namespace As %String) as %String ``` 返回给定属性的值的命名空间,其中`AttributeName`和`Namespace`指定感兴趣的属性。
文章
Hao Ma · 十一月 17, 2021

开发Ensemble REST服务

REF: https://docs.intersystems.com/healthconnectlatest/csp/docbook/Doc.View.cls?KEY=GREST REF: https://docs.intersystems.com/healthconnectlatest/csp/docbook/DocBook.UI.Page.cls?KEY=AFL_rest#AFL_C4838 开发REST服务有两个方式, 一个是生生的写代码, 定义接口的标准,被称为"Manually Coding"。第2个方式是目前越来越流行的"Sepcification-first",也就是使用描述性的语言定义接口规范,然后通过这个规范生成接口代码。第2种方式更快捷,但这里我还是从第一种介绍起,对理解里面的代码层次更容易一些,而这是调试一个接口必须的。 从代码开发REST服务 不同于HTTP和SOAP, Ensemble里面没有REST的inbound Adaptor,也没有可用的BS组件。在Production里开发一个REST服务的步骤是: 1. 开发一个REST Service, 这个Service是一个CSP Page, 是一个网页服务,和Ensemble没关系。要在Production中使用这个服务,您需要在这个服务里调用一个Production的业务服务BS。 2. 要访问这个REST页面服务, 您需要配置一个Web Application。Web Application的配置项上有一个选项: "REST 分派类"。这样配置好之后, Web Application收到相应的URL后就会调用这个REST页面,页面再去调用Production的BS。 3. 最后,您需要在BS中处理收到的JSON, 发送给其他组件,以传递给接收方系统。 如果您看的了代码包里的EnsLib.REST.Service类, 它继承了%CSP.REST页面, 也继承了BusinessService,非常符合Ensemble的结构设计。But, 别用。在线文档中有专门的说明。 Although InterSystems IRIS defines a class EnsLib.REST.ServiceOpens in a new window, that is a subclass of %CSP.RESTOpens in a new window, we recommend that you not use this class because it provides an incomplete implementation of %CSP.REST Opens in a new window. 让我们开始开发一个简单的REST服务并加入Production: Step 1: 创建以下代码,解释一下: - 继承%CSP.REST,这是个专用于REST的CSP页面 - UrlMap是一个XData, 在COS语言里用于在代码里放置固定的xml数据结构。UrlMap定义从收到的URL到本类里不同的方法之间的映射。 - 方法中入参可以是任意的数据结构和用户定义的类结构,不需要出参。如果直接返回消息给调用者,直接"write"一个流或者字符串 Class SEDemo.IO.REST.SampleService Extends %CSP.REST { XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ] { } ClassMethod Test(pInput As %String) As %Status { write "Received: "_pInput,! Quit 1 } ClassMethod GetPatientById(pID As %String) As %Status { Try{ Set tObj=##class(SEDemo.Common.Patient).%OpenId(pID),tStream = "" d ##class(%ZEN.Auxiliary.jsonProvider).%WriteJSONStreamFromObject(.tStream,tObj) w tStream.Read() } Catch (e) {Set tSC=e.AsStatus()} Quit tSC } } Step 2: 创建Web Application 在管理界面System Administration > Security > Applications > Web Applications,创建一个用于接收此REST服务的Web APPlication, 设置"Dispatch Class"为当前类。 假设创建的Web Applicaiton为"/CSP/myrest",注意: - 选中“Enable Application" - 权限: 分配本命名空间数据库的资源,默认是%DB_%Default。 后面会详细介绍权限和用户管理的细节。 Step 3: 测试你的REST service 你可以选择自己喜欢的测试方式,比如用浏览器,POSTMAN, SoapUI..., 下面是我测试的记录: CNMBPHMA:~ hma$ curl -v http://172.16.58.200:52773/csp/myrest/Test/333 * Trying 172.16.58.200... * TCP_NODELAY set * Connected to 172.16.58.200 (172.16.58.200) port 52773 (#0) > GET /csp/myrest/Test/333 HTTP/1.1 > Host: 172.16.58.200:52773 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 14 Jul 2021 06:47:26 GMT < Server: Apache < CACHE-CONTROL: no-cache < EXPIRES: Thu, 29 Oct 1998 17:04:19 GMT < PRAGMA: no-cache < CONTENT-LENGTH: 15 < Content-Type: text/html; charset=utf-8 < Received: 333 * Connection #0 to host 172.16.58.200 left intact CNMBPHMA:~ hma$ 这里是一个匿名访问,如果需要用户认证,修改一下重发: CNMBPHMA:~ hma$ curl -u 'superuser:SYS' http://172.16.58.200:52773/csp/myrest/Test/333 Received: 333 CNMBPHMA:~ hma$ 注意两点: 1. 到目前为止我们测试的其实是一个HTTP请求和响应,虽然内部用了%CSP.REST的类, 但响应中'Content-Type'还是'text/html' 2. 代码中没有处理出错和查不到结果的情况 3. 到目前为止和Ensemble Production没有任何关系。 Step 4: 将服务加入Ensemble Production 加入Production的意思实际上时调用一个Production的BusinessService。 让我们先创建一个简单的Service. ///不使用Adapter, 收到任何请求,同步发送给目标组件 Class Test.BS.GeneralService Extends Ens.BusinessService { Method OnProcessInput(pInput As %RegisteredObject, Output pOutput As %RegisteredObject) As %Status { set tRequest=##class(Ens.StringRequest).%New() Set tStatus = ..SendRequestSync("Test.BO.dummyOperation", tRequest, .tResponse) set pOutput = tResponse Quit tStatus } } 当需要前面的REST服务来调用这个BusinessService的时候, 需要在method里面加入直接调用的语句,比如上面的GetPatientById() ClassMethod GetPatientById(pID As %String) As %Status { set status = ##class(Ens.Director).CreateBusinessService("Test.BS.GeneralService", .tService) if $$$ISOK(status) { set status = service.OnProcessInput(pID, .tResponse) } w tResponse,! }
文章
Hao Ma · 一月 15, 2021

IAM实践指南——OAuth 2.0下的API保卫战(第二部分)

在这个由三部分组成的系列文章中,我们将展示如何在OAuth 2.0标准下使用IAM简单地为IRIS中的未经验证的服务添加安全性。 在第一部分中,我们介绍了一些OAuth 2.0背景知识,以及IRIS和IAM的初始定义和配置,以帮助读者理解确保服务安全的整个过程。 现在,本文将详细讨论和演示配置IAM所需的步骤——验证传入请求中的访问令牌,并在验证成功时将请求转发到后端。 本系列的最后一部分将讨论和演示IAM生成访问令牌(充当授权服务器)并对其进行验证时所需的配置,以及一些重要的最终考虑事项。 如果您想试用IAM,请联系InterSystems销售代表。 场景1:IAM作为访问令牌验证器 在该场景中,需要使用一个外部授权服务器生成JWT(JSON Web Token)格式的访问令牌。该JWT使用了RS256算法和私钥签名。为了验证JWT签名,另一方(本例中是IAM)需要拥有授权服务器提供的公钥。 由外部授权服务器生成的JWT主体中还包括一个名为“exp”的声明(包含该令牌过期的时间戳),以及另一个名为“iss”的声明(包含授权服务器的地址)。 因此,IAM需要先使用授权服务器的公钥和JWT内部“exp”声明中包含的过期时间戳对JWT签名进行验证,然后再将请求转发给IRIS。 对IAM进行相应配置时,首先要向IAM中的“SampleIRISService”添加一个名为“JWT”的插件。为此,请转到IAM中的Services页面并复制“SampleIRISService”的ID,稍后会用到。 之后,打开插件,点击“New Plugin”按钮,找到“JWT”插件,点击启用。 在下个页面中,将“SampleIRISService”ID粘贴在“service_id”字段中,然后在“config.claims_to_verify”参数中选中“exp”框。 注意,“config.key_claim_name”参数的值是“iss”。后面会用到。 然后,点击“Create”按钮。 完成操作后,找到左侧菜单中的“Consumers”部分,然后单击先前创建的“ClientApp”。点击“Credentials”标签,然后单击按钮“New JWT Credential”。 在下一页中,选择JWT签名算法(本例中为RS256),并将公钥(这是授权服务器提供的PEM格式的公钥)粘贴到“rsa_public_key”字段中。 在“key”字段中,在添加JWT插件时需要用到之前在“config.key_claim_name”字段中输入的JWT声明内容。所以在本例中,需要插入的是JWT的iss声明内容(本例中是授权服务器的地址)。 之后,单击“Create”按钮。 提示:出于调试目的,可以使用一个在线工具对JWT进行解码,将公钥粘贴进去就可以检查声明内容及其值,并且验证签名。该在线工具的链接如下:https://jwt.io/#debugger 现在,添加了JWT插件后,就不能发送未经身份验证的请求了。如下所示,对URL的一个简单GET请求(未经身份验证): http://iamhost:8000/event/1 返回一个未经授权的信息,以及状态码“401未经授权”。 为了从IRIS获得结果,需要将JWT添加到请求中。 首先,需要向授权服务器请求JWT。如果POST请求与主体中的一些键值对(包括用户和客户端信息)一起发出,那么在这里使用的自定义授权服务器将向以下URL返回一个JWT: https://authorizationserver:5001/auth 该请求及其响应如下所示: 然后,可以将响应中获得的JWT添加到授权标头中作为Bearer令牌使用,并将GET请求发送到和之前相同的URL: http://iamhost:8000/event/1 或者将它作为querystring参数添加进去。当添加JWT插件(本例中是“jwt”)时,querystring关键字是在“config.uri_param_names”字段中指定的值 最后,如果在“config.cookie_names”字段中输入任意名称,选择将JWT作为cookie包含在请求中。 请继续阅读本系列的第三部分也即最后一部分,了解IAM生成和验证访问令牌所需的配置,以及一些重要的最终考虑因素。
问题
Yufeng Li · 六月 14, 2021

IRISHealth-2019.1.0.510.4 能在 CentOS 7.5上部署吗?

IRISHealth-2019.1.0.510.4 能在 CentOS 7.5上部署吗? 可以的,您可以通过WRC账号下载红帽版本安装即可。