搜索​​​​

清除过滤器
文章
Jingwei Wang · 二月 15

使用嵌入式 Python 和 OpenAI API 在 IRIS 中进行数据标签

大型语言模型(例如 OpenAI 的 GPT-4)的发明和普及掀起了一波创新解决方案浪潮,这些解决方案可以利用大量非结构化数据,在此之前,人工处理这些数据是不切实际的,甚至是不可能的。此类应用程序可能包括数据检索(请参阅 Don Woodlock 的 ML301 课程,了解检索增强生成的精彩介绍)、情感分析,甚至完全自主的 AI 代理等! 在本文中,我想演示如何使用 IRIS 的嵌入式 Python 功能直接与 Python OpenAI 库交互,方法是构建一个简单的数据标记应用程序,该应用程序将自动为我们插入IRIS 表中的记录分配关键字。然后,这些关键字可用于搜索和分类数据,以及用于数据分析目的。我将使用客户对产品的评论作为示例用例。 先决条件 运行的IRIS实例 OpenAI API 密钥(您可以在此处创建) 配置好的开发环境(本文将使用VS Code ) Review类 让我们首先创建一个 ObjectScript 类,该类将定义客户评论的数据模型。为了简单起见,我们将只定义 4 个 %String 字段:客户姓名、产品名称、评论正文以及我们将生成的关键字。该类应该扩展%Persistent,以便我们可以将其对象保存到磁盘。 Class DataTagging.Review Extends %Persistent { Property Name As %String(MAXLEN = 50) [ Required ]; Property Product As %String(MAXLEN = 50) [ Required ]; Property ReviewBody As %String(MAXLEN = 300) [ Required ]; Property Keywords As %String(MAXLEN = 300) [ SqlComputed, SqlComputeOnChange = ReviewBody ]; } 由于我们希望在插入或更新 ReviewBody 属性时自动计算 Keywords属性,因此我将其标记为SqlComputed。您可以在此处了解有关计算值的更多信息。 KeywordsComputation方法 我们现在想要定义一种方法,用于根据ReviewBody计算Keywords。我们可以使用Embedded Python直接与官方的openai Python包进行交互。但首先,我们需要安装它。为此,请运行以下 shell 命令: <your-IRIS-installation-path>/bin/irispip install --target <your-IRIS-installation-path>/Mgr/python openai 我们现在可以使用 OpenAI 的聊天完成 API 来生成关键字: ClassMethod KeywordsComputation(cols As %Library.PropertyHelper) As %String [ Language = python ] { ''' This method is used to compute the value of the Keywords property by calling the OpenAI API to generate a list of keywords based on the review body. ''' from openai import OpenAI client = OpenAI( # Defaults to os.environ.get("OPENAI_API_KEY") api_key="<your-api-key>", ) # Set the prompt; use few-shot learning to give examples of the desired output user_prompt = "Generate a list of keywords that summarize the content of a customer review of a product. " \ + "Output a JSON array of strings.\n\n" \ + "Excellent watch. I got the blue version and love the color. The battery life could've been better though.\n\nKeywords:\n" \ + "[\"Color\", \"Battery\"]\n\n" \ + "Ordered the shoes. The delivery was quick and the quality of the material is terrific!.\n\nKeywords:\n" \ + "[\"Delivery\", \"Quality\", \"Material\"]\n\n" \ + cols.getfield("ReviewBody") + "\n\nKeywords:" # Call the OpenAI API to generate the keywords chat_completion = client.chat.completions.create( model="gpt-4", # Change this to use a different model messages=[ { "role": "user", "content": user_prompt } ], temperature=0.5, # Controls how "creative" the model is max_tokens=1024, # Controls the maximum number of tokens to generate ) # Return the array of keywords as a JSON string return chat_completion.choices[0].message.content } 请注意,在提示中,我首先指定了我希望 GPT-4 如何“生成总结产品客户评论内容的关键字列表”的一般说明,然后给出两个示例输入以及所需的输入输出。然后,我插入 cols.getfield("ReviewBody") 并以“Keywords:”一词结束提示,通过提供与我给出的示例格式相同的关键字来推动它完成句子。这是Few-Shot Prompting技术的一个简单示例。 为了演示的简单性,我选择将关键字存储为 JSON 字符串;在生产中存储它们的更好方法可能是DynamicArray ,但我将把它作为练习留给读者。 关键词生成 现在,我们可以通过管理门户使用以下 SQL 脚本向表中插入一行来测试我们的数据标记应用程序: INSERT INTO DataTagging.Review (Name, Product, ReviewBody) VALUES ('Ivan', 'BMW 330i', 'Solid car overall. Had some engine problems but got everything fixed under the warranty.') 如下所示,它自动为我们生成了四个关键字。做得好! 结论 总而言之,InterSystems IRIS 嵌入 Python 的能力在处理非结构化数据时提供了多种可能性。利用 OpenAI 的强大功能进行自动数据标记只是利用这一强大功能可以实现的目标之一。这可以减少人为错误并提高整体效率。
文章
Hao Ma · 五月 15

IRIS/Caché SQL优化经验分享 - 优化关键字

SQL查询优化器一般情况下能给出最好的查询计划,但不是所有情况都这样,所以InterSystems SQL还提供了一个方式, 也就是在查询语句里加入`optimize-option keyword(优化关键字)`, 用来人工的修改查询计划。 比如下面的查询: ```sql SELECT AVG(SaleAmt) FROM %PARALLEL User.AllSales GROUP BY Region ``` 其中的%PARALLEL, 就是最常用的优化关键字, 它强制SQL优化器使用多进程并行处理这个SQL。 您可以这样理解: 如果查询优化器足够聪明,那么绝大多数情况下,根本就不需要优化关键字来人工干预。因此,您也一定不奇怪在不同的IRIS/Caché版本中, 关键字的表现可能不一样。越新的版本,应该是越少用到。比如上面的%PARALLEL, 在Caché的大多数版本中, 在查询中加上它一般都能提高查询速度,而在IRIS中,尤其是2023版本以后, 同样的SQL查询语句,很大的可能查询优化器已经自动使用多进程并行查询了,不再需要用户人工干预了。 因此,先总结有关优化关键字的要点: 1. 优化关键字主要是FROM语句中使用。 UPDATE, INSERT语句也有可以使用的关键字,比如%NOJOURAL等等, 这里我不介绍了,请各位自己查询文档。 > INSERT, UPDATE的关键字常用的有:%NOCHECK %NOINDEX %NOLOCK %NOTRIGGER 等等 2. 各个不同版本的文档中这部分内容有少许的不同。 3. 使用查询关键字要结合阅读查询计划,需要经验的积累。用的多了, 在当前版本什么样的查询需要添加关键字就比较有数了。 最新版本的联机文档在: [Specify Optimization Hints in Queries | Configure SQL Performance Options](https://docs.intersystems.com/iris20241/csp/docbook/DocBook.UI.Page.cls?KEY=GSOC_hints#GSOC_hints_clausekeys) ## %PARALLEL 指定查询使用多个进程并行处理。在Query Plan中您可以得到证实。有关Query Plan的阅读请看前面的帖子。 ## %IGNOREINDEX 指定不用某一个或者某几个index。比如以下查询: ```sql select min(ps_supplycost) from %PARALLEL %IGNOREINDEX SQLUser.supplier.SUPPLIER_PK %IGNOREINDEX SQLUser.part.PART_PK %IGNOREINDEX SQLUser.nation.Nation_PK %IGNOREINDEX SQLUser.region.REGION_PK partsupp, supplier, nation, region where p_partkey = ps_partkey and s_suppkey = ps_suppkey and s_nationkey = n_nationkey and n_regionkey = r_regionkey and r_name = 'AFRICA' ... ``` *为什么要强制不用某些索引?* 一个是用在测试中,经常会比较不同索引的表现。比如你原来有个复合索引,它希望试试新创建的索引是不是更好, 那么很可能您需要告诉SQL引擎不要用以前的索引了。 还有就是您发现某个索引的使用没有让查询性能变好,强制不用它结果可以使用另一个索引,从而来得到更好的查询速度。 ## %ALLINDEX 用于测试所有可用的索引。 SQL引擎默认会在多个可用的索引中选中它判断最高效的,但这个判断不是总正确。加入%ALLINDEX会在生成查询计划前,测试所有可用的索引,以证实或者调整判断。 用到比较多的情况是有多个范围查询字句的情况。在Caché和早期IRIS版本中, 很多情况下, 使用%ALLINDEX会带来性能的提升, 尽管对所有可用索引做测试会有个额外开支. 比如以下的语句, ```sql SELECT TOP 5 ID, Name, Age, SSN FROM %ALLINDEX Sample.Person WHERE (:Name IS NULL or Name %STARTSWITH :Name) AND (:Age IS NULL or Age >= :Age) } ``` ## %NOINDEX 在最新版的IRIS文档中, 这个关键字已经去掉了。 我自己的测试中,在2022年后的IRIS中, 它其实已经不起作用了。 但在Caché中, 非常多的使用%NOINDEX的例子。 [Caché在线文档中的这段](https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_optquery#GSQLOPT_optquery_altshowplans)是这么说的:当绝大多数数据被条件选中(或未被选中)时,这种方法最常用。在小于 () 条件语句下,使用 %NOINDEX 条件级提示通常是有益的。对于“等于”条件语句,使用 %NOINDEX 条件级提示没有任何好处。对于连接条件语句,不支持在 =* 和 *= WHERE 子句外部连接中使用 %NOINDEX;而在 ON 子句连接中使用 %NOINDEX。 这是文档上的例子: E.Age
文章
姚 鑫 · 三月 27, 2021

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

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

独立模式下 EMPI 的安装和适配 - FHIR之转换和摄取

大家好。 在上一篇文章中,我们了解了如何配置 EMPI 来接收 FHIR 消息。为此,我们安装了 InterSystems 提供的 FHIR 适配器,该适配器配置了一个可以向其发送 FHIR 消息的 REST 端点。然后,我们将获取消息并将其转换为 %String,我们将通过 TCP 将其发送到 HSPIDATA 命名空间中配置的 EMPI 的输出。 好吧,是时候看看我们如何检索消息、将其转换回 %DynamicObject 并将其解析为 EMPI 用来存储信息的类。 TCP消息接收 正如我们所指出的,从配置了 FHIR 资源接收的生产中,我们已将消息发送到我们有业务服务侦听的特定 TCP 端口,在我们的例子中,该业务服务将是一个简单的EnsLib.TCP。 PassthroughService的目标是捕获消息并将其转发到业务流程,我们将在其中执行所需的数据转换。 这里有我们的商业服务: 这是它的基本配置: FHIR 消息的转变 正如你所看到的,我们只配置了通过 TCP 接收消息的端口以及我们将向其发送消息的组件,在我们的例子中我们将其称为 Local.BP.FHIRProcess,让我们看一下说类来看看我们如何从 FHIR 资源中检索信息: Class Local.BP.FHIRProcess Extends Ens.BusinessProcess [ ClassType = persistent ] { Method OnRequest(pRequest As Ens.StreamContainer, Output pResponse As Ens.Response) As %Status { set tDynObj = {}. %FromJSON (pRequest.Stream) If (tDynObj '= "" ) { set hubRequest = ##class (HS.Message.AddUpdateHubRequest). %New () // Create AddUpdateHub Message // Name, sex, DOB set givenIter = tDynObj.name. %Get ( 0 ).given. %GetIterator () while givenIter. %GetNext (, .givenName){ if (hubRequest.FirstName '= "" ) { Set hubRequest.FirstName=givenName } else { Set hubRequest.FirstName=hubRequest.FirstName_ " " _givenName } } Set hubRequest.FirstName=tDynObj.name. %Get ( 0 ).given. %Get ( 0 ) Set hubRequest.LastName=tDynObj.name. %Get ( 0 ).family Set hubRequest.Sex=tDynObj.gender Set hubRequest.DOB=hubRequest.DOBDisplayToLogical(tDynObj.birthDate) // Inserts full birth name information for the patient set nameIter = tDynObj.name. %GetIterator () while nameIter. %GetNext (, .name){ Set tName = ##class (HS.Types.PersonName). %New () if (name.prefix '= "" ) { Set tName.Prefix = name.prefix. %Get ( 0 ) } Set tName.Given = name.given. %Get ( 0 ) Set tName.Middle = "" Set tName.Family = name.family Set tName.Suffix = "" Set tName.Type= ^Ens .LookupTable( "TypeOfName" ,name. use ) Do hubRequest.Names.Insert(tName) } set identIter = tDynObj.identifier. %GetIterator () while identIter. %GetNext (, .identifier){ if (identifier.type'= "" ){ if (identifier.type.coding. %Get ( 0 ).code = "MR" ) { Set hubRequest.MRN = identifier.value Set hubRequest.AssigningAuthority = ^Ens .LookupTable( "hospital" ,identifier.system) Set hubRequest.Facility = ^Ens .LookupTable( "hospital" ,identifier.system) } elseif (identifier.type.coding. %Get ( 0 ).code = "SS" ) { Set hubRequest.SSN = identifier.value } else { Set tIdent= ##class (HS.Types.Identifier). %New () Set tIdent.Root = identifier.system // refers to an Assigning Authority entry in the OID Registry Set tIdent.Extension = identifier.value Set tIdent.AssigningAuthorityName = identifier.system Set tIdent. Use = identifier.type.coding. %Get ( 0 ).code Do hubRequest.Identifiers.Insert(tIdent) } } } // Address set addressIter = tDynObj.address. %GetIterator () while addressIter. %GetNext (, .address){ Set addr= ##class (HS.Types.Address). %New () Set addr.City=address.city Set addr.State=address.state Set addr.Country=address.country Set addr.StreetLine=address.line. %Get ( 0 ) Do hubRequest.Addresses.Insert(addr) } //Telephone set identTel = tDynObj.telecom. %GetIterator () while identTel. %GetNext (, .telecom){ if (telecom.system = "phone" ) { Set tel= ##class (HS.Types.Telecom). %New () Set tel.PhoneNumber=telecom.value Do hubRequest.Telecoms.Insert(tel) } } } Set tSC = ..SendRequestSync ( "HS.Hub.MPI.Manager" , hubRequest, .pResponse) Quit tSC } Storage Default { <Type> %Storage.Persistent </Type> } } 让我们更详细地看看我们正在做什么: 首先我们收到了业务服务发来的消息: Method OnRequest(pRequest As Ens.StreamContainer, Output pResponse As Ens.Response) As %Status { set tDynObj = {}. %FromJSON (pRequest.Stream) 正如我们在 OnRequest 方法的签名中看到的,输入消息对应于Ens.StreamContainer类型的类。 %String 类型消息的这种转换已在业务服务中进行。在该方法的第一行中,我们要做的是检索在 pRequest 变量中作为 Stream 找到的消息。然后,我们使用 %FromJSON 语句将其转换为 %DynamicObject。 通过将消息映射到动态对象,我们将能够访问已发送的 FHIR 资源的每个字段: set tDynObj = {}. %FromJSON (pRequest.Stream) If (tDynObj '= "" ) { set hubRequest = ##class (HS.Message.AddUpdateHubRequest). %New () // Create AddUpdateHub Message // Name, sex, DOB set givenIter = tDynObj.name. %Get ( 0 ).given. %GetIterator () while givenIter. %GetNext (, .givenName){ if (hubRequest.FirstName '= "" ) { Set hubRequest.FirstName=givenName } else { Set hubRequest.FirstName=hubRequest.FirstName_ " " _givenName } } Set hubRequest.FirstName=tDynObj.name. %Get ( 0 ).given. %Get ( 0 ) Set hubRequest.LastName=tDynObj.name. %Get ( 0 ).family Set hubRequest.Sex=tDynObj.gender Set hubRequest.DOB=hubRequest.DOBDisplayToLogical(tDynObj.birthDate) 在此片段中,我们看到如何创建HS.Message.AddUpdateHubRequest类的对象,该对象是我们将发送到负责在 EMPI 内执行相应操作的业务操作 HS.Hub.MPI.Manager 的对象,无论是是创建新患者或更新它,以及将其与 EMPI 中已有的其他患者可能存在的可能匹配项链接起来。 下一步是使用从业务服务接收到的数据填充新对象。正如您所看到的,我们所做的就是从刚刚创建的动态对象的不同字段中检索数据。动态对象的格式与 HL7 FHIR 为患者资源定义的格式完全对应,您可以直接在HL7 FHIR 网页上查看示例 对于我们的示例,我们从 HL7 FHIR 页面本身提供的列表中选择了该患者: { "resourceType" : "Patient" , "id" : "example" , "text" : { "status" : "generated" , "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n\t\t\t<table>\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Name</td>\n\t\t\t\t\t\t<td>Peter James \n <b>Chalmers</b> ("Jim")\n </td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Address</td>\n\t\t\t\t\t\t<td>534 Erewhon, Pleasantville, Vic, 3999</td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Contacts</td>\n\t\t\t\t\t\t<td>Home: unknown. Work: (03) 5555 6473</td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Id</td>\n\t\t\t\t\t\t<td>MRN: 12345 (Acme Healthcare)</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>" }, "identifier" : [ { "use" : "usual" , "type" : { "coding" : [ { "system" : "http://terminology.hl7.org/CodeSystem/v2-0203" , "code" : "MR" } ] }, "system" : "urn:oid:1.2.36.146.595.217.0.1" , "value" : "12345" , "period" : { "start" : "2001-05-06" }, "assigner" : { "display" : "Acme Healthcare" } } ], "active" : true , "name" : [ { "use" : "official" , "family" : "Chalmers" , "given" : [ "Peter" , "James" ] }, { "use" : "usual" , "given" : [ "Jim" ] }, { "use" : "maiden" , "family" : "Windsor" , "given" : [ "Peter" , "James" ], "period" : { "end" : "2002" } } ], "telecom" : [ { "use" : "home" }, { "system" : "phone" , "value" : "(03) 5555 6473" , "use" : "work" , "rank" : 1 }, { "system" : "phone" , "value" : "(03) 3410 5613" , "use" : "mobile" , "rank" : 2 }, { "system" : "phone" , "value" : "(03) 5555 8834" , "use" : "old" , "period" : { "end" : "2014" } } ], "gender" : "male" , "birthDate" : "1974-12-25" , "_birthDate" : { "extension" : [ { "url" : "http://hl7.org/fhir/StructureDefinition/patient-birthTime" , "valueDateTime" : "1974-12-25T14:35:45-05:00" } ] }, "deceasedBoolean" : false , "address" : [ { "use" : "home" , "type" : "both" , "text" : "534 Erewhon St PeasantVille, Rainbow, Vic 3999" , "line" : [ "534 Erewhon St" ], "city" : "PleasantVille" , "district" : "Rainbow" , "state" : "Vic" , "postalCode" : "3999" , "period" : { "start" : "1974-12-25" } } ], "contact" : [ { "relationship" : [ { "coding" : [ { "system" : "http://terminology.hl7.org/CodeSystem/v2-0131" , "code" : "N" } ] } ], "name" : { "family" : "du Marché" , "_family" : { "extension" : [ { "url" : "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix" , "valueString" : "VV" } ] }, "given" : [ "Bénédicte" ] }, "telecom" : [ { "system" : "phone" , "value" : "+33 (237) 998327" } ], "address" : { "use" : "home" , "type" : "both" , "line" : [ "534 Erewhon St" ], "city" : "PleasantVille" , "district" : "Rainbow" , "state" : "Vic" , "postalCode" : "3999" , "period" : { "start" : "1974-12-25" } }, "gender" : "female" , "period" : { "start" : "2012" } } ], "managingOrganization" : { "reference" : "Organization/1" } } 首先,我们创建了 2 个查找表,用于映射姓名类型和分配医疗记录号 (MR) 的权限,第一个与 EMPI 管理的类型兼容,第二个用于识别分配权限生成标识符: Set tName.Type= ^Ens .LookupTable( "TypeOfName" ,name. use ) Set hubRequest.AssigningAuthority = ^Ens .LookupTable( "hospital" ,identifier.system) Set hubRequest.Facility = ^Ens .LookupTable( "hospital" ,identifier.system) 启动测试消息 完美,让我们针对我们在上一篇文章中定义的端点启动 FHIR 消息: 正如您所看到的,我们收到了 200 响应,这仅意味着 EMPI 已正确接收到消息,现在让我们看看在我们的生产中生成的跟踪: 这里我们有我们的病人,您可以看到转换已成功执行,并且 FHIR 消息中报告的所有字段都已正确分配。可以看到,一条IDUpdateNotificationRequest通知消息已经生成了。当在系统中执行创建或更新患者的操作时,会生成此类通知。 很好,让我们通过按姓名搜索患者来检查患者是否在我们的系统中正确注册: 答对了!让我们更详细地看看我们亲爱的Peter的数据: 相当完美!我们的 EMPI 中已经包含了有关患者的所有必要信息。正如您所看到的,该机制非常简单,让我们回顾一下我们执行的步骤: 我们已将 InterSystems 提供的 FHIR 适配器工具安装在配置为支持互操作性的命名空间(与 EMPI 独立安装生成的命名空间不同的命名空间,在我的例子中称为 WEBINAR)中。 我们在此命名空间中创建了一个业务操作,它将接收到的HS.FHIRServer.Interop.Request类型的消息转换为 %String,并将其发送到在 EMPI 命名空间 (HSPIDATA) 的生产中配置的业务服务。 接下来,我们添加了EnsLib.TCP.PassthroughService类的业务服务,该类接收从 WEBINAR 命名空间的生成发送的消息并重定向到业务流程Local.BP.FHIRProcess 。 在 BP Local.BP.FHIRProcess 中,我们已将接收到的 Stream 转换为HS.Message.AddUpdateHubRequest类型的对象,并将其发送到业务运营HS.Hub.MPI.Manager ,该管理器将负责将其注册到我们的EMPI。 正如您所看到的,EMPI 功能与 IRIS 集成引擎提供的功能的结合使我们能够使用几乎任何类型的技术。 我希望这篇文章对您有用。如果您有任何问题或建议,您已经知道,请发表评论,我将很乐意为您解答。 原贴作者:@Luis Angel
文章
姚 鑫 · 三月 16, 2021

第十一章 SQL隐式联接(箭头语法)

# 第十一章 SQL隐式联接(箭头语法) **InterSystems SQL提供了一个特殊的`–>`运算符,作为从相关表中获取值的快捷方式,而在某些常见情况下无需指定显式的`JOIN`即可。可以使用此箭头语法代替显式联接语法,也可以将其与显式联接语法结合使用。箭头语法执行左外部联接。** **箭头语法可用于类的属性或父表的关系属性的引用。其他类型的关系和外键不支持箭头语法。不能在`ON`子句中使用箭头语法(`–>`)。** # 属性引用 可以使用`- >`操作符作为从`“引用表”`获取值的简写。 例如,假设定义了两个类:`Company`: ```java Class Sample.Company Extends %Persistent [DdlAllowed] { /// The Company name Property Name As %String; } ``` `Employee`: ```java Class Sample.Employee Extends %Persistent [DdlAllowed] { /// The Employee name Property Name As %String; /// The Company this Employee works for Property Company As Company; } ``` `Employee`类包含一个属性,该属性是对`Company`对象的引用。 在基于对象的应用程序中,可以使用点语法遵循此引用。 例如,要查找`Employee`工作的`Company`名称: ```java Set name = employee.Company.Name ``` 可以使用使用外部连接来连接`Employee`和`Company`表的SQL语句来执行相同的任务: ```sql SELECT Sample.Employee.Name, Sample.Company.Name AS CompName FROM Sample.Employee LEFT OUTER JOIN Sample.Company ON Sample.Employee.Company = Sample.Company.ID ``` ![image](/sites/default/files/inline/images/1_27.png) 使用`- >`操作符,可以更简洁地执行相同的外连接操作: ```sql SELECT Name, Company->Name AS CompName FROM Sample.Employee ``` ![image](/sites/default/files/inline/images/2_16.png) 只要在表中有引用列,就可以使用`–>`运算符;也就是说,其列的值是被引用表的ID(本质上是外键的特殊情况)。在这种情况下,`Sample.Employee`的`Company`字段包含`Sample.Company`表中记录的`ID`。可以在可以在查询中使用列表达式的任何地方使用`–>`运算符。例如,在`WHERE`子句中: ```sql SELECT Name,Company AS CompID,Company->Name AS CompName FROM Sample.Employee WHERE Company->Name %STARTSWITH 'G' ``` ![image](/sites/default/files/inline/images/3_15.png) 使用`–>`运算符,可以更简洁地执行相同的`OUTER JOIN`操作: 这等效于: ```sql SELECT E.Name,E.Company AS CompID,C.Name AS CompName FROM Sample.Employee AS E, Sample.Company AS C WHERE E.Company = C.ID AND C.Name %STARTSWITH 'G' ``` **请注意,在这种情况下,此等效查询使用`INNER JOIN`。** 以下示例使用箭头语法访问`Sample.Person`中的`“Spouse”`字段。如示例所示,`Sample.Employee`中的`Spouse`字段包含`Sample.Person`中记录的`ID`。本示例返回`Employee`与其`Spouse`的`Home_State`相同的`Home_State`或`Office_State`的那些记录: ```sql SELECT Name,Spouse,Home_State,Office_State,Spouse->Home_State AS SpouseState FROM Sample.Employee WHERE Home_State=Spouse->Home_State OR Office_State=Spouse->Home_State ``` ![image](/sites/default/files/inline/images/4_10.png) 可以在`GROUP BY`子句中使用–>运算符: ```sql SELECT Name,Company->Name AS CompName FROM Sample.Employee GROUP BY Company->Name ``` ![image](/sites/default/files/inline/images/5_4.png) 可以在`ORDER BY`子句中使用`–>`运算符: ```sql SELECT Name,Company->Name AS CompName FROM Sample.Employee ORDER BY Company->Name ``` ![image](/sites/default/files/inline/images/6_3.png) 或在`ORDER BY`子句中为`–>`运算符列引用列别名: ```sql SELECT Name,Company->Name AS CompName FROM Sample.Employee ORDER BY CompName ``` 支持复合箭头语法,如以下示例所示。在此示例中,`Cinema.Review`表包含`“Film”`字段,其中包含`Cinema.Film`表的行`ID`。 `Cinema.Film`表包含`Category`字段,其中包含`Cinema.Category`表的行`ID`。因此,`Film-> Category-> CategoryName`访问以下三个表,以返回具有`ReviewScore`的每部电影的`CategoryName`: ```sql SELECT ReviewScore,Film,Film->Title,Film->Category,Film->Category->CategoryName FROM Cinema.Review ORDER BY ReviewScore ``` # 子表引用 可以使用`–>`运算符来引用子表。例如,如果`LineItems`是`Orders`表的子表,则可以指定: ```sql SELECT LineItems->amount FROM Orders ``` 请注意,在`Orders`中没有称为`LineItems`的属性。 `LineItems`是包含数量字段的子表的名称。该查询在结果集中为每个`Order`行生成多个行。它等效于: ```sql SELECT L.amount FROM Orders O LEFT JOIN LineItems L ON O.id=L.custorder ``` 其中`ustust`是`LineItems`表的父引用字段。 # 箭头语法权限 使用箭头语法时,必须对两个表中的引用数据都具有`SELECT`权限。必须在被引用的列上具有表级`SELECT`权限或列级`SELECT`权限。使用列级权限,需要对被引用表以及被引用列的ID具有`SELECT`权限。 以下示例演示了所需的列级权限: ```sql SELECT Name,Company->Name AS CompanyName FROM Sample.Employee GROUP BY Company->Name ORDER BY Company->Name ``` 在上面的示例中,必须对`Sample.Employee.Name`,`Sample.Company.Name`和`Sample.Company.ID`具有列级`SELECT`权限: ```java // d ##class(PHA.TEST.SQL).arrow() ClassMethod arrow() { SET tStatement = ##class(%SQL.Statement).%New() SET privchk1="%CHECKPRIV SELECT (Name,ID) ON Sample.Company" SET privchk2="%CHECKPRIV SELECT (Name) ON Sample.Employee" CompanyPrivTest SET qStatus = tStatement.%Prepare(privchk1) IF qStatus'=1 { WRITE "%Prepare 失败:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() IF rset.%SQLCODE=0 { WRITE !,"拥有Company权限",! } ELSE { WRITE !,"无权限: SQLCODE=",rset.%SQLCODE,! } EmployeePrivTest SET qStatus = tStatement.%Prepare(privchk2) IF qStatus'=1 { WRITE "%Prepare 失败:" DO $System.Status.DisplayError(qStatus) QUIT } SET rset = tStatement.%Execute() IF rset.%SQLCODE=0 { WRITE !,"拥有Employee权限",! } ELSE { WRITE !,"无权限: SQLCODE=",rset.%SQLCODE } } ``` ```java DHC-APP>d ##class(PHA.TEST.SQL).arrow() 拥有Company权限 拥有Employee权限 ```
文章
Claire Zheng · 八月 17, 2021

FHIR标准和国际基于FHIR的互联互通实践(6):FHIR如何用一个标准涵盖尽可能多的用例?

回过头来,业务场景都是千人千面的, FHIR怎么能够用一个标准涵盖尽可能多的用例?HL7吸收了V3的教训,在V3里面不成功的、或者说采纳度比较低的一个原因就V3试图穷举所有用例,由HL7组织自己来规范这些用例。这个是蛮沉重的教训,这也是V3的方法论虽然好,但是这套实施的路线在国际上有很大障碍的原因。 HL7已经不试图再自己穷举所有用例开发标准。它在FHIR的标准开发上,使用的是80/20原则。80/20的原则就是它自己专注在80%的业务都需要的那些数据部分上,其它20%交给使用者自己去进行相应的扩展,也就说它有很好的扩展性。 FHIR到底怎么来进行扩展?FHIR可以对模型、值集、API进行扩展,并且进一步的进行约束。而这些扩展本身也是资源,因此扩展可以像其它资源一样被创建、被访问、被交换。 怎么把你的扩展告诉别人? 在HL7 V2年代扩展很容易做,用z字段就可以来做扩展。但是你自己做的扩展约束需要通过文档和人工沟通的方式跟对方来进行人工沟通,告诉人家你是怎么扩展的,你是怎么约束的,你是怎么计划使用HL7消息。 而FHIR是通过Profile(规范)来进行约束和扩展表达的。将你做的扩展和约束与被扩展和约束的资源通过Profile(规范)来进行关联,然后注册这些Profile(规范),让别人能够找到。对方的技术系统只要通过你发给他的这些约束和扩展过的资源里面所标记的Profile的URL,就可以来得到获取并够解析Profile(规范),这样就能够理解你所做的约束和扩展,实现机器可读。这是FHIR跟之前标准在扩展模式上非常大的区别。 另外因为涉及到了用例,通常Profile(规范)是级联的。先按国家和地区的通用用例来建立一个区域级的、一个大的、有共性的Profile(规范),然后区域内的医疗机构可以以此为基础来创建自己下一级的Profile(规范),自己可能有一些扩展——例如说梅奥诊所,它可以基于美国的核心的Profile(规范)、扩展过的患者的资源上,来进行自己的一些患者信息扩展——这种级联扩展方式能够保证最大程度的互操作能力,即在同一个区域里面的所有机构,都共享一个通用的资源,可以保证互操作能力的最大化实现。 另外一个概念就是实施指南(Implementation Guides)。FHIR本身是一个平台规范,它本身关注在能力建设和生态建设上,这意味着广大的用户可以基于FHIR来开发出不同的解决方案。而实施指南就是说明如何使用FHIR的资源和能力来构建解决方案的。 注意区别Profile(规范)与FHIR的实施指南(Implementation Guide)。FHIR实施指南(Implementation Guide)相当于是什么?相当于经线,描述的是针对业务场景所需要的使用的方法;而Profile(规范)相当于是纬线,描述的是应用场景所需要的标准本身。HL7组织提供了一个实施指南注册的一个网站,大家都可以将自己的用例的实施指南注册在上面,所有人都可以看到、获取、利用、学习它。 上图是我把网站上列出来的所有注册的实施指南做的一个汇总。从这张汇总出来的实施指南的统计图来看,它涉及的业务场景是非常广泛的,包括了电子病历访问、管理、公卫、财务等等,这也说明了FHIR资源的覆盖面其实非常广,它可以覆盖从临床、管理、财务、设备、组学等方方面面。 FHIR还有生态。HL7设计FHIR初衷就是让它成为一个生态的标准,围绕着FHIR其实已经有一个不断扩大的生态圈。这里面列出来一些包括刚才我们提到的SMART on FHIR。 注:本文根据InterSystems中国技术总监乔鹏演讲整理而成。
文章
Michael Lei · 六月 1, 2022

部分IRIS 2022 年度编程大奖赛作品展示—— 利用IRIS 一体化机器学习IntegratedML来预测糖尿病的Web 应用

糖尿病可以从医学界熟知的一些参数中发现。这样,为了帮助医学界和计算机软件系统,特别是人工智能软件,美国国家糖尿病和消化道及肾脏疾病研究所发布了一个非常有用的数据集,用于训练糖尿病检测/预测的机器学习算法。这份出版物可以在最大和最知名的ML数据库Kaggle上找到,网址是https://www.kaggle.com/datasets/mathchi/diabetes-data-set。 该糖尿病数据集有以下元数据信息(来源:https://www.kaggle.com/datasets/mathchi/diabetes-data-set): 怀孕:怀孕次数 葡萄糖: 口服葡萄糖耐量试验中2小时的血浆葡萄糖浓度 Plasma glucose concentration a 2 hours in an oral glucose tolerance test 血压: 舒张压(mm Hg) 皮肤厚度: 肱三头肌皮褶厚度(mm) 胰岛素: 2小时血清胰岛素(mu U/ml) BMI: 体重指数 (体重 kg/(身高 m)^2) 糖尿病血统函数: 糖尿病血统函数(它提供了一些关于亲属中的糖尿病史以及这些亲属与病人的遗传关系的数据。这种对遗传影响的测量使我们了解到一个人可能有的遗传风险与糖尿病的发病有关--来源:https://machinelearningmastery.com/case-study-predicting-the-onset-of-diabetes-within-five-years-part-1-of-3/) 年龄: 结果: 类变量 (0 or 1) 实例数量: 768 属性数量: 8 + 1个类变量 对每个属性: (全部为numeric数字量化类型) 怀孕次数 口服葡萄糖耐量试验中2小时的血浆葡萄糖浓度 舒张压 (mm Hg) 三头肌皮褶厚度 (mm) 2小时血清胰岛素 (mu U/ml) BMI指数 (体重 kg/(身高 m)^2) 糖尿病血统函数 年龄 类变量 (0 or 1) 缺失属性值: 是 类分布: (类值为1解释为 "糖尿病测试阳性") 从Kaggle获取糖尿病数据 Kaggle的糖尿病数据可以通过Health-Dataset程序加载到IRIS表中:https://openexchange.intersystems.com/package/Health-Dataset。要做到这一点,在你的module.xml项目中,设置依赖关系(Health Dataset的ModuleReference)。 Module.xml with Health Dataset application reference <?xml version="1.0" encoding="UTF-8"?> <Export generator="Cache" version="25"> <Document name="predict-diseases.ZPM"> <Module> <Name>predict-diseases</Name> <Version>1.0.0</Version> <Packaging>module</Packaging> <SourcesRoot>src/iris</SourcesRoot> <Resource Name="dc.predict.disease.PKG"/> <Dependencies> <ModuleReference> <Name>swagger-ui</Name> <Version>1.*.*</Version> </ModuleReference> <ModuleReference> <Name>dataset-health</Name> <Version>*</Version> </ModuleReference> </Dependencies> <CSPApplication Url="/predict-diseases" DispatchClass="dc.predict.disease.PredictDiseaseRESTApp" MatchRoles=":{$dbrole}" PasswordAuthEnabled="1" UnauthenticatedEnabled="1" Recurse="1" UseCookies="2" CookiePath="/predict-diseases" /> <CSPApplication CookiePath="/disease-predictor/" DefaultTimeout="900" SourcePath="/src/csp" DeployPath="${cspdir}/csp/${namespace}/" MatchRoles=":{$dbrole}" PasswordAuthEnabled="0" Recurse="1" ServeFiles="1" ServeFilesTimeout="3600" UnauthenticatedEnabled="1" Url="/disease-predictor" UseSessionCookie="2" /> </Module> </Document> </Export> 预测糖尿病的前端和后端应用程序 访问 Open Exchange 应用连接 (https://openexchange.intersystems.com/package/Disease-Predictor) 并遵守以下步骤: Clone/git 把repo pull 到任何本地目录 $ git clone https://github.com/yurimarx/predict-diseases.git 打开 该目录下Docker终端并执行: $ docker-compose build 执行IRIS container: $ docker-compose up -d 在管理门户中执行查询来训练AI模型: http://localhost:52773/csp/sys/exp/%25CSP.UI.Portal.SQL.Home.zen?$NAMESPACE=USER 创建用来训练的 VIEW : CREATE VIEW DiabetesTrain AS SELECT Outcome, age, bloodpressure, bmi, diabetespedigree, glucose, insulin, pregnancies, skinthickness FROM dc_data_health.Diabetes 利用View视图来创建 AI 模型: CREATE MODEL DiabetesModel PREDICTING (Outcome) FROM DiabetesTrain 训练模型: TRAIN MODEL DiabetesModel 访问 http://localhost:52773/disease-predictor/index.html 来使用疾病预测器qian frontend and predict diseases like this: 幕后工作 后端预测糖尿病的类方法 InterSystems IRIS 支持执行Select 并使用上一个创建的模型来预测。 Backend ClassMethod to predict Diabetes /// Predict Diabetes ClassMethod PredictDiabetes() As %Status { Try { Set data = {}.%FromJSON(%request.Content) Set qry = "SELECT PREDICT(DiabetesModel) As PredictedDiabetes, " _"age, bloodpressure, bmi, diabetespedigree, glucose, insulin, " _"pregnancies, skinthickness " _"FROM (SELECT "_data.age_" AS age, " _data.bloodpressure_" As bloodpressure, " _data.bmi_" AS bmi, " _data.diabetespedigree_" AS diabetespedigree, " _data.glucose_" As glucose, " _data.insulin_" AS insulin, " _data.pregnancies_" As pregnancies, " _data.skinthickness_" AS skinthickness)" Set tStatement = ##class(%SQL.Statement).%New() Set qStatus = tStatement.%Prepare(qry) If qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT} Set rset = tStatement.%Execute() Do rset.%Next() Set Response = {} Set Response.PredictedDiabetes = rset.PredictedDiabetes Set Response.age = rset.age Set Response.bloodpressure = rset.bloodpressure Set Response.bmi = rset.bmi Set Response.diabetespedigree = rset.diabetespedigree Set Response.glucose = rset.glucose Set Response.insulin = rset.insulin Set Response.pregnancies = rset.pregnancies Set Response.skinthickness = rset.skinthickness Set %response.Status = 200 Set %response.Headers("Access-Control-Allow-Origin")="*" Write Response.%ToJSON() Return 1 } Catch err { write !, "Error name: ", ?20, err.Name, !, "Error code: ", ?20, err.Code, !, "Error location: ", ?20, err.Location, !, "Additional data: ", ?20, err.Data, ! Return 0 } } 现在,任何网络应用都可以使用预测并显示预测结果。欢迎在frontend 文件夹查看本应用的源代码。
文章
Michael Lei · 九月 13, 2022

使用 Globals存储思维导图

Globals是InterSystems IRIS的数据持久性的核心。它很灵活,允许存储JSON文档、关系数据、面向对象的数据、OLAP立方体和自定义数据模型,例如思维导图。要了解如何使用globals来存储、删除和获取思维导图数据,请遵循以下步骤: 1. 把repo Clone/git到任意本地目录 $ git clone https://github.com/yurimarx/global-mindmap.git 2. 在该目录下打开Docker 终端并执行: $ docker-compose build 3. 启动 IRIS 容器: $ docker-compose up -d 4. 访问 http://localhost:3000 来使用思维导图的前端并创建类似以上的思维导图 本例子的源代码 存储数据 (更多请访问: https://www.npmjs.com/package/mind-elixir): { topic: 'node topic', id: 'bd1c24420cd2c2f5', style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' }, parent: null, tags: ['Tag'], icons: ['😀'], hyperLink: 'https://github.com/ssshooter/mind-elixir-core', } 注意parent属性,它被用来在mindmap节点之间建立父/子关系。 使用Globals 来存储思维导图的源代码 ClassMethod StoreMindmapNode /// Store mindmap node ClassMethod StoreMindmapNode() As %Status { Try { Set data = {}.%FromJSON(%request.Content) Set ^mindmap(data.id) = data.id /// set mindmap key Set ^mindmap(data.id, "topic") = data.topic /// set topic subscript Set ^mindmap(data.id, "style", "fontSize") = data.style.fontSize /// set style properties subscripts Set ^mindmap(data.id, "style", "color") = data.style.color Set ^mindmap(data.id, "style", "background") = data.style.background Set ^mindmap(data.id, "parent") = data.parent /// store parent id subscript Set ^mindmap(data.id, "tags") = data.tags.%ToJSON() /// store tags subscript Set ^mindmap(data.id, "icons") = data.icons.%ToJSON() /// store icons subscript Set ^mindmap(data.id, "hyperLink") = data.hyperLink /// store hyperLink subscript Set %response.Status = 200 Set %response.Headers("Access-Control-Allow-Origin")="*" Write "Saved" Return $$$OK } Catch err { write !, "Error name: ", ?20, err.Name, !, "Error code: ", ?20, err.Code, !, "Error location: ", ?20, err.Location, !, "Additional data: ", ?20, err.Data, ! Return $$$NOTOK } } 我们创建了一个名为^mindmap的Global。对于每个思维导图的属性,它被存储在一个Globals下标中。下标的键是mindmap的id属性。 删除思维导图节点的源代码 - kill the global ClassMethod DeleteMindmapNode /// Delete mindmap node ClassMethod DeleteMindmapNode(id As %String) As %Status { Try { Kill ^mindmap(id) /// delete selected mindmap node using the id (global key) Set %response.Status = 200 Set %response.Headers("Access-Control-Allow-Origin")="*" Write "Deleted" Return $$$OK } Catch err { write !, "Error name: ", ?20, err.Name, !, "Error code: ", ?20, err.Code, !, "Error location: ", ?20, err.Location, !, "Additional data: ", ?20, err.Data, ! Return $$$NOTOK } } 这个例子使用mindmap.id作为mindmap的Global Key,所以删除很容易: call Kill ^mindmap(<mindmap id>) 获得所有存储内容的源代码- 用 $ORDER循环globals ClassMethod GetMindmap - return all mindmap global nodes /// Get mindmap content ClassMethod GetMindmap() As %Status { Try { Set Nodes = [] Set Key = $Order(^mindmap("")) /// get the first mindmap node stored - the root Set Row = 0 While (Key '= "") { /// while get child mindmap nodes Do Nodes.%Push({}) /// create a item into result Set Nodes.%Get(Row).style = {} Set Nodes.%Get(Row).id = Key /// return the id property Set Nodes.%Get(Row).hyperLink = ^mindmap(Key,"hyperLink") /// return the hyperlink property Set Nodes.%Get(Row).icons = ^mindmap(Key,"icons") /// return icons property Set Nodes.%Get(Row).parent = ^mindmap(Key,"parent") /// return parent id property Set Nodes.%Get(Row).style.background = ^mindmap(Key,"style", "background") /// return the style properties Set Nodes.%Get(Row).style.color = ^mindmap(Key,"style", "color") Set Nodes.%Get(Row).style.fontSize = ^mindmap(Key,"style", "fontSize") Set Nodes.%Get(Row).tags = ^mindmap(Key,"tags") /// return tags property Set Nodes.%Get(Row).topic = ^mindmap(Key,"topic") /// return topic property (title mindmap node) Set Row = Row + 1 Set Key = $Order(^mindmap(Key)) /// get the key to the next mindmap global node } Set %response.Status = 200 Set %response.Headers("Access-Control-Allow-Origin")="*" Write Nodes.%ToJSON() Return $$$OK } Catch err { write !, "Error name: ", ?20, err.Name, !, "Error code: ", ?20, err.Code, !, "Error location: ", ?20, err.Location, !, "Additional data: ", ?20, err.Data, ! Return $$$NOTOK } } 用$Order(^mindmap("")) - empty "" - 得到第一个mindmap Global (根节点)。对于每个属性值,我们使用^mindmap(Key,<property name>)。最后,调用$Order(^mindmap(Key))来获得下一个事件。 前端 Mind-elixir和React被用来渲染和编辑mindmap,消耗使用IRIS构建的API后端。见mindmap的反应组件: Mindmap React component - consuming IRIS REST API import React from "react"; import MindElixir, { E } from "mind-elixir"; import axios from 'axios'; class Mindmap extends React.Component { componentDidMount() { this.dynamicWidth = window.innerWidth; this.dynamicHeight = window.innerHeight; axios.get(`http://localhost:52773/global-mindmap/hasContent`) .then(res => { if (res.data == "1") { axios.get(`http://localhost:52773/global-mindmap/get`) .then(res2 => { this.ME = new MindElixir({ el: "#map", direction: MindElixir.LEFT, data: this.renderExistentMindmap(res2.data), draggable: true, // default true contextMenu: true, // default true toolBar: true, // default true nodeMenu: true, // default true keypress: true // default true }); this.ME.bus.addListener('operation', operation => { console.log(operation) if (operation.name == 'finishEdit' || operation.name == 'editStyle') { this.saveMindmapNode(operation.obj) } else if (operation.name == 'removeNode') { this.deleteMindmapNode(operation.obj.id) } }) this.ME.init(); }) } else { this.ME = new MindElixir({ el: "#map", direction: MindElixir.LEFT, data: MindElixir.new("New Mindmap"), draggable: true, // default true contextMenu: true, // default true toolBar: true, // default true nodeMenu: true, // default true keypress: true // default true }); this.ME.bus.addListener('operation', operation => { console.log(operation) if (operation.name == 'finishEdit' || operation.name == 'editStyle') { this.saveMindmapNode(operation.obj) } else if (operation.name == 'removeNode') { this.deleteMindmapNode(operation.obj.id) } }) this.saveMindmapNode(this.ME.nodeData) this.ME.init(); } }) } render() { return ( <div id="map" style={{ height: window.innerHeight + 'px', width: '100%' }} /> ); } deleteMindmapNode(mindmapNodeId) { axios.delete(`http://localhost:52773/global-mindmap/delete/${mindmapNodeId}`) .then(res => { console.log(res); console.log(res.data); }) } saveMindmapNode(node) { axios.post(`http://localhost:52773/global-mindmap/save`, { topic: (node.topic == undefined ? "" : node.topic), id: node.id, style: (node.style == undefined ? "" : node.style), parent: (node.parent == undefined ? "" : node.parent.id), tags: (node.tags == undefined ? [] : node.tags), icons: (node.icons == undefined ? [] : node.icons), hyperLink: (node.hyperLink == undefined ? "" : node.hyperLink) }) .then(res => { console.log(res); console.log(res.data); }) } renderExistentMindmap(data) { let root = data[0] let nodeData = { id: root.id, topic: root.topic, root: true, style: { background: root.style.background, color: root.style.color, fontSize: root.style.fontSize, }, hyperLink: root.hyperLink, children: [] } this.createTree(nodeData, data) return { nodeData } } createTree(nodeData, data) { for(let i = 1; i < data.length; i++) { if(data[i].parent == nodeData.id) { let newNode = { id: data[i].id, topic: data[i].topic, root: false, style: { background: data[i].style.background, color: data[i].style.color, fontSize: data[i].style.fontSize, }, hyperLink: data[i].hyperLink, children: [] } nodeData.children.push(newNode) this.createTree(newNode, data) } } } } export default Mindmap;
文章
Lilian Huang · 七月 9, 2023

从 IRIS 嵌入式 Python 动态创建 HL7 消息

#Embedded Python #HL7 #InterSystems IRIS for Health 写在回复社区帖子《Python能否动态创建HL7消息》中。 前提条件和设置 使用一个启用了集成的命名空间。注意:USER命名空间默认不启用互操作性。如果以下建议创建一个新的互操作性命名空间来探索功能。 # 切换到ZN "[互操作性名称空间名称]" # 启动交互式Python shell:Do $SYSTEM.Python.Shell() 启动脚本 #Load dependencies import datetime as dt import uuid # Cache current time in CCYYMMDDHHMMss format hl7_datetime_now=dt.datetime.now().strftime('%Y%m%d%H%M%S') # Create HL7 Message hl7=iris.cls("EnsLib.HL7.Message")._New() # Set the doc type # 2.5.1:ORU_R01 - Unsolicited transmission of an observation message hl7.PokeDocType("2.5.1:ORU_R01") 这些信息的结构可以从管理门户中获取 创建MSH(消息头段)。 // MSH Segment hl7.SetValueAt('OutApp','MSH:SendingApplication') hl7.SetValueAt('OutFac','MSH:SendingFacility') hl7.SetValueAt('InApp','MSH:ReceivingApplication') hl7.SetValueAt('InFac','MSH:ReceivingFacility') hl7.SetValueAt(hl7_datetime_now,'MSH:DateTimeOfMessage') hl7.SetValueAt('ORU','MSH:MessageType.MessageCode') hl7.SetValueAt('R01','MSH:MessageType.TriggerEvent') hl7.SetValueAt('ORU_R01','MSH:MessageType.MessageStructure') hl7.SetValueAt(str(uuid.uuid4()),'MSH:MessageControlID') hl7.SetValueAt('2.5.1','MSH:ProcessingID') 编码和解码 HL7文件被格式化为段每个段被分隔符("|")和重复元素("~")划分为多个元素在一个元素内有"^"分界符和"&"子分界符。当定界符作为实际的文本内容出现时,它将被"\"和其他取代定界符的字符转义。通常,"&"是有问题的,因为它可能经常出现在信息中,导致接收系统读取时出现截断现象。HL7段有一个内置的方法,用于根据当前为信息选择的定界符来转义内容。一个常见的模式是获得对第一个段的引用 # Do this line the variable "msh" is used later > msh=hl7.GetSegmentAt(1) 然后可以调用Escape,例如用Python的原始字符串: > msh.Escape(r"a&b~c^d") 'a\\T\\b\\R\\c\\S\\d' The segment can also be used to Unescape back for example: > msh.Unescape('a\\T\\b\\R\\c\\S\\d') 'a&b~c^d' 因此,在设置预计包含分隔符的内容时,可以为信息转义这些分隔符 hl7.SetValueAt(msh.Escape(r"a&b~c^d"),'MSH:ReceivingFacility') 检索内容时可以不加转义 msh.Unescape(hl7.GetValueAt('MSH:ReceivingFacility')) 在这个例子中,只是将msh重新设置为以前的值 hl7.SetValueAt('InFac','MSH:ReceivingFacility') 仔细检查到目前为止的部分: > hl7.GetValueAt('MSH') 'MSH|^~\\&|OutApp|OutFac|InApp|InFac|20230610100040||ORU^R01^ORU_R01|2dfab415-51aa-4c75-a7e7-a63aedfb53cc|2.5.1' 人口统计学(PID)部分 # Virtual path prefix for PID seg='PIDgrpgrp(1).PIDgrp.PID:' hl7.SetValueAt('1',seg+'SetIDPID') hl7.SetValueAt('12345',seg+'PatientIdentifierList(1).IDNumber') hl7.SetValueAt('MRN',seg+'PatientIdentifierList(1).AssigningAuthority') hl7.SetValueAt('MR',seg+'PatientIdentifierList(1).IdentifierTypeCode') hl7.SetValueAt(msh.Escape('Redfield'), seg+'PatientName(1).FamilyName') hl7.SetValueAt(msh.Escape('Claire') ,seg+'PatientName(1).GivenName') hl7.SetValueAt('19640101',seg+'DateTimeofBirth') hl7.SetValueAt('F',seg+'AdministrativeSex') hl7.SetValueAt(msh.Escape('Umbrella Corporation') ,seg+'PatientAddress.StreetAddress') hl7.SetValueAt(msh.Escape('Umbrella Drive') ,seg+'PatientAddress.OtherDesignation') hl7.SetValueAt(msh.Escape('Raccoon City') ,seg+'PatientAddress.City') hl7.SetValueAt(msh.Escape('MO') ,seg+'PatientAddress.StateorProvince') hl7.SetValueAt(msh.Escape('63117') ,seg+'PatientAddress.ZiporPostalCode') 仔细检查PID段的内容 > hl7.GetValueAt(seg[0:-1]) 'PID|1||12345^^^MRN^MR||Redfield^Claire||19640101|F|||Umbrella Corporation^Umbrella Drive^Raccoon City^MO^63117' 订单控制部分 seg='PIDgrpgrp(1).ORCgrp(1).ORC:' hl7.SetValueAt('RE',seg+'OrderControl') hl7.SetValueAt('10003681',seg+'PlacerOrderNumber') hl7.SetValueAt('99001725',seg+'FillerOrderNumber') hl7.SetValueAt('AG104',seg+'OrderingProvider') hl7.SetValueAt('L43',seg+'EnterersLocation') 仔细检查ORC部分的内容 > hl7.GetValueAt(seg[0:-1]) 'ORC|RE|10003681|99001725|||||||||AG104|L43' 观察请求 seg='PIDgrpgrp(1).ORCgrp(1).OBR:' hl7.SetValueAt('1',seg+'SetIDOBR') hl7.SetValueAt('10003681',seg+'PlacerOrderNumber') hl7.SetValueAt('99001725',seg+'FillerOrderNumber') hl7.SetValueAt('20210428100729',seg+'ResultsRptStatusChngDateTime') hl7.SetValueAt('F',seg+'ResultStatus') hl7.SetValueAt('U',seg+'QuantityTiming.Priority') OBX 观察/结果 seg='PIDgrpgrp(1).ORCgrp(1).OBXgrp(1).OBX:' hl7.SetValueAt('1',seg+'SetIDOBX') hl7.SetValueAt('TX',seg+'ValueType') hl7.SetValueAt('V8132',seg+'ObservationIdentifier.Identifier') hl7.SetValueAt(msh.Escape('G-Virus') , seg+'ObservationIdentifier.Identifier') hl7.SetValueAt(msh.Escape('17.8 log10') ,seg+'ObservationValue') hl7.SetValueAt(msh.Escape('RNA copies/mL') ,seg+'Units') hl7.SetValueAt('F',seg+'ObservationResultStatus') hl7.SetValueAt('20210428100729',seg+'DateTimeoftheObservation') hl7.SetValueAt('AG001',seg+'ResponsibleObserver.IDNumber') hl7.SetValueAt('Birkin',seg+'ResponsibleObserver.FamilyName') hl7.SetValueAt('William',seg+'ResponsibleObserver.GivenName') hl7.SetValueAt('AG001',seg+'ResponsibleObserver.IDNumber') hl7.SetValueAt('UXL43',seg+'EquipmentInstanceIdentifier') NTE - 注释和评论 seg='PIDgrpgrp(1).ORCgrp(1).OBXgrp(1).NTE(1):' hl7.SetValueAt('1',seg+'SetIDNTE') hl7.SetValueAt(msh.Escape('Expected late onset Hyphema. Contain but do not approach.') ,seg+'Comment') 向终端打印全部信息 > print(hl7.OutputToString()) MSH|^~\&|OutApp|OutFac|InApp|InFac|20230610141201||ORU^R01^ORU_R01|2dfab415-51aa-4c75-a7e7-a63aedfb53cc|2.5.1 PID|1||12345^^^MRN^MR||Redfield^Claire||19640101|F|||Umbrella Corporation^Umbrella Drive^Raccoon City^MO^63117 ORC|RE|10003681|99001725|||||||||AG104|L43 OBR|1|10003681|99001725|||||||||||||||||||20210428100729|||F||^^^^^U OBX|1|TX|G-Virus||17.8 log10|RNA copies/mL|||||F|||20210428100729||AG001^Birkin^William||UXL43 NTE|1||Expected late onset Hyphema. Contain but do not approach. 陷阱 如果一个元素的内容包含一个诸如 "8@%SYS.Python "的值,很可能是需要用字符串值或字符串属性来代替。 例如,uuid在MSH结构中被 "str "包裹着。 原文请查看 来自 Alex Woodhead https://community.intersystems.com/post/dynamically-creating-hl7-message-iris-embedded-python
文章
姚 鑫 · 四月 25, 2021

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

# 第五章 优化查询性能(四) # 注释选项 可以在`SELECT`、`INSERT`、`UPDATE`、`DELETE`或`TRUNCATE`表命令中为查询优化器指定一个或多个注释选项。 注释选项指定查询优化器在编译SQL查询期间使用的选项。 通常,注释选项用于覆盖特定查询的系统范围默认配置。 ## 语法 语法`/*#OPTIONS */`(在`/*`和`#`之间没有空格)指定了一个注释选项。 注释选项不是注释; 它为查询优化器指定一个值。 注释选项使用`JSON`语法指定,通常是`“key:value”`对,例如: `/*#OPTIONS {"optionName":value} */`。 支持更复杂的JSON语法,比如嵌套值。 注释选项不是注释; 除了`JSON`语法之外,它可能不包含任何文本。 包含非`json`文本在`/* ... */`分隔符导致`SQLCODE -153`错误。 InterSystems SQL不验证`JSON`字符串的内容。 `#OPTIONS`关键字必须用大写字母指定。 `JSON`的大括号语法中不应该使用空格。 如果SQL代码用引号括起来,比如动态SQL语句,JSON语法中的引号应该是双引号。 例如:`myquery="SELECT Name FROM Sample.MyTest /*#OPTIONS {""optName"":""optValue""} */"`. 可以在SQL代码中任何可以指定注释的地方指定`/*#OPTIONS */` comment选项。 在显示的语句文本中,注释选项总是作为注释显示在语句文本的末尾。 你可以在SQL代码中指定多个`/*#OPTIONS */` comment选项。 它们按照指定的顺序显示在返回的语句文本中。 如果为同一个选项指定了多个注释选项,则使用`last`指定的选项值。 以下的注释选项被记录在案: - `/*#OPTIONS {"BiasAsOutlier":1} */` - `/*#OPTIONS {"DynamicSQLTypeList":"10,1,11"}` - `/*#OPTIONS {"NoTempFile":1} */` ## 显示 `/*#OPTIONS */` comment选项显示在SQL语句文本的末尾,而不管它们是在SQL命令中指定的位置。 一些显示的`/*#OPTIONS */` comment选项没有在SQL命令中指定,而是由编译器的预处理器生成的。 例如 `/*#OPTIONS {"DynamicSQLTypeList": ...} */` `/*#OPTIONS */` comment选项显示在`Show Plan`语句文本、缓存的查询查询文本和SQL语句语句文本中。 为仅在`/*#OPTIONS */` comment选项中不同的查询创建一个单独的缓存查询。 # 并行查询处理 并行查询提示指示系统在多处理器系统上运行时执行并行查询处理。 这可以极大地提高某些类型查询的性能。 SQL优化器确定一个特定的查询是否可以从并行处理中受益,并在适当的时候执行并行处理。 指定并行查询提示并不强制对每个查询进行并行处理,只强制那些可能从并行处理中受益的查询。 如果系统不是多处理器系统,则此选项无效。 要确定当前系统上的处理器数量,请使用 `%SYSTEM.Util.NumberOfCPUs() `方法。 可以通过两种方式指定并行查询处理: - 在系统范围内,通过设置`auto parallel`选项。 - 在每个查询的`FROM`子句中指定`%PARALLEL`关键字。 并行查询处理应用于`SELECT`查询。 它不应用于插入、更新或删除操作。 ## 系统范围的并行查询处理 可以使用以下选项之一来配置系统范围的自动并行查询处理: - 在管理门户中选择System Administration,然后选择Configuration,然后选择SQL和对象设置,最后选择SQL。 查看或更改在单个进程中执行查询复选框。 注意,该复选框的默认值是未选中的,这意味着并行处理在默认情况下是激活的。 - 调用`$SYSTEM.SQL.Util.SetOption()`方法,如下: `SET status=$SYSTEM.SQL.Util.SetOption("AutoParallel",1,.oldval)`. 默认值是1(自动并行处理激活)。 要确定当前的设置,调用`$SYSTEM.SQL.CurrentSettings()`,它会显示为`%PARALLEL`选项启用自动提示。 注意,更改此配置设置将清除所有名称空间中的所有缓存查询。 当激活时,自动并行查询提示指示SQL优化器对任何可能受益于这种处理的查询应用并行处理。 在IRIS 2019.1及其后续版本中,自动并行处理是默认激活的。 从IRIS 2018.1升级到IRIS 2019.1的用户需要明确激活自动并行处理。 SQL优化器用于决定是否对查询执行并行处理的一个选项是自动并行阈值。 如果激活了系统范围的自动并行处理(默认),可以使用`$SYSTEM.SQL.Util.SetOption()`方法将自动并行处理的优化阈值设置为整数值,如下所示: `SET status=$SYSTEM.SQL.Util.SetOption("AutoParallelThreshold",n,.oldval)`。 `n`阈值越高,将此特性应用于查询的可能性就越低。 此阈值用于复杂的优化计算,但可以将此值视为必须驻留在已访问映射中的元组的最小数量。 默认值为3200。 最小值为0。 要确定当前的设置,调用`$SYSTEM.SQL.CurrentSettings()`,它显示`%PARALLEL`选项的自动提示阈值。 当自动并行处理被激活时,在分片环境中执行的查询将始终使用并行处理执行,而不管并行阈值是多少。 ## 针对特定查询的并行查询处理 可选的`%PARALLEL`关键字在查询的`FROM`子句中指定。 它建议跨系统的IRIS使用多个处理器(如果适用的话)并行处理查询。 这可以显著提高使用一个或多个`COUNT`、`SUM`、`AVG`、`MAX`或`MIN`聚合函数和`/`或`groupby`子句的查询的性能,以及许多其他类型的查询。 这些通常是处理大量数据并返回小结果集的查询。 例如,`SELECT AVG(SaleAmt) FROM %PARALLEL User.AllSales GROUP BY Region`都可使用并行处理。 **仅指定聚合函数、表达式和子查询的“一行”查询执行并行处理,无论是否带有`GROUP BY`子句。 但是,同时指定单个字段和一个或多个聚合函数的“多行”查询不会执行并行处理,除非它包含`GROUP BY`子句。 例如,`SELECT Name,AVG(Age) FROM %PARALLEL Sample.Person`不执行并行处理,但是 `SELECT Name,AVG(Age) FROM %PARALLEL Sample.Person GROUP BY Home_State` 执行并行处理。** 如果在运行时模式下编译指定`%PARALLEL`的查询,则所有常量都被解释为ODBC格式。 指定`%PARALLEL`可能会降低某些查询的性能。 在一个有多个并发用户的系统上运行`%PARALLEL`查询可能会降低整体性能。 在查询视图时可以执行并行处理。 但是,即使显式地指定了`%parallel`关键字,也不会对指定`%VID`的查询执行并行处理。 ### `%PARALLEL`的子查询 `%PARALLEL`用于`SELECT`查询及其子查询。 插入命令子查询不能使用`%PARALLEL`。 当应用于与外围查询相关的子查询时,`%PARALLEL`将被忽略。 例如: ```sql SELECT name,age FROM Sample.Person AS p WHERE 30 e.dob.` 这是因为SQL优化将这种类型的连接转换为完整的外部连接。 对于完整的外部连接,`%PARALLEL`将被忽略。 - `%PARALLEL`和`%INORDER`优化不能同时使用; 如果两者都指定,`%PARALLEL`将被忽略。 - 查询引用一个视图并返回一个视图ID (`%VID`)。 - 如果表有`BITMAPEXTENT`索引,`COUNT(*)`不使用并行处理。 - `%PARALLEL`用于使用标准数据存储定义的表。 可能不支持将其与自定义存储格式一起使用。 `%PARALLEL`不支持全局临时表或具有扩展全局引用存储的表。 - `%PARALLEL`用于可以访问一个表的所有行的查询,使用行级安全(`ROWLEVELSECURITY`)定义的表不能执行并行处理。 - `%PARALLEL`用于存储在本地数据库中的数据。 它不支持映射到远程数据库的全局节点。 ## 共享内存的考虑 对于并行处理,IRIS支持多个进程间队列(`IPQ`)。 每个`IPQ`处理单个并行查询。 它允许并行工作单元子流程将数据行发送回主流程,这样主流程就不必等待工作单元完成。 这使得并行查询能够尽可能快地返回第一行数据,而不必等待整个查询完成。 它还改进了聚合函数的性能。 并行查询执行使用来自通用内存堆(`gmheap`)的共享内存。 如果使用并行SQL查询执行,用户可能需要增加`gmheap`大小。 一般来说,每个`IPQ`的内存需求是`4 x 64k = 256k`。 InterSystems IRIS将一个并行SQL查询拆分为可用的`CPU`核数。 因此,用户需要分配这么多额外的`gmheap`: ```java x x 256 = ``` 注意,这个公式不是100%准确的,因为一个并行查询可以产生同样是并行的子查询。 因此,明智的做法是分配比这个公式指定的更多的额外`gmheap`。 分配足够的`gmheap`失败将导致错误报告给`messages.log`。 SQL查询可能会失败。 其他子系统尝试分配`gmheap`时也可能出现其他错误。 要查看一个实例的`gmheap`使用情况,特别是`IPQ`使用情况,请在管理门户的主页上选择System Operation,然后选择System usage,然后单击Shared Memory Heap usage链接; ![image](/sites/default/files/inline/images/2_23.png) 要更改通用内存堆或`gmheap`(有时称为共享内存堆或SMH)的大小,请从管理门户的主页选择“系统管理”,然后是“配置”,然后是“附加设置”,最后是“高级内存”; ![image](/sites/default/files/inline/images/3_19.png) ![image](/sites/default/files/inline/images/4_14.png) ## 缓存查询注意事项 如果你正在运行一个缓存的SQL查询,使用`%PARALLEL`,当这个查询被初始化时,你做了一些事情来清除缓存的查询,那么这个查询可能会从一个工人作业报告一个``错误。 导致缓存查询被清除的典型情况是调用`$SYSTEM.SQL.Purge()`或重新编译该查询引用的类。 重新编译类将自动清除与该类相关的任何缓存查询。 如果发生此错误,再次运行查询可能会成功执行。 从查询中删除`%PARALLEL`可以避免出现此错误。 ## SQL语句和计划状态 使用`%PARALLEL`的SQL查询可以产生多条SQL语句。 这些SQL语句的计划状态是`Unfrozen/Parallel`。 计划状态为“已冻结”/“并行”的查询不能通过用户操作进行冻结。 # 生成报告 可以使用生成报告工具向InterSystems Worldwide Response Center (WRC) customer support提交查询性能报告,以便进行分析。 可以使用以下任意一种方式从管理门户运行生成报告工具: 1. 必须首先从WRC获得WRC跟踪号。可以使用每个管理门户页面顶部的Contact按钮从管理门户联系WRC。在WRC编号区域中输入此跟踪编号。可以使用此跟踪编号来报告单个查询或多个查询的性能。 2. 在“SQL语句”区域中,输入查询文本。右上角将显示一个X图标。可以使用此图标清除SQL语句区。查询完成后,选择保存查询按钮。系统生成查询计划并收集指定查询的运行时统计信息。无论系统范围的运行时统计信息设置如何,生成报告工具始终使用收集选项3:记录查询的所有模块级别的统计信息进行收集。由于在此级别收集统计信息可能需要时间,因此强烈建议您选中“在后台运行保存查询进程”复选框。默认情况下,此复选框处于选中状态。 当后台任务启动时,该工具显示“请等待……”,禁用页面上的所有字段,并显示一个新的视图进程按钮。 单击View Process按钮将在新选项卡中打开Process Details页面。 在流程详细信息页面,您可以查看该流程,并可以“暂停”、“恢复”或“终止”该流程。 进程的状态反映在Save查询页面上。 当流程完成时,当前保存的查询表将被刷新,View process按钮将消失,页面上的所有字段将被启用。 3. 对每个查询执行步骤2。 每个查询将被添加到当前保存的Queries表中。 注意,该表可以包含具有相同WRC跟踪号的查询,也可以包含具有不同跟踪号的查询。 完成所有查询后,继续步骤4。 对于列出的每个查询,可以选择Details链接。 该链接将打开一个单独的页面,其中显示完整的SQL语句、属性(包括WRC跟踪号和IRIS软件版本),以及包含每个模块的性能统计信息的查询计划。 - 要删除单个查询,请从“当前保存的查询”表中选中这些查询的复选框,然后单击“清除”按钮。 - 要删除与WRC跟踪编号关联的所有查询,请从当前保存的查询表中选择一行。WRC编号显示在页面顶部的WRC编号区域。如果您随后单击清除按钮,则对该WRC编号的所有查询都将被删除。 4. 使用查询复选框选择要报告给WRC的查询。要选择与WRC跟踪编号关联的所有查询,请从当前保存的查询表中选择一行,而不是使用复选框。在这两种情况下,都可以选择Generate Report按钮。生成报告工具创建一个XML文件,其中包括查询语句、具有运行时统计信息的查询计划、类定义以及与每个所选查询相关联的SQL int文件。 如果选择与单个WRC跟踪编号关联的查询,则生成的文件将具有默认名称,如`WRC12345.xml`。如果选择与多个WRC跟踪编号关联的查询,则生成的文件将具有默认名称`WRCMultiple.xml`。 将出现一个对话框,要求指定保存报告的位置。保存报告后,可以单击Mail to链接将报告发送给WRC客户支持。使用邮件客户端的附加/插入功能附加文件。
文章
Hao Ma · 一月 10, 2021

完整性检查_ 加速还是减速

虽然 Caché 和 InterSystems IRIS 数据库的[完整性](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCDI_integrity)完全不会受到系统故障的影响,但物理存储设备故障确实会损坏其存储的数据。 因此,许多站点选择运行定期数据库完整性检查,尤其要与备份配合,以验证在发生灾难时是否可以依赖给定的备份。 系统管理员在应对涉及存储损坏的灾难时,也可能强烈需要完整性检查。 完整性检查必须读取所检查的 global 的每个块(如果尚未在缓冲区中),并且按照 global 结构指示的顺序读取。 这会花费大量时间,**但完整性检查能够以存储子系统可以承受的最快速度进行读取**。 在某些情况下,需要以这种方式运行以尽快获得结果。 在其他情况下,完整性检查需要更加保守,以避免消耗过多的存储子系统带宽。 ## 行动计划 以下概述适合大多数情况。 本文其余部分中的详细讨论提供了采取其中任一行动或得出其他行动方案所需的信息。 1. 如果使用 Linux 并且完整性检查很慢,请参阅下面有关启用异步 I/O 的信息。 2. 如果完整性检查必须尽快完成,则在隔离的环境中运行;或者如果迫切需要结果,则使用多进程完整性检查来并行检查多个 global 或数据库。 进程数乘以每个进程将执行的并发异步读取数(默认为 8,如果使用 Linux 并且禁用异步 I/O 则为 1)是实时并发读取数的限制。 假定平均数是限制数量的一半,然后与存储子系统的能力进行比较。 例如,存储由 20 个驱动器条带化,每个进程的默认并发读取数为 8,则可能需要 5 个或更多进程才能利用存储子系统的全部能力 (5*8/2=20)。 3. 在平衡完整性检查速度与对生产的影响时,首先调整多进程完整性检查的进程数,然后如果需要的话,查看可调参数 SetAsyncReadBuffers。 对于长期解决方案(以及为消除误报),请参见下面的隔离完整性检查。 4. 如果已经被限制为一个进程(例如有一个极大的 global 或存在其他外部约束),并且完整性检查的速度需要上下调整,则查看下面的可调参数 SetAsyncReadBuffers。 ## 多进程完整性检查 让完整性检查更快完成(以更高的速度使用系统资源)的一般解决方案是将工作分给多个并行进程。 一些完整性检查用户界面和 API 会这样做,而其他一些则使用单个进程。 对进程的分配按 global 进行,因此对单个 global 的检查始终由一个进程执行(Caché 2018.1 之前的版本按数据库而不是按 global 分配工作)。 多进程完整性检查的主要 API 是 **CheckLIst^Integrity**(有关详细信息,请参阅[文档](https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=GCDI_integrity#GCDI_integrity_verify_utility))。 它将结果收集在一个临时的 global 中,通过 Display^Integrity 来显示。 以下是使用 5 个进程检查 3 个数据库的示例。 这里如省略数据库列表参数,将检查所有数据库。 set dblist=$listbuild(“/data/db1/”,”/data/db2/”,”/data/db3/”) set sc=$$CheckList^Integrity(,dblist,,,5) do Display^Integrity() kill ^IRIS.TempIntegrityOutput(+$job) /* Note: evaluating ‘sc’ above isn’t needed just to display the results, but...    $system.Status.IsOK(sc) - ran successfully and found no errors    $system.Status.GetErrorCodes(sc)=$$$ERRORCODE($$$IntegrityCheckErrors) // 267                            - ran successfully, but found errors.    Else - a problem may have prevented some portion from running, ‘sc’ may have            multiple error codes, one of which may be $$$IntegrityCheckErrors. */ 像这样使用 CheckLIst^Integrity 是实现我们感兴趣的控制水平的最直接方法。 管理门户接口和完整性检查任务(内置但未安排)使用多个进程,但可能无法为我们的用途提供足够的控制。* 其他完整性检查接口,尤其是终端用户接口 ^INTEGRIT 或 ^Integrity 以及 Silent^Integrity,在单个进程中执行完整性检查。 因此,这些接口不能以最快的速度完成检查,并且它们使用的资源也较少。 但一个优点是,它们的结果是可见的,可以记录到文件或输出到终端,因为每个 global 都会被检查,而且顺序明确。 ## 异步 I/O 完整性检查进程会排查 global 的每个指针块,一次检查一个,根据它指向的数据块的内容来进行验证。 数据块以异步 I/O 的方式读取,以确保每时每刻都有一定数量的读取请求供存储子系统处理,并且每次读取完成后都进行验证。 在 Linux 上,异步 I/O 只有与直接 I/O 结合时才有效,而 InterSystems IRIS 2020.3 之前的版本默认不启用直接 I/O。 这解释了大量 Linux 上完整性检查时间过长的情况。 幸运的是,可以在 Cache 2018.1、IRIS 2019.1 及以后的版本上启用直接 I/O,方法是在 .cpf 文件的 [config] 部分中设置 **wduseasyncio=1**,然后重新启动。 通常建议设置此参数,以在繁忙系统上实现 I/O 可伸缩性,并且自 Caché 2015.2 起,在非 Linux 平台上默认设置此参数。 在启用之前,确保已经为数据库缓存(global 缓冲区)配置了足够的内存,因为启用直接 I/O 后,数据库将不再被 Linux(冗余)缓存。 未启用时,完整性检查执行的读取会同步完成,不能有效利用存储。 在所有平台上,完整性检查进程一次执行的读取数默认设置为 8。 如果必须更改单个完整性检查进程从磁盘读取的速率,可以调整此参数 – 向上调会使单个进程更快完成,向下调则使用更少的存储带宽。 请记住: * 此参数应用于每个完整性检查进程。 当使用多个进程时,进程数会使实时读取数增加。更改并行完整性检查进程数会产生较大影响,因此这通常是先做的事情。 每个进程还受到计算时间的限制(除其他限制外),因此增加此参数的值所获得的收益也有限。 * 这只在存储子系统处理并发读取的能力范围内有效。 如果数据库存储在单个本地驱动器上,再高的数值也没有用处,而在几十个驱动器上条带化的存储阵列可以并发处理几十个读取。 要从 %SYS 命名空间调整此参数,则 **do SetAsyncReadBuffers^Integrity(**value**)**。 要查看当前值,则 **write $$GetAsyncReadBuffers^Integrity()**。 更改在检查下一个 global 时生效。 目前,该设置在系统重启后不会保持,虽然可以将其添加到 SYSTEM^%ZSTART 中。 有一个相似的参数用于在磁盘上的块是连续(或接近连续)分布时控制每次读取的最大大小。 此参数很少需要用到,尽管具有高存储延迟的系统或具有较大块大小的数据库可能会从微调中受益。 该值的单位为 64KB,因此值 1 表示 64KB,4 表示 256KB 等等。0(默认值)表示让系统选择,当前选择 1 (64KB)。 此参数的 ^Integrity 函数(类似于上面提及的函数)为 **SetAsyncReadBufferSize** 和 **GetAsyncReadBufferSize**。 ## 隔离完整性检查 许多站点直接在生产系统上运行定期完整性检查。 这当然是最简单的配置,但并不理想。 除了完整性检查对存储带宽的影响,并发数据库更新活动有时还可能导致误报错误(尽管检查算法内置了缓解措施)。 因此,在生产系统上运行的完整性检查所报告的错误,需要由管理员进行评估和/或重新检查。 很多时候,存在更好的选择。 可以将存储快照或备份映像挂载到另一台主机上,在那里由隔离的 Caché 或 IRIS 实例运行完整性检查。 这样不仅可以防止任何误报,而且如果存储也与生产隔离,运行完整性检查可以充分利用存储带宽并更快完成。 这种方法非常适合使用完整性检查来验证备份的模型;经过验证的备份可以有效验证截至生成备份前的生产情况。 还可以通过云和虚拟化平台更容易地从快照建立可用的隔离环境。   * * * * 管理门户接口、完整性检查任务和 SYS.Database 的 IntegrityCheck 方法会选择相当多的进程(等于 CPU 内核数),在很多情况下缺少所需的控制。 管理门户和任务还会对任何报告错误的 global 执行完整的重新检查,以识别可能因并发更新而出现的误报。 除了完整性检查算法内置的误报缓解措施,也可能进行这种重新检查;在某些情况下,由于会花费额外的时间(重新检查在单个进程中运行,并检查整个 global),可能并不需要重新检查。 此行为将来可能会更改。
文章
Nicky Zhu · 五月 20, 2021

互操作消息统一管理系列:MessageBank

## 一. 企业信息库简介 企业信息库(MessageBank)是一个可选的远程归档设施,可以从多个来自不同实例的互操作性Production中收集信息、事件日志项目和搜索表项。如下图所示: ![image](/sites/default/files/inline/images/1_1_0.png) 这套环境由两种角色的实例构成: 企业信息库服务器,它本身也是一个Production,完全由Message Bank服务组成,接收来自任何数量的客户Production提交的消息、日志等。 客户端Operation(Message Bank Operation),将其添加到一个正在运行的Production中,并用企业信息库服务器的地址进行配置。如连接通畅,消息和日志即可自动转发到Message Bank并在其中存储。 为了使你能方便地看到信息库中的信息,InterSystems IRIS®提供了以下附加选项。 对于企业信息库实例,管理门户自动包括企业监控器页面,在那里你可以监控客户端Production的状态,浏览消息库,并对被监控客户的消息进行检索。 对于每个客户端实例,你在消息库实例中配置一个到企业监控器的链接。 如下所示: ![image](/sites/default/files/inline/images/1_2_0.png) ## 二. 常见应用场景 ### 消息归档 在使用IRIS互操作性时,对于生产环境,为保障其有充足的磁盘空间和即时查询的效率,通常会采用消息和日志过期策略。在生产环境中只保留近期(如一个月)的信息以备回溯,过期数据将定期被清除。因此,如果有长期保留消息(如在生产环境清除周期之外还需要更长时间的回溯)的需求,则可以通过Message Bank对消息和日志进行长期保存。 ### 企业消息仓库 对于集成规模较大,集成业务较多的大型企业和集团(如大型医院、医联体、医共体),往往会采用多套互操作性实例支撑数据交换和集成业务。在这种环境下,可以通过Message Bank汇聚和存储整个企业环境下的所有互操作消息和日志,为业务集中监控、跨实例业务故障分析等工作创造条件。 ### 消息和日志再利用 理想条件下,实施互操作性项目之后,消息和日志中就会包括大量的业务数据,典型的包括下达的医嘱、患者信息、医疗记录等。通过对Message Bank中的数据进行分析和挖掘,能够获得有价值的业务信息。 接下来我们会为大家介绍Message Bank的搭建过程。 ## 三. 搭建Message Bank ### 创建Message Bank 命名空间 在生产所用的实例之外,我们需要使用一台独立的实例用于安装和配置Message Bank(实例安装过程和License激活过程从略,请查看安装文档或联系您的支持工程师)。 在该实例上,创建一个命名空间安装Message Bank,如下所示: ![image](/sites/default/files/inline/images/1_3_0.png) 由于Message Bank本质上由Production实现,因此创建命名空间时要选上对互操作Producation的支持。 InterSystems为大家提供了可以套用的Production模版。因此,请按照以下步骤创建Production: 在刚才创建的命名空间MessageStore下创建类MessageBank.BankProduction,继承Ens.Enterprise.MsgBank.Production并将Ens.Enterprise.MsgBank.Production中的XData代码块拷贝到新建的类中,如下所示: ![image](/sites/default/files/inline/images/1_4_0.png) 保存和编译该类,并在Interoperability菜单中加载该Production。 ![image](/sites/default/files/inline/images/1_5_0.png) 其中已部署了两个服务: MsgBankService:该服务通过TCP连接从其他Production接收消息 注意该Service默认使用9192端口与其他客户端通信。 ![image](/sites/default/files/inline/images/1_6_0.png) MonitorService:该服务收集其他实例的其他Production的运行状态 此时,这个Production已经具备了从其他实例的Production收集消息和事件信息的能力,可直接启动。当然,我们还需要配置与客户端的连接。 ### 为客户端Production添加消息转发Operation 假设我们已经有一个可运行的的如下所示的Production ![image](/sites/default/files/inline/images/1_7_0.png) 注意这个Production与Message Bank不在同一个实例上。 ![image](/sites/default/files/inline/images/1_8_0.png) 这个Production接收XML格式的报文并根据报文类型转发到不同的BO。 要将这个Production加入Message Bank,则需要对该客户端Production添加Business Operation Ens.Enterprise.MsgBankOperation。 ![image](/sites/default/files/inline/images/1_9_0.png) 对于该Operation,需要指定要连接的Message Bank的IP地址和端口。 ![image](/sites/default/files/inline/images/1_10_0.png) 同时,建议开启这个Operation的“启用存档”开关,保证在Message Bank临时故障时挂起消息,在故障恢复后还能捕捉到故障期间的消息和日志。 配置完成后启用该Operation。 ### 在Message Bank中加入客户端信息 上述连接建立后,客户端和Message Bank间的连接已建立,还需要配置Message Bank和客户端Production之间的程序信息(相当于注册)才能正常工作。 #### 添加客户端连接凭据 ![image](/sites/default/files/inline/images/1_11_0.png) Message Bank需要通过Web请求访问客户端信息,因此,需要配置客户端凭据,即可通过管理门户访问客户端Production的用户名和密码(对访问权限的设计和配置,可参见我们之前的文章:IRIS中的权限管理) #### 在Message Bank上配置客户端信息 在Message Bank中的Interoperability菜单中找到“企业系统”项 ![image](/sites/default/files/inline/images/1_12_0.png) 在操作页面上通过“新建连接” ![image](/sites/default/files/inline/images/1_13_0.png) 新建连接添加客户端信息。 ![image](/sites/default/files/inline/images/1_14_0.png) 注意其中的服务Web应用路径为该客户端实例上Production所在的命名空间的Web Application根路径,并引用之前填写的凭据。 如配置正确,可通过企业监视器查看连接状态 ![image](/sites/default/files/inline/images/1_15_0.png) 连接成功的状态如下: ![image](/sites/default/files/inline/images/1_16_0.png) #### 在客户端上添加Message Bank连接信息(可选步骤) 如果需要在客户端上通过链接查看消息仓库的信息,则可以配置链接。 在客户端上,在被采集的Production所在的命名空间的Interoerability菜单中“消息仓库链接”配置 ![image](/sites/default/files/inline/images/1_17_0.png) 输入Message Bank所在的IP、端口和Production所在的命名空间,保存并“开始”即可跳转到Messsage Bank的企业监视器。 ![image](/sites/default/files/inline/images/1_18_0.png) 需要注意的是,该配置固定采用了/csp/[namespace]为Message Bank的Web Application路径,而在Message Bank实例上,这个Web Application默认的路径是/csp/healthshare/messagestorage。可通过在Message Bank上添加一个Web Application,拷贝/csp/healthshare/messagestorage的配置。 ![image](/sites/default/files/inline/images/1_19_0.png) ## 四. Message Bank的实施效果 ### 测试消息 在客户端的Production中触发任意流程产生消息,如下所示: ![image](/sites/default/files/inline/images/1_20_0.png) 此时通过Message Bank中的“消息仓库查看器”即可查询存储在消息仓库中的消息 ![image](/sites/default/files/inline/images/1_21_0.png) 如下: ![image](/sites/default/files/inline/images/1_22_0.png) 可以注意到该消息已被同步到消息仓库。 需要注意,使用“消息仓库查看器”时,查询的是在Message Bank中存储的消息数据,使用在Message Bank上定义的Search Table或索引进行查询;如果通过“企业消息查看器”查询,则是链接到客户端的消息查看器查询,应用的是在客户端上定义的索引。 ### 消息的存储 根据在源系统的消息类型的不同,传递到Message Bank后会以不同的形式保存消息。 #### 虚拟文档 对于HL7 V2等标准消息或基于XML虚拟文档的消息,在Message Bank这一侧也同样以虚拟文档的形式保存。 ![image](/sites/default/files/inline/images/1_23_0.png) 特别注意其中的如下属性: MessageBodyClassName:该类型为消息在Message Bank侧持久化的类型。 ClientBodyClassName:该类型为消息在客户端侧持久化的类型。 在本例中可以看到,客户端通过EnsLib.EDI.XML.Document类型传递的消息,在Message Bank中也是通过EnsLib.EDI.XML.Document保存。 MessageBodyId:消息在Message Bank中的物理主键 ClientBodyId:客户端侧持久化消息的物理主键 ClientSessionId:客户端会话Id #### 结构化消息 对于基于Ens.Request等持久化类型的消息,在Message Bank这一侧则默认使用字符流来保存。 例如,对于如下的客户端结构化消息传输 ![image](/sites/default/files/inline/images/1_24_0.png) 在Message Bank中的保存形式为: ![image](/sites/default/files/inline/images/1_25_0.png) 可见: MessageBodyClassName:消息在Message Bank中以%Stream.GlobalCharacter即字符流进行保存 因此,无论是保存为EnsLib.EDI.XML.Document或是%Stream.GlobalCharacter,在Message Bank中保存的消息本身都缺乏足够的结构化特征和索引以支持对消息体的检索,我们会在下一篇教程《[互操作消息统一管理系列:SearchTable加速检索](https://cn.community.intersystems.com/post/%E4%BA%92%E6%93%8D%E4%BD%9C%E6%B6%88%E6%81%AF%E7%BB%9F%E4%B8%80%E7%AE%A1%E7%90%86%E7%B3%BB%E5%88%97%EF%BC%9Asearchtable%E5%8A%A0%E9%80%9F%E6%A3%80%E7%B4%A2)》中介绍如何通过构建Search Table来检索这些消息。 对于Message Bank相关的内容,可参见: https://docs.intersystems.com/healthconnect20201/csp/docbook/DocBook.UI.Page.cls?KEY=EGDV_message_bank 也欢迎与我们联系获得更详细的信息。
文章
Weiwei Gu · 六月 27, 2022

Globals 是管理数据的魔剑 : 第一部分

Globals,这些存储数据的魔剑,已经存在了一段时间,但是没有多少人能够有效地使用它们,也没有多少人知道这个超级武器。 如果你把Globals的东西用在它们真正能发挥作用的地方,其结果可能是惊人的,要么是性能的提高,要么是整体解决方案的大幅简化 (1, 2). Globals提供了一种特殊的存储和处理数据的方式,它与SQL表完全不同。它们在1966年首次出现在 M(UMPS)编程语言中, 该语言最初用于医学数据库。现在它仍然以同样的方式被使用,但也被其他一些以可靠性和高性能为首要任务的行业所采用:金融、交易等。 后来M(UMPS)演变为 Caché ObjectScript (COS). COS是由InterSystems公司开发的,作为M的一个超集. 其原始语言仍然被开发者社区所接受,并在一些实现中保持活力。在网络上有几个活跃的网址,比如:MUMPS Google group, Mumps User's group), effective ISO Standard等等 现代基于Globals的数据库支持交易、日志、复制、分区等。这意味着它们可以被用来构建现代的、可靠的、快速的分布式系统。 Gloabls并不将你限制于关系模型的范围内。它们让你可以自由地创建为特定任务优化的数据结构。对于许多应用来说,合理地使用好的Globals就如一颗真正的银子弹头,它所提供的速度是传统关系型应用的开发者所梦寐以求的。 作为一种存储数据的方法,globals可以在许多现代编程语言中使用,包括高级和低级语言。因此,本文将特别关注Globals本身,而不是它们曾经来自的语言。 Globals 是如何工作的 让我们先了解一下globals是如何工作的,它们有哪些优点。 我们可以从不同的角度来看待globals。在文章的这一部分,我们可将把它们看成是树形状结构或分层的数据存储空间。 简单地说,Globals是一个持久化的数组。一个自动保存在磁盘上的数组。 很难想象有什么比这更简单的方法来存储数据。在程序代码中(用COS/M语言编写),与普通关联数组的唯一区别是站在它们名字前面的^符号。 若将数据保存为Globals, 你可以不需要知道SQL,因为所有必要的命令都非常简单,在一个小时内就可以学会。 让我们从最简单的例子开始,一个有两个分支的单层的树形结构。例子是用COS(Caché ObjectScript) 写的。 Set ^a("+7926X") = "John Sidorov" Set ^a("+7916Y") = "Sergey Smith" 当数据被插入一个Global(Set命令)时,有3件事情会自动发生: 1.将数据保存到磁盘。2.编制索引。括号里的是下标,等号右边的是а节点值。3. 排序。数据是按一个键来排序的。下一次的遍历会把 "Sergey Smith "放到第一个位置,然后是 "John Sidorov"。当从global获得一个用户列表时,数据库不会在排序上花费时间。实际上你可以请求一个从任何键开始的排序列表,甚至是一个不存在的键(输出将从这个键之后的第一个真正的键开始)。所有这些操作都以惊人的速度进行。在我的个人系统(i5-3340,16GB,HDD WD 1TB Blue)上,我设法在一个进程中达到105万次插入/秒。在多核系统上,速度可以达到几千万次/秒 的插入。 当然,记录插入速度本身并不能说明什么。例如,我们可以将数据写入文本文件--根据传言,这就是Visa的处理方式。然而,通过globals,我们得到了一个结构化和索引化的存储,你可以在工作中享受其高速和易用性。 globals最大的优势是在其中插入新节点的速度。 数据在全局中总是有索引的。单层和深入的树形遍历总是非常快的。 让我们在global中添加一些二级和三级的分支看看: Set ^a("+7926X", "city") = "Moscow" Set ^a("+7926X", "city", "street") = "Req Square" Set ^a("+7926X", "age") = 25 Set ^a("+7916Y", "city") = "London" Set ^a("+7916Y", "city", "street") = "Baker Street" Set ^a("+7916Y", "age") = 36 显然,你可以使用globals建立多层级的“树”。由于每次插入后的自动索引,因而其对任何节点的访问几乎都是即时的。任何一级的树枝都可以按一个键进行排序。 正如你所看到的,数据可以被存储在键和值中。一个键的综合长度(所有索引的长度之和)可以达到511字节,而Caché中的值可以达到3.6MB的大小。树中的层数(维数)上限为31。 还有一件很酷的事情:你可以在不定义顶级节点的值的情况下建立一棵“树”。 Set ^b("a", "b", "c", "d") = 1 Set ^b("a", "b", "c", "e") = 2 Set ^b("a", "b", "f", "g") = 3 空的圆圈是没有值的节点。 为了更好地理解globals,让我们把它们与其他树进行比较:所谓“花园树”和“文件系统名称树”。 让我们把globals与最熟悉的层次结构进行比较:如下的Orchard tree-“生长在花园和田野中的普通树”,以及File system-文件系统。 我们可以看到,叶子和果实只生长在普通树木的枝干末端。文件系统--信息也被存储在树枝的末端,也被称为全文件名。 而下面这里是一个Global的数据结构: 不同: 1.内部节点:Global中的信息可以存储在每个节点中,而不是只存储在分支末端。2.外部节点:globals必须有定义的分支末端(有值的末端),这对文件系统和"花园树"来说不是强制性的。 关于内部节点,我们可以把global的结构看作是文件系统的名字树和花园树结构的超集。所以global的结构是一个更灵活的结构。 一般来说,global是一个结构化的树,支持在每个节点中保存数据。 为了更好地理解globals是如何工作的,让我们想象一下,如果文件系统的创建者使用与globals相同的方法来存储信息,会发生什么? 1. 如果一个文件夹中的最后一个文件被删除了,那么这个文件夹本身以及所有只包含这个被删除的文件夹的高层文件夹也会被删除。 2. 这样就根本不需要文件夹了。会有带子文件的文件和不带子文件的文件。如果你把它与普通的树作比较,每个分支都会变成一个果实。 3. 像README.txt这样的东西可能就不再需要了。所有你需要说的关于文件夹的内容都可以写在文件夹文件本身。一般来说,文件名和文件夹名是没有区别的(例如,/etc/readme可以是文件夹,也可以是文件),这意味着我们只需要操作文件就可以了。 4. 带有子文件夹和文件的文件夹可以更快地被删除。网络上有一些文章讲述了删除数百万个小文件是多么的耗时和困难(1, 2, 3). 然而,如果你创建一个基于Global的假的文件系统,它将只需要几秒钟甚至几分之一秒。当我在家里的电脑上测试删除子树时,我成功地从HDD(不是SDD)上的两级树上删除了96-341万个节点。值得一提的是,我们讨论的是删除Global树的一部分,而不是删除包含Global的整个文件。 子树的移除是globals的另一个优势:你不需要递归来做这个。它的速度快得令人难以置信。 在我们的树中,这可以通过一个Kill的命令来完成。 Kill ^a("+7926X") 下面是一个小表格,它可以让你更好地了解你可以在Global上执行的操作 Cache object script中与Globals有关的关键命令和功能 Set设置 设置(初始化)分支到一个节点(如果未定义)和节点值 Merge合并 复制一棵子树 Kill 删除一棵字树 ZKill 删除一个特定节点的值。源自该节点的子树不受影响。 $Query 对树进行全面深入的遍历 $Order 返回同一级别的下一个下标 $Data 检查一个节点是否被定义 $Increment 节点值的原子递增,以避免ACID的读和写。最新的建议是使用 $Sequence 来代替 感谢你的关注,我很乐意回答你的任何问题。 免责声明:本文反映了作者的个人观点,与InterSystems的官方立场无关。 让我们期待下一篇继续 "Globals 是存储数据的魔剑-树 :第二部分 (待翻译) 你将了解到哪些类型的数据可以显示在globals中,以及它们在哪些地方效果最好。 好文!
文章
姚 鑫 · 八月 22, 2022

第九章 配置数据库(一)

# 第九章 配置数据库(一) 数据库是使用数据库向导创建的 `IRIS.DAT` 文件。 `IRIS`数据库保存称为全局变量的多维数组中的数据和称为例程的可执行内容,以及类和表定义。 全局变量和例程包括方法、类、网页、SQL、BASIC和JavaScript文件 **注意:在 `Windows` 系统上,不要对 `IRIS.DAT` 数据库文件使用文件压缩。 (通过右键单击 `Windows` 资源管理器中的文件或文件夹并选择属性,然后选择高级,然后压缩内容以节省磁盘空间来压缩文件;压缩后,文件夹名称或文件名在 `Windows` 资源管理器中呈现为蓝色。)如果压缩`IRIS.DAT` 文件,它所属的实例将无法启动,并出现误导性错误。** `IRIS` 数据库根据需要动态扩展(假设有可用空间),但可以指定最大大小。如果使用默认的 `8KB` 块大小,数据库可以增长到 `32 TB`。 可以动态更改大多数数据库配置;可以在系统运行时创建和删除数据库以及修改数据库属性。 **注意:这些主题描述了使用管理门户手动配置数据库的过程。 `IRIS` 还包含可用于自动化数据库配置的编程工具。可以使用新选项卡类中的 `Config.Databases` 来创建和配置数据库;还可以使用 `^DATABASE` 命令行实用程序配置数据库。** **配置数据库的另一种方法是将 `CreateDatabase`、`ModifyDatabase` 或 `DeleteDatabase` 操作与配置合并结合使用。配置合并允许通过应用声明性合并文件来自定义 `IRIS` 实例,该文件指定要应用于该实例的设置和操作。** # Background `IRIS` 将数据——持久多维数组(`globals`)以及可执行代码(例程)——存储在一个或多个称为数据库的物理结构中。数据库由存储在本地操作系统中的一个或多个物理文件组成。一个 `IRIS` 系统可能(并且通常确实)有多个数据库。 每个 `IRIS` 系统都维护一个数据库缓存——一个本地共享内存缓冲区,用于缓存从物理数据库中检索到的数据。这种高速缓存大大减少了访问数据所需的昂贵 `I/O` 操作的数量,并提供了 `IRIS` 的许多性能优势。 `IRIS` 应用程序通过命名空间访问数据。命名空间提供存储在一个或多个物理数据库中的数据(全局变量和例程)的逻辑视图。一个 `IRIS` 系统可能(并且通常确实)有多个命名空间。 `IRIS` 将逻辑命名空间中可见的数据映射到一个或多个物理数据库。这种映射为应用程序提供了一种强大的机制,可以在不更改应用程序逻辑的情况下更改应用程序的物理部署。 在最简单的情况下,命名空间和数据库之间存在一一对应关系,但许多系统利用定义命名空间的能力来提供对多个数据库中数据的访问。例如,一个系统可以有多个命名空间,每个命名空间提供存储在一个或多个物理数据库中的数据的不同逻辑视图。 # 数据库注意事项 ## 数据库总限制 可以在单个 `IRIS` 实例中配置的数据库数量的绝对限制(如果有足够的存储空间)是 `15,998`。其他限制如下: - 数据库的目录信息不能超过 `256 KB`。这意味着,如果数据库目录名称的平均长度较长,则实例可以拥有较少的数据库总数。以下公式描述了这种关系: ```math maximum_DBs = 258048/ (avg_DB_path_length + 3) ``` 例如,如果所有数据库目录路径的格式为 `c:\InterSystems\IRIS\mgr\DBNNNN\`,则平均长度为 `33` 个字节。因此,最大数据库数为 `7,168`,计算如下:`258048/ (33 + 3) = 7168`。 - 镜像数据库在 `15,998` 的绝对限制中计数两次。如果实例上的所有数据库都进行了镜像,则有效限制为 `7,499` 个数据库。这是因为 `IRIS` 为镜像数据库创建了两个数据库定义;一个用于目录路径 (`c:\InterSystems\IRIS\mgr\DBNNNN\`),另一个用于镜像定义 (`:mirror:MIRRORNAME:MirrorDBName`)。 - 可以同时使用的数据库数量受操作系统对打开文件数量(每个进程或系统范围)的限制的限制。 `IRIS` 将大约一半的操作系统打开文件分配留给自己和设备使用。 ## 数据库配置注意事项 以下是配置数据库时要考虑的提示: - `IRIS` 提供了一个无缝选项,可以在多个物理数据库 (`IRIS.DAT`) 文件中传播数据。因此,可以根据需要构建具有多个数据库的应用程序或通过全局或下标级映射拆分数据。 - 根据可用于管理任务(如备份、恢复、完整性检查等)的基础设施,将数据库大小保持在可管理的范围内。 - 建议将流全局变量(如果将流存储在 `IRIS.DAT` 数据库文件中)全局映射到单独的数据库,并且将流数据库配置为大 (`64 KB`) 块大小。 - 根据工作负载,考虑替代(更大)块大小可能比默认的 `8 KB` 数据库块大小更有利。 ## 大数据块大小注意事项 除了 `IRIS` 支持的 `8 KB`(默认)块大小(始终启用)之外,还可以启用以下块大小: - `16 KB (16384)` - `32 KB (32768)` - `64 KB (65536)` 但是,在创建使用大块的数据库时应该谨慎,因为使用它们会影响系统的性能。 在启用和使用大的块大小之前,请考虑以下几点: - 如果应用程序工作负载主要由顺序插入或顺序读取/查询组成,那么大的块大小可以提高性能。 - 如果应用程序工作负载主要由随机插入或随机读取/查询组成,那么大的块大小可能会降低性能。 由于对于给定的数据库缓存总大小,较大的块大小会导致缓存更少的块,为了减少对随机数据库访问的影响,还应该考虑将更多的总内存用作数据库缓存。 - 对于索引类型的数据库,默认的块大小(`8 KB`)确保最佳性能; 较大的块大小可能会降低性能。 如果正在考虑为数据设置更大的块大小,那么应该考虑将索引全局变量映射到一个单独的`8 KB`块大小的数据库。 要创建一个使用不支持的块大小的数据库,请执行以下操作: 1. 使用启动设置页面(系统管理>附加设置>启动)的设置启用块大小,在配置参数文件引用的`DBSizesAllowed`条目中描述。 2. 在启动设置页面(系统管理>附加设置>启动),按照内存和启动设置中的描述,为启用的块大小配置数据库缓存。 3. 重新启动 4. 按照创建本地数据库中的说明创建数据库。 # 数据库兼容性注意事项 如创建本地数据库过程中所述,可以通过复制或移动 `IRIS.DAT` 文件将 `IRIS` 数据库复制或移动到创建它的实例之外的实例,或临时装载在另一个实例中创建的数据库在同一个系统上。还可以将数据库的备份(请参阅数据完整性指南的“备份和恢复”一章)恢复到其原始实例以外的实例。但是,为避免数据不兼容,必须满足以下要求: - 目标(新)实例必须使用相同的字符宽度(`8`位或`Unicode`; 请参阅安装指南中的新选项卡中的字符宽度设置),并使用相同的区域设置(请参阅使用管理门户的NLS设置页面)作为创建数据库的实例。 此要求的一个例外是使用基于 `ISO 8859 Latin-1` 字符集的区域设置的 `8` 位实例与使用相应宽字符区域设置的 `Unicode` 实例兼容。例如,使用 `enu8` 语言环境在 `8` 位实例中创建的数据库可以在使用 `enuw` 语言环境的 `Unicode` 实例中使用。 - 如果源实例和目标实例位于不同字节序的系统上,则数据库必须转换为目标实例的字节序后才能使用。 根据平台的不同,多字节数据存储在最低内存地址(即首先)中的最高有效字节或最低有效字节:当最高有效字节首先存储时,称为“大端;”当首先存储最低有效字节时,它被称为“小端”。 当使用在不同端序的系统上创建的现有`IRIS.DAT`定义数据库时,请在使用数据库之前使用`cvendian`实用程。
文章
Michael Lei · 十二月 7, 2022

ECP 与 Docker

大家好! 这是关于使用 Docker 初始化 IRIS 实例的系列文章中的第三篇。 这次,我们将关注企业缓存协议(**E**nterprise **C**ache **P**rotocol,ECP)。 ECP 允许以一种非常简单的方式将某些 IRIS 实例配置为应用程序服务器,将其他实例配置为数据服务器。 有关详细的技术信息,请参阅官方文档。 本文旨在介绍: * 如何编写数据服务器的初始化脚本,以及如何编写一个或多个应用程序服务器的初始化脚本。 * 如何使用 Docker 在这些节点之间建立加密连接。 为此,我们通常使用我们在以前的 Web 网关中已经看到的一些工具,以及描述 OpenSSL、envsubst 和 Config-API 等工具的镜像文章。 ## 要求 ECP 不适用于 IRIS 社区版。 因此,需要访问全球响应中心才能下载容器许可证并连接到 containers.intersystems.com 注册表。 ## 准备系统 系统必须与容器共享一些本地文件。 需要创建特定用户和组来避免出现“访问被拒绝”错误。 ```bash sudo useradd --uid 51773 --user-group irisowner sudo useradd --uid 52773 --user-group irisuser sudo groupmod --gid 51773 irisowner sudo groupmod --gid 52773 irisuser ``` 如果您还没有“iris.key”许可证,请从 WRC 下载,并将其添加到您的主目录中。 ## 检索示例存储库 除“iris.key”许可证外,您需要的所有其他文件都可以在公共存储库中找到,因此,首先将其克隆: ```bash git clone https://github.com/lscalese/ecp-with-docker.git cd ecp-with-docker ``` ## SSL 证书 为了加密应用程序服务器与数据服务器之间的通信,我们需要 SSL 证书。 可以使用现成的脚本(“gen-certificates.sh”)。 但是,您可以随意修改脚本,使证书设置与您的位置、公司等保持一致。 执行: ```bash sh ./gen-certificates.sh ``` 生成的证书现在位于“./certificates”目录中。 | 文件 | 容器 | 描述 | | ------------------------------ | ------------- | ---------------- | | ./certificates/CA_Server.cer | 应用程序服务器和数据服务器 | 机构服务器证书 | | ./certificates/app_server.cer | 应用程序服务器 | IRIS 应用程序服务器实例证书 | | ./certificates/app_server.key | 应用程序服务器 | 相关私钥 | | ./certificates/data_server.cer | 数据服务器 | IRIS 数据服务器实例证书 | | ./certificates/data_server.key | 数据服务器 | 相关私钥 | ## 构建镜像 首先,登录 Intersystems docker 注册表。 在构建期间,将从注册表中下载基础镜像: ```bash docker login -u="YourWRCLogin" -p="YourICRToken" containers.intersystems.com ``` 如果您不知道自己的Token,请使用您的 WRC 帐户登录 https://containers.intersystems.com/。 在此构建过程中,我们将向 IRIS 基础镜像添加一些软件实用程序: * **gettext-base**:它将允许我们使用“envsubst”命令替换配置文件中的环境变量。 * **iputils-arping**:如果我们想要镜像数据服务器,则需要使用此实用程序。 * **ZPM**:ObjectScript 软件包管理器。 [Dockerfile](https://github.com/lscalese/ecp-with-docker/blob/master/Dockerfile): ``` ARG IMAGE=containers.intersystems.com/intersystems/iris:2022.2.0.281.0 # Don't need to download the image from WRC. It will be pulled from ICR at build time. FROM $IMAGE USER root # Install iputils-arping to have an arping command. It's required to configure Virtual IP. # Download the latest ZPM version (ZPM is included only with community edition). RUN apt-get update && apt-get install iputils-arping gettext-base && \ rm -rf /var/lib/apt/lists/* USER ${ISC_PACKAGE_MGRUSER} WORKDIR /home/irisowner/demo RUN --mount=type=bind,src=.,dst=. \ iris start IRIS && \ iris session IRIS < iris.script && \ iris stop IRIS quietly ``` 此 Dockerfile 中除最后一行外没有什么特别之处。 它将 IRIS 数据服务器实例配置为最多接受 3 个应用程序服务器。 请注意,此配置需要重新启动 IRIS。 我们在构建过程中分配此参数的值,以避免稍后编写重新启动脚本。 开始构建: ```bash docker-compose build –no-cache ``` ## 配置文件 在配置 IRIS 实例(应用程序服务器和数据服务器)时,我们使用 JSON config-api 文件格式。 您会注意到这些文件包含环境变量 "${variable_name}"。 它们的值在“docker-compose.yml”文件的“environment”部分定义,我们稍后将在本文档中看到。 这些变量将在使用“envsubst”实用程序加载文件之前被替换掉。 ### 数据服务器 对于数据服务器,我们将: * 启用 ECP 服务并定义授权客户端(应用程序服务器)列表。 * 创建加密通信所需的“SSL %ECPServer”配置。 * 创建“myappdata”数据库。 它将用作来自应用程序服务器的远程数据库。 (data-serer.json)[https://github.com/lscalese/ecp-with-docker/blob/master/config-files/data-server.json] ```json { "Security.Services" : { "%Service_ECP" : { "Enabled" : true, "ClientSystems":"${CLIENT_SYSTEMS}", "AutheEnabled":"1024" } }, "Security.SSLConfigs": { "%ECPServer": { "CAFile": "${CA_ROOT}", "CertificateFile": "${CA_SERVER}", "Name": "%ECPServer", "PrivateKeyFile": "${CA_PRIVATE_KEY}", "Type": "1", "VerifyPeer": 3 } }, "Security.System": { "SSLECPServer":1 }, "SYS.Databases":{ "/usr/irissys/mgr/myappdata/" : {} }, "Databases":{ "myappdata" : { "Directory" : "/usr/irissys/mgr/myappdata/" } } } ``` 此配置文件由“init_datasrv.sh”脚本在数据服务器容器启动时加载。 连接到数据服务器的所有应用程序服务器都必须可信。 此脚本将在 100 秒内自动验证所有连接,以限制管理门户中的手动操作。 当然,可以对其进行改进以提高安全性。 ### 应用程序服务器 对于应用程序服务器,我们将: * 启用 ECP 服务。 * 创建通信加密所需的 SSL 配置“%ECPClient”。 * 配置与数据服务器的连接信息。 * 创建远程数据库“myappdata”的配置。 * 在“USER”命名空间中创建到“myappdata”数据库的全局映射“demo.*”。 它可以让我们稍后测试 ECP 的运行。 [app-server.json](https://github.com/lscalese/ecp-with-docker/blob/master/config-files/app-server.json): ```json { "Security.Services" : { "%Service_ECP" : { "Enabled" : true } }, "Security.SSLConfigs": { "%ECPClient": { "CAFile": "${CA_ROOT}", "CertificateFile": "${CA_CLIENT}", "Name": "%ECPClient", "PrivateKeyFile": "${CA_PRIVATE_KEY}", "Type": "0" } }, "ECPServers" : { "${DATASERVER_NAME}" : { "Name" : "${DATASERVER_NAME}", "Address" : "${DATASERVER_IP}", "Port" : "${DATASERVER_PORT}", "SSLConfig" : "1" } }, "Databases": { "myappdata" : { "Directory" : "/usr/irissys/mgr/myappdata/", "Name" : "${REMOTE_DB_NAME}", "Server" : "${DATASERVER_NAME}" } }, "MapGlobals":{ "USER": [{ "Name" : "demo.*", "Database" : "myappdata" }] } } ``` 配置文件由“[init_appsrv.sh](https://github.com/lscalese/ecp-with-docker/blob/master/init_appsrv.sh)”脚本在应用程序服务器容器启动时加载。 ## 启动容器 现在,我们可以启动容器: * 2 个应用程序服务器。 * 1 个数据服务器。 为此,请运行: docker-compose up –scale ecp-demo-app-server=2 请参阅 [docker-compose](https://github.com/lscalese/ecp-with-docker/blob/master/docker-compose.yml) 文件以了解详情: ``` # Variables are defined in .env file # to show the resolved docker-compose file, execute # docker-compose config version: '3.7' services: ecp-demo-data-server: build: . image: ecp-demo container_name: ecp-demo-data-server hostname: data-server networks: app_net: environment: # List of allowed ECP clients (application server). - CLIENT_SYSTEMS=ecp-with-docker_ecp-demo-app-server_1;ecp-with-docker_ecp-demo-app-server_2;ecp-with-docker_ecp-demo-app-server_3 # Path authority server certificate - CA_ROOT=/certificates/CA_Server.cer # Path to data server certificate - CA_SERVER=/certificates/data_server.cer # Path to private key of the data server certificate - CA_PRIVATE_KEY=/certificates/data_server.key # Path to Config-API file to initiliaze this IRIS instance - IRIS_CONFIGAPI_FILE=/home/irisowner/demo/data-server.json ports: - "81:52773" volumes: # Post start script - data server initilization. - ./init_datasrv.sh:/home/irisowner/demo/init_datasrv.sh # Mount certificates (see gen-certificates.sh to generate certificates) - ./certificates/app_server.cer:/certificates/data_server.cer - ./certificates/app_server.key:/certificates/data_server.key - ./certificates/CA_Server.cer:/certificates/CA_Server.cer # Mount config file - ./config-files/data-server.json:/home/irisowner/demo/data-server.json # IRIS License - ~/iris.key:/usr/irissys/mgr/iris.key command: -a /home/irisowner/demo/init_datasrv.sh ecp-demo-app-server: image: ecp-demo networks: app_net: environment: # Hostname or IP of the data server. - DATASERVER_IP=data-server - DATASERVER_NAME=data-server - DATASERVER_PORT=1972 # Path authority server certificate - CA_ROOT=/certificates/CA_Server.cer - CA_CLIENT=/certificates/app_server.cer - CA_PRIVATE_KEY=/certificates/app_server.key - IRIS_CONFIGAPI_FILE=/home/irisowner/demo/app-server.json ports: - 52773 volumes: # Post start script - application server initilization. - ./init_appsrv.sh:/home/irisowner/demo/init_appsrv.sh # Mount certificates - ./certificates/CA_Server.cer:/certificates/CA_Server.cer # Path to private key of the data server certificate - ./certificates/app_server.cer:/certificates/app_server.cer # Path to private key of the data server certificate - ./certificates/app_server.key:/certificates/app_server.key # Path to Config-API file to initiliaze this IRIS instance - ./config-files/app-server.json:/home/irisowner/demo/app-server.json # IRIS License - ~/iris.key:/usr/irissys/mgr/iris.key command: -a /home/irisowner/demo/init_appsrv.sh networks: app_net: ipam: driver: default config: # APP_NET_SUBNET variable is defined in .env file - subnet: "${APP_NET_SUBNET}" ``` ## 我们来测试一下! ### 访问数据服务器管理门户 容器已启动。 我们从数据服务器中检查一下状态。 端口 52773 映射到本地端口 81,因此可以使用此地址 [http://localhost:81/csp/sys/utilhome.csp](http://localhost:81/csp/sys/utilhome.csp) 进行访问 使用默认登录名\密码登录,然后转到 System -> Configuration -> ECP Params(系统 -> 配置 -> ECP 参数)。 点击“ECP Application Servers”(ECP 应用程序服务器)。 如果一切正常,您应该会看到 2 个状态为“Normal”(正常)的应用程序服务器。 客户端名称的结构为 "数据服务器名称":"应用程序服务器主机名":"IRIS 实例名称"。 本例中,我们没有设置应用程序服务器主机名,因此我们将获得自动生成的主机名。 ![应用程序服务器列表](https://raw.githubusercontent.com/lscalese/ecp-with-docker/master/img/app-server-list-en.png) ### 访问应用程序服务器管理门户 要连接到应用程序服务器的管理门户,首先需要获取端口号。 由于我们使用了“--scale”选项,我们无法在 docker-compose 文件中设置端口。 因此,必须使用 `docker ps` 命令检索它们: ``` docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a1844f38939f ecp-demo "/tini -- /iris-main…" 25 minutes ago Up 25 minutes (unhealthy) 1972/tcp, 2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:81->52773/tcp, :::81->52773/tcp ecp-demo-data-server 4fa9623be1f8 ecp-demo "/tini -- /iris-main…" 25 minutes ago Up 25 minutes (unhealthy) 1972/tcp, 2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:49170->52773/tcp, :::49170->52773/tcp ecp-with-docker_ecp-demo-app-server_1 ecff03aa62b6 ecp-demo "/tini -- /iris-main…" 25 minutes ago Up 25 minutes (unhealthy) 1972/tcp, 2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:49169->52773/tcp, :::49169->52773/tcp ecp-with-docker_ecp-demo-app-server_2 ``` 在本示例中,端口: * 49170,用于第一个应用程序服务器 http://localhost:49170/csp/sys/utilhome.csp * 49169,用于第二个应用程序服务器 http://localhost:49169/csp/sys/utilhome.csp ![数据服务器](https://raw.githubusercontent.com/lscalese/ecp-with-docker/master/img/data-server-status-en.png) ### 远程数据库上的读/写测试 我们在终端中执行一些读/写测试。 在第一个应用程序服务器上打开一个 IRIS 终端: ``` docker exec -it ecp-with-docker_ecp-demo-app-server_1 iris session iris Set ^demo.ecp=$zdt($h,3,1) _ “ write from the first application server.” ``` 现在,在第二个应用程序服务器上打开一个终端: ``` docker exec -it ecp-with-docker_ecp-demo-app-server_2 iris session iris Set ^demo.ecp(2)=$zdt($h,3,1) _ " write from the second application server." zwrite ^demo.ecp ``` 您应该会看到两个服务器中的响应: ``` ^demo.ecp(1)="2022-07-05 23:05:10 write from the first application server." ^demo.ecp(2)="2022-07-05 23:07:44 write from the second application server." ``` 最后,在数据服务器上打开一个 IRIS 终端并执行全局 demo.ecp 读取: ``` docker exec -it ecp-demo-data-server iris session iris zwrite ^["^^/usr/irissys/mgr/myappdata/"]demo.ecp ^["^^/usr/irissys/mgr/myappdata/"]demo.ecp(1)="2022-07-05 23:05:10 write from the first application server." ^["^^/usr/irissys/mgr/myappdata/"]demo.ecp(2)="2022-07-05 23:07:44 write from the second application server." ``` 希望大家喜欢这篇文章。 欢迎您发表评论。