搜索​​​​

清除过滤器
文章
姚 鑫 · 三月 21, 2021

第十二章 使用嵌入式SQL(五)

# 第十二章 使用嵌入式SQL(五) # 嵌入式SQL变量 以下局部变量在嵌入式SQL中具有特殊用途。这些局部变量名称区分大小写。在过程启动时,这些变量是不确定的。它们由嵌入式SQL操作设置。也可以使用SET命令直接设置它们,或使用`NEW`命令将其重置为未定义。像任何局部变量一样,值将在过程持续期间或直到设置为另一个值或使用`NEW`进行定义之前一直存在。例如,某些成功的嵌入式SQL操作未设置`%ROWID`。执行这些操作后,`%ROWID`是未定义的或保持设置为其先前值。 - `%msg` - `%ROWCOUNT` - `%ROWID` - `SQLCODE` 这些局部变量不是由Dynamic SQL设置的。 (请注意,SQL Shell和Management Portal SQL接口执行Dynamic SQL。)相反,Dynamic SQL设置相应的对象属性。 在嵌入式SQL中使用以下ObjectScript特殊变量。这些特殊的变量名称不区分大小写。在过程启动时,这些变量将初始化为一个值。它们由嵌入式SQL操作设置。不能使用SET或NEW命令直接设置它们。 - `$TLEVEL` - `$USERNAME` 作为已定义的InterSystems IRIS嵌入式SQL接口的一部分,InterSystems IRIS可以在嵌入式SQL处理期间设置任何这些变量。 如果嵌入式SQL在类方法中(`procedureBlock = ON`),则系统会自动将所有这些变量放在`PublicList`中,并自动将`SQLCODE`,`%ROWID`,`%ROWCOUNT`,`%msg`以及SQL语句。可以通过引用方法来传递这些变量;通过引用传递的变量将不会在类方法过程块中自动更新。 如果嵌入式SQL在例程中,则程序员有责任在调用嵌入式SQL之前新建`%msg`,`%ROWCOUNT`,`%ROWID`和`SQLCODE`变量。更新这些变量可防止干扰这些变量的先前设置。为避免``错误,不应在迭代周期内执行此`NEW`操作。 ## %msg 包含系统提供的错误消息字符串的变量。如果InterSystems SQL将`SQLCODE`设置为负整数(表示错误),则仅设置`%msg`。如果`SQLCODE`设置为`0`或`100`,则`%msg`变量与其先前值保持不变。 此行为不同于相应的Dynamic SQL `%Message`属性,当没有当前错误时,该属性将设置为空字符串。 在某些情况下,特定的`SQLCODE`错误代码可能与一个以上的`%msg`字符串相关联,描述了生成`SQLCODE`的不同条件。 `%msg`还可以接受用户定义的消息字符串。当触发器代码显式设置`%ok = 0`来中止触发器时,这最常用于从触发器发出用户定义的消息。 当执行SQL代码时,将使用有效的NLS语言生成错误消息字符串。可以在不同的NLS语言环境中编译SQL代码。该消息将根据运行时NLS环境生成。请参见`$ SYS.NLS.Locale.Language`。 ## %ROWCOUNT 一个整数计数器,指示受特定语句影响的行数。 - `INSERT`,`UPDATE`,`INSERT OR UPDATE`和`DELETE`将`%ROWCOUNT`设置为受影响的行数。带有显式值的INSERT命令只能影响一行,因此将`%ROWCOUNT`设置为`0`或`1`。`INSERT`查询结果,`UPDATE`或`DELETE`可以影响多行,因此可以将`%ROWCOUNT`设置为0或正数。整数。 - 无论删除多少行还是删除任何行,`TRUNCATE TABLE`始终将`%ROWCOUNT`设置为`–1`。因此,要确定实际删除的行数,请在`TRUNCATE TABLE`之前对表执行`COUNT(*)`,或者使用`DELETE`而不是`TRUNCATE TABLE`删除表中的所有行。 - 没有声明游标的`SELECT`只能作用于一行,因此执行简单的`SELECT`总是会将`%ROWCOUNT`设置为`1`(与检索到的选择标准匹配的单行)或`0`(没有与选择标准匹配的行)。 - `DECLARE`游标名`CURSOR FOR SELECT`不会初始化`%ROWCOUNT`; `SELECT`之后,`%ROWCOUNT`不变,而`OPEN`游标名之后,`%ROWCOUNT`不变。第一个成功的`FETCH`设置`%ROWCOUNT`。如果没有行符合查询选择条件,则`FETCH`设置`%ROWCOUNT = 0`;否则,设置`%ROWCOUNT = 0`。如果`FETCH`检索与查询选择条件匹配的行,则它将设置`%ROWCOUNT = 1`。随后的每个获取行的`FETCH`都将递增`%ROWCOUNT`。 `CLOSE`时或`FETCH`发出`SQLCODE 100`(无数据或无更多数据)时,`%ROWCOUNT`包含已检索的总行数。 此`SELECT`行为与相应的Dynamic SQL`%ROWCOUNT`属性不同,该属性在查询执行完成时设置为0,并且仅在程序迭代查询返回的结果集时才递增。 如果`SELECT`查询仅返回聚合函数,则每个`FETCH`都将设置`%ROWCOUNT = 1`。即使表中没有数据,第一个`FETCH`始终以`SQLCODE = 0`来完成;任何后续的`FETCH`均以`SQLCODE = 100`完成,并设置`%ROWCOUNT = 1`。 以下嵌入式SQL示例声明一个游标,并使用`FETCH`来获取表中的每一行。到达数据结尾(`SQLCODE = 100`)时,`%ROWCOUNT`包含已检索的行数: ```java /// d ##class(PHA.TEST.SQL).ROWCOUNT() ClassMethod ROWCOUNT() { SET name="LastName,FirstName",state="##" &sql(DECLARE EmpCursor CURSOR FOR SELECT Name, Home_State INTO :name,:state FROM Sample.Person WHERE Home_State %STARTSWITH 'M') WRITE !,"BEFORE: Name=",name," State=",state &sql(OPEN EmpCursor) QUIT:(SQLCODE'=0) FOR { &sql(FETCH EmpCursor) QUIT:SQLCODE WRITE !,"Row fetch count: ",%ROWCOUNT WRITE " Name=",name," State=",state } WRITE !,"最终提取SQLCODE: ",SQLCODE &sql(CLOSE EmpCursor) WRITE !,"AFTER: Name=",name," State=",state WRITE !,"提取的总行数: ",%ROWCOUNT } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).ROWCOUNT() BEFORE: Name=LastName,FirstName State=## Row fetch count: 1 Name=O'Rielly,Chris H. State=MS Row fetch count: 2 Name=Orwell,John V. State=MT Row fetch count: 3 Name=Zevon,Heloisa O. State=MI ... Row fetch count: 37 Name=Joyce,Elmo R. State=MO Row fetch count: 38 Name=Jafari,Christine Z. State=MI 最终提取SQLCODE: 100 AFTER: Name=Jafari,Christine Z. State=OH 提取的总行数: 38 ``` 以下嵌入式SQL示例执行`UPDATE`并设置受更改影响的行数: ```java /// d ##class(PHA.TEST.SQL).ROWCOUNT1() ClassMethod ROWCOUNT1() { &sql(UPDATE Sample.Employee SET Salary = (Salary * 1.1) WHERE Salary < 50000) IF SQLCODEd ##class(PHA.TEST.SQL).ROWCOUNT1() Employees: 48 ``` **请记住,所有嵌入式SQL语句(在给定进程内)都会修改`%ROWCOUNT`变量。如需要`%ROWCOUNT`提供的值,请确保在执行其他Embedded SQL语句之前获取其值。根据嵌入式SQL的调用方式,可能必须在输入嵌入式SQL之前新建`%ROWCOUNT`变量。** **另请注意,显式回滚事务不会影响`%ROWCOUNT`的值。例如,以下内容将报告已进行了更改,即使它们已经滚动了。** ```java /// d ##class(PHA.TEST.SQL).ROWCOUNT2() ClassMethod ROWCOUNT2() { TSTART // 开始事务 NEW SQLCODE,%ROWCOUNT,%ROWID &sql(UPDATE Sample.Employee SET Salary = (Salary * 1.1) WHERE Salary < 50000) IF SQLCODEd ##class(PHA.TEST.SQL).ROWCOUNT2() Employees: 37 ``` 隐式事务(例如,如果UPDATE未通过约束检查)由`%ROWCOUNT`反映。 ## %ROWID 初始化进程时,未定义`%ROWID`。当发出`NEW %ROWID`命令时,`%ROWID`将重置为未定义。 `%ROWID`由下面描述的嵌入式SQL操作设置。如果该操作不成功或成功完成,但未获取或修改任何行,则`%ROWID`值与其先前值保持不变:未定义,或由先前的嵌入式SQL操作设置为某个值。因此,在每个嵌入式SQL操作之前,请务必新建`%ROWID`。 `%ROWID`设置为受以下操作影响的最后一行的`RowID`: - `INSERT`,`UPDATE`,`INSERT OR UPDATE`或`DELETE`:单行操作后,`%ROWID`变量包含系统分配的`RowID`(对象ID)值,该值分配给插入,更新或删除的记录。经过多行操作之后,`%ROWID`变量包含系统分配的最后一条插入,更新或删除的记录的`RowID`(对象ID)的值。如果未插入,更新或删除任何记录,则`%ROWID`变量值将保持不变。 `TRUNCATE TABLE`没有设置`%ROWID`。 - 基于游标的`SELECT:DECLARE`游标名称`CURSOR`和`OPEN`游标名称语句未初始化`%ROWID`; `%ROWID`值与其先前值保持不变。第一个成功的`FETCH`设置`%ROWID`。随后的每个获取行的`FETCH`都会将`%ROWID`重置为当前`RowID`值。如果`FETCH`检索一行可更新游标,则会设置`%ROWID`。可更新游标是其中顶部`FROM`子句仅包含一个元素(单个表名或可更新视图名)的游标。如果游标不可更新,则`%ROWID`保持不变。如果没有行符合查询选择条件,则`FETCH`不会更改先前的`%ROWID`值(如果有)。 `CLOSE`时或`FETCH`发出`SQLCODE 100`(无数据或无更多数据)时,`%ROWID`包含检索到的最后一行的`RowID`。 具有`DISTINCT`关键字或`GROUP BY`子句的基于游标的`SELECT`不会设置`%ROWID`。 `%ROWID`值与其先前的值(如果有)保持不变。 如果基于游标的`SELECT`仅返回聚合函数值,则不会设置`%ROWID`。如果它同时返回字段值和聚合函数值,则将每个`FETCH`的`%ROWID`值设置为查询返回的最后一行的`RowID`。 - 没有声明游标的`SELECT`不会设置`%ROWID`。完成简单的`SELECT`语句后,`%ROWID`值将保持不变。 在Dynamic SQL中,相应的`%ROWID`属性返回插入,更新或删除的最后一条记录的`RowID`值。执行`SELECT`查询时,Dynamic SQL不会返回`%ROWID`属性值。 可以使用以下方法调用从ObjectScript中检索当前的`%ROWID`: ``` DHC-APP> WRITE $SYSTEM.SQL.GetROWID() 213 ``` 在执行`INSERT`,`UPDATE`,`DELETE`,`TRUNCATE TABLE`或基于游标的`SELECT`操作之后,`LAST_IDENTITY` SQL函数将为最近修改的记录返回`IDENTITY`字段的值。如果表没有`IDENTITY`字段,则此函数返回最近修改记录的`RowID`。 ## SQLCODE 运行嵌入式SQL查询后,必须在处理输出主机变量之前检查`SQLCODE`。 如果`SQLCODE = 0`,则查询成功完成并返回数据。输出主机变量包含字段值。 如果`SQLCODE = 100`,则查询成功完成,但是输出主机变量值可能不同。任何一个: - 查询返回一个或多个数据行(`SQLCODE = 0`),然后到达数据的末尾(`SQLCODE = 100`),在这种情况下,输出主机变量设置为返回的最后一行的字段值。 `%ROWCOUNT> 0`。 - 查询未返回任何数据,在这种情况下,输出主机变量未定义。 `%ROWCOUNT = 0`。 如果查询仅返回聚合函数,则即使表中没有数据,第一个`FETCH`也会始终以`SQLCODE = 0`和`%ROWCOUNT = 1`来完成。第二个`FETCH`以`SQLCODE = 100`和`%ROWCOUNT = 1`结束。如果表中没有数据或没有数据与查询条件匹配,查询将根据需要将输出主机变量设置为0或空字符串。 如果`SQLCODE`为负数,则查询失败,并显示错误条件。 根据嵌入式SQL的调用方式,可能必须在输入嵌入式SQL之前新建`SQLCODE`变量。在触发代码中,将`SQLCODE`设置为非零值会自动将`%ok = 0`设置为中止并回滚触发操作。 在动态SQL中,相应的`%SQLCODE`属性返回SQL错误代码值。 ## $TLEVEL 事务级计数器。 InterSystems SQL将`$TLEVEL`初始化为0。 如果没有当前事务,`$TLEVEL`为0。 - 初始`START TRANSACTION`将`$LEVEL`设置为1。其他`START TRANSACTION`语句对`$TLEVEL`无效。 - 每个`SAVEPOINT`语句将`$TLEVEL`加1。 - `ROLLBACK TO SAVEPOINT`点名语句减少`$TLEVEL`。递减量取决于指定的保存点。 - `COMMIT`将`$LEVEL`重置为0。 - `ROLLBACK`将`$LEVEL`重置为0。 还可以使用`%INTRANSACTION`语句来确定事务是否在进行中。 `$TLEVEL`也由ObjectScript事务命令设置。 ## $USERNAME SQL用户名与InterSystems IRIS用户名相同,存储在ObjectScript `$USERNAME`特殊变量中。用户名可以用作系统范围的默认架构,也可以用作架构搜索路径中的元素。
文章
王喆 👀 · 九月 21, 2023

IRIS自动安装集群--manifest(安装清单)

