搜索​​​​

清除过滤器
文章
姚 鑫 · 五月 24, 2021

第五章 向邮件添加附件

# 第五章 向邮件添加附件 # 向邮件添加附件 可以将附件添加到电子邮件或消息部分(具体地说,是添加到`%Net.MailMessagePart`或`%Net.MailMessage`的实例)。要执行此操作,请使用以下方法: 这些方法中的每一种都会将附件添加到原始邮件(或邮件部分)的`Parts`数组中,并自动将`IsMultiPart`属性设置为1。 ### AttachFile() ```java method AttachFile(Dir As %String, File As %String, isBinary As %Boolean = 1, charset As %String = "", ByRef count As %Integer) as %Status ``` 将给定文件附加到电子邮件。默认情况下,文件以二进制附件的形式发送,但您可以将其指定为文本。如果文件是文本,还可以指定该文件使用的字符集。 具体地说,此方法创建`%Net.MailMessagePart`的实例,并根据需要将文件内容放在`BinaryData`或`TextData`属性中,并根据需要设置`CharSet`属性和`TextData.TranslateTable`属性。该方法通过引用返回一个整数,该整数指示此新消息部分在部件数组中的位置。 此方法还设置消息或消息部分的`Dir`和`FileName`属性。 ### AttachStream() ```java method AttachStream(stream As %Stream.Object, Filename As %String, isBinary As %Boolean = 1, charset As %String = "", ByRef count As %Integer) as %Status ``` 将给定流附加到电子邮件。如果指定了`Filename`,则附件被视为文件附件。否则,它将被视为内联附件。 ### AttachNewMessage() ```java method AttachNewMessage() as %Net.MailMessagePart ``` 创建`%Net.MailMessage`的新实例,将其添加到消息中,并返回新修改的父消息或消息部分。 ``` AttachEmail() ``` 给定一封电子邮件(`%Net.MailMessage`的实例),此方法会将其添加到邮件中。此方法还设置消息或消息部分的`Dir`和`FileName`属性。 注意:此方法将`contentType`设置为`"message/rfc822"`。在这种情况下,不能添加任何其他附件。 示例:`MessageWithAttach()` 以下示例生成一封带有一个硬编码附件的简单电子邮件。它不为邮件提供任何地址;可以在实际发送邮件时提供该信息 ```java /// w ##class(PHA.TEST.HTTP).MessageWithAttachment() ClassMethod MessageWithAttachment() As %Net.MailMessage { Set msg = ##class(%Net.MailMessage).%New() Set msg.Subject="Message with attachment "_$h Set msg.IsBinary=0 Set msg.IsHTML=0 Do msg.TextData.Write("This is the main message body.") //add an attachment Set status=msg.AttachFile("E:\", "HttpDemo.pdf") If $$$ISERR(status) { Do $System.Status.DisplayError(status) Quit $$$NULLOREF } b Quit msg } ``` # 使用SMTP服务器发送电子邮件 如果有权访问SMTP服务器,则可以发送电子邮件。SMTP服务器必须正在运行,并且必须具有使用它所需的权限。要发送电子邮件,请执行以下操作: 1. 创建`%Net.SMTP`实例并根据需要设置其属性,特别是以下属性: - `Smtpserver`是正在使用的`SMTP`服务器的名称。 - 端口是在`SMTP`服务器上使用的端口;默认值为25。 - 时区指定RFC 822指定的服务器时区,例如 `"EST"` 或 `"-0400"` 或 `"LOCAL"`。如果未设置,消息将使用世界时。 此对象描述将使用的`SMTP`服务器。 2. 如果`SMTP`服务器需要身份验证,请指定必要的凭据。为此: a. 创建`%Net.Authenticator`的实例。 b. 设置此对象的用户名和密码属性。 c. 将`%Net.SMTP`实例的验证器属性设置为等于此对象。 d. 如果邮件本身具有授权发件人,请设置`%Net.SMTP`实例的`AuthFrom`属性。 3. 要使用到`SMTP`服务器的`SSL/TLS`连接,请执行以下操作: a. 将`SSLConfiguration`属性设置为要使用的已激活`SSL/TLS`配置的名称。 `SSL/TLS`配置包括一个名为`Configuration Name`的选项,该选项是在此设置中使用的字符串。 b. 将`UseSTARTTLS`属性设置为0或1。 在大多数情况下,使用值0。如果服务器交互在普通`TCP`套接字上开始,然后在与普通套接字相同的端口上切换到`TLS`,则使用值1。 或者,将`SSLCheckServerIdentity`属性设置为1。如果要验证证书中的主机服务器名称,请执行此操作。 4. 创建要发送的电子邮件(如“创建单部分电子邮件”和“创建多部分电子邮件”中所述)。 5. 调用`SMTP`实例的`send()`方法。此方法返回一个状态,应该检查该状态。 6. 如果返回的状态指示错误,请检查`Error`属性,该属性包含错误消息本身。 7. 检查`FailedSend`属性,该属性包含发送操作失败的电子邮件地址列表。 以下各节中的示例使用了两种不同的免费SMTP服务,这些服务在编写本手册时是可用的。选择这些服务并不意味着特别认可。还要注意的是,这些示例并没有显示实际的密码。 `Samples`命名空间中还有其他示例。要查找它们,请在该命名空间中搜索`%Net.SMTP`。 重要提示:`%Net.SMTP`将邮件正文写入临时文件流。默认情况下,该文件被写入命名空间目录,如果该目录需要特殊的写入权限,则不会创建该文件,并且您会得到一个空的消息正文。 可以为这些临时文件定义新路径,并选择不限制写访问的路径(例如,`/tmp`)。为此,请设置全局节点`%SYS("StreamLocation",namespace)`,其中`NAMESPACE`是运行代码的名称空间。例如: ``` Set ^%SYS("StreamLocation","SAMPLES")="/tmp" ``` 如果`%SYS("StreamLocation",namespace)`为`NULL`,则InterSystems IRIS使用`%SYS("TempDir",namespace)`指定的目录。如果未设置`%SYS("TempDir",namespace)`,则IRIS使用 `%SYS("TempDir")`指定的目录 示例1:`HotPOPAsSMTP()`和`SendSimpleMessage()` 此示例由一起使用的两个方法组成。第一个创建`%Net.SMTP`的实例,该实例使用已在`HotPOP SMTP`服务器上设置的测试帐户: ```java /// w ##class(PHA.TEST.HTTP).HotPOPAsSMTP() ClassMethod HotPOPAsSMTP() As %Net.SMTP { Set server=##class(%Net.SMTP).%New() Set server.smtpserver="smtp.hotpop.com" //HotPOP SMTP服务器使用默认端口(25) Set server.port=25 //创建对象以进行身份验证 Set auth=##class(%Net.Authenticator).%New() Set auth.UserName="isctest@hotpop.com" Set auth.Password="123pass" Set server.authenticator=auth Set server.AuthFrom=auth.UserName b Quit server } ``` 下一个方法使用提供的SMTP服务器作为参数发送一条简单、唯一的消息: ```java ClassMethod SendSimpleMessage(server As %Net.SMTP) As %List { Set msg = ##class(%Net.MailMessage).%New() Set From=server.authenticator.UserName Set:From="" From="xxx@xxx.com" Set msg.From = From Do msg.To.Insert("xxx@xxx.com") //Do msg.Cc.Insert("yyy@yyy.com") //Do msg.Bcc.Insert("zzz@zzz.com") Set msg.Subject="Unique subject line here "_$H Set msg.IsBinary=0 Set msg.IsHTML=0 Do msg.TextData.Write("This is the message.") Set status=server.Send(msg) If $$$ISERR(status) { Do $System.Status.DisplayError(status) Write server.Error Quit "" } Quit server.FailedSend } ``` 示例2:`YPOPsAsSMTP()` 此示例创建使用`YPOPS`的`%Net.SMTP`实例的实例,`YPOPS`是一种客户端软件,提供对`Yahoo`电子邮件帐户的`SMTP`和`POP3`访问。它使用已为此目的设置的测试帐户: ```java ClassMethod YPOPsAsSMTP() As %Net.SMTP { Set server=##class(%Net.SMTP).%New() //local host acts as the server Set server.smtpserver="127.0.0.1" //YPOPs uses default port, apparently Set server.port=25 //Create object to carry authentication Set auth=##class(%Net.Authenticator).%New() //YPOPs works with a Yahoo email account Set auth.UserName="isc.test@yahoo.com" Set auth.Password="123pass" Set server.authenticator=auth Set server.AuthFrom=auth.UserName Quit server } ``` 可以将其与上例中所示的`SendSimpleMessage`方法一起使用。 示例3:`SendMessage()` 以下更灵活的方法同时接受`SMTP`服务器和电子邮件。电子邮件应已包含主题行(如果`SMTP`服务器要求),但不必包含地址。然后,此方法将电子邮件发送到一组硬编码的测试目的地: ```java ClassMethod SendMessage(server As %Net.SMTP, msg As %Net.MailMessage) As %Status { Set From=server.authenticator.UserName //make sure From: user is same as used in authentication Set msg.From = From //finish addressing the message Do msg.To.Insert("xxx@xxx.com") //send the message to various test email addresses Do msg.To.Insert("isctest@hotpop.com") Do msg.To.Insert("isc_test@hotmail.com") Do msg.To.Insert("isctest001@gmail.com") Do msg.To.Insert("isc.test@yahoo.com") Set status=server.Send(msg) If $$$ISERR(status) { Do $System.Status.DisplayError(status) Write server.Error Quit $$$ERROR($$$GeneralError,"Failed to send message") } Quit $$$OK } ``` ## `%Net.SMTP`的其他属性 `%Net.SMTP`类还具有一些您可能需要的其他属性,具体取决于使用的SMTP服务器: - `AllowHeaderEncoding`指定`Send()`方法是否对非`ASCII`标头文本进行编码。默认值为1,这意味着非`ASCII`标头文本按照RFC 2047指定的方式进行编码。 - `ContinueAfterBadSend`指定在检测到失败的电子邮件地址后是否继续尝试发送邮件。如果`ContinueAfterBadSend`为1,系统会将失败的电子邮件地址添加到`FailedSend`属性的列表中。默认值为0。 - `ShowBcc`指定是否将密件抄送标头写入电子邮件。这些通常会被SMTP服务器过滤掉。
文章
water huang · 三月 27, 2023

医院信息化建设实战教程:如何在不允许使用Git的情况下自动备份代码/自动执行代码?

