搜索​​​​

清除过滤器
文章
王喆 👀 · 九月 13, 2022

IRIS快速查询服务思路分享

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

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

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

FHIR 用例集: 打破数字医疗壁垒,实现高质量发展

FHIR 用例集: 打破数字医疗壁垒,实现高质量发展 --促进互联互通,改进工作流程,提高数据洞察 简介 HL7® FHIR®(快速医疗互操作性资源)是以电子方式访问、交换和管理医疗信息的国际标准。与以往的标准不同,FHIR 可让帮助行业从业者轻松构建创新应用程序,有效地收集、汇总和分析来自不同来源的各种医疗保健和管理数据。医疗机构、社保/保险公司、政府机构、生命科学公司、医疗设备制造商和医疗科技等多种主体利用 FHIR 来简化信息流、提高数据洞察力、改善临床效果和业务成果。 FHIR 基于 JSON、HTTP 和 REST 等流行的网络技术。有了 FHIR,没有医疗信息化背景的软件开发人员也能使用熟悉的开发工具和开源技术,快速、轻松地满足政府机构、临床医生、研究人员、医疗行业从业者以及各类市场主体的数据需求。 FHIR 是一种灵活、适应性强的医疗数据模型,可轻松定制,以实现各种用例的互操作性。FHIR 由称为 "资源 "的离散、可计算的数据对象组成,以实现最佳效率。通过 FHIR 资源,应用程序可以访问单个医疗记录元素,而无需检索摘要文档中包含的所有数据。 本文回顾了 FHIR 的实际应用,并提供了 InterSystems 客户如何使用 FHIR 连接不同系统、加速数字化转型和提高数据洞察力的真实案例。 FHIR 商机无限 FHIR 正在改变医疗健康数据的访问和交换。无论您是为政府、医疗机构、公共卫生机构、保险公司还是厂商工作,FHIR 都能帮助您高效地获取、检索和共享来自电子病历系统、智能医疗设备、可穿戴设备、临床试验和公共卫生监测系统等不同来源的医疗数据。 当前应用和未来的 FHIR 用例FHIR支持实现大量的不同业务场景。您可以在各种部署场景中将 FHIR 用于各种目的。下面的列表总结了 FHIR 在不同行业领域的一些当前应用和潜在的未来用例。 医疗机构 应用场景: 患者数据访问 API机会:可以基于FHIR资源和技术框架实现卫健委互联互通三年攻坚计划以及国家数据局"数据要素x医疗行业"三年行动计划中提到的相关电子健康档案共享、检验检查互认、医疗行业数据要素流通、交易等战略目标,通过基于标准的 (FHIR) API 让患者以程序化的方式访问其健康数据(病史、化验结果、治疗计划等),以及未来可能的全国统一医疗健康档案超级APP(患者端)。 应用场景: 临床决策支持机会: 使用 FHIR 改善临床决策系统的洞察力。将实时电子病历数据安全传输到第三方系统进行分析并返回建议,帮助临床医生做出明智决策。与以往的标准和方法不同,使用 FHIR,您可以将临床决策支持功能直接嵌入电子病历,以简化流程。 应用场景: 医疗机构与支付方(医保/保险公司)的合规数据交换 机会: 利用 FHIR 自动化医疗机构与支付方之间的数据交换。消除资源密集、耗时的人工流程(降低飞行检查和审计成本)。允许医疗机构直接将电子病历数据转发给支付方,无需人工干预。 使用案例: 临床试验和研究机会: 使用 FHIR 无缝共享临床试验招募和分析所需的患者数据,加快临床研究进程。 设备制造商、医疗科技公司和应用开发商 用例:远程医疗和远程监控机会: 使用 FHIR 可将患者数据从家用医疗设备安全地传输给医疗服务提供者,以便他们有效地远程监控和管理患者。 用例: 移动医疗应用程序机遇: 患者可以在手机端访问在不同医院治疗的电子病历,并且确保患者数据的隐私和安全。 用例: 慢性病管理应用程序机会: 使用 FHIR 在医疗服务提供者之间无缝共享患者数据,以实现一致的监控和协调的护理计划。 用例: 药物管理应用程序机会: 为临床医生和护理人员创建多功能药物管理应用程序。使用 FHIR 在区域全民健康信息平台之间高效共享处方信息、用药计划和药房记录。 生命科学公司、政府机构和付款人 用例:健康信息交换机会: 使用 FHIR,政府、公共卫生、保险公司等可高效开展电子健康档案/电子病历共享调阅数据,以进行质量评估、护理差距识别、理赔裁定以及开展潜在的数据交易等。 用例: 护理计划机会: 利用 FHIR,让跨机构护理团队--医生、家庭医疗工作者、社区护理人员、家庭成员等--能够无缝交换信息。让不同的医疗保健系统进行有效沟通。确保所有护理团队成员都能获得最新的患者信息。 用例: 公共卫生报告机会: 使用 FHIR 有效地汇总和共享患者数据,以进行监控和人口健康管理,从而简化公共卫生报告。利用电子病历批量检索功能。(注:该功能自 2022 年起已成为所有美国电子病历系统的强制性要求,在WHO、OECD、欧盟、亚洲、港澳台等地区也正在逐步推广普及) 以上只是部分FHIR的用例,有了FHIR,从业者可以打开无限想象空间,创建丰富多样、互联互通的数字医疗创新应用。
文章
姚 鑫 · 四月 21, 2021

第四章 缓存查询(二)

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

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

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

第四十六章 Caché 变量大全 ^$GLOBAL 变量

# 第四十六章 Caché 变量大全 ^$GLOBAL 变量 提供有关全局变量和进程私有全局变量的信息。 # 大纲 ```java ^$|nspace|GLOBAL(global_name) ^$|nspace|G(global_name) ^$||GLOBAL(global_name) ^$||G(global_name) ``` # 参数 - `|nspace|` 或 `[nspace]` - 可选-扩展SSVN引用,可以是显式名称空间名称,也可以是隐含名称空间。必须计算为带引号的字符串,该字符串括在方括号(`[“nspace”]`)或竖线(`|“nspace”|`)中。命名空间名称不区分大小写;它们以大写字母存储和显示。 - global_name 计算结果为包含无下标全局名称的字符串的表达式。全局名称区分大小写。使用`^$||global()`语法时,与进程专用全局名称相对应的无下标全局名称:`^a`表示`^||a`。 # 描述 可以将`^$GLOBAL`用作`$DATA`、`$ORDER`和`$QUERY`函数的参数,以返回有关当前名称空间(默认名称空间)或指定名称空间中是否存在全局变量的信息。还可以使用`^$global`返回有关存在进程私有全局变量的信息。 ## 进程私有全局变量 可以使用`^$global`获取有关所有命名空间中是否存在进程私有全局变量的信息。可以将进程专用全局的查找指定为`^$||global`或`^$|“^”|global`。 例如,要获取有关进程私有全局`^||a`及其后代的信息,可以指定`$DATA(^$||global(“^a”))`。进程私有全局变量不是特定于名称空间的,因此在定义进程私有全局变量时,无论当前名称空间如何,此查找都会返回有关`^||a`的信息。 请注意,`^$GLOBAL`不支持在`GLOBAL_NAME`本身中指定进程专用全局语法。使用进程专用全局语法指定`GLOBAL_NAME`会导致``错误。 # 参数 ## nspace 此可选参数允许`^$GLOBAL`查找在另一个命名空间中定义的`GLOBAL_NAME`。这称为扩展SSVN参考。可以显式地将命名空间名称指定为带引号的字符串文字、变量,也可以通过指定隐含的命名空间来指定。命名空间名称不区分大小写。可以使用方括号语法`[“user”]`或环境语法`|“user”|`。Nspace分隔符前后不允许有空格 可以使用以下方法测试是否定义了命名空间: ```java DHC-APP>WRITE ##class(%SYS.Namespace).Exists("USER") 1 DHC-APP>WRITE ##class(%SYS.Namespace).Exists("LOSER") 0 ``` 以使用`$NAMESPACE`特殊变量来确定当前名称空间。更改当前名称空间的首选方式是新建`$NAMESPACE`,然后设置`$NAMESPACE=“nspace ename”`。 ## global_name 计算结果为包含无下标全局名称的字符串的表达式。全局变量区分大小写。 - `^$global(“^a”)`:`global_name“^a”`在当前名称空间中查找此全局名称及其后代。它不查找进程私有全局`“^||a”`。 - `^$|"USER"|GLOBAL("^a")`:global_name `"^a"`在`“user”`名称空间中查找此全局名称及其后代。它不查找进程-私有全局`"^||a"`。 -` ^$||GLOBAL("^a")`:global_name `"^a"`在所有名称空间中查找进程私有全局`"^||a"`及其后代。它不查找全`"^a"`。 # 示例 以下示例显示如何将`^$GLOBAL`用作`$DATA`、`$ORDER`和`$QUERY`函数的参数。 ## 作为`$DATA`的参数 `^$GLOBAL`作为`$DATA`的参数返回一个整数值,表示指定的全局名称是否作为`^$GLOBAL`节点存在。下表显示了`$DATA`可以返回的整数值。 Value | Meaning ---|--- 0| 全局名称不存在 1| 全局名称是包含数据但没有子代的现有节点。 10| 全局名称是没有数据但具有子代的现有节点。 11| 全局名称是包含数据的现有节点,并且具有子代。 下面的示例测试当前命名空间中是否存在指定的全局变量: ```java /// d ##class(PHA.TEST.SpecialVariables).GLOBAL() ClassMethod GLOBAL() { KILL ^GBL WRITE $DATA(^$GLOBAL("^GBL")),! SET ^GBL="test" WRITE $DATA(^$GLOBAL("^GBL")),! SET ^GBL(1,1,1)="subscripts test" WRITE $DATA(^$GLOBAL("^GBL")) } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).GLOBAL() 0 1 11 ``` 下面的示例测试user命名空间中是否存在指定的全局变量: ```java /// d ##class(PHA.TEST.SpecialVariables).GLOBAL1() ClassMethod GLOBAL1() { SET $NAMESPACE="USER" SET ^GBL(1)="test" SET $NAMESPACE="%SYS" WRITE $DATA(^$|"USER"|GLOBAL("^GBL")) } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).GLOBAL1() 10 ``` 下面的示例测试任何命名空间中是否存在指定的进程私有全局变量: ```java /// d ##class(PHA.TEST.SpecialVariables).GLOBAL2() ClassMethod GLOBAL2() { SET $NAMESPACE="USER" SET ^||PPG(1)="test" SET $NAMESPACE="%SYS" WRITE $DATA(^$||GLOBAL("^PPG")) } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).GLOBAL2() 10 ``` ## 作为`$ORDER`的参数 `$ORDER(^$|nspace|GLOBAL( global_name),direction)` `^$GLOBAL`作为`$ORDER`的参数,将排序序列中的下一个或上一个全局名称返回到指定的全局名称。如果`^$GLOBAL`中不存在这样的全局名称节点,`$ORDER`将返回空字符串。 注意:`$ORDER(^$GLOBAL(NAME))`不会从IRISSYS数据库返回`%global names`。 Direction参数指定是返回下一个全局名称还是返回上一个全局名称。如果不提供方向参数,InterSystems IRIS会将排序顺序中的下一个全局名称返回给您指定的全局名称。 以下子例程搜索当前名称空间,并将全局名称存储在名为global的本地数组中。 ```java /// d ##class(PHA.TEST.SpecialVariables).GLOBAL3() ClassMethod GLOBAL3() { GLOB SET NAME="" WRITE !,"以下全局变量在 ",$NAMESPACE FOR I=1:1 { SET NAME=$ORDER(^$GLOBAL(NAME)) WRITE !,NAME QUIT:NAME="" SET GLOBAL(I)=NAME } WRITE !,"全部完成" QUIT } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).GLOBAL3() 以下全局变量在 DHC-APP ^%ISCWorkQueue ^%cspSession ^%qCacheMsg ^%qCacheMsgNames ^%qCacheObjectErrors ^%qCacheObjectKey ^%qCacheObjectQualifier ^%qCacheSQL ^%qHTMLElementD ^%qJavaMetaDictionary ^%qMgtPortal.Index ^%qPublicSuffix ^%qStream ^%qcspRule ^A ^AA Visible+4^%SYS.GD DHC-APP> ``` ## 作为`$QUERY`的参数 `^$GLOBAL`作为`$QUERY`的参数,按排序顺序将下一个全局名称返回到指定的全局名称。如果`^$GLOBAL`中不存在这样的全局名称作为节点,则`$QUERY`将返回空字符串。 注意:`$QUERY(^$GLOBAL(NAME))`不会从IRISSYS数据库返回`%GLOBAL NAMES`。 在以下示例中,用`user`命名空间中存在三个全局变量(`^GBL1`、`^GBL2`和`^GBL3`)。 ```java /// d ##class(PHA.TEST.SpecialVariables).GLOBAL4() ClassMethod GLOBAL4() { NEW $NAMESPACE SET $NAMESPACE="USER" SET (^GBL1,^GBL2,^GBL3)="TEST" NEW $NAMESPACE SET $NAMESPACE="%SYS" WRITE $QUERY(^$|"USER"|GLOBAL("^GBL1")),! WRITE $QUERY(^$|"USER"|GLOBAL("^GBL2")) NEW $NAMESPACE SET $NAMESPACE="USER" KILL ^GBL1,^GBL2,^GBL3 } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).GLOBAL4() ^$|"USER"|GLOBAL("^GBL2") ^$|"USER"|GLOBAL("^GBL3") ``` ## 作为`MERGE`的参数 `^$GLOBAL`作为`MERGE`命令的源参数,将全局目录复制到目标变量。`Merge`将每个全局名称添加为具有空值的目标下标。下面的示例显示了这一点: ```java MERGE gbls=^$GLOBAL("") ZWRITE gbls ``` ```java ... gbls("^zlgsql")="" gbls("^zlgtem")="" gbls("^zlgtem1")="" gbls("^zlgtem4")="" gbls("^zlgtemp")="" gbls("^zlgtemp1")="" gbls("^zlgtemp3")="" gbls("^zlgtemp5")="" gbls("^zlgtmp")="" gbls("^zlj")="" gbls("^zll")="" gbls("^zltmp")="" gbls("^zmc")="" gbls("^znum")="" gbls("^zpeterc")="" gbls("^zsb")="" gbls("^zseq")="" gbls("^zstock")="" gbls("^ztTmp")="" gbls("^ztrap1")="" gbls("^zwb1")="" gbls("^zwhtmp")="" gbls("^zx")="" gbls("^zx1")="" gbls("^zx2")="" gbls("^zxdd")="" gbls("^zyb")="" gbls("^zyb1")="" gbls("^zyb2")="" gbls("^zyl")="" gbls("^zzTT")="" gbls("^zzdt")="" gbls("^zzp")="" gbls("^zzy")="" gbls("^zzz")="" ```
文章
Michael Lei · 四月 24, 2022