前言 生产环境下我们部署和使用IRiS引擎,往往采用其主备镜像模式,虽然此架构简单但是往往我们需要持续在电脑前点击或者操作1到2小时,如果中间有个环节出现了问题有时我们可能需要部署一天. 接下来我分享的是IRIS自带的一个功能帮助我们部署---manifest-安装清单。他的主要使用方式是提前通过配置约定好我们期望的安装设置,在安装的过程中由IRIS程序直接执行脚本,简化IRIS集群的部署,减少运维人员的操作步骤,让我们有更多的精力放在实际项目和业务上。 1 简介 %Installer 实用程序允许您定义描述和配置特定 InterSystems IRIS 配置的安装清单,而不是分步安装过程。为此,我们需要创建一个类,其中包含描述所需配置的 XData 块,使用包含通常在安装期间提供的信息(超级服务器端口、操作系统等)的变量。我们还可以在类中包含一个使用 XData 块生成代码以配置实例的方法。本文提供了安装清单的示例,您可以复制和粘贴这个示例尝试使用。 定义清单后,可以在安装期间、从终端会话或代码调用它。注意:清单必须在 %SYS 命名空间中运行。 2 Manifest的最终成品 此成品展示的是一个一键安装主、备、仲裁的机器命令,此方法的使用可以便捷快速的安装主备环境,其基本每一行都有注释其说明: Include %occInclude /// Classname: App.MirrorInstall <br/> /// Summary: 镜像安装清单 <br/> /// Version: 1.0 <br/> /// Date: 2023年09月13日 14:23:24 <br/> /// Author: 王喆 <br/> Class App.MirrorInstall { XData Install [ XMLNamespace = INSTALLER ] { <Manifest> <!-- 镜像配置 --> <Log Text="镜像名称 " Level="0"/> <Var Name="MirrorName" Value="MIRRORSET" /> <Log Text=" 虚拟IP " Level="0"/> <Var Name="VituralIP" Value="192.168.98.110/24" /> <Log Text="仲裁机IP " Level="0"/> <Var Name="ArbiterNode" Value = "192.168.98.103|2188" /> <Log Text="网卡名称 " Level="0"/> <Var Name="PrimaryNetworkAdapter" Value = "Ethernet0" /> <Log Text="主机IP " Level="0"/> <Var Name="MasterIP" Value = "192.168.98.101" /> <Log Text="主机端口 " Level="0"/> <Var Name="MasterPort" Value = "2188" /> <Log Text="主机名称 " Level="0"/> <Var Name="MasterName" Value = "IRIS01" /> <Log Text="备机网卡名称 " Level="0"/> <Var Name="BackupNetworkAdapter" Value = "Ethernet0" /> <Log Text="备机IP " Level="0"/> <Var Name="BackupIP" Value = "192.168.98.102" /> <Log Text="备机名称 " Level="0"/> <Var Name="BackupName" Value = "IRIS02" /> <Log Text="实例名称 " Level="0"/> <Var Name="InstanceName" Value = "IRISHEALTH" /> <Log Text="镜像模式: 主机1或者备机0 " Level="0"/> <!-- <Var Name="MirrorModel" Value="1" />--> <!-- 安装文件所在的目录 --> <!-- <Var Name="INSTALLERDIR" Value = "D:\deploy" /> --> <!-- 实例所在的安装目录 --> <!-- <Var Name="PRODDIR" Value = "C:\InterSystems\IRISHealth" /> --> <!-- 激活 --> <!-- 通用内存堆的大小。 --> <SystemSetting Name="Config.config.gmheap" Value="1048576"/> <!-- 通用内存堆的大小。 --> <SystemSetting Name="Config.config.locksiz" Value="134217728"/> <!-- 错误日志中的最大条目数。 --> <SystemSetting Name="Config.config.errlog" Value="10000"/> <!-- 用于缓存例程缓冲区的共享内存大小。 --> <SystemSetting Name="Config.config.routines" Value="256"/> <!-- 为 8KB 数据库缓存分配的内存 一般为内存的一半 --> <SystemSetting Name="Config.config.globals8kb" Value="1000"/> <!-- 写入图像日志文件目录。 --> <SystemSetting Name="Config.config.wijdir" Value="D:/cache/wij"/> <!-- 日志文件的主要位置。 --> <SystemSetting Name="Config.Journal.CurrentDirectory" Value="E:/cache/journal"/> <!-- 日志文件的备用位置。 --> <SystemSetting Name="Config.Journal.AlternateDirectory" Value="D:/cache/journal"/> <!-- Caché 清除已完成日志文件之前的天数 --> <SystemSetting Name="Config.Journal.DaysBeforePurge" Value="3"/> <!-- 在此连续备份数后 --> <SystemSetting Name="Config.Journal.BackupsBeforePurge" Value="3"/> <!-- 最大IRISTempSizeAtStart --> <SystemSetting Name="Config.Startup.MaxIRISTempSizeAtStart" Value="10"/> <IfDef Var="INSTALLERDIR"> <Log Text=" 激活 " Level="0"/> <Invoke Class="App.MirrorInstall" Method="ConfigureInstance" CheckStatus="0"> <Arg Value="${INSTALLERDIR},${PRODDIR}"/> </Invoke> </IfDef> <!-- 创建命名空间 --> <Log Text="BKLINIK 命名空间存在与否判断 " Level="0"/> <If Condition='(##class(Config.Namespaces).Exists("BKLINIK")=0)'> <Log Text="不存在开始创建 " Level="0"/> <Namespace Name="BKLINIK" Create="yes" Code="BKLINIKAPP" Ensemble="1" Data="BKLINIKMSG"> <Configuration> <!-- 消息的目录 --> <Database Name="BKLINIKMSG" Dir="D:/DB/BKLINIKMSG" Create="yes"/> <!-- 代码的目录 --> <Database Name="BKLINIKAPP" Dir="D:/DB/BKLINIKAPP" Create="yes"/> <!-- 配置映射 --> <ClassMapping Package="HS" From="HSLIB"/> <ClassMapping Package="HSMOD" From="HSLIB"/> <ClassMapping Package="SchemaMap" From="HSLIB"/> <RoutineMapping Routines="HS.*" From="HSLIB" /> <RoutineMapping Routines="HSMOD.*" Type="INC" From="HSLIB" /> <RoutineMapping Routines="HSMOD.*" From="HSLIB" /> <RoutineMapping Routines="SchemaMap*" Type="INC" From="HSLIB" /> <GlobalMapping Global="%SYS" From="IRISSYS" /> <GlobalMapping Global="%SYS("HealthShare")" From="HSSYS"/> <GlobalMapping Global="EnsHL7.Annotation("HealthShare_2.5")" From="HSLIB" /> <GlobalMapping Global="EnsHL7.Description("HealthShare_2.5")" From="HSLIB" /> <GlobalMapping Global="EnsHL7.Schema("HealthShare_2.5")" From="HSLIB" /> <GlobalMapping Global="IRIS.Msg("HS")" From="HSLIB" /> <GlobalMapping Global="IRIS.Msg("HSErr")" From="HSLIB" /> <GlobalMapping Global="IRIS.Msg("HSFHIRErr")" From="HSLIB" /> <GlobalMapping Global="IRIS.Msg("HSFHIRXErr")" From="HSLIB" /> <GlobalMapping Global="IRIS.MsgNames("HS")" From="HSLIB" /> <GlobalMapping Global="IRIS.MsgNames("HSErr")" From="HSLIB" /> <GlobalMapping Global="IRIS.MsgNames("HSFHIRErr")" From="HSLIB" /> <GlobalMapping Global="IRIS.MsgNames("HSFHIRXErr")" From="HSLIB" /> </Configuration> </Namespace> <Log Text="创建命名空间 BKLINIK 结束" Level="0"/> <Log Text="Production启动" Level="0"/> <Namespace Name="BKLINIK" Create="no"> <Log Text=" 设置web应用程序 " Level="0"/> <CSPApplication Url="/csp/bklinik" Directory="${CSPDIR}bklinik" AuthenticationMethods="64" IsNamespaceDefault="true" Grant="%ALL" /> <CSPApplication Url="/csp/bklinik/services" Description="" Directory="${CSPDIR}bklinik\services" Resource="" Grant="%ALL" Recurse="1" LoginClass="" CookiePath="/csp/bklinik/services" AuthenticationMethods="64"/> <Log Text=" 导入代码 " Level="0"/> <!-- 如果设置了 SourceDir 就加载其中的文件 --> <!-- <IfDef Var="SourceDir"> --> <!-- <Log Text="SourceDir已定义-从脱机安装 ${SourceDir}" Level="0"/>--> <Import File="${INSTALLERDIR}\distr\"/> <!-- 设置命名空间的Production自动启动 前提得有这个--> <!-- <Production Name="BKLINIKPKG.FoundationProduction" AutoStart="1" /> --> <!-- </IfDef> --> <Log Text=" 导入代码成功 " Level="0"/> </Namespace> </If> <Log Text=" 镜像 " Level="0"/> <IfDef Var="MirrorModel"> <Log Text="Master已定义-从脱机安装 ${主机}" Level="0"/> <If Condition='(${MirrorModel}=1)' > <!-- 创建主镜像 --> <Log Text=" 创建主镜像 " Level="0"/> <Invoke Class="App.MirrorInstall" Method="CreateMirror" CheckStatus="0"> <Arg Value="${MirrorName},${VituralIP},${ArbiterNode},${PrimaryNetworkAdapter},${MasterIP},${MasterName}"/> </Invoke> </If> <If Condition='(${MirrorModel}=0)' > <!-- 加入镜像 --> <Log Text=" 加入镜像 " Level="0"/> <Invoke Class="App.MirrorInstall" Method="JoinMirror" CheckStatus="0"> <Arg Value="${MirrorName},${MasterName},${MasterIP},${MasterPort},${BackupNetworkAdapter},${BackupIP},${InstanceName}"/> </Invoke> </If> </IfDef> </Manifest> } /// MethodName: setup <br> /// Summary: 入口点方法,您需要调用,在类编译时,它从清单生成CachéObjectScript代码之后,您可以从终端运行此安装程序: <br> /// Parameter: Set pVars("Namespace")="NewNamespace" /// Set pVars("SourceDir")="C:\deploy\distr\" /// set pVart("MirrorModel")=1 /// Do ##class(App.MirrorInstall).setup(.pVart) <br> /// Return: { Boolean } <br> /// Date: 2023年09月13日 15:39:05 <br> /// Author: 王喆 <br> ClassMethod setup(ByRef pVars, pLogLevel As %Integer = 0, pInstaller As %Installer.Installer, pLogger As %Installer.AbstractLogger) As %Status [ CodeMode = objectgenerator, Internal ] { Quit ##class(%Installer.Manifest).%Generate(%compiledclass, %code, "Install") } /// MethodName: ConfigureInstance <br> /// Summary: 激活的方法 <br> /// Parameter: { Object } <br> /// Return: { %Status } <br> /// Date: 2023年09月13日 14:24:17 <br> /// Author: 王喆 <br> ClassMethod ConfigureInstance(InstallerDir As %String, ProdDir As %String) As %Status { Set tSC = $ZF(-1,"copy "_ InstallerDir _ "\iris.key "_ProdDir_"\mgr\iris.key") Set tSC = ##class(%SYSTEM.License).Upgrade() Set tSC = $ZF(-1,"net start ISCAgent") //do ##class() Quit tSC } /// MethodName: CreateMirror <br> /// Summary: 创建镜像 <br> /// Parameter: { Object } <br> /// Return: { %Status } <br> /// Date: 2023年09月13日 14:24:52 <br> /// Author: 王喆 <br> ClassMethod CreateMirror(MirrorName As %String, VituralIP As %String, ArbiterNode As %String, PrimaryNetworkAdapter As %String, MasterIP As %String, MasterName As %String) As %Status { ZNspace "%SYS" #; Start %Service_Mirror 开启镜像服务 Set tSC = ##class(Security.Services).Get("%Service_Mirror",.properties) Set properties("Enabled")=1 Set tSC = ##class(Security.Services).Modify("%Service_Mirror",.properties) Set MirrorInfo("VirtualAddress") = VituralIP Set MirrorInfo("ArbiterNode") = ArbiterNode If "" '= MasterIP{ Set MirrorInfo("ECPAddress") = MasterIP } If "" '= PrimaryNetworkAdapter{ Set MirrorInfo("VirtualAddressInterface") = PrimaryNetworkAdapter } #; if "" '= MasterName #; Set MasterName = ##class(SYS.Mirror).DefaultSystemName() Try { Set tSC = ##class(SYS.Mirror).CreateNewMirrorSet(MirrorName,MasterName,.MirrorInfo) If $$$ISERR(tSC){ Set errobj = ##class(%Exception.General).%New() Set errobj.Name = "Error #",errobj.Code = $SYSTEM.Status.GetErrorCodes(tSC),errobj.Data = $SYSTEM.Status.GetOneStatusText(tSC,1) Throw errobj } } Catch ex { Write ex.DisplayString() } //Create Mirror Se1t //do ##class(SYS.Mirror). Quit tSC } /// MethodName: JoinMirror <br> /// Summary: 加入镜像 <br> /// Parameter: { Object } <br> /// Return: { %Status } <br> /// Date: 2023年09月13日 14:25:19 <br> /// Author: 王喆 <br> ClassMethod JoinMirror(MirrorName As %String, MasterName As %String, MasterIP As %String, MasterPort As %Integer, BackupNetworkAdapter As %String, BackupIP As %String, InstanceName As %String) As %Status { ZNspace "%SYS" //Start %Service_Mirror Set tSC = ##class(Security.Services).Get("%Service_Mirror",.properties) Set properties("Enabled")=1 Set tSC = ##class(Security.Services).Modify("%Service_Mirror",.properties) If "" '= BackupNetworkAdapter{ Set LocalInfo("VirtualAddressInterface") = BackupNetworkAdapter } If "" '= BackupIP{ Set LocalInfo("ECPAddress") = BackupIP } Try { Set SysName = ##class(SYS.Mirror).DefaultSystemName() Set tSC = ##class(SYS.Mirror).JoinMirrorAsFailoverMember(MirrorName,SysName,InstanceName,MasterIP,MasterPort,.LocalInfo) If $$$ISERR(tSC){ Set errobj = ##class(%Exception.General).%New() Set errobj.Name = "Error #",errobj.Code = $SYSTEM.Status.GetErrorCodes(tSC),errobj.Data = $SYSTEM.Status.GetOneStatusText(tSC,1) Throw errobj } } Catch ex { Write ex.DisplayString() } Quit tSC } } 3 安装清单执行 3.1 前置条件: 特别注意:windows server r2 2012 先安装2022版本的IRIS务必去微软官网下载Windows8.1-KB2999226-x64.msu并安装(必须) 预先安装 Java环境推荐Java8(必须) 服务器磁盘分区得有C盘 D盘 E盘可用(推荐) 部署包解压路径 D:\deploy(可选) 安装浏览器(可选) 3.2安装脚本 以管理员权限运行cmd,进入D://deploy 执行安装程序,其在安装时候就可以配置好镜像: 主机执行: ./IRISHealth-2022.1.2.574.0-win_x64.exe INSTALLERMANIFEST="D:\deploy\distr\MirrorInstall.xml" INSTALLERMANIFESTPARAMS="MirrorModel=1,PRODDIR=C:\InterSystems\IRISHealth,INSTALLERDIR=D:\deploy,GlobalBuffers=32768,RoutineCache=1000" INSTALLERMANIFESTLOGFILE="D:\deploy\mirrorLog.txt" INSTALLERMANIFESTLOGLEVEL="3" 备机执行: ./IRISHealth-2022.1.2.574.0-win_x64.exe INSTALLERMANIFEST="D:\deploy\distr\MirrorInstall.xml" INSTALLERMANIFESTPARAMS="MirrorModel=0,PRODDIR=C:\InterSystems\IRISHealth,INSTALLERDIR=D:\deploy,GlobalBuffers=32768,RoutineCache=1000" INSTALLERMANIFESTLOGFILE="D:\deploy\mirrorLog.txt" INSTALLERMANIFESTLOGLEVEL="3" 脚本解释: ./IRISHealth-2022.1.2.574.0-win_x64.exe // 安装程序 INSTALLERMANIFEST="D:\deploy\distr\MirrorInstall.xml" // 安装清单所在的目录 INSTALLERMANIFESTPARAMS=" // 安装的时候传入的参数 MirrorModel=1, // 1-主机 0-备机 PRODDIR=C:\InterSystems\IRISHealth, // 你想安装的目录 INSTALLERDIR=D:\deploy, // 安装文件所在的目录 GlobalBuffers=32768, // Global内存的大小 RoutineCache=1000" // Routine内存的大小 INSTALLERMANIFESTLOGFILE="D:\deploy\mirrorLog.txt" // 安装时候记录的日志位置 INSTALLERMANIFESTLOGLEVEL="3" // 日志级别 好了以上是我本次分享的关于IRIS关于Manifest的相关内容,本文只对manifest做了示例,具体的如何使用及其步骤在官网中描述的比较清楚,如果有需要大家可以去官网看看。 如果您觉得对你有帮助就给我点个赞吧,感激不尽!!!
文章
Michael Lei · 十二月 9, 2022

基于 IRIS SQL高级功能实现 CI/CD的技术原理和指导

