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

每一行数据在逻辑上对应一个订单,单行中的所有数据在物理上存储在一起。
这种模式便于快速添加或更新订单。订单可以一次添加一个,数据库的每次写入正好对应一行。当发生了订单的事务,除了要更改的行之外,无需访问或更新表中的任何数据。
让我们考虑另外的情况:假设公司想找到每个月的平均销售收入。为此,您只需要两列的信息: 订单日期和总金额。但是,如果使用基于行的存储,则必须检索每个订单的所有数据才能获得此信息。即使在 OrderDate 列上使用了索引,仍需要读取日期范围内每个订单的完整行,才能获得总金额。如果订单数量较多,这样做的效率会非常低。
这就是列式存储的作用所在。数据不是按行存储在一起,而是按列存储在一起。从逻辑上讲,表和数据之间的关联保持不变。改变的只是物理存储方法。
如下图所示数据是按一列列,也就是一个字段一个字段存储的。

让我们看看使用列式存储时,如何获取给定月份的平均收入。您只需读取两列数据: 订单日期和总金额。您可以对 OrderDate 列进行过滤,选择某个月份的订单,然后使用快速向量化处理从 TotalAmount 列计算平均值。
这样存储数据有几个好处。首先,可以更快地运行分析查询。列式存储大大减少了普通查询必须检索的无关数据量。我们不需要读取每个订单的所有数据,而只需要读取订单日期和总金额列。由于我们是按日期进行筛选,因此读取的所有数据都是相关的。
利用现代 CPU 上的低级指令,还可以对数据块而不是单个值进行操作。这种效率的提高可以大大改善您的分析能力。
列式存储的另一个好处是,列中的所有元素都具有相同的数据类型,从而实现了高效压缩。例如,Status 列中的所有条目都只由几个可能的字符串组成。在这种情况下,列可以维护一个不同状态的字典,并只存储指向字典条目的指针,而不是完整的字符串。这样做可以避免存储重复值,从而大大提高每列的编码效率。这也使得列数据的读取更加高效。.
在数据仓库等典型的分析性工作负载中,列式存储的优势非常明显。不过,在许多使用案例中,纯粹的列存储模型并不是最佳方法。例如,在运行事务检索或更新单行时,列式存储就不太理想。因为您需要访问存储该行数据的所有列,最终会调用比所需更多的数据。
在 InterSystems IRIS 中,数据存储在Global中,因此可以同时使用基于行的存储和列式存储。利用这种灵活的方法,你可以选择按列存储某些需要快速查询的数据,同时按行存储其余数据,这样你仍然可以运行快速事务来检索或更新单行。
状态和优先级等数据可能会更新几次,因此这些事务将受益于行存储。您可以将 TotalAmount 存储在列存储中,以便于访问和分析,同时将表的其他部分存储在行中,如下图所示:
**列存储的实现**
列存储的实现非常简单:只需在表定义的末尾添加一个存储类型子句 WITH STORAGETYPE = COLUMNAR 即可。查询数据的方式不会发生任何变化,因此只要使用列式存储定义了表,就可以开始运行快速分析查询。
下面是列存储表的定义例子:
```sql
CREATE TABLE Sample.TransactionHistory (
AccountNumber INTEGER,
TransactionDate DATE,
Description VARCHAR(100),
Amount NUMERIC(10,2),
Type VARCHAR(10))
WITH STORAGETYPE = COLUMNAR
```
而下面的定义中表的存储类型为行存储(默认存储方式), 而Amount字段定义为列式存储
```sql
CREATE TABLE Sample.BankTransaction (
AccountNumber INTEGER,
TransactionDate DATE,
Description VARCHAR(100),
Amount NUMERIC(10,2) WITH STORAGETYPE = COLUMNAR,
Type VARCHAR(10))
```
除了上面的定义外,IRIS还运行用户创建基于列式存储的索引。对于已有的行存储表的数据中的某列创建列式存储的索引,可以达到将此列定义为列存储的相同效果。
```sql
CREATE TABLE Sample.BankTransaction (
AccountNumber INTEGER,
TransactionDate DATE,
Description VARCHAR(100),
Amount NUMERIC(10,2),
Type VARCHAR(10))
CREATE COLUMNAR INDEX AmountIndex
ON Sample.BankTransaction(Amount)
```
**行列存储的选择**
对于利用InterSystems SQL或Objects的应用程序,尤其是事务处理类应用程序,行存储布局是正确的选择。而如果应用程序采用的是分析类操作,如果分析性查询的性能不令人满意,在保持行存储布局的情况下,应该对于用于聚合的数字字段,或范围条件的字段,添加列式索引。最后, OLAP数据平台, 数据仓库,使用列式存储,或者行列混存布局,可以大幅提高查询效率。
如果您想了解更多, 请阅读[在线文档-选择SQL表的存储类型](https://docs.intersystems.com/iris20242/csp/docbook/DocBook.UI.Page.cls?KEY=GSOD_storage)
文章
姚 鑫 · 七月 5, 2021
[toc]
# 第二十八章 定制SAX解析器创建自定义内容处理程序
# 创建自定义内容处理程序
如果直接调用InterSystems IRIS SAX解析器,则可以根据自己的需要创建自定义内容处理程序。本节讨论以下主题:
- Overview
- 要在内容处理程序中自定义的方法的描述
- `%XML.SAX.Parser`类中解析方法的参数列表摘要
- 示例
## 创建自定义内容处理程序概述
要定制InterSystems IRIS SAX解析器导入和处理XML的方式,请创建并使用定制的SAX内容处理程序。具体地说,创建`%XML.SAX.ContentHandler`的子类。然后,在新类中,重写任何默认方法以执行所需的操作。在解析XML文档时使用新的内容处理程序作为参数;为此,需要使用`%XML.SAX.Parser`类的解析方法。
此操作如下图所示:

创建和使用自定义导入机制的过程如下:
1. 创建扩展`%XML.SAX.ContentHandler`的类。
2. 在该类中,包括希望覆盖的方法,并根据需要提供新定义。
3. 在使用`%XML.SAX.Parser`的分析方法之一(即`ParseFile()`、`ParseStream()`、`ParseString()`或`ParseURL()`)编写读取XML文档的类方法。
调用分析方法时,请将自定义内容处理程序指定为参数。
## SAX内容处理程序的可定制方法
`%XML.SAX.ContentHandler`类在特定时间自动执行某些方法。通过覆盖它们,您可以自定义内容处理程序的行为。
### 响应事件
`%XML.SAX.ContentHandle`类分析XML文件,并在它到达XML文件中的特定点时生成事件。根据事件的不同,会执行不同的方法。这些方法如下:
- `OnPostParse()` — 在XML解析完成时触发。
- `characters()` — 由字符数据触发。
- `comment()` — 注释触发
- `endCData()` —由CDATA部分的末尾触发。
- `endDocument()` —由文档结尾触发。
- `endDTD()` — 由DTD结束触发。
- `endElement()` —由元素的末尾触发。
- `endEntity()` — 由一个实体的终结触发。
- `endPrefixMapping()` — 由名称空间前缀映射的结束触发。
- `ignorableWhitespace()` — 由元素内容中的可忽略空格触发。
- `processingInstruction()` — 由XML处理指令触发。
- `skippedEntity()` — 被跳过的实体触发。
- `startCData()` —由CDATA部分的开头触发。
- `startDocument()` — 由文档的开头触发。
- `startDTD()` — 由DTD的开头触发。
- `startElement()` — 由元素的开始触发。
- `startEntity()` — 由一个实体的开始触发。
- `startPrefixMapping()` — 由名称空间前缀映射的开始触发。
默认情况下,这些方法是空的,可以在自定义内容处理程序中覆盖它们。
### 处理错误
`%XML.SAX.ContentHandler`类在遇到某些错误时也会执行方法:
- `error()` — 由可恢复的解析器错误触发。
- `fatalError()` — 由致命的XML解析错误触发。
- `warning()` — 由解析器警告通知触发。
默认情况下,这些方法为空,可以在自定义内容处理程序中重写它们。
### 计算事件掩码
当调用InterSystems IRIS SAX解析器(通过`%XML.SAX.Parser`类)时,可以指定一个掩码参数来指示哪些回调是感兴趣的。如果未指定掩码参数,解析器将调用内容处理程序的`Mask()`方法。此方法返回一个整数,该整数指定与内容处理程序的重写方法相对应的复合掩码。
例如,假设创建了一个自定义内容处理程序,其中包含`startElement()`和`endElement()`方法的新版本。在本例中,`Mask()`方法返回一个数值,该数值等于`$$$SAXSTARTELEMENT`和`$$$SAXENDELEMENT`,之和,这两个标志对应于这两个事件。如果没有为解析方法指定掩码参数,则解析器将调用内容处理程序的`Mask()`方法,因此只处理这两个事件。
### 其他有用的方法
`%XML.SAX.ContentHandler`类提供在特殊情况下有用的其他方法:
- `LocatePosition()`-通过引用返回两个参数,这两个参数指示解析的文档中的当前位置。第一个表示行号,第二个表示行偏移。
- `PushHandler()`-在堆栈上推送新的内容处理程序。SAX的所有后续回调都将转到这个新的内容处理程序,直到该处理程序完成处理。
如果在解析一种类型的文档时遇到想要以不同方式解析的一段XML,则可以使用此方法。在本例中,当检测到要以不同方式处理的段时,调用`PushHandler()`方法,该方法将创建一个新的内容处理程序实例。所有回调都会转到此内容处理程序,直到调用`PopHandler()`返回上一个内容处理程序。
- `PopHandler()`-返回堆栈上的上一个内容处理程序。
这些是`final`方法,不能重写。
## SAX解析方法的参数列表
要指定文档源,请使用`%XML.SAX.Parser`类的`ParseFile()`、`ParseStream()`、`ParseString()`或`ParseURL()`方法。在任何情况下,源文档都必须是格式良好的XML文档;也就是说,它必须遵守XML语法的基本规则。完整的参数列表按顺序如下:
1. pFilename, pStream, pString, or pURL — 文档源.
2. pHandler — 内容处理程序,它是`%XML.SAX.ContentHandler`类的实例。
3. pResolver — 分析源时使用的实体解析器。
4. pFlags — 用于控制SAX解析器执行的验证和处理的标志。
5. pMask — 用于指定XML源中感兴趣的项的掩码。通常不需要指定此参数,因为对于`%XML.SAX.Parser`的解析方法,默认掩码为`0`。这意味着解析器调用内容处理程序的`Mask()`方法。该方法通过检测(在编译期间)在事件处理程序中自定义的所有事件回调来计算掩码。只处理那些事件回调。
6. pSchemaSpec — 验证文档源所依据的架构规范。此参数是一个字符串,其中包含以逗号分隔的命名空间/URL对列表:
```
"namespace URL,namespace URL"
```
这里,`Namespace`是用于模式的XML名称空间,`URL`是提供模式文档位置的`URL`。名称空间和`URL`值之间有一个空格字符。
7. pHttpRequest (For the ParseURL() method only) — 这里,`Namespace`是用于模式的XML名称空间,URL是提供模式文档位置的URL。名称空间和URL值之间有一个空格字符。
8. pSSLConfiguration — 客户端`SSL/TLS`配置的配置名称。
注意:请注意,此参数列表与`%XML.TextReader`类的解析方法略有不同。有一点不同,`%XML.TextReader`不提供指定自定义内容处理程序的选项。
## SAX处理程序示例
想要一个文件中出现的所有XML元素的列表。要做到这一点,只需记录每个开始元素。那么这个过程是这样的:
1. 创建一个名为`MyApp.Handler`的类,它扩展`%XML.SAX.ContentHandler`:
```
Class MyApp.Handler Extends %XML.SAX.ContentHandler
{
}
```
2. 使用以下内容覆盖`startElement()`方法:
```
Class MyApp.MyHandler extends %XML.SAX.ContentHandler
{
// ...
Method startElement(uri as %String, localname as %String,
qname as %String, attrs as %List)
{
//we have found an element
write !,"Element: ",localname
}
}
```
3. 将一个类方法添加到读取和分析外部文件的Handler类:
```
Class MyApp.MyHandler extends %XML.SAX.ContentHandler
{
// ...
ClassMethod ReadFile(file as %String) as %Status
{
//create an instance of this class
set handler=..%New()
//parse the given file using this instance
set status=##class(%XML.SAX.Parser).ParseFile(file,handler)
//quit with status
quit status
}
}
```
请注意,这是一个类方法,因为它在应用程序中被调用以执行其处理。此方法执行以下操作:
1. 它创建内容处理程序对象的实例:
```
set handler=..%New()
```
2. 它在一个调用`%XML.SAX.Parser`的`ParseFile()`方法。这将验证并解析文档(由`fileName`指定),并调用内容处理程序对象的各种事件处理方法:
```
set status=##class(%XML.SAX.Parser).ParseFile(file,handler)
```
每次在解析器解析文档时发生事件(如开始或结束元素)时,解析器都会调用内容处理程序对象中的适当方法。在本例中,唯一被覆盖的方法是`startElement()`,它随后写出元素名称。对于其他事件,例如到达`End`元素,不会发生任何事情(默认行为)。
3. 当`ParseFile()`方法到达文件末尾时,它返回。处理程序对象超出作用域,并自动从内存中删除。
4. 在应用程序中的相应点,调用`ReadFile()`方法,将文件传递给解析:
```
Do ##class(Samples.MyHandler).ReadFile(filename)
```
其中,filename是正在读取的文件的路径。
例如,如果文件的内容如下:
```
Edwards,Angela U.
1980-04-19
K8134
Vail
94059
Uberoth,Wilma I.
Wells,George H.
```
则此示例的输出如下所示:
```
Element: Root
Element: Person
Element: Name
Element: DOB
Element: GroupID
Element: HomeAddress
Element: City
Element: Zip
Element: Doctors
Element: Doctor
Element: Name
Element: Doctor
Element: Name
```
# 使用HTTPS
`%XML.SAX.Parser`支持`HTTPS`。也就是说,可以使用此类执行以下操作:
- (对于`ParseURL()`)解析`HTTPS`位置提供的XML文档。
- (对于所有解析方法)解析`HTTPS`位置的实体。
在所有情况下,如果这些项目中的任何一个是`在HTTPS`位置上提供的,请执行以下操作:
1. 使用管理门户创建包含所需连接详细信息的`SSL/TLS`配置。这是一次性的步骤。
2. 调用`%XML.SAX.Parser`的适用解析方法时,请指定`pSSLConfiguration`参数。
默认情况下,InterSystems IRIS使用`Xerces`图元解析。`%XML.SAX.Parser`仅在以下情况下使用其自己的实体解析:
- `PSSLConfiguration`参数非空。
- 已配置代理服务器。
文章
Qiao Peng · 一月 30, 2022
各种技术在交换数据的时候,就需要知道对方给的数据使用什么字符集和字符编码,否则很可能就解码错了。这里列举了医疗行业常见的数据交换技术方式和它们对字符集使用的声明方式。
2.1 文件
文件是字符型数据最常见的交换方式,文本编辑工具通常在保存时都会让用户选择保存成什么字符编码。对于不同的字符编码,文件是如何保存的呢?
通常会在文件头使用字节顺序标志(BOM,Byte Order Mark)来标记文件的编码。下表是常见的编码格式对应的BOM,注意ANSI并不需要BOM,我把它列在这里的目的是希望一目了然。
字节
编码格式
00 00 FE FF
UTF-32, big-endian
FF FE 00 00
UTF-32, little-endian
FE FF
UTF-16, big-endian
FF FE
UTF-16, little-endian
EF BB BF
UTF-8
空
ANSI
例如,汉字的“中”的各种编码如下:
用Windows的写字板软件将这个汉字分别保存成UTF-8、ANSI、Unicode编码,在UltraEdit打开其16进制模式查看,就可以看到如下的输出:
这里ANSI其实保存的是GBK码(使用GBK代码页936),Unicode其实保存的是UTF16编码。试图以ANSI保存文件时,对超出了GBK编码范围的汉字,Windows写字板软件会提示包含Unicode文字,以ANSI保存会丢失数据,如下图:
对于ANSI保存的包含汉字的文件,代码页信息并不在文件里。并不是所有的文本编辑器和文字处理代码都能正确解析这样的文件,因为它们并不知道代码页。这是可能造成文件中文乱码的一个原因。
另外,前面提到通过BOM可以确定文件编码方式,但并不是所有的文件都使用了BOM。因此特定的文本编辑器和文字处理代码对中文都可能产生显示乱码。
2.2 HTTP
对于HTTP消息内容,包括SOAP、RESTful,浏览器/客户端和服务器怎么知道字符编码呢?
HTTP头的Content-Type可以通过参数charset指定文字编码。例如:
Content-Type: text/html; charset = UTF-8
如果没有正确配置charset,就可能产生乱码。例如网页表单提交的中文数据,服务器没有正确解码从而产生乱码。
不同的Web服务器都可以设置默认的charset,例如Apache可以修改httpd.conf文件,通过配置AddDefaultCharset指定默认字符集编码。
2.3 XML
XML文件当然可以使用前面提到的文件编码方式设置,以HTTP传递的XML数据也可以使用HTTP的文字编码设置。同时,XML规范自己也定义了文字编码声明的方式,从而保证通过任何方式传递(例如TCP)的XML都可以被正确解析。
XML定义的编码方式是设置encoding属性,例如:
<?xml version="1.0" encoding="UTF-8"?>
注意,JSON并没有声明文字编码的设置。通常JSON数据都是通过HTTP传递的,因此使用HTTP的文字编码设置。
2.4 数据库连接
数据库连接也是造成文字乱码的一个重灾区。客户端从数据库服务器获取数据、向数据库服务器提交数据,怎么知道数据的文字编码呢?
和数据库相关的文字编码有2部分:数据库内码、数据库连接使用的字符编码。
数据库内码:
由于Unicode码历史并不悠久,数据库管理系统的历史远早于Unicode,直到最近这20年,数据库厂商才开始支持Unicode内码。当然,每个厂商的Unicode内码编码也不一样,例如InterSystems的Caché和IRIS是UTF-16格式,而Oracle是UTF-8和UTF-16。因为UTF-8处理效率低,大多数Unicode数据库都使用UTF-16。现在依然能看到不是Unicode内码的老版本数据库。
很多数据库相关的字符编码问题和数据库内码设置有关。例如国内不少的Oracle安装时没设置过字符编码,数据库内码NLS_CHARACTERSET默认为US7ASCII,客户端字符集NLS_LANG默认为AMERICAN_AMERICA.US7ASCII。这并不会造成中文无法保存,因为数据库只是将客户端的数据逐位保存下来,无论什么编码。但这样保存的中文数据只有Oracle自己的客户端能正常显示,其它的客户端可能就按ASCII处理从而造成乱码。因此正确设置数据库的内码很关键。
数据库连接使用的字符编码:
ODBC:
直到ODBC 3.5标准(1997年)之前绝大多数的ODBC连接的函数调用和字符串编码都是ANSI(单字节或双字节),所以中文生僻字大多都会处理异常。
ODBC 3.5 规定ODBC驱动管理器要能够透明地处理Unicode和ANSI之间的转换,从而让ANSI和Unicode的数据库客户端都可以正确地向数据库获取和提交数据。
当然,并不是所有的Unicode字符都能转为ANSI码,例如中文“”字,所以ANSI的数据库客户端依然会遇到生僻字乱码。
JDBC:
Java对字符串使用UTF-16编码,如果数据库内码也使用UTF-16,那么不会有乱码问题,例如InterSystems IRIS。但很多数据库并非使用UTF-16,这些数据库的JDBC驱动就需要支持UTF-16和数据库内码之间的转换。通常可以通过设置JDBC的连接字符串的特定属性来实现。
例如mysql的连接字符串中指定characterEncoding:
jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
其它连接方式:
另外,很多数据库还提供XDBC之外的连接方式,这些连接方式有自己的字符编码逻辑。
2.5 HL7 V2
在医疗行业,HL7 V2是目前最为广泛使用的消息交换标准。它可以通过文件、TCP、HTTP、SOAP 等多种通道交换。因此,HL7 V2消息标准中,也有设置消息字符编码的字段:MSH-18。正确设置该字段有助于不同的系统正确地处理HL7 V2消息里的字符数据。
例如下面的HL7消息在MSH段设置其字符编码为UTF-8:
MSH|^~\&||PHLS|||||ORU^R01|||2.5|||AL|||UTF-8|
PID||S2345|S2345^^^PHLS^MR|C9876^^^COR^XX~S45008787^^^MA^DL|张^三||19301019|M|||1 Memorial Drive^^剑桥市^MA^02142||||||||063070516
PV1||O|||||ISCGP001^建国^李|||||||EO|||||HSVN00008|||||||||||||||||||||||||20200912090700|20200912090700
ORC||00265-001|0606:H00550R||||^^^202009120910||202009120910|||ISCGP001^建国^李|PHLS||||||||PHLS||||||||LAB
OBR||00265-001|0606:H00550R|CBCD^血常规^L|||202009121049|||||||202009120937|Blood|ISCGP001^Moore^James||||||202009121227|||F
OBX||NM|WBC^WHITE BLOOD CELL COUNT||6.24|10(9)/L|4.0-10.6||||F|||202009121049
OBX||NM|RBC^RED BLOOD CELL COUNT||4.99|10x12/L|4.5-5.9||||F|||202009121049
OBX||NM|HGB^HEMOGLOBIN||13.6|g/dL|12.0-16.0||||F|||202009121049
OBX||NM|HCT^HEMATOCRIT||41.6|Percent|36.0-46.0||||F|||202009121049
2.6 其它
在上面提到的和字符集相关的乱码之外,有时我们会混淆一些其它的、并非真正乱码的情况。下面这些中文显示的“乱码”,并非乱码:
URL编码:
根据RFC 3986,如果URL的路径中有URL的保留字,就需要对URL路径中的保留字使用转义符% 进行转码,也叫做百分号编码。例如ASCII中下面的字符都需要转义:
!
#
$
&
'
(
)
*
+
,
/
:
;
=
?
@
[
]
%21
%23
%24
%26
%27
%28
%29
%2A
%2B
%2C
%2F
%3A
%3B
%3D
%3F
%40
%5B
%5D
对于非ASCII码字符,按其UTF-8编码字节顺序加%转义。例如“https://cn.community.intersystems.com/post/多语言字符集系列文章-第一篇-多语言字符集和相关标准简史”会被转义为:
https://cn.community.intersystems.com/post/%E5%A4%9A%E8%AF%AD%E8%A8%80%E5%AD%97%E7%AC%A6%E9%9B%86%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0-%E7%AC%AC%E4%B8%80%E7%AF%87-%E5%A4%9A%E8%AF%AD%E8%A8%80%E5%AD%97%E7%AC%A6%E9%9B%86%E5%92%8C%E7%9B%B8%E5%85%B3%E6%A0%87%E5%87%86%E7%AE%80%E5%8F%B2
其中%E5%A4%9A就是“多”字的UTF-8编码E5A49A的3个字节加上%。
在InterSystems技术平台上,可以用$ZCVT("%E5%A4%9A","I","URI")将URL编码翻译为中文字符,或使用$ZCVT("多","O","URI")将汉字进行URL编码。
HTML实体编码(转义):
HTML实体编码(HTML Entity Encode)将字符编码为&开头;结尾的字符串,例如"<"编码为"<"。而中文字符就会被按其unicode编码进行转义,例如“广”字的unicode 10进制码为24191,所以它的HTML实体编码是"广"。在InterSystems技术平台上,可以用$ZCVT("广","I","HTML")将其翻译为中文字符。
Base64 编码:
当使用SOAP传递的数据是一个XML字符串或其它应被视为二进制类型的数据时,通常使用Base64对其进行编码,从而不会破坏XML结构解析。所以它也不是乱码。
文章
Jingwei Wang · 十二月 23, 2021
正如你在我的简介中看到的,我在一所大学教书,我想分享我对IRIS(或之前的Caché)教学的看法。
已经有一段时间了,但我还记得在今年早些时候看到YURI MARX GOMES关于 "用InterSystems对象和SQL开发 "一系列课程。他对第1天、第2天和第3天的课程内容进行了简要的描述,并附有讲师Joel Solon的评论。我心想,分享我自己的经验可能会有趣。
在我真正写下我教给学生的东西之前,先简单介绍一下我的经历。
在我毕业拿到硕士学位回到大学工作后,我们系决定更新我们的课程,在普通课程中增加几门新课程。其中一门是 "后关系型数据库"。它是为研究生第一年的学生讲授的。开始时,它包括72小时的讲座和72小时的实践。现在变为秋季学期72小时的讲座和36小时的实践。
由于我是一名新员工,而且是一名年轻有为的员工,我被赋予了讲授这门新学科的职责。我感到惊讶和惊恐是不言而喻的。首先,我根本没有任何教学方面的实践经验。其次,我只有夏天的三个月时间来学习一项对我来说完全陌生的技术并准备课程的讲授。幸运的是,我已经知道了应该教授哪个数据库。这个数据库就是InterSystems Caché。
总之,我或多或少地准备好了,然后我亲爱的学生们的问题开始了。例如,为什么他们必须学习这个数据库,他们在哪里以及如何使用这个数据库,等等。由于当时是2010年,我还很年轻,没有经验,而且这方面的书也不多,我决定直接去找源头,即InterSystems。不知怎的,我最终与Solon先生交谈,他给了我一些很好的提示,还把我介绍给了Evgeny Shvarov。从那时起,一切都变得更加容易和清晰了。
在接下来的几年里,我根据现代的趋势和要求,对我的课程内容做了相当多的改变。在开始的时候,为了展示如何从其他应用程序中利用这个数据库,我使用了Java绑定和.NET管理提供者。后来出现了eXTreme for Java,然后是eXTreme for .NET。ZEN在一两年后被教授和遗忘。现在是RESTful和SOAP服务以及CSP。很多东西都变了,但核心部分几乎是一样的。
既然我们在这里讨论的是讲授硕士水平的课程,我有很多的期望。首先,我希望我的学生知道面向对象的范式,并且能够绘制正确的UML类图。其次,我希望他们了解关系型数据库(包括索引、键等)和SQL。最后,他们应该至少有一个简单的网络开发的知识。
考虑到所有的先决条件,以下是我的教学大纲的大纲:
第1节 后关系型数据库概述:面向对象,对象关系型,以及不同种类的NoSQL数据库,并有模型和例子的描述。多模型数据库和例子。
第2节 架构、结构和管理IRIS的基础知识,作为后关系型DBMS的示例,IRIS管理的基础知识 : 数据库和命名空间一起工作。语法、命令、变量、表达式,以及ObjectScript的一些功能。还有用户代码、异常和事务。
第3节 分层模型 : 列表和多维数组(以及globals)。处理列表和数组的函数。这里是第一个任务--设计一个有4个下标层次的global,并使用至少4个函数与数组一起工作,对数据进行处理。
第4节 对象模型: 类,分层,继承,参数,不同类型的属性(以及如何使用它们),不同类型的方法(使用计算属性的例子,单元测试,使用Populate创建测试数据,用户数据类型),参数(以及如何使用它们)。数据是如何存储的,以及如何设置存储。这里的任务包括设计一个具有不同类型属性的类图(引用、内置对象、关系、集合和流);创建这些类和每个类的几个对象,并在IDE中把所有东西连接起来;创建一个计算属性、一个用户数据类型、单元测试以检查所有约束条件是否工作(如最小或最大长度/值、必需属性、唯一属性等)和生成测试数据。
第5节 关系模型:对象模型和关系模型之间的相关性。嵌入式(简单语句和游标)和动态SQL。类查询(基于SQL和COS)。隐式连接。IDKEY与其附带的方法。这里的任务是将不同类型的SQL查询通过join和参数添加到之前的任务中的类中,创建一个触发器,将唯一的属性改为IDKEY,看看它是如何改变相关的globals的。
第6节。从IRIS外部访问数据 :CSP和关于它的一切。RESTful服务和客户端(处理JSON的类)。SOAP服务和客户端(处理XML的类)。这里的任务是从之前的任务中选择一个类,并创建一个CSP页面,列出所选类的所有对象,并给出编辑、创建和删除对象的功能。然后使用RESTful和SOAP服务做同样的事情(创建方法来返回所有对象、一个特定的对象、更新和删除一个特定的对象)。
课程安排就这样了。然而,现在有了嵌入式Python,我可能会改用它,因为所有关于ObjectScript语法的问题和评论都很扰乱我的神经。
当然,COVID-19和学校的封锁给整个工作方式带来了一些变化。我认为,这些变化是最好的。而且,这真的取决于事情的组织方式。例如,其中一个好处是,现在我可以向学生展示知道我在整个学期所讲的所有内容的有用性。
一般来说,在学期末,他们有一份作业,来检查他们对课程主要部分的知识的掌握。以前,他们在课堂上写作业,我可以监督他们。之后,由于大流行病的发生非常意外,我不得不随机应变,为每个学生提供一套独特的任务。不幸的是,在那个时候,没有现成的软件,所以我决定写一个简单的网络门户来处理这个作业。由于现在已经过去了将近两年,我的简单门户变成了一个RESTful服务,从不同的集合中随机选择问题,将它们分配给学生,并接收答案。所有这些都非常现代化,而且易于使用(和编写) .
现在轮到了我现在的学生来写这个作业(使这个社区里有一些乐趣),你可以参加测试并发送你的答案, 当然,问题将与我的学生的问题不同(而且更容易)。我将使用InterSystems学习实验室的服务器,所以你只有几天的时间来做这件事并检查你的知识。
我在问题中使用的类图如下:
要获得这些问题,只需向以下地址发送一个带有你姓名的GET请求:
http://52773-1-e5a0b608.labs.learning.intersystems.com/community/task/%3CYour_name>
你就会得到你的4套问题:
要发送答案,只需向以下地址发送一个POST请求:
http://52773-1-e5a0b608.labs.learning.intersystems.com/community/answer/<Your_name>
在body中填写一个JSON:
{
"Answer1": "answer 1",
"Answer2": "answer 2",
"Answer3": "answer 3",
"Answer4": "answer 4"
}
我会在某个时候检查你的答案(因为有这个讨厌的东西叫做时差),你可以通过向以下地址发送GET请求来获得你的结果:
http://52773-1-e5a0b608.labs.learning.intersystems.com/controlwork/marks/<Your_name>
如果分数是空的,说明我还没有时间去检查(或者我还没有起床)。分数如下。
0 分意味着答案是完全错误的。
1 分意味着答案或多或少是正确的。
2 分意味着答案是正确的。
第一个发送答案的人将得到我的一个虚拟拥抱(或者一个真正的拥抱,如果我们见面的话)
总之,如果你对我的课程有任何意见或问题,不要犹豫,请在评论区写出来。
查看原帖 由 @Iryna.Mykhailova 撰写 第1天、第2天和第3天的课程,这三篇文章都有中文的,请换成中文社区链接,谢谢!
文章
姚 鑫 · 二月 19, 2021
# 第四十一章 Caché 变量大全 $ZSTORAGE 变量
包含进程的最大可用内存。
# 大纲
```
$ZSTORAGE
$ZS
```
# 描述
`$ZSTORAGE`包含`JOB`的进程私有内存的最大内存量(以`KB`为单位)。此内存可用于局部变量、堆栈和其他表。此内存限制不包括例程目标代码的空间。此内存根据需要分配给进程,例如在分配数组时。
一旦将此内存分配给进程,通常在该进程退出之前不会释放它。但是,当大量内存被使用(例如,大于32MB)然后被释放时,系统间IRIS会尝试在可能的情况下将释放的内存释放回操作系统。
还可以使用`$ZSTORAGE`设置最大内存大小。例如,以下语句将作业的最大进程专用内存设置为`524288 KB`:
```java
SET $ZSTORAGE=524288
```
更改`$ZSTORAGE`会更改`$STORAGE`特殊变量的初始值,该变量包含进程的当前可用内存(以字节为单位)。
`$ZSTORAGE`的最大值为`2147483647`。`$ZSTORAGE`默认值为`262144`。`$ZSTORAGE`的最小值为`128`。`$ZSTORAGE`值大于最大值或小于最小值会自动默认为最大值或最小值。`$ZSTORAGE`设置为整数值;InterSystems IRIS截断任何小数部分(如果指定)。
可以通过更改最大每进程内存(KB)系统配置设置来更改`$ZSTORAGE`默认值。在管理门户中,依次选择System Administration、Configuration、Systtem Configuration、Memory和Startup。可以根据需要增加每个进程的最大内存(KB),最大为2147483647 KB。更改每个进程的最大内存(KB)会更改后续启动的进程的`$ZSTORAGE`值;对当前进程的`$ZSTORAGE`值没有影响。


# 示例
以下示例将`$ZSTORAGE`设置为其最大值和最小值。尝试将`$ZSTORAGE`设置为小于最小值的值(16)时,会自动将`$ZSTORAGE`设置为其最小值(128):
```java
/// d ##class(PHA.TEST.SpecialVariables).ZS()
ClassMethod ZS()
{
SET $ZS=128
WRITE "minimum storage=",$ZS,!
SET $ZS=16
WRITE "less than minimum storage=",$ZS,!
SET $ZS=2147483647
WRITE "maximum storage=",$ZS,!
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZS()
minimum storage=128
less than minimum storage=128
maximum storage=2147483647
```
文章
Claire Zheng · 八月 17, 2021
什么是互联互通?我们所说的互联互通其实就是国际上的互操作性,HIMSS对于互操作性定义的是:不同的信息系统、设备、应用系统之间、程序之间,在机构区域和国家边界之内,以及跨机构、区域和国家边界,以协调的方式来访问交换集成和协作使用数据的能力。
它对于使用效果提出了一些目标,比如希望能够提供及时和无缝的信息的可移植性,和优化个人和人群的健康。这是它定义的对于互操作性的4个不同的级别:
基础级别就比较简单,仅仅打通了系统之间进行数据通讯的通道;
结构级别,在基础级别之上定义了数据交换的格式和语法;
再上面有语义级别,语义级别是建立在行业通用的数据模型和数据编码之上的,使用标准化的行业语义来定义数据元素,使用标准的值集,因此语义级别的互操作性才是全行业可以理解的,并且有确定的行业意义的这种互操作,也就是说语义级别的互操作才是基于标准的互操作;
在语义级别之上,还有组织级别的互操作。通常这些都是由国家、国际的行业协会、行业标准开发组织开发的,加入了很多的其他方面的一些考虑,比如说政策的、社会的、法律的,分析了通用的业务流程和工作流,在这个基础之上设定了参与互操作的各方的角色、权限、服务、知情同意策略等等。
我国目前的互联互通标准化成熟度测试,其实就是组织级别的互操作。
注:本文根据InterSystems中国技术总监乔鹏演讲整理而成。
文章
Claire Zheng · 八月 17, 2021
在医疗行业要实现互操作,应该要达到语义级别。只有达到语义级别才能保障医疗信息的准确和医疗行为的安全。而要达到语义级别,我们需要基于标准。
这里标准特别多,我们把它称之为五位一体。标准有5个方面。
首先是词汇/术语标准,是相互通讯的健康医疗信息系统需要依赖于结构化的词汇术语、代码值集、分类系统来进行相应的表述。词汇和术语的标准就是表达健康概念的标准。例如我们在使用的世界卫生组织的ICD-10,当然还有其他的,比如SNOMED-CT等等。
第二是内容标准,是描述信息交换过程中间的数据内容和结构的标准。它还包括了通用数据的定义,例如我们熟悉的HL7 CDA、 HL7 V2、 C-CDA,这些都是内容标准。
第三是传输标准,定义了计算机系统、文档架构、临床模板、用户界面,数据的连接之间的交换的消息的格式,传输的方式等等。通过传输方式,可以来确定是通过推、还是拉的方式来进行数据的共享交换。在这个层面DICOM 、IHE都是传输的一个标准。
第四是隐私与安全的标准,也就是说我们要确定谁、什么时候、出于什么目的、可以使用哪种个人健康信息的权利,以及我们如何来保护这些健康信息的机密性、可用性、完整性的一些标准。美国有HIPPA,欧洲的GDPR,这些都是关于隐私和安全的。
第五是标识符的标准,标识符的标准是用来唯一标识患者、机构、医护技、设备等等的标准,例如说我们互联互通里面用到的OID。
注:本文根据InterSystems中国技术总监乔鹏演讲整理而成。
问题
j ay · 三月 22, 2023
1、默认samples空间没有Backup.General无法进行外部备份
2、%SYS空间有Backup.General
Backup.General怎么在其他空间里创建这个类 我没明白问题, 外部备份是在%sys命名空间执行的呀, 为什么要在在sample里执行备份呢? 1、我对这个cache不太理解,第一次做这个,我目前做的外部备份是:
前提:所有数据都是用sample命名空间进行添加的
a. 用sample命名空间,去连接cache,调用Backup.General ExternalFreeze进行冻结
b. 备份mgr目录下面sample数据库文件CACHE.DAT
c. 调用ExternalThaw解冻
问题:1.如果我用%SYS命名空间去冻结解冻,备份mgr目录下面sample数据库文件CACHE.DAT,数据是恢复不了的
2.日志journal备份后在重新写入恢复,cache会启动不了
我不知道这样的备份恢复流程对不对,能说一下正确的外部备份流程吗 冻结其实是控制系统的写进程,让它暂时挂起来,在数据库日志上做个标记。这些都不是那一个数据库比如sample的事情,是整个系统的操作。
你的问题1: 如果我用%SYS命名空间去冻结解冻,备份mgr目录下面sample数据库文件CACHE.DAT,数据是恢复不了的。 怎么恢复不了?那一步出错了?
问题2: 你是先恢复数据库, 再恢复日志的是吗?出的什么错误?
这样, 你先看看这两个链接, 如果在有问题, 我找我们的专家和你私聊。第2个帖子有点长,里面连冻结解冻的脚本都包括了, 很详细 ,:)
https://cn.community.intersystems.com/post/faq-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E7%B3%BB%E5%88%97-%E7%B3%BB%E7%BB%9F%E7%AE%A1%E7%90%86%E7%AF%87-%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A4%87%E4%BB%BD
https://community.intersystems.com/post/intersystems-data-platforms-and-performance-%E2%80%93-vm-backups-and-cach%C3%A9-freezethaw-scripts
文章
姚 鑫 · 二月 14
# 第二十四章 R 开头的术语
### 结果集 (result set)
**对象(Objects)**
结果集包含查询返回的数据。可以使用查询接口处理结果集。
### 角色 (role)
**系统**
`InterSystems` 安全系统中的实体,可以接收执行操作和查看或更改数据的能力。这些能力以特权的形式授予角色。当用户成为角色的成员时,它会获得角色的特权。
### 角色分配机制 (role-assignment mechanism)
**系统**
将各种角色赋予用户以使用 `IRI`S 的方式。角色分配可以使用 `IRIS` 原生机制、`LDAP` 或委托(即用户定义)代码进行。这是授权过程的一部分。
### 回滚 (roll back)
**系统**
在系统故障后从 `IRIS` 数据库中移除不完整事务的过程。`IRIS` 在其恢复程序中移除这些更改。请参见日志记录、事务。
### 前滚 (roll forward)
**系统**
在系统故障发生并从备份重建数据库时,将日志化的更改重新应用到数据库的过程。请参见日志记录。
### 根类 (root class)
**对象(Objects)**
范围的根类是其层次结构中的顶层类。每个类都是其自身范围的根类。然而,根类通常指类层次结构的主要持久超类,特别是在数据存储的上下文中。
### 例程 (routine)
**系统**
在 `IRIS` 中,`ObjectScript` 程序。例程存储在 `IRIS` 数据库中。
### 例程缓冲区 (routine buffers)
**系统**
允许例程使用的虚拟内存量。
### 行 (row)
**SQL**
描述关系表域中实体的相关字段值的组。例如,在 `Customers` 表中,一行描述一个单一客户。传统数据处理术语中也称为记录(`record`),关系数据库术语中称为元组(`tuple`)。
### 行 ID (Row ID)
**对象(Objects)**
行 `ID` 唯一地标识表中的一行。如果表是从`IRIS` 类投射的,行 `ID` 是等效对象的 `ID`(自动生成或由 `ID Key` 指定)。
### 行规范 (row specification)
**对象(Objects)**
类查询的行规范提供有关该查询返回的字段的信息。
文章
Louis Lu · 十二月 25, 2024
IRIS 2024.2 版本包含一项变更(DP-432503),该变更要求Web Gateway最初连接到 IRIS 时所使用的用户(通常是 CSPSystem)必须对承载 REST web applicatioon 的dispatch类的数据库具备 READ 权限。
在不满足该条件的情况下,就会引发一个错误,这会向调用者返回一个 HTTP 404 状态码,而非预期的 HTTP 401 状态码。
显然,这个问题将在 2024.3 版本中修复,参考编号为 DP-432898 / ALI048:REST 登录端点将返回 401 HTTP 错误码而非 404,但作为持续交付(CD)版本,2024.2 不会获得维护版本修正。
解决方法是让 CSPSystem 用户对安装WebTerminal所在命名空间的数据库具备读权限。
下面是所需的步骤:
1. 创建一个新的资源 “%DB_WEBTERMINAL”,并设置 “WEBTERMINAL” 数据库使用该资源,而非 “%DB_% DEFAULT”。
2. 创建一个名为 “% DB_WEBTERMINAL” 的角色,该角色能让角色持有者对 “% DB_WEBTERMINAL” 资源具有读写(RW)访问权限。
3. 再创建另一个角色(我将其命名为 “DBread_WEBTERMINAL”),该角色仅赋予角色持有者对该资源的读(R)访问权限。
4. 为 “CSPSystem” 用户赋予 “DBread_WEBTERMINAL” 角色。这样可以解决 2024.2 版本存在的这个漏洞。
5. 编辑 “/terminalsocket” web application,并将 “%DB_WEBTERMINAL” 添加到 “应用程序角色(Application Roles)” 选项卡中。这一步骤是必要的,因为WebTerminal 最初是以 “UnknownUser” 身份运行其 websocket 进程的,并且甚至在切换为以已认证用户身份运行之前,就需要更新其数据库中的状态信息。
另一种简便单但不是安全的方式:
创建一个具有读写(RW)公共权限的新安全资源 “%DB_WEBTERMINAL”,然后设置 “WEBTERMINAL” 数据库使用该资源,而非 “%DB_%DEFAULT”。
更多以及详细信息参见下面内容:https://github.com/intersystems-community/webterminal/issues/155 我在那个webterminal 退出了terminal,下次登录进来还是退出状态,我应该怎么重新进入 麻烦上传Terminal屏幕截图,我们看一下是什么状态。
文章
姚 鑫 · 二月 27, 2021
# 第四十九章 Caché 变量大全 ^$ROUTINE 变量
提供例程信息。
# 大纲
```java
^$|nspace|ROUTINE(routine_name)
^$|nspace|R(routine_name)
```
## 参数
- `|nspace|`或`[nspace]` 可选-扩展SSVN引用,可以是显式名称空间名称,也可以是隐含名称空间。必须计算为带引号的字符串,该字符串括在方括号(`[“nspace”]`)或竖线(`|“nspace”|`)中。命名空间名称不区分大小写;它们以大写字母存储和显示。
- routine_name 计算结果为包含例程名称的字符串的表达式。
# 描述
可以将`^$ROUTINE`结构化系统变量用作`$DATA`、`$ORDER`和`$QUERY`函数的参数,以从当前命名空间(默认)或指定命名空间返回例程信息。`^$ROUTINE`返回有关例程的OBJ代码版本的例程信息。
在InterSystems ObjectScript中,一个例程有三个代码版本:MAC(用户编写的代码,可能包括宏预处理器语句)、INT(编译的MAC代码,用于执行宏预处理)和OBJ(可执行目标代码)。可以使用`^$ROUTINE global`返回关于int代码版本的信息。可以使用`^$ROUTINE`返回有关OBJ代码版本的信息。
# 参数
## nspace
此可选参数允许使用扩展SSVN引用在另一个命名空间中指定全局。可以显式地将命名空间名称指定为带引号的字符串文字或变量,也可以通过指定隐含的命名空间来指定。命名空间名称不区分大小写。可以使用方括号语法`[“user”]`或环境语法`|“user”|`。Nspace分隔符前后不允许有空格。
```java
WRITE ##class(%SYS.Namespace).Exists("USER"),! ; an existing namespace
WRITE ##class(%SYS.Namespace).Exists("LOSER") ; a non-existent namespace
```
可以使用`$NAMESPACE`特殊变量来确定当前名称空间。更改当前名称空间的首选方式是新建`$NAMESPACE`,然后设置`$NAMESPACE=“nspace ename”`。
## routine_name
计算结果为包含现有例程名称的字符串的表达式。例程名称在前255个字符内必须是唯一的;应避免超过220个字符。
# 示例
以下示例使用`^$`例程作为`$DATA`、`$ORDER`和`$QUERY`函数的参数。
## 作为$DATA的参数
`$DATA(^$|nspace|ROUTINE(routine_name))`
`^$ROUTINE`作为`$DATA`的参数将返回一个整数值,该整数值指定例程名OBJ代码版本是否作为`^$ROUTINE`中的节点存在。下表显示了`$DATA`可以返回的整数值。
Value| Meaning
---|---
0 |例程不存在
10 |例程存在
下面的Terminal示例测试myrou例程的OBJ代码版本是否存在。此示例假定在`USER`名称空间中有一个名为myrou的已编译MAC例程:
```java
USER>WRITE ^ROUTINE("myrou",0,"GENERATED") // INT code version exists
1
USER>WRITE $DATA(^$ROUTINE("myrou")) // OBJ code version exists
1
USER>KILL ^rOBJ("myrou") // Kills the OBJ code version
USER>DO ^myrou
DO ^myrou
^
*myrou
USER>WRITE ^ROUTINE("myrou",0,"GENERATED") // INT code version exists
1
USER>WRITE $DATA(^$ROUTINE("myrou")) // OBJ code version does not exist
0
USER>
```
## 作为`$ORDER`的参数
`$ORDER(^$|nspace|ROUTINE( routine_name),direction)`
`^$ROUTINE`作为`$ORDER`的参数,按整理顺序返回指定的例程名称的下一个或上一个例程名称。如果在`^$ROUTINE`中没有这样的例程名称作为节点存在,则`$ORDER`返回空字符串。
direction参数指定是否返回下一个或上一个例程名称:1 =下一个,-1 =上一个。如果不提供方向参数,则InterSystems IRIS将按整理顺序将下一个例程名称返回到指定的名称。
以下子例程搜索USER名称空间,并将例程名称存储在名为ROUTINE的本地数组中。
```java
/// d ##class(PHA.TEST.SpecialVariables).ROUTINE()
ClassMethod ROUTINE()
{
SET rname=""
FOR I=1:1 {
SET rname=$ORDER(^$|"USER"|ROUTINE(rname))
QUIT:rname=""
SET ROUTINE(I)=rname
WRITE !,"Routine name: ",rname
}
WRITE !,"All routines stored"
QUIT
}
```
```java
Routine name: INFORMATION.SCHEMA.TABLECONSTRAINTS.1
Routine name: INFORMATION.SCHEMA.TABLES.0
Routine name: INFORMATION.SCHEMA.TABLES.1
Routine name: INFORMATION.SCHEMA.TRIGGERS.0
Routine name: INFORMATION.SCHEMA.TRIGGERS.1
Routine name: INFORMATION.SCHEMA.VIEWCOLUMNUSAGE.0
Routine name: INFORMATION.SCHEMA.VIEWCOLUMNUSAGE.1
Routine name: INFORMATION.SCHEMA.VIEWS.0
Routine name: INFORMATION.SCHEMA.VIEWS.1
Routine name: INFORMATION.SCHEMA.VIEWTABLEUSAGE.0
Routine name: INFORMATION.SCHEMA.VIEWTABLEUSAGE.1
All routines stored
```
## 作为$QUERY的参数
`$QUERY(^$|nspace|ROUTINE(routine_name))`
`^$ROUTINE`作为`$QUERY`的参数,按整理顺序将下一个例程名称返回到指定的例程名称。指定的例程名称不必存在。如果以后在排序序列中没有例程名称,则`$QUERY(^$ROUTINE)`返回一个空字符串。
在下面的示例中,两个`$QUERY`函数在`USER`名称空间中指定例程名称之后返回下一个例程。
```java
/// d ##class(PHA.TEST.SpecialVariables).ROUTINE1()
ClassMethod ROUTINE1()
{
SET rname=""
WRITE !,"1st routine: ",$QUERY(^$|"USER"|ROUTINE(rname))
SET rname="%m"
WRITE !,"1st ",rname, " routine: ",$QUERY(^$|"USER"|ROUTINE(rname))
QUIT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).ROUTINE1()
1st routine: ^$|"USER"|ROUTINE("%APILIB")
1st %m routine: ^$|"USER"|ROUTINE("%mgw1")
``` 能够理解总结知识,但是结合到场景不知道如何使用。 知识讲的很仔细全面,GET了
文章
姚 鑫 · 七月 22, 2021
# 第九章 触发器定义
描述触发器定义的结构。
# 介绍
触发器是在SQL中发生特定事件时执行的代码段。InterSystems IRIS支持基于执行`INSERT`、`UPDATE`和`DELETE`命令的触发器。根据触发器定义,指定的代码将在相关命令执行之前或之后立即执行。每个事件可以有多个触发器,只要它们被分配了执行顺序。
可以向持久类添加触发器定义。它们在其他类中没有意义。
# 详情
触发器定义具有以下结构:
```java
/// description
Trigger name [ keyword_list ]
{ implementation }
```
- `description` 描述(可选)旨在显示在“类参考”中。默认情况下,描述为空白。
- `name`(必需)是触发器的名称。这必须是有效的类成员名称,并且不能与任何其他类成员名称冲突。
- `keyword_list`(必需)是以逗号分隔的关键字列表,用于进一步定义触发器。
- `implementation` 实现(必需)是零行或多行ObjectScript代码,用于定义触发触发器时要执行的代码。
# 示例
```java
/// 此触发器在每次插入后更新日志表
Trigger LogEvent [ Event = INSERT, Time = AFTER ]
{
// 获取插入行的行id
NEW id
SET id = {ID}
// 将值插入日志表
&sql(INSERT INTO LogTable (TableName, IDValue) VALUES ('MyApp.Person', :id))
}
```
# 第十章 扩展数据块
描述XData块的结构。
# 介绍
`XData`块是包含在类定义中的命名数据单元,通常由类中的方法使用。最常见的情况是,它是一个XML文档,但是它可以由其他形式的数据组成,例如`JSON`或`YAML`。
# 详情
`XData`块具有以下结构:
```java
/// description
XData name [ keyword_list ]
{
data
}
```
- `description` 描述(可选)旨在显示在“类别参考”中。默认情况下,描述为空白。
- `name`(必需)是`XData`块的名称。这必须是有效的类成员名称,并且不能与任何其他类成员名称冲突。
- `data` 数据(可选)包含扩展数据块的有效载荷。如果是XML,则它必须是格式良好的文档(只有一个根元素),开头没有XML声明。
- `keyword_list`(可选)是以逗号分隔的关键字列表,进一步定义了`XData`块。如果省略此列表,也要省略方括号。
# 示例
```java
Class Demo.CoffeeMakerRESTServer Extends %CSP.REST
{
Parameter HandleCorsRequest = 1
XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
}
```
# 第十一章 类关键字 - Abstract
指定这是否是抽象类。
# 用法
要将类标记为抽象类,请使用以下语法:
```java
Class MyApp.MyClass [ Abstract ]
{ //class members }
```
否则,省略此关键字或将单词Not放在关键字的前面。
# 详解
如果一个类是抽象的,就不能创建它的实例。
# 对子类的影响
此关键字不是继承的。
# 默认
如果省略这个关键字,这个类就不是抽象的。
# 第十二章 类关键字 - ClassType
指定此类的类型(或行为)。
# 用法
要指定类的类型(如果需要),请使用以下语法:
```java
Class MyApp.MyClass [ ClassType = classtype ]
{ //class members }
```
其中`classtype`是下列之一:
- `datatype` — 该类是一个数据类型类,用于表示文字值。
- `persistent` 持久—该类表示要存储在数据库中的数据。
- `serial` —该类表示要存储在另一个持久对象中(处于序列化状态)的数据。
- `stream` —该类表示流数据。
- `view`视图—该类用于定义一个SQL视图。
- `index` —该类是一个索引类,一个定义索引接口的专用类。
- 空字符串,表示此类没有特定类型。抽象类通常不指定类类型。
如果未指定此关键字,则类类型从主超类继承(如果有)。
请注意,`ClassType`是为`%RegisteredObject`、`%SerialObject`、`%Persistent`和数据类型类等系统类指定的,因此如果对这些类进行子类化,通常不需要指定此关键字。
# 详解
此关键字指定如何使用此类。类别编译器使用类别类型关键字来决定如何编译类别。例如,如果`ClassType`是持久性的,则类编译器还会调用存储编译器来为类生成持久性代码。除非明确定义,否则`ClassType`的值要么是默认值,要么是从主超类继承而来的。
对于持久性类,只有在标准持久性行为被重写时,才需要显式的`ClassType`语句。如果一个类定义包含这样的语句,要么是因为开发人员指定了它,要么是因为这个类起源于用旧版本的InterSystems IRIS开发的代码。
# 对子类的影响
这个关键字是从主超类继承的。子类可以覆盖关键字的值。
# 默认
如果省略此关键字,类类型将从主超类继承(如果有)。
注意:分片类的类类型不能有持久以外的任何值。
文章
Claire Zheng · 三月 14, 2023
本文根据InterSystems中国技术总监乔鹏( @Peng.Qiao )的演讲“互联互通套件赋能数据利用与应用创新”整理而成。
IRIS医疗版互联互通套件的缘起与发展演进
来源HL7:正在到来的挑战
http://hl7.org/fhir/change.html
这是来自HL7官网上的一张图,描述了我们在医疗卫生行业面临的一些挑战,以及信息化建设在应对挑战中发挥的作用。当今,医疗卫生、生物学、信息技术有很强的融合趋势,加之社会变革带来的经济方面的需求,同时构成颠覆传统医疗卫生行业的因素。
这张图显示了从“被动医疗”转向“主动医疗”过程中信息的爆炸式增长,信息共享交换推动了我们对信息的利用,在这一进程中,医疗卫生信息化起着核心作用——而让信息更具价值,赋予信息标准化和互操作能力的过程,这也是InterSystems一直努力的方向,我们在国内支持大量医院实现了互联互通建设。在建设过程中,我们注意到项目的定量部分的建设成本占比是比较高的,很多的工作都花在了合规性和相关管理工具的开发上——应用标准的实施是有成本的,而对于标准的理解在各个项目上水平不尽相同,这就进一步影响了互联互通项目的建设成果。有鉴于此,2021年我们发布了InterSystems IRIS医疗版互联互通套件1.0版,初衷在于将我国的互联互通建设标准放到我们自己的技术平台里,为用户提供开箱即用的、标准合规性的基础和相应工具——这套工具包括了我们的医疗卫生信息模型、文档模型与管理、互联互通服务接口等,从而可以降低用户的实施成本和相应的实施风险。
在互联互通套件1.0版,我们提供了这样一些能力:
· 在医疗卫生信息模型部分,我们提供了完整的卫生数据元与值集管理、数据集管理,并且提供了本地术语注册,以及针对于本地术语和标准术语之间的转换服务。
· 在文档部分,我们提供了电子病历文档模型和模型管理能力、电子病历文档的校验、数据源质量的分析,以及自动生成合规的互联互通文档。
· 在服务部分,我们提供了互联互通标准服务接口、互联互通标准消息模型、ESB服务总线,以及通用的服务发布/订阅功能。
我们知道,互联互通评测的目标之一就是“以评促用”,实现合规仅仅是第一步,我们希望可以为用户提供更多能力,让用户可以真正把符合互联互通标准的医院信息平台应用起来。于是我们继续发布了InterSystems IRIS医疗版互联互通套件2.0版。
传统数据中心建设面临的一个普遍问题是标准缺乏,导致数据中心的建设成本相对较高,而且复用性很差,很难形成生态。因为缺乏标准,所以数据治理、上面可以运行的数据应用,大多都是由数据厂商自己开发的,这就像早期不同的手机厂商提供的功能手机,就那么几个应用可以用,它不是智能手机,功能是非常有限的。
所以2.0版强调的是生态。在规划2.0版的时候,我们将重点放在了核心数据资产上——我们希望在2.0中建设、治理、保存核心数据资产,助力用户应用好数据资产,并且通过基于标准的开放能力,推动医疗卫生信息化的生态建设。
我们在2.0版打通了国内互联互通和FHIR两大生态,希望通过这种生态融合提升数据资产的价值。我们借助FHIR来治理数据资产,令互联互通消息和互联互通文档都能够转换成FHIR资源,直接保存在FHIR资源仓库里面。
我们使用FHIR资源模型来提供统一的行业语义,通过FHIR API,包括InterSystems提供的互联互通服务,向用户提供统一的、基于行业语义的互操作能力,这样一来,用户能够以生态的方式来扩展数据资产的利用,体现数据资产的核心价值。
我们也注意到,行业中的确已经有很多关于微服务架构的思考。在2.0版本上,我们还扩展了服务架构,新增了对新的应用开发架构——微服务架构——的支持,这对于微服务架构在行业中的落地肯定是有推动作用的。
此外,我们增加了全面的API网关能力,用户可以通过 API网关的形式,对访问互联互通的医院信息平台的这些客户端来进行流量控制、认证管理等,实现服务的全生命周期管理。
在2.0版中,我们还提供了一个完整的FHIR服务器,以打通FHIR生态与国内互联互通生态。在决策支持上,FHIR生态已经提供决策支持架构和利用机器学习的一些用例,我们也基于FHIR为决策者提供完整的、关联的、实时的决策要素,比如说通过底层互操作架构来提供整个流程闭环的基础,通过内置的机器学习、自然语言处理、商业智能等工具,为用户提供数据决策支持工具。
在平台监控方面,2.0版新增了互联互通监控指标,以及针对通用的互操作性的指标监控能力。
在实际工作中,我们注意到互联互通项目建成之后,有一些项目的建设成果并没有得到全面地利用。例如,根据最近一位北京专家的调研结果,很多用户在电子健康档案的相关项目建设上面花费了巨额经费,但是在数据调阅频次上面却“低得出人意料”——我想这是相当一部分项目的现状。
互联互通建设的成果是我们的医院信息平台,我们是以医院互联互通为标准来建设医院信息平台的,那么医院信息平台上的数据利用应该是我们的核心。
因此在3.0的版本里,我们希望以最佳实践的方式来展现如何充分利用我们的互联互通的生态,提升互联互通价值,通过向用户提供一些用例,抛砖引玉地引导用户来正确使用互联互通建设成果,真正发挥出数据的价值。我们规划了两类互联互通应用方向:一个是数据利用,另一个是应用创新。
数据利用可以有很多方向,例如数字孪生,我们可以通过更完整、更实时的数据来建立准确、实时的患者画像;例如数据编织(data fabric),我们可以把所有数据来源进行统一编织,建立一个跨数据来源的、统一的语义平台;例如在数据流通领域发挥核心价值。
在应用创新上,借助打通FHIR和互联互通发展生态,我们可以利用FHIR应用能力来建设互联互通的应用生态。例如在FHIR生态里有Smart on FHIR,这是一个全新的、即插即用的软件开发的架构;再如基于FHIR生态的CDS Hooks决策支持架构等。这些都是我们可以来参考、引进和学习的。
我借助两则近期的新闻来介绍一下如何利用互联互通建设生态来满足数据利用和应用创新。
“数据二十条”:打破数据垄断、实现数据要素价值
2022年12月19日,国务院发布了《中共中央 国务院关于构建数据基础制度更好发挥数据要素作用的意见》(以下简称“《意见》”)。
这份意见为数据流通奠定了一个政策基础。简单梳理一下这份重要的意见里面关键词可以发现其核心就是打破数据垄断、实现数据要素价值。
关键词梳理,依据《中共中央国务院关于构建数据基础制度更好发挥数据要素作用的意见》
在实现方法上,我们就要重视对数据质量标准化体系的建设,基于信息安全和标准化的数据采集,整合互联互通和互操作性,《意见》在实现方法上面提出了一些非常重要的观点,如原始数据不出域,数据可用不可见等等这样的一些关键点,都是我们未来在医疗卫生信息数据流通上的指导思想和指导原则。
那么互联互通套件如何来支持这种政策要求?
在互联互通套件2.0架构里面,我们治理后的医疗卫生信息保存在FHIR资源仓库里。FHIR这种对象模型非常适合表达医疗卫生的复杂数据模型,但是对类似数据分析上报这些传统应用来说,通常会使用基于SQL的数据分析和操作工具,如果使用FHIR资源,操作起来是很难的。此外,要让用户直接使用FHIR资源仓库,也不符合《意见》提到的“原始数据不出域”的要求。
所以互联互通套件提供了FHIR SQL构建器,这是一个图形化的工具,用户可以将FHIR资源按需求映射成不同的SQL表——例如数据上报需要用到数据的一部分内容,可能内部数据用户对于数据需求范围是比较大的,外部用户对于数据需求范围可能就会更窄一些——不管是用户需求是什么样的,都可以通过映射的方式提供相应的SQL表。这种方式既满足了数据用户,使他们能够通过熟悉的工具(SQL)获得数据和操作数据,同时也满足了“原始数据不出域”的要求。
另外需要强调的一点是,SQL映射不是创建一份数据拷贝。它并没有把FHIR资源仓库原始数据拷贝给用户,而是通过映射的方式,映射出来一份虚拟SQL表。用户可以以SQL的访问方式来操作它——通过这种模式提供的数据是实时的、完整的、按需提供的。
Epic的生态创新:以标准为基础,推动应用建设的专业化
互联互通赋能应用创新这部分,面临着很多基于标准的问题和限制。
以美国为例,美国国家卫生IT协调员办公室研究发现,只有29%的用户能够将外部来源的卫生信息和应用整合到自己的电子健康档案中,也就是说美国是没有实现“应用通”的,只有不到1/3的电子健康档案是可以使用别的厂商开发的应用的。
2022年12月还有一个非常重要的新闻,12月9日的这则新闻来自全球电子病历领头羊Epic,Epic发布了名为“Connection Hub”的平台,这个平台向所有开发者开放。
这意味着什么?
我们来看看Epic目前拥有的能力。作为一个电子病历厂商,Epic有自己的应用市场,类似于智能手机的应用市场。
截至2022年12月22日,这个应用市场里共有619个应用,这些应用都是不同厂商开发的,这些应用都可以作为一个即插即用的应用直接下载并安装在Epic的电子健康档案系统上,所以这是一个非常有意思的事情。
作为行业里最重要的电子病历厂商之一,Epic对这些应用的开发趋势,包括它持有的这种开放的、应用的生态理念,我相信会为行业应用创新带来很多价值。
回顾一下我们面临的现状,我们的数据中心是缺乏标准的,每个厂商的数据中心都是不同的数据模型,提供厂商自定义的数据接口,所以当基于这些数据来进行应用开发的时候,我们做的是低水平重复建设和无法复用的应用建设。
例如,几乎每个项目的数据中心产品里都会部署一个患者360视图或类似应用,但这个应用每个厂商、甚至每个项目都会重新开发一次,所以这是一个低水平重复建设的典型应用。而且即便觉得好,如果换一个数据中心产品,这个应用也无法直接移植过去。
在缺乏标准的情况下,这些数据中心及相关的应用建设变得非常昂贵。如果我们整个行业能够采用一个标准的语义来建设数据中心或者电子健康档案,那么通过提供基于行业统一语义的数据和基于标准的数据接口,就可以让应用开发厂商只需关注在业务,潜心开发高水平应用。而且基于生态标准的应用可以运行在任何基于标准的数据中心(或电子健康档案)上,这就为释放数据应用价值奠定了非常好的基础。
回到前面这则新闻,Epic的标准是什么?Epic采用了FHIR标准,它基于最新的R4开放了55个资源和450个FHIR API,这样一来,它就可以运行类似于“儿童生长发育曲线”这类的Smart on FHIR应用,加速应用创新。
Epic能做到的,我相信我们也可以借鉴,InterSystems IRIS医疗版互联互通套件已经提供了针对FHIR仓库的能力,并且打通了互联互通与FHIR生态。
InterSystems IRIS医疗版
互联互通套件能力
以互联互通为基础,让医院信息平台成为数字化转型的核心
我想借助这张IDC于2019年发布的“数字化转型平台”示意图作为结尾。这张图是一张概念图,描述了我们数字化转型平台的功能,以及通过无限循环的路径来摆脱传统的技术栈的思维模式。
它将数字化转型平台定义为一个加速企业数字化转型的技术架构,用来支撑我们快速创建面向市场的数字化的服务和体验的平台。同时通过它来积极推动机构内部IT环境的现代化,使它成为将数据运用于业务的智能核心——基于互联互通将数据、算法、代码和模型进行资产化管理、整理、复用和共享。
数字化转型已经颠覆了很多行业,我相信医疗卫生信息行业也不会例外。我们希望通过互联互通套件,以互联互通为基础,让医院信息平台成为医疗卫生行业数字化转型的智能核心。
文章
Weiwei Gu · 五月 4, 2023
我们客户的一个共同需求是配置 HealthShare HealthConnect 和 IRIS的高可用性模式。
市场上的其他集成引擎通常被宣传为具有“高可用性”配置,但事实并非如此。通常,这些解决方案与外部数据库一起使用,因此,如果这些数据库未配置为高可用性,当发生数据库崩溃或与它的连接丢失时,整个集成工具将变得不可用。
对于 InterSystems 解决方案,这个问题不存在,因为数据库是工具本身的一部分和核心。 InterSystems 如何解决高可用性问题?深奥的配置会把我们拖入异化和疯狂的漩涡?不!在 InterSystems,我们倾听并处理了您的投诉(正如我们一直努力做的那样 ;)),并且我们已将镜像功能提供给所有用户和开发人员。
镜像
镜像如何工作?这个概念本身非常简单。如您所知,IRIS 和 HealthShare 都使用一个日志系统,该系统记录每个实例的数据库上的所有更新操作。这个日志系统是后来帮助我们在崩溃后恢复实例而不会丢失数据的系统。好吧,这些日志文件在镜像中配置的实例之间发送,允许并保持镜像中配置的实例永久更新。
架构
让我们简要解释一下在 Mirror 中配置的系统架构是什么样的:
在故障转移模式下配置的两个实例:
主动节点——接收所有常规的读/写操作。
被动节点:在读取模式下,它同步接收主动节点产生的任何变化。
0-14个异步实例:你可以使用多个异步实例,它们可以是两种类型:
DR 异步(灾难恢复):处于读取模式的节点不是故障转移的一部分,尽管它们可以被手动提升成故障转移节点。如果是这样,它们可以在其他两个故障转移节点发生故障时自动提升为主节点。您的数据更新是异步的,所以不能保证其数据是最新的。
报告异步:异步更新节点,用于 BI 任务或数据挖掘。它们不能升级为故障转移,因为可以对数据执行写入。
ISCAgent:安装在每个实例所在的服务器上。它将负责监视所述服务器实例的状态。这是镜像服务器之间除了直接通信之外的另一种通信方式。
Arbiter:它是一个独立于构成镜像的服务器安装的 ISCAgent,并允许通过监视安装的 ISCAgent 和 IRIS/HealthShare 实例来提高安全性和内部故障转移的控制。它的安装不是必须的。
这是一个由只有两个节点的故障转移形成的镜像的操作:
先前的警告
与本文相关的项目没有允许配置镜像的活动许可证。如果你想尝试,直接给我发邮件或者在文末添加评论,我会联系你。
在 Docker 中部署
对于本文,我们将在 Docker 中建立一个小项目,允许我们设置 2 个故障转移实例和一个Arbiter 。默认情况下,可用于 Docker 的 IRIS 映像已经安装并配置了 ISCAgent,因此我们可以跳过该步骤。有必要配置与来自 Visual Studio Code 的文章关联的项目,因为这将使我们以后可以更轻松地使用服务器文件。
让我们看看我们的 docker-compose.yml 会有什么形式:
version: '3.3'
services:
arbiter:
container_name: arbiter
hostname: arbiter
image: containers.intersystems.com/intersystems/arbiter:2022.1.0.209.0
init: true
command:
- /usr/local/etc/irissys/startISCAgent.sh 2188
mirrorA:
image: containers.intersystems.com/intersystems/iris:2022.1.0.209.0
container_name: mirrorA
depends_on:
- arbiter
ports:
- "52775:52773"
volumes:
- ./sharedA:/shared
- ./install:/install
- ./management:/management
command:
--check-caps false
--key /install/iris.key
-a /install/installer.sh
environment:
- ISC_DATA_DIRECTORY=/shared/durable
hostname: mirrorA
mirrorB:
image: containers.intersystems.com/intersystems/iris:2022.1.0.209.0
container_name: mirrorB
depends_on:
- arbiter
- mirrorA
ports:
- "52776:52773"
volumes:
- ./sharedB:/shared
- ./install:/install
- ./management:/management
command:
--check-caps false
--key /install/iris.key
-a /install/installer.sh
environment:
- ISC_DATA_DIRECTORY=/shared/durable
hostname: mirrorB
可以看到我们定义了3个容器:
Arbiter :它对应于将被部署以控制将形成 Mirror Failover 的 IRIS 实例的 ISCAgent(即使图像称为 Arbiter)。当启动容器时,它将执行一个 shell 文件,该文件将启动侦听容器端口 2188 的 ISCAgent。
mirrorA :将部署 IRIS v.2022.1.0.209 映像的容器,稍后我们将其配置为主故障转移节点。
mirrorB :将部署 IRIS v.2022.1.0.209 镜像的容器,稍后我们将其配置为辅助故障转移节点。
当我们执行docker-compose up -d命令时,定义的容器将部署在我们的 Docker 中,它在我们的 Docker 桌面中应该看起来像这样(如果我们从 Windows 执行此操作)。
镜像配置。
部署容器后,我们将继续访问我们将在镜像中配置的实例,第一个将在端口 52775 ( mirrorA ) 上侦听,第二个在 52776 ( mirrorB ) 上侦听。访问用户和密码将是superuser/ SYS
由于实例部署在 Docker 中,我们将有两个选项来配置我们服务器的 IP。第一种是在配置中直接使用我们容器的名称(这是最简单的方法)或检查 Docker 为每个容器分配的 IP(打开控制台并执行返回分配的 IP 的 ifconfig)。为了清楚起见,我们将在示例中使用我们为每个容器指定的名称作为 Docker 中每个容器的地址。
首先,我们将配置我们将用作故障转移活动节点的实例。在我们的例子中,它将是我们所说的mirrorA 。
第一步是启用镜像服务,因此我们将从管理门户访问镜像菜单: System Administration --> Configuration --> Mirror Settings --> Enable Mirror Service并标记Service Enabled检查:
启用服务后,我们可以开始配置我们的活动节点。启用该服务后,您将能够在镜像菜单中看到新选项已启用:
在这种情况下,由于我们还没有创建任何镜像配置,我们必须使用Create Mirror选项创建一个新配置。当我们访问这个选项时,管理门户将打开一个新窗口,我们可以从中配置我们的镜像:
让我们仔细看看每个选项:
Mirror Name :我们将用来标识我们的镜像的名称。对于我们的示例,我们将其称为 MIRRORSET
需要 SSL/TLS :对于我们的示例,我们不会使用 SSL/TLS 配置连接,尽管在生产环境中,防止在实例之间没有任何类型的加密的情况下共享日志文件会比操作方便更重要。如果您有兴趣配置它,您可以在文档的以下URL中获得所有必要的信息。
使用 Arbiter :此选项不是强制性的,但强烈推荐,因为它为我们的镜像配置增加了一层安全性。对于我们的示例,我们将选中它并指示我们运行 Arbiter 的 IP。对于我们的示例,IP 将位于容器名称arbiter中。
用户虚拟 IP :在 Linux/Unix 环境中,此选项非常有趣,因为它允许我们为将由我们的镜像管理的活动节点配置虚拟 IP。此虚拟 IP 必须与故障转移节点属于同一子网。虚拟IP的操作非常简单,当主动节点出现故障时镜像会自动在待提升的被动节点所在的服务器上配置虚拟IP。这样,被动节点到主动节点的升级对用户来说将是完全透明的,因为他们将继续连接到同一个 IP,即使它将配置在不同的服务器上。如果您想了解有关虚拟 IP 的更多信息,可以查看文档的此URL 。
其余的配置可以保持原样。在屏幕右侧我们会看到镜像中这个节点的相关信息:
Mirror Member Name :这个镜像成员的名称,默认情况下它将采用服务器的名称以及实例的名称。
超级服务器地址:这个节点的超级服务器 IP 地址,在我们的例子中是mirrorA 。
Agent Port :配置了该节点对应的ISCAgent的端口。默认为2188 。
配置必要的字段后,我们可以继续保存镜像。我们可以从镜像监视器(系统操作-->镜像监视器)检查配置情况。
完美,这里我们有了新配置的镜像。如您所见,只有我们刚刚创建的活动节点出现。很好,接下来让我们在故障转移中添加我们的被动节点。我们访问mirrorB管理门户并访问镜像设置菜单。正如我们已经为mirrorA实例所做的那样,我们必须启用镜像服务。我们重复该操作,一旦菜单选项更新,我们将选择Join as Failover 。
这里我们有镜像连接屏幕。让我们简要解释一下每个字段的含义:
镜像名称:我们在创建时为镜像指定的名称,在我们的示例中为MIRRORSET 。
Agent Address on Other System :部署主动节点ISCAgent的服务器IP,对我们来说就是mirrorA。
代理端口:我们创建镜像的服务器的ISCAgent监听端口。默认为2188 。
InterSystems IRIS 实例名称:主动节点上 IRIS 实例的名称。在这种情况下,它与被动节点IRIS的一致。
保存镜像数据后,我们将可以选择定义与我们正在配置的被动节点相关的信息。我们再来看看被动节点可以配置的字段:
镜像成员名称:被动节点将在镜像中使用的名称。默认情况下由服务器名称和实例组成。
超级服务器地址:被动节点中超级服务器的 IP 地址。在这种情况下mirrorB 。
代理端口:我们配置的被动节点服务器上安装的ISCAgent的监听端口。默认为2188 。
SSL/TLS 要求:在此示例中不可配置,我们不使用 SSL/TLS。
镜像私有地址:被动节点的 IP 地址。正如我们所见,在使用 Docker 时,我们可以使用容器名称mirrorB 。
代理地址:安装 ISCAgent 的服务器的 IP 地址。和以前一样, mirrorB 。
我们按照指示保存配置,然后返回到镜像监视器以验证我们是否已正确配置所有内容。我们可以将mirrorA中的主动节点和mirrorB中的被动节点的监视器可视化。让我们看看这两个实例之间的差异。
活动节点mirrorA上的镜像监视器:
被动节点mirrorB上的镜像监视器:
如您所见,显示的信息是相似的,基本上改变了故障转移成员的顺序。选项也不同,让我们看看其中的一些:
主动节点mirrorA :
设置无故障转移:防止在作为其中一部分的任何实例停止的情况下执行故障转移。
降级其他成员:从镜像配置中删除其他故障转移成员(在本例中为mirrorB )。
被动节点mirrorB :
Stop Mirror On This Member :停止故障转移被动节点上的镜像同步。
降级为 DR 成员:将此节点从其实时同步故障转移的一部分降级为异步模式下的灾难恢复模式。
完美,我们已经配置了节点,现在让我们看看配置的最后一步。我们必须决定哪些表将成为镜像的一部分,并在两个节点上进行配置。如果您查看与本文相关的 Open Exchange 项目的 README.md,您将看到我们配置和部署了两个通常用于训练的应用程序。当我们启动 Docker 容器时,这些应用程序会自动部署,默认情况下会创建 NAMESPACES 和数据库。
第一个应用程序是COMPANY ,它允许我们保存公司记录,第二个应用程序是PHONEBOOK ,它允许我们添加与注册公司以及客户相关的个人联系人。
让我们添加一家公司:
现在让我们为之前的公司创建个人联系人:
公司数据将在COMPANY数据库中注册,联系人数据在PERSONAL中,这两个数据库都已映射,以便可以从命名空间 PHONEBOOK 访问它们。如果我们检查两个节点中的表,我们将看到在mirrorA中我们有公司和联系人的数据,但在mirrorB中仍然没有任何数据,这是合乎逻辑的。
mirrorA注册的公司:
好的,让我们继续在我们的镜像上配置数据库。为此,从我们的活动节点 ( mirrorA ),我们访问本地数据库管理屏幕(系统管理员-->配置-->系统配置-->本地数据库)并单击添加到镜像选项,我们必须选择从列表中我们要添加的所有数据库并从屏幕上读取消息:
一旦我们将数据库从主动节点添加到镜像中,我们必须对其进行备份或复制数据库文件 (IRIS.dat) 并将它们还原到被动节点上。如果您决定直接复制 IRIS.dat 文件,请记住您必须冻结要复制的数据库中的写入,您可以在文档的以下URL中查看必要的命令。在我们的例子中,没有必要暂停,因为除了我们之外没有人在写入数据。
在复制数据库文件之前,让我们从活动节点的监视器上检查镜像的状态:
让我们看看被动节点:
正如我们所看到的,从被动节点我们被告知,虽然我们在镜像中配置了 3 个数据库,但配置尚未完成。让我们继续将数据库从主动节点复制到被动节点,不要忘记我们必须卸载被动节点的数据库才能进行复制,为此我们将从管理门户访问系统配置—— >数据库并访问它们中的每一个,我们继续卸载它们。
完美的!卸载的数据库。让我们从 Visual Studio Code 访问与本文相关的项目代码,看看我们有 IRIS 安装所在的文件夹, sharedA用于mirrorA和sharedB用于mirrorB 。让我们访问 COMPANY、CUSTOMER 和 PERSONAL 数据库所在的文件夹 ( /sharedA/durable/mgr ),然后继续将镜像中每个数据库的 IRIS.dat 复制到 mirrorB 的相应目录 ( /sharedB/durable/mgr) ).
复制完成后,我们再次挂载mirrorB数据库并从mirrorB中的镜像监视器检查已配置数据库的状态:
答对了!我们的镜像已经识别出数据库,现在我们只需要激活和更新它们。为此,我们将点击Activate操作,然后点击Catchup ,这将在激活后出现。让我们看看他们是如何结束的:
完美,我们的数据库已经在镜像中正确配置,如果我们查询 COMPANY 数据库,我们应该看到我们之前从mirrorA注册的记录:
显然我们的COMPANY数据库中有我们之前在mirrorA中输入的记录,毕竟我们已经复制了整个数据库。让我们继续从mirrorA添加一个新公司,我们将其称为“另一家公司”,然后再次查询 COMPANY 数据库表:
在这里,我们完成了。我们只需要确保我们在镜像中配置的数据库对于被动节点mirrorB处于只读模式:
他们在那里!是只读模式R 。好吧,我们已经配置了镜像并同步了数据库。如果我们的产品正在运行,这不会成为问题,因为镜像会自动负责管理它们,并在主动节点出现故障时在被动节点中启动它们。
非常感谢大家走到这一步!它很长,但我希望你觉得它有用。