清除过滤器
文章
姚 鑫 · 八月 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
大家好!
这是关于使用 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."
```
希望大家喜欢这篇文章。 欢迎您发表评论。
文章
姚 鑫 · 十一月 6, 2021
# 第六十八章 SQL命令 SAVEPOINT
在事务中标记一个点。
# 大纲
```java
SAVEPOINT pointname
```
## 参数
- `pointname` - 保存点的名称,指定为标识符。
# 描述
`SAVEPOINT`语句标记事务中的一个点。建立保存点使能够执行事务回滚到保存点,撤消在此期间完成的所有工作并释放在此期间获得的所有锁。在长期运行的事务或具有内部控制结构的事务中,通常希望能够回滚事务的一部分,而不撤消在事务期间提交的所有工作。
保存点的建立会递增`$TLEVEL`事务级别计数器。回滚到保存点会将`$TLEVEL`事务级别计数器递减到紧接在保存点之前的值。可以在一个事务内建立最多`255`个保存点。超过这个保存点数量会导致`SQLCODE-400`致命错误,这是在SQL执行期间捕获的`` 异常。终端提示符将当前事务级别显示为提示符的`TLn:`前缀,其中`n`是介于`1`和`255`之间的整数,表示当前$TLEVEL计数。
每个保存点都与一个保存点名称相关联,这是一个唯一的标识符。保存点名称不区分大小写。保存点名称可以是分隔的标识符。
- 如果指定的保存点没有点名,或者指定的点名不是有效的标识符或SQL保留字,则会发出运行时`SQLCODE-301`错误。
- 如果指定点名称以`“SYS”`开头的保存点,则会发出运行时`SQLCODE-302`错误。这些保存点名称是保留的。
保存点名称不区分大小写;因此`resetpt`,` ResetPt`和`“RESETPT”`是相同的点名。此重复项是在回滚到保存点期间检测到的,而不是在保存点期间检测到的。当指定具有重复点名的`SAVEPOINT`语句时, IRIS会递增事务级别计数器,就像点名是唯一的一样。但是,最近的点名称会覆盖保存点名称表中所有先前重复的值。因此,当指定回滚到保存点点名时, IRIS会回滚到具有该点名称的最近建立的保存点,并相应地递减事务级别计数器。但是,如果再次指定回滚到同名的保存点点名,则会生成`SQLCODE-375`错误,并显示`%msg:Cannot Rollback to Unestabled SavePoint‘name’`,整个事务将回滚,`$TLEVEL`计数恢复为`0`。
## 使用保存点
嵌入式SQL、动态SQL、ODBC和JDBC支持`SAVEPOINT`语句。在`JDBC`中,`connection.setSavepoint(Pointname)`设置一个保存点,`connection.roll back(Pointname)`回滚到指定的保存点。
如果已建立保存点,请执行以下操作:
- 回滚到保存点点名将回滚自指定保存点以来所做的工作,删除该保存点和所有中间保存点,并将`$TLEVEL`事务级别计数器递减删除的保存点数量。如果`pointname`不存在或已经回滚,此命令将回滚整个事务,将`$TLEVEL`重置为`0`,并释放所有锁。
- 回滚回滚当前事务期间完成的所有工作,回滚自`START TRANSACTION`以来完成的工作。它将`$TLEVEL`事务级别计数器重置为零,并释放所有锁。请注意,常规回滚会忽略保存点。
- `COMMIT`提交在当前事务期间完成的所有工作。它将`$TLEVEL`事务级别计数器重置为零,并释放所有锁。请注意,提交操作会忽略保存点。
在事务内发出第二个`START TRANSACTION`对保存点或`$TLEVEL`事务级别计数器没有影响。
如果事务操作未能成功完成,则会发出`SQLCODE-400`错误。
# 示例
以下嵌入式`SQL`示例创建具有两个保存点的事务:
```java
ClassMethod Savepoint()
{
n SQLCODE,%ROWCOUNT,%ROWID
&sql(
START TRANSACTION
)
&sql(
DELETE FROM Sample.Person WHERE Name = NULL
)
if SQLCODE = 100 {
w !,"没有要删除的空名称记录"
} elseif SQLCODE '= 0 {
&sql(ROLLBACK)
} else {
w !,%ROWCOUNT," 已删除Null Name记录"
}
&sql(
SAVEPOINT svpt_age1
)
&sql(
DELETE FROM Sample.Person WHERE Age = NULL
)
if SQLCODE = 100 {
w !,"没有要删除的空年龄记录"
} elseif SQLCODE '= 0 {
&sql(ROLLBACK TO SAVEPOINT svpt_age1)
} else {
w !,%ROWCOUNT," 删除空年龄记录"
}
&sql(
SAVEPOINT svpt_age2
)
&sql(
DELETE FROM Sample.Person WHERE Age > 65
)
if SQLCODE = 0 {
&sql(COMMIT)
} elseif SQLCODE = 100 {
&sql(COMMIT)
} else {
&sql(
ROLLBACK TO SAVEPOINT svpt_age2
)
w !,"退休年龄删除失败"
}
&sql(COMMIT)
&sql(COMMIT)
}
```
# ObjectScript和SQL事务
**使用`TSTART`和`TCOMMIT`的`ObjectScript`事务处理与使用SQL语句`START transaction`、`SAVEPOINT`和`COMMIT`的SQL事务处理不同,也不兼容。
ObjectScript和InterSystems SQL都提供了对嵌套事务的有限支持。
ObjectScript事务处理不与`SQL`锁控制变量交互;
特别需要关注的是`SQL`锁升级变量。
应用程序不应该尝试混合这两种事务处理类型。**
如果事务涉及`SQL`更新语句,则事务应该由`SQL START transaction`语句启动,并使用`SQL COMMIT`语句提交。
使用`TSTART/TCOMMIT`嵌套的方法可以包含在事务中,只要它们不初始化事务。
方法和存储过程通常不应该使用SQL事务控制语句,除非按照设计,它们是事务的主控制器。
文章
姚 鑫 · 六月 20, 2021
# 第十三章 将XML文档表示为DOM
`%XML.Document`类和`%XML.Node`类使可以将任意XML文档表示为DOM(文档对象模型)。然后,可以导航此对象并对其进行修改。还可以创建一个新的DOM并将其添加到其中。
**注意:使用的任何XML文档的XML声明都应该指明该文档的字符编码,并且文档应该按照声明的方式进行编码。如果未声明字符编码,InterSystems IRIS将使用本书前面的“输入和输出的字符编码”中描述的默认值。如果这些默认值不正确,请修改XML声明,使其指定实际使用的字符集。**
# 将XML文档作为DOM打开
要打开现有XML文档以用作DOM,请执行以下操作:
1. 创建`%XML.Reader`的实例。
2. 也可以指定此实例的`Format`属性,以指定要导入的文件的格式。
**默认情况下, IRIS假定XML文件为文字格式。如果文件是SOAP编码格式,则必须指明这一点,以便可以正确读取该文件。**
除非使用`Correlate()`和`Next()`,否则此属性无效。
3. 请使用`%XML.Reader`的以下方法之一。
- `OpenFile()` — 打开一个文件。
- `OpenStream()` —打开一个流。
- `OpenString()` — 打开字符串。
- `OpenURL()` — 打开URL。
在每种情况下,都可以选择为该方法指定第二个参数,以重写`Format`属性的值。
4. 访问`Document`属性,它是一个DOM。此属性是`%XML.Document`实例,它提供了可用于查找有关整个文档的信息的方法。例如,`CountNamespace()`返回DOM使用的名称空间总数。
或者,如果流包含XML文档,调用`%XML.Document`的`GetDocumentFromStream()`方法。返回`%XML.Document`的实例。
## 示例1:将文件转换为DOM
例如,下面的方法读取一个XML文件,并在表示该文档的返回`%XML.Document`的一个实例:
```java
ClassMethod GetXMLDocFromFile(file) As %XML.Document
{
s reader = ##class(%XML.Reader).%New()
s status = reader.OpenFile(file)
if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF}
s document = reader.Document
q document
}
```
## 示例2:将对象转换为DOM
以下方法接受`OREF`,并在表示该对象中返回`%XML.Document`的实例。该方法假定`OREF`是启用XML的类的实例:
```java
ClassMethod GetXMLDoc(object) As %XML.Document
{
//确保这是启用XML的类的实例
if '$IsObject(object){
w "参数不是对象"
q $$$NULLOREF
}
s classname = $CLASSNAME(object)
s isxml = $CLASSMETHOD(classname,"%Extends","%XML.Adaptor")
if 'isxml {
w "参数不是启用XML的类的实例"
q $$$NULLOREF
}
//步骤1-将对象作为XML写入流
s writer = ##class(%XML.Writer).%New()
s stream = ##class(%GlobalCharacterStream).%New()
s status = writer.OutputToStream(stream)
if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF}
s status = writer.RootObject(object)
if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF}
//步骤2-从流中提取%XML.Document
s status = ##class(%XML.Document).GetDocumentFromStream(stream,.document)
if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF}
quit document
}
```
# 获取DOM的名称空间
当 IRIS读取XML文档并创建DOM时,它会标识文档中使用的所有名称空间,并为每个名称空间分配一个索引号。
在`%XML.Document`实例提供了以下方法,可以使用这些方法查找有关文档中命名空间的信息:
### CountNamespace()
返回文档中的命名空间数。
### FindNamespace()
返回与给定命名空间对应的索引。
### GetNamespace()
返回给定索引的XML命名空间URI。
下面的示例方法显示一个报表,其中显示文档中使用的命名空间:
```java
ClassMethod ShowNamespaces(doc As %XML.Document)
{
s count = doc.CountNamespace()
w !, "文档中的命名空间数: "_count
for i = 1 : 1 : count {
w !, "Namespace "_i_" is "_doc.GetNamespace(i)
}
}
```
# 导航DOM的节点
要访问文档的节点,可以使用两种不同的技术:
- 使用`%XML.Document`实例的`GetNode()`方法。此方法接受一个整数,它指示从1开始的节点号。
- 调用`%XML.Document`实例的`GetDocumentElement()`方法。
此方法返回`%XML.Node`的实例,提供用于访问有关根节点的信息以及移动到其他节点的属性和方法。以下小节提供了有关使用`%XML.Node`的详细信息。
## 移动到子节点或同级节点
要移动到子节点或同级节点,请使用`%XML.Node`实例的以下方法。:
- `MoveToFirstChild()`
- `MoveToLastChild()`
- `MoveToNextSibling()`
- `MoveToPreviousSibling()`
这些方法中的每一个都移动到另一个节点(如方法名称所示)。如果是,则该方法返回TRUE。如果不是,则返回False,焦点与调用该方法之前相同。
这些方法中的每一个都有一个可选参数`skipWhitespace`。如果此参数为真,则该方法将忽略任何空格。`SkipWhitespace`的默认值为false。
## 移动到父节点
要移动到当前节点的父节点,请使用`%XML.Node`实例的`MoveToParent()`方法。
此方法接受一个可选参数`restrictDocumentNode`。如果此参数为真,则该方法不会移动到文档节点(根)。`restrictDocumentNode`的默认值为False。
## 移动到特定节点
要移动到特定节点,可以设置`%XML.Node`实例的`NodeId`属性。例如:
```java
set saveNode = node.NodeId
//..... lots of processing
//...
// restore position
set node.NodeId=saveNode
```
## 使用id属性
在某些情况下,XML文档可能包括名为`id`的属性,该属性用于标识文档中的不同节点。例如:
```java
Jack O'Neill
Samantha Carter
Daniel Jackson
```
如果(如本例所示)文档使用名为`id`的属性,则可以使用它导航到该节点。为此,可以使用文档的`GetNodeById()`方法,该方法返回`%XML.Node`的一个实例。(请注意,与大多数其他导航方法不同,此方法可从`%XML.Document`,而不是`%XML.Node`。)
文章
姚 鑫 · 七月 12, 2021
# 第一章 查询目录和驱动器
`%Library.File`(简称`%File`)为处理文件和目录提供了广泛的API。本文将介绍该API的主要功能。有关属性、方法和查询的规范列表,请参见类参考。
注意:如果指定了部分文件名或目录名,这些方法中的大多数都引用的项相对于包含正在使用的命名空间的默认全局数据库的目录。该目录在本文中称为“默认目录”。这条规则的任何例外都在文章中注明。
此外,仅当基础操作系统将文件名和目录名视为区分大小写时,这些方法才会将文件名或目录名视为区分大小写。也就是说,文件或目录名在Unix上区分大小写,但在Windows上不区分大小写。
# 查询目录和驱动器
## 列出目录的内容
`FileSet`类查询列出目录的内容。此查询按顺序接受以下参数:
1. `directory` — 指定要检查的目录的名称。
2. `wildcards` 通配符 — 指定要匹配的文件名模式(如果有)。
3. `sortby` 排序依据 — 指定如何对结果进行排序。使用以下值之一:
- `Name` 名称—文件的名称(默认)
- `Type` 类型—项目类型
- `DateCreated` 创建日期—创建文件的日期和时间
- `DateModified` 日期修改—文件上次修改的日期和时间
- `Size` 大小—文件大小
4. `includedirs` —指定如何处理给定目录中的目录。如果此参数为真(1),查询将返回任何文件之前的所有目录,并且目录名忽略通配符参数。如果此参数为false (0),通配符参数适用于文件和目录。默认值为0。
5. `delimiter` 分隔符—指定通配符参数中通配符之间的分隔符。默认值为;
此查询返回的结果集提供了以下字段:
- `Name` 名称—项目的完整路径名。
- `Type` 类型—项目的类型:`F`表示文件,`D`表示目录,`S`表示符号链接。
- `Size` 大小—文件大小,以字节为单位。对于目录和符号链接,此字段为空。
- `DateCreated` 创建日期—创建项目时的日期和时间,格式为`yyyy-mm-dd hh:mm:ss`。
- `DateModified` 日期修改—上次修改项目的日期和时间,格式为`yyyy-mm-dd hh:mm:ss`。
- `ItemName` 项目名称—项目的简称。对于文件,这是单独的文件名,没有目录。对于目录,这只是目录路径的最后一部分。
注意:Windows是目前唯一跟踪实际创建日期的平台。其他平台存储最后一次文件状态更改的日期。
下面是一个使用这个类查询的简单示例:
```java
/// desc:查看目标路径所有文件。
/// w ##class(Demo.FileDemo).ShowDir("C:\InterSystems\Cache\mgr", "*.log", "Size")
/// w ##class(Demo.FileDemo).ShowDir("E:\temp", "*.xml", "Size")
ClassMethod ShowDir(dir As %String = "", wildcard As %String = "", sort As %String = "Name")
{
s stmt = ##class(%SQL.Statement).%New()
s status = stmt.%PrepareClassQuery("%File", "FileSet")
if $$$ISERR(status) {
do $system.OBJ.DisplayError(status)
quit
}
s resultSet = stmt.%Execute(dir, wildcard, sort)
while resultSet.%Next() {
w !, resultSet.%Get("Name")
w " ", resultSet.%Get("Type")
w " ", resultSet.%Get("Size")
}
q ""
}
```
从终端对指定目录运行此方法,筛选日志文件,并按文件大小排序,结果如下所示:
```java
DHC-APP> w ##class(Demo.FileDemo).ShowDir("E:\temp", "*.xml", "Size")
E:\temp\testPerson.xml F 117
E:\temp\samplePerson.xml F 327
E:\temp\xmlnewtest.xml F 351
E:\temp\Person.xml F 259854
E:\temp\tempPerson.xml F 259854
```
又例如,下面的方法递归检查目录及其所有子目录,并写出它找到的每个文件的名称:
```java
/// w ##class(Demo.FileDemo).ShowFilesInDir("E:\temp")
ClassMethod ShowFilesInDir(directory As %String = "")
{
s stmt = ##class(%SQL.Statement).%New()
s status = stmt.%PrepareClassQuery("%File", "FileSet")
if $$$ISERR(status) {
d $system.OBJ.DisplayError(status)
q
}
s resultSet = stmt.%Execute(directory)
while resultSet.%Next() {
s name = resultSet.%Get("Name")
s type = resultSet.%Get("Type")
if (type = "F") {
w !, name
} elseif (type = "D"){
d ..ShowFilesInDir(name)
}
}
q ""
}
```
在默认目录下的终端中运行此方法会产生如下结果:
```java
DHC-APP>w ##class(Demo.FileDemo).ShowFilesInDir("E:\temp")
E:\temp\config.txt
E:\temp\game.jpg
E:\temp\Person.xml
E:\temp\ppg.txt
E:\temp\qcache.txt
E:\temp\rfc7158.html
E:\temp\rfc7158.txt
E:\temp\samplePerson.xml
E:\temp\SecurityXml.txt
E:\temp\temp1.txt
E:\temp\tempPerson.xml
E:\temp\test\Tests.xml
E:\temp\testPerson.xml
E:\temp\Testzf.dll
E:\temp\textReader.txt
E:\temp\xmlnewtest.xml
E:\temp\xmlXpath.txt
E:\temp\yaoxin.txt
E:\temp\yxtest.txt
E:\temp\yxtest_Errors.log
E:\temp\yxtest_Unsupported.log
E:\temp\汉子转拼音global.gof
```
## 列出驱动器或装载的文件系统
`Drivelist`类查询列出可用的驱动器(在Windows上)或已装载的文件系统(在Unix上)。此查询接受一个参数:
1. `fullyqualified`-如果此参数为1,则查询在每个Windows驱动器名称上都包含一个尾随反斜杠。对其他平台没有影响。默认值为0。
此查询返回的结果集提供了一个字段:
- `Drive` 驱动器—驱动器的名称(在Windows上)或装载的文件系统的名称(在Unix上)。
以下示例显示了如何使用该查询:
```java
/// w ##class(Demo.FileDemo).ShowDrives()
ClassMethod ShowDrives()
{
s stmt = ##class(%SQL.Statement).%New()
s status = stmt.%PrepareClassQuery("%File","DriveList")
if $$$ISERR(status) {
d $system.OBJ.DisplayError(status)
q
}
s resultSet = stmt.%Execute(1)
while resultSet.%Next() {
w !, resultSet.%Get("Drive")
}
q ""
}
```
在终端中运行该方法会得到如下结果:
```java
DHC-APP>w ##class(Demo.FileDemo).ShowDrives()
c:\
d:\
e:\
g:\
```
文章
姚 鑫 · 六月 11, 2021
# 第四章 添加命名空间声明
# 添加命名空间声明
## 默认行为
在`%XML.Writer`会自动插入命名空间声明,生成命名空间前缀,并在适当的地方应用前缀。例如,以下类定义:
```java
Class Sample.Person Extends (%Persistent, %Populate, %XML.Adaptor)
{
Parameter NAMESPACE = "http://www.yaoxin.com";
}
```
如果导出此类的多个对象,则会看到类似以下内容:
```java
DHC-APP> w ##class(Demo.XmlDemo).Obj2Xml(1)
yaoxin
111-11-1117
1990-04-25
889 Clinton Drive
St Louis
WI
78672
9619 Ash Avenue
Ukiah
AL
56589
濮氶懌
111-11-1115
Red
Orange
Yellow
Green
Red
Orange
Yellow
31
```
名称空间声明会自动添加到每个`` 元素。只将其添加到文档的根目录。
## 手动添加声明
可以控制何时将命名空间引入XML输出。以下方法都会影响所写入的下一个元素(但不会影响该元素之后的任何元素)。为方便起见,其中几种方法添加了标准的`W3`名称空间。
通常使用这些方法将命名空间声明添加到文档的根元素;也就是说,在调用`RootObject()`或`RootElement()`之前调用其中一个或多个方法。
注意:这些方法都没有将任何元素分配给名称空间,并且这些名称空间永远不会作为默认名称空间添加。在生成特定元素时,需要指明它使用的名称空间,如后面的“编写根元素”和“生成XML元素”中所述。
### AddNamespace()
```java
method AddNamespace(namespace As %String,
prefix As %String,
schemaLocation As %String) as %Status
```
添加指定的命名空间。这里,`Namespace`是要添加的名称空间,`Prefix`是该名称空间的可选前缀,`schemaLocation`是指示相应架构位置的可选URI。
如果未指定前缀,则会自动生成前缀(格式为S01、S02等)。
下面的示例显示了此方法的效果。首先,假设`Person`类被分配给一个名称空间(类参数中的`NAMESPACE`)。如果在未首先调用`AddNamespace()`方法的情况下生成此类实例的输出,则可能会收到如下所示的输出:
```java
Love,Bart Y.
...
```
或者,在编写根元素之前按如下方式调用`AddNamespace()`方法:
```java
set status=writer.AddNamespace("http:///www.person.org","p")
```
如果随后生成根元素,则输出如下所示:
```java
...
```
或者,假设在调用`AddNamespace()`方法时指定了第三个参数,该参数提供了关联架构的位置:
```java
set status=writer.AddNamespace("http:///www.person.org","p","http://www.MyCompany.com/schemas/person.xsd")
```
在这种情况下,如果随后生成Root元素,则输出如下所示:
```java
...
```
### AddInstanceNamespace()
```java
method AddInstanceNamespace(prefix As %String) as %Status
```
添加W3架构实例命名空间。这里的前缀是用于此命名空间的可选前缀。默认前缀为`XSI`。
```java
...
```
### AddSchemaNamespace()
```java
method AddSchemaNamespace(prefix As %String) as %Status
```
添加`W3`架构命名空间。这里的前缀是用于此命名空间的可选前缀。默认前缀为`s`。
```java
...
```
### AddSOAPNamespace()
```java
method AddSOAPNamespace(soapPrefix As %String,
schemaPrefix As %String,
xsiPrefix As %String) as %Status
```
添加`W3 SOAP`编码命名空间、`SOAP`架构命名空间和`SOAP`架构实例命名空间。此方法有三个可选参数:用于这些命名空间的前缀。默认前缀分别为`SOAP-Enc`、`s`和`XSI`。
```java
...
```
### AddSOAP12Namespace()
```java
method AddSOAP12Namespace(soapPrefix As %String,
schemaPrefix As %String,
xsiPrefix As %String) as %Status
```
添加`W3 SOAP 1.2`编码命名空间、`SOAP`架构命名空间和`SOAP`架构实例命名空间。
```java
...
```
可以使用这些方法中的多个方法。如果使用其中的多个命名空间,则受影响的元素将包含所有指定命名空间的声明。
# 编写根元素
每个XML文档必须恰好包含一个根元素。有两种方法可以创建此元素:
- 根元素可能直接对应于一个启用了InterSystems IRIS XML的对象。
在本例中,使用`RootObject()`方法,该方法将指定的启用XML的对象作为根元素写入。输出包括该对象中包含的所有对象引用。根元素获取该对象的结构,不能插入其他元素您可以指定根元素的名称,也可以使用由启用XML的对象定义的默认值。
前面的示例使用了此技术。
- 根元素可能只是一组元素的包装器(可能是一组支持XML的对象)。
在本例中,使用`RootElement()`方法,该方法插入具有指定名称的根级元素。如果此文档缩进,此方法还会增加后续操作的缩进级别。
然后调用其他方法为根元素内的一个或多个元素生成输出。在根目录中,可以按照选择的任何顺序或逻辑包含所需的元素。之后,调用`EndRootElement()`方法关闭根元素。
在这两种情况下,都可以指定要用于根元素的命名空间,只有在启用了`XML`的类没有`Namespace`参数值的情况下才会应用该命名空间。
请记住,如果文档包含文档类型声明,则该`DTD`的名称必须与根元素的名称相同。
文章
姚 鑫 · 六月 12, 2021
# 第五章 生成XML元素
# 生成XML元素
如果使用`RootElement()`启动文档的根元素,则负责生成该根元素内的每个元素。有三个选择:
## 将对象生成为元素
可以从InterSystems IRIS对象生成输出作为元素。在本例中,使用`object()`方法,该方法写入支持XML的对象。输出包括该对象中包含的所有对象引用。可以指定此元素的名称,也可以使用在对象中定义的默认值。
只能在`RootElement()`和`EndRootElement()`方法之间使用`object()`方法。
此示例为给定启用XML的类的所有已保存实例生成输出:
```java
/// desc:将表里数据输出本地文件里
/// w ##class(PHA.TEST.Xml).WriteAll("Sample.Person")
ClassMethod WriteTableAllToXml(cls As %String = "", directory As %String = "E:\temp\")
{
if '##class(%Dictionary.CompiledClass).%ExistsId(cls) {
Write !, "类不存在或未编译"
Quit
}
s check=$classmethod(cls, "%Extends", "%XML.Adaptor")
If 'check {
Write !, "类不扩展%XML.Adaptor"
Quit
}
s filename = directory_"Person"_".xml"
s writer = ##class(%XML.Writer).%New()
s writer.Indent=1
s status = writer.OutputToFile(filename)
if $$$ISERR(status) { do $System.Status.DisplayError(status) quit }
s status=writer.RootElement("SampleOutput")
if $$$ISERR(status) { do $System.Status.DisplayError(status) quit }
//获取给定类范围内对象的ID
s stmt = ##class(%SQL.Statement).%New()
s status = stmt.%PrepareClassQuery(cls,"Extent")
if $$$ISERR(status) { do $System.Status.DisplayError(status) quit }
s rset = stmt.%Execute()
while (rset.%Next()) {
//对于每个ID,写入该对象
set objid = rset.%Get("ID")
set obj = $CLASSMETHOD(cls,"%OpenId",objid)
set status = writer.Object(obj)
if $$$ISERR(status) {Do $System.Status.DisplayError(status) Quit}}
d writer.EndRootElement()
d writer.EndDocument()
q ""
}
```
此方法的输出包含给定类的所有已保存对象,这些对象嵌套在根元素中。对于`Sample.Person`,输出如下:
```java
Tillem,Robert Y.
967-54-9687
1961-11-27
3355 First Court
Reston
WY
11090
4922 Main Drive
Newton
NM
98073
Red
47
Waters,Ed X.
361-66-2801
1957-05-29
5947 Madison Drive
...
```
## 手动构建元素
以手动构造XML元素。在本例中,使用`element()`方法,该方法使用提供的名称写入元素的开始标记。然后,可以编写内容、属性和子元素。使用`EndElement()`方法指示元素的结束。
相关方法如下:
### Element()
```java
method Element(tag, namespace As %String) as %Status
```
写入开始标记。可以为元素提供命名空间,只有在启用了XML的类没有`Namespace`参数的值时才会应用该命名空间。
### WriteAttribute()
```java
method WriteAttribute(name As %String,
value As %String = "",
namespace As %String,
valueNamespace As %String = "",
global As %Boolean = 0) as %Status
```
写入属性。必须指定属性名称和值。参数命名空间是属性名称的命名空间。参数`valueNamespace`是属性值的名称空间;当值在XML模式名称空间中定义时使用。
对于GLOBAL,如果属性在关联的XML架构中是全局的,因此应该有前缀,请指定TRUE。
如果使用此方法,则必须在`Element()`(或`RootElement()`)之后直接使用它。
### WriteChars()
```java
method WriteChars(text) as %Status
```
写入字符串,执行使该字符串适合作为元素内容所需的任何必要转义。参数必须`%String`类型或`%CharacterStream`类型。
### WriteCData()
```java
method WriteCData(text) as %Status
```
参数必须`%String`类型或`%CharacterStream`类型。
### WriteBase64()
```java
method WriteBase64(binary) as %Status
```
将指定的二进制字节编码为`base-64`,并将结果文本写入元素的内容。该参数的类型必须为`%Binary`或`%BinaryStream`。
### WriteBinHex()
```java
method WriteBinHex(binary) as %Status
```
将指定的二进制字节编码为二进制,并将结果文本写入元素的内容。该参数的类型必须为`%Binary`或`%BinaryStream`。
### EndElement()
```java
method EndElement() as %Status
```
结束可以与其匹配的元素。
只能在`RootElement()`和`EndRootElement()`方法之间使用这些方法。
注意:这里描述的方法旨在使能够向XML文档编写特定的逻辑片段,但在某些情况下,可能需要更多的控制。`%XML.Writer`类提供了一个附加方法`write()`,可以使用该方法编写任意字符串。有责任确保结果是格式良好的XML文档;不提供任何验证。
示例
下面是一个示例例程:
```java
/// w ##class(Demo.XmlDemo).WriteObjXml()
ClassMethod WriteObjXml()
{
set writer=##class(%XML.Writer).%New()
set writer.Indent=1
set status=writer.OutputToDevice()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.StartDocument()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.RootElement("root")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.Element("SampleElement")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.WriteAttribute("Attribute","12345")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.Element("subelement")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.WriteChars("yao")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndElement()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.Element("subelement")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.WriteChars("xin")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndElement()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndElement()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndRootElement()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndDocument()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
q ""
}
```
```java
DHC-APP>w ##class(Demo.XmlDemo).WriteObjXml()
yao
xin
```
### 使用%XMLL.Element
在前一节中,我们使用了Element()并指定了要生成的元素;我们还可以指定名称空间。在某些情况下,类中使用%XML.Element的实例,而不是使用元素名称。此类具有以下属性:
- Local属性指定此元素是否为其父元素的本地元素,这会影响命名空间的控制。
- Namespace属性指定此元素的命名空间。
- Tagname属性指定此元素的名称。
这里还可以使用前面描述的WriteAttribute()方法。
文章
Tete Zhang · 九月 14, 2022
从消息查看器看到清除周期以外的消息没有被正常清除
这种情况先抽查这些消息所处的会话中是否有未完成操作周期的消息(状态为除“Completed”“Error”“Discarded”之外的状态)。如有,且定期清除任务配置了“KeepIntegrity”,且该环境并不需要保留这些消息,可通过关闭清除任务中的“KeepIntegrity”配置清除这些会话和包含的消息。如果有这类消息,但是定期清除任务未配置“KeepIntegrity”,可能是定期清除任务的逻辑或消息数据问题导致清楚任务查找的时候没有覆盖这些消息,请联系WRC帮助排查具体原因。
有关定期清除任务的更多信息请参见文档
Purging Production Data | Managing Productions | InterSystems IRIS for Health 2022.1
从消息查看器看不到清除周期之外的消息,但是^%GSIZE显示有global占据了很大的磁盘空间
这种情况需要具体排查每个较大的global。可能有以下原因:
系统定义的global占用很大空间。您可以联系WRC帮助排查具体原因。
自定义的global占用很大空间。这可能是消息中嵌套的持久化数据或流数据,或者是和消息没有直接关系的独立的表里面的数据。请对创建这种global的代码进行复盘,找到创建逻辑并增加相应的数据管理逻辑。
孤立消息 (Orphan Messages)
孤立消息是指不存在配套Message Header的所有消息对象。
定期清除任务会根据Message Header里的时间信息和状态信息去判断一条消息是否符合清除条件。Message Header是在消息从一个组件发向另一个组件的时候被创建的。所以,当我们创建将被永久存储的对象之前,我们都要思考:这个对象会被保存吗?被保存后会被发送到另一个组件吗?如果不会被发送,该对象将不存在配套的Message Header,也就不会被定期清除。这种情况我们需要开发相应的自定义逻辑去定期管理该表中的数据,或确保该对象被发送到某个组件以创建Message Header。消息对象中嵌套对象或流的情况要尤其注意,对每一个嵌套的对象或流都要定义相对应的%OnDelete()删除逻辑。
在测试阶段我们可以做如下的测试:
测试前跑^%GSIZE报告并检查磁盘存储
跑一套测试消息
用清除任务删除系统上所有的消息(DaysToKeep=0)
跑^%GSIZE报告并检查磁盘存储
如果对比前后的^%GSIZE报告和磁盘空间之后,发现清除任务完成后没有遗留多余的数据,那么这就证明我们的逻辑中对消息及相关嵌套数据进行了很好的管理。反之如果发现了遗留数据,我们可以在研发测试阶段就对问题进行排查,尽量避免开放生产环境以后出现磁盘满或数据库过大的问题。
如果发现了环境中有孤立消息的问题,请联系WRC进行排查和消息清除管理。
HL7v2:孤立字段(Orphan Segment)
HL7v2在数据库中的存储逻辑如下。
EnsLib.HL7.Message对象存在以下两个global里:
^EnsLib.H.MessageD
^EnsHL7.Segment
示例:
HL7v2消息 (^EnsLib.H.MessageD global):
1: ^EnsLib.H.MessageD = 1257406
2: ^EnsLib.H.MessageD(1257406) = $lb("","","2.3:ORU_R01",0,"2019-06-03 15:28:38.819","2.3.1","C:\Support\inarchive\testoru.txt_2019-06-03_11.28.38.814","","")
3: ^EnsLib.H.MessageD(1257406,"segs") = 5
4: ^EnsLib.H.MessageD(1257406,"segs",1) = "11612,25"
5: ^EnsLib.H.MessageD(1257406,"segs",2) = "11612,26"
6: ^EnsLib.H.MessageD(1257406,"segs",3) = "11612,27"
7: ^EnsLib.H.MessageD(1257406,"segs",4) = "11612,28"
8: ^EnsLib.H.MessageD(1257406,"segs",5) = "11612,29"
其中, 125706是该HL7v2消息的Object ID。Global值"11612,25","11612,26"指向相应的HL7v2字段。
HL7v2字段 (^EnsHL7.Segment global):
1: ^EnsHL7.Segment(11612) = 30
2: ^EnsHL7.Segment(11612,25) = "|^~\&MSH|^~\&||GA0000||VAERS PROCESSOR|20010331605||ORU^R01|20010422GA03|T|2.3.1|||AL|"
3: ^EnsHL7.Segment(11612,25,0,1257406) = ""
其中,11612是创建该HL7v2消息的进程 ID (PID)。^EnsHL7.Segment(11612,25) 存储了该字段的具体数据。^EnsHL7.Segment(11612,25,0,1257406) 中的第四个值(1257406)是这个字段所属消息的Object ID。
从以上示例可以看出,HL7v2字段数据存储于^EnsHL7.Segment global。所以在^%GSIZE中看到^EnsHL7.Segment global比 ^EnsLib.H.MessageD global大是正常现象。使用平台自带的逻辑在最新版本上目前也没有已知问题会导致孤立字段。如果您持续观察^%GSIZE报告,发现^EnsHL7.Segment global的大小出现异常增长,可以联系WRC排查是否有孤立字段的情况。
文章
Michael Lei · 十月 10, 2022
Hi 大家好,
我最近开始学习InterSystems IRIS 的互操作性,我发现官方文档对理解它的工作原理很有帮助,尽管我自己在实现它时仍有一些困难。在我的同事的帮助下,我成功地创建了一个系统的Demo,并从实践中学习。因此,我决定写一下文章,分享我得到的帮助,来帮助更多的其他人。
介绍
首先,让我们掌握一些基本概念:
互操作性 - 这个词的含义并不像它的发音那样复杂--它基本上是把各种信息从一个系统带到另一个系统的“魔术”。
业务主机 - 如果把互操作性比作是魔术,那么业务主机Business Host就是魔术师的魔法帽--业务主机里有能够识别和接收信息的业务服务Business Service/BS,并将其作为消息发送给业务流程BP或业务操作BO。业务操作执行所需的操作(顾名思义)并传递信息。业务流程控制着消息的流动:它们定义了消息的去向(基于你所选择的任何东西)以及它是如何被传递的。
适配器 - 适配器是一些我们可以用来识别和操作我们可能要处理的各种信息的类。在实践中,我们把它们作为参数和(可选)属性来访问其方法和属性
准备搭建Production
从简单的开始比较容易--让我们先想想服务和操作--比如说你有一个接收一种信息的服务,它很容易被我们唯一的操作所识别。
当生产的目的和它的部分非常清楚时,开发就比较容易。如果你愿意,画一张图或写下你希望它完成的步骤可能会有帮助。
例如:
首先要问自己, "需要做什么?" - 在我的演示中,我需要操作一个SQL表--我要把一本书的标题和作者等信息,并将其插入到一个表中。
"那么,我需要Production做什么呢?- 它必须接收一个包含书名和作者的信息,并执行一个SQL INSERT。
"好的,如何让这件事发生?- 业务服务(BS)将接收标题和作者,将其传递给业务操作(BO)。BO执行SQL代码。
"现在我有了信息将遵循的路径,我需要理解信息。它是什么?" - 我有很多方法可以发送数据。我可以在一个文件中发送,或一个REST应用程序,甚至一个电子邮件。让我们选择文件来开始简单。我的BS将接收文件,读取它并将其信息发送给BO。BO执行查询。
开始干活
你可以从你对最有信心的代码部分开始。
业务服务 Business Service/BS
我是从业务服务BS开始的. 现在我很清楚,我需要实现Request(请求)类,以便它存储数据和将被发送到的操作。
业务操作Business Operation/BO
请求Request
Request类很简单。我只用它来存储一些数据。%XML.Adaptor是为了在管理门户上显示Request以进行错误管理。
业务操作Business Operation
BO有一个消息地图,为每一种到达的消息提供足够的方向。例如,如果在BS中,在SendRequestSync方法和Request参数中,我使用了不同的类型,如 "Demo.Books.BO.SearchTable.Request",我可以用这种消息类型创建另一个MapItem,引用一个Search方法。
方法Method
在这里,你实现了操作应该做的任何事情.
管理门户设置
最后,为了让事情顺利进行,请遵循以下步骤:
管理门户 > 互操作性 > 列表 > 生产 > 新建(Management Portal > Interoperability > List > Productions > New)
门户网站将用Production生产信息创建一个类。
然后,你用你创建的类添加一个服务,并设置文件路径(你将把输入文件放在哪里)和工作路径。
另外,用你创建的类添加一个操作,必要时指定其设置。
几点观察
业务操作可以接收一个同步请求,这意味着服务只有在BO检索到它的信息后才会响应,因为服务的响应取决于BO的响应,例如,如果它必须在表操作中执行一个搜索。因为Production生产只执行INSERT操作,BS只需要发送信息,BO就会INSERT;不需要响应,所以我们可以有一个异步请求。
讨论例如文件和SQL适配器等适配器的规格超出了本文的范围,本文的目的是对编码和步骤进行概述,以便更好地理解实际工作。
欢迎与我联系--我很乐意提供任何可能的帮助!
文章
姚 鑫 · 六月 4, 2023
# 第二十四章 开发Productions - ObjectScript Productions - 定义业务服务
本页介绍如何定义业务服务类。
提示: `IRIS® `提供使用特定入站适配器的专用业务服务类,其中之一可能适合需要。如果是这样,则不需要编程。有关部分列表,请参阅 `Introducing Interoperability Productions` 中的连接选项。
# 介绍
业务服务负责接受来自外部应用程序的请求到 `IRIS`。下图显示了它是如何工作的:
请注意,此图仅显示数据的输入流,而不是可选响应。
业务服务负责以下活动:
- 等待特定的外部事件(例如来自应用程序的通知、收到 `TCP` 消息等)。
- 读取、解析和验证伴随此类事件的数据,
- 如果需要,返回对外部应用程序的确认,表明已收到事件。
- 创建请求消息的实例并将其转发到适当的业务流程或业务操作以进行处理。
业务服务的目的通常是接收数据输入。在大多数情况下,业务服务有一个与之关联的入站适配器。但是,在某些情况下不需要适配器,因为应用程序能够将请求消息发送到服务中,或者因为业务服务已被编写为处理特定类型的外部调用,例如来自复合应用程序的调用。这种类型的业务服务称为无适配器业务服务。
当业务服务具有入站适配器时,它处于数据拉取(而不是推送)模式。在这种模式下,业务服务会定期轮询适配器,看它是否有数据。同时,如果适配器随时遇到输入数据,它会调用业务服务来处理输入。
当业务服务没有适配器时,它不会拉取数据。相反,客户端应用程序调用业务服务并告诉它处理输入(这是一种数据推送模式)。
# 关键原则
首先,务必阅读 `Programming in InterSystems IRIS`。
在业务服务中,可以访问关联适配器的属性和方法,这些适配器作为业务服务的 Adapter 属性提供。这意味着可以更改适配器的默认行为;这样做可能合适也可能不合适。记住封装原则很有用。封装的思想是适配器类应该负责技术特定的逻辑,而业务服务类应该负责生产特定的逻辑。
如果发现有必要在业务服务类中大量或频繁地改变适配器类的行为,那么创建适配器类的自定义子类可能更合适。请参阅不太常见的任务。
这个原则也适用于商业运作。
# 定义业务服务类
要创建一个业务服务类,定义一个类如下:
- 类必须在(或子类)中扩展 `Ens.BusinessService`。
- 在类中,`ADAPTER` 参数必须等于此业务服务要使用的适配器类的名称。
提示:如果只是希望业务服务定期唤醒和运行而不关心 `IRIS` 外部的事件,请使用适配器类 `Ens.InboundAdapter`。
- 类必须实现 `OnProcessInput()` 方法,如实现 `OnProcessInput()` 方法中所述。
- 类可以添加或删除设置。请参阅添加和删除设置。
- 类可以实现任何或所有启动和拆卸方法。请参阅覆盖启动和停止行为。
- 类可以包含完成自身内部工作的方法。
有关业务服务类的示例,请参阅适配器指南。
# 实施 `OnProcessInput()` 方法
在业务服务类中, `OnProcessInput()` 方法可以具有以下通用签名:
```java
Method OnProcessInput(pInput As %RegisteredObject, pOutput As %RegisteredObject) As %Status
```
这里的`pInput`是适配器要发送给这个业务服务的输入对象,`pOutput`是输出对象。
首先查看选择的适配器类。 建议编辑 `OnProcessInput()` 方法签名以使用适配器所需的特定输入参数。
`OnProcessInput()` 方法应该执行以下部分或全部操作:
1. 可选地设置业务服务类的属性(在任何适当的时间)。最受关注的业务服务属性是 `%WaitForNextCallInterval`。它的值控制 `IRIS` 调用适配器的 `OnTask()` 方法的频率。
有关其他属性,请参阅 `Ens.BusinessService`的类参考。
2. 如有必要,验证输入对象。
3. 检查输入对象并决定如何使用它。
4. 创建请求消息类的实例,这将是业务服务发送的消息。
5. 对于请求消息,使用输入对象中的值适当地设置其属性。
6. 确定要将请求消息发送到哪里。当发送消息时,将需要在生产中使用业务主机的配置名称。
7. 将请求消息发送到生产(业务流程或业务操作)中的目的地。请参阅下一节。
8. 确保设置输出参数 (`pOutput`)。通常,将其设置为等于您收到的响应消息。此步骤是必需的。
9. 返回适当的状态。此步骤是必需的。
文章
Claire Zheng · 十月 18, 2022
各位开发者社区的同学们,大家好!
您想更好地获得帮助、讨论有趣的功能、发布公告或分享您的知识吗?在这篇文章中,我们将告诉你如何做到这一切。
我们将通过以下几部分来分享“如何做”:
一般发帖步骤
问题
文章或公告
讨论
一般发帖步骤
首先,你需要点击开发者社区网站顶部菜单中的“发布新帖”按钮:
之后,您将看到编辑器中显示创建一个问题、一则公告、一篇文章或一个讨论。不同类型的帖子有自己的一组必填字段和可选字段。
首先,让我们讨论所有类型的帖子的公共字段,然后继续讨论细节。
基本上,每篇文章都有一个标题*、正文*、组*、标签和一些额外的选项,你可以在其中添加调查或附加PDF文件。所有用星号(*)标记的文本字段都是必填项。因此,首先,你需要选择帖子的类型,可以像上面提到的问题,公告,文章或讨论。
接下来,请用最精确和简洁的方式表述你的问题的主要思想,并将其作为标题。
之后,在文章主体中,你可以写任何你想与他人分享的东西。在写文章的时候有两个选择。你可以使用编辑器“所见即所得”(WYSIWYG)模式或者Markdown。当然,这两种方法得到的结果是一样的。
vs.
在你写完文本后,你必须选择组,通常是InterSystems提供的技术、产品或服务。
在组字段之后,有一个标签字段,您可以在其中添加与文章内容相关的标签。有相当多的标签,所以请认真选择,因为其他成员会通过这些标签寻找或排序所需要的信息。
在标签下面,有一个链接可以查看更多选项。在那里,您可以附加一个pdf文档(例如,pdf格式的事件时间表)并提供您想要显示的名称。
你可以通过“更多选项”做的另一件事是添加投票。在字段中填写一个问题、可能的答案、选择持续时间等。
完成后,你可以预览你的帖子,看看它对其他人来说是什么样子,你可以保存它以便以后继续编辑,或立即发布它。
此外,您可以预约发布您的文章。只需点击向下箭头,选择安排帖子,并设置日期和时间。
一切都设置好后,只需点击安排帖子,就完成了。
基本上,这是创建帖子的常见功能。
问题
从它的名字来看,很明显,如果你需要别人的帮助,你应该选择这种类型的帖子。在这里,在开发者社区中,有很多专家,有些人可能已经遇到了相同的情况。所以不要犹豫,提出问题或回答问题吧:)
要寻求帮助,请阐明你的问题的主要思想,并将其作为一个标题写下来。接下来,请选择您正在使用的产品版本,因为不同版本具有不同的功能和类,一些建议可能对某些版本有用,而对其他版本无用。
更准确地说,您可以提供当前在$ZV文本框中使用的完整构建。要获得完整版本,可以打开Terminal并执行以下命令:
write $ZV
在你正在使用的IDE中也可以执行相同的操作,或者你可以在管理门户(Management Portal)中看到:
其余字段与前面描述的相同。
文章或公告
要分享你的知识或发布公告,你应该分别选择一种类型的帖子——文章或公告。这些类型的帖子除了公共字段之外还有一些额外的字段。这些是上一篇文章、下一篇文章和打开Exchange应用程序链接。
因此,基本上,如果当前的文章/声明(或讨论的分支)链接到另一篇文章,您可以在“上一个公告”字段中添加链接,这样其他社区成员将在文章末尾看到以下相关文章块。
你不需要再打开上一篇文章去添加到下一篇文章的链接,它将自动链接。
添加完这些链接后,用户也可以通过使用链接文章右上角的导航按钮轻松地从一个帖子导航到另一个帖子。
如果你的帖子在Open Exchange上有一个项目链接到它,你可以在相应的字段中添加这个项目的链接。
讨论
要开始关于某个功能的对话,或者分享您使用该技术的经验并寻求反馈,您可以开始一个讨论。这种类型的文章有所有公共字段,也有到上一篇和下一篇的链接。
就这些!
这就是您在社区上开始发布一个新帖子时所需要知道的。
期待着看到您的精彩发帖:)
文章
Kelly Huang · 七月 12, 2023
FHIR 通过提供标准化数据模型来构建医疗保健应用程序并促进不同医疗保健系统之间的数据交换,彻底改变了医疗保健行业。由于 FHIR 标准基于现代 API 驱动的方法,因此移动和 Web 开发人员更容易使用它。然而,与 FHIR API 交互仍然具有挑战性,尤其是在使用自然语言查询数据时。
隆重推出FHIR - AI 和 OpenAPI 链应用程序,该解决方案允许用户使用自然语言查询与 FHIR API 进行交互。该应用程序使用OpenAI 、 LangChain和Streamlit构建,简化了查询 FHIR API 的过程并使其更加用户友好。
FHIR OpenAPI 规范是什么?
OpenAPI 规范(以前称为 Swagger,目前是OpenAPI Initiative的一部分)已成为软件开发领域的重要工具,使开发人员能够更有效地设计、记录 API 并与 API 交互。 OpenAPI 规范定义了一种标准的机器可读格式来描述 RESTful API,提供了一种清晰一致的方式来理解其功能并有效地使用它们。
在医疗保健领域,FHIR 成为数据交换和互操作性的领先标准。为了增强FHIR的互操作能力, HL7正式记录了FHIR OpenAPI规范,使开发人员能够将FHIR资源和操作无缝集成到他们的软件解决方案中。
FHIR OpenAPI 规范的优点:
标准化 API 描述:OpenAPI 规范提供 FHIR 资源、操作和交互的全面且标准化的描述。开发人员可以轻松了解基于 FHIR 的 API 的结构和功能,从而更轻松地构建集成并与医疗保健系统交互。
促进互操作性:促进开发人员之间的协作,推动 FHIR 标准和最佳实践的采用。该规范提供了一种通用语言和框架,用于讨论基于 FHIR 的集成和实现,促进开发人员之间的协作。
增强的文档和测试:交互式文档和测试套件,以便更好地理解和验证。开发人员可以创建详细的API文档,使其他开发人员更容易理解和使用基于FHIR的API。基于规范的测试套件可以对API集成进行全面的测试和验证,确保医疗数据交换的可靠性和准确性。
改进的开发人员体验:自动生成客户端库和 SDK 以实现无缝集成。这简化了集成过程,并减少了将 FHIR 功能合并到应用程序中所需的时间和精力
FHIR、OpenAI 和 OpenAPI Chain 如何协同工作?
FHIR - AI 和 OpenAPI Chain应用程序利用 LangChain 来加载和解析 OpenAPI 规范( OpenAPI Chain )。之后,根据这些规范,通过 OpenAI 给出的提示链旨在理解自然语言查询并将其转换为适当的 FHIR API 请求。用户可以用简单的语言提出问题,应用程序将与所选的 FHIR API 交互以检索相关信息。
例如,用户可能会问:“患者 John Doe (ID 111) 的最新血压读数是多少?”然后,应用程序会将此查询转换为 FHIR API 请求,获取所需的数据,并以易于理解的格式将其呈现给用户。
FHIR - AI 和 OpenAPI 链的优势
用户友好的交互:通过允许用户使用自然语言查询与 FHIR API 交互,该应用程序使非技术用户可以更轻松地访问和分析医疗保健数据。
提高效率:该应用程序简化了查询 FHIR API 的过程,减少了获取相关信息所需的时间和精力。此外,它还有可能减少从应用程序中查找任何特定信息的点击次数(花费的时间)。
可定制:FHIR 标准简化了从任何 FHIR 服务器检索一致数据的过程,从而可以轻松定制。它可以轻松配置为与任何 FHIR API 无缝集成,为不同的医疗保健数据需求提供灵活且适应性强的解决方案。
FHIR 入门 - AI 和 OpenAPI 链
要开始使用 FHIR - AI 和 OpenAPI Chain 应用程序,请按照以下步骤操作:
从OpenAI Platform获取 OpenAI API 密钥。
获取 FHIR 服务器 API 端点。您可以使用自己的示例 FHIR 服务器(需要未经身份验证的访问),也可以按照InterSystems IRIS FHIR 学习平台中给出的说明创建临时示例服务器。
在线试用该应用程序或使用提供的说明在本地进行设置。
通过集成人工智能和自然语言处理功能,FHIR - AI 和 OpenAPI Chain 应用程序提供了一种与 FHIR API 交互的更直观的方式,使所有技术背景的用户都更容易访问和分析医疗数据。
如果您发现我们的应用程序很有前途,请在大奖赛中投票!
如果您能想到使用此实现的任何潜在应用程序,请随时在讨论线程中分享它们。
@Ikram Shah 致敬原创作者~
文章
Jingwei Wang · 九月 16, 2022
连接前准备:
Python 开发环境
DB-API驱动:irispython wheel 文件
Connection String
步骤:
安装irispython wheel 文件
pip install intersystems_irispython-3.2.0-py3-none-any.whl
Connection String:其中import iris 用来导入iris, connection = iris.connect是connection string。connection.close()用来断开连接。
import iris
def main():
connection_string = "localhost:1972/USER"
username = "SQLAdmin"
password = "deployment-password"
connection = iris.connect(connection_string, username, password)
# when finished, use the line below to close the connection
# connection.close()
if __name__ == "__main__":
main()
文章
姚 鑫 · 三月 29, 2021
# 第十三章 使用动态SQL(七)
# SQL元数据
动态SQL提供以下类型的元数据:
- 在“准备”之后,描述查询类型的元数据。
- 在“准备”之后,描述查询中选择项的元数据(“列”和“扩展列信息”)。
- 在准备之后,描述查询参数的元数据:参数,`:var`参数和常量。 (语句参数,形式参数和对象)
- 执行之后,描述查询结果集的元数据。在执行Prepare操作(`%Prepare()`,`%PrepareClassQuery()`或`%ExecDirect()`)之后,可以使用`%SQL.StatementMetadata`属性值。
- 可以直接为最新的`%Prepare()`返回`%SQL.Statement`元数据属性。
- 可以返回包含`%SQL.StatementMetadata`属性的oref的`%SQL.Statement%Metadata`属性。这使可以返回多个准备操作的元数据。
`SELECT`或`CALL`语句返回所有这些元数据。 `INSERT`,`UPDATE`或`DELETE`返回语句类型元数据和形式参数。
## 语句类型元数据
使用`%SQL.Statement`类进行`Prepare`之后,可以使用`%SQL.StatementMetadata statementType`属性来确定准备哪种类型的SQL语句,如以下示例所示。本示例使用`%SQL.Statement%Metadata`属性来保存和比较两`个Prepare`操作的元数据:
```java
/// d ##class(PHA.TEST.SQL).MetaData()
ClassMethod MetaData()
{
SET tStatement = ##class(%SQL.Statement).%New()
SET myquery1 = "SELECT TOP ? Name,Age,AVG(Age),CURRENT_DATE FROM Sample.Person"
SET myquery2 = "CALL Sample.SP_Sample_By_Name(?)"
SET qStatus = tStatement.%Prepare(myquery1)
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
SET meta1 = tStatement.%Metadata
SET qStatus = tStatement.%Prepare(myquery2)
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
SET meta2 = tStatement.%Metadata
WRITE "语句类型query 1: ",meta1.statementType,!
WRITE "语句类型query 2: ",meta2.statementType,!
WRITE "End of metadata"
}
```
```java
DHC-APP>d ##class(PHA.TEST.SQL).MetaData()
语句类型query 1: 1
语句类型query 2: 45
End of metadata
```
`statementType`属性的“类引用”条目列出了语句类型整数代码。最常见的代码是1(`SELECT`查询)和45(`CALL`到存储的查询)。
可以使用`%GetImplementationDetails()`实例方法返回相同的信息,如成功准备的结果中所述。
执行查询后,可以从结果集中返回语句类型名称(例如`SELECT`)。
## 选择项目Select-item元数据
使用`%SQL.Statement`类准备`SELECT`或`CALL`语句之后,可以通过显示所有元数据或指定各个元数据项来返回有关查询中指定的每个选择项列的元数据。此列元数据包括ODBC数据类型信息,以及客户端类型和InterSystems Objects属性的起源以及类类型信息。
以下示例返回最近准备的查询中指定的列数:
```java
/// d ##class(PHA.TEST.SQL).MetaData1()
ClassMethod MetaData1()
{
SET myquery = "SELECT %ID AS id,Name,DOB,Age,AVG(Age),CURRENT_DATE,Home_State 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}
WRITE "Number of columns=",tStatement.%Metadata.columnCount,!
WRITE "End of metadata"
}
```
```java
DHC-APP>d ##class(PHA.TEST.SQL).MetaData1()
Number of columns=7
End of metadata
```
以下示例返回列名称(或列别名),ODBC数据类型,最大数据长度(精度),以及每个`SELECT`项目字段的比例:
```java
/// d ##class(PHA.TEST.SQL).MetaData2()
ClassMethod MetaData2()
{
SET $NAMESPACE="SAMPLES"
SET myquery=2
SET myquery(1)="SELECT Name AS VendorName,LastPayDate,MinPayment,NetDays,"
SET myquery(2)="AVG(MinPayment),$HOROLOG,%TABLENAME FROM Sample.Vendor"
SET rset = ##class(%SQL.Statement).%New()
SET qStatus = rset.%Prepare(.myquery)
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
SET x=rset.%Metadata.columns.Count()
SET x=1
WHILE rset.%Metadata.columns.GetAt(x) {
SET column=rset.%Metadata.columns.GetAt(x)
WRITE !,x," ",column.colName," 是数据类型 ",column.ODBCType
WRITE " 大小为 ",column.precision," 规模 = ",column.scale
SET x=x+1
}
WRITE !,"End of metadata"
}
```
```java
DHC-APP>d ##class(PHA.TEST.SQL).MetaData2()
1 VendorName 是数据类型 12 大小为 50 规模 = 0
2 LastPayDate 是数据类型 9 大小为 10 规模 = 0
3 MinPayment 是数据类型 8 大小为 6 规模 = 0
4 NetDays 是数据类型 4 大小为 3 规模 = 0
5 Aggregate_5 是数据类型 8 大小为 20 规模 = 0
6 Expression_6 是数据类型 12 大小为 255 规模 = 0
7 Literal_7 是数据类型 12 大小为 13 规模 = 0
End of metadata
```
下面的示例使用`%SQL.StatementMetadata%Display()`实例方法显示所有列元数据:
```java
/// d ##class(PHA.TEST.SQL).MetaData3()
ClassMethod MetaData3()
{
SET tStatement = ##class(%SQL.Statement).%New()
SET qStatus = tStatement.%Prepare("SELECT %ID AS id,Name,DOB,Age,AVG(Age),CURRENT_DATE,Home_State FROM Sample.Person")
IF qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT}
DO tStatement.%Metadata.%Display()
WRITE !,"End of metadata"
}
```
```java
DHC-APP>d ##class(PHA.TEST.SQL).MetaData3()
Columns (SQLRESULTCOL, property 'columns'):
Column Name Type Prec Scale Null Label Table Schema CType
----------- ---- ---- ----- ---- ------------ ------------ ------------ -----
id 4 10 0 0 id Person Sample 5
Name 12 50 0 0 Name Person Sample 10
DOB 9 10 0 1 DOB Person Sample 2
Age 4 10 0 1 Age Person Sample 5
Aggregate_5 2 20 8 1 Aggregate_5 14
Expression_6 9 11 0 2 Expression_6 2
Home_State 12 2 0 1 Home_State Person Sample 10
Extended Column Info (SQLRESULTCOL)
Flags: 1:AutoIncrement,2:CaseSensitive,3:Currency,4:ReadOnly,5:RowVersion,
6:Unique,7:Aliased,8:Expression,9:Hidden,10:Identity,11:KeyColumn,
12:RowId
Column Name Linked Prop Type Class Flags
------------ --------------------- --------------------- -----------------------
id Sample.Person Y,N,N,Y,N,Y,Y,N,N,Y,Y,Y
Name Sample.Person.Name %Library.String N,N,N,N,N,N,N,N,N,N,N,N
DOB Sample.Person.DOB %Library.Date N,N,N,N,N,N,N,N,N,N,N,N
Age Sample.Person.Age %Library.Integer N,N,N,N,N,N,N,N,N,N,N,N
Aggregate_5 %Library.Numeric N,N,N,Y,N,N,Y,N,N,N,N,N
Expression_6 %Library.Date N,N,N,Y,N,N,Y,Y,N,N,N,N
Home_State Sample.Address.State
%Library.String N,N,N,N,N,N,N,N,N,N,N,N
Statement Parameters (property 'parameters'):
Nbr. Type precision scale nullable colName columntype
---- ---- --------- ----- -------- ------------ ----------
Formal Parameters (property 'formalParameters'):
Nbr. Type precision scale nullable colName columntype
---- ---- --------- ----- -------- ------------ ----------
Objects:
Col Column Name Extent ExportCall
--- ----------- ----------------- -----------------------------
1 id Sample.Person ##class(Sample.Person).%SQLQuickLoad
```
这将返回所选字段的两个表列表。第一列元数据表列出了列定义信息:
显示标题 | `%SQL.StatementColumn`属性 | 描述
---|---|---
Column Name | colName |列的SQL名称。如果为该列提供了别名,则会在此处列出该列的别名,而不是字段名称。名称和别名将被截断为12个字符。对于表达式,聚合,文字,主机变量或子查询,列出了分配的`“ Expression_n”`,`“ Aggregate_n”`,`“ Literal_n”`,`“ HostVar_n”`或`“ Subquery_n”`标签(`n`为`SELECT`项序列号)。如果为表达式,聚合,文字,主机变量或子查询分配了别名,则在此处列出该别名。
Type| ODBCType |ODBC数据类型的整数代码。请注意,这些ODBC数据类型代码与CType数据类型代码不同。
Prec| precision|精度或最大长度(以字符为单位)。日期,时间,PosixTime和TimeStamp数据类型中描述了TIME数据类型的精度和小数位元数据。
Scale| scale| 小数位数的最大数目。对于整数或非数值返回0。日期,时间,PosixTime和TimeStamp数据类型中描述了`TIME`数据类型的精度和小数位元数据。
Null| isNullable| 一个整数值,指示是否将列定义为`Non-NULL(0)`,或者是否允许`NULL(1)`。 RowID返回0。如果`SELECT`项是可能导致`NULL`的聚合或子查询,或者如果它指定`NULL`文字,则该项设置为1。如果`SELECT`项是表达式或主机变量,则设置此项到2(无法确定)。
Label| label| 列名或列别名(与列名相同)。
Table| tableName| SQL表名称。即使为表指定了别名,也始终在此处列出实际的表名。如果`SELECT`项是表达式或聚合,则不会列出任何表名。如果`SELECT`项是子查询,则列出子查询表名称。
Schema| schemaName|表的架构名称。如果未指定架构名称,则返回系统范围的默认架构。如果`SELECT`项是表达式或聚合,则不会列出任何模式名称。如果SELECT项是子查询,则不会列出任何架构名称。
CType| clientType| 客户端数据类型的整数代码。
第二列元数据表列出了扩展列信息。扩展列信息表列出了具有十二个布尔标志(SQLRESULTCOL)的每一列,这些标志被指定为Y(是)或N(否):
显示标题 | `%SQL.StatementColumn`属性 | 描述
---|---|---
1: AutoIncrement| isAutoIncrement| TRowID和IDENTITY字段返回Y。
2: CaseSensitive| isCaseSensitive |具有`%EXACT`归类的字符串数据类型字段返回Y。引用`%SerialObject`嵌入式对象的属性返回Y。
3: Currency| isCurrency| 使用%Library.Currency数据类型定义的字段,例如`MONEY`数据类型。
4: ReadOnly |isReadOnly|表达式,聚合,文字,`HostVar`或子查询返回Y。RowID,IDENTITY和RowVersion字段返回Y。
5: RowVersion| isRowVersion|RowVersion字段返回Y。
6: Unique| isUnique| 定义为具有唯一值约束的字段。 RowID和IDENTITY字段返回Y。
7: Aliased| isAliased| 系统为非字段选择项提供别名。因此,无论用户是否通过指定列别名替换了系统别名,表达式,聚合,文字,HostVar或子查询都将返回Y。此标志不受用户指定的列别名的影响。
8: Expression| isExpression| 表达式返回Y。
9: Hidden| isHidden| 如果使用`%PUBLICROWID`或`SqlRowIdPrivate = 0`(默认值)定义表,则RowID字段返回N。否则,RowID字段返回Y。引用`%SerialObject`嵌入式对象的属性返回Y。
10: Identity| isIdentity| 定义为IDENTITY字段的字段返回Y。如果未隐藏RowID,则RowID字段返回Y。
11: KeyColumn| isKeyColumn| 定义为主键字段或外键约束目标的字段。 RowID字段返回Y。
12: RowID| isRowId |ROWID和Identity字段返回Y.
扩展列信息元数据表列出了每个选定字段的列名称(SQL名称或列别名),链接属性(链接的持久性类属性)和类型类(数据类型类)。请注意,链接属性列出了持久性类名(不是SQL表名)和属性名(不是列别名)。
- 对于普通表字段(`SELECT Name FROM Sample.Person`): `Linked Prop=Sample.Person.Name, Type Class=%Library.String`.
- 对于表格的RowID (`SELECT %ID FROM Sample.Person`): `Linked Prop= [none], Type Class=Sample.Person`.
- 对于表达式,聚合,文字,`HostVar`或子查询 (`SELECT COUNT(Name) FROM Sample.Person`): `Linked Prop= [none], Type Class=%Library.BigInt`.
- 供参考`%Serial Object`嵌入式对象属性 (`SELECT Home_State FROM Sample.Person`). `Linked Prop=Sample.Address.State, Type Class=%Library.String.`
- 对于引用`%SerialObject`嵌入式对象的字段(`SELECT Home FROM Sample.Person`). `Linked Prop=Sample.Person.Home, Type Class=Sample.Address`.
在此示例中,`Sample.Person`中的`Home_State`字段引用`%SerialObject`类`Sample.Address`的`State`属性。
下面的示例返回带有一个形式参数(也就是语句参数)的被调用存储过程的元数据:
```java
/// d ##class(PHA.TEST.SQL).MetaData4()
ClassMethod MetaData4()
{
SET $NAMESPACE="SAMPLES"
SET mysql = "CALL Sample.SP_Sample_By_Name(?)"
SET tStatement = ##class(%SQL.Statement).%New()
SET qStatus = tStatement.%Prepare(.mysql)
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
DO tStatement.%Metadata.%Display()
WRITE !,"End of metadata"
}
```
它不仅返回列(字段)信息,还返回语句参数,形式参数和对象的值。
以下示例返回具有三个形式参数的的元数据。这三个参数之一用问号(`?`)指定,使其成为语句参数:
```java
/// d ##class(PHA.TEST.SQL).MetaData5()
ClassMethod MetaData5()
{
SET $NAMESPACE="SAMPLES"
SET mycall = "CALL personsets(?,'MA')"
SET tStatement = ##class(%SQL.Statement).%New(0,"sample")
SET qStatus = tStatement.%Prepare(mycall)
IF qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT}
DO tStatement.%Metadata.%Display()
WRITE !,"End of metadata"
}
```
```java
DHC-APP>d ##class(PHA.TEST.SQL).MetaData5()
Columns (SQLRESULTCOL, property 'columns'):
Column Name Type Prec Scale Null Label Table Schema CType
----------- ---- ---- ----- ---- ------------ ------------ ------------ -----
Extended Column Info (SQLRESULTCOL)
Flags: 1:AutoIncrement,2:CaseSensitive,3:Currency,4:ReadOnly,5:RowVersion,
6:Unique,7:Aliased,8:Expression,9:Hidden,10:Identity,11:KeyColumn,
12:RowId
Column Name Linked Prop Type Class Flags
------------ --------------------- --------------------- -----------------------
Statement Parameters (property 'parameters'):
Nbr. Type precision scale nullable colName columntype
---- ---- --------- ----- -------- ------------ ----------
1 12 50 0 2 name 1
Formal Parameters (property 'formalParameters'):
Nbr. Type precision scale nullable colName columntype
---- ---- --------- ----- -------- ------------ ----------
1 4 4 0 2 _isc_sp_ret_val 5
2 12 50 0 2 name 1
3 12 50 0 2 state 1
Objects:
Col Column Name Extent ExportCall
--- ----------- ----------------- -----------------------------
End of metadata
```
请注意,此元数据不返回任何列信息,但是“语句参数”,“形式参数”列表包含列名称和数据类型。
## Query参数元数据
使用`%SQL.Statement`类进行`Prepare`之后,您可以返回有关查询参数的元数据:输入参数(指定为问号(`?`)),输入主机变量(指定为`:varname`)和常量(文字值)。可以返回以下元数据:
- `?`参数`:parameterCount`属性
- ODBC数据类型为`?`参数`:%SQL.StatementMetadata%Display()`实例方法“语句参数”列表。
- ?,v(:var)和c(常量)参数的列表:`%GetImplementationDetails()`实例方法,如成功准备的结果中所述。
- ?,v(:var)和c(常量)参数的ODBC数据类型:`formalParameters`属性。
`%SQL.StatementMetadata%Display()`实例方法“形式参数”列表。
- 查询文本,其中显示以下参数:`%GetImplementationDetails()`实例方法,如成功准备结果中所述。
语句元数据`%Display()`方法列出了“语句参数”和“形式参数”。对于每个参数,它列出了顺序参数号,ODBC数据类型,精度,小数位数,该参数是否可为空(2表示始终提供一个值)及其对应的属性名称(colName)和列类型。
请注意,某些ODBC数据类型以负整数形式返回。
下面的示例按顺序返回每个查询参数(`?`,`:var`和常量)的ODBC数据类型。请注意,`TOP`参数以数据类型12(`VARCHAR`)而不是数据类型4(`INTEGER`)返回,因为可以指定`TOP ALL`:
```java
/// d ##class(PHA.TEST.SQL).MetaData6()
ClassMethod MetaData6()
{
SET myquery = 4
SET myquery(1) = "SELECT TOP ? Name,DOB,Age+10 "
SET myquery(2) = "FROM Sample.Person"
SET myquery(3) = "WHERE %ID BETWEEN :startid :endid AND DOB=?"
SET myquery(4) = "ORDER BY $PIECE(Name,',',?)"
SET tStatement = ##class(%SQL.Statement).%New()
SET qStatus = tStatement.%Prepare(.myquery)
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
SET prepmeta = tStatement.%Metadata
WRITE "Number of ? parameters=",prepmeta.parameterCount,!
SET formalobj = prepmeta.formalParameters
SET i=1
WHILE formalobj.GetAt(i) {
SET prop=formalobj.GetAt(i)
WRITE prop.colName," type= ",prop.ODBCType,!
SET i=i+1
}
WRITE "End of metadata"
}
```
执行`Execute`之后,无法从查询结果集元数据中获取参数元数据。在结果集中,所有参数均已解析。因此`parameterCount = 0`,`formalParameters`不包含任何数据。
## Query结果集元数据
使用`%SQL.Statemen`t类执行`Execute`之后,可以通过调用返回结果集元数据:
- `%SQL.StatementResult`类的属性。
- `%SQL.StatementResult%GetMetadata()`方法,访问`%SQL.StatementMetadata`类属性。
### %SQL.StatementResult属性
执行查询操作后,`%SQL.StatementResult`返回:
- `%StatementType`属性返回与最近执行的SQL语句相对应的整数代码。以下是这些整数代码的部分列表:`1 = SELECT; 2 = INSERT; 3 = UPDATE; 4 = DELETE or TRUNCATE TABLE; 9 = CREATE TABLE; 15 = CREATE INDEX; 45 = CALL`.
- `%StatementTypeName`计算的属性基于`%StatementType`返回最近执行的SQL语句的命令名称。此名称以大写字母返回。请注意,`TRUNCATE TABLE`操作将作为`DELETE`返回。即使执行了更新操作,`INSERT OR UPDATE`也将作为`INSERT`返回。
- `%ResultColumnCount`属性返回结果集行中的列数。
下面的示例显示这些属性:
```java
/// d ##class(PHA.TEST.SQL).MetaData7()
ClassMethod MetaData7()
{
SET myquery = "SELECT TOP ? Name,DOB,Age FROM Sample.Person WHERE Age > ?"
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(10,55)
IF rset.%SQLCODE=0 {
WRITE "Statement type=",rset.%StatementType,!
WRITE "Statement name=",rset.%StatementTypeName,!
WRITE "Column count=",rset.%ResultColumnCount,!
WRITE "End of metadata"
} ELSE {
WRITE !,"SQLCODE=",rset.%SQLCODE," ",rset.%Message
}
}
```
### %SQL.StatementResult %GetMetadata()
执行之后,可以使用`%SQL.StatementResult %GetMetadata()`方法访问`%SQL.StatementMetadata`类属性。这些是在Prepare之后由`%SQL.Statement%Metadata`属性访问的相同属性。
以下示例显示了属性:
```java
/// d ##class(PHA.TEST.SQL).MetaData8()
ClassMethod MetaData8()
{
SET myquery=2
SET myquery(1)="SELECT Name AS VendorName,LastPayDate,MinPayment,NetDays,"
SET myquery(2)="AVG(MinPayment),$HOROLOG,%TABLENAME FROM Sample.Vendor"
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()
IF rset.%SQLCODE=0 {
SET rsmeta=rset.%GetMetadata()
SET x=rsmeta.columns.Count()
SET x=1
WHILE rsmeta.columns.GetAt(x) {
SET column=rsmeta.columns.GetAt(x)
WRITE !,x," ",column.colName," is data type ",column.ODBCType
WRITE " with a size of ",column.precision," and scale = ",column.scale
SET x=x+1 }
} ELSE {
WRITE !,"SQLCODE=",rset.%SQLCODE," ",rset.%Message
}
WRITE !,"End of metadata"
}
```
请注意,结果集元数据不提供参数元数据。这是因为`Execute`操作会解析所有参数。因此,在结果集中,`parameterCount = 0`,而`formalParameters`不包含任何数据。
# 审核动态SQL
InterSystems IRIS支持动态SQL语句的可选审核。启用%System /%SQL / DynamicStatement系统审核事件时,将执行动态SQL审核。默认情况下,未启用此系统审核事件。
如果启用%System /%SQL / DynamicStatement,则系统将自动审核在系统范围内执行的每个`%SQL.Statement`动态语句。审核将信息记录在审核数据库中。
要查看审核数据库,请依次转到管理门户,系统管理,选择安全性,审核,然后查看审核数据库。可以将“事件名称”过滤器设置为DynamicStatement,以将View Audit Database限制为Dynamic SQL语句。审核数据库列出了时间(本地时间戳),用户,PID(进程ID)和事件的描述。说明指定动态SQL语句的类型。例如,SQL SELECT语句(`%SQL.Statement`)或SQL CREATE VIEW语句(`%SQL.Statement`)。
通过选择事件的详细信息链接,可以列出其他信息,包括事件数据。事件数据包括执行的SQL语句和该语句的任何参数的值。例如:
```java
SELECT TOP ? Name , Age FROM Sample . MyTest WHERE Name %STARTSWITH ?
/*#OPTIONS {"DynamicSQLTypeList":",1"} */
Parameter values:
%CallArgs(1)=5
%CallArgs(2)="Fred"
```
事件数据的总长度(包括语句和参数)为3,632,952个字符。如果该语句和参数长于3632952,则事件数据将被截断。
InterSystems IRIS还支持ODBC和JDBC语句的审核(事件名称= XDBCStatement),以及嵌入式SQL语句的审核(事件名称= EmbeddedStatement)。
文章
Michael Lei · 七月 4, 2021
(ECP) Caché 出色的可用性和扩展特性之一是企业缓存协议 (ECP)。 在应用程序开发过程中,如对使用 ECP 的分布式处理加以考虑,可以横向扩展 Caché 应用程序的架构。 应用程序处理可以调整为非常高的速率,处理能力从单个应用程序服务器扩展到最多 255 个应用程序服务器,并且不需要任何应用程序更改。
在我参与的 TrakCare 部署中,ECP 已广泛使用多年。 十年前,主要供应商之一的一台“大型”x86 服务器可能总共只有八个核心。 对于大型部署来说,ECP 是横向扩展商业服务器处理能力的方式,不适合单台昂贵的大型企业服务器。 即使是高核心数的企业服务器也有限制,因此 ECP 也用于扩展这些服务器上的部署。
如今,大多数的新 TrakCare 部署或升级到当前硬件_不需要 ECP_ 即可扩展。 目前的双插槽 x86 生产服务器可以拥有数十个核心和巨大容量的内存。 我们看到,在最近的 Caché 版本中,TrakCare 以及许多其他 Caché 应用程序具有可预测的线性扩展能力,能够随着单台服务器中 CPU 核心数量和内存的增加而支持逐渐增多的用户和事务。 在现场,我看到大多数的新部署都是虚拟化的,即使如此,虚拟机也可以根据需要扩展到主机服务器的规模。 如果资源需求超过单个物理主机可以提供的资源,则使用 ECP 进行横向扩展。
- ___提示:___ _为了简化管理和部署规模,在部署 ECP 之前,先在单台服务器内扩展。_
在本帖中,我将展示一个示例架构以及 ECP 工作原理的基础知识,然后评论性能注意事项,重点是存储。
有关配置 ECP 和应用程序开发的具体信息,请参见在线的 [Caché 分布式数据管理指南](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GDDM),并且[社区上有一个 ECP 学习轨迹](https://community.intersystems.com/learning-track/enterprise-cache-protocol-ecp-videos)。
ECP 的其他关键特性之一是提高了应用程序可用性,有关详细信息,请参见 [Caché 高可用性指南](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GHA_ecp)中的 ECP 部分。
[本系列其他帖子的列表](https://cn.community.intersystems.com/post/intersystems-数据平台的容量规划和性能系列文章)
# ECP 架构基础知识
ECP 的架构和运行在概念上很简单,ECP 提供了在多个服务器系统之间有效共享数据、锁定和可执行代码的方法。 从应用程序服务器角度看,数据和代码远程存储在_数据服务器_上,但缓存在_应用程序服务器_的本地内存中,以提供对活动数据的有效访问,同时尽可能减少网络流量。
数据服务器管理对磁盘上持久性存储的数据库读写,而多个应用程序服务器是解决方案的主力,执行大多数应用程序处理。
## 多层架构
ECP 采用多层架构。 描述处理层和它们扮演的角色有多种不同的方式,以下是我在描述基于 Web 浏览器的 Caché 应用程序时发现很有用的方式,也是我的帖子的模型和术语。 我知道可能有不同的方法来细分层级,但现在先使用我的方法 :)
基于浏览器的应用程序(例如 Caché Server Pages (CSP))使用多层架构,其中表示、应用程序处理和数据管理功能在逻辑上是分开的。 具有不同角色的__逻辑__“服务器”填充各层。 逻辑服务器不必保留在单独的物理主机或虚拟服务器上,出于成本效益和可管理性的考虑,部分甚至全部逻辑服务器可能位于单个主机或操作系统实例上。 随着部署规模的扩展,服务器可以通过 ECP 划分到多个物理或虚拟主机上,从而可根据需要分散处理工作负载,而无需更改应用程序。
主机系统可以是物理的或虚拟化的,具体取决于容量和可用性要求。 以下层和逻辑服务器构成了一个部署:
- _表示层:_包括在基于浏览器的客户端和应用程序层之间充当网关的 Web 服务器。
- _应用程序层:_这是 ECP 应用程序服务器所在的位置。 如上文所述,这是一个逻辑模型,其中应用程序服务器不必与数据服务器分开,而且除了最大型的站点外,所有情况下通常都不需要分开。 该层还可能包括进行专门处理的其他服务器,如报告服务器。
- _数据层:_这是数据服务器所在的位置。 数据服务器执行事务处理,是存储在 Caché 数据库中的应用程序代码和数据存储库。 数据服务器负责读写持久性磁盘存储。
## 逻辑架构
下图是一个基于浏览器的应用程序在部署为三层架构时的逻辑视图:
尽管初看之下该架构可能很复杂,但构成它的组件仍然与安装在单台服务器上的 Caché 系统的组件相同,只是逻辑组件安装在多个物理或虚拟服务器上。 服务器之间的所有通信都通过 TCP/IP 进行。
### 逻辑视图中的 ECP 操作
上图从顶部开始,显示用户安全地连接到多个已进行负载平衡的 Web 服务器。 这些 Web 服务器在客户端和应用程序层(应用程序服务器)之间传递 CSP 网页请求,应用程序层进行所有处理,允许动态创建内容,并通过 Web 服务器将完成的页面返回给客户端。
在这个三层模型中,应用程序处理通过 ECP 分散到多个应用程序服务器上。 应用程序只将数据(您的应用程序数据库)视为应用程序服务器的本地数据。
当应用程序服务器发出数据请求时,它将尝试从本地缓存满足请求,如果不能满足,ECP 将向数据服务器请求必要的数据,数据服务器自己的缓存可能会满足请求,否则将从磁盘获取数据。 数据服务器对应用程序服务器的回复包括存储该数据的数据库块。 这些块将被使用,并且此时将缓存到应用程序服务器上。 ECP 自动负责管理整个网络中的缓存一致性,并将变化传播回数据服务器。 客户端会体验到快速响应,因为它们经常使用本地缓存的数据。
默认情况下,Web 服务器与首选的应用程序服务器通信,确保同一应用程序服务器满足相关数据的后续请求,因为这些数据可能已经在本地缓存中。
- ___提示:___ _如 [Caché 文档](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GDDM_develop#GDDM_develop_considerations)中详述,在循环或负载平衡方案中,应避免用户连接到应用程序服务器,因为这会影响应用程序服务器上缓存的优势。 理想情况下,相同的用户或用户组保持连接到同一应用程序服务器。_
该解决方案通过在表示层添加 Web 服务器和在应用程序层添加其他应用程序服务器来进行扩展,无需用户停机。 数据层通过增加数据服务器的 CPU 和内存来进行扩展。
## 物理架构
下图显示了与三层逻辑架构示例相同的三层部署中使用的物理主机示例:
请注意,在每一层部署物理或虚拟主机时均采用 n+1 或 n+2 模式,以确保在主机故障或计划维护时保持 100% 能力。 由于用户分布在多个 Web 和应用程序服务器上,单个服务器故障只会影响少量用户,他们会自动重新连接到其余服务器之一。
数据管理层具有高度可用性,例如,位于连接到一个或多个存储阵列的故障转移集群上(例如,虚拟化 HA、InterSystems 数据库镜像或传统的故障转移集群)。 如果硬件或服务出现故障,集群将在其中一个幸存节点上重启服务。 ECP 的一个附加好处是内置弹性,并且在数据库节点集群发生故障转移时能保持事务完整性,应用程序用户将观察到处理暂停,直到故障转移和自动恢复完成,随后用户将无缝继续,不会断开连接。
同样的架构也可以映射到虚拟化服务器,例如,VMware vSphere 可用于虚拟化应用程序服务器。
# ECP 容量规划
如上文所述,数据服务器管理对持久性磁盘的数据库读写,而多个应用程序服务器是解决方案的主力,执行大多数应用程序处理。 这是考虑系统资源容量规划时的一个关键概念,总结来说:
- ___数据服务器___(有时称为数据库服务器)通常执行很少的应用程序处理,因此_对 CPU 要求低_,但该服务器执行大部分存储 IO,因此可能有_非常高的存储 IOPS_,即数据库读写以及日志写入(稍后将详细介绍日志 IO)。
- ___应用程序服务器___执行大多数应用程序处理,因此_对 CPU 要求高_,但存储 IO 非常少。
通常,调整 ECP 服务器 CPU、内存和 IO 要求的规则与调整非常大的单服务器解决方案的规则相同,同时考虑 N+1 或 N+2 台服务器以确保高可用性。
## 基本 CPU 和存储规模调整:
假设 My_Application 需要最多 72 个 CPU 核心进行应用程序处理(记得还要考虑余量),并且预计在写入守护进程周期期间需要 20,000 次写入,以及 10,000 次随机数据库读取的持续峰值。
一个简单的虚拟或物理服务器规模调整方案为:
- 4 台 32 CPU 应用程序服务器(3 台服务器 + 1 台服务器用于确保 HA)。 低 IOPS 要求。
- 2 台 10 CPU 数据服务器(镜像或集群以确保 HA)。 [低延迟 IOPS 要求](https://cn.community.intersystems.com/post/数据平台和性能-第-6-部分-caché-存储-io-配置文件)为 20K 写入、10K 读取,加上 WIJ 和日志。
虽然数据服务器只执行非常少的处理,但考虑到系统和 Caché 进程,将其规模调整为 8-10 个 CPU。 应用程序服务器的规模可以根据每台物理主机的最佳性价比和/或可用性来进行调整。 横向扩展时会有一些效率损失,但通常可以在服务器块中增加处理能力,并预计吞吐量有近乎线性的增长。 限制更有可能首先在存储 IO 中出现。
- ___提示:____与确保 HA 一样,要考虑主机、机箱或机架故障的影响。 在 VMWare 上虚拟化应用程序和数据服务器时,确保应用 vSphere DRS 和相关性规则以分散处理负载并确保可用性。_
## 日志同步 IO 要求
ECP 部署的另一个容量规划注意事项是,由于日志同步,它们需要较高 IO,并且对存储响应时间的要求非常严格,以保持数据服务器上日志记录的可伸缩性 。 同步请求可以触发对日志中最后一个块的写入,以确保数据耐久性。
不过您的情况可能有所不同;在一个典型的以高事务处理速率运行的客户站点上,我经常看到非 ECP 配置上的日志写入 IOPS 为每秒十几次。 在繁忙的系统上使用 ECP 时,由于 ECP 强制日志同步,可以在日志磁盘上看到 100 到 1000 的写入 IOPS。
- ___提示:____如果在繁忙的系统上显示 mgstat 或查看 [pButtons](https://cn.community.intersystems.com/post/intersystems-数据平台和性能-–-第-1-篇) 中的 mgstat,您将看到 Jrnwrts(日志写入次数),您将在存储 IO 资源规划中对其加以考虑。 在 ECP 数据服务器上,还有未显示在 mgstat 中的对日志磁盘的日志同步写入,要了解这些信息,您需要查看日志磁盘的操作系统指标,例如使用 iostat 查看_。
### 什么是日志同步?
需要日志同步的原因:
- 确保在数据服务器发生故障时数据的耐久性和可恢复性。
- 它们也是确保应用程序服务器之间的缓存一致性的触发器。
在非 ECP 配置中,对 Caché 数据库的修改将写入日志缓冲区(128 x 64K 缓冲区),当日志缓冲区满时或每两秒由日志守护程序写入磁盘上的日志文件。 Caché 为整个缓冲区分配 64k,并且这些缓冲区总是被重复使用,而不是被销毁和重新创建,Caché 只是跟踪末尾偏移量。 在大多数情况下(除非一次进行大量更新),日志写入次数非常小。
ECP 系统中也有日志同步。 日志同步可以定义为将当前日志缓冲区的相关部分重新写入磁盘,以确保磁盘上的日志始终是最新的。 因此,日志同步会请求多次重新写入同一日志块的某个部分(大小在 2k 到 64k 之间)。
ECP 客户端上可以触发日志同步请求的事件为更新(SET 或 KILL)或 LOCK。 例如,对于每个 SET 或 KILL,都会将当前日志缓冲区写入(或重新写入)磁盘。 在非常繁忙的系统中,单次同步操作中的日志同步可能被捆绑或延迟为多个同步请求。
### 日志同步的容量规划
为确保持续的吞吐量,日志同步的平均写入响应时间必须:
- _