在数量众多、形形色色的 SQL 数据库市场中,InterSystems IRIS 作为一个超越 SQL 的平台脱颖而出,它提供无缝的多模型体验,支持丰富的开发范式。 特别是,先进的对象-关系引擎已经帮助组织为其数据密集型工作负载的每个方面使用了最适合的开发方式,例如在通过对象获取数据并同时通过 SQL 查询数据。 持久类与 SQL 表相对应,其属性与表中的各列相对应,可以使用用户定义的函数或存储过程轻松访问业务逻辑。 在这篇文章中,我们将深入了解表面之下的一点底层技术,讨论它可能如何影响您的开发和部署方式。 这是我们计划发展和改进的产品领域,因此请不要犹豫,在下面的评论区分享您的观点和体验。 保存存储定义 {Saving the Storage Definition} 编写全新的业务逻辑很容易,而且假如您有定义明确的 API 和规范,那么调整或扩展通常也很容易。 但是,当它不仅仅是业务逻辑,还涉及持久化数据时,从初始版本更改的任何内容都将需要能够妥善处理通过早期版本获取的数据。 在 InterSystems IRIS 上,数据和代码在一个高性能引擎中共存,没有您在其他 3GL 或 4GL 编程框架中可能看到的六个抽象层。 这意味着,在使用默认存储时,只有一个非常薄且透明的映射将您的类属性映射到每行数据Global节点的 $list 位置。 如果想添加或移除属性,您不希望已移除的属性中的数据显示在新属性下。 类属性的这个映射是由存储定义进行处理的,这是一个您可能已经在类定义的底部注意到的神秘XML 块。 首次编译类时,将根据该类的属性和参数生成新的存储定义。 当您对类定义进行更改后,在重新编译时,这些更改将与现有存储定义进行协同和修正,以便与现有数据保持兼容。 因此,当您试着重构类时,存储定义会仔细考虑您之前做的事情,并确保新旧数据都可以访问。 我们称之为**Schema 演进**。 在其他大多数 SQL 数据库中,表的物理存储是不透明的,如果可见的话,并且只能通过类似 `ALTER TABLE` 语句进行修改。 这些都是标准 DDL(数据定义语言)命令,但表达能力比直接在 IRIS 上修改类定义和过程代码要差很多。 InterSystems 致力于让 IRIS 开发者能够干净利落地分离代码和数据,因为这对于确保应用程序的顺利打包和部署至关重要。 存储定义在这方面发挥着独特的作用,因为它可以捕获两者之间的映射方式。 这就是值得在传统开发和 CI/CD pipeline不同实践的背景下对其进行深入研究的原因。 导出为通用描述语言 Universal Definition Language 如今,源代码管理是以文件为基础的,所以我们先来看看 IRIS 的主要文件导出格式。 顾名思义,通用描述语言UDL意味着您在 InterSystems IRIS 上编写的所有代码的文件格式是通用的。 这是使用 VS Code ObjectScript 插件时的默认导出格式,并产生易于读取的文件,该文件与您在 IDE 中看到的内容几乎完全相同,并且您的应用程序中的每个类(表)都有一个单独的 .cls 文件。 您可以使用$SYSTEM.OBJ.Export 或者是用 VS Code 插件来创建 UDL 文件。 在Studio的时代,您可能还记得一种 XML 格式,它会捕获与 UDL 相同的信息,并允许将多个类分组到一个导出中。 虽然这种格式在某些场景中很方便,但在读取和跟踪不同版本之间的差异时却不太实用,因此我们暂时将其忽略。 由于 UDL 旨在捕获 IRIS 可以表达的关于类的所有内容,它会包含类定义的所有元素,包括完整的存储定义。 在导入已包含存储定义的类定义时,IRIS 会验证该存储定义是否涵盖该类的所有属性和索引,如果是,只需按原样接受并覆盖该类先前的存储定义即可。 这使得 UDL 成为一种管理类及其存储定义版本的实用格式,因为无论您将它部署到何处,它都会保留通过先前版本的类获取的数据的向后兼容性。 如果您是一位硬核开发者,您可能想知道这些存储定义是否会不断增长,是否需要无限期地携带这些“包袱”。 存储定义的目的是保持与预先存在的数据的兼容性,因此如果您知道没有这些数据,并且希望摆脱冗长的兼容和继承关系,则可以通过从类定义中移除存储定义并让类编译器重新生成来“重置”存储定义。 例如,您可以使用它来利用新的最佳实践,比如扩展集的使用,它们会实现经过哈希处理的Global名称,并将每个索引分离到自己的全局名称中,从而提高效率。 为了在应用程序内实现向后兼容性,我们不能在 %Persistent 超类中统一更改此类默认值(但我们在使用 `CREATE TABLE` DDL 命令从头开始创建表时会应用它们),因此定期检查类及其存储是很有价值的。 用户也可以直接编辑存储定义 XML文件,但应格外小心,因为这可能会导致现有数据无法访问。 存储定义会提供类之间的智能映射,并随着Schema的演进而自动调整。 还有什么? 静态还是动态? 您可能知道,InterSystems IRIS SQL 引擎充分利用表统计信息来确定用户执行的任何给定语句的最佳查询计划。 表统计信息包括表大小、值在列中的分布方式等指标。 此信息可以帮助 IRIS SQL 优化器确定哪个索引最有用,按何种顺序连接表等,因此直观地说,统计信息越新,优化查询计划的机会就越大。 遗憾的是,直到我们在 IRIS 2021.2 中引入快速块采样之前,收集准确的表统计信息一直是一项计算成本高昂的操作。 因此,当客户将同一应用程序部署到数据模式基本相同的许多环境中时,有必要考虑应用程序代码中的表统计信息部分并将其包含在表定义中。 这就是如今在 IRIS 上您会发现存储定义内嵌入了表统计信息的原因。 通过手动调用 `TUNE TABLE` 收集表统计信息或通过查询(见下文)隐式收集表统计信息时,新统计信息会写入存储定义,此表的现有查询计划将失效,因此它们可以在下次执行时利用新统计信息。 由于它们是存储定义的一部分,这些统计信息将是 UDL 类导出的一部分,因此会出现在源代码存储库中。 如果是经过仔细审查的已打包应用程序的统计信息,这是可取的,因为您会希望这些特定的统计信息能够推动生成所有应用程序部署的查询计划。 从 2021.2 开始,当查询一个根本没有任何统计资料且符合快速块采样条件的表时,IRIS会在查询计划开始时自动收集表的统计数据。在我们的测试中,使用最新的统计数据对比完全没有统计数据而言的好处,明显大于即时收集统计数据的成本。然而,对于一些客户来说,这会有一些副作用,即在开发人员的实例上自动收集的统计数据最终出现在源控制系统的存储定义中,并最终出现在打包的应用程序中。很明显,该开发者环境中的数据以及其上的统计数据可能无法代表真正的客户部署,并导致次优的查询计划。 这种情况可以轻松避免。 可以在调用$SYSTEM.OBJ.Export()时使用 `/exportselectivity=0` 限定符,在类定义导出中排除表统计信息。 可以使用$SYSTEM.OBJ.SetQualifiers("/exportselectivity=0")配置此标志的系统默认值。 然后,可以由最终部署中的自动收集来决定如何获取代表性的统计信息,将明确的统计信息收集作为部署过程的一部分,这会覆盖应用程序可能打包的任何内容,或者通过它们自己的导入/导出函数单独管理表统计信息:$SYSTEM.SQL.Stats.Table.Export()和 Import()。 从长远来看,我们打算将表统计信息移到数据中,而不是作为代码的一部分,并且更清晰地区分开发者明确定义的统计信息与从实际数据中收集的统计信息。 此外,我们正在计划实现更大程度的自动化,以根据表数据随时间的变化情况定期更新这些统计信息。 总结 在这篇文章中,我们概述了存储定义在 IRIS 对象-关系引擎中的作用,它如何支持Schema的演进,以及将其包含在源代码管理系统中的意义。 我们还介绍了目前将表统计信息存储在该存储定义中的原因,以及为了确保您的应用程序部署最终具有代表实际客户数据的统计信息而建议的开发做法。 如上所述,我们计划进一步增强这些功能,因此我们期待您对当前功能和计划功能提供反馈意见并适当优化我们的设计。
文章
Hao Ma · 二月 16, 2023

ChatGPT 为您创建消息转换?

A "big" or a "little" ask for ChatGPT? 几周前我尝试了 OpenAI GPT 的编码模型,看看它是否可以在医疗保健系统之间进行一些消息转换。它肯定可以,在相当大的程度上。已经将近 3 周了,对于 ChatGPT 来说是很长很长的时间,所以我想知道它现在成长得有多快,以及它是否可以为我们做一些集成工程师的工作,例如它是否可以创建一个 InterSystems COS DTL将 HL7 转换为 FHIR 信息? 在不到一两分钟的时间内,我立即得到了一些答案。 测试 首先我想测试一下我是在和它背后的正确“人”说话 问题一:如何将HL7 V2.4报文转为FHIR STU3? ChatGPT: 将 HL7 V2.4 消息转换为 FHIR STU3 涉及多个步骤,因为这两个标准具有不同的数据模型和结构。以下是该过程的高级概述: 识别 HL7 消息中需要映射到 FHIR 资源的相关数据元素。这将取决于具体用例和交换的数据。 为每个数据元素定义映射规则。这涉及识别对应于 HL7 数据元素的等效 FHIR 资源和元素,以及任何必要的转换或翻译。 编写一个程序或脚本,可以从 HL7 消息中提取数据,应用映射规则,并创建相应的 FHIR 资源。这可能涉及使用正则表达式、解析库和 FHIR 资源创建库的组合。 根据 STU3 规范验证 FHIR 资源,以确保它们符合标准并包含所有必需的元素。 以下是此过程如何将 HL7 V2.4 观察消息转换为 FHIR STU3 观察资源的示例: 识别 HL7 消息中的相关数据元素,例如观察代码、值和单位。 定义每个数据元素的映射规则,例如将 HL7 观察代码映射到 FHIR CodeableConcept,将值和单位映射到 FHIR 数量。 编写可以从 HL7 消息中提取数据、应用映射规则并创建具有适当元素的 FHIR 观察资源的程序或脚本。 根据 STU3 规范验证 FHIR 观察资源,以确保它包含所有必需的元素并且有效。 具体的实现细节将取决于所使用的编程语言和库,以及映射的特定 HL7 消息和 FHIR 资源。建议查阅 HL7 和 FHIR 规范,并在需要时寻求专家建议。 好吧,这不仅听起来是对的——实际上它的回答比大多数人会做的更全面。它现在引起了我的注意,所以让我们开始吧。 问题 2:您能否在 Python 中创建示例转换以将 HL7 转换为 FHIR ChatGPT : 当然!下面是一个示例 Python 代码片段,演示了如何将 HL7 消息转换为 FHIR STU3 观察资源: from hl7apy import core from fhir.resources.observation import Observation # Parse the HL7 message hl7_message = 'MSH|^~\&|SENDING_APP|SENDING_FACILITY|RECEIVING_APP|RECEIVING_FACILITY|20220527120512||ORU^R01|MSGID12345|P|2.4\r' \ 'OBX|1|NM|GLU^Glucose||100|mg/dL||N|||F|||20220527120512\r' hl7_parsed = core.parse_message(hl7_message) # Map HL7 elements to FHIR elements observation = Observation() observation.code = { "coding": [{ "system": "http://loinc.org", "code": hl7_parsed.obx[0].observation_id.value, "display": hl7_parsed.obx[0].observation_id.value }] } observation.valueQuantity = { "value": hl7_parsed.obx[0].value.value, "unit": hl7_parsed.obx[0].units.value, "system": "http://unitsofmeasure.org", "code": hl7_parsed.obx[0].units.value } # Serialize the FHIR resource as JSON observation_json = observation.json() # Print the serialized FHIR resource print(observation_json) 此代码使用hl7apy库解析HL7 消息并提取相关数据元素,并从fhir.resources中使用fhir.resources.observation类----- 创建 FHIR 观察资源的模块。 在此示例中,代码将 HL7 观察 ID 映射到 FHIR 代码元素,并将 HL7 值和单位映射到 FHIR valueQuantity 元素。然后使用json()方法将 FHIR 资源序列化为 JSON。 请注意,这是一个简单示例,您可能需要根据具体用例映射更多数据元素。此外,您还需要安装 hl7apy 和 fhir.resources库才能运行此代码。 这是个好答案 - 给出了一个不错的样板代码。现在让我们给它一个具体的挑战: 问题 3:您能否提供在 Intersystems COS DTL 中从 HL7 V2 到 FHIR Stu3 的示例转换 ChatGPT : 当然!以下是如何在 InterSystems COS DTL(数据转换语言)中将 HL7 V2 消息转换为 FHIR STU3 观察资源的示例: Class DTL.HL7toFHIR Extends %RegisteredObject { ClassMethod TransformHL7ToObservation(hl7Message As %String) As %String [ CodeMode = expression, DDLAllowed = 0 ] { set src = ##class(EnsLib.HL7.Message).%New() set src.DocType = "2.4" set src.Parse(hl7Message) set obs = ##class(FHIR.Resource.Observation).%New() set obs.subject = { "reference": "Patient/"_src.MSH.GetSendingApplication() } set obs.status = "final" set loincCode = src.GetSegment("OBX", 1).GetField(3).GetField(1) set obs.code = { "coding": [ { "system": "http://loinc.org", "code": loincCode, "display": loincCode } ] } set value = src.GetSegment("OBX", 1).GetField(5).GetField(1) set units = src.GetSegment("OBX", 1).GetField(6).GetField(1) set obs.valueQuantity = { "value": value, "unit": units, "system": "http://unitsofmeasure.org", "code": units } quit obs.%ToJSON() } } 此代码定义了一个名为 TransformHL7ToObservationI()的类方法,它将 HL7 V2 消息作为输入并返回 FHIR STU3 观察资源作为 JSON 字符串。 该方法首先创建一个新的 EnsLib.HL7.Message 对象并使用它来解析输入的 HL7 消息。然后创建一个新的 FHIR.Resource.Observation 对象并设置 subject和 status 元素。 接下来,代码从 HL7 消息中提取 LOINC 代码、值和单位,并将它们映射到 FHIR 资源中的相应元素。 最后,该方法将 FHIR 资源序列化为 JSON 并将其返回。 要使用此类方法,您可以从另一个 COS 方法或使用 InterSystems 的 REST API 从外部系统调用它。下面是如何从另一个 COS 方法调用该方法的示例: set hl7Message = "MSH|^~\&|SENDING_APP|SENDING_FACILITY|RECEIVING_APP|RECEIVING_FACILITY|20220527120512||ORU^R01|MSGID12345|P|2.4\rOBX|1|NM|GLU^Glucose||100|mg/dL||N|||F|||20220527120512\r" set fhirObservation = ##class (DTL.HL7toFHIR).TransformHL7ToObservation(hl7Message) write fhirObservation 请注意,您需要在 InterSystems 环境中定义 -----107----- 和 -----108----- 类,并具有运行 COS 代码的适当权限。 好吧,这可能是迄今为止我们所能预料到的最有效、最全面、最切题的答案,只需大约 25 秒。是不是? 既然我们在这里,我们需要公平对待我也喜欢的其他广泛使用的选项。于是我又怀着信心继续问了几个问题: 问题4:能否给出Orion Rhapsody中从HL7 V2到FHIR Stu3的转换示例 ChatGPT : 当然!以下是如何在 Orion Rhapsody 中将 HL7 V2 消息转换为 FHIR STU3 观察资源的示例: 首先,创建一个新的 Rhapsody 通道并添加一个 HL7 Connector 组件来接收输入的 HL7 V2 消息。 将新的数据转换组件添加到通道并将其配置为使用自定义脚本。 在数据转换组件中,添加以下脚本以将 HL7 V2 消息转换为 FHIR STU3 观察资源: function transform(msg) { var hl7 = Packages.ca.uhn.hl7v2.model.v24.message.ORU_R01(msg.getRawData()); var observation = new Packages.org.hl7.fhir.dstu3.model.Observation(); observation.setStatus(Packages.org.hl7.fhir.dstu3.model.Observation.ObservationStatus.FINAL); var codeableConcept = new Packages.org.hl7.fhir.dstu3.model.CodeableConcept(); var coding = new Packages.org.hl7.fhir.dstu3.model.Coding(); coding.setSystem("http://loinc.org"); coding.setCode(hl7.getRESPONSE().getORDER_OBSERVATION()[0].getOBR().getUniversalServiceIdentifier().getIdentifier().getValue()); coding.setDisplay(hl7.getRESPONSE().getORDER_OBSERVATION()[0].getOBR().getUniversalServiceIdentifier().getText().getValue()); codeableConcept.addCoding(coding); observation.setCode(codeableConcept); var quantity = new Packages.org.hl7.fhir.dstu3.model.Quantity(); quantity.setValue(hl7.getRESPONSE().getORDER_OBSERVATION()[0].getOBSERVATION()[0].getOBX().getObservationValue()[0].getValue().getValue()); quantity.setUnit(hl7.getRESPONSE().getORDER_OBSERVATION()[0].getOBSERVATION()[0].getOBX().getUnits().getText().getValue()); observation.setValue(quantity); var reference = new Packages.org.hl7.fhir.dstu3.model.Reference(); reference.setReference("Patient/" + hl7.getPATIENT_RESULT().getPATIENT().getPid3_PatientIdentifierList()[0].getID().getValue()); observation.setSubject(reference); return JSON.stringify(observation); } 保存脚本并通过向通道发送 HL7 V2 消息来测试转换。转换后的 FHIR STU3 观察资源应由数据转换组件以 JSON 格式输出。 此脚本使用 HAPI FHIR 和 HAPI HL7 v2 库,它们包含在 Orion Rhapsody 中。它首先使用 HAPI HL7 库解析输入的 HL7 V2 消息。 接下来,脚本创建一个新的 FHIR STU3 观测资源并设置它的 status, code, value, 和 subject 元素基于 HL7 消息中的相应值。 最后,脚本将 FHIR 资源序列化为 JSON 并将其返回。 请注意,这只是一个简单示例,可能不适用于所有 HL7 V2 消息类型或 FHIR 资源。您可能需要修改脚本以处理不同的消息类型或将其他字段映射到 FHIR 资源。 问题 5你能在 Mirth 集成引擎中创建一个示例转换,将 HL7 v2.4 转换为 FHIR R4 ChatGPT : 当然!以下是如何在 Mirth 中将 HL7 V2.4 消息转换为 FHIR R4 观察资源的示例: 首先,创建一个新的 Mirth 频道并添加一个 HL7v2 Listener 源连接器以接收输入的 HL7 V2 消息。 将新的 JavaScript Writer 目标连接器添加到通道并将其配置为使用自定义脚本。 在 JavaScript Writer 连接器中,添加以下脚本以将 HL7 V2 消息转换为 FHIR R4 观察资源: var hl7msg = msg['HL7v2 Message'].toString(); // get the HL7 V2 message var hl7data = Packages.ca.uhn.hl7v2.parser.Parser.parse(hl7msg); // parse the message with the HAPI HL7 parser // extract values from the HL7 message and create the FHIR resource var obs = new Packages.org.hl7.fhir.r4.model.Observation(); obs.setStatus(Packages.org.hl7.fhir.r4.model.Observation.ObservationStatus.FINAL); var code = new Packages.org.hl7.fhir.r4.model.CodeableConcept(); code.addCoding() .setSystem("http://loinc.org") .setCode(hl7data.get("OBR-4-1").getValue()) .setDisplay(hl7data.get("OBR-4-2").getValue()); obs.setCode(code); var value = new Packages.org.hl7.fhir.r4.model.Quantity(); value.setValue(hl7data.get("OBX-5-1").getValue()); value.setUnit(hl7data.get("OBX-6").getValue()); obs.setValue(value); var subject = new Packages.org.hl7.fhir.r4.model.Reference(); subject.setReference("Patient/" + hl7data.get("PID-3-1").getValue()); obs.setSubject(subject); // output the FHIR resource as JSON var json = org.hl7.fhir.r4.formats.JsonParser().setPrettyPrint(true).encodeResourceToString(obs); return json; 保存脚本并通过向通道发送 HL7 V2 消息来测试转换。转换后的 FHIR R4 观察资源应由 JavaScript Writer 连接器以 JSON 格式输出。 此脚本使用 HAPI FHIR 和 HAPI HL7 v2 库,它们包含在 Mirth 中。它首先从 Mirth 消息对象中提取 HL7 V2 消息,并使用 HAPI HL7 解析器对其进行解析。 接下来,该脚本创建一个新的 FHIR R4 观察资源并设置其 status, code, value, 和 subject 元素基于 HL7 消息中的相应值。 最后,脚本将 FHIR 资源序列化为 JSON 并将其返回。 请注意,这只是一个简单示例,可能不适用于所有 HL7 V2 消息类型或 FHIR 资源。您可能需要修改脚本以处理不同的消息类型或将其他字段映射到 FHIR 资源。 结论? 我不再有任何结论或下一步,因为即使是我的想象力也无法赶上它的飞跃。看起来我现在的选择似乎很有限:要么我可以花时间挑出它不完美的编码风格,要么我可以再过几周重新考虑我盘子里剩下的东西?我需要比以往任何时候都更仔细地思考这件事的潜力。 :)开个玩笑,虽然我很喜欢在这个论坛上发帖(感谢主持人),但我想到的另一个想法是,这“可能”实际上是一些竞争激烈的利基市场参与者迈向大众市场的重要推动力,对吧?由于各种原因,过去需要数年时间才能真正适应某些编码语言和脚本,但现在情况正在发生变化,使用 ChatGPT,它不仅提供精心编写的文档、说明和示例,而且还可以在未来几个月或几年内自动制造您选择的工程工具,对吗?它似乎能够提升“编程语言”的水平,最终使得编程语言中非功能性的特点,比如性能和服务质量等,将变得更重要。 太强了,就很离谱,这比官方文档还好使吗 还是有些错误的,详情可以到英文原文评论中看到
文章
姚 鑫 · 二月 19, 2021