基于Docker的Apache Web Gateway

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

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

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

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

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

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

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

Caché Mirroring 101:简要指南和常见问题解答

镜像101 Caché 镜像是一种可靠、廉价且易于实施的高可用性和灾难恢复解决方案,适用于基于 Caché 和 Ensemble 的应用程序。镜像在广泛的计划内和计划外中断情况下提供自动故障转移,应用程序恢复时间通常限制在几秒钟内。逻辑数据复制消除了存储作为单点故障和数据损坏的根源。升级可以在很少或没有停机时间的情况下执行。 但是,部署 Caché 镜像确实需要大量规划,并且涉及许多不同的过程。与任何其他关键基础设施组件一样,操作镜像需要持续监控和维护。 您可以通过两种方式使用本文:作为常见问题列表,或作为理解和评估镜像、规划镜像、配置镜像和操作镜像的简要顺序指南。每个答案都包含指向每个主题的详细讨论以及每个任务的分步过程的链接。 当您准备好开始规划镜像部署时,您的起点应该始终是Caché 高可用性指南“镜像”一章的镜像架构和规划部分。 经常问的问题 了解和评估镜像 镜像有什么好处? 镜像能否部署在虚拟化环境中? 镜像可以部署在云端吗? 镜像的基本设计是什么? 数据库副本如何与实时生产数据库同步? 自动故障转移是如何触发的?有没有它没有涵盖的情况? 镜像是否提供灾难恢复? 规划镜像 如何规划镜像的架构?将包括哪些成员,他们将在哪里? 哪些网络和延迟注意事项申请?镜像需要什么样的网络配置? 在故障转移时将应用程序连接重定向到新主节点的选项有哪些? 镜像中的 Caché 实例有哪些兼容性要求? 如何将现有数据库迁移到镜像? 如果将镜像部署在虚拟化环境中,我应该考虑什么? 配置镜像 我需要考虑哪些配置准则? 如何保护镜像? 如何配置镜像虚拟IP地址(镜像VIP)? 我在哪里以及如何安装仲裁器? 如何安装和启动 ISCAgent? 如何创建和配置镜像? 如何创建镜像数据库?如何将现有数据库添加到镜像? 如何确保 ECP 在故障转移后重定向应用程序服务器连接? 当镜像 VIP 不可用时(例如在云中),我如何确保重定向应用程序连接? 如何将 Caché Shadow转换为镜像? 我应该查看哪些其他配置细节? 管理镜像 如何监控镜像的运行? 如何修改镜像?我能做什么调整? 我可以在镜像中添加成员吗?消除一?如何完全删除镜像? 如果我需要暂时从镜像中删除成员怎么办? 我必须一次升级镜像吗?我必须把镜子从生产中取出来做吗? 我应该了解哪些其他镜像或镜像相关的管理程序和细节? 镜像中断程序 了解和评估镜像 镜像有什么好处? 对于基于 Caché 和 Ensemble 的应用程序,存在三种实现高可用性的主要方法: 故障转移集群、 虚拟化 HA和 Caché 镜像。前两者最大的缺点是依赖共享存储,存储失败后果不堪设想;可选的存储级冗余可以改善这一点,但也可以延续某些类型的数据损坏。此外,软件升级需要大量的停机时间,对于许多故障,应用程序恢复时间可能有几分钟。 通过使用两个具有独立存储和逻辑数据复制的物理独立系统,镜像避免了共享存储问题,升级不需要停机或停机时间很短,应用程序恢复时间通常为几秒钟。这种方案还提供可靠和强大的灾难恢复能力,灾难恢复站点(DR)可以位于距生产数据中心任何适当的距离。 镜像的主要限制是它只复制数据库本身;应用程序所需的外部文件需要额外的解决方案,安全和配置管理目前是分散的。 以下资源提供了这些 HA 方法的详细分析和比较,以及有关镜像优势的更多信息: 系统故障转移策略( Caché 高可用性指南) 高可用性策略(白皮书) 业务连续性的高可用性(视频) 缓存镜像:高可用性的冒险(视频) 镜像:吞吐量架构(在线学习) InterSystems Caché:数据库镜像:执行概述(白皮书) 镜像介绍(在线学习) HealthShare:通过镜像实现高可用性(在线学习) 镜像能否部署在虚拟化环境中? 镜像经常部署在虚拟化环境中。镜像通过自动故障转移对计划内或计划外中断提供即时响应,而虚拟化 HA 软件会在机器或操作系统意外中断后自动重启托管镜像成员的虚拟机。从而允许故障成员快速重新加入镜像以充当备份(或在必要时接管为主)。 有关使用此方法的信息,请参阅 InterSystems 白皮书高可用性策略。 镜像可以部署在云端吗? 镜像可以有效部署在云端。由于云网络限制,使用虚拟 IP 地址(镜像 VIP)在故障转移后重定向应用程序连接通常是不可能的,但这可以使用负载均衡器等网络流量管理器有效克服。 镜像的基本设计是什么? 一个 Caché 镜像通常包括物理上独立的主机上的两个 Caché 实例,称为故障转移成员;镜像自动将主角色分配给一个,而另一个成为备份。应用程序更新主数据库,而镜像使备份数据库与主数据库保持同步。 当主服务器发生故障或不可用时,备份服务器会自动接管主服务器,并将应用程序连接重定向到它。当主实例恢复运行时,它会自动成为备份实例。 操作员启动的人工切换可用于在计划的维护或升级停机期间保持可用性。 镜像可选地包含称为asyncs的其他成员,用于灾难恢复以及商业智能和数据仓库目的。 一个镜像也可以只使用一个故障转移成员和一定数量的异步,例如当灾难恢复是主要目标时。 数据库副本如何与实时生产数据库同步? 镜像的备份成员和异步成员使用日志文件(Journal文件)与主成员保持同步,日志文件包含自上次备份以来对 Caché 实例中的数据库所做更改的时间顺序记录。在镜像中,来自主数据库的日志文件被发送到其他成员并dejournaled日志记录——也就是说,其中记录的更改被应用到数据库的本地副本,使它们与主数据库保持同步。 日志记录从主数据库到备份的传输是同步的,主数据库在关键点等待备份的确认。这使故障转移成员保持紧密同步,并且备份处于活动状态(Active),并准备好接管为主。异步从主服务器异步接收日志数据,因此有时可能会滞后一些日志记录。 自动故障转移是如何触发的?有没有它没有涵盖的情况? 只有在确认主服务器在没有人工干预的情况下不能再作为主服务器运行时,备份服务器才能自动接管。当故障转移成员之间的直接通信中断时,备份从第三方系统( 仲裁器)获得帮助以确认这一点,仲裁器与两个故障转移成员保持独立联系。 此外,如果备份无法确认其拥有或无法从主服务器获取最新的日志数据,则无法发生自动故障转移。在每个故障转移主机上独立于 Caché 实例运行的代理进程,称为ISCAgents ,参与自动故障转移逻辑和机制的这一方面和其他方面。 假设仲裁器正常运行,几乎所有计划外的主机故障都包括在内;只有将故障转移成员彼此隔离并与仲裁器隔离的网络故障,才能阻止活动备份接管发生故障或不可用的主要成员。 镜像是否提供灾难恢复? 一种类型的异步镜像成员是灾难恢复 (DR) 异步。 DR 异步具有主数据库上所有镜像数据库的副本,并且可以随时提升为故障转移成员。当中断导致镜像没有正常运行的故障转移成员时,您可以手动切换到被提升后的 DR 异步;数据丢失的程度将取决于发生中断时 DR 异步落后于主服务器多远,以及前主服务器的主机系统是否正常运行,是否允许它获取额外的日志数据。提升的 DR 异步也可用于许多其他计划内和计划外中断情况。 规划镜像 如何规划镜像的架构?将包括哪些成员,他们将在哪里? 镜像的大小、成员资格和物理分布将取决于您部署它的原因以及许多基础设施和操作因素,允许多种可能的配置 具有两个故障转移成员的镜像通过自动故障转移提供高可用性。在可选的异步成员中,一个或多个 DR 异步可以提供数据安全和灾难恢复能力,而报告异步用于数据挖掘和商业智能等目的。单个报告异步最多可以属于 10 个独立的镜像,从而使其可以充当企业范围的数据仓库,将来自不同位置的相关数据库集合在一起。 如果不需要自动故障转移,镜像也可以包含一个故障转移成员和多个用于灾难恢复和报告目的的异步。 一个镜像最多可以包含 16 个成员。因为故障转移成员之间需要低延迟连接,因此通常位于同一地点,但异步成员可以位于本地或单独的数据中心,包括为 DR 异步上的数据提供最大安全性的地理位置偏远的位置。 一台主机上可以安装多个镜像成员,但需要额外规划。 哪些网络和延迟注意事项适用?镜像需要什么样的网络配置? 主要的网络配置考虑因素包括可靠性、带宽和网络延迟,这是应用程序性能的重要考虑因素。选择对主要成员传输给其他成员的日志数据进行压缩是通常但不必须的做法。 每个镜像成员都有几个不同的网络地址,用于不同的目的,在规划支持您的镜像所需的网络配置之前,应该很好地理解这些地址。 包含在单个数据中心、机房或校园内的镜像以及涉及双数据中心和地理上分离的灾难恢复的镜像的示例镜像和网络配置将帮助您定义所需的网络配置。 在故障转移时将应用程序连接重定向到新主节点的选项有哪些? 镜像和 Caché 内置了几个自动重定向选项,包括使用虚拟 IP 地址 (VIP) 进行镜像、将 ECP 数据服务器标识为镜像连接,以及镜像感知 CSP 网关。 镜像 VIP 通常是一种非常有效的解决方案,但确实需要一些提前规划,尤其是在网络配置方面。 还提供一系列外部技术选项,包括使用网络流量管理器(例如负载平衡器) 、自动或手动 DNS 更新、应用程序级编程和用户级程序。 镜像中的 Caché 实例有哪些兼容性要求? 在确定要添加到镜像的系统之前,请务必查看Caché 实例和平台字节顺序兼容性的要求。由于故障转移成员可以随时交换主要和备份的角色,因此它们应该尽可能相似; CPU 和内存配置应该相同或接近,存储子系统应该具有可比性。 如何将现有数据库迁移到镜像? 任何 Caché 数据库都可以轻松添加到镜像中;它所需要的只是能够备份和恢复数据库,或复制其CACHE.DAT文件。程序在下一节中说明。 如果将镜像部署在虚拟化环境中,我应该考虑什么? 在虚拟化环境中使用镜像时,规划虚拟镜像成员主机与物理主机和存储之间的正确关系很重要;镜像和虚拟化平台方面也有重要的操作考虑因素。 配置镜像 我需要考虑哪些配置指南? 如果您计划配置镜像虚拟 IP 地址 (VIP) ,InterSystems 建议将故障转移成员配置为使用相同的超级服务器端口和Web 服务器端口。 主要故障转移成员上的 Caché 实例配置(例如用户、角色、名称空间和映射)或未镜像的数据(例如与 SQL 网关和 Web 服务器配置相关的文件)都不会被其他镜像成员上的镜像复制。因此,在发生故障转移时启用备份或任何 DR 异步成员(可能被提升)以接管主服务器所需的任何设置或文件必须在这些成员上手动复制并根据需要进行更新。 不要在配置为镜像成员的任何系统上禁用 Internet 控制消息协议 (ICMP);镜像依靠 ICMP 来检测成员是否可达。 由于日志记录是镜像同步的基础,因此必须监视和优化故障转移成员上的日志记录性能并通常遵循日志记录最佳实践。特别是,InterSystems 建议您增加所有镜像成员上的共享内存堆大小(Shared memory heap size)。 如何保护镜像? 保护镜像通信的主要方法是 SSL/TLS,它使用 X.509 证书加密镜像内的所有流量。强烈建议使用 SSL/TLS 安全性。要在镜像上启用 SSL/TLS,您必须首先在每个镜像成员上创建一个镜像 SSL/TLS 配置;您可能会发现在创建镜像之前执行此操作最方便。启用 SSL/TLS 时,添加到镜像的每个成员都必须在主服务器上获得授权;成员的 X.509 证书更新时也是如此。 对于使用 SSL/TLS 的镜像的另一层保护,您可以激活日志加密。这意味着日志记录在主服务器上创建时使用其活动加密密钥之一进行加密,并在其他成员取消日志记录之前解密。备份和所有异步必须激活相同的密钥,备份和 DR 异步也必须使用它来加密数据。 配置镜像使用的网络的方式对镜像的安全性也有重要影响。 如何配置镜像虚拟IP地址(镜像VIP)? 镜像 VIP 是通过在创建和添加成员到镜像或修改镜像时指定详细信息来配置的,但是需要一些准备工作,包括所需信息的标识以及镜像成员的主机和 Caché 实例的可能配置。 我在哪里以及如何安装仲裁器? 仲裁器的位置应尽量减少仲裁器和故障转移成员意外同时中断的风险(如果两个故障转移都失败,则仲裁器变得无关紧要),因此其位置主要取决于故障转移成员的位置。单个系统可以配置为多个镜像的仲裁器,前提是它的位置适合每个镜像。托管镜像的一个或多个故障转移或 DR 异步成员的系统不应配置为该镜像的仲裁者。 任何运行 2015.1 或更高版本 ISCAgent 的系统,包括托管一个或多个 Caché 2015.1 或更高版本实例的系统,都可以配置为仲裁器。您可以准备任何其他受支持的系统(OpenVMS 系统除外),包括托管 2015.1 之前的 Caché 实例的系统,通过安装 ISCAgent将其配置为仲裁器。 如何安装和启动 ISCAgent? ISCAgent 随 Caché 自动安装,因此安装在任何镜像成员上。但是,必须将代理配置为在每个镜像成员上的系统启动时启动。 如何创建和配置镜像? 配置镜像是一个多步骤的过程: 创建镜像并配置第一个故障转移成员 配置第二个故障转移成员(如果需要) 授权第二个故障转移成员,如果使用 SSL/TLS(推荐) 配置异步镜像成员(如果需要,DR 或报告) 授权新的异步成员,如果使用 SSL/TLS(推荐) 在完成这些步骤中的任何一个之后,您可以在镜像监视器中查看镜像的状态以确认结果是否符合预期。 如何创建镜像数据库?如何将现有数据库添加到镜像? 在将数据库添加到镜像之前,您可能需要查看某些镜像数据库注意事项,这些注意事项与哪些内容可以镜像和哪些内容不能镜像、镜像和Shadow的同时使用、镜像数据库属性的传播以及镜像下每个实例的最大数据库数有关。 创建镜像数据库和添加现有数据库的过程是不同的,因为对镜像数据库的更改记录在镜像日志文件中,这与非镜像日志文件不同。如果数据库创建为镜像数据库,它从一开始就使用镜像日志文件,这使得通过在每个镜像成员上创建具有相同镜像名称的镜像数据库,可以很容易地将新数据库添加到镜像中。 当您将现有的非镜像数据库添加为主数据库上的镜像数据库时,它会从使用非镜像日志文件切换到镜像日志文件。因此,您不能简单地在其他成员上创建数据库,因为镜像无法将非镜像日志文件传送给其他成员。取而代之的是,在将数据库添加到主数据库的镜像后,您必须将其备份并在其他成员上恢复,或者将其CACHE.DAT文件复制到其他成员。 如何确保 ECP 在故障转移后重定向应用程序服务器连接? 无论您是否配置了镜像 VIP,您都可以通过将镜像 ECP 数据服务器配置为连接到它的每个 ECP 应用程序服务器上的镜像连接来确保 ECP 连接被重定向到新的主服务器。 (应用服务器不使用 VIP;因为它定期从指定主机收集信息,它会自动检测故障转移并切换到新的主服务器。) 当无法使用镜像 VIP 时(例如在云中),如何重定向应用程序连接? 只有当镜像成员位于同一网络子网上时才能使用镜像 VIP,而当它们位于不同的数据中心时通常不会出现这种情况。出于类似的原因,VIP 通常不是云中部署的选项。 可以使用一系列外部技术替代方案,包括使用负载均衡器(物理或虚拟)等网络流量管理器,可用于实现与 VIP 相同级别的透明度,向客户端应用程序提供单个地址或设备。其他可能的机制包括自动或手动 DNS 更新、应用程序级编程和用户级程序。 如何将 Caché Shadow转换为镜像? 镜像提供了一个Shadow到镜像实用程序,允许您将Shadow源和目标以及它们之间映射的Shadow数据库转换为具有主数据库、备份或异步数据库和镜像数据库的镜像。 我应该查看哪些其他配置细节? 虽然默认值通常是所需的全部,但您可能希望自定义 ISCAgent 端口号。 在主要故障转移成员上,您可能希望将代码从现有的^ZSTU或^ZSTART例程移动到用户定义的^ZMIRROR 例程,它允许您为特定镜像事件实现自定义的、特定于配置的逻辑和机制,以便它是直到镜像初始化后才执行。 将镜像与 Ensemble 一起使用时,您应该了解具有镜像数据的 Ensemble 命名空间的特殊要求以及 Ensemble Autostart 在镜像环境中的功能。 管理镜像 如何监控镜像的运行? 您可以在任何镜像成员的 Caché 管理门户中加载的Mirror Monitor提供有关的详细信息 镜像及其每个成员的运行状态,包括使用 SSL/TLS 时成员的 x.509 DN。 在故障转移成员上,两个故障转移成员的网络地址和仲裁器连接状态,以及仲裁器的地址;在异步上,报告异步所属的镜像。 在备份和异步成员上, 日志数据从主数据传输的状态和日志数据的Dejournaling,以及日志数据从主数据到达的速率。 加载镜像监视器的成员上镜像数据库的状态。 Mirror Monitor 还允许您执行许多操作,包括查看和搜索成员的日志文件、 将 DR 异步提升为故障转移成员或将备份降级为 DR 异步,以及激活、赶上和删除镜像数据库。 您可以在镜像成员的%SYS命名空间中使用 Caché 系统状态例程 ( ^%SS ) 来监视其镜像通信进程。 如何修改镜像?我可以修改什么? 在主服务器上编辑镜像以更改镜像的配置(包括 SSL/TLS、镜像 VIP 等)并在网络配置更改时更新成员的网络地址。您还必须编辑主服务器上的镜像以授权其他成员上的 X.509 证书更新。 在异步上编辑镜像以更改异步类型,将报告异步添加到另一个镜像,并进行其他特定于异步的更改。 您可以使用Mirror Monitor从任何成员(且仅该成员)的镜像中删除镜像数据库,尽管其影响因所涉及的成员类型而异。 我可以在镜像中添加成员吗?删除一个?如何完全删除镜像? 您始终可以将异步成员添加到镜像中,最多可添加 16 个成员。如果你有一个故障转移成员和少于 15 个异步,你总是可以添加一个备份。您还可以通过将 DR 异步提升为故障转移成员来替换备份,这会自动将当前备份降级为 DR 异步。 您可以编辑任何成员的镜像以从镜像中删除该成员。要完全删除镜像,您必须按特定顺序删除成员并采取其他步骤。 如果我需要暂时从镜像中删除成员怎么办? 您可以使用镜像监视器通过断开成员与镜像的连接来无限期地停止备份或异步成员上的镜像,例如进行维护或(在异步情况下)减少网络负载。 在异步上,您还可以暂停镜像中所有数据库的Dejournaling,而不暂停从主数据库到异步数据库的日志数据传输。 我必须一次升级镜像吗?我必须把镜像从生产中取出来做吗? 镜像的所有故障转移和 DR 异步成员必须是相同的 Caché 版本,并且只能在镜像升级期间有所不同。一旦升级的成员成为主要成员,您就无法使用其他故障转移成员或任何 DR 异步成员,直到它们也升级为止。通常,最佳做法是同时将报告异步升级到同一版本。 您选择的升级过程取决于您是进行维护版本升级、 不对镜像数据库进行任何更改的主要升级,还是对镜像数据库进行更改的主要升级。所提供的程序旨在最大限度地减少应用程序停机时间;在前两种情况下,您通常可以完全避免停机时间,而在后一种情况下,它通常仅限于执行计划的故障转移和进行所需的镜像数据库更改所需的时间。 当您在计划停机期间进行重大升级并且不需要最小化应用程序停机时间时,您可能还想使用一个更简单的过程。 我应该了解哪些其他镜像或镜像相关的管理程序和细节? 您可以在未使用SSL/TLS 的镜像上启用安全性,只要每个成员都具有有效的镜像 SSL/TLS 配置。 您可以为未使用它的镜像激活日志加密,只要该镜像使用 SSL/TLS 安全性并且用于加密主要日志数据的活动加密密钥在备份和所有异步中也处于活动状态。 根据您的硬件和网络配置,您可能需要调整镜像的服务质量超时(QoS 超时)设置,这在故障转移机制中起着重要作用。通常,如果需要更快地响应中断,则可以在部署在具有专用本地网络的物理(非虚拟化)主机上的镜像上减小此设置。 如果绝大多数镜像数据库更新由高度压缩的数据(如压缩图像)或加密数据组成,则日志数据压缩预计不会有效,因此可能会浪费 CPU 时间。在这种情况下,您可以选择配置或修改镜像以将日志数据设置为Uncompressed 。 (使用 Caché 数据库加密或日志加密不是选择压缩的一个因素。) 如果主要成员和其他镜像成员之间的网络延迟成为问题,您可以通过微调操作系统 TCP 参数来减少它,以允许主要成员和备份/异步成员分别建立适当大小的发送和接收缓冲区. ^MIRROR 例程为所有镜像任务提供了管理门户的命令行替代方案。 SYS.Mirror API 提供了以编程方式调用通过管理门户和^MIRROR例程可用的镜像操作的方法。 镜像中断程序 有关处理各种计划内和计划外镜像中断情况的建议过程的概述,请参阅镜像中断过程。
文章
Qiao Peng · 十月 17, 2023

