搜索​​​​

清除过滤器
文章
Hao Ma · 一月 30, 2021

WebGateway系列(3): 配置IIS连接IRIS

IIS在Windows Server里是默认安装,在Windows7和Windows10里面需要用户到"控制面板>程序"里面找到"Turn Windows features on or off"的设置来启动。 本文假设IIS已经在用户的服务器上启动,并且正常工作。 通常情况下,当IRIS安装在Windows系统时,用户会在同一台机器上使用IIS,很少有单独安装一台IIS服务器给远程IRIS提供Web服务器的,当然这样也绝无问题。 有两个软件包可以安装CSP Gateway。一个是IRIS的安装包。在IRIS的安装过程中, 如果有IIS正在工作, 那么安装程序会自动的安装CSP Gateway。 2018年以前的Ensemble或者Cache'的版本的安装过程中会跳出一个询问框,让用户选择是否"安装CSP网关。。。”。而后来的版本大多不做询问而自动为用户做了选择。 如果需要强制安装或者不安装CSP Gateway,那么需要在安装中选"Customer Component"设置。 另一个软件包是单独的CSP Gateway安装包, 可以在InterSystems的WRC或者工程师处得到。这是一个只有10多兆的很小的安装包,它只安装CSP Gateway, 并不安装IRIS实例,适合在单独的IIS硬件服务器上安装CSP Gateway; 或者, 当一个服务器上已有了IRIS, 但后来想添加IIS服务和CSP Gateway,用这个单独的安装包也很方便。 单独的CSP Gateway的安装会在IIS的工作目录“C:\Inetpub"下添加子目录CSPGateway,或者WebGateway, 然后在IIS的default Website上添加CSP Gateway的配置。整个配置相当简单。下面的步骤是在Windows10上单独安装Web Gateway安装包2020.1连接同一台服务器上的IRIS的的过程。 其中后面配置CSP Gateway访问IRIS应用的部分可以参考Apache上配置CSP Gateway的文档。 ## CSP Gateway安装 1. 使用浏览器访问 http://localhost, 显示IIS的欢迎主页, 确认IIS已工作。 2. 打开WebGateway-20201.1.xxxx-win_x64应用程序开始安装。安装时会提示用户输入连接的IRIS Server的IP地址和端口。默认的Application Name用CSP,IP地址端口使用localhost, 51773。安装过程会重启IIS服务,安装结束后用户不用手工重启。 3. 安装后的检查IIS - 检查Web Gateway安装后的文件。 确认在IIS的安装目录(默认为C:\inetput)下安装了CSPGateway子目录, 其中包含若干dll文件。它们是IIS中用到的CSP Gateway的模块,在不同的CSP Gateway版本中这些Dll文件的数量和名字稍有不同。 - 创建了c:/intersystems/WebGateway的文件夹,早些的版本中并不创建这个目录。 - 打开IIS配置界面, 确认在Default Site下安装了CSP application. 在某些版本的Web Gateway安装后, /csp被安装为Virtual Directory, 关于IIS中application和virtual diectory的区别请自行查询, 无论安装成那种类型, 对csp的使用和配置没有区别。使用HealthConnect 2018以前版本的安装包安装的CSP Gateway生成CSP和ensemble两个Application。 4. 查看CSP Gateway登录页面。 登录http://localhost/csp/bin/Systems/Module.cxw。 在主页上会显示Web Gateway的版本, Web Server Type是"Microseof-IIS"。配置文件和日志文件在c:\Inetpub\CSPGateway目录下。 (如果是打开时出现HTTP错误500.19, 你需要重启IIS) 5. 查看连接的IRIS Server。 从左边菜单栏进入Server Access配置界面。其中csp是默认安装的IRIS连接。2018以前的版本可能用的是LOCAL。不管怎么说,使用"Edit Server"打开,可以看到连接的IRIS的端口和设置。 配置UserName"CSPSystem"和Password "SYS",并保存。 在左边菜单栏,使用"Test Server Connection"测试到“csp"的连接,测试成功会显示IRIS的版本。 Test Server Connection Server connection test was successful: csp (localhost:51773) $ZVersion: IRIS for Windows (x86-64) 2020.2 (Build 199U) Tue Apr 28 2020 17:17:56 EDT 6. 访问IRIS维护主页 (可选) 从链接 http://WebServer/csp/sys/Utilhome.csp 访问IRIS维护主页System Management Portal应该可以成功了,但您会发现有部分网页内容(element)无法加载。这是因为在默认的安装中,只将"csp cls zen cxw"这4种类型的请求发送给CSP Gateway, 而被称为Static file的文件,比如.js, .css, .png等等类型的文件并没有被发送给CSP Gateway. 这是另外的一个安全机制,强制客户人工的配置是否需要从Web服务器访问IRIS维护主页。如果答案是NO, 那么访问IRIS维护页面就只能通过PWS,用IRIS服务器的52773的接口。 如果用户认为从Web服务器访问IRIS维护页面是必要的, 需要修改CSPFileTypes配置,把任意类型的请求发送给IRIS。 7. 访问IRIS上的其他Web Application IRIS上其他的Web Application, 比如”/api", ”/test"等等,通常情况下当Web Application被添加后,配置会自动同步到Web Gateway, 用户不用去Web Gateway的页面配置路由。但IIS上必须填写响应的配置,才能把请求从IIS发送到CSP Gateway. 这样操作: - 添加applications。如下图所示, 在IIS的default site下添加了两个新application, test和api. - 为每个applcation配置"Handler Mappings"。**使用右边动作栏中的”Add Module Mapping",而不是另3种动作。** 还要注意不要勾选"Request Restrictions"的"invoke handler only if request is mapped to"选择框,这样在"Handler Mappings"页面看到的Path Type一栏中显示的是"Unspecified", 否则会显示"File"或者其他内容。 - 测试访问一些应用, 比如 http://172.16.58.200/api/mgmnt/v2/ http://172.16.58.200/test/test.webservice1.cls 一般情况下,到目前为止IIS已经能够正常工作,将需要的请求发送给IRIS。如果出现故障或者需要调整CSP gateway的配置,请参考[WebGateway系列_配置Apache连接IRIS] 请求如何在nginx配置呢 @Hao.Ma 马老师给看看? 目前只有在线文档, 回头我们写一个贴上来
文章
Hao Ma · 一月 15, 2021

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

在这个由三个部分组成的系列文章中,介绍了如何在OAuth 2.0标准下使用IAM简单地为IRIS中的未经验证的服务添加安全性。 第一部分介绍了一些OAuth 2.0背景知识,以及IRIS和IAM的一些初始定义和配置,以帮助读者理解确保服务安全的整个过程。 第二部分详细讨论和演示了配置IAM所需的步骤——验证传入请求中的访问令牌,并在验证成功时将请求转发到后端。 本系列的最后一部分将讨论和演示IAM生成访问令牌(充当授权服务器)并对其进行验证时所需的配置,以及一些重要的最终考虑事项。 如果您想试用IAM,请联系InterSystems销售代表。 场景2:IAM作为授权服务器和访问令牌验证器 与上个场景不同的是,该场景中将使用一个名为“OAuth 2.0 Authentication”的插件。 如果要在资源所有者密码凭证流中将IAM作为授权服务器使用,客户端应用程序必须对用户名和密码进行身份验证。只有在身份验证成功时,才能发出获取IAM访问令牌的请求。 首先,将其添加到“SampleIRISService”中。正如下面截屏所示,需要填充一些不同的字段来配置此插件。 首先,将“SampleIRISService”的ID粘贴到“service_id”字段中,这样就可以在服务中启用该插件。 在“config.auth_header_name”字段中,需要指定携带授权令牌的头名称。本例中,我保留默认值“authorization”。 “OAuth 2.0 Authentication”插件支持的OAuth 2.0流包括授权码授权(Authorization Code Grant)、客户端凭证(Client Credentials)、隐式授予(Implicit Grant)或资源所有者密码凭证授权(Resource Owner Password Credentials Grant)。我们在本文中使用的是“资源所有者密码凭证”流,故选中“config.enable_password_grant”。 在“config.provision_key”字段中输入要用作配置密钥的字符串。此值用来向IAM请求访问令牌。 本例中,我保留了所有其他字段的默认值。可以在此处查看插件文档中每个字段的完整引用。 下面是插件配置的最终效果: 创建插件后,需要为“ClientApp”客户端创建凭证。 为此,打开左侧菜单上的“Consumers”,然后单击“ClientApp”。接下来,点击“Credentials”标签,然后点击“New OAuth 2.0 Application”按钮。 在下个页面的“name”字段输入名称,以标识应用程序,在“client_id”和“client_secret”字段分别定义客户端ID和客户端密钥,最后在应用程序中输入URL,在“redirect_uri”字段上授权后,用户将被发送到该URL。然后,单击“Create”。 现在,可以发送请求了。 需要发出的第一个请求是获取IAM访问令牌。“OAuth 2.0 Authentication”插件自动创建一个端点,并将“/oauth2/token”路径附加到已经创建的路由上。 注意:必须使用HTTPS协议和IAM的代理端口(默认端口为8443)监听TLS/SSL请求。这是OAuth 2.0规范要求。 因此在本例中,需要向URL发出一个POST请求: https://iamhost:8443/event/oauth2/token 请求主体中应包括以下JSON: { "client_id": "clientid", "client_secret": "clientsecret", "grant_type": "password", "provision_key": "provisionkey", "authenticated_userid": "1" } 如上所示,该JSON包含了在创建“OAuth 2.0 Authentication”插件时定义的值(如“grant_type”和“provision_key”),以及在创建客户端凭证时定义的值(如“client_id”和“client_secret”)。 当提供的用户名和密码成功通过身份验证后,客户端应用程序还应该添加“authenticated_userid”参数值作为已通过身份验证的用户的唯一标志。 该请求及其响应如下: 现在可以请求从上面的响应中获取事件数据(包括“access_token”值),并作为对URL的GET请求中的“bearner token” https://iamhost:8443/event/1 如果访问令牌过期,可以使用收到的刷新令牌和过期的访问令牌一起生成一个新的访问令牌,方法是向用于获取访问令牌的相同端点发出POST请求,但主体略有不同: { "client_id": "clientid", "client_secret": "clientsecret", "grant_type": "refresh_token", "refresh_token": "E50m6Yd9xWy6lybgo3DOvu5ktZTjzkwF" }  该请求及其响应如下: “OAuth 2.0 Authentication”插件的一个有趣很好的特性是能够查看和禁用访问令牌。 若要查看令牌列表,向以下IAM的Admin API终端发送GET请求即可: https://iamhost:8444/{workspace_name}/oauth2_tokens 其中{workspace_name}是IAM工作区的名称。如果启用了RBAC,则需要输入必要的凭证才能调用IAM Admin API。 注意,“credential_id”是在ClientApp客户端内部创建的OAuth应用程序(本例中名为SampleApp)的id,“service_id”是应用此插件的“SampleIRISService”的id。 要想禁用令牌,可以向以下端点发送删除请求 https://iamhost:8444/Sample/oauth2_tokens/{token_id} 其中{token_id}是要禁用的令牌id。 如果尝试使用无效令牌,将包含该无效令牌的GET请求作为Bearer Token 发送到URL,则会收到一条消息,提示令牌无效或已过期: https://iamhost:8443/event/1 最后需要考虑的因素 本文演示了如何将IAM中的OAuth 2.0身份验证添加到IRIS中未经身份验证的服务中。需要牢记的是,IRIS中的服务本身仍是未经身份验证的。因此,如果有人绕过IAM层直接调用IRIS服务端点,则可以在不经过任何身份验证的情况下查看信息。出于该原因,在网络级别设置安全规则以防止不必要的请求绕过IAM层是很重要的。 可以在此处了解更多有关IAM的信息。 如果您想试用IAM,请联系InterSystems销售代表。
文章
姚 鑫 · 五月 27, 2021

第八章 处理收到的电子邮件