第四十一章 Caché 变量大全 $ZSTORAGE 变量

# 第四十一章 Caché 变量大全 $ZSTORAGE 变量 包含进程的最大可用内存。 # 大纲 ``` $ZSTORAGE $ZS ``` # 描述 `$ZSTORAGE`包含`JOB`的进程私有内存的最大内存量(以`KB`为单位)。此内存可用于局部变量、堆栈和其他表。此内存限制不包括例程目标代码的空间。此内存根据需要分配给进程,例如在分配数组时。 一旦将此内存分配给进程,通常在该进程退出之前不会释放它。但是,当大量内存被使用(例如,大于32MB)然后被释放时,系统间IRIS会尝试在可能的情况下将释放的内存释放回操作系统。 还可以使用`$ZSTORAGE`设置最大内存大小。例如,以下语句将作业的最大进程专用内存设置为`524288 KB`: ```java SET $ZSTORAGE=524288 ``` 更改`$ZSTORAGE`会更改`$STORAGE`特殊变量的初始值,该变量包含进程的当前可用内存(以字节为单位)。 `$ZSTORAGE`的最大值为`2147483647`。`$ZSTORAGE`默认值为`262144`。`$ZSTORAGE`的最小值为`128`。`$ZSTORAGE`值大于最大值或小于最小值会自动默认为最大值或最小值。`$ZSTORAGE`设置为整数值;InterSystems IRIS截断任何小数部分(如果指定)。 可以通过更改最大每进程内存(KB)系统配置设置来更改`$ZSTORAGE`默认值。在管理门户中,依次选择System Administration、Configuration、Systtem Configuration、Memory和Startup。可以根据需要增加每个进程的最大内存(KB),最大为2147483647 KB。更改每个进程的最大内存(KB)会更改后续启动的进程的`$ZSTORAGE`值;对当前进程的`$ZSTORAGE`值没有影响。 ![image](77C3C8B1AB324C1993338A39D5579A17) ![image](85C6C2092243441193944B91DB6F6191) # 示例 以下示例将`$ZSTORAGE`设置为其最大值和最小值。尝试将`$ZSTORAGE`设置为小于最小值的值(16)时,会自动将`$ZSTORAGE`设置为其最小值(128): ```java /// d ##class(PHA.TEST.SpecialVariables).ZS() ClassMethod ZS() { SET $ZS=128 WRITE "minimum storage=",$ZS,! SET $ZS=16 WRITE "less than minimum storage=",$ZS,! SET $ZS=2147483647 WRITE "maximum storage=",$ZS,! } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZS() minimum storage=128 less than minimum storage=128 maximum storage=2147483647 ```
文章
Claire Zheng · 八月 17, 2021

FHIR标准和国际基于FHIR的互联互通实践(1):如何定义互联互通?

什么是互联互通?我们所说的互联互通其实就是国际上的互操作性,HIMSS对于互操作性定义的是:不同的信息系统、设备、应用系统之间、程序之间,在机构区域和国家边界之内,以及跨机构、区域和国家边界,以协调的方式来访问交换集成和协作使用数据的能力。 它对于使用效果提出了一些目标,比如希望能够提供及时和无缝的信息的可移植性,和优化个人和人群的健康。这是它定义的对于互操作性的4个不同的级别: 基础级别就比较简单,仅仅打通了系统之间进行数据通讯的通道; 结构级别,在基础级别之上定义了数据交换的格式和语法; 再上面有语义级别,语义级别是建立在行业通用的数据模型和数据编码之上的,使用标准化的行业语义来定义数据元素,使用标准的值集,因此语义级别的互操作性才是全行业可以理解的,并且有确定的行业意义的这种互操作,也就是说语义级别的互操作才是基于标准的互操作; 在语义级别之上,还有组织级别的互操作。通常这些都是由国家、国际的行业协会、行业标准开发组织开发的,加入了很多的其他方面的一些考虑,比如说政策的、社会的、法律的,分析了通用的业务流程和工作流,在这个基础之上设定了参与互操作的各方的角色、权限、服务、知情同意策略等等。 我国目前的互联互通标准化成熟度测试,其实就是组织级别的互操作。 注:本文根据InterSystems中国技术总监乔鹏演讲整理而成。
文章
Claire Zheng · 八月 17, 2021

FHIR标准和国际基于FHIR的互联互通实践(2):互操作标准构成

在医疗行业要实现互操作,应该要达到语义级别。只有达到语义级别才能保障医疗信息的准确和医疗行为的安全。而要达到语义级别,我们需要基于标准。 这里标准特别多,我们把它称之为五位一体。标准有5个方面。 首先是词汇/术语标准,是相互通讯的健康医疗信息系统需要依赖于结构化的词汇术语、代码值集、分类系统来进行相应的表述。词汇和术语的标准就是表达健康概念的标准。例如我们在使用的世界卫生组织的ICD-10,当然还有其他的,比如SNOMED-CT等等。 第二是内容标准,是描述信息交换过程中间的数据内容和结构的标准。它还包括了通用数据的定义,例如我们熟悉的HL7 CDA、 HL7 V2、 C-CDA,这些都是内容标准。 第三是传输标准,定义了计算机系统、文档架构、临床模板、用户界面,数据的连接之间的交换的消息的格式,传输的方式等等。通过传输方式,可以来确定是通过推、还是拉的方式来进行数据的共享交换。在这个层面DICOM 、IHE都是传输的一个标准。 第四是隐私与安全的标准,也就是说我们要确定谁、什么时候、出于什么目的、可以使用哪种个人健康信息的权利,以及我们如何来保护这些健康信息的机密性、可用性、完整性的一些标准。美国有HIPPA,欧洲的GDPR,这些都是关于隐私和安全的。 第五是标识符的标准,标识符的标准是用来唯一标识患者、机构、医护技、设备等等的标准,例如说我们互联互通里面用到的OID。 注:本文根据InterSystems中国技术总监乔鹏演讲整理而成。
问题
j ay · 三月 22, 2023

cache2016版本默认sample空间没有Backup.General

1、默认samples空间没有Backup.General无法进行外部备份 2、%SYS空间有Backup.General Backup.General怎么在其他空间里创建这个类 我没明白问题, 外部备份是在%sys命名空间执行的呀, 为什么要在在sample里执行备份呢? 1、我对这个cache不太理解,第一次做这个,我目前做的外部备份是: 前提:所有数据都是用sample命名空间进行添加的 a. 用sample命名空间,去连接cache,调用Backup.General ExternalFreeze进行冻结 b. 备份mgr目录下面sample数据库文件CACHE.DAT c. 调用ExternalThaw解冻 问题:1.如果我用%SYS命名空间去冻结解冻,备份mgr目录下面sample数据库文件CACHE.DAT,数据是恢复不了的 2.日志journal备份后在重新写入恢复,cache会启动不了 我不知道这样的备份恢复流程对不对,能说一下正确的外部备份流程吗 冻结其实是控制系统的写进程,让它暂时挂起来,在数据库日志上做个标记。这些都不是那一个数据库比如sample的事情,是整个系统的操作。 你的问题1: 如果我用%SYS命名空间去冻结解冻,备份mgr目录下面sample数据库文件CACHE.DAT,数据是恢复不了的。 怎么恢复不了?那一步出错了? 问题2: 你是先恢复数据库, 再恢复日志的是吗?出的什么错误? 这样, 你先看看这两个链接, 如果在有问题, 我找我们的专家和你私聊。第2个帖子有点长,里面连冻结解冻的脚本都包括了, 很详细 ,:) https://cn.community.intersystems.com/post/faq-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E7%B3%BB%E5%88%97-%E7%B3%BB%E7%BB%9F%E7%AE%A1%E7%90%86%E7%AF%87-%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD https://community.intersystems.com/post/intersystems-data-platforms-and-performance-%E2%80%93-vm-backups-and-cach%C3%A9-freezethaw-scripts
文章
Hao Ma · 四月 29, 2021

WebGateway系列(4): 配置HTTPS访问IRIS的Web服务

经常被问到有关IRIS如何支持SSL,HTTPS的问题,有必要写个东西介绍一下。 ##HTTPS的原理 简单的说,https实现两个目的:一是访问网站加密,2是确认被访问的网站是真的。 首先,被访问的网站要申请一个证书,这个证书必须是权威机构发放的,比如google, VeriSign等等,所有的浏览器里有预装了这些组织的公钥(Public Key),因此能确认你提供的证书真是这些组织给出的,而这个证书可以证明你的网站的身份。注意证书证明的是提供服务的组织和服务的真实性,和用什么设备没关系,也就是说,IRIS不管证书的事儿。 接下去,被访问的服务器可以生成公钥和私钥,和客户端交换key,生成整个世界只有两者知道的security code,用来两者之间数据的交换。详细的过程和消息交互可以在网上找到很多很好的文章和视频,比如这个: [How does HTTPS work? What's a CA? What's a self-signed Certificate?](https://www.youtube.com/watch?v=T4Df5_cojAs)。 如果是测试环境或者使用者可以控制的内部网络,self-signed证书非常常用。self-signed证书就是不去花钱找人认证,而是告诉客户端,我这个证书是自己认证的,你知道我这台机器试内网的一个机器,不用权威机构证明我服务器的身份,咱们交换一下钥匙把通信加密了吧。操作系统,各种Web服务器都提供这样的假证书,可以用于测试。浏览器访问这样的网站时会提醒用户这个网站不安全,客户需要确认继续访问。 ## IRIS的https访问 如果要访问的是IRIS上的http服务或者页面,需要做的是在连接IRIS的Web服务器配置SSL/TLS。有关IRIS和IRIS WebGateway的介绍,请查看[这个系列前面的文章](https://cn.community.intersystems.com/post/webgateway%E7%B3%BB%E5%88%971-web-gateway%E4%BB%8B%E7%BB%8D)。 *不需要在IRIS或者IRIS Gateway做任何配置。在IRIS文档里有各种有关SSL/TLS的内容,除非你要开发一个TCP层的使用SSL/TLS的应用或者IRIS作为客户端访问其他HTTPS的服务,你根本不用阅读。* 下面简单介绍配置Apache Web服务器简单实现IRIS管理页面的HTTPS访问的步骤。 **1.apache Web服务器安装SSL.** 如果你的Apache没有安装过SSL组件,运行下面命令安装 ``` yum -y install mod_ssl ``` 命令执行结束安装完成后,在/etc/httpd/modules目录会添加了mod_ssl.so,并且在/etc/httpd/conf.d 目录下会出现一个ssl.conf文件。 如果是Windows, 您需要下载使用Windows的Apache服务器,比如从这个页面:[Apach2.4.46](https://www.apachehaus.com/cgi-bin/download.plx)。 按照说明,您需要将软件解压缩到一个目录,比如c:/Apache24,然后执行 "httpd -k install"安装。 并且, 你要确保httpd.conf文件中下面两行没有被注释 LoadModule ssl_module modules/mod_ssl.so Include conf/extra/httpd-ssl.conf 访问https://WebServerIP,你会被浏览器提醒这不是个可信任的网站,是不是还要继续访问,确认后会看到Apache的测试页,访问是成功的。 **2.到IRIS的WebGateway的连接。** 我一般放在一个单独的配置文件里,在linux下是在./conf.d/isc.conf, 在Windows系统是在./extra/httpd-isc.conf。这个配置文件是要被include在httpd.conf里面。配置https并不需要修改这个配置文件。下面是在Windows下的httpd-isc.conf的配置示意。 LoadModule csp_module_sa C:/InterSystems/WebGateway/CSPa24.dll SetHandler csp-handler-sa SetHandler csp-handler-sa CSPFileTypes * Alias /csp/ c:/InterSystems/WebGateway/csp/ AllowOverride None Options MultiViews FollowSymLinks ExecCGI Require all granted Require all denied 这时您应该可以测试到IRIS管理页面的HTTPS访问了。 **3. 获得证书并添加到Web服务器。** 这步是可选的。面向公众服务的Web服务通常会购物证书, 而内部服务个个客户的网络中会有相关的CA的处理方式,相应的如何修改Apache服务器的配置请自行查看文档。 五一节快乐
文章
Michael Lei · 一月 26, 2022