FHIR生态

2023年6月底,世卫组织(WHO)和HL7签署了合作协议,利用HL7 FHIR提供互操作性,来支撑WHO的SMART指南(SMART Guideline)愿景 - 使用数智化的方式推动并加速一致化的健康干预措施建议,让世界上每个人都能立即从临床、公卫和数据使用建议中充分受益。 作为WHO的《2020-2025 年全球数字卫生战略》的一部分,SMART 指南使用 FHIR 、HL7的临床质量语言 (CQL) 和ICD标准以表达 WHO 的各种健康和临床指南,实现数据互操作、决策支持与指标、术语的一致性。这些标准被进一步利用来为各国及其合作伙伴开发一个由软件库、服务和工具组成的支持生态系统,并作为数字公共产品服务全球卫生健康事业。 为什么世卫组织会采用FHIR作为卫生信息互操作的标准在全球推广其一致化的健康干预措施建议?因为FHIR不仅标准成熟适用,而且还具有一个极具生命力的生态。 一个有生命力的标准会吸引生态的构建,而完善的生态将促进标准的成熟和演进。HL7 FHIR作为新一代的卫生信息互操作标准,其生态已经初具规模并蓬勃发展。 HL7 FHIR的知识产权类型 HL7 FHIR的知识产权是CC0,也就是知识共享。任何机构、组织和个人都可以无需向HL7申请而免费使用、扩展FHIR的标准。其知识产权类型配合FHIR标准的丰满程度,极大地鼓励和促进了基于FHIR的生态建设,应该也是WHO采用FHIR的原因之一。 FHIR的标准发布和标准的推广 标准应该是方便可及的 - 不仅有用户可阅读、可理解的文字说明,更需要要可以直接下载让计算机可用、可理解的电子结构化标准。 HL7 FHIR官网详细说明了每个版本、每个FHIR资源的结构与关系、使用范围、用例和示例。在下载页面提供了各种版本的标准、值集、profile和工具的免费下载。 对于用户的扩展、再约束和实施指南,有专门的实施指南注册和发布网站。这里可以免费注册自己的实施指南、也可以访问、查阅和下载别人的实施指南,从而让基于FHIR标准的自定义扩展可以无障碍地被分享、使用、理解,甚至进一步扩展。 下图是发布在注册网站的按用例类型统计的FHIR实施指南: 这众多方向的实施指南也是FHIR横跨交叉领域建立起成熟生态的体现。FHIR有什么快速建立生态的秘诀? 成熟的卫生信息标准要能应对各种行业互操作挑战,FHIR有一个四层机制用于制定标准并用各种互操作挑战来测试、验证和推进FHIR落地: 工作组(workgroups):FHIR有40多个工作组,专注不同的领域的需求,并制定和改进相关FHIR资源和用例标准。例如FHIR基础架构、基因组学、电子健康档案、财务管理、设备... 加速器计划(accelerators):为了推进在主要互操作领域的成熟和落地,FHIR建立加速器计划让每个领域的各个利益相关方参与进来,通过研究各方的需求、凝聚各方的智慧来推动FHIR。如今已经有8个不同领域的加速器计划: 例如Vulcan是专注连接临床研究、转化研究和医疗保健的加速器,它的成员不仅有HL7这样的标准开发组织,还有学会 - 例如约翰霍普金斯医学院,行业协会 - 例如全球医疗数据科学社区PHUSE,政府机构 - 例如FDA,技术厂商 - 例如InterSystems,药厂 - 例如GSK,甚至意见领袖。 课题(projects):FHIR通过课题,研究具体的需求、实现具体的目标,让FHIR扎实、可用。例如Vulcan加速器有以下课题: 课题 目标 Schedule of Activities (SoA) 活动安排 用FHIR表示电子表格中的活动时间表。 使得研究中的每项活动的描述、时间和标识都能保持一致 Real World Data (RWD) 真实世界数据 以标准化的格式从EHR中提取数据,以支持临床研究,特别是向监管机构提交数据 Phenotypic Data 表型数据 为基因组研究和基因组医学提供更多高质量的标准化表型信息 Electronic Product Information (ePI) 电子产品信息 为产品信息(各论)定义一个共同的结构,支持患者对产品数据的跨边界交换 Adverse Events (AE) 不良事件 支持对不良事件的报告和格式进行标准化。 提高相关FHIR资源的成熟度 FHIR to OMOP FHIR与OMOP映射 支持开发FHIR到OMOP的数据传输,以便更好地分析临床数据,用于研究 连接测试马拉松(connectathons):这是一个针对技术厂商的FHIR互操作系列化的一致性认证。每年3次的连接测试马拉松会确定众多的具体互操作用例,厂商选择并参与这些用例,用FHIR进行跨厂商的互操作测试。它不仅是技术厂商验证自己的FHIR互操作一致性的试验场,更是通过测试和反馈来发现标准的问题、确定标准适用性的大型沟通会。 FHIR confluence上公布有历次的连接测试马拉松的用例说明、实施指南、学习资料等详尽的资料。 除了这些手段,HL7还有FHIR认证,建立FHIR标准的智力资源池、确保FHIR在全球的正确采纳。 FHIR标准的适应性 FHIR的适应性核心在于其标准的设计 - 通过profile,在资源模型层面已经考虑到如何让用户进行不破坏标准的扩展和再约束;在标准成熟上,设计了成熟度模型,让标准基于实际使用和反馈逐步成熟。 Profile可以让用户裁剪、扩展FHIR标准,以适用于自己的术语体系和用例场景,实现基于统一标准的千人千面。 在标准的理解与反馈上,FHIR官方沟通提供了开放的交流和反馈的渠道。 FHIR生态的工具 成熟的生态工具是FHIR的一大亮点。这些工具是整个生态贡献的,好的工具得到广泛认同和采纳,既促进了标准的理解与使用、也避免了低水平的重复建设。 1. 标准学习工具: 理解和学习是标准推行的第一要务。除了汗牛充栋的学习材料和视频,FHIR还有不错的学习网站,例如Clinfhir ,最初设计是方便医生理解如何用FHIR构建和解决自己的用例的,但实际上也被广大卫生信息从业者用于理解FHIR标准。 2. 测试数据生成工具: 想学习标准?没有什么比直观的数据更能说明问题了。FHIR生态下有名的Synthea是一个基于马塞诸塞州的患者真实数据经过统计、混淆后的FHIR测试数据生成工具,可以按用户要求生成指定数量的、符合真实数据分布的FHIR资源,会为每个生成的虚拟患者生成一个FHIR boundle文件,并生成对应的医院、医生等FHIR资源。大家可以免费下载Synthea使用它产生测试数据。 另外,国内也广泛使用的MIMIC - 麻省理工贝斯以色列迪康医学中心的有5万多患者真实完整的高质量重症医疗数据集,如今也有了FHIR版本。 3. FHIR服务器: 还没有FHIR服务器,怎么测试FHIR? FHIR生态下有大量的免费沙箱,用户可以选择它们进行标准的学习和测试。例如官网提供的沙箱和各个厂商提供的沙箱。通过各种API工具,例如postman,学习者无需注册即可以了解FHIR标准的方方面面,甚至将自己的测试数据加载进去并测试自己的解决方案。 4. 标准扩展和再约束构建工具: 如何方便、直观地构建自己的术语、扩展和再约束(Profile)和用例?FHIR生态下有众多公司提供的免费工具可用 - 随君取用。例如术语扩展可以用Snapper和FSH、进行小规模profile开发可以用可视化的Forge或Trifolia-on-FHIR、进行大规模的profile和实施指南开发可以用FSH。 5. 标准验证工具: 需要基于profile对FHIR资源进行校验?资源更多了,不仅有FHIR官网提供的FHIR资源校验网页,还有各种开发语言版本的校验工具代码: JAVA C#/DotNet FHIR生态下百花齐放的各种应用架构、应用方向 更令人眼前一亮的是FHIR生态下各种应用架构、应用方向和众多其它生态对FHIR的采纳。 应用开发架构: FHIR提供了标准卫生信息模型和相应的API,为行业应用的快速开发提供了坚实的基础。FHIR生态下最有名的SMART on FHIR,实现即插即用和可复用的应用开发架构。在国际卫生信息互操作标准发展简史中有简要介绍。 SMART on FHIR市场已经有大量的应用可以直接下载部署。 决策支持架构: 决策支持已经是卫生信息数字化转型的核心需求之一。卫生信息化已经建设了各种基于知识库和基于机器学习的决策支持系统,涵盖了临床、业务管理、费用、组学与科研、公卫、健康管理等全部业务,但仍面临众多挑战。 任何知识库系统和决策支持系统面临的一个关键挑战是决策支持的可移植性!如果决策支持厂商都按自己的数据、术语和服务标准构建解决方案,用户在使用多个决策支持产品时,将面临大量数据转换和映射及服务集成带来的非常高的实施成本和潜在决策错误风险。 FHIR通过Clinical Reasoning模块和CDS Hooks分别提供了本地决策支持架构和外部决策支持架构,通过标准化降低成本和风险、提高决策效率和范围。这里是对CDS Hooks的介绍。 其它标准对FHIR的采纳: 相较于之前流行的互操作标准,FHIR在标准化、灵活性、可用性 三方面取得了很好的平衡。FHIR资源模型比大多数的行业通用数据模型(CDM)都简化,方便使用。曾经各自为战的众多标准都发现FHIR无处不在,且FHIR资源和API可以作为自己的数据和访问数据的基石,而融入FHIR生态可以更方便获得数据、获得更多的推广、发挥更大的价值,因此一系列的XX on FHIR项目应运而生 - 或者直接采纳FHIR、或者与FHIR相兼容。除了上面提到的SMART on FHIR,这里简单汇总一下主要的已完成和进行中的on FHIR项目和标准。 1. IHE IHE(Integrating the Healthcare Enterprise)是国际上比较流行且成功的卫生信息交换服务规范。它一直采用流行和稳定的互操作基础标准来开发自己的服务规范,最初使用DICOM + HL7 V2消息,后来用到HL7 V3 和CDA。IHE发现新的FHIR互操作标准有助于应对新的用例、并更好解决老的用例,认为FHIR会成为最流行的互操作基础标准,因此已经发布了很多基于FHIR的IHE服务,尤其是那些和移动业务相关的服务,例如移动患者人口统计查询 (PDQm)。 2. OMOP on FHIR OMOP(Observational Medical Outcomes Partnership)是包括国内在内全球科研人员进行真实世界研究的重要工具,它开发了通用数据模型CDM和分析工具库。 HL7国际和OHDSI宣布合作提供单一的通用数据模型,用于共享临床护理和观察研究信息 - 这就是OMOP on FHIR项目。 OMOP-on-FHIR 是构建在 OMOP CDM 数据库之上的 FHIR 服务器,它提供中间映射层,实现OMOP CDM和FHIR资源之前的双向转换,从而打通两大生态,使临床医生和研究人员能够从多个来源提取数据并以相同的结构进行分析处理与共享交换而不会降低数据质量,可以同时使用两个生态下丰富的应用与工具,利用各自的生态优势。例如OMOP让FHIR生态可以利用其丰富的预测模型,而FHIR让OMOP的研究分析可以集成到临床工作流程中,推动精准医学的落地。 3. FHIR to CDISC Joint Mapping CDISC 是一个标准开发组织,开发了生物制药行业使用的诸多数据标准,常用于提交临床试验数据以进行分析和监管审批。 通过与HL7合作,FHIR to CDISC Joint Mapping实施指南定义了FHIR 与三个特定 CDISC 标准之间的映射: 研究数据列表模型实施指南 (SDTMIG) 3.2 临床数据采集标准协调实施指南 (CDASH) 2.1 实验室1.0.1 通过简化 HL7 FHIR和 CDISC 标准之间的数据转换,消除使用临床信息支持科研的障碍。用途包括: 捕获“真实世界证据”(RWE),让那些不是为临床试验目的采集的数据可以用于研究监管 利用FHIR 的 SMART等技术,直接在临床系统内部捕获试验驱动的数据,而不是建立单独的临床试验管理解决方案 在回顾性研究中更容易利用临床数据 创建病例报告表单 (CRF),链接到使用 FHIR 资源和Profile定义的数据元素 使两个标准社区的专家能够理解彼此的术语,并随着两套规范的不断发展更好地协调它们 4. 通用数据模型协调 Common Data Models Harmonization(CDMH) 在卫生信息领域,有众多的通用数据模型(Common Data Models)服务于不同的或相同的业务领域。虽然都是“通用”数据模型,但数据在彼此之间并不通用。 FHIR的细颗粒度统一语义资源模型可以作为众多通用数据模型间的桥梁。通用数据模型协调(CDMH)目标就是借助FHIR打通各个通用数据模型,让它们的数据可以相互转换。 CDMH 项目由美国FDA 领导,与其他联邦政府机构合作。已发布的通用数据模型协调 (CDMH) FHIR 实施指南 (IG) 将重点放在以患者为中心的结果研究 (PCOR) 和其它目的提取的观察数据的映射和转换为 FHIR 格式。该项目重点关注以下四种通用数据模型 (CDM) 到 FHIR 的映射: 以患者为中心的结果研究网络 (PCORNet) 整合生物学和床边 (Informatics for Integrating Biology & the Bedside - i2b2) 临床试验 (ACT) 信息学,也称为 i2b2/ACT。 观察性医疗结果合作伙伴 (OMOP) 美国食品和药物管理局的哨兵(Sentinel) 5. Arden Syntax on FHIR 和HL7的临床质量语言(Clinical Quality Language - CQL)类似,Arden Syntax 是一种结构化、可执行的医学知识表示和处理语言,将医学知识表达为独立的单元 - 医学逻辑模块(Medical Logical Modules),常用于设计CDS系统,构建临床指南规则和临床决策规则。 新版本 Arden Syntax 3.0 版采用FHIR进行扩展,重新定义了基于FHIR的标准化的数据模型和数据访问方式。作为经过审计、基于共识的迭代 HL7 标准开发流程的一部分,3.0版已成功通过投票。 6. HL7 V2 to FHIR HL7 V2在全球依然有很高的采纳度,但其局限性和FHIR的成熟度都在推动从V2到FHIR的迁移。HL7 V2 to FHIR 项目建立实施指南,将HL7 V2的组件映射到FHIR组件:V2的消息、消息段、数据类型和词汇分别映射到 FHIR 的Bundle、FHIR资源、数据类型和编码系统,并对FHIR进行相应扩展以弥补二者间的差距。 7. C-CDA on FHIR C-CDA是最广泛实施的 HL7 CDA 实施指南之一,涵盖了临床护理的文档范围。CDA 和 FHIR 之间的互操作能力是推动临床文档进化的重要渠道。 C-CDA on FHIR 实施指南 (IG) 定义了一系列 FHIR 配置文件,以表示 C-CDA 中的各种文档类型,并弥补二者设计上的差异。C-CDA on FHIR 利用FHIR使文档标准更为精简。 还有更多的on FHIR项目没有介绍到,例如SNOMED on FHIR、PDMP on FHIR... 同时可以预期还会有越来越多的on FHIR项目会不断涌现。 不仅是这些on FHIR 项目,越来越多的机构发现FHIR的价值,将自己原来的数据模型改为FHIR。例如美国互操作核心数据集USCDI(U.S. Core Data for Interoperability) 起初采用通用临床数据集CCDS作为模型, 如今已经完全采纳FHIR,并且成为美国国家FHIR标准US Core的一部分。FHIR也得到了很多国家采纳作为国家级卫生信息互操作的标准。 大规模数据统计与分析: 一个好的标准应该有助于解决完整的行业需求。FHIR作为行业互操作标准已经超越了传统互操作的能力范围,除了互操作的数据模型、消息、文档、服务和API,FHIR服务器加上FHIR资源仓库为大规模的卫生信息持久化和访问提供了方案。 FHIR的完整蓝图目前尚缺一块拼图 - 基于FHIR的大规模数据统计与分析。 1. 大规模数据检索 FHIR API提供检索类型的API,通过查询参数(Search Parameter)对资源进行检索。 例如: 想要获取所有检验项目为loinc 1234-1,且检验结果小于9.2的Observation资源,可以用这样的查询参数: GET http://fhirsvr.com/Observation? code-value-quantity=loinc|1234-1$lt9.2 除了FHIR Core发布的查询参数,用户还可以扩展自己的查询参数,满足检索需求。 FHIR标准里的FHIR Path为FHIR资源模型提供了类似于XPath的资源路径导航和获取语言,可以方便地筛选、过滤层次化的FHIR数据。 但FHIR查询API和FHIR Path都仅适合于单资源类型的检索,对于需要多类型资源联合分析、汇聚、统计等分析需求无能为力。 2. 大规模的数据统计分析 HL7为临床质量指标与决策支持提出了临床质量语言(Clinical Quality Language - CQL) ,CQL如今基于FHIR,使用FHIR资源模型来构建标准化的指标体系,以支持决策和基于指标的管理。 对于科研数据分析,借助上面介绍的OMOP on FHIR和其它项目,用户可以用自己熟悉的科研工具并利用FHIR数据支持自己的科研工作,本质上是将FHIR数据转换并导入自己的科研工具。 对于通用大规模数据统计分析,虽然FHIR提供了API、FHIR资源数据序列化的JSON、XML可以作为文档进行分析,但市面上的统计分析工具和机器学习工具大都支持SQL,SQL也是最流行的数据统计分析语言。 FHIR的深层次化模型是立体的、对象化的,而SQL是扁平的、表格化的。这个差异让FHIR对主流分析工具和机器学习工具不友好。这对基于FHIR原生的大规模数据分析利用造成了障碍,是FHIR最需要完善的那一块。 FHIR和生态已经创立了很多项目,努力补上这一环。 SQL on FHIR SQL on FHIR项目的思路是为SQL用户提供FHIR的SQL表示层。SQL表示层提供一个机制:让用户根据自己的需要基于FHIR Path定义视图。这里的视图不是SQL视图,而是一个SQL模型的逻辑表达,由一个新的FHIR工件ViewDefinition定义。各个技术厂商负责物理实现它并展现为SQL表。 例如下面的视图定义: { "resourceType": "http://hl7.org/fhir/uv/sql-on-fhir/StructureDefinition/ViewDefinition", "select": [ { "column": [ { "path": "getResourceKey()", "alias": "id" }, { "path": "gender" } ] }, { "column": [ { "path": "given.join(' ')", "alias": "given_name", "description": "A single given name field with all names joined together." }, { "path": "family", "alias": "family_name" } ], "forEach": "name.where(use = 'official').first()" } ], "name": "patient_demographics", "status": "draft", "resource": "Patient" } 它定义一张这样的SQL表: 考虑到FHIR资源模型的复杂,SQL on FHIR目前尚待成熟。当前是版本2,尚未发布,且有很多限制,例如不能在视图里定义跨资源的字段。 技术厂商的FHIR资源SQL实现 除了SQL on FHIR项目,很多技术厂商也在借助自身技术上的优势为FHIR提供SQL访问层。 例如InterSystems IRIS是一个多模型数据平台技术,它可以同时支持对FHIR资源逻辑模型使用对象建模、对FHIR序列化的JSON/XML使用文档建模,并将这些模型投射为SQL模型。InterSystems IRIS正是借助于这个特性,提供一个名为FHIR SQL构建器(FHIR SQL Builder)的工具,用户通过图形化方式拖拽建立需要的SQL模型,而无需拷贝和转换数据。 FHIR生态正展现出蓬勃的生命力,如今已经是百花齐放。FHIR展现的统一行业语义能力和强大的生态,不仅帮助WHO发布数字公共产品服务,也可以赋能卫生信息数字化转型。
文章
Qianzhu Liu · 四月 25, 2021