# 第八章 处理收到的电子邮件 # 处理收到的电子邮件 本节介绍如何处理通过`%Net.POP3`检索到的电子邮件(`%Net.MailMessage`)。 ## Message Basics 检索电子邮件(`%Net.MailMessage`)后,通常首先确定它是哪种类型的邮件以及如何阅读它;也就是说,它是否是多部分邮件以及各部分是否是二进制的。在此步骤中,您可以使用`ContentType`属性。或者,可以使用`IsBinary`、`IsHTML`和`IsMultiPart`属性,它们间接提供与`contentType`相同的信息。 如果消息是多部分消息,则每个部分都是`%Net.MailMessagePart`的一个实例。 ## Message Headers 消息本身和消息的每个部分都有一组标头。 `%Net.MailMessage`和`%Net.MailMessagePart`类提供的属性使可以轻松访问最常用的标头。例如,`%Net.MailMessage`提供收件人、发件人、主题和日期等属性。`Headers`数组属性允许访问任何自定义标题. 此外,如果已通过`%Net.POP3`检索到消息,则可以使用`GetAttribute()`方法。在给定标头名称和属性的情况下,此方法返回该属性的值。 ## Message Contents 了解常规消息结构后,请使用以下技术检索内容: - 对于多部分消息,请使用`Parts`属性,该属性是部分的数组。`Parts.Count()`给出部件的数量。每个部件的键都是一个整数,从1开始。使用`GetAt()`方法检索给定的部件。消息部分是`%Net.MailMessagePart`的实例。 - 对于二进制消息(或消息部分),请使用`BinaryData`属性。 - 对于文本消息(或消息部分),请使用`TextData`属性。 - 如果`IsHTML`为0,则`TextData`属性为普通文本字符串。 - 如果`IsHTML`为1,则`TextData`属性为HTML文本字符串。 请注意,发送邮件的电子邮件客户端确定邮件中的任何包装。邮件服务器无法控制这一点, ## 其他消息信息 `MessageSize`属性表示邮件的总长度(不包括任何附加的电子邮件)。 以下方法提供有关消息的其他信息: ### GetLocalDateTime() 返回检索消息的日期和时间,并转换为`$HOROLOG`格式的本地时间。 ### GetUTCDateTime() 返回检索消息的日期和时间,并以`$HOROLOG`格式转换为UTC。 ### GetUTCSeconds() 返回自1840年12月31日以来检索消息的日期和时间(秒)。 以下类方法也可用于时间/日期转换: ### HToSeconds() 将`$HOROLOG`格式的日期/时间转换为自1840年12月31日以来的秒的类方法。 ### SecondsToH() 将自1840年12月31日以来的秒数转换为`$HOROLOG`格式的日期/时间的类方法。 示例1:`ShowMsgInfo()` ```java ClassMethod ShowMsgInfo(msg as %Net.MailMessage) { Write "Message details *****",! Write "To (count): ", msg.To.Count(),! Write "From: ", msg.From,! Write "Cc (count): ", msg.Cc.Count(),! Write "Bcc (count): ", msg.Bcc.Count(),! Write "Date: ", msg.Date,! Write "Subject: ", msg.Subject,! Write "Sender: ", msg.Sender,! Write "IsMultipart: ", msg.IsMultiPart,! Write "Number of parts: ", msg.Parts.Count(),! Write "Number of headers: ", msg.Headers.Count(),! Write "IsBinary: ", msg.IsBinary,! Write "IsHTML: ", msg.IsHTML,! Write "TextData: ", msg.TextData.Read(),! Write "BinaryData: ", msg.BinaryData.Read(),! } ``` 此方法产生类似于以下内容的输出: ```java Message details ***** To (count): 1 From: "XXX XXX" Cc (count): 0 Bcc (count): 0 Date: Fri, 16 Nov 2007 11:57:46 -0500 Subject: test 5 Sender: IsMultipart: 0 Number of parts: 0 Number of headers: 16 IsBinary: 0 IsHTML: 0 TextData: This is test number 5, which is plain text. BinaryData: ``` 示例2:`ShowMsgPartInfo()` 以下方法写入有关消息一部分的信息: ```java ClassMethod ShowMsgPartInfo(msg as %Net.MailMessage, partno as %Integer) { Set part=msg.Parts.GetAt(partno) Write "Message part details *****",! Write "Message part: ", partno,! Write "IsMultipart: ", part.IsMultiPart,! Write "Number of parts: ", part.Parts.Count(),! Write "Number of headers: ", part.Headers.Count(),! Write "IsBinary: ", part.IsBinary,! Write "IsHTML: ", part.IsHTML,! Write "TextData: ", part.TextData.Read(),! Write "BinaryData: ", part.BinaryData.Read(),! } ``` 这将产生与以下内容类似的输出(给定的消息与前面显示的不同): ```java Message part details ***** Message part: 1 IsMultipart: 0 Number of parts: 0 Number of headers: 2 IsBinary: 0 IsHTML: 0 TextData: 1 test string BinaryData: ``` 示例3:`ShowMsgHeaders()` 下面的方法写入有关消息头的信息;可以编写一个类似的方法,对消息部分执行相同的操作。 ```java ClassMethod ShowMsgHeaders(msg as %Net.MailMessage) { Set headers=msg.Headers Write "Number of headers: ", headers.Count(),! //iterate through the headers Set key="" For { Set value=headers.GetNext(.key) Quit:key="" Write "Header:",key,! Write "Value: ",value,!! } } ``` 这将产生类似于以下内容的输出: ```java Number of headers: 16 Header: content-class Value: urn:content-classes:message Header: content-type Value: multipart/alternative; boundary="----_=_NextPart_001_01C8286D.D9A7F3B1" Header: date Value: Fri, 16 Nov 2007 11:29:24 -0500 Header: from Value: "XXX XXX" Header: message-id Value: Header: mime-version Value: 1.0 ... ``` # 自动编码和字符翻译 电子邮件部分包含有关使用的字符集和使用的内容传输编码(如果有的话)的信息。作为参考,本节介绍如何使用此信息。 ## 外发电子邮件 `%Net.SMTP`检查每个部分的字符集属性,然后应用适当的转换表。 如果未指定给定部件的字符集属性,InterSystems IRIS将使用UTF-8。 `%Net.SMTP`还检查`ContentTransferEncoding`属性。如果此属性为 `"base64"`或`"quoted-printable"`,则在创建消息时,`%Net.SMTP`会根据需要对正文进行编码。(如果内容传输编码为 `"7bit"` 或 `"7bit"`,则不需要编码。) 重要提示:请注意,如果内容为`“Base64”`编码,则不能包含任何`Unicode`字符。如果要发送的内容包括`Unicode`字符,请确保使用`$ZCONVERT`将内容转换为UTF-8。 ## 传入电子邮件 `%Net.POP3`检查每个邮件部分的`Content-Transfer-Encoding`标头,并根据需要对正文进行解码。 然后`%Net.POP3`检查每个邮件部分的`Content-Type`标头。这会影响消息部分的字符集属性,还会控制在InterSystems IRIS中创建消息部分时使用的转换表。
文章
Michael Lei · 七月 3, 2023

基于LangChain的IRIS ChatGPT – 释放大语言模型LLM的全部潜力

你好社区在本文中,我将介绍我的应用程序irisChatGPT ,它是基于LangChain Framework构建的。首先,让我们对框架进行一个简单的概述。 全世界都在谈论ChatGPT以及大型语言模型 (LLM) 如何变得如此强大,并且表现超出预期,提供类似人类的对话。这只是将其应用于每个企业和每个领域的开始! 剩下的最重要的问题是如何将这种能力应用于适合企业需求的特定领域数据和特定场景响应行为。 LangChain为这个问题提供了结构化且有效的答案! LangChain 技术可以帮助实现法学硕士的巨大潜力,通过围绕法学硕士提供抽象层并使法学硕士的使用变得简单有效,从而构建令人惊叹的应用程序。 LangChain 是一个框架,可以快速轻松地开发使用大型语言模型(例如 GPT-3)的应用程序。 然而,该框架引入了额外的可能性,例如,轻松使用外部数据源(例如维基百科)来放大模型提供的功能。我相信你们都可能尝试过使用 Chat-GPT,并发现它无法回答特定日期之后发生的事件。在这种情况下,在维基百科上搜索可以帮助 GPT 回答更多问题。 LangChain结构 该框架分为六个模块,每个模块允许您管理与法学硕士互动的不同方面。让我们看看这些模块是什么。 模型:允许您实例化和使用三种不同类型的语言模型,它们是: 大型语言模型 (LLM):这些能够理解自然语言的基础机器学习模型。它们接受输入中的字符串并在输出中生成字符串。 聊天模型:由 LLM 支持的模型,但专门用于与用户聊天。您可以在这里阅读更多内容。 文本嵌入模型:这些模型用于将文本数据投影到几何空间中。这些模型将文本作为输入并返回数字列表,即文本的嵌入。 提示:提示是我们如何与模型交互以尝试从中获取输出。现在知道如何编写有效的提示至关重要。这个框架模块可以让我们更好的管理提示。例如,通过创建我们可以重用的模板。 索引:最好的模型通常是与一些文本数据相结合的模型,以便为模型添加上下文或解释某些内容。这个模块可以帮助我们做到这一点。 链:很多时候,要解决任务,对 LLM 的单个 API 调用是不够的。该模块允许集成其他工具。例如,一个调用可以是一个组合链,其目的是从维基百科获取信息,然后将此信息作为模型的输入。该模块允许连接多个工具以解决复杂的任务。 内存:该模块允许我们在模型调用之间创建持久状态。能够使用记住过去说过的话的模型肯定会改善我们的应用程序。 代理:代理是一个法学硕士,它做出决定,采取行动,观察其所做的事情,并以这种方式继续,直到完成其任务。该模块提供了一组可以使用的代理。 现在让我们更详细地了解一下如何利用不同的模块来实现代码。 Langchain工作原理 步骤1 :用户向LangChain发送问题第2步 :LangChain将此问题发送至Embedding Model步骤3:嵌入模型将文本转换为向量,文本以向量形式存储在数据库中并返回给LangChain步骤4 :LangChain将这些向量发送到向量数据库(有多个向量数据库,我们在我们的应用程序中使用chroma)步骤5:向量数据库返回前 K 个近似最近邻 ( KNN ) 向量第6步:LangChain 将问题与KNN向量一起发送到大型语言模型 (LLM) (我们在应用程序中使用 OpenAI)步骤7:LLM向Langchain返回答案步骤8:Langchain将答案返回给用户 关于申请 irisChatGPT应用程序利用围绕大型语言模型 (LLM) 构建的最热门 Python 框架LangChain的功能。 LangChain 是一个框架,可以快速轻松地开发使用大型语言模型的应用程序。应用程序是在系统间嵌入式 Python功能的帮助下使用 objectscript 构建的。它还包含Streamlit Web 应用程序,这是一个开源 Python 应用程序框架,用于为数据科学和机器学习创建漂亮的 Web 应用程序。 特征 以下是应用程序功能列表以及相关屏幕截图 内置Intersystems ObjectScript 参考ChatGPT 内置InterSystems 大奖赛 2023 ChatGPT 使用 SQLDatabaseChain 回答有关缓存数据库的问题 创建您自己的 chatGPT 模型并与其聊天 OpenAI 聊天GPT 维基百科搜索 使用DuckDuckGo(DDG)通用搜索引擎在互联网上搜索 使用Python REPL LangChain功能生成Python代码 Streamlit Web 应用程序在线演示 谢谢
文章
Michael Lei · 七月 4, 2023

小实验--生成式AI和 FHIR 结合