跟Robert Cemper老师学习 IRIS 和Caché

@Robert.Cemper1003 我们全球(几乎是)最资深最活跃粉丝最多的社区成员 (最畅销!). Robert 老师从1970年代就开始从事软件行业,从1978年就开始从事MUMPS, DSM, ... Caché, ...IRIS等等直到今天还在活越地写代码。所以跟着Robert老师超过100篇的社区文章中学习 InterSystems 技术是最好不过的选择了。我们按照主题精选了一些文章,后面会陆续翻译,大家希望优先翻译哪一篇,欢迎在评论区留言。 关于SQL DB Migration using SQLgateway IRIS 使用 SQLgateway 迁移到IRIS. Dataset Lightweight M:N 和 M:N Relationship 高级SQL 关系 Using ClassQueries() as Tables 把ClassQueries 当成表来使用 (就像使用objectscript的视图 ) Effective use of Collection Indexing and Querying Collections through SQL SQL 查询采集索引 Show Global by SQL SELECT 看 globals的SQL 语句 SQL for ^ERROR Global get ^ERROR global 的SQL 语句 SPOOL as SQL Table get the SPOOL report 的SQL 语句 Materialized Views 如何用IRIS实现 "物化视图" Semi-Persistent Classes and Tables 仅仅持久化你需要的类的内容 Static WHERE Conditions 使用sql 类方法来筛选where里面的数据 Repairing your Index 重构索引------------------------------------------------------------------ 关于Python 和原生 API AoC2021-rcc 使用嵌入式Python的25个样本代码 Using ZPM for Node.js 从Node.js 使用 zpm 模块 WebSocket Client with embedded Python 嵌入式Python的WebSocket 客户端 Trying Embedded Python IRIS内的python 例子 Native API for ObjectScript Demo 和 IRIS Native API for ObjectScript 简单的原生API demo IRIS-NativeAPI-Nodejs-compact 和 WebSocket Client JS with IRIS Native API as Docker Micro Server Node.js 和原生 NativeAPI的轻量级docker实例 Client for WebSockets based on Node.js 基于 Node.js的web sockets ------------------------------------------------------------------ 关于 Globals, ObjectScript, ZPM 和 COS Traditional Debugging in ObjectScript Debug 你的 ObjectScript 代码 The future position of ObjectScript ObjectScript开发的未来 Helper for ObjectScript Language Extensions 扩展ObjectScript的帮助app FOREACH for ObjectScript 从Macro到foreach 的 ObjectScript ObjectScript over ODBC ObjectScript使用 ODBC Global Scanning & Slicing 输出任何级别的globals细节 ZPMshow - a helper for tired fingers 如何使用ZPM Un-Typical persistence 关于持久化的高级技巧 persistence snapshot to JSON 从快照 到 JSON A function to check if string is JSON object 检查字符串是否为JSON对象的功能 fast JSON formatting for Caché / Ensemble 和 fast JSON formatting for IRIS 漂亮地格式化你的 JSON 字符串 Organize %ZLANG** 关于ZLANG的技巧 Wrap JSON to multi lines / Test ISJSON JSON 多行处理 Parameter passing to Language Extentions 为语言扩展给参数传值(SystemFunctions, SystemVariables, SystemCommands) Terminal Multi-Line Command Editor 和 Terminal Multi-Line Option 多行终端 Backport %JSON.* to Caché 把%JSON backport到 Caché Moving Code from IRIS to Caché 把代码从 IRIS 移动到 Caché Multidimensional Property Persistence - Part 1 (Classic) 持久化多维属性 SUDOKU demo ObjectScript SUDOKU Demo Synchronize Data with DSTIME DSTime 例子 Adopted Bitmaps example now on Open Exchange 和The adopted Bitmap 使用 BITMAPS的应用例子 Client for WebSockets based on CSP 基于CSP的web socket客户端 Execute Server Commands from Caché / Ensemble / IRIS 从caché/ensemble/IRIS执行服务器命令. Sharding evaluation #1 和 Manual Setup of Sharding IRIS分片技术 Global Time Management datetime属性里的UTC time properties Summary on Local Variable Scoping Variable scopings The 'unlimited' UNIQUE index UNIQUE 索引技术 @Indirection and eXECUTE - why ? 动态调用 Light weight EXCEL download 轻量数据集/excel Date before Dec.1840 ? Negative $H(orolog) ? 关于日期 dates------------------------------------------------------------------ 关于互操作性 Interoperability Generating OFX [V1] 为MS OFX format自定义 production ------------------------------------------------------------------ 关于IRIS 基础架构和docker Storage Considerations on large data sets 处理大型数据的存储考虑 datasets Docker Desktop Windows - disk space consumption 关于 docker 磁盘空间管理 SSH for IRIS container IRIS容器的 SSH 访问. How to execute IRIS restart from inside 正确滴重启 IRIS Splitting an IRIS db to multiple drives 把IRISDB分解到多个HD drives How to shrink IRISTEMP db in runtime? 节省 IRISTEMP 磁盘分配 IRIS easy ECP workbench 使用IRIS ECP Making development in Docker environment easier 让 docker 环境下的开发更容易 Using ECP across IRIS and Caché 跨 IRIS 和 Cache 使用ECP ECP between IRIS and Caché / Ensemble 在 IRIS, Cache 和 Ensemble同时存在的情况下使用ECP moving code between IRIS and CACHÉ 在 IRIS 和 Cache之间移动代码 Break in case of Emergency 紧急情况下的远程访问 IRIS-Docker-micro-Durability 再次运行 docker 实例时保留参数 Docker vs. Durability 较少可用资源下的Docker instances WebSocket Client IRIS internal %Net.WebSocket.客户端 WebSocket Echo Server in IRIS Web socker demo A more useFull Object Dump 一个更有用的Object dump Using Interjob communication (IJC) 使用 IJC Background Jobs over ECP 分布式任务--ECP 下的后台任务 Simple Remote Server Control 简单的远程服务器监控 SPOOL - the forgotten device SPOOL 管理被遗忘的设备------------------------------------------------------------------ 其他 Successful Troubleshooting 成功的Troubleshooting 感谢🙏 Robert Cemper先生, 我们感谢您! From The Roots to InterSystems Robert先生的经历简介
文章
Hao Ma · 三月 26, 2021

精华文章---基于Docker的一体化集成AI环境中部署机器学习/深度学习模型