鱼与熊掌兼得 – 利用医院信息系统实践《医疗机构处方审核规范》

如何做到处方审核便捷性与安全性共存,一直是个“鱼与熊掌”的情形。开始正文前,先说个故事—— 20年前我做医学生的时候,临床医学院和药学院的大楼是邻居。每天看着药学院的同学出入,脑海中浮现出的是各大药企年会上的销售精英以及金庸先生笔下完美的小说人物黄药师和他的九花玉露丸。时不常会上前聊几句,感慨一下需要熟记各种化学分子式的艰辛。后来步入医院,真正成为一名医生,却仿佛与药房和药师没了交集。只是偶尔实在疲累的夜晚,与同在值班的护士聊天,听到过一次“要是我在药房工作就好了,跟超市服务员似的,照单上架、拿货,轻松至极”。直到一次凛冽冬日的急诊“大夜”(从头天晚上6点至第二天早上8点的14小时“绝命班型”称之为“大夜”),楼道里挤满了待诊的患者,呻吟、哮鸣、叹息之声此起彼伏。正在诊室里对桌奋笔疾书的同事(那时候还没有电子病历系统)被轻拍了一下肩膀,抬眼望去,一个身着白大衣的小伙子立于身旁,手里拿着一张处方。定睛看了下胸牌 – 急诊药房xxx。“请问有什么事吗?”同事低下头继续专注于病历。“王医生,您给一个发热的3岁患者开具了一盒阿莫西林胶囊,叮嘱家属除去胶囊外壳后服用。咱们药房现在备有“阿莫西林口服混悬液”,服用起来更方便。您看是否需要重新开具处方?”得知这种情形,同事当即调整了治疗方案,家属也对药师的提议极其满意。虽然并不是什么至关重要的调整,我和同事却都深深体会到处方审核以及药师本应承担职责的重要性。再后来,又间断有药房因为剂量错误等原因建议医师调整处方的案例出现,我们便更是感叹于有能干又负责的药师们做坚强后盾是多么美好的事情。 2018年7月国家卫生健康委员会、国家中医药管理局、中央军委后勤保障部联合印发《医疗机构处方审核规范》(后面简称《规范》),第一次明确:1)“处方审核”是指药学专业技术人员运用专业知识与实践技能,根据相关法律法规、规章制度与技术规范等,对医师在诊疗活动中为患者开具的处方,进行合法性、规范性和适宜性审核,并作出是否同意调配发药决定的药学技术服务。2)所有处方均应当经审核通过后方可进入划价收费和调配环节,未经审核通过的处方不得收费和调配。旨在达到提升处方质量、增加医疗效率和有效控制费用的综合目标。 其实“处方审核”并非一个新鲜事物,只不过在《规范》未出台之前,大多数医疗机构实行的是“事后处方点评”。“事后处方点评”对临床诊治步骤没有任何影响,在规定的时间集中评估“已完成”处方的1)药品 – 诊断匹配度,2)药品剂量、剂型、时限准确度,3)药品是否存在负面相互作用。“事后处方点评”属于药房或药事委员会的独立行为,可以每天进行,也可以每周或每月进行,甚至有些医疗机构仅作处方定期抽查。不合格处方开具者会受到警告和教育,发生范围广的错误处方会全院通告,以提升后续处方的质量。这样做的原因有二:1)医生对所开具处方全权负责,这在医生接受医学教育和临床培训时即已明确,药师仅承担辅助工作;即便药师偶尔提出异议,也大多以医生拒绝接受建议而收场;2)在临床诊治流程中增加任何一个环节,都势必增加医务人员工作量和患者等候时间、降低医疗效率和患者满意度,最终对医疗机构声誉和收益造成不良影响。 因此,如何做到处方审核便捷性与安全性共存,一直是个“鱼与熊掌”的情形。为满足《规范》中对于处方审核流程和内容的要求,做到鱼与熊掌兼得,除外医疗机构管理层面的配合、医务人员的理解和执行,还要充分利用科技手段,例如医院信息系统,以确保流程转变顺畅、效果呈现显著。 大多数公立医疗机构已经或者即将遵循的“处方审核流程”如下图所示(部分民营/私立医院存在的预付费/套餐流程,不包含在本文讨论范围),每个环节中医院信息系统可以做到的支持也一并列出,以方便各位读者理解。 一、医院信息系统助力处方审核 之 便捷性 要在原有的已经运行多年的临床诊治流程中添加一整个独立的新环节,也就是“处方审核”环节,并将其对效率和工作量的影响减至最低,就需要该环节与前后两个环节做到无缝对接。同时需要系统自动承担该环节中的“非必须人力完成”的部分,且对“必须人力完成”的部分进行充分决策支持。 1. 展示“待审核”处方列表 在“事后处方点评”时期,负责点评的药师仅需要在规定的时间内评估系统中处方合格与否;而新流程需要药师在接收到医生开具处方的第一时间对其进行评估,并针对不合格处方与开具处方医师进行有效沟通,具有强实效性。以一个正常规模的三级医院为例,药房一天接收到的处方少则几千、多则上万,如何迅速定位到需要审核的处方以及每张处方的状态,对于提升工作效率至关重要。 医院信息系统可以在流程变更中加入“待审核处方列表”,在单独的页面显示,或者作为处方诸多状态(包括:待审核、审核通过、审核未通过、被拒绝、配药、摆药、发药、完成,等)中的一个,在页面的某一部分显示。所有新开具处方以及需要二次审核的处方均自动进入“待审核处方列表”,审核完成后进入下一状态列表,以确保药师不会遗漏任何新开具的或者需要二次审核的处方。样例如下图: 2. 开放“查阅患者病历”和“查阅药品说明书“权限 药师进行处方审核时需要根据患者情况(例如:年龄、性别、身高、体重等基本信息,以及诊断、过敏史、症状、是否妊娠等临床信息)以及参考药品说明书。最便捷的方法是,医院信息系统在“处方审核“页面设置患者病历和药品说明书链接,当药师需要时,在不切换账号和页面的前提下,即可进行浏览。 3. 记录审核过程和沟通结果 同样是在处方审核页面,设置“审核通过”、“审核未通过/待修改”、“拒绝处方”等按钮,预设各类原因供药师选择,同时预留文本框以供药师对特殊情形和事件进行文字描述。这种系统设置较传统的“集中记录处方审核结果”的方式明显增加工作效率,以及结果记录的准确性。 二、医院信息系统助力处方审核 之 安全性 上文提到,《规范》中所指“处方审核”为“事前处方审核”,即发生在收费、配药、摆药、发药等一些列行为之前,与医生开具的处方有实时互动和干预。其主要目的就在于最大化处方质量,避免用药误差给患者带来负面影响。 针对这个目标,医院信息系统可以从以下方面进行支持: 1. 设置处方开具权限和处方审核权限 确保处方质量的第一步即确保开具处方和审核处方的医师具备相应资格,例如:是否获得执业医师/药师资格、是否达到工作年限、是否评聘中/高级职称、是否通过毒麻药/限制级抗生素使用考核等,在源头上把控处方开具人和处方审核人的知识、经验与处方内容、等级相匹配。未达到上述资质的医务人员无法进入处方开具或处方审核页面。 2. 配备药品字典和开启药品临床决策支持 医师和药师在学校学习和临床实践中会不断积累药品相关知识,但做到对所有药品信息熟记于心、随时运用却是很难。且当今医学药学飞速发展,即便每日更新知识,仍然难以做到对新药、最新研究全盘掌握。在系统中整合国内国外公认的药品字典,并将药品字典中的临床决策支持与系统对接,例如:药品 – 诊断冲突、药品 – 妊娠冲突、药品 – 过敏冲突、药品 – 药品冲突等,在发现处方中存在上述情况时,发送警告,及时提醒,为保证处方质量提供双重保证。 3. 超量超时自动提醒 某些药品无论因为“病情容易变化、需要随时调整”,还是因为“社保、商保报销限制”,均可能存在每日、每剂、总量和总时长的上限规定,系统通过预设这些限制,在处方提交前自动审核,并对与预设限制不匹配的药品发出警告。 4. 记录“否决系统提示”原因 尽管系统可以自动审核部分处方内容,并在不同场景下进行决策支持,医师和药师依然是诊疗决策者和处方责任人。因此,除妊娠危险性X级药品等绝对禁忌外(例如:妊娠期间绝对禁用阿托伐他汀),系统应当给予医师和药师“否决系统提示”的权限以及记录具体原因的方式。药师通过阅读医师记录的“否决系统提示”原因,也可以减少不必要的沟通,快速对处方审核作出判定。 系统的优势功能越多,处方的安全性和审核的便捷性越高。如果有灵活的信息交互平台和可信的数据库做配合,医疗机构还可以通过处方审核中记录的所有数据(例如:通过每一个环节发生的时间点判断耗时最长环节,或者通过“审核未通过”处方最常见原因等),对临床诊治流程进行调整、对相关医务人员进行培训,以达到助力运营管理和优化临床决策的作用,最终与“提质增效控费”的目标完美契合。 非常棒的文章!
文章
姚 鑫 · 九月 10, 2021