这是个实验项目,使用OpenAI API与FHIR资源和Python相结合来回答医疗行业的用户提问。 ## 项目想法 生成式人工智能,如[OpenAI上提供的LLM模型](https://platform.openai.com/docs/models), 已被证明在理解和回答高层次问题方面具有显著能力。他们使用大量的数据来训练他们的模型,因此他们可以回答复杂的问题。 他们甚至可以[使用编程语言,根据提示创建代码](https://platform.openai.com/examples?category=code) --我不得不承认,让我的工作自动化的想法让我感到有些焦虑。但到目前为止,似乎这是人们必须要习惯的事情,不管你喜不喜欢。所以我决定做一些尝试。 这个项目的主要想法是在我读到[这篇文章](https://the-decoder.com/chatgpt-programs-ar-app-using-only-natural-language-chatarkit/)关于[ChatARKit项目](https://github.com/trzy/ChatARKit)时产生的。这个项目使用OpenAI的API来解释语音命令,在智能手机摄像头的实时视频中渲染3D物体--非常酷的项目。而且,这似乎是一个热门话题,因为我发现最近有一篇[论文](https://dl.acm.org/doi/pdf/10.1145/3581791.3597296)遵循类似的想法。 让我最担心的是使用ChatGPT对AR进行**编程。由于有一个开放的github repo,我搜索了一下,发现[作者是如何使用ChatGPT生成代码的](https://github.com/trzy/ChatARKit/blob/master/iOS/ChatARKit/ChatARKit/Engine/ChatGPT.swift)。这种技术被称为*提示工程Prompt Engineering*--[这是维基百科关于它的文章](https://en.wikipedia.org/wiki/Prompt_engineering),或者这两个更实用的参考资料: [1](https://microsoft.github.io/prompt-engineering/)和[2](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/concepts/advanced-prompt-engineering?pivots=programming-language-chat-completions)。 所以我想--为什么不结合FHIR和Python试试类似的东西?以下是我的想法: ![Project basic idea](https://community.intersystems.com/sites/default/files/inline/images/project-diagram-01_4.png) 其主要构成是: - 一个提示工程模块,将命令人工智能模型使用FHIR和Python - 一个OpenAI API集成模块 - 一个Python解释器,用于执行生成的代码 - 一个FHIR服务器,回答人工智能模型生成的查询 基本思路是使用[OpenAI Completion API](https://platform.openai.com/docs/api-reference/completions),要求人工智能将问题分解为一堆FHIR查询。然后,人工智能模型创建一个Python脚本来处理InterSystems IRIS for Health中FHIR服务器返回的FHIR资源。 如果这个简单的设计是有效的,用户就可以得到应用的分析模型尚未支持的问题的答案。此外,这些由人工智能模型回答的问题可以被分析,以发现对用户需求的新见解。 这种设计的另一个好处是,你不需要用外部的API暴露你的数据和模型。例如,你可以问关于病人的问题,而不需要将病人数据或你的数据库模式发送到人工智能服务器上。由于人工智能模型使用公共可用的功能--FHIR和Python,你也不需要发布内部数据。. 但是,这种设计也导致了一些问题,比如: - 如何引导人工智能根据用户需求使用FHIR和Python? - 人工智能模型产生的答案是否正确?是否有可能对它们有信心? - 如何处理运行外部生成的Python代码的安全问题? 因此,为了尝试解决一下这些问题,我对最初的设计做了一些阐述,得到了这个: ![Project refined idea](https://community.intersystems.com/sites/default/files/inline/images/project-diagram-02_2.png) 我在项目里增加了一些新的元素: - 一个代码分析器来扫描安全问题 - 一个日志记录器,用于记录重要事件,以便进行进一步分析 - 一个用于进一步整合的API REST 因此,这个项目旨在验证这个概念,它可以支持实验来收集信息,以尝试回答这些问题。 在接下来的章节中,你会发现如何安装该项目并试用它。 然后,你会看到我在尝试回答上述问题时得到的一些结果和一些结论。 希望你觉得它有用。我们也非常欢迎你为这个项目做出贡献! ## 项目尝试 要试一试,请打开IRIS终端,运行以下内容: ```objectscript ZN "USER" Do ##class(fhirgenerativeai.FHIRGenerativeAIService).RunInTerminal("") ``` 例如,以下问题被用来测试该项目: 1. 数据集里有多少病人? 2. 病人的平均年龄是多少? 3. 给我所有的条件(代码和名称),去除重复的。将结果以表格的形式呈现出来。(不要使用pandas) 4. 有多少病人患有病毒性鼻窦炎(代码444814009)? 5. 病毒性鼻窦炎(代码444814009)在患者群体中的流行率是多少?对于多次出现相同病情的患者,考虑只打一次就可以计算出来。 6. 在病毒性鼻窦炎(代码444814009)患者中,性别组的分布是怎样的? 你可以找到这些问题的输出例子[这里](https://github.com/jrpereirajr/iris-fhir-generative-ai/blob/master/misc/tests-accuacy)。 > 请注意,如果你在你的系统上尝试,结果可能会有所不同,即使你使用相同的提示。这是由于LLM模型的随机性。 这些问题是由ChatGPT提出的。他们要求这些问题是以复杂程度不断提高的方式来创建的。第3个问题是个例外,它是由作者提出的。 ## 提示工程Prompt Engineering 项目使用的提示Prompt可以在方法`GetSystemTemplate()`中找到[这里](https://github.com/jrpereirajr/iris-fhir-generative-ai/blob/master/src/fhirgenerativeai/PromptService.cls)。 它遵循提示工程的指南,首先你给人工智能模型分配一个角色,然后输入一堆限制条件和指令。它的每个部分的意图都有注释,所以你可以理解它是如何工作的。 请注意一种接口定义的使用,当模型被指示假设一个已经定义好的名为`CallFHIR()`的函数与FHIR交互,而不是自己声明一些东西。这是受ChatARKit项目的启发,作者在该项目中定义了一整套函数,为使用AR库抽象出复杂的行为。 在这里,我使用这个技术来避免直接创建代码进行HTTP调用的模式。 这里一个有趣的发现是关于强迫人工智能模型以XML格式返回其响应。由于打算返回的是Python代码,我在XML中使用了CDATA块,将其对称化。 尽管在提示中明确指出响应格式必须是XML格式,但在以XML格式发送用户提示后,AI模型就开始遵循这个指令。你可以在上面提到的同一个类中的`FormatUserPrompt()`方法中看到这一点。 ## 代码分析器 该模块使用[bandit库](https://bandit.readthedocs.io/en/latest/)来扫描安全问题。 这个库生成Python程序的AST,并针对常见的安全问题对其进行测试。你可以在这些链接中找到被扫描的问题种类: - [测试插件](https://bandit.readthedocs.io/en/latest/plugins/index.html#complete-test-plugin-listing) - [调用黑名单](https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html) - [导入黑名单](https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html) 由人工智能模型返回的每一个Python代码都会针对这些安全问题进行扫描。如果发现有问题,就会取消执行并记录错误。 ## 日志记录器 所有的事件都被记录下来,以便在表[LogTable](https://github.com/jrpereirajr/iris-fhir-generative-ai/blob/master/src/fhirgenerativeai/LogTable.cls)中作进一步分析。 每个回答问题的运行都有一个会话ID。你可以在表中的'SessionID'列中找到它,并通过将它传递给方法`RunInTerminal("", )`来获得所有事件。例如: ```objectscript Do ##class(fhirgenerativeai.FHIRGenerativeAIService).RunInTerminal("", "asdfghjk12345678") ``` 你也可以用这个SQL来检查所有的日志事件: ```sql SELECT * FROM fhirgenerativeai.LogTable order by id desc ``` ## 测试 我执行了一些测试以获得信息来衡量人工智能模型的性能。 每个测试执行了15次,它们的输出被存储在[this](https://github.com/jrpereirajr/iris-fhir-generative-ai/blob/master/misc/tests-accuacy)和[this](https://github.com/jrpereirajr/iris-fhir-generative-ai/blob/master/misc/tests-security)的目录下。 > 请注意,如果你在你的系统上尝试,结果可能会有所不同,即使你使用相同的提示。这是由于LLM模型的随机性。 ### 准确率 对于问题#1的测试,有`14个结果6`和`1个错误`。正确值是`6'。所以它是`100%`正确的,但有`6%`的执行失败。 验证#1结果的SQL语句: ```sql SELECT count(*) FROM HSFHIR_X0001_S.Patient ``` 对于第2题的测试,有`3个结果52`,`6个结果52.5`和`6个错误`。正确的数值--考虑到有小数点的年龄,是`52.5'。所以我认为这两个值都是正确的,因为这一点差异可能是由于提示不明确造成的--它没有提到任何关于允许或不允许带小数的年龄。因此,它是`100%`正确的,但执行失败的是`40%`。 验证#2结果的SQL语句: ```sql SELECT birthdate, DATEDIFF(yy,birthdate,current_date), avg(DATEDIFF(yy,birthdate,current_date)) FROM HSFHIR_X0001_S.Patient ``` 在第3个问题的测试中,有 "3个错误 "和 "12个有23个不同元素的表格"。表的值不在相同的位置和格式中,但我还是认为这因为错误格式的提示造成的。因此,它是`100%`正确的,但有`20%`的执行失败。 验证#3结果的SQL语句: ```sql SELECT code, count(*) FROM HSFHIR_X0001_S.Condition group by code ``` 对于第4题的测试,有`2个错误`,`12个结果7`和`1个结果4`。正确值是`4'。所以它是`12%`正确的,有执行失败的`13%`。 验证#4结果的SQL语句: ```sql SELECT p.Key patient, count(c._id) qtde_conditions, list(c.code) conditions FROM HSFHIR_X0001_S.Patient p join HSFHIR_X0001_S.Condition c on c.patient = p.key where code like '%444814009%' group by p.Key ``` 对于5号问题的测试,有`11个错误`,`3个结果为4`,`1个结果为0.6`。正确值是`4'。所以它的正确率是`75%`,执行失败率是`73%`。 用来验证#5结果的SQL与#4所用的相同。 对于6号问题的测试,有`12个错误`和`3个结果{女性:4,男性:2,其他: 0}`. 正确的值是`{女性:3,男性:1,其他: 0}`. 所以它是`0%`正确的,有执行失败的`80%`。 验证#6结果的SQL语句: ```sql SELECT p.gender, count(distinct p._id) qty_patients, list(p.key) patients, list(c.code) conditions FROM HSFHIR_X0001_S.Patient p join HSFHIR_X0001_S.Condition c on c.patient = p.key where code like '%444814009%' group by p.gender ``` 总结一下测试结果: | 测试编号 # |准确率 | 执行失败率 | |--------|----------|-------------------| | 1 | 100% | 6% | | 2 | 100% | 40% | | 3 | 100% | 20% | | 4 | 12% | 13% | | 5 | 75 % | 73% | | 6 | 0% | 80% | ### 安全性 你可以在[测试目录](https://github.com/jrpereirajr/iris-fhir-generative-ai/blob/master/misc/tests-accuacy)中检查生成的代码,似乎人工智能模型没有产生安全问题--这也是代码分析器模块发现的结果,没有安全问题。 因此,为了测试代码分析器是否能够检测到安全问题,[执行Python代码的模块](https://github.com/jrpereirajr/iris-fhir-generative-ai/blob/master/src/fhirgenerativeai/PythonService.cls)有一个名为 "代码注入 "的属性,可以注入恶意代码,以便进行测试。 因此,为安全测试注入了以下代码: 1. 安全测试 #1: ```python print(eval("os.chmod('%s', 0o777)" % 'test.txt')) ``` 这段代码试图访问SO文件系统并改变一个文件的权限。这显然是一个不符合项目意图的代码,所以它必须被阻止。 2. 安全测试 #2: ```python import requests url = "https://postman-echo.com/get" params = {"arg": "value"} response = requests.get(url, params=params) data = response.json() print(data) ``` 这段代码试图向一个外部地址发出HTTP请求--本例中是Postman API。如果这段代码被执行,本地信息就会被攻击者轻易地获取。 正如你在[安全测试目录](https://github.com/jrpereirajr/iris-fhir-generative-ai/blob/master/misc/tests-security)中所看到的,代码分析器成功地发现了安全问题。 ## 结论 就准确性而言,人工智能模型在低复杂度的问题上得到了正确的结果,但随着问题复杂度的增加而开始失败。同样的情况也出现在执行失败上。因此,问题越复杂,人工智能模型产生的代码就越多,无法执行,导致错误结果的概率就越大。 这意味着需要对提示做出一些努力。例如,在[问题#6的代码](https://github.com/jrpereirajr/iris-fhir-generative-ai/blob/master/misc/tests-accuacy/6/1688265739062.txt)中,错误在于只询问病人而忽略了条件。这种分析对于指导提示的改变是必要的。 总的来说,人工智能模型在这次测试中的表现表明,在能够回答分析性问题之前,它仍然需要更多的改进。 这是由于人工智能模型的随机性质。我的意思是,在上面提到的ChatARKit项目中,如果人工智能模型渲染的三维物体并不完全在要求的地方,但接近它,可能用户不会介意。不幸的是,同样的情况并不适用于分析性问题,答案需要精确。 但是,我并不是说人工智能模型不能执行这样的任务。我要说的是,这个项目中使用的设计需要改进。 需要注意的是,这个项目没有使用更先进的技术来使用生成器AI,像[Langchain](https://python.langchain.com/docs/get_started/introduction.html)和[AutoGPT](https://autogpt.net/autogpt-installation-and-features/)。这里使用了一种更 "纯粹 "的方法,但使用这种更复杂的工具可能会导致更好的结果。 关于安全性,代码分析器发现了所有测试的安全问题。 然而,这并不意味着由人工智能模型生成的代码是100%安全的。此外,允许执行外部生成的Python代码可能绝对是危险的。你甚至不能百分之百地确定提供Python代码的系统实际上是OpenAI的API服务器...... 避免安全问题的一个更好的方法可能是尝试其他不如Python强大的语言,或者尝试创建你自己的 "语言 "并将其呈现给AI模型,就像在[这个非常简单的例子](https://platform.openai.com/examples/default-text-to-command)。 最后,重要的是要注意,像代码性能这样的方面在这个项目中没有涉及,可能也会成为未来工作的一个好主题。 所以,我希望大家能发现这个项目的有趣和有用。 > **免责声明:这是一个实验性项目。它将向OpenAI API发送数据,并在你的系统上执行由AI生成的代码。所以,不要在生产系统上使用它。还要注意,由于OpenAI的API调用是收费的。使用它的风险由你自己承担。它不是一个可用于生产的项目。** Hi! Just here to a quick update: now we published a video about this project. Enjoy it: 😊
文章
Jeff Liu · 二月 5, 2021

精华文章置顶--使用VSCode 进行IRIS 开发