**关键字**:IRIS,IntegratedML,Flask,FastAPI,TensorFlow Serving,HAProxy,Docker,Covid-19 ## 目的: 过去几个月里,我们提到了一些深度学习和机器学习的快速演示,包括一个简单的 Covid-19 X 射线图像分类器和一个用于可能的 ICU 入院的 Covid-19 实验室结果分类器。  我们还介绍了 ICU 分类器的 IntegratedML 演示实现。  虽然“数据科学”远足仍在继续,但从“数据工程”的角度来看,或许也是尝试一些 AI 服务部署的好时机 - 我们能否将目前所接触到的一切都封装成一套服务 API?  我们可以利用哪些常用的工具、组件和基础架构,以最简单的方式实现这样的服务堆栈?   ## 范围 ### **范围内:** 作为快速入门,我们可以使用 docker-compose 将以下 docker 化组件部署到 AWS Ubuntu 服务器中 * **HAProxy ** - 负载均衡器 * **Gunicorn** vs. **Univorn ** - Web 网关****服务器 * **Flask** vs. **FastAPI** - Web 应用 UI 的应用服务器、服务 API 定义和热图生成等 * **TensorFlow Model Serving** vs. **TensorFlow-GPU Model Serving** - 图像等分类的应用后端服务器 * IRIS **IntegratedML** - 带有 SQL 界面的统一 App+DB AutoML * **Jupyter Notebook** 中模拟客户端进行**基准测试**的 Python3 *  Docker 和 **docker-compose** * 配备 Tesla T4 GPU 的 **AWS Ubuntu** 16.04  注:配备 GPU 的 TensorFlow Serving 仅用于演示目的 - 您只需关闭 GPU 相关镜像(在 dockerfile 中)和配置(在 docker-compose.yml 中)。 ### **范围外**或者在下一个愿望清单上: * **Nginx** 或 **Apache** 等 Web 服务器在演示中暂时省略。 * **RabbitMQ** 和 Redis - 用于可靠消息传递的队列代理,可由 IRIS 或 Ensemble 替代。    * **IAM** ([Intersystems API Manger](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=AIAM)) 或 **Kong** 在愿望清单上 * **SAM **(Intersystems [System Alert & Monitoring](https://docs.intersystems.com/sam/csp/docbook/DocBook.UI.Page.cls?KEY=ASAM))  * **ICM** ([Intersystems Cloud Manager](https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=PAGE_DEPLOYMENT_ICM)) 与 **Kubernetes** Operator - 诞生以来一直是我的最爱 * **FHIR**(基于 Intesystems IRIS 的 FHIR R4 服务器和 FHIR Sandbox,用于 FHIR 应用上的 SMART) * **CI/CD** devop 工具或 **Github Actions** “机器学习工程师”必然会动手遍历这些组件,在服务生命周期内提供一些生产环境。 随着时间的推移,我们可以扩大范围。   ## GitHub 仓库 完整源代码位于 [integratedML-demo-template 仓库](https://github.com/intersystems-community/integratedml-demo-template)也与新仓库一同重用。   ## 部署模式 以下为此“Docker 中的 AI 演示”测试框架的逻辑部署模式。 出于演示目的,我特意创建了 2 个独立的堆栈,用于深度学习分类以及 Web 渲染,然后使用 HAProxy 作为软负载均衡器,以无状态方式在这 2 个堆栈之间分配传入的 API 请求。 * Guniorn + Flask + TensorFlow Serving * Univcorn + FaskAPI + TensorFlow Serving GPU IRIS 与 IntegratedML 用于机器学习演示示例,即 ICU 预测的前一篇文章中。 在目前的演示中,我省略了一些生产服务需要或考虑的常用组件: * Web 服务器:Nginx 或 Apache 等 HAProxy 和 Gunicorn/Uvicorn 之间需要它们,以进行正确的 HTTP 会话处理,即避免 DoS 攻击等。 * 队列管理器和数据库:RabbitMQ 和/或 Redis 等,在 Flask/FastAPI 和后端服务之间,用于可靠的异步服务和数据/配置持久性等。   * API 网关:IAM 或 Kong 集群,在 HAProxy 负载均衡器和 Web 服务器之间进行 API 管理,不创建单点故障。 * 监视和警报:SAM 很不错。 * 为 CI/CD DevOps 进行配置:云中立的部署和管理以及带有其他常见 devops 工具的 CI/CD 将需要带 K8s 的 ICM。 其实,IRIS 本身当然可以作为企业级队列管理器以及用于可靠消息传递的高性能数据库。 在模式分析中,很明显 IRIS 可以代替 RabbitMQ/Redis/MongoDB 等队列代理和数据库,得到更好的整合,大幅减少延迟,并提高整体性能。  还有,IRIS Web Gateway(先前为 CSP Gateway)当然可以代替 Gunicorn 或 Unicorn 等,对吧?     ## 环境拓扑 在全 Docker 组件中实现上述逻辑模式有几种常见选项。 首先:   * docker-compose * docker swarm 等 * Kubernetes 等  * 带 K8s 操作的 ICM 这个演示从功能性 PoC 和一些基准测试的“docker-compose”开始。 当然,我们很想使用 K8s,也有可能随着时间的推移使用 ICM。  如 [docker-compose.yml](https://github.com/zhongli1990/covid-ai-demo-deployment/blob/master/docker-compose.yml) 文件中所述,它的环境拓扑在 AWS Ubuntu 服务器上的物理实现最终将是:   上图显示了如何将所有 Docker 实例的**服务端口**映射并直接暴露于 Ubuntu 服务器以进行演示。 在生产中应该全部经过安全加固。 纯粹出于演示目的,所有容器都连接到同一个 Docker 网络中;而在生产中,它将被分为外部可路由和内部不可路由。   ## Docker 化组件  下面显示了主机中的那些**存储卷**如何按照这个 [docker-compose.yml](https://github.com/zhongli1990/covid-ai-demo-deployment/blob/master/docker-compose.yml) 文件的指示挂载到各个容器实例:  ubuntu@ip-172-31-35-104:/zhong/flask-xray$ tree ./ -L 2 ./ ├── covid19 (Flask+Gunicorn container and Tensorflow Serving container will mount here) │ ├── app.py (Flask main app: Both web application and API service interfaces are defined and implemented here) │ ├── covid19_models (Tensorflow models are published and versioned here for image classification Tensorflow Serving container with CPU) │ ├── Dockerfile (Flask server with Gunicorn: CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:5000", "--workers", "4", "--threads", "2"]) │ ├── models (Models in .h5 format for Flask app and API demo of heatmap generation by grad-cam on X-Rays) │ ├── __pycache__ │ ├── README.md │ ├── requirements.txt (Python packages needed for the full Flask+Gunicorn apps) │ ├── scripts │ ├── static (Web static files) │ ├── templates (Web rendering templates) │ ├── tensorflow_serving (Config file for tensorflow serving service) │ └── test_images ├── covid-fastapi (FastAPI+Uvicorn container and Tensorflow Serving with GPU container will mount here) │ ├── covid19_models (Tensorflow serving GPU models are published and versioned here for image classification) │ ├── Dockerfile (Uvicorn+FastAPI server are started here: CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4" ]) │ ├── main.py (FastAPI app: both web application and API service interfaces are defined and implemented here) │ ├── models (Models in .h5 format for FastAPI app and API demo of heatmap generation by grad-cam on X-Rays) │ ├── __pycache__ │ ├── README.md │ ├── requirements.txt │ ├── scripts │ ├── static │ ├── templates │ ├── tensorflow_serving │ └── test_images ├── docker-compose.yml (Full stack Docker definition file. Version 2.3 is used to accommodate Docker GPU "nvidia runtime", otherwise can be version 3.x) ├── haproxy (HAProxy docker service is defined here. Note: sticky session can be defined for backend LB. ) │ ├── Dockerfile │ └── haproxy.cfg └── notebooks (Jupyter Notebook container service with Tensorflow 2.2 and Tensorboard etc) ├── Dockerfile ├── notebooks (Sample notebook files to emulate external API Client apps for functional tests and API benchmark tests in Python on the load balancer etc) └── requirements.txt 注:以上 [docker-compose.yml](https://github.com/zhongli1990/covid-ai-demo-deployment/blob/master/docker-compose.yml) 用于Covid-19 X 射线的深度学习演示。  它与另一个 [integratedML-demo-template](https://github.com/intersystems-community/integratedml-demo-template) 的 [docker-compose.yml](https://github.com/intersystems-community/integratedml-demo-template/blob/master/docker-compose.yml) 一起使用,形成环境拓扑中显示的完整服务堆栈。     ## 服务启动  简单的 **docker-compose up -d** 即可启动所有容器服务: ubuntu@ip-172-31-35-104:~$ docker psCONTAINER ID        IMAGE                                 COMMAND                  CREATED             STATUS                PORTS                                                                              NAMES31b682b6961d        iris-aa-server:2020.3AA               "/iris-main"             7 weeks ago         Up 2 days (healthy)   2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:8091->51773/tcp, 0.0.0.0:8092->52773/tcp   iml-template-master_irisimlsvr_16a0f22ad3ffc        haproxy:0.0.1                         "/docker-entrypoint.…"   8 weeks ago         Up 2 days             0.0.0.0:8088->8088/tcp                                                             flask-xray_lb_171b5163d8960        ai-service-fastapi:0.2.0              "uvicorn main:app --…"   8 weeks ago         Up 2 days             0.0.0.0:8056->8000/tcp                                                             flask-xray_fastapi_1400e1d6c0f69        tensorflow/serving:latest-gpu         "/usr/bin/tf_serving…"   8 weeks ago         Up 2 days             0.0.0.0:8520->8500/tcp, 0.0.0.0:8521->8501/tcp                                     flask-xray_tf2svg2_1eaac88e9b1a7        ai-service-flask:0.1.0                "gunicorn app:app --…"   8 weeks ago         Up 2 days             0.0.0.0:8051->5000/tcp                                                             flask-xray_flask_1e07ccd30a32b        tensorflow/serving                    "/usr/bin/tf_serving…"   8 weeks ago         Up 2 days             0.0.0.0:8510->8500/tcp, 0.0.0.0:8511->8501/tcp                                     flask-xray_tf2svg1_1390dc13023f2        tf2-jupyter:0.1.0                     "/bin/sh -c '/bin/ba…"   8 weeks ago         Up 2 days             0.0.0.0:8506->6006/tcp, 0.0.0.0:8586->8888/tcp                                     flask-xray_tf2jpt_188e8709404ac        tf2-jupyter-jdbc:1.0.0-iml-template   "/bin/sh -c '/bin/ba…"   2 months ago        Up 2 days             0.0.0.0:6026->6006/tcp, 0.0.0.0:8896->8888/tcp                                     iml-template-master_tf2jupyter_1 以 **docker-compose up --scale fastapi=2 --scale flask=2 -d** 为例,将水平扩展到 2 个 Gunicorn+Flask 容器和 2 个 Univcorn+FastAPI 容器: ubuntu@ip-172-31-35-104:/zhong/flask-xray$ docker psCONTAINER ID        IMAGE                                 COMMAND                  CREATED             STATUS                PORTS                                                                              NAMESdbee3c20ea95        ai-service-fastapi:0.2.0              "uvicorn main:app --…"   4 minutes ago       Up 4 minutes          0.0.0.0:8057->8000/tcp                                                             flask-xray_fastapi_295bcd8535aa6        ai-service-flask:0.1.0                "gunicorn app:app --…"   4 minutes ago       Up 4 minutes          0.0.0.0:8052->5000/tcp                                                             flask-xray_flask_2 ... ... 在“integrtedML-demo-template”的工作目录下再运行一个“docker-compose up -d”,就出现了上面列表中的 irisimlsvr 和 tf2jupyter 容器。   ## 测试 ### **1. 带有简单 UI 的 AI 演示 Web 应用** 启动上述 docker 服务后,我们可以访问 AWS EC2 实例中托管的 [Covid-19 肺部 X 射线检测](https://community.intersystems.com/post/run-some-covid-19-lung-x-ray-classification-and-ct-detection-demos)演示 Web 应用,临时地址为 http://ec2-18-134-16-118.eu-west-2.compute.amazonaws.com:8056/ 以下是从我的手机截取的屏幕。  它有一个非常简单的演示 UI:基本上只需要点击“Choose File”,然后点击“Submit”按钮,上传 [X 射线图像](https://github.com/zhongli1990/Covid19-X-Rays/tree/master/all/test),然后应用就会显示分类报告。 如果图像被分类为 Covid-19 X 射线图像,则会[显示热图](https://community.intersystems.com/post/explainability-and-visibility-covid-19-x-ray-classifiers-deep-learning),通过 DL 模拟“检测到的”病变区域;如果未被分类为 Covid-19 X 射线图像,分类报告将仅显示上传的 X 射线图像。          该 Web 应用是一个 Python 服务器页面,其逻辑主要在 [FastAPI 的 main.py](https://github.com/zhongli1990/covid-ai-demo-deployment/blob/master/covid-fastapi/main.py) 文件以及 [Flask 的 app.py](https://github.com/zhongli1990/covid-ai-demo-deployment/blob/master/covid19/app.py) 文件中进行编码。 如果有更多的空闲时间,我可能会详细说明 Flask 和 FastAPI 之间的编码和惯例差异。  其实我希望可以为 AI 演示托管对比 Flask、FastAPI 与 IRIS。    ### **2. 测试演示 API**       FastAPI(在端口 8056 处公开)内置 Swagger API 文档,如下所示。 这非常好用。 我需要做的就是在其 URL 中使用“/docs”,例如:  ![](/sites/default/files/inline/images/images/image(875).png) 我内置了一些占位符(如 /hello 和 /items)和一些真正的演示 API 接口(如 /healthcheck、/predict 和 predict/heatmap)。   **来对这些 API 进行一个快速测试**,在我为这个 AI 演示服务准备的一个 [Jupyter Notebook 示例文件](https://github.com/zhongli1990/covid-ai-demo-deployment/tree/master/notebooks/notebooks)中运行一些 Python 行(作为 API 客户端应用模拟器)。   下面我以运行这个文件为例: 首先测试后端 TF-Serving(端口 8511)和 TF-Serving-GPU(端口 8521)是否正常运行:  !curl http://172.17.0.1:8511/v1/models/covid19 # tensorflow serving !curl http://172.17.0.1:8521/v1/models/covid19 # tensorflow-gpu serving { "model_version_status": [ { "version": "2", "state": "AVAILABLE", "status": { "error_code": "OK", "error_message": "" } } ] } { "model_version_status": [ { "version": "2", "state": "AVAILABLE", "status": { "error_code": "OK", "error_message": "" } } ] }   然后测试以下服务 API 是否正常运行: Gunicorn+Flask+TF-Serving Unicorn+FastAPI+TF-Serving-GPU 以上麻烦服务之前的负载均衡器 HAProxy r = requests.get('http://172.17.0.1:8051/covid19/api/v1/healthcheck') # tf srving docker with cpu print(r.status_code, r.text) r = requests.get('http://172.17.0.1:8056/covid19/api/v1/healthcheck') # tf-serving docker with gpu print(r.status_code, r.text) r = requests.get('http://172.17.0.1:8088/covid19/api/v1/healthcheck') # tf-serving docker with HAproxy print(r.status_code, r.text) 结果应为: 200 Covid19 detector API is live! 200 "Covid19 detector API is live!\n\n" 200 "Covid19 detector API is live!\n\n"   测试一些功能性 API 接口(例如 **/predict/heatmap**)来返回输入 X 射线图像的分类和热图结果。  根据 API 定义,在通过 HTTP POST 发送之前,入站图像为 based64 编码: %%time # importing the requests library import argparse import base64 import requests # defining the api-endpoint API_ENDPOINT = "http://172.17.0.1:8051/covid19/api/v1/predict/heatmap" image_path = './Covid_M/all/test/covid/nejmoa2001191_f3-PA.jpeg' #image_path = './Covid_M/all/test/normal/NORMAL2-IM-1400-0001.jpeg' #image_path = './Covid_M/all/test/pneumonia_bac/person1940_bacteria_4859.jpeg' b64_image = "" # Encoding the JPG,PNG,etc. image to base64 format with open(image_path, "rb") as imageFile: b64_image = base64.b64encode(imageFile.read()) # data to be sent to api data = {'b64': b64_image} # sending post request and saving response as response object r = requests.post(url=API_ENDPOINT, data=data) print(r.status_code, r.text) # extracting the response print("{}".format(r.text)) 所有此类[测试图像也已上传到 GitHub](https://github.com/zhongli1990/Covid19-X-Rays/tree/master/all/test)。  以上代码的结果将为: 200 {"Input_Image":"http://localhost:8051/static/source/0198f0ae-85a0-470b-bc31-dc1918c15b9620200906-170443.png","Output_Heatmap":"http://localhost:8051/static/result/Covid19_98_0198f0ae-85a0-470b-bc31-dc1918c15b9620200906-170443.png.png","X-Ray_Classfication_Raw_Result":[[0.805902302,0.15601939,0.038078323]],"X-Ray_Classification_Covid19_Probability":0.98,"X-Ray_Classification_Result":"Covid-19 POSITIVE","model_name":"Customised Incpetion V3"} {"Input_Image":"http://localhost:8051/static/source/0198f0ae-85a0-470b-bc31-dc1918c15b9620200906-170443.png","Output_Heatmap":"http://localhost:8051/static/result/Covid19_98_0198f0ae-85a0-470b-bc31-dc1918c15b9620200906-170443.png.png","X-Ray_Classfication_Raw_Result":[[0.805902302,0.15601939,0.038078323]],"X-Ray_Classification_Covid19_Probability":0.98,"X-Ray_Classification_Result":"Covid-19 POSITIVE","model_name":"Customised Incpetion V3"} CPU times: user 16 ms, sys: 0 ns, total: 16 ms Wall time: 946 ms   ### **3. 基准测试演示服务 API** 我们设置了一个 HAProxy 负载均衡器实例。 我们还启动了一个有 4 个工作进程的 Flask 服务,以及一个也有 4 个工作进程的 FastAPI 服务。 为什么不直接在 Notebook 文件中创建 8 个 Pyhon 进程,模拟 8 个并发 API 客户端向演示服务 API 发送请求,看看会发生什么  #from concurrent.futures import ThreadPoolExecutor as PoolExecutor from concurrent.futures import ProcessPoolExecutor as PoolExecutor import http.client import socket import time start = time.time() #laodbalancer: API_ENDPOINT_LB = "http://172.17.0.1:8088/covid19/api/v1/predict/heatmap" API_ENDPOINT_FLASK = "http://172.17.0.1:8052/covid19/api/v1/predict/heatmap" API_ENDPOINT_FastAPI = "http://172.17.0.1:8057/covid19/api/v1/predict/heatmap" def get_it(url): try: # loop over the images for imagePathTest in imagePathsTest: b64_image = "" with open(imagePathTest, "rb") as imageFile: b64_image = base64.b64encode(imageFile.read()) data = {'b64': b64_image} r = requests.post(url, data=data) #print(imagePathTest, r.status_code, r.text) return r except socket.timeout: # in a real world scenario you would probably do stuff if the # socket goes into timeout pass urls = [API_ENDPOINT_LB, API_ENDPOINT_LB, API_ENDPOINT_LB, API_ENDPOINT_LB, API_ENDPOINT_LB, API_ENDPOINT_LB, API_ENDPOINT_LB, API_ENDPOINT_LB] with PoolExecutor(max_workers=16) as executor: for _ in executor.map(get_it, urls): pass print("--- %s seconds ---" % (time.time() - start)) 因此,处理 8x27 = 216 张测试图像花了 74s。 这个负载均衡的演示堆栈每秒能够处理 3 张图像(通过将分类和热图结果返回客户端): --- 74.37691688537598 seconds --- 从 Putty 会话的 Top 命令中,我们可以看到在上述基准脚本开始运行后,8 个服务器进程(4 个 gunicorn + 4 个 unicorn/python)开始加速   ## 未来计划 这篇帖子只是将“All-in-Docker AI 演示”部署堆栈组合为测试框架的起点。 接下来,我希望根据 FHIR R4 等添加更多的 API 演示接口(例如 Covid-19 ICU 预测接口等),并添加一些支持 DICOM 输入格式。 这也可以成为一个测试台,用于探索与 IRIS 托管的 ML 功能更紧密的集成。 未来它也可以用作测试框架(也是一个非常简单的框架),随着我们在医疗影像、人群健康或个性化预测以及 NLP 等各个 AI 领域的发展,截取越来越多的 ML 或 DL 专业模型。 我还在[上一篇帖子的末尾(在“未来计划”部分)](https://community.intersystems.com/post/run-some-covid-19-icu-predictions-ml-vs-integratedml-part-ii)列出了一个愿望清单。   
文章
姚 鑫 · 五月 16, 2021

第二章 使用%UnitTest进行单元测试