第十二章 SQL命令 CREATE QUERY

# 第十二章 SQL命令 CREATE QUERY 创建`Query` # 大纲 ```sql CREATE QUERY queryname(parameter_list) [characteristics] [ LANGUAGE SQL ] BEGIN code_body ; END CREATE QUERY queryname(parameter_list) [characteristics] LANGUAGE OBJECTSCRIPT { code_body } ``` # 参数 - `queryname` - 要在存储过程类中创建的查询的名称。`queryname`必须是有效的标识符。过程名可以是限定的(`schema.procname`),也可以是非限定的(`procname`)。非限定过程名接受默认模式名。即使没有指定参数,`queryname`也必须后跟括号。 - `parameter_list` - 可选-传递给查询的参数列表。参数列表用圆括号括起来,列表中的参数用逗号分隔。即使没有指定参数,括号也是必须的。 - `characteristics` - 可选-指定查询特征的一个或多个关键字。允许的关键字有结果、容器`ID`、`FOR`、`FINAL`、`PROCEDURE`、`SELECTMODE`。多个特征由空白(空格或换行符)分隔。特性可以以任何顺序指定。 - `LANGUAGE OBJECTSCRIPT`,`LANGUAGE SQL` - 可选—指定用于`code_body`的编程语言的关键字子句。指定语言对象脚本或语言SQL。如果省略了`LANGUAGE`子句,则默认为`SQL`。 - `code_body` - 查询的程序代码。SQL程序代码以`BEGIN`关键字开头,以`END`关键字结尾。查询的`code_body`只包含一个完整的`SQL`语句(一个`SELECT`语句)。该`SELECT`语句以分号(`;`)结束。`ObjectScript`程序代码用花括号括起来。`ObjectScript`代码行必须缩进。 # 描述 `CREATE QUERY`语句在类中创建一个查询。 默认情况下,名为`MySelect`的查询将被存储为`User.queryMySelect`或`SQLUser.queryMySelect`。 `CREATE QUERY`创建的查询可能作为存储过程公开,也可能不作为存储过程公开。 要创建公开为存储过程的查询,必须指定`procedure`关键字作为其特征之一。 还可以使用`CREATE PROCEDURE`语句创建作为存储过程公开的查询。 为了创建查询,必须拥有`%CREATE_QUERY`管理权限,如`GRANT`命令所指定的。如果试图为已定义所有者的现有类创建查询,则必须以该类的所有者身份登录。否则,操作将失败,并出现`SQLCODE -99`错误。 如果类定义是已部署的类,则不能在类中创建查询。此操作失败,出现`SQLCODE -400`错误,出现`%msgUnable to execute DDL that modifies a deployed class: 'classname'`。 # 参数 ## queryname 要创建为存储过程的查询的名称。此名称可以是非限定名称(`StoreName`)并采用默认架构名称,也可以通过指定架构名称(`Patient.StoreName`)进行限定。可以使用`$SYSTEM.SQL.Schema.Default()`方法来确定当前系统范围内的默认架构名称。系统范围内的初始默认模式名是`SQLUser`,它对应于类包名`User`。 注意,`FOR`特征(将在下面描述)覆盖`queryname`中指定的类名。 如果已经存在具有此名称的方法,则操作将失败,并出现`SQLCODE -361`错误。 生成的类的名称是对应于架构名称的包名,后跟一个点,后跟`“query”`,后跟指定的`queryname`。例如,如果非限定查询名`RandomLetter`采用初始默认模式`SQLUser`,则得到的类名将是:`User.queryRandomLetter`。 `SQL`不允许指定只以字母大小写不同的查询名。 指定一个与现有查询名称仅在字母大小写上不同的查询名称将导致`SQLCODE -400`错误。 如果指定的`queryname`已经存在于当前命名空间中,系统将生成`SQLCODE -361`错误。 ## parameter-list 用于将值传递给查询的参数的参数声明列表。 形参列表用圆括号括起来,列表中的形参声明用逗号分隔。 括号是必须的,即使没有指定参数。 列表中的每个参数声明由(按顺序)组成: - 一个可选关键字,指定参数模式是`IN`(输入值)、`OUT`(输出值)还是`INOUT`(修改值)。 如果省略,默认参数模式为`IN`。 - 参数名称。 参数名称区分大小写。 - 参数的数据类型。 - 可选:默认值。可以指定`DEFAULT`关键字后跟一个默认值;`DEFAULT`关键字是可选的。如果没有指定默认值,则假定默认值为`NULL`。 下面的示例创建了一个公开为存储过程的查询,该存储过程具有两个输入参数,这两个参数都具有默认值。 `topnum`输入参数指定可选的`DEFAULT`关键字; `minage`输入参数忽略了这个关键字: ```sql CREATE QUERY AgeQuery(IN topnum INT DEFAULT 10,IN minage INT 20) PROCEDURE BEGIN SELECT TOP :topnum Name,Age FROM Sample.Person WHERE Age > :minage ; END ``` 以下是该查询的所有有效CALL语句:`Call AgeQuery(6,65);Call AgeQuery(6);Call AgeQuery(,65);Call AgeQuery()`。 ```sql CALL AgeQuery(6,65); CALL AgeQuery(6); CALL AgeQuery(,65); CALL AgeQuery() ``` ![image](/sites/default/files/inline/images/tu_pian__9.png) ## characteristics 可用的特征关键字如下: - `CONTAINID integer` - 指定返回ID的字段(如果有)。将`CONTAINID`设置为返回ID的列的编号,如果没有列返回ID,则设置为0。 IRIS不验证命名字段是否确实包含ID,因此此处的用户错误会导致数据不一致。 - `FOR className` - 指定要在其中创建方法的类的名称。如果该类不存在,则会创建它。还可以通过限定方法名称来指定类名。在`FOR`子句中指定的类名将覆盖通过限定方法名指定的类名。 - `FINAL` - 指定子类不能重写该方法。默认情况下,方法不是最终的。`Final`关键字由子类继承。 - `PROCEDURE` - 指定查询为SQL存储过程。存储过程由子类继承。(此关键字可以缩写为`proc`。) - `RESULTS (result_set)` - 按查询返回数据字段的顺序指定数据字段。如果指定`RESULTS`子句,则必须将查询返回的所有字段作为逗号分隔的列表列出,并将其括在圆括号中。指定比查询返回的字段少或多的字段会导致`SQLCODE-76`基数不匹配错误。为每个字段指定列名(将用作列标题)和数据类型。如果使用SQL语言,则可以省略`RESULTS`子句。如果省略`RESULTS`子句,则会在类编译期间自动生成`ROWSPEC`。 - `SELECTMODE mode` - 指定用于编译查询的模式。可能的值有`Logical`、`ODBC`、`Runtime`和`Display`。默认值为运行时。 如果指定的方法关键字(如`PRIVATE`或`RETURNS`)对查询无效,系统将生成`SQLCODE-47`错误。指定重复特征会导致`SQLCODE-44`错误。 `SELECTMODE`子句指定返回数据的模式。如果模式值是逻辑值,则返回逻辑值(内部存储)。例如,日期以`$HOROLOG`格式返回。如果模式值为`ODBC`,则应用逻辑到`ODBC`的转换,并返回`ODBC`格式值。如果模式值为`DISPLAY`,则应用逻辑到显示的转换,并返回显示格式值。如果模式值为`RUNTIME`,则可以通过设置`%SQL.Statement`类`%SelectMode`属性在执行时设置模式(设置为`LOGICAL`、`ODBC`或`DISPLAY`),运行时模式的值为`Logical`。为`SELECTMODE`指定的值将添加到`ObjectScript`类方法代码的开头:`#SQLCompile select=mode`。 `RESULTS`子句指定查询的结果。`RESULTS`子句中的`SQL`数据类型参数被转换为查询的`ROWSPEC`中相应的 IRIS数据类型参数。例如,`RESULTS`子句`RESULTS(Code VARCHAR(15))`生成`ROWSPEC`规`范ROWSPEC=“Code:%Library.String(MAXLEN=15)”`。 ## LANGUAGE 指定`CODE_BODY`使用的语言的关键字子句。允许的子句是`Language OBJECTSCRIPT`或`Language SQL`。如果省略`LANGUAGE`子句,则默认为`SQL`。 如果语言是`SQL`,则会生成`%Library.SQLQuery`类型的类查询。如果语言是`OBJECTSCRIPT`,则会生成`%Library.Query`类型的类查询。 ## code_body 要创建的查询的程序代码。可以在SQL或ObjectScript中指定此代码。使用的语言必须与`LANGUAGE`子句匹配。但是,在ObjectScript中指定的代码可以包含嵌入式`SQL`。 如果指定的代码是`SQL`,则它必须由单个`SELECT`语句组成。`SQL`中查询的程序代码以`BEGIN`关键字开头,后跟程序代码(`SELECT`语句)。在程序代码的末尾,指定分号(`;`),然后指定`END`关键字。 如果指定的代码是`OBJECTSCRIPT`,则它必须包含对 IRIS提供的`%Library.Query`类的`Execute()`和`Fetch()`类方法的调用,并且可以包含`Close()`、`FetchRows()`和`GetInfo()`方法调用。ObjectScript代码用大括号括起来。如果`EXECUTE()`或`FETCH()`丢失,则编译时会生成`SQLCODE-46`错误。 如果`ObjectScript`代码块将数据提取到局部变量(例如,`Row`)中,则必须以行`set Row=""`结束代码块,以指示数据结束条件。 如果查询公开为存储过程(通过在`Characteristic`中指定`PROCEDURE`关键字),则它使用过程上下文处理程序在过程及其调用方之间来回传递过程上下文。 调用存储过程时,`%Library.SQLProcContext`类的对象在`%sqlcontext`变量中实例化。这用于在过程及其调用者(例如,`ODBC`服务器)之间来回传递过程上下文。 `%sqlcontext`由几个属性组成,包括错误对象、`SQLCODE`错误状态、SQL行数和错误消息。下面的示例显示了用于设置其中几个值的值: ```java SET %sqlcontext.%SQLCODE=SQLCODE SET %sqlcontext.%ROWCOUNT=%ROWCOUNT SET %sqlcontext.%Message=%msg ``` `SQLCODE`和`%ROWCOUNT`的值由`SQL`语句的执行自动设置。每次执行前都会重置`%sqlcontext`对象。 或者,可以通过实例化`%SYSTEM.Error`对象并将其设置为`%sqlcontext.Error`来建立错误上下文。 IRIS使用提供的代码生成查询的实际代码。 # 示例 下面的嵌入式SQL示例创建名为`DocTestPersonState`的查询。它不声明任何参数,设置`SELECTMODE`特征,并采用默认语言(SQL): ```java ClassMethod CreateQuery() { &sql( CREATE QUERY DocTestPersonState() SELECTMODE RUNTIME BEGIN SELECT Name,Home_State FROM Sample.Person ; END ) if SQLCODE=0 { w !,"创建查询" } elseif SQLCODE=-361 { w !,"查询存在: ",%msg } else { w !,"创建 QUERY 错误 ",SQLCODE } } ``` 可以转到管理门户,选择Classes选项,然后选择`Samples`命名空间。将在那里找到由上面的示例创建的查询:`User.queryDocTestPersonState.cls`。在重新运行上面的程序示例之前,您可以从该显示中删除此查询。当然,可以使用`DROP QUERY`删除创建的查询。 ```java Class User.queryDocTestPersonState Extends %Library.RegisteredObject [ ClassType = "", DdlAllowed, Owner = {yx}, Not ProcedureBlock ] { Query DocTestPersonState() As %Library.SQLQuery(SELECTMODE = "RUNTIME") { SELECT Name,Home_State FROM Sample.Person } } ``` 下面的嵌入式SQL示例创建一个名为`DocTestSQLCODEList`的基于方法的查询,该查询获取`SQLCODE`及其说明的列表。它设置结果结果集特征,将语言设置为`ObjectScript`,并调用`Execute()`、`Fetch()`和`Close()`方法: ```java ClassMethod CreateQuery1() { &sql( CREATE QUERY DocTestSQLCODEList() RESULTS ( SQLCODE SMALLINT, Description VARCHAR(100) ) PROCEDURE LANGUAGE OBJECTSCRIPT Execute(INOUT QHandle BINARY(255)) { s QHandle=1, %i(QHandle)="" q ##lit($$$OK) } Fetch(INOUT QHandle BINARY(255), INOUT Row %List, INOUT AtEnd INT) { s AtEnd = 0, Row = "" s %i(QHandle) = $o(^%qCacheSQL("SQLCODE", %i(QHandle))) if %i(QHandle) = "" { s AtEnd = 1 q ##lit($$$OK) } s Row = $lb(%i(QHandle), ^%qCacheSQL("SQLCODE", %i(QHandle), 1, 1)) q ##lit($$$OK) } Close(INOUT QHandle BINARY(255)) { k %i(QHandle) q ##lit($$$OK) } ) if SQLCODE=0 { w !,"创建查询" } elseif SQLCODE=-361 { w !,"查询存在: ",%msg } else { w !,"创建 QUERY 错误 ",SQLCODE _ " "_%msg } } ``` 可以转到管理门户,选择Classes选项,然后选择`Samples`命名空间。将在那里找到由上面的示例创建的查询:`User.queryDocTestSQLCODEList.cls`。在重新运行上面的程序示例之前,可以从该显示中删除此查询。当然,可以使用`DROP QUERY`删除创建的查询。 下面的动态SQL示例创建名为DocTest的查询,然后使用`%SQL.Statement`类的`%PrepareClassQuery()`方法执行此查询: ```java ClassMethod CreateQuery2() { s SQLCODE = 0 /* 创建 Query */ s myquery=4 s myquery(1) = "CREATE QUERY DocTest() SELECTMODE RUNTIME " s myquery(2) = "BEGIN " s myquery(3) = "SELECT TOP 5 Name,Home_State FROM Sample.Person ; " s myquery(4) = "END" s tStatement = ##class(%SQL.Statement).%New() s qStatus = tStatement.%Prepare(.myquery) if qStatus '= 1 { w "%Prepare 失败:" DO $System.Status.DisplayError(qStatus) q } s rset = tStatement.%Execute() if SQLCODE = 0 { w !,"创建查询" } elseif SQLCODE=-361 { w !,"查询存在: ",%msg } else { w !,"创建 QUERY 错误 ",SQLCODE _ " "_%msg } /* 调用 Query */ w !,"调用 Query",! s cqStatus = tStatement.%PrepareClassQuery("User.queryDocTest","DocTest") if cqStatus'=1 { w "%PrepareClassQuery 失败:" d $System.Status.DisplayError(cqStatus) } s rset = tStatement.%Execute() w "Query 数据",!,! while rset.%Next() { d rset.%Print() } w !,"结束结束" /* 删除 Query */ &sql(DROP QUERY DocTest) if SQLCODE = 0 { w !,"删除 Query" } } ```
文章
Qiao Peng · 一月 17, 2022