一、背景 1.1 我遇到了几个项目,他们的接口服务器崩溃了。 项目上希望尽快恢复服务器。他们的服务器在局域网上运行,他们不能使用git,服务器中有多个命名空间运行不同的服务,而且通常只有一台平台服务器。 1.2 如果消息中有字符流类型的属性,消息搜索页面不支持使用字符流属性进行过滤,因此很难找到想要的消息。 1.3 其他同事可能会更新服务器上的代码,代码中可能有些错误。 2.挑战 2.1 如何快速恢复? 2.2 如何支持字符流属性过滤消息? 2.3 如何在编译类时自动备份? 3.解决方案 1.编译时自动导出为备份文件 首先,我们定义一个名为“SYS.base”的类,它只有一个名为“ CLSBAKPATH”的参数,并设置它的值 Class SYS.Base Extends %RegisteredObject { Parameter CLSBAKPATH = "D:\IRIS\CLSBAK" ; } 然后,定义一个名为“SYS.Projection”的类,它继承了 base和%Projection.AbstractProjection,添加“Projection Reference As SYS.Projection”,重写类方法“CreateProjection”; 代码如下: Class SYS.Projection Extends ( %Projection.AbstractProjection , Base) { Projection Reference As SYS.Projection ; ClassMethod CreateProjection(classname As %String , ByRef parameters As %String , modified As %String , qstruct) As %Status { w ! s changetime=^oddDEF(classname,63) s changetime=$zdt(changetime,3) w "class"_classname_" is modified at "_changetime_" !",! s CLSBAKPATH=..#CLSBAKPATH i CLSBAKPATH[ "/" d .s PL= "/" e d .s PL= "\" s:$e(CLSBAKPATH,*)'=PL CLSBAKPATH=CLSBAKPATH_PL s sc= $SYSTEM.OBJ.Export(classname_".cls",CLSBAKPATH_classname_$tr(changetime,":-","")_ ".xml" ) q $$$OK } } 执行该方法后,被编译的类将直接导出到目标路径“CLSBAKPATH”,它的名称将类似于“类名20230217130218.xml” 最后,如何使用它? 选择一个要自动备份的类,让它继承“SYS.Projection”。当类被编译时,它会被导出。如果你不修改重新编译它,它会覆盖旧的备份文件。 顺便说一句,我认为关键点是 %Projection.AbstractProjection,有了“CreateProjection”,我们可以做更多,如果你想在生产环境中更新一些代码,并且又不想让人再手动去执行一些目标方法代码,这是个好方法。他需要做的是将xml文件导入studio,并编译它,如果勾选“Compile Imported Items”框,那就不再需要单独编译。也就是导入的时候就会自动编译,编译的时候就自动执行,相当于就是导入后自动执行! 到目前为止,它只备份了一个类文件。如果我们想要备份更多怎么办? 方法来了!简单来说,挂任务! 通常用于备份接口代码和数据。 在本教程中,我们在本地驱动器中备份文件。 首先,我们定义一个名为“Common.SYS”的类 然后添加一个名为“ Export ”的类方法,它是主要方法,它将调用其他功能方法。 A导出类,B导出凭证,C导出GLB。 类看起来像: Class Common.SYS Extends %RegisteredObject { /// Exportclass ClassMethod ExportAllClassesIndividual(NS As %String = "" ) { s Drive=..#Drive s currentns= $ZUTIL ( 67 , 6 , $j ) i NS'= "" d .w:'$d(^["%sys"]CONFIG("Namespaces",NS)) "namespace "_NS_" is not existed,please check it!" ,! .q:'$d(^[ "%sys"]CONFIG("Namespaces",NS)) .zn NS .s FilePath=Drive_":/BakFile/"_NS_"/" .i '##class(%File).DirectoryExists(FilePath) d ..d ##class(%File).CreateDirectoryChain(FilePath) .s sc= $system .OBJ.ExportAllClassesIndividual(FilePath,"b",.el,"","CDYZone,Sample,User" ) .i sc'=1 d ..w " namespace "_NS_" get an error when export class! "_$System.Status.GetErrorText(sc),! e d .s alns="" .f s alns=$o(^["%sys"]CONFIG("Namespaces",alns)) q:alns="" d ..q:$e(alns,1)="%" ..q:(alns="ENSDEMO")||(alns="ENSEMBLE")||(alns["-") ..q:'$d(^[alns]CacheMsg("Confirm")) ..zn alns ..s FilePath=Drive_":/BakFile/"_alns_"/" ..i '##class(%File).DirectoryExists(FilePath) d ...d ##class(%File).CreateDirectoryChain(FilePath) ..s sc=$system.OBJ.ExportAllClassesIndividual(FilePath,"b",.el,"","Common,SYS" ) ..i sc'=1 d ...w " namespace "_alns_" get an error when export class! "_ $System.Status.GetErrorText(sc),! zn currentns q $$$OK } /// Ens.Config.Credentials ClassMethod ExportAllCredentials(NS As %String = "" ) { s Drive=..#Drive s currentns= $ZUTIL ( 67 , 6 , $j ) i NS'= "" d . w :' $d (^[ "%sys" ]CONFIG( "Namespaces" ,NS)) "namespace " _NS_ " is not existed,please check it!" ,! . q :' $d (^[ "%sys" ]CONFIG( "Namespaces" ,NS)) . q :' $d (^[NS]Ens.Conf.CredentialsD) . zn NS . s Data=[] . s FilePath=Drive_ ":/BakFile/" _NS_ "/Ens_Config_Credentials" .i ' ##class ( %File ).DirectoryExists(FilePath) d ..d ##class ( %File ).CreateDirectoryChain(FilePath) . s Data=[] . s CID= "" .f s CID= $o ( ^Ens .Conf.CredentialsD(CID)) q :CID= "" d ..s obj= ##class (Ens.Config.Credentials). %OpenId (CID) ..q :' $IsObject (obj) ..s data={} ..s data.SystemName=obj.SystemName ..s data.Username=obj.Username ..s data.Password=obj.Password ..d Data. %Push (data) . d :Data. %Size ()> 0 ..SaveFile (FilePath,Data. %ToJSON ()) e d . ;w currentns,! . s alns= "" .f s alns= $o (^[ "%sys" ]CONFIG( "Namespaces" ,alns)) q :alns= "" d ..q : $e (alns, 1 )= "%" ..q :(alns= "ENSDEMO" )||(alns= "ENSEMBLE" )||(alns[ "-" ) ..q :' $d (^[alns]CacheMsg( "Confirm" )) ..q :' $d (^[alns]Ens.Conf.CredentialsD) ..zn alns ..s FilePath=Drive_ ":/BakFile/" _alns_ "/Ens_Config_Credentials" ..i ' ##class ( %File ).DirectoryExists(FilePath) d . ..d ##class ( %File ).CreateDirectoryChain(FilePath) ..s Data=[] ..s CID= "" ..f s CID= $o ( ^Ens .Conf.CredentialsD(CID)) q :CID= "" d . ..s obj= ##class (Ens.Config.Credentials). %OpenId (CID) . ..q :' $IsObject (obj) . ..s data={} . ..s data.SystemName=obj.SystemName . ..s data.Username=obj.Username . ..s data.Password=obj.Password . ..d Data. %Push (data) ..d :Data. %Size ()> 0 ..SaveFile (FilePath,Data. %ToJSON ()) zn currentns q $$$OK } ClassMethod SaveFile(FilePath, Data) { s file= ##class ( %FileCharacterStream ). %New () s file.Filename=FilePath_ "/Credentials.JSON" d file. Write (Data) s sc=file. %Save () i sc'= 1 d . w FilePath_ " save Credential failed! " _ $System .Status.GetErrorText(sc),! e d . w FilePath_ " save Credential success! " ,! q $$$OK } ClassMethod SaveGLB(NS) { s Drive=..#Drive w :' $d (^[ "%sys" ]CONFIG( "Namespaces" ,NS)) "namespace " _NS_ " is not existed,please check it!" ,! q :' $d (^[ "%sys" ]CONFIG( "Namespaces" ,NS)) 1 s rset = ##class ( %ResultSet ). %New ( "%SYS.GlobalQuery:NameSpaceList" ) q :'rset.QueryIsValid() "invalid Query:" d rset.Execute(NS, "CDY*" ) s Flag= 0 while (rset.Next()){ s HasData=rset.GetDataByName( "HasData" ) i HasData= 1 d . s Name=rset.GetDataByName( "Name" ) . q :Name[ "PushConfigListD" ;some needed globals . s Data(Name)= 1 . s Flag= 1 } w :Flag= 0 NS_ " namespace doesn't have global to be exported" ,! q :Flag= 0 NS_ " namespace doesn't have global to be exported" s FilePath=Drive_ ":/BakFile/" _NS_ "/GLB/" i ' ##class ( %File ).DirectoryExists(FilePath) d . d ##class ( %File ).CreateDirectoryChain(FilePath) q ##class ( %Library.Global ).Export(NS,.Data,FilePath_ "SYS.gof" ) } /// ExportAll CDY* Globals ClassMethod ExportAllCDYGLB(NS As %String = "" ) { s currentns= $ZUTIL ( 67 , 6 , $j ) i NS'= "" d . w :' $d (^[ "%sys" ]CONFIG( "Namespaces" ,NS)) "namespace " _NS_ " is not existed,please check it!" ,! . q :' $d (^[ "%sys" ]CONFIG( "Namespaces" ,NS)) . s sc= ..SaveGLB (NS) .i sc'= 1 d ..w " namespace " _NS_ " get an error when export global! " _ $System .Status.GetErrorText(sc),! e d . s alns= "" .f s alns= $o (^[ "%sys" ]CONFIG( "Namespaces" ,alns)) q :alns= "" d ..q : $e (alns, 1 )= "%" ..q :(alns= "ENSDEMO" )||(alns= "ENSEMBLE" )||(alns[ "-" ) ..q :' $d (^[alns]CacheMsg( "Confirm" )) ..s sc= ..SaveGLB (alns) ..i sc'= 1 d . ..w " namespace" _alns_ " get an error when export global! " _ $System .Status.GetErrorText(sc),! zn currentns q $$$OK } /// location to store backuped file Parameter Drive = "d" ; ClassMethod Export() { s NS= "" ///namespace d ..ExportAllClassesIndividual (NS) ///class file(include package) d ..ExportAllCredentials (NS) ///Credential information d ..ExportAllCDYGLB (NS) ///Global file, or data q $$$OK } } 在 iris 中添加一个自定义任务,你就能保留最新的代码和数据。 优化消息搜索 当我得到使消息搜索页面支持使用字符流类型的属性进行过滤的解决方案时(为了解决这个问题,我问 intersystems 的工程师,她告诉我,使用“EnsPortal.MsgFilter.Assistant”,覆盖类方法“GetSQLCondition”。所以我做了一个类扩展 EnsPortal.MsgFilter.Assistant.it 似乎已经解决了问题。在编译类之后然后 s ^EnsPortal.Settings("MessageViewer","AssistantClass")=class ). 我写的代码如下: Class ENSLIB.MsgFilterAssistant Extends EnsPortal.MsgFilter.Assistant { ClassMethod GetSQLCondition(pOperator As %String , pProp As %String , pValue As %String , pDisplay As %Boolean = 0 ) As %String { if (pValue = "" ) || ((pOperator '= "Like" ) && (pOperator '= "NotLike" )) quit ##super (pOperator, pProp, pValue, pDisplay) if ( "%%" = $extract (pValue, *- 2 , *- 1 )) { set pValue = "'" _ $extract (pValue, 1 , *- 3 ) _ "' ESCAPE '" _ $extract (pValue, *) _ "'" } else { set pValue = "'" _ $replace (pValue, "'" , "''" ) _ "'" } quit "substring(" _ pProp _ ", 1, 3000000) " _ $case (pOperator, "Like" : "LIKE" , "NotLike" : "NOT LIKE" ) _ " " _ pValue } /// w ##class(ENSLIB.MsgFilterAssistant).SetMV() ClassMethod SetMV() { s ^EnsPortal .Settings( "MessageViewer" , "AssistantClass" )= "ENSLIB.MsgFilterAssistant" q $$$OK } } 导入代码后,必须有人执行'##class(ENSLIB.MsgFilterAssistant).SetMV()',然后它才能工作!所以我想如果有一种方法可以在导入或编译后自动运行 SetMV,我寻找解决方案,从论坛到开放交换,最后我想到可以用 Projection,代码更改为: Class ENSLIB.MsgFilterAssistant Extends (EnsPortal.MsgFilter.Assistant, %Projection.AbstractProjection ) { Projection Reference As MsgFilterAssistant ; ClassMethod GetSQLCondition(pOperator As %String , pProp As %String , pValue As %String , pDisplay As %Boolean = 0 ) As %String { if (pValue = "" ) || ((pOperator '= "Like" ) && (pOperator '= "NotLike" )) quit ##super (pOperator, pProp, pValue, pDisplay) if ( "%%" = $extract (pValue, *- 2 , *- 1 )) { set pValue = "'" _ $extract (pValue, 1 , *- 3 ) _ "' ESCAPE '" _ $extract (pValue, *) _ "'" } else { set pValue = "'" _ $replace (pValue, "'" , "''" ) _ "'" } quit "substring(" _ pProp _ ", 1, 3000000) " _ $case (pOperator, "Like" : "LIKE" , "NotLike" : "NOT LIKE" ) _ " " _ pValue } /// w ##class(ENSLIB.MsgFilterAssistant).SetMV() ClassMethod SetMV() { s ^EnsPortal .Settings( "MessageViewer" , "AssistantClass" )= $this q $$$OK } ClassMethod CreateProjection(cls As %String , ByRef params) As %Status { w ! w "开始执行" ,! d ..SetMV () w "执行完成" ,! q $$$OK } } 类编译完成后会自动执行SetMV!是否有更好的实现方式呢? 如果你有更好的方法,请分享! 4.更多 我将继续寻找更好的方法来复制源类和配置数据。 为黄老师点赞!
文章
sun yao · 十月 12, 2022

前端操作自动生成BS、BP、BO

## **概述** 现有Ensemble平台BS(服务)、BP(流程)、BO(操作)需对平台及开发语言有一定的了解才能实现,为简化用户操作,现对现有平台进行二次封装,通过API接口的形式进行前后端分离,通过前端界面操作实现BS(对外提供的服务)、BP、BO(逻辑处理或调用外部的服务)自动生成(通过%Dictionary实现),具体实现如下。 ## **一、开发技术和工具** 版本:Ensemble 2017.2.1 ## **二、涉及公用类** ### 2.1 %Dictionary.ClassDefinition(自定义类) • property **Super** as %CacheString; Specifies one or more superclasses for the class. 定义一个或多个父类,继承父类 • property** ProcedureBlock** as %Boolean [ InitialExpression = 0 ]; Specifies that the class uses procedure block for method code. 设置类是否允许使用程序块,程序块强制实施变量作用域:方法无法看到由其调用方定义的变量,程序块中的任何变量都会自动成为私有变量 • relationship **Parameters** as %Dictionary.ParameterDefinition [ Inverse = parent,Cardinality = children ]; Parameter. 定义类参数,如全局变量、适配器等相关定义 • relationship **Methods** as %Dictionary.MethodDefinition [ Inverse = parent,Cardinality = children ]; Method. 定义类方法 参考链接:http://localhost:57772/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=%25SYS&CLASSNAME=%25Dictionary.ClassDefinition ### 2.2 %Dictionary.ParameterDefinition(自定义类参数) • property **Name** as %Dictionary.CacheIdentifier [ Required ]; The name of the parameter. 定义参数名 • property **Default** as %CacheString [ SqlFieldName = _Default ]; Specifies a default value for the parameter assuming the Expression keyword is blank. 定义参数默认值,不设置则为空 • property **Description** as %CacheString; Specifies a description of the parameter. 定义参数描述 参考链接:http://localhost:57772/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=%25SYS&CLASSNAME=%25Dictionary.ParameterDefinition ###2.3 %Dictionary.MethodDefinition(自定义类方法) • property **Name** as %Dictionary.CacheIdentifier [ Required ]; The name of the method. 定义方法名 • property **ClassMethod** as %Boolean [ InitialExpression = 0 ]; Specifies that the method is a class method. Instance methods can only be invoked via an instantiated object while class methods can be directly invoked without an object instance. 指定该方法是类方法。实例方法只能通过实例化的对象调用,而类方法可以在没有对象实例的情况下直接调用。 • property **FormalSpec** as %CacheString; Specifies the list of arguments. Each argument is of the format [&|*][:][=] where & means pass-by-reference and * means output-only. 定义方法入参,每个入参格式为“参数名:参数类型=默认值”,如:code:%String=”” • property **ReturnType** as %Dictionary.CacheClassname; Specifies the data type of the value returned by a call to the method. Setting ReturnType to an empty string specifies that there is no return value. 定义方法返回值,设置为空则无返回值 • property **WebMethod** as %Boolean [ InitialExpression = 0 ]; Specifies that a method can be invoked as a web method using the SOAP protocol. 设置方法是否为web方法,适用于SOAP协议 • property **Implementation** as %Stream.TmpCharacter; The code that is executed when the method is invoked. In the case of an expression method, this is an expression. In the case of a call method, this is the name of a Cache routine to call. 调用方法时执行的代码。对于表达式方法,这是一个表达式。对于调用方法,这是要调用的缓存例程的名称 参考链接:http://localhost:57772/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=%25SYS&CLASSNAME=%25Dictionary.MethodDefinition ###2.4 Ens.Config.Production • property **Items** as list of Ens.Config.Item(XMLNAME="Item",XMLPROJECTION="ELEMENT"); 定义Production下的BS、BP、BO,根据父类确认属于哪一类 • method **SaveToClass**(pItem As Ens.Config.Item = $$$NULLOREF) as %Status This method saves the production into the XData of the corresponding class 参考链接: http://localhost:57772/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=ENSLIB&CLASSNAME=Ens.Config.Production ###2.5 Ens.Config.Item(BS服务、BP流程、BO操作) • property **PoolSize** as %Integer(MINVAL=0,XMLPROJECTION="ATTRIBUTE"); Number of jobs to start for this config item. Default value: 0 for Business Processes (i.e. use shared Actor Pool) 1 for FIFO message router Business Processes (i.e. use a dedicated job) 1 for Business Operations 0 for adapterless Business Services 1 for others For TCP based Services with JobPerConnection=1, this value is used to limit the number of connection jobs if its value is greater than 1. A value of 0 or 1 places no limit on the number of connection jobs. 设置缓冲池大小 • property **Name** as %String(MAXLEN=128,XMLPROJECTION="ATTRIBUTE") [ Required ]; The name of this config item. Default is the class name. 设置BS、BP、BO名称 • property **ClassName** as %String(MAXLEN=128,XMLPROJECTION="ATTRIBUTE") [ Required ]; Class name of this config item. 设置BS、BP、BO类名称 • property **Category** as %String(MAXLEN=2500,XMLPROJECTION="ATTRIBUTE"); Optional list of categories this item belongs to, comma-separated. This is only used for display purposes and does not affect the behavior of this item. 设置类别 • property **Comment** as %String(MAXLEN=512,XMLPROJECTION="ATTRIBUTE"); Optional comment text for this component. 设置注释 • property **Enabled** as %Boolean(XMLPROJECTION="ATTRIBUTE") [ InitialExpression = 1 ]; Whether this config item is enabled or not. 设置启用停用标志 参考链接:http://localhost:57772/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=ENSLIB&CLASSNAME=Ens.Config.Item ##**三、实现方法** ###3.1 创建BS模板类 创建模板类,后续类生成方法体通过模板类获取 /// BS的SOAP模板 Class HIP.Platform.Template.BSSOAPTemplate Extends EnsLib.SOAP.Service { Parameter ADAPTER; Parameter NAMESPACE = "http://tempuri.org"; Parameter SERVICENAME = "BSSOAPTemplate"; Method TemplateFun(code As %String, data As %GlobalCharacterStream) As %GlobalCharacterStream [ WebMethod ] { set OutStream=##class(%GlobalCharacterStream).%New() try{ s ..%ConfigName = $classname($this) set sourceCode=$p($classname($this),".",4) //PUB000X set methodCode=##safeexpression(""""_$get(%methodname)_"""") //SendDataFromHis s messageCode = $p(code,"^",1) s requestType= $select($p(code,"^",2)="REST":"REST", 1:"SOAP") set proc = ##class(%SYS.ProcessQuery).%OpenId($j) //当前进程 获取调用服务客户端的IP地址 set sc = ##class(HIP.Service.PublishService).GetAllowedIP(sourceCode) if +sc=1 { s allowedIP = $p(sc,"^",2) if allowedIP '[ proc.ClientIPAddress { SET oref=##class(%Exception.General).%New("","无权限",,"您的IP地址不允许访问,请联系管理员") THROW oref } }else{ return sc } s request = ##class(HIP.Platform.Message.Request).%New() s request.sourceCode=sourceCode //PUB0001 s request.requestType=requestType //REST SOAP s request.inputFlag="0" //-1表示失败,0表示未处理,1表示成功 s request.inputStream = data //JSON流,或者XML流 s request.messageCode=messageCode //BOE0001 Set tSC=..SendRequestSync("HIP.Platform.BP.ProcessCode",request,.pOutput) If $$$ISERR(tSC) Do ..ReturnMethodStatusFault(tSC) d OutStream.CopyFrom(pOutput.outStream) return OutStream }catch err { set OutStream=##class(%GlobalCharacterStream).%New() do OutStream.Write(err.DisplayString()) return OutStream } } Storage Default { %%CLASSNAME ^HIP.PlatforE240.BSSOAPTemplateD BSSOAPTemplateDefaultData ^HIP.PlatforE240.BSSOAPTemplateD ^HIP.PlatforE240.BSSOAPTemplateI ^HIP.PlatforE240.BSSOAPTemplateS %Library.CacheStorage } } ###3.2 自动生成BS,并添加至Production中 通过模板类自动生成WebService方法,并添加到Production的BS中 /// 创建BS服务 PUB00XX服务,提供给第三方调用 /// d ##class(HIP.Util.SOAP).BSCreateSOAPInfo("PUB0001","提供给HIS访问平台") ClassMethod BSCreateSOAPInfo(Code As %String, Desc As %String) As %Status { ///HIP.Platform.BS.PUB0001 s src = "HIP.Platform.BS."_Code_".PublishWebService" s isExist = 0 try { set isExist=##class(%Dictionary.ClassDefinition).%ExistsId(src) if isExist=1 { //类已存在则更新,先删除再插入 set classObj = ##class(%Dictionary.ClassDefinition).%OpenId(src) d classObj.Parameters.Clear() d classObj.Properties.Clear() d classObj.Indices.Clear() d classObj.ForeignKeys.Clear() d classObj.Methods.Clear() }else { //类不存在则新建 set classObj = ##class(%Dictionary.ClassDefinition).%New(src) } //设置父类 s classObj.Super="EnsLib.SOAP.Service" //设置允许使用程序块,则可动态定义变量 s classObj.ProcedureBlock=1 ///Parameter的值 //设置适配器 set ParDef = ##class(%Dictionary.ParameterDefinition).%New() set ParDef.Name="ADAPTER" d classObj.Parameters.Insert(ParDef) set ParDef = ##class(%Dictionary.ParameterDefinition).%New() //设置服务名 set ParDef.Name="SERVICENAME" set ParDef.Default=Code set ParDef.Description=Desc d classObj.Parameters.Insert(ParDef) //设置命名空间 set ParDef = ##class(%Dictionary.ParameterDefinition).%New() set ParDef.Name="NAMESPACE" set ParDef.Default="www.boe.com" d classObj.Parameters.Insert(ParDef) ///函数模板代码,通过模板类获取 s methodTemplate = ##class(%Dictionary.MethodDefinition).%OpenId("HIP.Platform.Template.BSSOAPTemplate||TemplateFun") Set methodObj=##class(%Dictionary.MethodDefinition).%OpenId(src_"||SendData") if methodObj="" Set methodObj=##class(%Dictionary.MethodDefinition).%New(src_".SendData") //设置方法名 set methodObj.Name="SendData" set methodObj.ClassMethod=0 //set methodObj.FormalSpec="code:%String,data:%GlobalCharacterStream,*pOutput:HIP.Platform.Message.Response" //设置方法入参 set methodObj.FormalSpec="code:%String,data:%GlobalCharacterStream" //设置方法返回值 set methodObj.ReturnType="%GlobalCharacterStream" //设置方法为WebService方法 set methodObj.WebMethod=1 //设置方法具体实现代码,通过模板类获取 set methodObj.Implementation=methodTemplate.Implementation d classObj.Methods.Insert(methodObj) set sc=classObj.%Save() if $$$ISERR(sc) { return $system.Status.GetErrorText(sc) }else{ d $system.OBJ.Compile(src,"ck/displaylog=0") } if isExist=0 { //存储到production中 s prodObj = ##class(Ens.Config.Production).%OpenId("HIP.Platform.Production") if $IsObject($G(prodObj)){ Set item = ##class(Ens.Config.Item).%New() Set item.PoolSize = 1 Set item.Name = src Set item.ClassName = src Set:item.Name="" item.Name = item.ClassName Set item.Category = "" Set item.Comment = Desc Set item.Enabled = 1 Set tSC = prodObj.Items.Insert(item) If $$$ISOK(tSC) { // save production (and item) Set tSC = prodObj.%Save() set ^TempSy("tSC")=tSC If ($$$ISOK(tSC)) { // update production class Set tSC = prodObj.SaveToClass() } return tSC } If $$$ISERR(tSC) return $system.Status.GetErrorText(tSC) } } return $$$OK } catch(ex) { return ex.DisplayString() } } ###**四、 结果展示** 运行 d ##class(HIP.Util.SOAP).BSCreateSOAPInfo("PUB0001","提供给HIS访问平台") 后,Studio中自动生成HIP.Platform.BS.PUB0001.PublishWebService.cls 类 如下: ![image](/sites/default/files/inline/images/zi_dong_sheng_cheng_lei__0.png) 打开Portal管理界面,Production配置,可看到该服务已添加至Production中,如下: ![image](/sites/default/files/inline/images/production_1.png) 可直接通过soapUI调用,地址 http://localhost:57772/csp/hip/HIP.Platform.BS.PUB0001.PublishWebService.CLS?WSDL=1 ![image](/sites/default/files/inline/images/soapui.png) InterSystems消息查看 ![image](/sites/default/files/inline/images/xiao_xi_1.png) ![image](/sites/default/files/inline/images/xiao_xi_2.png) ![image](/sites/default/files/inline/images/xiao_xi_3.png) ###**五、 结论与猜想** 同理,BO也可通过该方法实现自动生成,另可通过建立REST服务或WebService服务的方式通过前端调用该方法实现前端自动生成BS、BP、BO,以简化用户操作,但该方法存在问题点,如BP都为公用单个BP,消息并发量大时可能导致BP堵塞问题,可能实现的解决方法为前端先单独调用接口创建BP,后生成BS,再通过配置实现BS到BP的关联,大家感兴趣可自行尝试,以上,谢谢! 点赞 讲解的非常详细,非常有用。 非常有用的知识 加油 感谢分享 膜拜大佬 学习到了👍👍👍 非常有价值,思路清晰,值得学习 真好 博主的思路值得学习! 谢谢分享,学习了
文章
Nicky Zhu · 十一月 15, 2021

关于信息平台/数据中台技术,你应该知道的八件事

查看原文 近日,国家卫健委统计信息中心发布了两则通知—— 2021年10月25日,国家卫健委统计信息中心发布《关于开展国家医疗健康信息互联互通标准化成熟度评测工作的通知》,这意味着新一年的评测工作开始启动。 2021年11月5日,国家卫健委统计信息中心发布了“关于2020年度国家医疗健康信息互联互通标准化成熟度测评结果(第二批)公示的通知”,公布了第二批10个区域和92家医院的测评结果。 这两则通知,再次将“互联互通”带到了医疗IT人的面前。而每每谈到互联互通,就不可避免地要谈到集成平台、信息平台和数据中台等项目建设问题,本文将从供应商选择、技术选型等从八个核心问题,浅谈关于平台和中台的那些事。 一、如何选择供应商? 如上图所示,如果我们把平台/中台项目的实施方称作解决方案提供商,那么每一家解决方案提供方背后还会有一家产品技术提供方解决方案,因为解决方案提供方往往需要借助成熟的产品来实现信息平台和数据中台项目,以聚焦所服务医院客户的具体需求,并加速实施效率,所以一个平台/中台供应链条相对比较长。也因此,医院/医疗集团需要花费更多的精力在产品和解决方案的组合中进行选型。选型的标准也成为许多信息中心或者CIO们关注的首要问题。 首先要考虑平台/中台解决方案提供方本身的品牌和实力:通常而言,选择全国性的解决方案提供方更安全一些,这类厂商的解决方案相对成熟、成功案例多,技术能力强,实施经验丰富;但是对于一些规模略小的医院而言,可能会顾虑这些厂商的客户太多,对本院的支持力度不够,或者是在该厂商在当地没有分公司,存在技术服务跟不上等问题,也可能会更倾向于初创企业或者本地解决方案提供方来做项目的集成或者实施,这两种选择都没有问题,关键是所选择的合作伙伴要值得信赖。 另外要考虑厂商背后的产品技术提供方:通常产品技术提供方不直接面向最终客户提供实施服务,而是通过本地合作伙伴向最终客户(即医院或者医疗集团)提供服务。但是作为解决方案的基础,该产品或者技术本身的先进性、可靠性以及未来的可扩展性都是需要重点衡量的因素。例如医院建设集成平台或者互联互通平台通常都会本着以评促建以评促用的目的,利用信息平台的建设契机,打通院内的信息和数据流程。此时,产品技术提供方有多少业务建模和流程整合的案例与经验也将在很大程度上影响项目的交付质量。 同样,医院也可以参考该产品技术提供方的行业积累、案例详情、服务承诺以及业界口碑等等。 总结一下,在选择方案时,需要考虑的实际上是产品本身的技术能力和对应的解决方案提供方的服务能力。因此,我们建议大家基于成熟的产品,选择能提供较好技术服务的解决方案提供方。如果产品并不成熟,那么即使解决方案提供方愿意常年提供驻场技术服务,也很难应对故障,也难以制订预案保障平台稳定运行。 二、技术路线的选择 在医疗行业进行业务和数据整合时,用户常常会需要在点对点集成模式、消息路由模式以及SOA架构模式进行技术决策。 事实上,从来就未曾出现过集成模式的最终解决方案。医院和医疗集团用某种特定的集成模式搭建自己的数字化高速公路时需要充分考虑该模式是否适合自己的场景,投入产出比是否符合自己的预期,以及是否能够充分利用该模式的优势。 举例而言,当医院考虑采用SOA架构时,需要考虑到遗留系统是否能够提供服务接口;在当前的业务运行条件下,是否能够承受由于接口的侵入式设计引入的风险,是否可能通过预案规避风险;以及医院是否已经或者将在平台投产前后具有实时数据分析的需求和技术储备。否则就将面临投入无法得到回报的质疑,甚至是规划无法落地的尴尬。 再看一个例子,如果要采用点对点模式集成,那么医院就需要考虑在平台投产可预见的周期(如3~5年)内,是否会面临跨部门跨系统数据利用需求快速增长的前景。如果有,那么,由于缺乏SOA架构能够提供的业务抽象和整合的能力,爆炸性增长的接口数量和数据整合需求会成为信息科难以应对的直接威胁。 正是由于集成模式的高度个性化,我们认为作为基础设施的集成平台类产品必须能够支持所有集成模式。一方面是满足各种不同类型的医院的需要,另一方面,医院也需要认识到,基础设施的建设从来都不是一蹴而就的“一锤子买卖”。您完全可以在集成需求数量较低时选用点对点模式快速投产,在需要进行流程和数据整合时应用SOA架构以获得企业全景视图的整合优势。而一个能够支持所有模式的产品才能赋能于客户,使之具备进行策略选择的优势。 三、开源策略的潜在风险 采用开源组件迅速获得能力,结合DevOps快速迭代开发是应对快速变化的市场环境和需求,进行产品化开发时的优先选择。在进行应用开发时,这样的策略通常有效。 然而优势与代价总是如影随形。借助开源组件的优势是能够快速获得能力,但开源组件的稳定性、可靠性和安全性则是每一个技术决策者都需要考察的关键风险,甚至开源组件许可证的更新都有可能为企业引入巨大的知识产权风险。 例如久负盛名的ElasticSearch,作为一款企业级搜索和分析引擎,它对于文本检索的能力和效率都有保障,被许多产品集成用于检索。但ElasticSearch及其依赖的其他组件已被检测出大量的安全性漏洞,例如可以引入中间人攻击的CVE-2021-22138,可以允许用户查看未授权敏感信息的CVE-2021-22147,以及可以允许用户通过ElasticSearch在服务器上运行任何OS指令的CVE-2014-3120等,风险列表每年都在更新。(风险列表可参见https://www.elastic.co/community/security)同样是ElasticSearch,在将开源授权更新为SSPL之后,如果业务用到了ElasticSearch并打包为可盈利产品,则ElasticSearch公司有权要求用户开放源码并收取费用。 因此,在使用大规模集成开源组件构建的产品时,医院需要评估产品技术提供方是否能够及时更新开源组件以获得安全性更新,并评估产品技术提供方是否能够及时跟踪和处理因授权变化会引入的法律风险。 集成平台、数据中台甚至是数据库这样的基础设施如果构建在大量的开源组件上,频繁的版本变动通常意味着组件集成风险的大幅升高,而跟踪和处理版本变更的技术和法务影响也将成为需要持续投入的持有成本。因此,一体化、完整知识产权的集成平台或数据中台产品在简化技术堆栈的同时也将大幅降低长期持有成本。对于医院用户来说,需要平衡评估一体化商业产品和开源集成产品的购买和持有成本,更需要考察产品技术提供方对开源组件的跟踪、更新和维护能力。 四、关注稳定与可靠性保障 核心业务系统、集成平台和数据中台这一类的关键系统,事关医院业务是否能持续运行,其运行稳定性与可靠性的重要意义不言而喻。基于主备、多活等冗余技术的平台高可用和灾备方案仍是为平台运行保驾护航的关键手段。 在市场上可见到的诸多产品中,有采用原生高可用方案的产品,也有集成第三方或开源技术高可用方案的产品。在这里,各位信息中心负责人或者技术决策者不得不考虑一个问题,即高可用方案的责任归属问题。 因此,即使一些非核心部件采用第三方技术,高可用方案也应采用产品原生技术。即使退一步来说,在没有原生高可用方案的情况下,您的解决方案提供方也应当承担起解决平台可用性和可靠性问题的技术服务角色。试想,当高可用方案失效或处于故障状态时,解决方案提供方采用了非原生高可用方案,届时难道能依赖开源社区的随机问答解决问题? 五、跨越技术门槛 医疗数字化进程与人工智能等目前的热点技术有很大不同,即必须基于当前业务。但由于医疗业务本身面临与新技术的融合,因此数字化进程也必须具备足够的灵活性,能够迅速应对业务过程的演化。而医疗业务流程或数据流程的演化,是需要业务专家、开发工程师、运维保障团队协作共同完成的,每一种角色都需要在平台上工作。因此,纯粹面向开发工程师的技术平台将无法有效应对业务流程本身的快速迭代。我们认为,一个成熟的集成平台/数据中台,需要为团队中不同角色的成员提供适合他们的开发/维护/测试工具,使各成员能以较低的成本各施所长。这些工具至少包括: · 图形化的流程、数据转换和业务规则建模工具,使得业务专家即使不了解业务组件的技术实现,也能利用平台上的组件搭建出适合医院的业务/数据流 · 专业的IDE和管理工具,供研发工程师扩展业务组件和对现有组件进行跟踪和组件级调优 · 监控和管理工具,供运维工程师监控平台运行的健康状况和性能,必要时对平台运行参数进行调优 六、集群的选择 我们理解一些工程师非常关心产品是否支持负载均衡。需要注意的是,对于现代的集成平台和数据中台而言,它们本身应当是由一系列的集群共同构成的分布式系统。 比如院内集成平台拥有基于Web的操作管理页面,运行API实现或数据流程的容器,有消息引擎,有数据库,因此,可以构成一个典型的由Web程序,API/应用服务器,消息引擎和数据库分层构建的分布式系统,而其中的每一层,都可以根据高可用与负载需求以不同目的的集群形态搭建出来。 以集成平台产品为例,通常,集成平台的Web管理程序由于并发操作的人很少,不需要单独进行集群化;而API/应用服务器层默认会采用高可用集群,对于业务量极大的用户,则可以采用负载均衡+高可用集群;数据库层同样如此,必要时还可以考虑部署读写分离+负载均衡+高可用集群;消息引擎则比较特别,如果不需要保障消息的先进先出特性,可以部署高可用和负载均衡集群,而对于需要保障消息处理时序的场景,则通常不能依赖负载均衡集群,或即使部署了负载均衡集群,也需要控制消息分规则,由单一实例处理这样的消息。 但是否采用及采用何种集群架构,则完全应当基于业务的实际需要和产品能力。举例而言,InterSystems产品可以支持负载均衡+高可用集群,还可以部署为读写分离+负载均衡+高可用集群,但通常我们并不会作为默认配置推荐给集成平台用户。原因在于,我们的产品在单台服务器上经性能测试可以达到20亿消息/天的处理效率。而根据我们对国内数百家三家医院的实际调查,即使在国内顶尖的三甲医院中,也未发现超过2千万消息/天的性能需求。因此,对于单体医院,高可用集群已足够使用。基于奥卡姆剃刀原理和成本控制的基本需求,负载均衡集群并无必要,反而会由于加大了架构的复杂性使持有、维护成本都大幅提升。而对于医疗集团客户,由于需要集成数十家三甲与二级医院,同时还需要控制单个服务器的成本,因此我们的一部分医疗集团客户部署了负载均衡+高可用集群并可进行弹性横向扩展。 当然,不同的产品有不同的性能指标,如果产品的本身性能表现无法支撑医院业务量,那么部署为负载均衡集群支撑医院的实际需求还是非常必要的。 七、技术服务保障 相比开源产品,基于商业产品搭建平台/中台解决方案最显著的附加价值主要来源于技术服务。无论是最终用户还是解决方案提供方,都能受益于产品提供方的技术服务。技术服务也是项目能否成功上线、持续稳定运行或者二次开发的重要保障,对于技术服务,需要考虑产品技术提供方是否能够提供下面的三种或者以上的方式: · 故障处理和技术支持:对于产品应用、二次开发的疑问,是否可获得技术支持资源以解决疑问?对于在产品运行过程中可能遭遇的软硬件故障,尤其是系统崩溃、宕机等高等级事件,是否能够获得直接的技术支持解决、定位和调查问题? · 产品培训:是否具备成体系的产品应用、二次开发和维护培训体系 · 在线课程 · 产品文档库 · 开发者社区:非工程师的客户往往不重视开发者社区的力量。实际上,作为可供全球开发者沟通的场所,在开发者社区往往能找到常见问题的解决方案,具体问题和场景的最佳实践,前瞻性技术研究等非常有价值的资料。 对于医院或是医疗集团客户来说,如果需要掌握信息平台或数据中台,能够达到自主维护、持续演进的目标,那么,无论是通过解决方案提供方还是通过产品技术提供方,都需要获得上述的多种技术服务支持。 八、选型中的常见问题 对于采用商业产品这一策略本身,需要经过大量的选型工作。产品技术提供方和解决方案提供商都会积极宣传自己的产品,而医院则需要对产品的特性,服务体系,性能表现,案例的代表性,综合实施效果等做出评估,方可得出对自己有利的评估结果。在这个过程中,客户往往还是需要综合运用多种手段,包括自行评估、走访典型案例和开展验证测试等手段,避免常见的一些问题,例如: · 技术的可执行性评估不足 例如对于仅支持消息引擎的集成平台,往往需要按照一种特定的消息类型进行通信,使系统间交互具备统一协议,并且系统都需要改造以接入消息引擎。这样的规划不能说不好,但医院的遗留系统能不能都配合平台进行改造或医院有没有足够的预算支撑改造项目落地,以及业务系统现场改造的风险,都会影响实施效果。因此需要切实评估和核实。 · 产品特性不能达到预期 例如对于具有ETL能力的产品,需要评估其对于大容量数据(例如初始化数据加载过程)进行批量采集、转换和落盘的处理效率,以便与借助简单的SQL JDBC连接逐条抽数和转换的SQL适配器相区别。由于两种模式在处理速度上通常有数量级的差别,如果使用SQL适配器模式,在大批量对数据进行ETL操作时将不可避免地遭遇瓶颈。 · 产品对主流技术的覆盖不全面 在现在的技术条件下,即使对同一类型的接口,也往往有多种技术选择。如产品不能提供对这些技术的覆盖,则用户需要投入额外的成本和风险完成接入。例如对常见的负载均衡方案而言,通常对于推模式接口(由外部调用触发的接口),例如SOAP WebService或者REST API,往往都能提供负载均衡;而对于拉模式接口(由产品自身自动触发),例如SQL扫描或一些CDC功能接口,则无法直接受益于负载均衡技术。假如实际业务中有大量需要通过SQL获取数据的接口,则负载均衡集群并没有多少意义。 再如SQL接口可以基于JDBC或ODBC连接,如果产品只能支持其中一种连接,那么对于遗留系统的接入能力将大打折扣。 · 缺乏技术验证过程和约束 对于架构和技术的落地,通常需要验证过程,用户才可能获得预期的效果。例如市场上存在对架构模式进行过度简化与概念偷换的现象。例如将SOA架构等价于ESB、将ESB的概念偷换为接口引擎、或将集成平台概念偷换为消息引擎,而在实施时更进一步地简化为接口的注册和连接,实际上变成了点对点模式。由于集成架构将影响未来3~5年的医院数字化转型过程中的难度和成本,点对点模式的后续实施成本将随接口的数量增加指数上升,导致后期的实施成本居高不下。 当然,充分了解到以上关于供应商选择与技术选型的8个问题,才是真正的互联互通建设的起点,更重要的是,医院信息负责人还需要真正读懂评测要求,并了解本院建设互联互通的整体目标以及医院管理层、临床业务部门等相关部门的不同述求,把这些目标与述求一一映射到平台/中台解决方案中,才是成功通关的秘籍。
文章
姚 鑫 · 五月 5, 2021

第三章 使用多维存储(全局变量)(一)

# 第三章 使用多维存储(全局变量)(一) 本章描述了使用多维存储(全局变量)可以执行的各种操作。 # 以全局变量存储数据 在全局节点中存储数据很简单:像对待任何其他变量一样对待全局变量。 区别在于对全局变量的操作是自动写入数据库的。 ## 创建全局变量 创建新的全局变量不需要设置工作;只需将数据设置为全局变量即可隐式创建新的全局结构。可以创建全局变量(或全局变量下标)并通过单个操作将数据放入其中,也可以创建全局变量(或下标)并通过将其设置为空字符串将其保留为空。在ObjectScript中,这些操作是使用`SET`命令完成的。 下面的例子定义了一个名为`Color`(如果还不存在)的全局变量,并将值`“Red”`与之关联。 如果已经存在一个名为`Color`的全局变量,那么这些示例将其修改为包含新信息。 在ObjectScript中: ```java SET ^Color = "Red" ``` 注意:在应用程序中使用直接全局访变量问时,应制定并遵守命名约定,以防止应用程序的不同部分相互“遍历”;这类似于为类、方法和其他变量开发命名约定。 ## 在全局变量节点中存储数据 要在全局下标节点中存储值,只需像设置任何其他变量数组一样设置全局节点的值。如果指定的节点以前不存在,则会创建该节点。如果它确实存在,则其内容将替换为新值。 可以通过表达式(称为全局引用)指定全局内的节点。全局引用由脱字符(`^`)、全局名称和(如果需要)一个或多个下标值组成。下标(如果有)用括号“()”括起来,并用逗号分隔。每个下标值本身都是一个表达式:文字值、变量、逻辑表达式,甚至是全局引用。 **设置全局节点的值是一个原子操作:它肯定会成功,不需要使用任何锁来确保并发性。** 以下都是有效的全局引用: 在ObjectScript中: ```java SET ^Data = 2 SET ^Data("Color")="Red" SET ^Data(1,1)=100 /*第二级下标(1,1)设置为值100。第一级下标(^DATA(1))不存储任何值。 */ SET ^Data(^Data)=10 /*全局变量^data的值是下标的名称。 */ SET ^Data(a,b)=50 /*局部变量a和b的值是下标的名称 */ SET ^Data(a+10)=50 ``` 此外,还可以在运行时使用间接方式构造全局引用。 ## 在全局变量节点中存储结构化数据 每个全局节点可以包含最多`32K`个字符的单个字符串。 数据通常以以下方式之一存储在节点中: - 作为最多`32K`个字符的单个字符串(具体地说,`32K - 1`)。 - 作为包含多条数据的字符分隔字符串。 要使用字符分隔符在节点中存储一组字段,只需使用连接操作符(`_`)将这些值连接在一起。下面的ObjectScript示例使用`#`字符作为分隔符: ```java SET ^Data(id)=field(1)_"#"_field(2)_"#"_field(3) ``` 检索数据时,可以使用`$PIECE`函数将字段拆分: ```java SET data = $GET(^Data(id)) FOR i=1:1:3 { SET field(i) = $PIECE(data,"#",i) } QUIT ``` - 作为包含多条数据的`$LIST`编码字符串。 `$LIST`函数使用特殊的长度编码方案,不需要保留分隔符。(这是InterSystems IRIS对象和SQL使用的默认结构。) 要在节点中存储一组字段,请使用`$LISTBUILD`函数构造列表: ```java SET ^Data(id)=$LISTBUILD(field(1),field(2),field(3)) ``` 检索数据时,可以使用`$LIST`或`$LISTGET`函数将字段拆分: ```java SET data = $GET(^Data(id)) FOR i = 1:1:3 { SET field(i)=$LIST(data,i) } QUIT ``` - 作为较大数据集(例如流或`“BLOB”`)的一部分。 **由于单个节点的数据量限制在略低于`32K`,因此可以通过将数据存储在一组连续节点中来实现更大的结构(如流):** ```java SET ^Data("Stream1",1) = "First part of stream...." SET ^Data("Stream1",2) = "Second part of stream...." SET ^Data("Stream1",3) = "Third part of stream...." ``` **获取流的代码(如`%GlobalCharacterStream`类提供的流)循环遍历结构中的连续节点,该结构将数据作为连续字符串提供**。 - 作为一个位串。 如果正在实现位图索引(位字符串中的位对应表中的行的索引),应该将全局索引的节点值设置为位字符串。 请注意IRIS使用压缩算法来编码位串; 因此,位串只能使用IRIS `$BIT`函数来处理。 - 作为一个空节点。 如果感兴趣的数据是由节点本身提供的,那么通常将实际下标设置为空字符串(`""`)。 例如,将名称与`ID`值相关联的索引通常是这样的: ```java SET ^Data("APPLE",1) = "" SET ^Data("ORANGE",2) = "" SET ^Data("BANANA",3) = "" ``` # 删除全局节点 要从数据库中删除一个全局节点、一组子节点或整个全局节点,请使用ObjectScript `kill`或`ZKILL`命令。 `Kill`命令删除特定全局引用处的所有节点(数据及其在数组中的相应条目),包括任何子代节点。也就是说,所有以指定下标开头的节点都将被删除。 例如,ObjectScript语句: ```java KILL ^Data ``` 删除整个`^Data`全局变量。对此全局变量的后续引用将返回``错误。 ObjectScript语句: ```java KILL ^Data(100) ``` 删除`^Data`全局变量中节点`100`的内容。如果有子代节点,如`^data(100,1)`、`^data(100,2)`和`^data(100,1,2,3)`,这些子节点也会被删除。 ObjectScript `ZKILL`命令用于删除指定的全局或全局下标节点。它不会删除子代子节点。 **注意:在杀死一个大型全局变量之后,该全局变量曾经占用的空间可能没有完全释放,因为垃圾收集器守护进程在后台将这些块标记为空闲。因此,在终止大型全局变量之后立即调用`SYS.Database`类的`ReturnUnusedSpace`方法可能不会返回预期大小的空间,因为该全局占用的块可能尚未释放。** **不能对全局变量使用`new`命令。** # 测试全变量局节点的存在 要测试特定全局变量(或其后代)是否包含数据,请使用`$DATA`函数。 `$DATA`返回一个值,该值指示指定的全局变量引用是否存在。可能的返回值包括: 状态值| 含义 ---|--- `0`| 全局变量未定义。 `1`| 全局变量存在并包含数据,但没有子代。请注意,空字符串(`“”`)可用作数据。 `10`| 全局变量有后代(包含指向子节点的向下指针),但本身不包含数据。对此类变量的任何直接引用都将导``错误。例如,如果`$data(^y)`返回`10`,则`SET x=^y`将产生``错误。 `11`| 全局变量既包含数据,又有后代(包含指向子节点的向下指针)。 # 检索全局变量节点的值 要获取存储在特定全局变量节点中的值,只需使用全局引用作为表达式: ```java SET color = ^Data("Color") ; assign to a local variable WRITE ^Data("Color") ; use as a command argument SET x=$LENGTH(^Data("Color")) ; use as a function parameter ``` ## `$GET`函数 还可以使用`$GET`函数获取全局节点的值: ```java SET mydata = $GET(^Data("Color")) ``` 这将检索指定节点的值(如果存在),如果该节点没有值,则返回空字符串(`“”`)。如果节点没有值,可以使用可选的第二个参数`$get`返回指定的默认值。 ## `WRITE`、`ZWRITE`和`ZZDUMP`命令 可以使用各种ObjectScript显示命令显示全局变量或全局变量子节点的内容。`WRITE`命令以字符串形式返回指定全局或子节点的值。`ZWRITE`命令返回全局变量的名称及其值,以及它的每个子代节点及其值。`ZZDUMP`命令以十六进制转储格式返回指定全局或子节点的值。
文章
姚 鑫 · 四月 23, 2021

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

# 第五章 优化查询性能(二) # 使用索引 **索引通过维护常见请求数据的排序子集,提供了一种优化查询的机制。 确定哪些字段应该被索引需要一些思考:太少或错误的索引和关键查询将运行太慢; 太多的索引会降低插入和更新性能(因为必须设置或更新索引值)。** ## 什么索引 要确定添加索引是否会提高查询性能,请从管理门户SQL接口运行查询,并在性能中注意全局引用的数量。 添加索引,然后重新运行查询,注意全局引用的数量。 一个有用的索引应该减少全局引用的数量。 可以通过在`WHERE`子句或`ON`子句条件前使用`%NOINDEX`关键字来防止使用索引。 应该为联接中指定的字段(属性)编制索引。左外部联接从左表开始,然后查看右表;因此,应该为右表中的字段建立索引。在下面的示例中,应该为`T2.f2`编制索引: ```sql FROM Table1 AS T1 LEFT OUTER JOIN Table2 AS T2 ON T1.f1 = T2.f2 ``` 内部联接应该在两个`ON`子句字段上都有索引。 执行“显示计划”,然后找到第一张map。 如果查询计划中的第一个项目是`“Read master map”`,或者查询计划调用的模块的第一个项目是`“Read master map”`,则查询的第一个映射是主映射,而不是索引映射。 因为主映射读取数据本身,而不是数据索引,这总是表明查询计划效率低下。 除非表相对较小,否则应该创建一个索引,以便在重新运行该查询时,查询计划的第一个映射表示“读取索引映射”。 应该索引在`WHERE`子句`equal`条件中指定的字段。 可能希望索引在`WHERE`子句范围条件中指定的字段,以及`GROUP BY`和`ORDER BY`子句中指定的字段。 在某些情况下,基于范围条件的索引可能会使查询变慢。如果绝大多数行满足指定的范围条件,则可能会发生这种情况。例如,如果将`QUERY`子句`WHERE Date < CURRENT_DATE` 用于大多数记录来自以前日期的数据库,则在`DATE`上编制索引实际上可能会降低查询速度。这是因为查询优化器假定范围条件将返回相对较少的行数,并针对此情况进行优化。可以通过在范围条件前面加上`%noindex`来确定是否发生这种情况,然后再次运行查询。 如果使用索引字段执行比较,则比较中指定的字段的排序规则类型应与其在相应索引中的排序规则类型相同。例如,`SELECT`的`WHERE`子句或联接的`ON`子句中的`Name`字段应该与为`Name`字段定义的索引具有相同的排序规则。如果字段排序规则和索引排序规则之间存在不匹配,则索引可能效率较低或可能根本不使用。 ![image](/sites/default/files/inline/images/1_39.png) ![image](/sites/default/files/inline/images/2_21.png) ## 索引配置选项 以下系统范围的配置方法可用于优化查询中索引的使用: - 要将主键用作`IDKey`索引,请设置`$SYSTEM.SQL.Util.SetOption()`方法,如下所示 `SET status=$SYSTEM.SQL.Util.SetOption("DDLPKeyNotIDKey",0,.oldval)`. 默认为`1` - 要将索引用于`SELECT DISTINCT`查询,请设置`$SYSTEM.SQL.Util.SetOption()`方法,如下所示: `SET status=$SYSTEM.SQL.Util.SetOption("FastDistinct",1,.oldval)`. 默认为`1` ## 索引使用情况分析 可以使用以下任一方法按SQL缓存查询分析索引使用情况: - 管理门户索引分析器SQL性能工具。 - `%SYS.PTools.UtilSQLAnalysis`方法`indexUsage()`、`tableScans()`、`tempIndices()`、`joinIndices()`和`outlierIndices()`。、 ## 索引分析 可以使用以下任一方法从管理门户分析SQL查询的索引使用情况: - 选择系统资源管理器,选择工具,选择SQL性能工具,然后选择索引分析器。 - 选择系统资源管理器,选择SQL,然后从工具下拉菜单中选择索引分析器。 索引分析器提供当前命名空间的SQL语句计数显示和五个索引分析报告选项。 ![image](/sites/default/files/inline/images/3_17.png) ![image](/sites/default/files/inline/images/4_13.png) ![image](/sites/default/files/inline/images/5_6.png) ### SQL语句计数 在SQL索引分析器的顶部有一个对命名空间中的所有SQL语句进行计数的选项。按收集SQL语句按钮。SQL索引分析器显示“正在收集SQL语句...”当计票进行时,然后“完成!”当清点完毕后。SQL语句分为三类进行计数:缓存查询计数、类方法计数和类查询计数。这些计数针对整个当前命名空间,不受架构选择选项的影响。 对应的方法是`%SYS.PTools.UtilSQLAnalysis`类中的`getSQLStmts()`。 可以使用清除语句按钮删除当前命名空间中收集的所有语句。该按钮调用`clearSQLStatements()`方法。 ### 报告选项 可以检查当前命名空间中选定架构的缓存查询报告,也可以(通过不选择架构)检查当前命名空间中所有缓存查询的报告。可以在此分析中跳过或包括系统类查询、`INSERT`语句和/或`IDKEY`索引。“架构选择”和“跳过选项”复选框是用户自定义的。 指数分析报告选项包括: - 索引使用:此选项获取当前名称空间中的所有缓存查询,为每个查询生成显示计划,并记录每个查询使用每个索引的次数以及名称空间中所有查询对每个索引的总使用量。这可用于显示未使用的索引,以便可以删除或修改这些索引以使其更有用。结果集从最少使用的索引到最常使用的索引排序。 对应的方法是`%SYS.PTools.UtilSQLAnalysis`类中的`indexUsage()`。要导出此方法生成的分析数据,请使用`exportIUAnalysis()`方法。 - 使用表扫描的查询:此选项标识当前名称空间中执行表扫描的所有查询。如果可能,应避免表扫描。表扫描并不总是可以避免的,但是如果一个表有大量的表扫描,那么应该检查为该表定义的索引。通常,表扫描列表和临时索引列表会重叠;修复其中一个会删除另一个。结果集按从最大块计数到最小块计数的顺序列出表格。提供了显示计划链接以显示对帐单文本和查询计划。 对应的方法是`%SYS.PTools.UtilSQLAnalysis`类中的`tableScans()`。要导出此方法生成的分析数据,请使用`exportTSAnalysis()`方法。 - 带临时索引的查询:此选项标识当前名称空间中构建临时索引以解析SQL的所有查询。有时,使用临时索引会有所帮助并提高性能,例如,基于范围条件构建一个小索引,然后InterSystems IRIS可以使用该索引按顺序读取主映射。有时,临时索引只是不同索引的子集,可能非常有效。其他情况下,临时索引会降低性能,例如,扫描`master may`以在具有条件的特性上构建临时索引。这种情况表明缺少所需的索引;应该向与临时索引匹配的类添加索引。结果集按从最大块计数到最小块计数的顺序列出表格。提供了显示计划链接以显示对帐单文本和查询计划。 对应的方法是`%SYS.PTools.UtilSQLAnalysis`类中的`tempIndices()`。要导出此方法生成的分析数据,请使用`exportTIAnalysis()`方法。 - 缺少联接索引的查询:此选项检查当前名称空间中具有联接的所有查询,并确定是否定义了支持该联接的索引。它将可用于支持联接的索引从0(不存在索引)排序到4(索引完全支持联接)。外部联接需要一个单向索引。内联接需要双向索引。默认情况下,结果集只包含`JoinIndexFlag 21 AND %NOINDEX E.Age < 65 ``` ![image](/sites/default/files/inline/images/6_5.png)
文章
姚 鑫 · 七月 9, 2021

第三十二章 XML基础知识概念

# 第三十二章 XML基础知识概念 ## attribute 以下形式的名值对: ```java ID="QD5690" ``` 属性位于元素中,如下所示,一个元素可以有任意数量的属性。 ```xml Cromley,Marcia N. ``` ## CDATA区域 表示不应该验证的文本,如下所示: ```xml ``` 一个`CDATA`(字符数据)区段不能包含字符串`]]`>,因为这个字符串标志着区段的结束。 这也意味着`CDATA`区段不能嵌套。 注意,`CDATA`部分的内容必须符合为XML文档指定的编码,XML文档的其余部分也是如此。 ## comment 不是XML文档主数据的一部分的插入说明。 注释是这样的: ```xml ``` ## content model 对XML元素的可能内容的抽象描述。 可能的内容模型如下: - 空内容模型(不允许有子元素或文本节点) - 简单内容模型(只允许文本节点) - 复杂内容模型(只有子元素) - 混合内容模型(允许子元素和文本节点) 在所有情况下,元素可能有也可能没有属性; 短语内容模型不涉及元素中属性的存在或不存在。 ## default namespace 给定上下文中任何非限定元素所属的名称空间。 添加的默认名称空间没有前缀。 例如: ```xml Isaacs,Rob G. 1981-01-29 ``` 因为这个名称空间声明没有使用前缀,所以``、``和``元素都属于这个名称空间。 注意,下面的XML没有使用默认名称空间,它实际上等同于前面的示例: ```xml Isaacs,Rob G. 1981-01-29 ``` ## DOM 文档对象模型(DOM)是表示XML和相关格式的对象模型。 ## DTD(文档类型定义) 包含在XML文档或外部文件中的一系列文本指令。 它定义了可以在文档中使用的所有有效元素和属性。 dtd本身不使用XML语法。 ## element 一个元素通常由两个标记(一个开始标记和一个结束标记)组成,可能包含文本和其他元素。 元素的内容是这两个标记之间的所有内容,包括文本和任何子元素。 下面是一个完整的XML元素,包含开始标记、文本内容和结束标记: ```xml Cromley,Marcia N. ``` 一个元素可以有任意数量的属性和任意数量的子元素。 空元素可以包含一个开始标记和一个结束标记,也可以只包含一个标记。 下面的例子是等价的: ```xml ``` 在实践中,元素很可能引用数据记录的不同部分,例如 ```xml Barnes,Gerry 1981-04-23 ``` ## entity (在XML文件中)表示一个或多个字符的文本单元。 一个实体有以下结构: ```xml &characters; ``` ## global element 全局元素和局部元素的概念适用于使用名称空间的文档。 全局元素的名称与局部元素的名称放在一个单独的符号空间中。 全局元素是其类型具有全局作用域的元素,即其类型在相应XML模式的顶层定义的元素。 作为``元素的子元素的元素声明被认为是全局声明。 任何其他元素声明都是局部元素,除非它通过ref属性引用全局声明,这实际上使它成为全局元素。 属性可以是全局的,也可以是局部的。 ## local element 不是全局的XML元素。 局部元素不显式属于任何名称空间,除非元素是限定的。 参见限定元素和全局元素。 ## namespace 名称空间是为标识符定义域的惟一字符串,以便基于xml的应用程序不会混淆一种类型的文档和另一种类型的文档。 它通常以URL(统一资源位置)的形式给出一个URI(统一资源指示器),它可能与实际的web地址对应,也可能不对应。 例如,`“http://www.w3.org”`是一个名称空间。 使用以下语法之一包含命名空间声明: ```xml xmlns="your_namespace_here" pre:xmlns="your_namespace_here" ``` 在这两种情况下,名称空间只在插入名称空间声明的上下文中使用。 在后一种情况下,名称空间与给定的前缀(pre)相关联。 当且仅当元素或属性也有此前缀时,元素或属性就属于该名称空间。 例如: ```xml Ravazzolo,Roberta X. 1943-10-24 ``` 命名空间声明使用`s01`前缀。 ``元素也使用了这个前缀,所以这个元素属于这个名称空间。 但是,``和``元素并不显式地属于任何命名空间。 ## 处理指令(PI) 一种指令(在序言中),旨在告诉应用程序如何使用XML文档或如何处理它。 一个例子; 这将样式表与文档关联起来。 ```xml ``` ## prolog XML文档中根元素之前的部分。 序言以XML声明(指示使用的XML版本)开始,然后可能包括DTD声明或模式声明以及处理指令。 (从技术上讲,不需要`DTD`或模式。 此外,从技术上讲,可以将两者放在同一个文件中。) ## root, root element, document element 每个XML文档都要求在最外层只有一个元素。 这称为根元素、根元素或文档元素。 根元素在序言之后。 ## qualified 如果显式地将元素或属性分配给名称空间,则该元素或属性是限定的。 考虑下面的例子,其中``的元素和属性是不限定的: ```xml Frost,Sally O. 1957-03-11 ``` 在这里,名称空间声明使用`s01`前缀。 没有默认的命名空间。 ``元素也使用了这个前缀,因此该元素属于这个名称空间。 ``和``元素或``属性没有前缀,因此它们不显式属于任何名称空间。 相反,考虑以下情况,其中``的元素和属性是限定的: ```xml Frost,Sally O. 1957-03-11 ``` 在本例中,``元素定义了一个默认名称空间,该名称空间应用于子元素和属性。 注意:XML模式属性`elementFormDefault`属性和`attributeFormDefault`属性控制在给定的模式中元素和属性是否被限定。 在InterSystems IRIS XML支持中,使用类参数来指定元素是否限定。 ## schema 一种为一组XML文档指定元信息的文档,可作为DTD的替代。 与DTD一样,可以使用模式来验证特定XML文档的内容。 对于某些应用程序,XML模式提供了与`dtd`相比的几个优势,包括: - XML模式是有效的XML文档,因此更容易开发操作模式的工具。 - XML模式可以指定一组更丰富的特性,并包含值的类型信息。 形式上,模式文档是符合W3 XML模式规范的XML文档(在`https://www.w3.org/XML/Schema`)。 它遵守XML规则,并使用一些额外的语法。 通常,文件的扩展名是`.xsd`。 ## style sheet 用XSLT编写的文档,描述如何将给定的XML文档转换为另一个XML或其他“人类可读”的文档。 ## text node 包含在开始元素和相应结束元素之间的一个或多个字符。 例如: ```xml sample text node ``` ## type 对数据解释的限制。 在XML模式中,每个元素和属性的定义对应于一个类型。 类型可以是简单的,也可以是复杂的。 每个属性都有一个简单类型。 简单类型还表示没有属性和子元素(只有文本节点)的元素。 复杂类型表示其他元素。 下面的模式片段展示了一些类型定义: ```xml ``` ## unqualified 如果没有显式地将元素或属性分配给名称空间,则该元素或属性是非限定的。 ## well-formed XML 遵循XML规则的XML文档或片段,例如有一个结束标记来匹配一个开始标记。 ## XML declaration 指示给定文档中使用的XML版本(以及可选的字符集)的语句。 如果包含,它必须是文档中的第一行。 一个例子: ```xml ``` ## XPath XPath (XML路径语言)是一种基于XML的表达式语言,用于从XML文档中获取数据。 结果可以是标量,也可以是原始文档的XML子树。 ## XSLT XSLT(可扩展样式表语言转换)是一种基于XML的语言,用于描述如何将给定的XML文档转换为另一个XML或其他“人类可读的”文档。
文章
Lilian Huang · 十月 24, 2022

自动部署自适应分析Adaptive Analytics

当我们使用IRIS时,我们通常有能力快速的部署一个现成使用的BI基础模块(数据、分析立方体和IRIS BI仪表盘)。当我们开始使用Adaptive Analytics时,我们通常希望有同样的功能。Adaptive Analytics拥有我们需要的所有工具。文档中包含了对如何使用开放的网络API的描述。用户界面和引擎之间的所有交互也都是通过内部的Web API发生的,并且可以被发射出来。 有必要将这两个过程自动化:在容器中部署Adaptive Analytics和直接部署到服务器系统。为此,最简单的方法是使用bash脚本来处理API。我们唯一需要的第三方应用程序是一个名为jq的JSON文件解析器。你可以使用以下命令来安装它: apt update apt install -y jq 首先,我们需要登录以获得一个API访问令牌。这个令牌也适用于引擎本身的方法。我们必须将访问令牌保存在一个变量中,因为现在我们几乎在每个请求中都需要它。对于一个标准的登录和密码admin/admin,该命令将看起来像这样: TOKEN=$(curl -u admin:admin --location --request GET 'http ://localhost:10500/default/auth') 接下来,我们需要一个活跃的引擎来与API交互。如果没有许可证检查,引擎是不可用的,所以我们要为它提供许可证。Web API没有这个选项,所以我们必须使用引擎命令: curl --location --request PUT 'http://127.0.0.1:10502/license' --header 'Content-Type: application/json' --data-binary "@$license" 许可证变量包含许可证文件的路径。当我们在不同的平台和不同容量的PC上做一些测试时,我们注意到一个奇特的现象。在系统启动脚本完成后,Adaptive Analytics可能会出现这样的情况:服务还没有启动,但初始化脚本已经将控制权交给了我们的脚本。为了确保引擎启动和运行,我们在一个循环中组织发送许可证,直到我们收到许可证已被接受的消息。这样做的代码看起来像这样: RESPONSE="1" TIME=1 echo "Waiting for engine ......." while [ "$RESPONSE" != "200 OK" ] do for license in /root/license/* do RESPONSE =$(curl --location --request PUT 'http://127.0.0.1:10502/license' --header 'Content-Type: application/json' --data-binary "@$license" | jq -r '.status.message') sleep 1s done echo "$TIME seconds waiting" TIME=$(($TIME + 1)) done echo "Engine started" 打印时间变量对于调试启动问题很有用。正如你所看到的,我们在一个特定的文件夹中循环浏览文件,以便不被文件名所束缚。我们将在未来再次使用这种方法。 现在我们可以与web API互动,我们可以将我们的项目上传到Adaptive Analytics。我们使用下面的代码将所有放在指定文件夹中的项目发送到引擎: for cube in /root/cubes/* do sleep 1s curl --location --request POST "http://localhost:10500/api/1.0/org/default/"Authorization: Bearer $TOKEN" --header "Content-Type: application/xml" --data-binary "@$cube" sleep 5s done 如果我们希望我们的项目可以被BI系统使用,我们必须发布它们。幸运的是,在API中也有这样的方法。由于项目在导入时得到一个随机的唯一ID,首先,我们应该把这些ID解析成一个变量: PROJECTS_ID=$(curl --location --request GET "http://localhost:10500/api/1.0/org/default/projects" -- header "Authorization: Bearer $TOKEN" | jq -r '.response[].id') 然后我们需要遍历变量中的所有项目并发布它们。 for project in $PROJECTS_ID do curl --location --request POST "http://localhost:10500/api/1.0/org/default/project/$project/publish" --header "Authorization: Bearer $TOKEN" sleep 1s done 现在我们需要告诉Adaptive Analytics如何连接到IRIS,以及应该如何命名连接,以便我们的项目能够接收它们。在API中有一个方法,但是它有不同的URL地址。事实证明,这是一个有文档记录的引擎方法。所以如果我们想使用它,我们需要访问一个不同的端口。该方法接受JSON文件形式的连接信息,但不可能从Adaptive Analytics以相同的格式导出它。API根据请求返回给我们的JSON文件有一些额外/缺失的字段。以下是用于IRIS连接的JSON文件模板: { "platformType": "iris", "name": "name_you_want", "connectionId": "name_you_want", "overrideConnectionId":true, "extraProperties": { "namespace": "Your_namespase ", "udafMode": "customer_managed" }, "aggregateSchema": "ATSCALE", "readOnly":false, "isImpersonationEnabled": false, "isCanaryAlwaysEnabled": false, "isPartialAggHitEnabled": false, "subgroups": [ { " name": "iris", "hosts": "iris", "port": 1972, "connectorType": "iris", "username": "Your_username", "password":"Your_password", "isKerberosClientEnabled": false , "queryRoles": [ "large_user_query_role", "small_user_query_role", "system_query_role" ], "extraProperties": {}, "connectionGroupsExtraProps": {} } ] } 值得一提的是“udafMode”参数:如果在IRIS中安装了UDAF,我们可以将其设置为“customer_managed”,如果没有安装UDAF,则设置为“none”。要了解更多关于UDAF是什么、为什么需要它以及在安装它时可能会遇到什么缺陷的信息,请查看这篇()文章。 当我们为所有连接准备好这样的文件时,我们应该从文件夹中加载它们: for connection in /root/connections/* do sleep 1s curl --location --request POST "http://localhost:10502/connection-groups/orgId/default" - -header "Authorization: Bearer $TOKEN" --header "Content-Type: application/json" --data-binary "@$connection" sleep 5s done 接下来,我们希望有机会将Logi Report Designer连接到Adaptive Analytics实例。这就是为什么我们需要模拟Adaptive Analytics初始化的第一个步骤之一。因此,我们为hive连接设置端口: sleep 1s curl --location --request POST "http://localhost:10502/organizations/orgId/default" --header "Authorization : Bearer $TOKEN" --header "Content-Type: application/json" --data-raw '{"hiveServer2Port": 11111}' 为了不超过IRIS社区版本中的连接限制,我们必须限制Adaptive Analytics的同时连接数量。根据我们的经验,为了防止在测试场景中超过限制,在5个连接中保留3个是值得的。我们使用下面的代码模拟更改设置: for settings_list in /root/settings/* do curl --location --request PATCH 'http://localhost:10502/settings' --header "Authorization: Bearer $TOKEN" --header "Content-Type: application/json" --data-binary "@$settings_list" done 在文件夹下的JSON文件中,写入如下内容: { "name": "bulkUpdate", "elements": [ { "name": "connection.pool.group.maxConnections", "value": "3" }, { "name": "connection.pool.user.maxConnections", "value": "3 " } ] } 根据我使用Adaptive Analytics的经验,这种限制对性能影响不大。 修改设置后,需要重新启动引擎: curl --location --request POST 'http://localhost:10500/api/1.0/referrerOrg/default/support/service-control/engine/restart' 现在我们有了自动准备Adaptive Analytics工作所需的一切。为方便您,我们已将上述脚本发表在: https://github.com/intersystems-community/dc-analytics/blob/master/atscale-server/entrypoint.sh 在这个存储库中,您还可以找到使用聚合更新计划的脚本。聚合是在IRIS中配置UDAF时由Adaptive Analytics生成的特殊表。这些表存储聚合查询的缓存结果,从而加快了BI系统的工作速度。Adaptive Analytics有一个更新聚合的内部逻辑,但是自己控制这个过程要方便得多。 我们曾经遇到过这样的情况:这些表存储的数据已经过时好几天了。相应地,Adaptive Analytics返回的值与真实值明显不同。为了避免这种情况,我们在更新BI系统中的数据之前设置了每日的聚合更新。 您可以在Adaptive Analytics的web界面中按每个立方体配置更新。然后,您将能够使用脚本将时间表导出和导入到另一个实例,或者使用导出的时间表文件作为备份。如果不想单独配置每个数据集,还可以在应用程序中找到一个脚本,将所有数据集设置为相同的更新计划。 最后一件便于自动化的事情是在开发阶段从Adaptive Analytics备份项目本身。为了设置这种自动化,我们编写了另一个OEX应用程序,它每天一次将所选项目保存到Git存储库中。 最后一件便于自动化的事情是在开发阶段从Adaptive Analytics备份项目本身。为了设置这种自动化,我们编写了另一个OEX应用程序OEX application,它每天一次将所选项目保存到Git存储库中。 初学者指南描述了在新报表文件中创建小部件的过程。如果使用“插入”选项卡或“组件”窗口,也可以将图表或表格插入到现有的报表中。
文章
Claire Zheng · 八月 17, 2021

FHIR标准和国际基于FHIR的互联互通实践(7):国际互联互通实践

国际互联互通的需求是在不断增长,这跟咱们国内的情况是非常类似的。这些年美国在互联互通领域的政策跟实践还是不少的,比如大家可能听到过包括“有意义的使用(Meaningful Use)”,“21世纪治愈法案(21st Century Cures Act)”,还有更多的政策上的驱动。这里先介绍一下“有意义的使用(Meaningful Use)”。 “有意义的使用(Meaningful Use)”其实源于2009年奥巴马签署的美国复兴与投资法案。“有意义的使用(Meaningful Use)”是它的俗称,标准的名称应该是“电子健康档案激励计划”。电子健康档案激励计划提出了很多使用标准,这些标准促进了认证电子病历的使用,分成了三个阶段:从2011年开始是第一阶段,第一阶段使用电子健康档案的技术来做数据的获取共享。之后在2014年做了第二个阶段,第二阶段来强调护理协调和患者信息的交流。第三个阶段是从2017年开始,它的目标是做一些更高级应用,例如电子处方临床学的支持。不过在“有意义的使用(Meaningful Use)”的过程中间,遇到了很多问题,因为它专注在电子病历的认证和使用上,很多用户抱怨已经被认证过的电子病历的能力是不足的。 所以在2018年4月份的时候, CMS将“有意义的使用(Meaningful Use)”,从“电子档案激励计划”改成了“促进互操作性计划”。在2018年的时候,“有意义的使用(Meaningful Use)”相当于是终结了。其替代者,也就是“促进互通的计划”,它现在还在执行。 上图是2020年的评分的标准,大家可以看到红框里面标出来的都是主要的评分项,基本上都集中在互联互通的能力之上的。现在美国正在执行的这些互操作性的政策,其实主要是美国的21世纪治愈法案(21st Century Cures Act),这个法案下有很多的子法律条款,提出了更具体、更可操作、更有计划性的互操作路线。 我挑几个给大家做个介绍。 首先第一个就是可信交换框架和共同协议草案,它设定了4个目标。这4个目标都是针对于提高医疗结果为目标的,它在这个目标里定义了未来的分阶段来进行设定目标,未来4年的目标就是建立所谓的“学习健康体系”。“学习健康体系”实现了临床、科研、公卫、患者个人、医保等各个利益方的互操作能力的健康信息体系。要实现这个目标,美国有一些具体的行动。行动之一就是ONC发布了一个“美国互操作核心数据集(U.S. Core Data for Interoperability)”。这个数据集跟咱们卫生数据集有点像,但是它包含的是数据类、数据元以及规范,建议了术语绑定的数据集。 这个数据集其实挺小的。这个数据集目前现在有两个版本,第一版本已经发布了。 上图包括了尚未发布的第二版的核心数据集,总共只有18个种类,这些数据集比我们的数据集要小得多。美国联邦政府现在要求所有的医疗机构都必须以HL7 FHIR R4的标准来共享交换核心数据集里面所有的患者数据。 还有另外一个法案,最新的“互操作能力和患者访问法规(The Interoperability and Patient Access final rule [CMS-9115-F])”。法规要求每个医院和保险公司都要开放针对于患者个人的数据查询的能力,需要使用FHIR的API,使用FHIR 4.0.1的标准来作为数据交换的基础,患者可以自己自由选择第三方的应用来查看、分享自己的数据。 这个法规从今年(2021年)1月1号开始实施的,目标要在三年之内能够完成。新一届的美国政府还要求所有的机构全都要全面的支持FHIR R4的API和FHIR的美国核心 Profile(总共25个Profile)。所以 FHIR在美国处于一个被快速采纳的阶段。 当然还有其他的机构,除了美国政策驱动之外,行业标准开发组织也在积极采用FHIR,例如说IHE。目前IHE总共开发了251个场景用例,现在基于FHIR已经达到了41个。IHE不仅在使用FHIR资源作为内容的格式,它也在使用FHIR的API来替代原来的互操作实现的方式。 FHIR在科研领域也大展身手。目前用于前瞻性临床研究的这些临床数据,其实很大程度上还是人工收集和分析出来的,需要手动查看临床数据图表、收集这些数据,增加了研究时间并且创造了很多出错的机会。通过FHIR标准,能够用FHIR标准化的协议和API使数据分析流程更加现代化。 “火神计划(Project Vulcan)”就是针对于科研发展的计划,这个计划的目标是将临床研究跟临床护理能够连接起来,让整个利益相关方能够聚集在一起,以弥合临床护理和临床研究之间的现有差距,战略性地连接行业合作,最大限度地利用集体资源,并提供集成的工具和资源。 这个项目在2019年9月份启动,目前有三个方向: 第一个就是建立表型数据的交换标准,其目标是使用FHIR在各种环境中使用和交换匿名化的患者病历级信息,这需要对FHIR资源进行进一步开发; 第二个是通过FHIR获取电子病历系统的数据,并直接生成科研所需数据的能力。当前正在和FDA协作处理药品数据; 第三个是采用FHIR资源描述科研活动计划。当前目标是将“临床数据交换标准联盟”的ODM-XML格式的活动计划转换为FHIR标准。 “火神加速器计划(HL7 Accelerator Program: Project Vulcan)”是一个比较典型的、能够体现出美国的标准合作应用模式的计划。 这个模式是将各个利益方都纳入进来。在支持科研和药品研发的计划中间包含了很多利益方,比如说标准开发组织HL7国际,政府代表FDA,科研机构(比如约翰逊霍普金斯医学院),当然还有行业团体(比如临床研究组织协会),很多的技术厂商(包括InterSystems公司)。通过多方参与来加速标准在科研和药品研发中间的落地。 其他国家也有很多启动了FHIR标准的互联互通的计划——例如沙特,沙特采用FHIR来进行排班计划,德国试图建立FHIR文档仓库上的所有能力,英国在使用FHIR来管理儿童的健康预警。 除此之外,有一个“全球数字健康伙伴关系”联盟,这是一个由30个国家和地区,以及世卫组织组成的合作的项目,中国香港特别行政区也在里面。它为全球参与者提供了交流数据共享、电子健康记录、电子处方、患者访问等最佳实践的机会。上图汇总了这些参与方使用互操作标准的情况。可以看到FHIR的标准的采纳度已经达到了17个,很快就会赶上最流行的V2,V2现在是19个。 以上全球对于互操作标准的采纳以及FHIR使用情况的简单介绍,如需了解更多,欢迎留言与我们交流!
文章
Michael Lei · 四月 9

Open AI 与 IRIS 集成 - 文件管理

人工智能不仅限于通过带有说明的文本生成图像,或通过简单的指示创建叙事。您还可以制作图片的变体,或为已有图片添加特殊背景。此外,您还可以获得音频转录,无论其语言和说话者的语速如何。让我们来分析一下文件管理是如何工作的。 问题描述 在分析 OpenAI 有关需要将文件作为输入值的方法的信息时,必须使用 multipart/form-data 提供参数。 在 IRIS 中,我们知道如何使用 JSON 内容创建对 POST 方法的调用。但在这种情况下,使用带有 Base64 格式文件内容的参数并不实用。 要在多址/表单数据(multipart/form-data)中包含文件内容,必须使用%Net.MIMEPart.类。 要在我们的调用中包含文件,应创建一个与类对象 %Net.MIMEPart 相关联的 Content-Disposition 标头 set content = ##class(%Net.MIMEPart).%New() set contentDisposition = "form-data; name="_$CHAR(34)_"image"_$CHAR(34) set contentDisposition = contentDisposition_"; filename="_$CHAR(34)_fileName_$CHAR(34) do content.SetHeader("Content-Disposition",contentDisposition) 由于我们使用请求类来保留进程的值,因此我们必须将 Base64 内容转换为流,以构成内容的主体。 我们可以使用StreamUtils实用程序将 Base64 转换为流。 注意:"pImage"变量包含文件内容的 Base64 字符串。 Do ##class(HS.Util.StreamUtils).Base64Encode(pImage, .tStream) Set content.Body = tStream 不过,在 2023 年全球峰会上,我有幸从 InterSystems 专家那里学到了一个更好的技巧。他告诉我,这种执行方法比 StreamUtils 更有效,因为 StreamUtils 最后会循环读取字符串并记录到 Stream 中。这个解决方案就像使用 JSON 并将其转换为 Stream 的 Get 一样简单。 set contentfile = {} set contentfile.file = pImage set content.Body = contentfile.%Get("file",,"stream<base64") 在调用中包含了所需的所有参数后,我们就可以创建一个新的 MIMEPart 类来封装部件了。 Set rootMIME = ##class(%Net.MIMEPart).%New() do rootMIME.Parts.Insert(content) set writer = ##class(%Net.MIMEWriter).%New() set tSC = writer.OutputToStream(tHttpRequest.EntityBody) set tSC = writer.WriteMIMEBody(rootMIME) Set tContentType = "multipart/form-data; boundary="_rootMIME.Boundary set tSC = ..Adapter.SendFormDataArray(.tHttpResponse, "POST", tHttpRequest,,,url) 这就是我们如何将文件内容发送到我们在 OpenAI 中需要的方法。 Image files图像文件 图像方法允许您发送图片并进行变化。由于所有插图都必须是 PNG 格式,因此当我们以 Base64 格式指明文件内容时,文件名会随机生成,并带有 PNG 扩展名。下面是一个如何更改照片的示例。 Original Variation 正如你所看到的,程序以自己的方式解释指令。它认为公司的标志是一个圆圈,所以用另一个圆圈代替了它。它还发现办公室有一扇玻璃门,于是用另一扇玻璃门代替,但暂时用砖墙代替。此外,它还修改了衬衫的颜色,并改变了男子手臂的位置。此外,OpenIA 还允许您通过提供一个蒙版来编辑图像,蒙版上有您想要插入提示内容的区域。利用同一幅图像,我应用了一个去掉图像背景的蒙版。 Original Mask 当我要求它把我传送到牙买加海滩时,得到了如下结果: 现在,下次见到亲朋好友时,您就可以炫耀自己的假期了 😊 Image图像 Endpoint: POST https://api.openai.com/v1/images/variations 它允许你对已有的图像进行修改。由于它不需要提示您要如何修改,因此我们必须相信人工智能的品味,它会如何解释这张图片。此外,我们还可以定义大小和返回结果的方式,无论是通过链接还是 Base64 格式的内容。 输入参数如下: image: 必选 在这里,您要提及要转换的图像文件。 n: 可选. 默认为 1 在此区域,您可以决定生成图像的最大数量。(使用 1 到 10 之间的数字)。 size: 可选. 默认 1024x1024 定义图像大小,其数值必需为 “256x256”, “512x512”, 或者 “1024x1024”. response_format: 可选.默认是“url” 这个参数是关于您希望如何返回生成图像的格式。此处的值应为 "url "或 "b64_json"。 Endpoint: POST https://api.openai.com/v1/images/edits 它可以让你修改现有的图片,根据掩码文件,按照提示创建图片。此外,我们还可以指定尺寸和返回结果的方式,无论是通过链接还是 Base64 格式的内容。输入参数如下: image: 必选 如上. mask: 必选 这部分是关于所应用的蒙版图像文件. n: 可选,默认 1 如上 size: 可选,默认 1024x1024 如上 response_format: 可选. 默认是 “url” 如上 Audio files声音文件 OpenAI 管理的不仅仅是图像。我们还可以使用音频文件来获取所提供录音的转录或翻译。这种方法使用 Whisper 模型,可以区分专有名词、品牌和俚语,从而提供正确的转录和翻译。例如,将 "微型机器 "作为一个品牌来谈论,与将 "微型机器 "作为一个普通名词翻译成西班牙语是不一样的。下面的例子是对 80 年代一个著名广告插播的转录: 因此,指示 Whisper 为我们转录音频的结果如下: { "text": "This is the Micromachine Man presenting the most midget miniature motorcade of micromachines. Each one has dramatic details, terrific trim, precision paint jobs, plus incredible micromachine pocket playsets. There's a police station, fire station, restaurant, service station, and more. Perfect pocket portables to take anyplace. And there are many miniature playsets to play with and each one comes with its own special edition micromachine vehicle and fun fantastic features that miraculously move. Raise the boat lift at the airport, marina, man the gun turret at the army base, clean your car at the car wash, raise the toll bridge. And these playsets fit together to form a micromachine world. Micromachine pocket playsets, so tremendously tiny, so perfectly precise, so dazzlingly detailed, you'll want to pocket them all. Micromachines and micromachine pocket playsets sold separately from Galoob. The smaller they are, the better they are." } 多么神奇! 你觉得呢? 之所以能取得上述成果,是因为 Whisper 模型接受了训练。我们可以从 OpenAI 页面提供的下图中看到一些相关信息。 更多信息可以访问 https://openai.com/research/whisper 请记住,告知程序文件名至关重要,因为服务需要知道它正在处理的文件类型(如 WAV、MP3、OGG 等)。由于我们在调用中只包含 Base64 内容,因此还必须指明文件扩展名,以便用随机文本和建议的扩展名创建文件名。例如,St.OpenAi.Msg.Audio.AudioRequest 消息的 "类型 "属性可显示音频的种类: MP3、OGG、WAV、FLAC 等。 Endpoint: https://api.openai.com/v1/audio/transcriptions 通过这种方法,您可以将音频内容转录为有声语言。 输入参数如下: file: 必要 在这里,您可以指定要转录的音频文件(而不是文件名)。它支持以下格式: FLAC、MP3、MP4、MPEG、MPGA、M4A、OGG、WAV 或 WEBM model: 必要 用于转录的模型。目前只有 "whisper-1 "可用 language: 可选. 默认是音频语言. 如果指定的话,根据 ISO-639-1,可以提高准确率和延时. prompt: 可选. 这是一段可选的文字,用于引导模型的风格或延续上一段音频。此处的信息必须与音频语言一致。. response_format. 可选,默认为 “json”. 在这一部分中,您要明确转录输出的格式。请使用以下选项之一: "json"、"text"、"verbose_json"。 temperature: 可选,默认为 0. 采样温度应介于 0 和 1 之间。 0.8 等较高值会使输出更加随机,而 0.2 等较低值则会使输出更加集中和确定。如果设置为 0,模型将使用对数似然自动提高温度,直到达到特定阈值。 本方法的文档请参考 https://platform.openai.com/docs/api-reference/audio/createTranscription<. Endpoint: https://api.openai.com/v1/audio/translations 此方法可将音频内容翻译成英语。输入参数如下: file: 必要 它是您要翻译的音频文件(而不是文件名)。它支持以下格式: FLAC、MP3、MP4、MPEG、MPGA、M4A、OGG、WAV 或 WEBM model: 必要. 如上. prompt: 可选l. 这是一段可选的文字,用于引导模型的风格或延续上一段音频。此处的信息必须使用英语。 response_format. 可选. 默认是 “json”. 在这里,您可以用以下选项之一决定转录输出的格式: "json"、"text"、"verbose_json"。 temperature: 可选. 默认为 0. 如上 更多文档请查阅 https://platform.openai.com/docs/api-reference/audio/createTranscription. 下一步? 由于 OpenAi 在不断发展,下一次迭代将是将文本转换为音频的方法,以及其他一些新功能。如果您喜欢这篇文章,请记得点个 "赞"。
文章
姚 鑫 · 四月 27, 2021

第七章 解释SQL查询计划

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

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

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

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

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

第七十三章 SQL命令 SET OPTION

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

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

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