# 第二章 使用%UnitTest进行单元测试 本教程的第二部分介绍了如何使用%UnitTest包对InterSystems IRIS代码进行单元测试。完成本教程的这一部分后,将能够: - 解释`%UnitTest`包中三个主要类的角色。 - 列出基于`%UnitTest`包的单元测试类和方法的要求。 - 创建并执行方法的单元测试。 - 浏览`%UnitTest.Manager`创建的测试报告。 - 执行单元测试时,使用`%UnitTest.TestCase`方法初始化和还原数据库数据。 # 什么是%UnitTest? `%UnitTest`包是一组为IRIS提供测试框架的类。在结构上,它类似于`xUnit`测试框架。`%UnitTest`为创建和执行以下各项的单元测试提供类和工具: - 类和方法 - ObjectScript例程(routines) - InterSystems SQL脚本 - Productions # 创建和执行单元测试套件 以下是创建和执行一套单元测试的基本步骤: 1. 创建一个(或多个)包含要测试的方法的类。 2. 创建扩展`%UnitTest.TestCase`的测试类(或多个测试类)。 3. 将方法添加到将测试方法输出的测试类。在每个方法中至少使用一个断言(`AssertX`宏)。每个测试方法名称都以`Test`开头。 4. 将测试类导出到文件。 5. 打开终端并切换到包含要测试的类的名称空间。为`^UnitTestRoot`分配一个字符串,该字符串包含包含导出的测试类文件的目录的父目录的路径。 6. 在终端中,运行`%UnitTest.Manager.RunTest`,向其传递包含测试类文件的(子)目录的名称。 7. 查看测试报告。终端中的输出包括网页的URL,该网页以易于阅读的表格形式显示结果。 # %UnitTest类 此表描述了用于为InterSystems IRIS类和方法创建和执行单元测试的主要`%UnitTest`类。 - `TestCase` 扩展此类以创建包含测试方法的类。如果一个或多个`AssertX`方法返回`False`,则测试失败;否则测试通过。将使用关联的宏调用`AssertX`方法。这些方法和宏是: - `AssertEqualsViaMacro`-如果表达式相等,则返回`TRUE`。使用`$$$AssertEquals`宏调用。 - `AssertNotEqualsViaMacro`-如果表达式不相等,则返回`TRUE`。使用`$$$AssertNotEquals`宏调用。 - `AssertStatusOKViaMacro`-如果返回的状态代码为1,则返回`TRUE`。使用`$$$AssertStatusOK`宏调用。 - `AssertStatusNotOKViaMacro`-如果返回的状态码为0,则返回`TRUE`。使用`$$$AssertStatusNotOK`宏调用。 - `AssertTrueViaMacro`-如果表达式为TRUE,则返回TRUE。使用`$$$AssertTrue`宏调用。 - `AssertNotTrueViaMacro`-如果表达式不为TRUE,则返回`TRUE`。使用`$$$AssertNotTrue`宏调用。 - `AssertFilesSameViaMacro`-如果两个文件相同,则返回`TRUE`。使用`$$$AssertFilesSame`宏调用。 - `LogMessage`-将日志消息写入`^UnitTestLog`全局。使用`$$$LogMessage`宏调用。 - 设置和拆除条件的方法包括: - `OnBeforeOneTest`-紧接在测试类中的每个测试方法之前执行。 - `OnBeforeAllTests`-在测试类中的任何测试方法之前执行一次。 - `OnAfterOneTest`-在测试类中的每个测试方法之后立即执行。 - `OnAfterAllTests`-在测试类中的所有测试方法执行完毕后执行一次。 - `Manager` 使用此类启动测试。其方法包括: - `RunTest` -在目录中执行一个测试或一组测试。 - `DebugRunTestCase`-执行一个测试或一组测试,而不加载或删除任何测试类。 - `Report` 定义报告执行一个测试或一组测试的结果的网页。 # 断言方法和宏 单元测试的主要测试操作来自`AssertX`方法及其关联宏。将直接调用宏来测试方法的输出。宏测试方法是否为给定的输入创建所需的输出。只要`AssertX`宏返回`FALSE`(或以错误结束),包含它的测试就会失败。 在创建代码时,请计划将创建的单元测试以测试代码。在这里的示例中,已经创建了一个名为`TestMe`的类,其中包含一个名为`Add`的方法。现在想测试一下新的`TestMe`类,看看它是否工作。 以下命令运行`AssertEquals`宏以测试`Add`方法的输入`(2,2)`是否等于`4`。 ```java Do $$$AssertEquals(##class(MyPackage.TestMe).Add(2,2),4, "Test Add(2,2)=4") ``` `AssertEquals`宏比较两个值并接受三个参数: 1. `##class(MyPackage.TestMe).Add(2,2)`-第一个值是以`2,2`作为输入进行测试的方法。 2. `4`-第二个值。 3. `"Test Add(2,2)=4"`-写在结果页上的文本说明。(此参数不影响测试。如果不包含测试描述,该类将使用求值的表达式创建一个测试描述。) 以下是用于测试对象是否正确保存的`AssertStatusOK`宏的示例。 ```java Do $$$AssertStatusOK(contact.%Save(),"Saving a Contact") ``` 此`AssertStatusOk`宏计算方法返回的状态。如果为1,则测试通过。 1. `Contact.%Save`-返回状态代码的表达式。 2. `"Saving a Contact"` -文本说明。这是测试报告的文档。这不会影响测试。 # 创建要在示例中使用的类 要完成以下动手示例,请使用`Atelier`创建以下类:`MyPackage.TestMe`和`MyPackage.Contact`。 - `MyPackage.TestMe` ```java Class MyPackage.TestMe Extends %RegisteredObject { ClassMethod Add(arg1 As %Integer, arg2 As %Integer) As %Integer { Return arg1 + arg2 } ClassMethod CreateContact(name As %String, type As %String) As MyPackage.Contact { Set contact = ##class(MyPackage.Contact).%New() Set contact.Name=name Set contact.ContactType=type Return contact } ClassMethod GetContactsByType(type As %String) As %ListOfObjects { Set list=##class(%Library.ResultSet).%New() } } ``` - `MyPackage.Contact` ```java Class MyPackage.Contact Extends (%Persistent, %Populate, %XML.Adaptor) { /// 描述联系的性质:: Personal or Business Property ContactType As %String(TRUNCATE = 1, VALUELIST = ",Business,Personal"); /// 表示联系人的姓名 Property Name As %String(POPSPEC = "Name()", TRUNCATE = 1) [ Required ]; Query ByContactType(type As %String) As %SQLQuery(CONTAINID = 1) { SELECT %ID FROM Contact WHERE (ContactType = :type) ORDER BY Name } Storage Default { %%CLASSNAME ContactType Name ^MyPackage.ContactD ContactDefaultData ^MyPackage.ContactD ^MyPackage.ContactI ^MyPackage.ContactS %Storage.Persistent } } ``` # 示例:创建并导出测试类 类`MyPackage.TestMe`包含一个名为`Add`的方法,该方法将两个整数相加。在此示例中,将创建并运行单元测试以检查`Add`方法是否正确地将两个整数相加。 创建将包含单元测试的测试类。以下是方法: 1. 使用Atelier在`MyPackage`包中创建名为`Tests`的新类。测试必须扩展`%UnitTest.TestCase`。 2. 添加以下名为`TestAdd`并编译测试的方法: ```java Class MyPackage.Tests Extends %UnitTest.TestCase { Method TestAdd() { do $$$AssertEquals(##class(MyPackage.TestMe).Add(2,2),4, "Test Add(2,2)=4") do $$$AssertNotEquals(##class(MyPackage.TestMe).Add(2,2),5,"Test Add(2,2)'=5") } } ``` 3. 将类测试导出到单元测试目录中的XML文件。如果尚未创建测试目录,请创建一个。此示例使用 `C:\unittests\mytests\。 a. 在Atelier中,单击文件>导出。 b. 在“Atelier ”下,单击“旧版XML文件”。单击下一步 c. 选择项目`Test.cls`和`c:\unittests\mytests\` 目录。 d. 单击Finish(完成)。 e. Atelier将测试类导出到`C:\unittests\mytests\cls\MyPackage`。 注意,目录名(在本例中为`mytest`)是一套测试的名称,也是`^UnitTestRoot`指定的目录的子级。运行`Manager.RunTest(“mytest”)`运行存储在`mytest`目录中的所有测试。 注意:还可以将测试类导出为`.cls`文件,而不是`XML`文件。也可以简单地从Atelier工作区复制它们,而不是导出它们。 # [源码](https://download.csdn.net/download/yaoxin521123/18703118)
文章
Qiao Peng · 一月 30, 2022

多语言字符集系列文章-- 第二篇 各种技术对字符集使用的声明

各种技术在交换数据的时候,就需要知道对方给的数据使用什么字符集和字符编码,否则很可能就解码错了。这里列举了医疗行业常见的数据交换技术方式和它们对字符集使用的声明方式。 2.1 文件 文件是字符型数据最常见的交换方式,文本编辑工具通常在保存时都会让用户选择保存成什么字符编码。对于不同的字符编码,文件是如何保存的呢? 通常会在文件头使用字节顺序标志(BOM,Byte Order Mark)来标记文件的编码。下表是常见的编码格式对应的BOM,注意ANSI并不需要BOM,我把它列在这里的目的是希望一目了然。 字节 编码格式 00 00 FE FF UTF-32, big-endian FF FE 00 00 UTF-32, little-endian FE FF UTF-16, big-endian FF FE UTF-16, little-endian EF BB BF UTF-8 空 ANSI 例如,汉字的“中”的各种编码如下: 用Windows的写字板软件将这个汉字分别保存成UTF-8、ANSI、Unicode编码,在UltraEdit打开其16进制模式查看,就可以看到如下的输出: 这里ANSI其实保存的是GBK码(使用GBK代码页936),Unicode其实保存的是UTF16编码。试图以ANSI保存文件时,对超出了GBK编码范围的汉字,Windows写字板软件会提示包含Unicode文字,以ANSI保存会丢失数据,如下图: 对于ANSI保存的包含汉字的文件,代码页信息并不在文件里。并不是所有的文本编辑器和文字处理代码都能正确解析这样的文件,因为它们并不知道代码页。这是可能造成文件中文乱码的一个原因。 另外,前面提到通过BOM可以确定文件编码方式,但并不是所有的文件都使用了BOM。因此特定的文本编辑器和文字处理代码对中文都可能产生显示乱码。 2.2 HTTP 对于HTTP消息内容,包括SOAP、RESTful,浏览器/客户端和服务器怎么知道字符编码呢? HTTP头的Content-Type可以通过参数charset指定文字编码。例如: Content-Type: text/html; charset = UTF-8 如果没有正确配置charset,就可能产生乱码。例如网页表单提交的中文数据,服务器没有正确解码从而产生乱码。 不同的Web服务器都可以设置默认的charset,例如Apache可以修改httpd.conf文件,通过配置AddDefaultCharset指定默认字符集编码。 2.3 XML XML文件当然可以使用前面提到的文件编码方式设置,以HTTP传递的XML数据也可以使用HTTP的文字编码设置。同时,XML规范自己也定义了文字编码声明的方式,从而保证通过任何方式传递(例如TCP)的XML都可以被正确解析。 XML定义的编码方式是设置encoding属性,例如: <?xml version="1.0" encoding="UTF-8"?> 注意,JSON并没有声明文字编码的设置。通常JSON数据都是通过HTTP传递的,因此使用HTTP的文字编码设置。 2.4 数据库连接 数据库连接也是造成文字乱码的一个重灾区。客户端从数据库服务器获取数据、向数据库服务器提交数据,怎么知道数据的文字编码呢? 和数据库相关的文字编码有2部分:数据库内码、数据库连接使用的字符编码。 数据库内码: 由于Unicode码历史并不悠久,数据库管理系统的历史远早于Unicode,直到最近这20年,数据库厂商才开始支持Unicode内码。当然,每个厂商的Unicode内码编码也不一样,例如InterSystems的Caché和IRIS是UTF-16格式,而Oracle是UTF-8和UTF-16。因为UTF-8处理效率低,大多数Unicode数据库都使用UTF-16。现在依然能看到不是Unicode内码的老版本数据库。 很多数据库相关的字符编码问题和数据库内码设置有关。例如国内不少的Oracle安装时没设置过字符编码,数据库内码NLS_CHARACTERSET默认为US7ASCII,客户端字符集NLS_LANG默认为AMERICAN_AMERICA.US7ASCII。这并不会造成中文无法保存,因为数据库只是将客户端的数据逐位保存下来,无论什么编码。但这样保存的中文数据只有Oracle自己的客户端能正常显示,其它的客户端可能就按ASCII处理从而造成乱码。因此正确设置数据库的内码很关键。 数据库连接使用的字符编码: ODBC: 直到ODBC 3.5标准(1997年)之前绝大多数的ODBC连接的函数调用和字符串编码都是ANSI(单字节或双字节),所以中文生僻字大多都会处理异常。 ODBC 3.5 规定ODBC驱动管理器要能够透明地处理Unicode和ANSI之间的转换,从而让ANSI和Unicode的数据库客户端都可以正确地向数据库获取和提交数据。 当然,并不是所有的Unicode字符都能转为ANSI码,例如中文“”字,所以ANSI的数据库客户端依然会遇到生僻字乱码。 JDBC: Java对字符串使用UTF-16编码,如果数据库内码也使用UTF-16,那么不会有乱码问题,例如InterSystems IRIS。但很多数据库并非使用UTF-16,这些数据库的JDBC驱动就需要支持UTF-16和数据库内码之间的转换。通常可以通过设置JDBC的连接字符串的特定属性来实现。 例如mysql的连接字符串中指定characterEncoding: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8 其它连接方式: 另外,很多数据库还提供XDBC之外的连接方式,这些连接方式有自己的字符编码逻辑。 2.5 HL7 V2 在医疗行业,HL7 V2是目前最为广泛使用的消息交换标准。它可以通过文件、TCP、HTTP、SOAP 等多种通道交换。因此,HL7 V2消息标准中,也有设置消息字符编码的字段:MSH-18。正确设置该字段有助于不同的系统正确地处理HL7 V2消息里的字符数据。 例如下面的HL7消息在MSH段设置其字符编码为UTF-8: MSH|^~\&||PHLS|||||ORU^R01|||2.5|||AL|||UTF-8| PID||S2345|S2345^^^PHLS^MR|C9876^^^COR^XX~S45008787^^^MA^DL|张^三||19301019|M|||1 Memorial Drive^^剑桥市^MA^02142||||||||063070516 PV1||O|||||ISCGP001^建国^李|||||||EO|||||HSVN00008|||||||||||||||||||||||||20200912090700|20200912090700 ORC||00265-001|0606:H00550R||||^^^202009120910||202009120910|||ISCGP001^建国^李|PHLS||||||||PHLS||||||||LAB OBR||00265-001|0606:H00550R|CBCD^血常规^L|||202009121049|||||||202009120937|Blood|ISCGP001^Moore^James||||||202009121227|||F OBX||NM|WBC^WHITE BLOOD CELL COUNT||6.24|10(9)/L|4.0-10.6||||F|||202009121049 OBX||NM|RBC^RED BLOOD CELL COUNT||4.99|10x12/L|4.5-5.9||||F|||202009121049 OBX||NM|HGB^HEMOGLOBIN||13.6|g/dL|12.0-16.0||||F|||202009121049 OBX||NM|HCT^HEMATOCRIT||41.6|Percent|36.0-46.0||||F|||202009121049 2.6 其它 在上面提到的和字符集相关的乱码之外,有时我们会混淆一些其它的、并非真正乱码的情况。下面这些中文显示的“乱码”,并非乱码: URL编码: 根据RFC 3986,如果URL的路径中有URL的保留字,就需要对URL路径中的保留字使用转义符% 进行转码,也叫做百分号编码。例如ASCII中下面的字符都需要转义: ! # $ & ' ( ) * + , / : ; = ? @ [ ] %21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D 对于非ASCII码字符,按其UTF-8编码字节顺序加%转义。例如“https://cn.community.intersystems.com/post/多语言字符集系列文章-第一篇-多语言字符集和相关标准简史”会被转义为: https://cn.community.intersystems.com/post/%E5%A4%9A%E8%AF%AD%E8%A8%80%E5%AD%97%E7%AC%A6%E9%9B%86%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0-%E7%AC%AC%E4%B8%80%E7%AF%87-%E5%A4%9A%E8%AF%AD%E8%A8%80%E5%AD%97%E7%AC%A6%E9%9B%86%E5%92%8C%E7%9B%B8%E5%85%B3%E6%A0%87%E5%87%86%E7%AE%80%E5%8F%B2 其中%E5%A4%9A就是“多”字的UTF-8编码E5A49A的3个字节加上%。 在InterSystems技术平台上,可以用$ZCVT("%E5%A4%9A","I","URI")将URL编码翻译为中文字符,或使用$ZCVT("多","O","URI")将汉字进行URL编码。 HTML实体编码(转义): HTML实体编码(HTML Entity Encode)将字符编码为&开头;结尾的字符串,例如"<"编码为"<"。而中文字符就会被按其unicode编码进行转义,例如“广”字的unicode 10进制码为24191,所以它的HTML实体编码是"广"。在InterSystems技术平台上,可以用$ZCVT("广","I","HTML")将其翻译为中文字符。 Base64 编码: 当使用SOAP传递的数据是一个XML字符串或其它应被视为二进制类型的数据时,通常使用Base64对其进行编码,从而不会破坏XML结构解析。所以它也不是乱码。
文章
Jingwei Wang · 十二月 23, 2021

在大学使用IRIS(一项有趣的任务)