精华文章--多语言字符集系列文章--第一篇 多语言字符集和相关标准简史

各大技术社区常年充斥着关于字符集支持、乱码的问题。Cache’/Ensemble/HealthConnect/IRIS的用户也经常遇到这类问题。为何文字乱码在信息化发展这么久后还会困扰我们?因为字符集、多语言实在有点复杂。 我计划写三篇:第一篇花点时间回顾一下多语言字符集的简史,第二篇介绍一下各种技术对于字符集和字符编码的使用声明,最后一篇会介绍常见的ISC技术和工具的乱码、尤其是中文乱码的现象和解决办法。第一篇 多语言字符集和相关标准简史 如果您已经了解多语言字符集和相关标准,请绕道此章。 相关概念 要理解多语言字符集,先了解一下相关概念。 字符(char):每个语言都有一系列特有的字符,例如英文26个字母、加减乘除等各种符号、中文汉字。 字形(glyphs):同样一个字符,有不同的写法、不同的风格和设计,就是不同的字形。 字体(font):字体算是计算机术语,是针对字符集的电子化的字符展现形式。 字符集(character set):通常是按语言归集的一个字符集合, 用于记录每个字符和对应的代码。 字符编码(encoding):针对字符集的特定编码格式。 字符集发展简史 电子字符集的发展历史很长,再加上不同语言字符集的复杂性,所以现在我们面对的字符集和字符编码是比较复杂的,而且相同字符集还有很多别名,在不同环境下看到的别名还不一样。下面仅介绍我们常见的字符集的发展历史,字符集全景远比这复杂。 1.1 单字节编码 1.1.1 ASCII(American Standard Code for Information Interchange): 它最初是Bell在1960年代基于电报码提出的,用于电传打印机。后成为ANSI标准(ANSI_X3.4-1968)。它使用7位2进制编码,表达128个字符:英文字母、阿拉伯数字和符号。这些符号包括可显示的符号,如加减乘除,也包含32个不能显示的控制符,如换行符(ASCII码为10进制数13)。 因为包含不能显示的符号,严格意义上,它不算字符集,而是一种字符编码。 128个字符码位,对于美国够用了,但随着计算机技术应用的扩展,其他拉丁语系国家的字符也需要表达,例如西欧语言里的变音字符,显然7位的ASCII不够用了。所以很多国家也基于7位编码,类似于ASCII,搞出了自己的字符集。这些字符集里,用ASCII的码位表达不同的字符。所以它们并不能与ASCII兼容。 1.1.2 ISO/IEC 8859、ANSI: 既然7位编码(128个)不够表达足够多的字符,最简单的办法是增加1位,就是8位2进制编码,这样就可以表达256个字符。而8位2进制数正好就是一个字节,用前面的128个编码位兼容ASCII,用后面增加的128个编码位表达其它拉丁字符,扩展基本没有难度。 最初ANSI发布了基于8位的标准,它就是这么做的,从而对各种欧洲国家的语言提供支持。所以8位的ASCII扩展字符集也叫做ANSI。后来ISO采用并扩展了该标准,就是ISO/IEC 8859系列字符集,例如ISO 8859-1 (Latin-1, 西欧语言) 、ISO 8859-2 (Latin-2, 中东欧语言)。这些字符集的低128个字符与ASCII一致,而高128个字符按不同的语言来安排不同的字符。所以,这些字符集并不互相兼容,差异就在这高128个字符。 注:要区别这些高128个字符到底是什么,微软公司使用代码页(code page)来确定当前使用什么字符集。Windows中可以用chcp来查看当前加载的代码页。另外,微软还将那些看不见的控制字符,替换成了可以显示的符号,例如笑脸符号。代码页应用到了今天,甚至包括了多字节编码,例如中文GBK的代码页是936。 1.2 双字节编码 1.2.1 GB2312、GBK、BIG5: 8位的编码基本可以解决拉丁语系的字符编码问题。可是以中日韩为代表的东亚文字字符数量远远超过了256个编码位。东亚国家开始考虑创建自己的数据集,即然一个8位的字节不足以编码这么多文字,最初大家就使用双字节来表达,从而创造出了一系列双字节字符集(Double Byte Character Set/DBCS),例如中文的GB2312、BIG5和日本的Shift JIS。 中国在1980年发布了GB2312这个中文字符集,思路与ANSI类似,但它使用2个8位字节编码,包含6763个汉字,同时它兼容ASCII。之后不久中国台湾地区发布了繁体字的BIG5(大五码)。 GB2312里的汉字覆盖面不够,尤其是用于人名、地名的字符。后续又出了一些列补充标准,如GB/T 7589 - 87、GB/T 12345 – 90,以及兼容ISO/IEC 10646-2003的GB13000。综合这些扩展,1995年发布了《汉字内码扩展规范》,也就是GBK,K就是扩展的意思。GBK包含 21003个汉字,大大提高了对生僻字的适用性。 1.3 多字节编码 在单字节、双字节编码的字符集时代,没有任何一个字符集能涵盖所有语言,这对于想在全球拓展业务的IT厂商,例如操作系统厂商、打印机厂商,是一个挑战。因此施乐、苹果等厂商于1988年组成统一码联盟,开发Unicode字符集。同时,ISO也开始开发ISO/IEC 10646 Universal Character Set (UCS) ,想统一字符集。很快双方就注意到对方在做相同的事情,并决定让正在开发的两个字符集标准兼容。ISO/IEC 10646定义了128组*256个平面*256行*256单元的编码空间,其中00组、00平面被称之为基本多语言平面(Basic Multilingual Plane,BMP),01到0F这15个平面称之为辅助多语言平面。理论可以编码20亿个字符,但由于有保留使用的编码范围,它实际可以编码679,477,248个字符。 1.3.1 Unicode(ISO/IEC 10646): Unicode早期(1991-1995)采用固定的2字节(16位)设计,理论可以编码65536个字符。1996年的Unicode 2.0开始,突破了这个限制,目前用到了21位(3个字节)的编码空间,未来计划扩展到31位(4个字节)。不过计算机表达时,统一用4个字节的表达形式:U+[4字节数],例如“中”字的Unicode码为:U+4E2D。它的目标是包含所有国家的文字,由于一些码位作为特殊用途保留,目前有超过1百万的可编码的码位。Unicode持续进化,截止2021年9月,已经发展到版本14,包含超过14万个字符。 它保留了前256个代码给ISO 8859-1,因此和ISO 8859-1、ASCII相兼容。注意,出于对之前字符集标准兼容的需求,有些相同的字符在Unicode中有多个编码,例如: Unicode有UTF-8、UTF-16、UTF-32这3种编码方式。UTF是Unicode Transformation Format的缩写,也就是对Unicode的编码方式。Unicode是4字节编码,而UTF-8、UTF-16都是变长编码,UTF-32是固定4字节编码并与Unicode码一致。 为何不直接使用Unicode,而是要使用UTF编码方式呢?原因挺多:要兼容之前的应用和代码、要降低编码长度从而节省磁盘、内存… UTF-8:在互联网世界最为流行,它以8位2进制作为一个字节,使用1个字节到4个字节来表达特定字符。对于兼容ASCII的字符,UTF-8使用1个字节就够了,而且编码与ASCII完全一致,因此既节省了内存和磁盘的存储空间,又兼容了之前的应用和代码。对于其它字符,UTF-8可以使用更多字节表达,并可以表达所有Unicode字符。 如何保证使用2个字节表达的UTF-8码不会与1个字节表达的UTF-8码产生混淆?UTF-8设计了一套规范,通过每个UTF-8开始字节的规律确保不会发生混淆,例如单字节的UTF-8码的字节第一位是0(2进制),而双字节的UTF-8码的首字节由110开始,三字节的UTF-8码的首字节由1110开始,四节字的UTF-8码的首字节由11110开始。下图是UTF-8的编码空间和编码规律。 UTF-16: 也是非常流行的Unicode编码方案,例如Windows就是用UTF-16编码,在很多Windows应用中所说的Unicode编码,其实就是UTF-16。它采用1个或2个16位编码单位。UTF-8以8位字节为单位,多字节UTF-8码严格按字节顺序表达即可。但UTF-16编码单位为16位(2个8位字节),那么就会产生这2个字节哪个放在前面的问题:大端序(Big-Endian)还是小端序(Little-Endian)*。不同的操作系统使用不同的顺序,例如Linux和Windows使用小端序,而Unix使用大端序。 既然要区分不同的端序,那就给文件/字符流的头部增加一个标识符吧,这就是BOM(Byte Order Mark),标识大端码时用0xFE 0xFF、标识小端码是用0xFF 0xFE。而FFFE(FEFF)不是任何Unicode字符的编码,从而不会造成误解。 *注:大小端序来自小说《格列佛游记》,故事里2个小人国为从大端敲开鸡蛋还是小端敲开鸡蛋发生战争。 *注:UTF-8并不需要BOM,因为不会产生类似的混淆。虽然不推荐,但的确有为UTF-8的BOM,它是0xEF 0xBB 0xBF,通常被用来标记这是UTF-8的文件。有时它会造成显示问题。 最初UTF-16用1个16位编码,可以编码65536个字符,涵盖常见中文字符没有问题,这个编码方案也被称为UCS-2,它定义的字符范围就是基本多语言平面( Basic Multilingual Plane,BMP)的字符范围。但随着2006年GB18030-2005的强制标准实施,需要包含GB18030里的7万多个汉字,因此UCS-2方案不够用了,需要动用15个辅助多语言平面。 1.3.2 GB18030: 随着ISO开发ISO/IEC 10646,中国也开始了GB13000的开发,它和ISO/IEC 10646一致,将中文字符编码进ISO/IEC 10646编码空间。1993年发布了GB13000-1993《信息技术 通用多八位编码字符集(UCS)第一部分:体系结构与基本多文种平面》,不过它和之前已经广泛使用的GB2312编码不兼容,所以并没有大规模采用。 随后,在2000年推出了GB18030,并在2005年修订。它的特点是兼容ASCII和GB2312码、基本兼容GBK、并包含Unicode中的所有中文字符。这是一个强制标准,是目前执行的中文字符集标准。它包含70244个汉字字符,采用单字节、双字节、四字节变长编码。单字节部分就是ASCII码,中文字符在双字节和四字节编码空间。 那么GB18030和Unicode到底是什么对应关系?GB18030兼容GB2312的部分,由于GB2312的开发早于Unicode,因此这部分GB18030(GB2312)编码与Unicode码不连续相关,因此是没办法通过算法来做二者编码的转换,只能通过对照表来关联和转换,如下图: 而另一部分的GB18030编码是在Unicode标准之后开发的,这部分与Unicode连续相关,也就是通过算法就能做二者间的转码,如下图: 这也是为什么有些文献说GB18030也是一种Unicode编码。 通过上面的字符集简史能看出,对于我们可能遇到各种字符集和不同的编码:ASCII、ANSI、Unicode、UTF-8、UTF-16、GB2312、GB18030… 我们怎么知道在数据交换中对方用的是什么字符集和字符编码? 下一个章节,我们将汇总不同的技术对于字符集/字符编码使用的声明。