VSCode 是目前很流行的一款免费开发工具,IRIS也支持使用其进行连接和开发,相比Studio 只能在windows 环境使用,Vscode 可以跨平台使用。 我们传统的工具Studio 是连接代码服务器的形式,不能便捷的使用目前流行的有本地代码的版本控制工具(如git),但VSCode可以存在本地代码,并且能方便的使用各类存在本地代码的版本控制工具。 以下的内容会帮助大家来配置使用VSCode连接IRIS 进行开发。 VSCode 可以在微软的官网免费下载https://code.visualstudio.com/ 如果安装为英文要切换为中文则可以通过Command Palette 中运行Configure Display Language,install another language, 选择中文,再根据提示安装相应的语言插件。 接下来在扩展商店里搜索intersystems 安装如图所见的3个扩展包 VSCode目前有两种方式可以链接IRIS: 直接连接代码服务器进行编辑 使用客户端方式连接,将代码下载到本地,便于使用有本地代码的版本控制工具 注:两种连接方式的连接端口都为服务器web port 下面具体讲解两种连接方式。 直接连接代码服务器进行编辑 其总体的使用方式和studio相似,直接连接服务器进行代码编辑。在vscode中可以使用全局只读的方式来浏览代码,从而避免误操作修改了代码。 如果会经常使用这个服务器,可以考虑先建立一个工作区,将设置保存在工作区,当然直连服务器的模式下,建立服务器不是必选项。 如果我们正确的安装了之前提到的三个扩展,其中下面扩展会帮助我们连接IRIS代码服务器,并保存在VSCode的全局 settings.json配置文件中: 进入VSCode 设置中找到“扩展”下的Intersystems Server Manager ,进入设置可见安装自带的服务器连接字符串可以依据用户需要进行添加和修改 配置完成可以回到工作区界面进行连接 选择Choose Server and Namespaces, 从右边的服务器列表中选择要连接的服务器,也可以使用“+”来根据提示信息来添加新的服务器。 选择服务器之后会提示输入密码(如果没有在配置中写明登录用户,也会提示输入登录用户)验证成功后可以选择想查看的命名空间,最后按照提示选择是以编辑或者只读方式来查看代码。 在服务器直连的模式下,可以将多个服务器的多个命名空间的代码添加至当前工作区。 目前的连接方式进入之后会频繁的要求登录验证,输入密码,这里实际可以将密码存在VSCode的钥匙链中: 1.打开命令面板 2.输入store 使用下图中的命令则可以为选定的服务器存储密码,但是每个服务器只能保存一个密码,相应的在命令面板中输入clear 会提示清除密码的选项用来删除已存储的密码 2. 使用客户端方式连接 如果使用客户端的连接方式,则必须要在工作区内添加一个文件夹,以及保存此工作区。当添加文件夹到工作区之后侧边栏会多出图示的按钮: 点击按钮选择Choose server and namespaces 此时如果在vscode中已经添加过想要连接的IRIS 代码服务器,这里会直接显示已存储的服务器,设置法与之前提到的直连方式相同。 连接完成后如果想添加多个命名空间到工作区,可以使用右上角的+号进行添加 接下来可以在客户端模式连接导出ObjectScript代码到本地。从而进行源代码的版本控制管理。 在objectscript explorer中选择任意的包,或者类右键选择export,则可以将其导出到目前工作区的文件夹下 代码会被导出到工作区目录的src文件夹下。 客户端模式中使用 GIT 首先打开一个GIT Repo文件夹,如果本来然后按照客户端连接方式连接用来开发的服务器。在任何的包或文件上点击import and compile 可以将版本控制的代码导入开发服务器。 注意这时候只要在本地新建文件并且保存,新的修改就会被同步到开发服务器实例上。 以客户端方式连接时连接多个服务器或多个命名空间的方式: 首先将一个文件夹添加到工作区,并保存工作区,在保存之后的工作区中可以看到如下的工作区配置文件( 此处我使用的默认工作区配置文件名,实际可以自行定义),其中定义了工作区的目录结构: 此时我们在这个工作区内添加一个子目录,在工作区的列表右键单击,选择“将文件夹添加到工作区”: 添加后可以看到,在工作区配置文件中增加了新的目录,为了这个显示层级更加清楚,可以在settings 中使用files.exclude 来过滤是否显示子目录 此时子再点击侧边栏中的 object script explorer,则会让用户选择使用哪一个子目录进行连接: 要使用另一个目录连接一个服务器的命名空间时,将第一个已连接的文件夹下.vscode 拷贝到第二个文件夹: 直接对这个文件进行修改,将其配置成为想连接的服务器以及命名空间 连接配置文件同样支持如下的写法: 此时再回到object script exploer,可见到添加了的服务器。 可见这里服务器的连接和本地代码编译的目标对象只由当前文件夹下.vscode/settings.json控制。 所有的之前提到的配置文件都可以手工编辑,不必一定在vscode中进行添加文件夹和配置连接。 在VSCode中调试 以客户端方式连接时可以进行调试: 在侧边栏选择调试按钮,首次调试应在工作区创建launch.json,并选择ObjectScript Debug 对于Class Method,可以直接在类中点击“Debug this Method” 下边是一个Class Method 实例的实例调试界面 附加到进程调试: 对于需要附加到的进程,在lauch.json中添加”program” 属性,并填入进程号,保存。 program 中也可以填入要debug的routine 比如“##class(Test.test).test()”, “name”可以修改成自定义的名字,之后会在debug运行按钮的下拉菜单显示。 调试的配置可以添加多个,都会显示在debug下拉菜单中 进入调试界面,点击xDebug 旁边的绿色启动按钮开始调试,其他同Class Method 调试方式 关于VSCode几个编辑器小技巧: 在底部栏点击当前连接的服务器会出现提示菜单,从这里可以便捷进入实例管理界面和class reference 在任意的类或者方法上点击右键可以转到定义。如果使用了#dim预定义变量,则可以在变量上使用转到变量声明,而且可以使用转到类型定义类型的定义。 尾声 当前版本不支持xml 代码文件导入!VSCode community中已经有人提到过这个问题,开发者回复之后的版本很有可能会加入 本项目的Github的issue地址,可以在这里提出问题:https://github.com/intersystems-community/vscode-objectscript/issues 支持的产品 :Caché/Ensemble 2016.2 以及更新, 以及所有的基于IRIS 的产品版本. 调试入参类型为%Library.DynamicAbstractObjec的参数如何输入入参呢 对象入参的话,的确直接调试没法从键盘输入,可能还是需要附加到进程或者写一个临时方法来生成这个入参对象,再调用想调试的方法检查断点。 知乎文章:使用VS Code进行Caché数据库开发 好的,谢谢
文章
Hao Ma · 十二月 23, 2024

从TTL值发现网络中的中间人攻击

技术支持团队在不同的项目中发现了类似中间人攻击的情况, 和各位分享一下。 我们的系统一般是安装在内网里,没有恶意的中间人攻击的风险。但是在有些医院发现了这样的情况:IT在网络中安装了某种网络监控或者嗅探的设备, 它会在通信通道中模拟其中一方,或者双方的通信节点, 以截获通信双方的网络流量。通常它不影响双方的通信,但偶尔,它会中断双方的连接, 造成业务的中断。实质上这也是一种中间人攻击的情况,只不过这是用户允许的行为,偶然出现了故障。 我们看看以下的例子: 以下的wireshark抓包截图中, 172.18.1.131和172.18.1.145在正常的通信过程中, 忽然收到了RST消息,造成了TCP连接上的复位。 其中172.18.1.131是intersystems的health connect系统, 它在序号50134的包里面首先发送了RST,因此客户怀疑是不是Health Connect出错,中断了连接,也就把问题提交了InterSystems的技术支持。 我们的技术支持检查了各种内部日志,没有发现任何错误,咨询了InterSystems的网络专家,才发现这是个网络层的错误,也就是说: 这个RST不是Health Connect发送的, 同样, 序号50135的RST消息, 也不一定是172.18.1.145发送的,这是中间网络层的行为。 经过客户的证实,发现客户在网络层安装的监控工具, 不知道什么原因,触动了某种安全机制,它模拟两个系统,分别向对方各发送了一个RST消息。 我们的技术人员之所以发现了疑似中间人攻击的情况, 是基于仔细研究了RST消息中的TTL值。 IP 数据包中的 TTL(Time To Live)是什么: 当 IP 数据包在网络中传输时,TTL 用于防止数据包在网络中无限循环转发。 发送方在创建 IP 数据包时设置一个初始 TTL 值,通常为 64、128 或 255 等。每经过一个路由器,TTL 值就会减 1。当 TTL 减为 0 时,路由器会丢弃该数据包,并向源地址发送一个 ICMP(Internet Control Message Protocol)超时消息。TTL确保网络中的数据包不会无限制地循环,避免网络拥塞。同时,TTL 也可以帮助网络管理员诊断网络问题,例如通过观察 TTL 值的变化来确定数据包经过的路由路径。可以通过 IP 数据包中的 TTL 值来大致判断路由信息,但这种方法并不十分精确,只能提供一些有限的线索。 为了理解上面一段话的意思,我们向不同的目的地发ping包观察一下TTL值: hma@CNMBP23HMA ~ % ping 127.0.0.1 PING 127.0.0.1 (127.0.0.1): 56 data bytes 64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.132 ms 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.162 ms 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.151 ms ^C --- 127.0.0.1 ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 0.132/0.148/0.162/0.012 ms hma@CNMBP23HMA ~ % ping bjsvmse01 PING bjsvmse01.iscinternal.com (172.19.85.68): 56 data bytes 64 bytes from 172.19.85.68: icmp_seq=0 ttl=64 time=10.388 ms 64 bytes from 172.19.85.68: icmp_seq=1 ttl=64 time=6.013 ms 64 bytes from 172.19.85.68: icmp_seq=2 ttl=64 time=4.614 ms ^C --- bjsvmse01.iscinternal.com ping statistics --- 7 packets transmitted, 7 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 3.941/5.778/10.388/2.050 ms hma@CNMBP23HMA ~ % ping www.baidu.com PING www.a.shifen.com (110.242.68.4): 56 data bytes 64 bytes from 110.242.68.4: icmp_seq=0 ttl=52 time=14.167 ms 64 bytes from 110.242.68.4: icmp_seq=1 ttl=52 time=12.411 ms 64 bytes from 110.242.68.4: icmp_seq=2 ttl=52 time=12.560 ms ^C --- www.a.shifen.com ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 12.411/13.046/14.167/0.795 ms hma@CNMBP23HMA ~ % ping www.intersystems.com PING e9933.b.akamaiedge.net (104.124.163.63): 56 data bytes 64 bytes from 104.124.163.63: icmp_seq=0 ttl=42 time=173.183 ms 64 bytes from 104.124.163.63: icmp_seq=1 ttl=42 time=170.947 ms 64 bytes from 104.124.163.63: icmp_seq=2 ttl=42 time=169.031 ms 64 bytes from 104.124.163.63: icmp_seq=3 ttl=42 time=171.195 ms 64 bytes from 104.124.163.63: icmp_seq=4 ttl=42 time=168.908 ms 64 bytes from 104.124.163.63: icmp_seq=5 ttl=42 time=169.736 ms 上面我分别发送ICMP到4个地址:127.0.0.1,bjsvmse01, www.baidu.com, www.intersystems.com, 从对端响应消息中显示的TTL值分别是64, 64, 52, 42。 TTL=64是Linux系统的TTL初始值。 在linux系统ping 127.0.0.1时返回的当然就是64。 第二个服务器bjsvmse01返回TTL也是64, 说明它和我的本机在同一个网络,他们中间没有路由器的转接。 第3个第4个ping返回的消息中的TTL分别是52和42, 理解为:从Baidu.com的服务器到我的本机,经过了64-52=12个路由,而从intersystems.com到我的本机经过了64-42=22个路由。因为intersystems.com的服务器在国外,经过更多路由。从ping的结果看, 响应时间也更长,平均响应要100多ms。 有了TTL的知识,我们来看看前面医院里的RST是怎么回事。 首先是RST的包, 可以看到TTL是128。 这就不对了。 128是Windows系统的TTL初始值, 而我们技术人员已经知道了, Health Connect是安装在一个Linux系统上,如果是Health Connect发出的RST,TTL应该是低于64的. 我们去和一个正常工作的从这个服务器发出的TCP消息来比较,比如上面的HTTP消息,下面的截中可以看到TTL是64, 这是对的。本来这两个服务器就在一个网络里。 因此, 结论就是: 抓包中显示为从Health Connect 172.1.18.131发出的RST消息,因为TTL不对, 应该不是真的从Health Connect服务器发出的。 类似的情况出现在不同的客户的不同医院环境, 所以我们认为维护或者支持任何可能应该了解一下这个情况。 如果遇到,请及时和医院的IT工程师了解情况。 有关TCP消息中的TTL值, 再补充几点: 如果源和目的服务器不在一个网络,TTL值不是64和128,也可以用来判断是否是真实的服务器发送的消息。我们有个支持的工单里, 发现了正常工作的TTL是62,而发送了一个错误的TCP消息,TTL是57, 这也是可疑且需要网络工程师确认的。 上面的例子中,从Health Connect对端,也就是172.18.1.145也发出了一个无法理解的RST消息。一个TCP端,收到一个RST消息后,不需要也不应该再回复,所以这个RST和上面Health Connect发出的RST没有关系。检查这个RST, 它的TTL值是128, 而正常通信时从这个服务器发出的消息也是128, 显然这是个Windows系统,所以这样的情况下, 只通过TTL是无法判断消息来源的身份真假的。除非在不同的地方,比如在中间的交换机上和该服务器上分别抓包比较,才可以得到线索。 网络路径中的某些设备(如防火墙、代理服务器等)对数据包进行了特殊处理。这些设备可能会修改数据包的 TTL 值,以确保数据包能够在网络中存活足够长的时间到达目的地. 典型的是当ping 8.8.8.8,也就 是 Google 的公共 DNS 服务器的时候,收到的TTL可能是不同的值,下面的截图中TTL是111, 如果您也测试一下, 您可能发现这个值是150, 170等等, 这时不只是源服务器的操作系统,LINUX/UNIX/Windows的初始值起作用,而是在中间网络设备将这个值做过特殊处理。 hma@CNMBP23HMA ~ % ping 8.8.8.8 PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: icmp_seq=0 ttl=111 time=50.109 ms 64 bytes from 8.8.8.8: icmp_seq=1 ttl=111 time=45.896 ms ^C --- 8.8.8.8 ping statistics --- 2 packets transmitted, 2 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 45.896/48.002/50.109/2.107 ms hma@CNMBP23HMA ~ % 因此,关于TTL值的局限性, 有这样的说法: 准确性有限: 由于网络中的路由可能会动态变化,TTL 值推断出的路由信息可能并不完全准确。 一些网络设备可能会对 TTL 值进行修改或干扰,导致推断结果不准确。 不完整性: 对于一些复杂的网络环境,如使用 VPN(Virtual Private Network)、代理服务器或网络地址转换(NAT)的情况,TTL 值可能无法准确反映真实的路由路径。
文章
Michael Lei · 七月 4, 2021

数据平台和性能 - 第 7 部分 用于确保性能、可伸缩性和可用性的企业缓存协议ECP