正如你在我的简介中看到的,我在一所大学教书,我想分享我对IRIS(或之前的Caché)教学的看法。 已经有一段时间了,但我还记得在今年早些时候看到YURI MARX GOMES关于 "用InterSystems对象和SQL开发 "一系列课程。他对第1天、第2天和第3天的课程内容进行了简要的描述,并附有讲师Joel Solon的评论。我心想,分享我自己的经验可能会有趣。 在我真正写下我教给学生的东西之前,先简单介绍一下我的经历。 在我毕业拿到硕士学位回到大学工作后,我们系决定更新我们的课程,在普通课程中增加几门新课程。其中一门是 "后关系型数据库"。它是为研究生第一年的学生讲授的。开始时,它包括72小时的讲座和72小时的实践。现在变为秋季学期72小时的讲座和36小时的实践。 由于我是一名新员工,而且是一名年轻有为的员工,我被赋予了讲授这门新学科的职责。我感到惊讶和惊恐是不言而喻的。首先,我根本没有任何教学方面的实践经验。其次,我只有夏天的三个月时间来学习一项对我来说完全陌生的技术并准备课程的讲授。幸运的是,我已经知道了应该教授哪个数据库。这个数据库就是InterSystems Caché。 总之,我或多或少地准备好了,然后我亲爱的学生们的问题开始了。例如,为什么他们必须学习这个数据库,他们在哪里以及如何使用这个数据库,等等。由于当时是2010年,我还很年轻,没有经验,而且这方面的书也不多,我决定直接去找源头,即InterSystems。不知怎的,我最终与Solon先生交谈,他给了我一些很好的提示,还把我介绍给了Evgeny Shvarov。从那时起,一切都变得更加容易和清晰了。 在接下来的几年里,我根据现代的趋势和要求,对我的课程内容做了相当多的改变。在开始的时候,为了展示如何从其他应用程序中利用这个数据库,我使用了Java绑定和.NET管理提供者。后来出现了eXTreme for Java,然后是eXTreme for .NET。ZEN在一两年后被教授和遗忘。现在是RESTful和SOAP服务以及CSP。很多东西都变了,但核心部分几乎是一样的。 既然我们在这里讨论的是讲授硕士水平的课程,我有很多的期望。首先,我希望我的学生知道面向对象的范式,并且能够绘制正确的UML类图。其次,我希望他们了解关系型数据库(包括索引、键等)和SQL。最后,他们应该至少有一个简单的网络开发的知识。 考虑到所有的先决条件,以下是我的教学大纲的大纲: 第1节 后关系型数据库概述:面向对象,对象关系型,以及不同种类的NoSQL数据库,并有模型和例子的描述。多模型数据库和例子。 第2节 架构、结构和管理IRIS的基础知识,作为后关系型DBMS的示例,IRIS管理的基础知识 : 数据库和命名空间一起工作。语法、命令、变量、表达式,以及ObjectScript的一些功能。还有用户代码、异常和事务。 第3节 分层模型 : 列表和多维数组(以及globals)。处理列表和数组的函数。这里是第一个任务--设计一个有4个下标层次的global,并使用至少4个函数与数组一起工作,对数据进行处理。 第4节 对象模型: 类,分层,继承,参数,不同类型的属性(以及如何使用它们),不同类型的方法(使用计算属性的例子,单元测试,使用Populate创建测试数据,用户数据类型),参数(以及如何使用它们)。数据是如何存储的,以及如何设置存储。这里的任务包括设计一个具有不同类型属性的类图(引用、内置对象、关系、集合和流);创建这些类和每个类的几个对象,并在IDE中把所有东西连接起来;创建一个计算属性、一个用户数据类型、单元测试以检查所有约束条件是否工作(如最小或最大长度/值、必需属性、唯一属性等)和生成测试数据。 第5节 关系模型:对象模型和关系模型之间的相关性。嵌入式(简单语句和游标)和动态SQL。类查询(基于SQL和COS)。隐式连接。IDKEY与其附带的方法。这里的任务是将不同类型的SQL查询通过join和参数添加到之前的任务中的类中,创建一个触发器,将唯一的属性改为IDKEY,看看它是如何改变相关的globals的。 第6节。从IRIS外部访问数据 :CSP和关于它的一切。RESTful服务和客户端(处理JSON的类)。SOAP服务和客户端(处理XML的类)。这里的任务是从之前的任务中选择一个类,并创建一个CSP页面,列出所选类的所有对象,并给出编辑、创建和删除对象的功能。然后使用RESTful和SOAP服务做同样的事情(创建方法来返回所有对象、一个特定的对象、更新和删除一个特定的对象)。 课程安排就这样了。然而,现在有了嵌入式Python,我可能会改用它,因为所有关于ObjectScript语法的问题和评论都很扰乱我的神经。 当然,COVID-19和学校的封锁给整个工作方式带来了一些变化。我认为,这些变化是最好的。而且,这真的取决于事情的组织方式。例如,其中一个好处是,现在我可以向学生展示知道我在整个学期所讲的所有内容的有用性。 一般来说,在学期末,他们有一份作业,来检查他们对课程主要部分的知识的掌握。以前,他们在课堂上写作业,我可以监督他们。之后,由于大流行病的发生非常意外,我不得不随机应变,为每个学生提供一套独特的任务。不幸的是,在那个时候,没有现成的软件,所以我决定写一个简单的网络门户来处理这个作业。由于现在已经过去了将近两年,我的简单门户变成了一个RESTful服务,从不同的集合中随机选择问题,将它们分配给学生,并接收答案。所有这些都非常现代化,而且易于使用(和编写) . 现在轮到了我现在的学生来写这个作业(使这个社区里有一些乐趣),你可以参加测试并发送你的答案, 当然,问题将与我的学生的问题不同(而且更容易)。我将使用InterSystems学习实验室的服务器,所以你只有几天的时间来做这件事并检查你的知识。 我在问题中使用的类图如下: 要获得这些问题,只需向以下地址发送一个带有你姓名的GET请求: http://52773-1-e5a0b608.labs.learning.intersystems.com/community/task/%3CYour_name> 你就会得到你的4套问题: 要发送答案,只需向以下地址发送一个POST请求: http://52773-1-e5a0b608.labs.learning.intersystems.com/community/answer/<Your_name> 在body中填写一个JSON: { "Answer1": "answer 1", "Answer2": "answer 2", "Answer3": "answer 3", "Answer4": "answer 4" } 我会在某个时候检查你的答案(因为有这个讨厌的东西叫做时差),你可以通过向以下地址发送GET请求来获得你的结果: http://52773-1-e5a0b608.labs.learning.intersystems.com/controlwork/marks/<Your_name> 如果分数是空的,说明我还没有时间去检查(或者我还没有起床)。分数如下。 0 分意味着答案是完全错误的。 1 分意味着答案或多或少是正确的。 2 分意味着答案是正确的。 第一个发送答案的人将得到我的一个虚拟拥抱(或者一个真正的拥抱,如果我们见面的话) 总之,如果你对我的课程有任何意见或问题,不要犹豫,请在评论区写出来。 查看原帖 由 @Irene.Mikhaylova 撰写 第1天、第2天和第3天的课程,这三篇文章都有中文的,请换成中文社区链接,谢谢!
文章
姚 鑫 · 七月 5, 2021

第二十八章 定制SAX解析器创建自定义内容处理程序

[toc] # 第二十八章 定制SAX解析器创建自定义内容处理程序 # 创建自定义内容处理程序 如果直接调用InterSystems IRIS SAX解析器,则可以根据自己的需要创建自定义内容处理程序。本节讨论以下主题: - Overview - 要在内容处理程序中自定义的方法的描述 - `%XML.SAX.Parser`类中解析方法的参数列表摘要 - 示例 ## 创建自定义内容处理程序概述 要定制InterSystems IRIS SAX解析器导入和处理XML的方式,请创建并使用定制的SAX内容处理程序。具体地说,创建`%XML.SAX.ContentHandler`的子类。然后,在新类中,重写任何默认方法以执行所需的操作。在解析XML文档时使用新的内容处理程序作为参数;为此,需要使用`%XML.SAX.Parser`类的解析方法。 此操作如下图所示: ![image](/sites/default/files/inline/images/tu_pian__6.png) 创建和使用自定义导入机制的过程如下: 1. 创建扩展`%XML.SAX.ContentHandler`的类。 2. 在该类中,包括希望覆盖的方法,并根据需要提供新定义。 3. 在使用`%XML.SAX.Parser`的分析方法之一(即`ParseFile()`、`ParseStream()`、`ParseString()`或`ParseURL()`)编写读取XML文档的类方法。 调用分析方法时,请将自定义内容处理程序指定为参数。 ## SAX内容处理程序的可定制方法 `%XML.SAX.ContentHandler`类在特定时间自动执行某些方法。通过覆盖它们,您可以自定义内容处理程序的行为。 ### 响应事件 `%XML.SAX.ContentHandle`类分析XML文件,并在它到达XML文件中的特定点时生成事件。根据事件的不同,会执行不同的方法。这些方法如下: - `OnPostParse()` — 在XML解析完成时触发。 - `characters()` — 由字符数据触发。 - `comment()` — 注释触发 - `endCData()` —由CDATA部分的末尾触发。 - `endDocument()` —由文档结尾触发。 - `endDTD()` — 由DTD结束触发。 - `endElement()` —由元素的末尾触发。 - `endEntity()` — 由一个实体的终结触发。 - `endPrefixMapping()` — 由名称空间前缀映射的结束触发。 - `ignorableWhitespace()` — 由元素内容中的可忽略空格触发。 - `processingInstruction()` — 由XML处理指令触发。 - `skippedEntity()` — 被跳过的实体触发。 - `startCData()` —由CDATA部分的开头触发。 - `startDocument()` — 由文档的开头触发。 - `startDTD()` — 由DTD的开头触发。 - `startElement()` — 由元素的开始触发。 - `startEntity()` — 由一个实体的开始触发。 - `startPrefixMapping()` — 由名称空间前缀映射的开始触发。 默认情况下,这些方法是空的,可以在自定义内容处理程序中覆盖它们。 ### 处理错误 `%XML.SAX.ContentHandler`类在遇到某些错误时也会执行方法: - `error()` — 由可恢复的解析器错误触发。 - `fatalError()` — 由致命的XML解析错误触发。 - `warning()` — 由解析器警告通知触发。 默认情况下,这些方法为空,可以在自定义内容处理程序中重写它们。 ### 计算事件掩码 当调用InterSystems IRIS SAX解析器(通过`%XML.SAX.Parser`类)时,可以指定一个掩码参数来指示哪些回调是感兴趣的。如果未指定掩码参数,解析器将调用内容处理程序的`Mask()`方法。此方法返回一个整数,该整数指定与内容处理程序的重写方法相对应的复合掩码。 例如,假设创建了一个自定义内容处理程序,其中包含`startElement()`和`endElement()`方法的新版本。在本例中,`Mask()`方法返回一个数值,该数值等于`$$$SAXSTARTELEMENT`和`$$$SAXENDELEMENT`,之和,这两个标志对应于这两个事件。如果没有为解析方法指定掩码参数,则解析器将调用内容处理程序的`Mask()`方法,因此只处理这两个事件。 ### 其他有用的方法 `%XML.SAX.ContentHandler`类提供在特殊情况下有用的其他方法: - `LocatePosition()`-通过引用返回两个参数,这两个参数指示解析的文档中的当前位置。第一个表示行号,第二个表示行偏移。 - `PushHandler()`-在堆栈上推送新的内容处理程序。SAX的所有后续回调都将转到这个新的内容处理程序,直到该处理程序完成处理。 如果在解析一种类型的文档时遇到想要以不同方式解析的一段XML,则可以使用此方法。在本例中,当检测到要以不同方式处理的段时,调用`PushHandler()`方法,该方法将创建一个新的内容处理程序实例。所有回调都会转到此内容处理程序,直到调用`PopHandler()`返回上一个内容处理程序。 - `PopHandler()`-返回堆栈上的上一个内容处理程序。 这些是`final`方法,不能重写。 ## SAX解析方法的参数列表 要指定文档源,请使用`%XML.SAX.Parser`类的`ParseFile()`、`ParseStream()`、`ParseString()`或`ParseURL()`方法。在任何情况下,源文档都必须是格式良好的XML文档;也就是说,它必须遵守XML语法的基本规则。完整的参数列表按顺序如下: 1. pFilename, pStream, pString, or pURL — 文档源. 2. pHandler — 内容处理程序,它是`%XML.SAX.ContentHandler`类的实例。 3. pResolver — 分析源时使用的实体解析器。 4. pFlags — 用于控制SAX解析器执行的验证和处理的标志。 5. pMask — 用于指定XML源中感兴趣的项的掩码。通常不需要指定此参数,因为对于`%XML.SAX.Parser`的解析方法,默认掩码为`0`。这意味着解析器调用内容处理程序的`Mask()`方法。该方法通过检测(在编译期间)在事件处理程序中自定义的所有事件回调来计算掩码。只处理那些事件回调。 6. pSchemaSpec — 验证文档源所依据的架构规范。此参数是一个字符串,其中包含以逗号分隔的命名空间/URL对列表: ``` "namespace URL,namespace URL" ``` 这里,`Namespace`是用于模式的XML名称空间,`URL`是提供模式文档位置的`URL`。名称空间和`URL`值之间有一个空格字符。 7. pHttpRequest (For the ParseURL() method only) — 这里,`Namespace`是用于模式的XML名称空间,URL是提供模式文档位置的URL。名称空间和URL值之间有一个空格字符。 8. pSSLConfiguration — 客户端`SSL/TLS`配置的配置名称。 注意:请注意,此参数列表与`%XML.TextReader`类的解析方法略有不同。有一点不同,`%XML.TextReader`不提供指定自定义内容处理程序的选项。 ## SAX处理程序示例 想要一个文件中出现的所有XML元素的列表。要做到这一点,只需记录每个开始元素。那么这个过程是这样的: 1. 创建一个名为`MyApp.Handler`的类,它扩展`%XML.SAX.ContentHandler`: ``` Class MyApp.Handler Extends %XML.SAX.ContentHandler { } ``` 2. 使用以下内容覆盖`startElement()`方法: ``` Class MyApp.MyHandler extends %XML.SAX.ContentHandler { // ... Method startElement(uri as %String, localname as %String, qname as %String, attrs as %List) { //we have found an element write !,"Element: ",localname } } ``` 3. 将一个类方法添加到读取和分析外部文件的Handler类: ``` Class MyApp.MyHandler extends %XML.SAX.ContentHandler { // ... ClassMethod ReadFile(file as %String) as %Status { //create an instance of this class set handler=..%New() //parse the given file using this instance set status=##class(%XML.SAX.Parser).ParseFile(file,handler) //quit with status quit status } } ``` 请注意,这是一个类方法,因为它在应用程序中被调用以执行其处理。此方法执行以下操作: 1. 它创建内容处理程序对象的实例: ``` set handler=..%New() ``` 2. 它在一个调用`%XML.SAX.Parser`的`ParseFile()`方法。这将验证并解析文档(由`fileName`指定),并调用内容处理程序对象的各种事件处理方法: ``` set status=##class(%XML.SAX.Parser).ParseFile(file,handler) ``` 每次在解析器解析文档时发生事件(如开始或结束元素)时,解析器都会调用内容处理程序对象中的适当方法。在本例中,唯一被覆盖的方法是`startElement()`,它随后写出元素名称。对于其他事件,例如到达`End`元素,不会发生任何事情(默认行为)。 3. 当`ParseFile()`方法到达文件末尾时,它返回。处理程序对象超出作用域,并自动从内存中删除。 4. 在应用程序中的相应点,调用`ReadFile()`方法,将文件传递给解析: ``` Do ##class(Samples.MyHandler).ReadFile(filename) ``` 其中,filename是正在读取的文件的路径。 例如,如果文件的内容如下: ``` Edwards,Angela U. 1980-04-19 K8134 Vail 94059 Uberoth,Wilma I. Wells,George H. ``` 则此示例的输出如下所示: ``` Element: Root Element: Person Element: Name Element: DOB Element: GroupID Element: HomeAddress Element: City Element: Zip Element: Doctors Element: Doctor Element: Name Element: Doctor Element: Name ``` # 使用HTTPS `%XML.SAX.Parser`支持`HTTPS`。也就是说,可以使用此类执行以下操作: - (对于`ParseURL()`)解析`HTTPS`位置提供的XML文档。 - (对于所有解析方法)解析`HTTPS`位置的实体。 在所有情况下,如果这些项目中的任何一个是`在HTTPS`位置上提供的,请执行以下操作: 1. 使用管理门户创建包含所需连接详细信息的`SSL/TLS`配置。这是一次性的步骤。 2. 调用`%XML.SAX.Parser`的适用解析方法时,请指定`pSSLConfiguration`参数。 默认情况下,InterSystems IRIS使用`Xerces`图元解析。`%XML.SAX.Parser`仅在以下情况下使用其自己的实体解析: - `PSSLConfiguration`参数非空。 - 已配置代理服务器。