(ECP) Caché 出色的可用性和扩展特性之一是企业缓存协议 (ECP)。 在应用程序开发过程中,如对使用 ECP 的分布式处理加以考虑,可以横向扩展 Caché 应用程序的架构。 应用程序处理可以调整为非常高的速率,处理能力从单个应用程序服务器扩展到最多 255 个应用程序服务器,并且不需要任何应用程序更改。 在我参与的 TrakCare 部署中,ECP 已广泛使用多年。 十年前,主要供应商之一的一台“大型”x86 服务器可能总共只有八个核心。 对于大型部署来说,ECP 是横向扩展商业服务器处理能力的方式,不适合单台昂贵的大型企业服务器。 即使是高核心数的企业服务器也有限制,因此 ECP 也用于扩展这些服务器上的部署。 如今,大多数的新 TrakCare 部署或升级到当前硬件_不需要 ECP_ 即可扩展。 目前的双插槽 x86 生产服务器可以拥有数十个核心和巨大容量的内存。 我们看到,在最近的 Caché 版本中,TrakCare 以及许多其他 Caché 应用程序具有可预测的线性扩展能力,能够随着单台服务器中 CPU 核心数量和内存的增加而支持逐渐增多的用户和事务。 在现场,我看到大多数的新部署都是虚拟化的,即使如此,虚拟机也可以根据需要扩展到主机服务器的规模。 如果资源需求超过单个物理主机可以提供的资源,则使用 ECP 进行横向扩展。 - ___提示:___ _为了简化管理和部署规模,在部署 ECP 之前,先在单台服务器内扩展。_ 在本帖中,我将展示一个示例架构以及 ECP 工作原理的基础知识,然后评论性能注意事项,重点是存储。 有关配置 ECP 和应用程序开发的具体信息,请参见在线的 [Caché 分布式数据管理指南](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GDDM),并且[社区上有一个 ECP 学习轨迹](https://community.intersystems.com/learning-track/enterprise-cache-protocol-ecp-videos)。 ECP 的其他关键特性之一是提高了应用程序可用性,有关详细信息,请参见 [Caché 高可用性指南](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GHA_ecp)中的 ECP 部分。 [本系列其他帖子的列表](https://cn.community.intersystems.com/post/intersystems-数据平台的容量规划和性能系列文章) # ECP 架构基础知识 ECP 的架构和运行在概念上很简单,ECP 提供了在多个服务器系统之间有效共享数据、锁定和可执行代码的方法。 从应用程序服务器角度看,数据和代码远程存储在_数据服务器_上,但缓存在_应用程序服务器_的本地内存中,以提供对活动数据的有效访问,同时尽可能减少网络流量。 数据服务器管理对磁盘上持久性存储的数据库读写,而多个应用程序服务器是解决方案的主力,执行大多数应用程序处理。 ## 多层架构 ECP 采用多层架构。 描述处理层和它们扮演的角色有多种不同的方式,以下是我在描述基于 Web 浏览器的 Caché 应用程序时发现很有用的方式,也是我的帖子的模型和术语。 我知道可能有不同的方法来细分层级,但现在先使用我的方法 :) 基于浏览器的应用程序(例如 Caché Server Pages (CSP))使用多层架构,其中表示、应用程序处理和数据管理功能在逻辑上是分开的。 具有不同角色的__逻辑__“服务器”填充各层。 逻辑服务器不必保留在单独的物理主机或虚拟服务器上,出于成本效益和可管理性的考虑,部分甚至全部逻辑服务器可能位于单个主机或操作系统实例上。 随着部署规模的扩展,服务器可以通过 ECP 划分到多个物理或虚拟主机上,从而可根据需要分散处理工作负载,而无需更改应用程序。 主机系统可以是物理的或虚拟化的,具体取决于容量和可用性要求。 以下层和逻辑服务器构成了一个部署: - _表示层:_包括在基于浏览器的客户端和应用程序层之间充当网关的 Web 服务器。 - _应用程序层:_这是 ECP 应用程序服务器所在的位置。 如上文所述,这是一个逻辑模型,其中应用程序服务器不必与数据服务器分开,而且除了最大型的站点外,所有情况下通常都不需要分开。 该层还可能包括进行专门处理的其他服务器,如报告服务器。 - _数据层:_这是数据服务器所在的位置。 数据服务器执行事务处理,是存储在 Caché 数据库中的应用程序代码和数据存储库。 数据服务器负责读写持久性磁盘存储。 ## 逻辑架构 下图是一个基于浏览器的应用程序在部署为三层架构时的逻辑视图: 尽管初看之下该架构可能很复杂,但构成它的组件仍然与安装在单台服务器上的 Caché 系统的组件相同,只是逻辑组件安装在多个物理或虚拟服务器上。 服务器之间的所有通信都通过 TCP/IP 进行。 ### 逻辑视图中的 ECP 操作 上图从顶部开始,显示用户安全地连接到多个已进行负载平衡的 Web 服务器。 这些 Web 服务器在客户端和应用程序层(应用程序服务器)之间传递 CSP 网页请求,应用程序层进行所有处理,允许动态创建内容,并通过 Web 服务器将完成的页面返回给客户端。 在这个三层模型中,应用程序处理通过 ECP 分散到多个应用程序服务器上。 应用程序只将数据(您的应用程序数据库)视为应用程序服务器的本地数据。 当应用程序服务器发出数据请求时,它将尝试从本地缓存满足请求,如果不能满足,ECP 将向数据服务器请求必要的数据,数据服务器自己的缓存可能会满足请求,否则将从磁盘获取数据。 数据服务器对应用程序服务器的回复包括存储该数据的数据库块。 这些块将被使用,并且此时将缓存到应用程序服务器上。 ECP 自动负责管理整个网络中的缓存一致性,并将变化传播回数据服务器。 客户端会体验到快速响应,因为它们经常使用本地缓存的数据。 默认情况下,Web 服务器与首选的应用程序服务器通信,确保同一应用程序服务器满足相关数据的后续请求,因为这些数据可能已经在本地缓存中。 - ___提示:___ _如 [Caché 文档](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GDDM_develop#GDDM_develop_considerations)中详述,在循环或负载平衡方案中,应避免用户连接到应用程序服务器,因为这会影响应用程序服务器上缓存的优势。 理想情况下,相同的用户或用户组保持连接到同一应用程序服务器。_ 该解决方案通过在表示层添加 Web 服务器和在应用程序层添加其他应用程序服务器来进行扩展,无需用户停机。 数据层通过增加数据服务器的 CPU 和内存来进行扩展。 ## 物理架构 下图显示了与三层逻辑架构示例相同的三层部署中使用的物理主机示例: 请注意,在每一层部署物理或虚拟主机时均采用 n+1 或 n+2 模式,以确保在主机故障或计划维护时保持 100% 能力。 由于用户分布在多个 Web 和应用程序服务器上,单个服务器故障只会影响少量用户,他们会自动重新连接到其余服务器之一。 数据管理层具有高度可用性,例如,位于连接到一个或多个存储阵列的故障转移集群上(例如,虚拟化 HA、InterSystems 数据库镜像或传统的故障转移集群)。 如果硬件或服务出现故障,集群将在其中一个幸存节点上重启服务。 ECP 的一个附加好处是内置弹性,并且在数据库节点集群发生故障转移时能保持事务完整性,应用程序用户将观察到处理暂停,直到故障转移和自动恢复完成,随后用户将无缝继续,不会断开连接。 同样的架构也可以映射到虚拟化服务器,例如,VMware vSphere 可用于虚拟化应用程序服务器。 # ECP 容量规划 如上文所述,数据服务器管理对持久性磁盘的数据库读写,而多个应用程序服务器是解决方案的主力,执行大多数应用程序处理。 这是考虑系统资源容量规划时的一个关键概念,总结来说: - ___数据服务器___(有时称为数据库服务器)通常执行很少的应用程序处理,因此_对 CPU 要求低_,但该服务器执行大部分存储 IO,因此可能有_非常高的存储 IOPS_,即数据库读写以及日志写入(稍后将详细介绍日志 IO)。 - ___应用程序服务器___执行大多数应用程序处理,因此_对 CPU 要求高_,但存储 IO 非常少。 通常,调整 ECP 服务器 CPU、内存和 IO 要求的规则与调整非常大的单服务器解决方案的规则相同,同时考虑 N+1 或 N+2 台服务器以确保高可用性。 ## 基本 CPU 和存储规模调整: 假设 My_Application 需要最多 72 个 CPU 核心进行应用程序处理(记得还要考虑余量),并且预计在写入守护进程周期期间需要 20,000 次写入,以及 10,000 次随机数据库读取的持续峰值。 一个简单的虚拟或物理服务器规模调整方案为: - 4 台 32 CPU 应用程序服务器(3 台服务器 + 1 台服务器用于确保 HA)。 低 IOPS 要求。 - 2 台 10 CPU 数据服务器(镜像或集群以确保 HA)。 [低延迟 IOPS 要求](https://cn.community.intersystems.com/post/数据平台和性能-第-6-部分-caché-存储-io-配置文件)为 20K 写入、10K 读取,加上 WIJ 和日志。 虽然数据服务器只执行非常少的处理,但考虑到系统和 Caché 进程,将其规模调整为 8-10 个 CPU。 应用程序服务器的规模可以根据每台物理主机的最佳性价比和/或可用性来进行调整。 横向扩展时会有一些效率损失,但通常可以在服务器块中增加处理能力,并预计吞吐量有近乎线性的增长。 限制更有可能首先在存储 IO 中出现。 - ___提示:____与确保 HA 一样,要考虑主机、机箱或机架故障的影响。 在 VMWare 上虚拟化应用程序和数据服务器时,确保应用 vSphere DRS 和相关性规则以分散处理负载并确保可用性。_ ## 日志同步 IO 要求 ECP 部署的另一个容量规划注意事项是,由于日志同步,它们需要较高 IO,并且对存储响应时间的要求非常严格,以保持数据服务器上日志记录的可伸缩性 。 同步请求可以触发对日志中最后一个块的写入,以确保数据耐久性。 不过您的情况可能有所不同;在一个典型的以高事务处理速率运行的客户站点上,我经常看到非 ECP 配置上的日志写入 IOPS 为每秒十几次。 在繁忙的系统上使用 ECP 时,由于 ECP 强制日志同步,可以在日志磁盘上看到 100 到 1000 的写入 IOPS。 - ___提示:____如果在繁忙的系统上显示 mgstat 或查看 [pButtons](https://cn.community.intersystems.com/post/intersystems-数据平台和性能-–-第-1-篇) 中的 mgstat,您将看到 Jrnwrts(日志写入次数),您将在存储 IO 资源规划中对其加以考虑。 在 ECP 数据服务器上,还有未显示在 mgstat 中的对日志磁盘的日志同步写入,要了解这些信息,您需要查看日志磁盘的操作系统指标,例如使用 iostat 查看_。 ### 什么是日志同步? 需要日志同步的原因: - 确保在数据服务器发生故障时数据的耐久性和可恢复性。 - 它们也是确保应用程序服务器之间的缓存一致性的触发器。 在非 ECP 配置中,对 Caché 数据库的修改将写入日志缓冲区(128 x 64K 缓冲区),当日志缓冲区满时或每两秒由日志守护程序写入磁盘上的日志文件。 Caché 为整个缓冲区分配 64k,并且这些缓冲区总是被重复使用,而不是被销毁和重新创建,Caché 只是跟踪末尾偏移量。 在大多数情况下(除非一次进行大量更新),日志写入次数非常小。 ECP 系统中也有日志同步。 日志同步可以定义为将当前日志缓冲区的相关部分重新写入磁盘,以确保磁盘上的日志始终是最新的。 因此,日志同步会请求多次重新写入同一日志块的某个部分(大小在 2k 到 64k 之间)。 ECP 客户端上可以触发日志同步请求的事件为更新(SET 或 KILL)或 LOCK。 例如,对于每个 SET 或 KILL,都会将当前日志缓冲区写入(或重新写入)磁盘。 在非常繁忙的系统中,单次同步操作中的日志同步可能被捆绑或延迟为多个同步请求。 ### 日志同步的容量规划 为确保持续的吞吐量,日志同步的平均写入响应时间必须: - _
文章
Jeff Liu · 一月 7, 2021

精华文章---在 Windows 主机上运行的 Hyper-V Ubuntu 虚拟机中配置 Docker 使用环境

这次我想谈一谈不专门针对 InterSystems IRIS 的东西,不过如果你想使用 Docker,并且你工作环境是安装了 Windows 10 专业版或企业版的 PC 或笔记本电脑,那么我认为这个很重要。 你可能知道,容器技术基本上来自于 Linux 世界,如今在 Linux 主机上发挥出最大潜能。 那些平常使用 Windows 的人会看到,Microsoft 和 Docker 在过去的几年做出了重要的努力,让我们可以在 Windows 系统上以非常简单的方式运行基于 Linux 映像的容器... 但是生产系统不支持这种方式,这是个大问题,如果我们要将持久性数据保留在主机系统中的容器之外,这样做非常不可靠... 这主要是由于 Windows 和 Linux 文件系统之间的巨大差异导致的。 最终,_Docker for Windows 自身使用了一个小型 linux 虚拟机 (_MobiLinux_) 来运行容器... 此操作对于 Windows 用户是透明的,而且效果完美,只要你不需要你的数据库比容器存活的时间更长... 好了,我们进入正题,很多时候为了避免出现问题和简化操作,我们需要一个完整的 Linux 系统,而且如果我们的服务器基于 Windows,那么唯一的方法就是通过虚拟机来实现。 至少在 Windows 中的 WSL2 发布之前是这样,但发布后就是另一回事了,不过它要变得足够强大稳定肯定还需要一些时间。 在本文中,我将一步一步告诉你如何在 Windows 服务器中的 Ubuntu 系统上安装一个能使用 Docker 容器进行工作的环境。 我们开始吧... 1. 启用 Hyper-V 如果尚未启用,则转到添加 `Windows 功能`并启用 Hyper-V。 你将需要重启(图片上的文本是西班牙语,但这就是我当前的区域设置。 如果你不懂堂吉诃德的语言,我希望加上说明能帮助你“解密”😉) ![](/sites/default/files/inline/images/images/image(424).png)   2. 在 Hyper-V 上创建一个 Ubuntu 虚拟机 我认为创建虚拟机 (VM) 没有更简单的方法了。 只需打开 `Hyper-V 管理器`的窗口,然后转到选项快速创建...(屏幕的右上角),使用已经提供的任一 Ubuntu 版本来创建你的虚拟机(你可以下载任何其他 Linux 的 iso 文件,创建不同发行版的虚拟机)。 在我的示例中,我选择了最新的 Ubuntu 版本:19.10。 不过,你在这里看到的一切内容也都适用于 18.04。 在 15 或 20 分钟内,具体取决于你下载映像花费的时间,新的虚拟机就创建完毕并准备就绪。 重要: 保持选项默认交换机 不变。这将保证你可以从主机和虚拟机访问互联网。 ![](/sites/default/files/inline/images/images/vm_ubuntu_network_start_defaultswitch_eth0.jpg) 3. 创建本地子网 使用虚拟机经常遇到的问题之一与网络配置有关... 有时有效,有时无效,或者连接 Wi-Fi 时有效,但连接网线就无效,或者是相反情况;或者如果我在 Windows 主机中建立一个 VPN,那么在虚拟机中就无法访问互联网,或者是虚拟机 (Linux) 和主机 (Windows) 之间的通信中断... 总之,非常让人抓狂! 这使得我在使用笔记本电脑进行开发、小型快速演示或展示时无法信任我的环境,而在这些场景下访问互联网很可能不如确保在主机与虚拟机之间进行可靠通信来得重要。 在 Windows 主机和虚拟机之间共享一个临时本地子网,可以解决这个问题。 要让它们互相通信,使用该子网就可以了。 你只需要为主机和虚拟机分配特定 IP 即可。 通过以下步骤可以很容易实现。 只需转到虚拟交换机管理器...,你可以在 `Hyper-V 管理器`中找到: ![](/sites/default/files/inline/images/images/image(425).png) 然后,转到选项新建虚拟交换机(之后就像虚拟机的新网卡一样): ![](/sites/default/files/inline/images/images/image(461).png) 确保将其定义为_内部网络_,选择我们想要的名称,其他选项保持默认 ![](/sites/default/files/inline/images/images/image(427).png) 现在,如果转到 _`Windows 控制面板 --> 网络和共享中心`_,我们会看到那里已经有了我们刚才创建的交换机: ![](/sites/default/files/inline/images/images/image(429).png)   4. 配置主机和虚拟机共享的本地子网 此时,你可以完成新的本地网络的配置。 为此,将光标放在连接 _Mi Nuevo Conmutador LOCAL_ 上,单击并转到属性,再转到 IPv4 协议,以便分配一个固定 IP 地址: ![](/sites/default/files/inline/images/images/image(449).png)   重要:在此处分配的 IP 将是主机 (Windows) 在该本地子网中的 IP。   5. 将新的本地网络链接并配置到虚拟机 现在回到 `Hyper-V 管理器`。 如果虚拟机正在运行,将其停止。 停止后,转到其配置并添加新的内部虚拟交换机: ![](/sites/default/files/inline/images/images/image(431).png) _(注意:在图片上可以看到另一个交换机 Hyper-V Conmutador INTERNO。 它用于我的另一个子网。 此配置中不需要它)_ 单击“添加”后,你只需选择先前创建的交换机: ![](/sites/default/files/inline/images/images/image(432).png) 好了,完成此操作后,依次单击“应用”、“接受”... 一切就绪!你只需启动并再次登录虚拟机即可完成内部连接的配置。 为此,在虚拟机启动后,单击网络图标(右上角),你将看到两个网络:_eth0_ 和 _eth1_。 _eth1_ 目前显示为断开连接: ![](/sites/default/files/inline/images/images/image(450).png) 进入以太网 (eht1) 的配置,并为此本地子网分配一个固定 IP,例如:_155.100.101.1_,子网掩码:_255.255.255.0_ ![](/sites/default/files/inline/images/images/image(452).png) 这样就完成了。 你的虚拟机标识为 IP 155.100.101.1,与主机共享同一子网。 7. 允许从虚拟机访问 Windows 10 你可能会发现 Windows 10 默认不允许其他服务器连接,对于 Windows 系统来说,你刚刚创建的虚拟机正是一个可能存在危险的外部服务器。因此,必须在防火墙中添加规则,才能从这些虚拟机连接到主机。 如何操作? 非常简单,只需在 `Windows 控制面板`中查找 `Windows Defender 防火墙`,转到高级配置,然后创建一条新的*入站规则*: ![](/sites/default/files/inline/images/images/image(451).png) 你可以设置一个端口或者一个或多个端口范围...(也可以设置针对所有端口的规则)... ![](/sites/default/files/inline/images/images/image(453).png) 我们需要的操作是_允许连接_... ![](/sites/default/files/inline/images/images/image(454).png) 用于_所有网络类型_... ![](/sites/default/files/inline/images/images/image(455).png) 为规则指定名称... ![](/sites/default/files/inline/images/images/image(456).png) **这里很重要**,指定名称后要立即再次打开新创建的规则的属性并*限制应用程序范围*,以便只应用于本地子网内的连接... ![](/sites/default/files/inline/images/images/image(457).png) 8. 就绪。 在新的 Ubuntu 虚拟机中安装 Docker 和任何其他应用程序 完成整个安装过程后,新虚拟机即就绪且为最新,并可以访问互联网等等。 你可以安装所需的应用程序... 至少要安装 Docker,这是一开始就有的想法,如果你需要连接公司网络,还可以安装 VPN 客户端,还有 VS Code、Eclipse+Atelier 等等。 具体来说,要在虚拟机中安装 Docker,可以按照以下说明进行操作: 确保 Docker 运行时正在工作,下载一些测试映像等等... 仅此而已。 这样... _**你已完成所有工作!**_,现在你将能够在 Ubuntu 虚拟机中无限制(除了硬件能力限制)运行容器,你可以从 Windows 10 主机、浏览器或应用程序连接到虚拟机,以及反过来从 Ubuntu 虚拟机连接到 Windows 10 主机。 所有使用你在共享本地子网中设置的 IP 地址的操作都将有效,无论是否建立 VPN,是通过 Wi-fi 适配器还是通过以太网电缆接入互联网。 啊... 最后一个建议。 如果要在 Windows 10 和虚拟机之间交换文件,一个非常有用且简单的选项是使用 [WinSCP](https://winscp.net/eng/download.php)。 它是免费的,而且非常好用。 当然,还有其他配置,但这是我使用的配置,已经证明是比较可靠的。 希望你也觉得它有用。 如果我帮助你避免了令人头疼的问题,这篇文章就值了。 编码愉快!     
文章
姚 鑫 · 三月 25, 2021

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

# 第十三章 使用动态SQL(三) # 执行SQL语句 有两种使用`%SQL.Statement`类执行SQL语句的方法: - `%Execute()`,它执行以前使用`%Prepare()`或`%PrepareClassQuery()`准备的SQL语句。 - `%ExecDirect()`,它同时准备和执行一条SQL语句。 也可以通过使用`$SYSTEM.SQL.Execute()`方法执行SQL语句而无需创建对象实例。此方法既准备又执行SQL语句。它创建一个缓存的查询。下面的终端示例显示`Execute()`方法: ```java USER>SET topnum=5 USER>SET rset=$SYSTEM.SQL.Execute("SELECT TOP :topnum Name,Age FROM Sample.Person") USER>DO rset.%Display() ``` ## %Execute() 准备查询后,可以通过调用`%SQL.Statement`类的`%Execute()`实例方法来执行查询。对于非`SELECT`语句,`%Execute()`调用所需的操作(例如执行`INSERT`)。对于`SELECT`查询,`%Execute()`生成一个结果集,用于后续遍历和数据检索。例如: ```java SET rset = tStatement.%Execute() ``` `%Execute()`方法为所有SQL语句设置`%SQL.StatementResult`类属性`%SQLCODE`和`%Message`。 `%Execute()`设置其他`%SQL.StatementResult`属性,如下所示: - `INSERT`,`UPDATE`,`INSERT`或`UPDATE`,`DELETE`和`TRUNCATE TABLE`语句将`%ROWCOUNT`设置为受操作影响的行数。 `TRUNCATE TABLE`无法确定删除的实际行数,因此将`%ROWCOUNT`设置为-1。 `INSERT`,`UPDATE`,`INSERT OR UPDATE`和`DELETE`将`%ROWID`设置为最后一条插入,更新或删除的记录的`RowID`值。如果该操作未插入,更新或删除任何记录,则`%ROWID`是未定义的,或保持设置为其先前值。 `TRUNCATE TABLE`没有设置`%ROWID`。 - `SELECT`语句在创建结果集时会将`%ROWCOUNT`属性设置为0。当程序遍历结果集的内容(例如,使用`%Next()`方法)时,`%ROWCOUNT`会增加。 `%Next()`返回1表示它位于一行上,返回0表示它位于最后一行之后(在结果集的末尾)。如果光标位于最后一行之后,则`%ROWCOUNT`的值指示结果集中包含的行数。 如果`SELECT`查询仅返回聚合函数,则每个`%Next()`都将设置`%ROWCOUNT = 1`。即使表中没有数据,第一个`%Next()`始终设置`%SQLCODE = 0`。任何后续的`%Next()`都会设置`%SQLCODE = 100`并设置`%ROWCOUNT = 1`。 `SELECT`还设置`%CurrentResult`和`%ResultColumnCount`。 `SELECT`未设置`%ROWID`。 可以使用`ZWRITE`返回所有`%SQL.StatementResult`类属性的值。 ## 具有输入参数的%Execute() `%Execute()`方法可以采用一个或多个与准备的SQL语句中的输入参数(以`“?”`表示)相对应的参数。 `%Execute()`参数对应于`“?”`的顺序字符出现在SQL语句中:第一个参数用于第一个`“?”`,第二个参数用于第二个`“?”`,依此类推。多个`%Execute()`参数以逗号分隔。可以通过指定占位符逗号来省略参数值。 `%Execute()`参数的数量必须与`“?”`相对应输入参数。如果`%Execute()`参数少于或大于相应的`“?”`输入参数,执行失败,并且`%SQLCODE`属性设置为`SQLCODE -400`错误。 可以使用输入参数为`SELECT`列表和其他查询子句(包括`TOP`子句和`WHERE`子句)提供文字值或表达式。不能使用输入参数为`SELECT`列表或其他查询子句提供列名或列名别名。 **当指定为显式`%Execute()`参数时,最大输入参数数为255。使用可变长度数组`%Execute(vals ...)`指定时,最大输入参数数为380。** 在执行`Prepare`之后,可以使用`Prepare`参数元数据来返回`?`的计数和所需的数据类型。输入参数。可以使用`%GetImplementationDetails()`方法返回`?`的列表。在准备好的查询中输入参数,并在查询文本中使用`?`输入参数显示在上下文中。 以下ObjectScript示例使用两个输入参数执行查询。它在`%Execute()`方法中指定输入参数值(21和26)。 ```java /// d ##class(PHA.TEST.SQL).PrepareClassQuery7() ClassMethod PrepareClassQuery7() { SET tStatement = ##class(%SQL.Statement).%New(1) SET tStatement.%SchemaPath = "MyTests,Sample,Cinema" SET myquery=2 SET myquery(1)="SELECT Name,DOB,Age FROM Person" SET myquery(2)="WHERE Age > ? AND Age < ? ORDER BY Age" SET qStatus = tStatement.%Prepare(.myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute(21,26) WRITE !,"Execute OK: SQLCODE=",rset.%SQLCODE,!! DO rset.%Display() WRITE !,"End of data: SQLCODE=",rset.%SQLCODE } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).PrepareClassQuery7() Execute OK: SQLCODE=0 Name DOB Age Van De Griek,Dick U. 1998-12-21 22 Peterson,Kirsten R. 1997-12-13 23 Van De Griek,Phil S. 1996-09-26 24 Wijnschenk,Lydia G. 1997-01-17 24 Xiang,Kirsten U. 1996-08-06 24 Schaefer,Usha G. 1995-09-16 25 Peterson,Sophia A. 1995-12-05 25 Petersburg,Bill O. 1995-10-23 25 8 Rows(s) Affected End of data: SQLCODE=100 ``` 下面的ObjectScript示例执行相同的查询。 `%Execute()`方法形式参数列表使用可变长度数组(`dynd ...`)指定不确定数量的输入参数值;在这种情况下,为`dynd`数组的下标。 `dynd`变量设置为2以指示两个下标值。 ```java /// d ##class(PHA.TEST.SQL).PrepareClassQuery8() ClassMethod PrepareClassQuery8() { SET tStatement = ##class(%SQL.Statement).%New(1) SET tStatement.%SchemaPath = "MyTests,Sample,Cinema" SET myquery=2 SET myquery(1)="SELECT Name,DOB,Age FROM Person" SET myquery(2)="WHERE Age > ? AND Age < ? ORDER BY Age" SET dynd=2,dynd(1)=21,dynd(2)=26 SET qStatus = tStatement.%Prepare(.myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute(dynd...) WRITE !,"Execute OK: SQLCODE=",rset.%SQLCODE,!! DO rset.%Display() WRITE !,"End of data: SQLCODE=",rset.%SQLCODE } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).PrepareClassQuery8() Execute OK: SQLCODE=0 Name DOB Age Van De Griek,Dick U. 1998-12-21 22 Peterson,Kirsten R. 1997-12-13 23 Van De Griek,Phil S. 1996-09-26 24 Wijnschenk,Lydia G. 1997-01-17 24 Xiang,Kirsten U. 1996-08-06 24 Schaefer,Usha G. 1995-09-16 25 Peterson,Sophia A. 1995-12-05 25 Petersburg,Bill O. 1995-10-23 25 8 Rows(s) Affected End of data: SQLCODE=100 ``` 可以对准备好的结果集执行多个`%Execute()`操作。这使可以多次运行查询,并提供不同的输入参数值。不必在`%Execute()`操作之间关闭结果集,如以下示例所示: ```java /// d ##class(PHA.TEST.SQL).PrepareClassQuery9() ClassMethod PrepareClassQuery9() { SET myquery="SELECT Name,SSN,Age FROM Sample.Person WHERE Name %STARTSWITH ?" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute("A") DO rset.%Display() WRITE !,"End of A data",!! SET rset = tStatement.%Execute("B") DO rset.%Display() WRITE !,"End of B data" } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).PrepareClassQuery9() Name SSN Age Alton,Martin S. 624-25-8488 47 Ahmed,Elmo X. 950-40-6135 77 Anderson,Mario L. 604-10-9256 77 Adams,Diane F. 640-77-5933 9 Anderson,Valery N. 882-50-4971 27 Alton,Phil T. 785-37-8519 68 Adams,Susan E. 947-66-8684 52 7 Rows(s) Affected End of A data Name SSN Age Bukowski,Mario V. 683-32-4214 85 Bachman,Susan O. 102-59-3932 88 Bush,Jules K. 547-97-7915 13 Basile,Filomena X. 888-66-1725 86 Browne,Robert X. 308-58-1444 82 Burroughs,Barbara H. 627-56-2213 86 Beatty,Molly Z. 794-64-5615 54 7 Rows(s) Affected End of B data ``` ### 使用TRY / CATCH处理%Execute错误 可以在`TRY`块结构内执行Dynamic SQL,将运行时错误传递给关联的`CATCH`块异常处理程序。对于`%Execute()`错误,可以使用`%Exception.SQL`类创建一个异常实例,然后将其扔到`CATCH`异常处理程序中。 下面的示例在发生`%Execute()`错误时创建一个SQL异常实例。在这种情况下,错误是数量之间的基数不匹配。输入参数(1)和`%Execute()`参数的数量(3)。它将`%SQLCODE`和`%Message`属性值(作为`Code`和`Data`)抛出到`CATCH`异常处理程序中。异常处理程序使用`%IsA()`实例方法测试异常类型,然后显示`%Execute()`错误: ```java /// d ##class(PHA.TEST.SQL).SQLTRY() ClassMethod SQLTRY() { TRY { SET myquery = "SELECT TOP ? Name,DOB FROM Sample.Person" SET tStatement = ##class(%SQL.Statement).%New() SET qStatus = tStatement.%Prepare(myquery) IF qStatus'=1 { WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute(7,9,4) IF rset.%SQLCODE=0 { WRITE !,"Executed query",! } ELSE { SET badSQL=##class(%Exception.SQL).%New(,rset.%SQLCODE,,rset.%Message) THROW badSQL } DO rset.%Display() WRITE !,"End of data" RETURN } CATCH exp { WRITE "In the CATCH block",! IF 1=exp.%IsA("%Exception.SQL") { WRITE "SQLCODE: ",exp.Code,! WRITE "Message: ",exp.Data,! } ELSE { WRITE "Not an SQL exception",! } RETURN } } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).SQLTRY() In the CATCH block SQLCODE: -400 Message: Dynamic SQL Execute, more parameter values passed than are specified in the dynamic statement ``` ## %ExecDirect() `%SQL.Statement`类提供`%ExecDirect()`类方法,该方法可以在单个操作中准备和执行查询。它可以准备指定的查询(如`%Prepare()`)或现有的类查询(如`%PrepareClassQuery()`)。 `%ExecDirect()`准备并执行指定的查询: ```java /// d ##class(PHA.TEST.SQL).ExecDirect() ClassMethod ExecDirect() { SET myquery=2 SET myquery(1)="SELECT Name,Age FROM Sample.Person" SET myquery(2)="WHERE Age > 21 AND Age < 30 ORDER BY Age" SET rset = ##class(%SQL.Statement).%ExecDirect(,.myquery) IF rset.%SQLCODE=0 { WRITE !,"ExecDirect OK",!! } ELSE { WRITE !,"ExecDirect SQLCODE=",rset.%SQLCODE,!,rset.%Message QUIT } DO rset.%Display() WRITE !,"End of data: SQLCODE=",rset.%SQLCODE } ``` ```java DHC-APP> d ##class(PHA.TEST.SQL).ExecDirect() ExecDirect OK Name Age Van De Griek,Dick U. 22 Peterson,Kirsten R. 23 Van De Griek,Phil S. 24 Wijnschenk,Lydia G. 24 Xiang,Kirsten U. 24 Schaefer,Usha G. 25 Peterson,Sophia A. 25 Petersburg,Bill O. 25 Ng,Josephine Z. 26 Munt,Valery W. 26 Ingleman,Martin T. 26 Eno,Diane U. 26 Pascal,Kim P. 27 Ipsen,Jane A. 27 Anderson,Valery N. 27 Gomez,Mo Q. 27 Xerxes,Angelo P. 28 Young,Barbara N. 29 18 Rows(s) Affected End of data: SQLCODE=100 ``` `%ExecDirect()`准备并执行现有的类查询: ```java /// d ##class(PHA.TEST.SQL).ExecDirect1() ClassMethod ExecDirect1() { SET mycallq = "?=CALL Sample.PersonSets('A','NH')" SET rset = ##class(%SQL.Statement).%ExecDirect(,mycallq) IF rset.%SQLCODE=0 { WRITE !,"ExecDirect OK",!! } ELSE { WRITE !,"ExecDirect SQLCODE=",rset.%SQLCODE,!,rset.%Message QUIT } DO rset.%Display() WRITE !,"End of data: SQLCODE=",rset.%SQLCODE } ``` 可以将输入参数值指定为`%ExecDirect()`类方法的第三个参数和后续参数,如以下示例所示: ```java /// d ##class(PHA.TEST.SQL).ExecDirect2() ClassMethod ExecDirect2() { SET myquery=2 SET myquery(1)="SELECT Name,Age FROM Sample.Person" SET myquery(2)="WHERE Age > ? AND Age < ? ORDER BY Age" SET rset = ##class(%SQL.Statement).%ExecDirect(,.myquery,12,20) IF rset.%SQLCODE'=0 { WRITE !,"1st ExecDirect SQLCODE=",rset.%SQLCODE,!,rset.%Message QUIT } DO rset.%Display() WRITE !,"End of teen data",!! SET rset2 = ##class(%SQL.Statement).%ExecDirect(,.myquery,19,30) IF rset2.%SQLCODE'=0 { WRITE !,"2nd ExecDirect SQLCODE=",rset2.%SQLCODE,!,rset2.%Message QUIT } DO rset2.%Display() WRITE !,"End of twenties data" } ``` ```java DHC-APP> d ##class(PHA.TEST.SQL).ExecDirect2() Name Age Bush,Jules K. 13 ... Eastman,Howard K. 18 9 Rows(s) Affected End of teen data Name Age Ingrahm,Susan N. 20 ... Young,Barbara N. 29 20 Rows(s) Affected End of twenties data ``` `%ExecDirect()`输入参数对应于`“?”`的顺序字符出现在SQL语句中:第三个参数用于第一个`“?”`,第四个参数用于第二个`“?”`,依此类推。可以通过指定占位符逗号来省略参数值。如果`%ExecDirect()`输入参数少于相应的`“?”`输入参数,则使用默认值(如果存在)。 在下面的示例中,第一个`%ExecDirect()`指定所有三个`“?”`输入参数,第二个`%ExecDirect()`仅指定第二个`?`输入参数,并省略第一个和第三个。它使用第三个输入参数的默认`Sample.PersonSets()('MA')`: ```java /// d ##class(PHA.TEST.SQL).ExecDirect3() ClassMethod ExecDirect3() { SET mycall = "?=CALL Sample.PersonSets(?,?)" SET rset = ##class(%SQL.Statement).%ExecDirect(,mycall,"","A","NH") IF rset.%SQLCODE'=0 {WRITE !,"1st ExecDirect SQLCODE=",rset.%SQLCODE,!,rset.%Message QUIT} DO rset.%Display() WRITE !,"End of A people data",!! SET rset2 = ##class(%SQL.Statement).%ExecDirect(,mycall,,"B") IF rset2.%SQLCODE'=0 {WRITE !,"2nd ExecDirect SQLCODE=",rset2.%SQLCODE,!,rset2.%Message QUIT} DO rset2.%Display() WRITE !,"End of B people data" } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).ExecDirect3() Output Values: 0. 1 Dumping result #1 Name DOB Spouse ... 1 Rows(s) Affected End of B people data ``` `%ExecDirect()`可以调用`%SQL.Statement%Display()`实例方法或`%GetImplementationDetails(`)实例方法以返回当前准备好的语句的详细信息。因为`%ExecDirect()`可以准备并执行指定的查询或现有的类查询,所以可以使用`%GetImplementationDetails()` `pStatementType`参数来确定准备哪种查询: ```java /// d ##class(PHA.TEST.SQL).ExecDirect4() ClassMethod ExecDirect4() { SET mycall = "?=CALL Sample.PersonSets('A',?)" SET rset = ##class(%SQL.Statement).%ExecDirect(tStatement,mycall,,"NH") IF rset.%SQLCODE'=0 { WRITE !,"ExecDirect SQLCODE=",rset.%SQLCODE,!,rset.%Message QUIT } SET bool = tStatement.%GetImplementationDetails(.pclassname,.ptext,.pargs,.pStatementType) IF bool=1 { IF pStatementType=1 {WRITE "Type= specified query",! } ELSEIF pStatementType=45 { WRITE "Type= existing class query",! } WRITE "Implementation class= ",pclassname,! WRITE "Statement text= ",ptext,! WRITE "Arguments= ",$LISTTOSTRING(pargs),!! } ELSE {WRITE "%GetImplementationDetails() failed"} DO rset.%Display() WRITE !,"End of data" } ```
文章
姚 鑫 · 十月 15, 2021

第四十六章 SQL命令 FROM(二)

[toc] # 第四十六章 SQL命令 FROM(二) ## %PARALLEL 这个可选关键字在查询的FROM子句中指定。 它建议 IRIS使用多个处理器(如果适用)并行处理查询。 这可以显著提高使用一个或多个`COUNT`、`SUM`、`AVG`、`MAX`或`MIN`聚合函数和/或`GROUP BY`子句的某些查询的性能,以及许多其他类型的查询。 这些通常是处理大量数据并返回小结果集的查询。 例如,`SELECT AVG(SaleAmt) FROM %PARALLEL User.AllSales GROUP BY Region`使用并行处理。 既指定单个字段又指定聚合函数且不包含`GROUP BY`子句的查询不能执行并行处理。 例如,`SELECT Name,AVG(Age) FROM %PARALLEL Sample`。 `Person`不执行并行处理,而是从`SELECT Name,AVG(Age) FROM %PARALLEL Sample.Person GROUP BY Home_State`执行并行处理。 `%PARALLEL`用于`SELECT`查询及其子查询。 `INSERT`命令子查询不能使用`%PARALLEL`。 指定`%PARALLEL`可能会降低某些查询的性能。 在具有多个并发用户的系统上使用`%PARALLEL`运行查询可能会导致整体性能下降。 注意:指定`%PARALLEL`的查询必须在读/写而不是只读的数据库中运行。 否则,可能发生``错误。 不管在`FROM`子句中是否存在`%PARALLEL`关键字,有些查询可能使用线性处理,而不是并行处理:有些查询不支持并行处理; 一些优化后的查询可能无法从并行处理中获益。 可以使用`Show Plan`确定 IRIS是否以及如何对查询进行了并行处理分区。 要确定当前系统上的处理器数量,使用 `%SYSTEM.Util.NumberOfCPUs()`方法。 ## %STARTTABLE 这个可选关键字指定查询优化器应该开始对`FROM`子句中列出的第一个表执行联接。 其余表的连接顺序留给查询优化器。 将此关键字与`%INORDER`进行比较,后者指定了完整的连接顺序。 `%STARTTABLE`不能与交叉连接或右外连接一起使用。 不能使用`%STARTTABLE`(或`%FIRSTTABLE`)从左`OUTER join`(或右`OUTER join`)的左边开始连接顺序。 如果指定的开始表与外部连接的要求不一致,则会生成一个`SQLCODE -34`错误:“优化器未能找到可用的连接顺序。” 为了避免这种情况,当与外部连接一起使用时,建议`%STARTTABLE`只与`ansi`风格的左外部连接或完整外部连接一起使用。 下表显示了在使用`%INORDER`和`%STARTTABLE`优化组合超查询父视图和内联视图时的合并行为: "" | 没有连接优化器的超查询 | 具有%STARTTABLE的超级查询 | 有`%INORDER`的超级查询 ---|---|---|--- 不带连接优化器的视图 | 如果可能,合并视图 |如果视图是超查询`start: don't merge`。否则,如果可能,合并视图。 |合并如果可能的话;视图的底层表是无序的。 使用`%STARTTABLE`查看 | 不合并 | 如果视图是超级查询`start: merge`,如果可能的话。视图的开始表变成了超级查询的开始表。否则,不合并。 | 不合并 使用`%INORDER`查看 | 不合并 | 不合并 | 如果视图不是由`%INORDER`控制的,则不要合并。否则,如果可能,合并视图;视图的顺序被替换为超级查询连接顺序。 `%FIRSTTABLE`提示在功能上与`%STARTTABLE`相同,但是提供了以任意顺序指定连接表序列的灵活性。 # FROM子句中的表值函数 表值函数是一个类查询,它被投影为一个存储过程,并返回单个结果集。 表值函数是任何具有`SqlProc TRUE`的类查询。 用作表值函数的类查询必须在`LOGICAL`或`RUNTIME`模式下编译。 当作为表值函数使用并在`RUNTIME`模式下编译时,表值函数查询将在`LOGICAL`模式下调用。 表值函数遵循与类查询的存储过程名称相同的命名约定。 参数括号是必须的; 括号可以是空的,可以包含一个字面值或一个主机变量,也可以包含一个用逗号分隔的字面值和主机变量列表。 如果不指定参数(空括号或空字符串),表值函数将返回所有数据行。 要使用表值函数发出查询,用户必须对定义表值函数的存储过程拥有`EXECUTE`权限。 用户还必须对表值函数查询访问的表或视图具有`SELECT`权限。 在下面的示例中,类查询`Sample.Person.ByName`被投影为一个存储过程,因此可以用作表值函数: ```sql SELECT Name,DOB FROM Sample.SP_Sample_By_Name('A') ``` 下面的动态SQL示例指定相同的表值函数。它使用`%Execute()`方法将参数值提供给`?`入参: ```java ClassMethod From() { s myquery="SELECT Name,DOB FROM Sample.SP_Sample_By_Name(?)" 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("A") d rset.%Display() w !,"End of A data",!! s rset = tStatement.%Execute("B") d rset.%Display() w !,"End of B data" } ``` 表值函数只能在`SELECT`语句或`DECLARE`语句的`FROM`子句中使用。表值函数名可以用模式名限定,也可以用非限定名(没有模式名)限定;非限定名使用默认模式。在`SELECT`语句`FROM`子句中,只要可以使用表名,就可以使用表值函数。它可以在视图或子查询中使用,并且可以使用逗号分隔的列表或显式联接语法与其他表引用项联接。 表值函数不能直接用于`INSERT`、`UPDATE`或`DELETE`语句。但是,可以为这些命令指定子查询,以指定表值函数。 SQL没有为表值函数定义`EXTENTSIZE`,也没有为表值函数列定义`SELECTIVITY`。 # FROM子句中的子查询 可以在`FROM`子句中指定子查询。 这称为流子查询。 子查询被视为与表相同的处理方式,包括它在`JOIN`语法中的使用以及使用`as`关键字可选地分配别名。 `FROM`子句可以以任何组合包含多个表、视图和子查询,但要受`JOIN`语法的限制,如`JOIN`中所述。 ```sql SELECT name,region FROM (SELECT t1.name,t1.state,t2.region FROM Employees AS t1 LEFT OUTER JOIN Regions AS t2 ON t1.state=t2.state) GROUP BY region ``` 子查询可以指定`TOP`子句。 当与`TOP`子句配对时,子查询可以包含`ORDER BY`子句。 子查询可以使用`SELECT *`语法,但有以下限制:因为`FROM`子句的结果是值表达式,所以包含`SELECT *`的子查询只能生成一列。 子查询中的连接不能是`NATURAL`连接或接受`USING`子句。 ## 从子查询和%VID 当调用`FROM`子查询时,它为返回的每个子查询行返回一个`%VID`。 `%VID`是一个整数计数器字段; 它的值是系统分配的、唯一的、非空的、非零的、不可修改的。 `%VID`仅在显式指定时返回。 它以数据类型`INTEGER`返回。 因为`%VID`值是顺序整数,所以如果子查询返回的是顺序数据,则它们更有意义; 子查询只能在与`TOP`子句配对时使用`ORDER BY`子句。 因为`%VID`是一个顺序整数,所以可以用它来确定带有`ORDER BY`子句的子查询中项目的排名。 在下面的示例中,`10`条最新的记录按名称顺序列出,但是使用`%VID`值可以很容易地看到它们的时间戳排名: ``` SELECT Name,%VID,TimeStamp FROM (SELECT TOP 10 * FROM MyTable ORDER BY TimeStamp DESC) ORDER BY Name ``` `%VID`的一个常见用途是`“window”`结果集,将执行划分为符合显示窗口中可用行数的顺序子集。 例如,显示`20`条记录,然后等待用户按`Enter`键,然后显示下`20`条记录。 ```java ClassMethod From1() { s myq=4 s myq(1)="SELECT %VID,* " s myq(2)="FROM (SELECT TOP 60 Name,Age FROM Sample.Person " s myq(3)="WHERE Age > 55 ORDER BY Name) " s myq(4)="WHERE %VID BETWEEN ? AND ?" s tStatement = ##class(%SQL.Statement).%New() s qStatus = tStatement.%Prepare(.myq) if qStatus'=1 {w "%Prepare failed:" d $System.Status.DisplayError(qStatus) q} for i=1:10:60 { s rset = tStatement.%Execute(i, i+9) while rset.%Next() { d rset.%Print() } w !! } w "End of data" } ``` ```java DHC-APP>d ##class(PHA.TEST.SQLCommand).From1() 1 "Ahmed,Elmo X." 78 2 "Alton,Phil T." 68 3 "Anderson,Mario L." 78 4 "Bachman,Susan O." 88 5 "Basile,Filomena X." 87 6 "Browne,Robert X." 83 7 "Bukowski,Mario V." 86 8 "Burroughs,Barbara H." 86 9 "Cerri,Stavros Q." 96 10 "Chadbourne,Barb B." 94 ``` # 可选FROM子句 如果`SELECT`项列表(直接或间接)没有引用表数据,则`FROM`子句是可选的。 这种`SELECT`可以用于从函数、运算符表达式、常量或宿主变量返回数据。 对于不引用表数据的查询: - 如果省略`FROM`子句,则不管`TOP`关键字值如何,最多返回一行数据; `TOP 0`不返回任何数据。 `DISTINCT`子句被忽略。 不需要特权。 - 如果指定了`FROM`子句,则必须指定当前命名空间中的现有表。 必须对该表具有`SELECT`权限,即使该表没有被引用。 除非指定了`TOP`或`DISTINCT`子句,或者用`WHERE`或`HAVING`子句限制它,否则返回的相同数据行数等于指定表中的行数。 指定DISTINCT子句将输出限制为单行数据。 `TOP`关键字将输出限制为`TOP`值指定的行数; `TOP 0`不返回任何数据。 无论是否有`FROM`子句,都可以指定后续子句(如`GROUP BY`、`HAVING`或`ORDER BY`)。 `WHERE`或`HAVING`子句可用于确定是否返回结果,或返回多少相同的结果行。 即使没有指定`FROM`子句,这些子句也可以引用表。 可以指定`GROUP BY`或`ORDER BY`子句,但这些子句没有意义。 下面是不引用表数据的`SELECT`语句示例。 两个示例都返回一行信息。 下面的例子省略了`FROM`子句。 `DISTINCT`关键字不是必需的,但是可以指定。 不允许使用`SELECT`子句。 ```sql SELECT 3+4 AS Arith, {fn NOW} AS NowDateTime, {fn DAYNAME({fn NOW})} AS NowDayName, UPPER('MixEd cASe EXPreSSioN') AS UpCase, {fn PI} AS PiConstant ``` 下面的示例包含一个`FROM`子句。 `DISTINCT`关键字用于返回单行数据。 `FROM`子句表引用必须是一个有效的表。 这里允许使用`ORDER BY`子句,但没有意义。 注意,`ORDER BY`子句必须指定一个有效的选择项别名: ```sql SELECT DISTINCT 3+4 AS Arith, {fn NOW} AS NowDateTime, {fn DAYNAME({fn NOW})} AS NowDayName, UPPER('MixEd cASe EXPreSSioN') AS UpCase, {fn PI} AS PiConstant FROM Sample.Person ORDER BY NowDateTime ``` 下面的例子都使用了`WHERE`子句来决定是否返回结果。 第一个包含`FROM`子句,并使用`DISTINCT`关键字返回单行数据。 第二个省略了`FROM`子句,因此最多返回一行数据。 在这两种情况下,`WHERE`子句表引用必须是具有`SELECT`权限的有效表: ```sql SELECT DISTINCT {fn NOW} AS DataOKDate FROM Sample.Person WHERE FOR SOME (Sample.Person)(Name %STARTSWITH 'A') ``` ```sql SELECT {fn NOW} AS DataOKDate WHERE FOR SOME (Sample.Person)(Name %STARTSWITH 'A') ```

#Global Summit 2024

0 帖子0 关注者

#Global Summit 2023

3 帖子0 关注者
文章
姚 鑫 · 二月 15, 2021

第三十五章 Caché 变量大全 $ZNSPACE 变量

# 第三十五章 Caché 变量大全 $ZNSPACE 变量 包含当前命名空间名称。 # 大纲 ```java $ZNSPACE ``` # 描述 `$ZNSPACE`包含当前命名空间的名称。通过设置`$ZNSPACE`,可以更改当前名称空间。 要获取当前命名空间名称,请执行以下操作: ```java DHC-APP>SET ns=$ZNSPACE DHC-APP>WRITE ns DHC-APP ``` 还可以通过调用`%SYSTEM.SYS`类的`Namespace()`方法来获取当前命名空间的名称,如下所示: ```java DHC-APP>SET ns=$SYSTEM.SYS.NameSpace() DHC-APP>WRITE ns DHC-APP ``` 可以使用`%SYS.Namespace`类的`Existes()`方法测试命名空间是否已定义,如下所示: ```java DHC-APP>WRITE ##class(%SYS.Namespace).Exists("USER") 1 DHC-APP>WRITE ##class(%SYS.Namespace).Exists("LOSER") 0 ``` 对于UNIX®系统,默认命名空间建立为系统配置选项。对于Windows系统,它是使用命令行启动选项设置的。 命名空间名称不区分大小写。InterSystems IRIS始终以全大写字母显示显式名称空间名称,以全小写字母显示隐含的名称空间名称。 要获取指定进程的命名空间名称,请使用`%SYS.ProcessQuery`类的方法,如下例所示: ```java DHC-APP>WRITE ##CLASS(%SYS.ProcessQuery).%OpenId($JOB).NameSpaceGet() DHC-APP ``` # 设置当前命名空间 可以使用`ZNSPACE`命令、`SET $NAMESPACE`、`SET $ZNSPACE`或`%cd`实用程序更改当前名称空间。 - 在终端命令提示符下,`ZNSPACE`命令是更改名称空间的首选方式。`SET $ZNSPACE`在功能上与`ZNSPACE`命令相同。 - 在代码例程中,新建`$NAMESPACE`,然后设置`$NAMESPACE = NAMESPACE`是更改当前名称空间的首选方式。通过使用`new $NAMESPACE`和`SET $NAMESPACE`,可以建立一个名称空间上下文,该上下文在方法结束或发生意外错误时自动恢复到前一个名称空间。 可以使用`SET $ZNSPACE`更改进程的当前命名空间。将新命名空间指定为字符串文字或计算结果为带引号的字符串的变量或表达式。可以指定显式名称空间(`“NAMESPACE”`)或隐式名称空间(`“^SYSTEM^DIR”或“^^DIR”`)。 如果指定当前命名空间,则`SET $ZNSPACE`不执行任何操作,也不返回任何错误。如果指定了一个未定义的名称空间,则`SET $ZNSPACE`会生成一个``错误。 不能`new $ZNSPACE`特殊变量。 # 示例 在以下示例中,如果当前命名空间不是`USER`,则`SET $ZNSPACE`命令会将当前命名空间更改为`USER`。请注意,由于`if`测试,命名空间必须全部用大写字母指定。 ```java /// d ##class(PHA.TEST.SpecialVariables).ZNSPACE() ClassMethod ZNSPACE() { SET ns="USER" IF $ZNSPACE=ns { WRITE !,"命名空间已经 ",$ZNSPACE } ELSEIF 1=##class(%SYS.Namespace).Exists(ns) { WRITE !,"命名空间是 ",$ZNSPACE SET $ZNSPACE=ns WRITE !,"将命名空间设置为 ",$ZNSPACE } ELSE { WRITE !,ns," 不是定义的命名空间" } QUIT } ``` ```java DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZNSPACE() 命名空间是 DHC-APP 将命名空间设置为 USER ``` 此示例要求`UnnownUser`已分配`%DB_IRISSYS`和`%DB_USER`角色。
文章
Michael Lei · 一月 17, 2023

ZPM 简单实现实战宝典

ZPM 设计用于与 InterSystems IRIS 数据平台的应用程序和模块一起使用。 它由两个组件组成:ZPN 客户端(用于管理模块的 CLI)和注册表(模块和元信息的数据库)。 我们可以使用 ZPM 来搜索、安装、升级、移除和发布模块。 使用 ZPM,可以安装 ObjectScript 类、前端应用程序、互操作性生产环境、IRIS BI 解决方案、IRIS 数据集或任何文件,例如嵌入式 Python wheel。  今天的这份实战宝典将分为 3 个部分: 1. 安装 ZPM 2. 生成模块 3. 在注册表中查找、安装、发布模块   1. 安装 ZPM * 下载最新版本的 ZPM(它应该是一个 XML 文件)[下载链接](https://pm.community.intersystems.com/packages/zpm/latest/installer) * 将下载的 XML 导入到 IRIS(它只能部署到已打开 IRIS 的 IRIS 终端)并按 Enter 键 _write $SYSTEM.OBJ.Load("C:\zpm.xml", "c")_ 请注意,“C:\zpm.xml”是下载的 XML 文件的路径,这一步可能需要一些时间。 * 安装完成后,只需输入 _zpm_,按 Enter 键,您会看到您在 zpm shell 中 ![](/sites/default/files/inline/images/images/screenshot_2022-10-07_111845.png) 2. 生成模块 在开始生成模块之前,我们需要准备一个文件夹,里面有一个或多个可以加载的文件,因此我在 C 盘下创建了一个名为 zpm 的文件夹。 执行命令 _generate C:/zpm_ 在指定所有必要的内容后,您的第一个模块已成功生成,您还会看到  ![](/sites/default/files/inline/images/images/screenshot_2022-10-07_151452.png) 注意:  1. 模块版本正在使用语义化版本控制  2. 模块源文件夹是包含所有类文件的文件夹 3. zpm 还提供了一个选项,可以添加 web 应用程序和依赖项,本例中我将其留空 现在,打开文件资源管理器,您会看到一个名为“module.xml”的文件,如下面的屏幕截图所示 ![](/sites/default/files/inline/images/images/screenshot_2022-10-07_161718.png) 输入命令 _load C:\ZPM\ _,您会看到您的模块已重新加载、验证、编译和激活  ![](/sites/default/files/inline/images/images/screenshot_2022-10-12_110126.png)   3. 在注册表中查找、安装、发布模块 在当前注册表中查找可用的软件包:_zpm:USER>search_ 举例来说,从当前注册表安装软件包,让我们在公共注册表中安装一个名为 zpmshow 的模块:_zpm:USER>install zpmshow _(命令为 install "moduleName") 加载后发布模块:_zpm:USER>publish myFirstZPMDemo_ 可以使用 _zpm:USER>search_ 验证发布,本例中,您可以看到“myfirstzpmdemo 0.1.0”位于当前注册表中。 ![](/sites/default/files/inline/images/images/screenshot_2022-10-12_110359.png) 注意:如果发布模块时遇到以下错误:_“ERROR! Publishing module, something went wrong”(错误!发布模块时出错了)_,确保当前注册表的状态为“已启用”且可用。 可以使用_ zpm:USER>repo -list, _验证当前注册表的状态。   ![](/sites/default/files/inline/images/images/screenshot_2022-10-10_152312.png)   可用视频:[点击此处](https://www.loom.com/share/0ca097f0dea4476ea294841295f972b2%C2%A0%C2%A0)