清除过滤器
文章
Michael Lei · 六月 23, 2021
部分 Ansible 帮助我解决了快速部署 Caché 和应用程序组件以进行数据平台基准测试的问题。 您可以使用相同的工具和方法来建立您的测试实验室、培训系统、开发或其他环境。 如果在客户站点部署应用程序,可以将大量部署自动化,并确保系统、Caché 和应用程序的配置符合您的应用程序最佳做法标准。
## 概述
作为一名技术架构师,我们的团队职责之一是在不同供应商的硬件和操作系统上对 InterSystems 数据平台进行基准测试。 通常,基础架构是预发布版,在必须归还或移交给其他人之前,我们的时间有限,因此,快速准确地设置基准测试,让我们有尽可能多的时间来做真正的基准测试工作,这一点至关重要。
多年来,我们通过 shell 脚本小程序来自动执行许多基准测试安装任务,并从速查表和检查清单中剪切和粘贴,但此类操作非常密集,而且容易出错,特别是有许多服务器并且在不同的操作系统之间切换时 - 在 SLES 11、Red Hat 6、Red Hat 7、AIX 等操作系统上安装或使用服务的差异可能很微小,让人厌烦。
在研究了几个可用于自动化配置和管理系统的软件选项之后,我选择了 Ansible 来执行预置数据平台、应用程序和基准测试组件的任务。 需要注意的是,我并没有规定 Ansible 是部署和配置的 **THE** 解决方案。 在选择 Ansible 之前,我研究了其他工具(如 Puppet 和 Chef)的功能和操作。 如果您的组织已经在使用其他工具,您可以使用它们 — 我在 Ansible 中使用的方法和命令等等应该可以转换到其他软件中,我希望这些帖子可以帮助您,不管您使用的是什么工具。
这是本系列的第一个帖子,将介绍在部署 InterSystems 数据平台应用程序时如何使用 Ansible。 本帖介绍如何通过安装 Caché 打下基础,下一帖将扩展解决方案以包括应用程序安装,包括使用 %installer 类。 本帖涵盖:
* Ansible 的概述和安装
* Ansible 便于管理和扩展的布局。
* 同时在一个或多个服务器上安装 Caché。
## 什么是 Ansible?
通过 Ansible 可以在配置一个或多个服务器的同时将复杂的任务自动化,并可以非常简单地添加新服务器。 任务会设计成幂等(您可以在同一台服务器上多次运行相同的脚本,得到的服务器配置将是相同的)。
我选择 Ansible 执行预置任务的一个关键原因是它对系统的要求最低(Python 2.7,Linux 服务器上自带 ),而且它是一个自包含解决方案 — Ansible 代码只安装在控制服务器上,并使用推送架构,通过 OpenSSH 在目标服务器上运行命令和脚本。 所预置的服务器上不需要任何代理。 作为对比,Chef 和 Puppet 采用拉取架构,软件在客户端服务器(Web、数据库等)上加载,并且客户端不断轮询主服务器以查找更新。 Ansible 的推送架构也适合按照您的计划需求逐步实施服务器。
Ansible 是开源的,由社区维护。 Ansible, Inc 从 2015 年开始为 Red Hat 所拥有。 Ansible, Inc 有一个高级的生命周期产品 (Ansible Tower),并提供收费的支持和培训,不过本帖中的所有内容均使用开源命令行版本。 还有一个活跃的社区 (Ansible Galaxy),您可以从中下载许多预制的解决方案来完成大量任务,如安装 Web 服务器、ftp、kerbros,不胜枚举。 例如,对于完整的基准测试部署项目,我包括了一个下载的 Apache 模块,并自定义成在 RHEL、SLES 或 Solaris(以及其他平台)上安装和配置 Apache 2.x。
Ansible 的下载和安装说明可以在 Ansible 网站和 Github 上找到。 如果您有问题或希望做出贡献,可以访问活跃社区。
https://www.ansible.com/get-startedhttp://docs.ansible.com
## 安装 Ansible
本帖中的示例已经在运行 Red Hat 7.0 和 7.2 的虚拟机上进行了测试 - 我也在我的安装了 Centos 7 的笔记本电脑上使用 virtual box 和 vagrant 对 Ansible 控制器服务器进行了初始测试。 Caché 不需要安装在控制器上,所以您的操作系统选择要多于 Caché 支持的平台列表。 为了简单起见,我使用了当前适用于 Red Hat 的 rpm 版本的 Ansible (Ansible 1.9.4),更高的版本可以从 GitHub 获取。
在示例中,我安装的是 cache-2015.2.2.805.0-lnxrhx64,但相同的常规过程也适用于 HealthShare 或 Ensemble 发行版。 您将在后面看到,我们使用特定文件名、目录路径等变量来参数化安装选项。
在第一个帖子中,我将任务削减为基本的 Caché 安装,因此大多数任务是独立于平台的。 当 Ansible playbook 启动时,首要任务之一是获取目标机器的清单 — 操作系统、接口卡、内存详细信息、CPU 数量、磁盘布局等,当运行命令以从目标上运行的实际命令(例如,Red Hat 上的 service start httpd 与 SLES 上的 /etc/init.d/apache2 restart)中提取 Ansible 脚本命令时,将使用目标操作系统的这些信息。
我假定您已读过说明,并且已按照您的平台说明在控制机上安装了 Ansible。
Ansible 必须使用 Linux 系统作为控制器,但目标系统可以是 Linux 或 Windows。 有关 Windows 目标的更多信息,请参见 Ansible 文档。
### 控制器系统安装示例:在 RHEL/CentOS 7 64 位上安装 Ansible
在 Red Hat 或 CentOS 上,必须先安装 epel-release (Extra Packages for Enterprise Linux) RPM,其中包含 Ansible。 epel 项目面向主要 Linux 发行版设计,提供了许多有用的开源软件包(网络、系统管理、监视等)。
[root@localhost tmp]# wget http://dl.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-7-5.noarch.rpm
:
:
[root@localhost tmp]# rpm -ivh epel-release-7-5.noarch.rpm
:
:
[root@localhost tmp]# yum --enablerepo=epel info ansible
Loaded plugins: langpacks, product-id, search-disabled-repos, subscription-manager
Installed Packages
Name : ansible
Arch : noarch
Version : 1.9.4
Release : 1.el7
Size : 7.0 M
Repo : installed
From repo : epel
Summary : SSH-based configuration management, deployment, and task execution system
URL : http://ansible.com
License : GPLv3+
Description :
: Ansible is a radically simple model-driven configuration management,
: multi-node deployment, and remote task execution system. Ansible works
: over SSH and does not require any software or daemons to be installed
: on remote nodes. Extension modules can be written in any language and
: are transferred to managed machines automatically.
[root@localhost tmp]#
[root@localhost tmp]# sudo yum install ansible
:
:
[root@localhost tmp]# ansible --version
ansible 1.9.4
configured module search path = None
**很好... 准备开始!**
## Ansible 方面
关于不同的 Ansible 组件(清单、Playbook、模块、角色等)的用法,应查看 Ansible 文档。
为了简化管理以及避免使用大而复杂的脚本文件,使用了预定义的目录结构和搜索路径。 在本帖中,我将使用一个采用 Ansible 建议的文件结构,当您考虑构建更大的安装示例时,可以将本结构用作模型。
所使用的 Ansible 模块带有注释和自我说明,可以在 Github 上获取。 下载文件并通读以了解工作流程。
我们的示例的基本目录包含以下文件;
* ansible.cfg:对 Ansible 默认值的更改。
* 清单:定义和描述工作环境。 例如服务器名称/IP。
* <各种>.yml 文件:这些文件描述了将为特定服务器角色运行的任务集。
### 术语
为了使后面的讨论更清晰,下面是一些 Ansible 术语的快速解释。
模块是用于创建对系统执行的自动化操作的构建块。 每个模块都为特定任务构建,可以使用参数更改该任务。 例如复制文件、创建用户、运行命令、启动服务等。 目前,默认的 Ansible 安装已包含 400 多个模块,另外还有更多来自社区的模块,您也可以创建您自己的模块。
模块组合在一起生成 play 和 playbook,作为执行自动化工作流程的一种方式。 一个 Play 可以有多个任务,一个 Playbook 可以有多个 Play。
角色允许您组合 Playbook。 角色可以被视为根据目标服务器使用情况分组在一起的服务器组件配置。 在本帖的示例中,角色构建配置层以构建服务器。
在我的基准测试设置中,我有以下角色来构建服务器:
* hs\_server\_common:配置操作系统,安装 Apache,安装 Caché。
* webserver:复制 Web 文件(csp、html、js 等),针对应用程序配置 Apache。
* generator:复制文件,创建和配置 webstress 生成器数据库、命名空间、全局映射等。
* dbserver:复制文件,配置数据库服务器系统设置、应用程序数据库、命名空间、全局映射等。
可以组合这些角色来构建不同的服务器类型:
* hs\_server\_common + webserver + generator = webstress 生成器服务器。
* hs\_server\_common + webserver = 应用程序 Web 服务器。
* hs\_server\_common + dbsevrer = 数据库服务器。
角色的组成以及每个角色中包含的配置将非常特定于要部署的应用程序。 本帖中的示例将使用最小的任务集,并假定操作系统已预先配置,但是,使用 Ansible 和 Galaxy 上的模块可以实现更复杂和全功能的系统配置。
## 关于安装 Caché 的说明 {.MsoNormal}
我写了几个例子来介绍一些有趣和有用的特性,下面是一些精华部分。 注意:这些示例可以用作 InterSystems 数据平台(Caché、HealthShare 和 Ensemble)的安装指南。 我写的示例是安装 HealthShare,但 HealthShare 和 Caché 示例中的操作是相同的。
**./testserver/roles/hs\_server\_common/tasks/main.yml**
这是配置操作系统、安装 Apache、安装 Caché 等常见任务的主线脚本。 本帖中对其进行了删减,使其只包含用于 Red Hat 的文件,以及只安装和配置 Caché。 您可以看到,Ansible 在启动后已经在 _ansible_*_ 变量(包括 _ansible\_os\_family_)中保存了操作系统信息,我们可以在脚本中使用这些变量做出决策。
**./testserver/roles/hs\_server\_common/tasks/configure-healthshare2015.yml**
这是用于安装 Caché 的主脚本。 浏览该脚本,会看到针对目标执行的任务的逻辑工作流程,包括:
* 创建操作系统用户和组。
* 从控制器上的清单文件夹复制安装文件。
* 解压缩安装文件。
* 使用静默安装方式安装 Caché(请参见下面的注释)。
* 复制 Caché 密钥文件
* 设置默认 Caché 实例
* 重启 Apache
* 重启 Caché
Caché 的静默安装有几个选项,包括:
* 使用 parameters.is 文件。 模板 .isc 文件由以前的安装创建,可以按原样使用,也可以修改。
* 使用 cinstall_silent 以及环境中设置的键值对。
* 使用 %installer 类。
在此示例中,我选择了使用 install\_silent,但是我还包括了一个注释掉的使用参数文件的备用方法,以说明如何在 Ansible 中使用模板文件(请参见 /roles/hs\_server\_common/templates/parameters\_hs20152_rh64.isc)。
在以后的帖子中,我将说明如何使用 %installer 类安装 Caché 以及设置数据库和命名空间。 有关安装选项的详细信息,请参见 Caché 在线文档,社区中也有一个非常好的帖子介绍了 %installer 类的使用。
当您想要安装并配置 Caché,以将 CSPGateway 与除了旧版本 Caché 内部的 Apache 版本以外的 Web 服务器一起使用时,参数文件很有用。 自 Caché 2016.1 起,%installer 提供此功能。
**./testserver/roles/hs\_server\_common/tasks/setup_RedHat.yml**
包含此示例是为了说明如何使用系统特定变量 (ansible_*) 和设置操作系统变量。
**./testserver/roles/hs\_server\_common/vars/***
变量文件包含键:值对形式的变量,如您所见,这是一种在不同环境和情况下重复使用相同脚本的方法。
## 运行 Caché 安装
对于此示例,我假定系统可用,并按如下方式进行设置。
1. 控制器已安装 Ansible,并且以下目录填充了来自 Github 的文件和结构。
* **./testserver/***:包含清单、.yml 文件等等的目录树。 包括...
* **./testserver/Distribution_Files/Cache**:(包含 Caché 分发包和 cache.key 的清单)。
2. 目标机器已安装 Red Hat 和 Apache。
您需要编辑以下文件来为您的测试环境自定义安装。
1. **inventory_test**
您需要编辑测试服务器名称或 IP 地址。
2. .**/testserver/roles/hs\_server\_common/vars/healthshare2015.yml**
您必须编辑路径以适合您的测试环境。 查看目标服务器的以下路径:
* **common\_install\_base_path**:清单文件将复制到该位置后解压,并运行 Caché 安装。
* **ISC\_PACKAGE\_INSTALLDIR**:Caché 安装目录
如果目标服务器上不存在这些目录路径,将会创建。
注:自动化部署的特性之一是并行构建多个服务器。 如果清单文件中有多个服务器,在各个目标服务器上将并发运行每个步骤,直至该步骤完成,然后再在组中的每个服务器上开始下一个步骤。 如果任何服务器上有步骤失败,脚本将停止。 您将看到一条错误消息,帮助您更正问题。 更正错误后,只需从头重新运行 — 这是脚本的一个关键特性 — 脚本设计成幂等。 幂等的意思是,在某个步骤运行某个模块(例如复制文件)时,如果文件已经存在,则该步骤不会重新运行,脚本只是继续下一个步骤。 copy 之类的模块有参数可以设置为强制复制,但这不是默认设置。 仔细检查脚本会发现,在某些情况下使用了“creates”参数,例如:
- name: unattended install of hs using cinstall_silent
shell: >
ISC_PACKAGE_INSTANCENAME="{{ ISC_PACKAGE_INSTANCENAME }}"
ISC_PACKAGE_INSTALLDIR="{{ ISC_PACKAGE_INSTALLDIR }}"
ISC_PACKAGE_UNICODE="{{ ISC_PACKAGE_UNICODE }}"
ISC_PACKAGE_INITIAL_SECURITY="{{ ISC_PACKAGE_INITIAL_SECURITY }}"
ISC_PACKAGE_MGRUSER="{{ ISC_PACKAGE_MGRUSER }}"
ISC_PACKAGE_MGRGROUP="{{ ISC_PACKAGE_MGRGROUP }}"
ISC_PACKAGE_USER_PASSWORD="{{ ISC_PACKAGE_USER_PASSWORD }}"
ISC_PACKAGE_CACHEUSER="{{ ISC_PACKAGE_CACHEUSER }}"
ISC_PACKAGE_CACHEGROUP="{{ ISC_PACKAGE_CACHEGROUP }}" ./cinstall_silent
chdir="{{ common_install_base_path }}/{{ hs_install_unpack_path }}"
args:
creates: "{{ ISC_PACKAGE_INSTALLDIR }}/cinstall.log"
上面一节使用 creates 参数告诉 Ansible 模块(在本例中是 shell 模块),此操作创建 cinstall.log 文件。 如果模块发现该文件(Caché 已经安装),则此步骤将不会运行。
好了,全部设置完毕后,我们可以运行安装了。
$ ansible-playbook dbserver.yml
PLAY [dbservers] **************************************************************
GATHERING FACTS ***************************************************************
ok: [db1]
TASK: [hs_server_common | include_vars healthshare2015.yml] *******************
ok: [db1]
TASK: [hs_server_common | include_vars os-RedHat.yml] *************************
ok: [db1]
etc
etc
etc
TASK: [hs_server_common | Create default cache group] *************************
changed: [db1]
TASK: [hs_server_common | Create default cache manager group] *****************
changed: [db1]
TASK: [hs_server_common | Create default cache user] **************************
changed: [db1]
TASK: [hs_server_common | Create default cache system users] ******************
changed: [db1]
TASK: [hs_server_common | Create full hs install temp directory] **************
changed: [db1]
TASK: [hs_server_common | Check tar file (gunzipped already) does not exist] ***
ok: [db1]
TASK: [hs_server_common | Copy healthshare install file] **********************
changed: [db1]
TASK: [hs_server_common | un zip hs folder] ***********************************
changed: [db1]
TASK: [hs_server_common | un tar hs install] **********************************
changed: [db1]
TASK: [hs_server_common | Create hs install directory] ************************
changed: [db1]
TASK: [hs_server_common | touch ztrak.conf.] **********************************
changed: [db1]
TASK: [hs_server_common | Process parameters file] ****************************
changed: [db1]
TASK: [hs_server_common | unattended install of hs using cinstall_silent] *****
changed: [db1]
TASK: [hs_server_common | copy hs key] ****************************************
changed: [db1]
TASK: [hs_server_common | Set default hs instance] ****************************
changed: [db1]
TASK: [hs_server_common | restart apache to initialize CSP.ini file] **********
changed: [db1]
NOTIFIED: [hs_server_common | restart healthshare] ****************************
changed: [db1]
PLAY RECAP ********************************************************************
db1 : ok=32 changed=21 unreachable=0 failed=0
如果我们查看目标服务器 — 数据库服务器 Caché 现在已经启动并运行。
$ ccontrol list
Configuration 'H2015' (default)
directory: /test/hs2015
versionid: 2015.2.1.705.0
conf file: cache.cpf (SuperServer port = 1972, WebServer = 57772)
status: running, since Wed Feb 17 15:59:11 2016
state: ok
## 总结
在后续的帖子中,我将构建包含其他任务的脚本,如编辑配置文件和使用 %installer 类配置应用程序。
如果您对此感兴趣并开始创建您自己的部署,请随时与我联系,提出问题或建议。 我经常在全球峰会上发表关于虚拟化和性能的演讲 - 因此,如果您参加今年的全球峰会,请介绍一下您自己,我非常乐意和您聊一聊 Ansible 的使用经验或任何其他系统架构话题。
文章
姚 鑫 · 四月 16, 2021
# 第二章 定义和构建索引(四)
# 位片索引
当数字数据字段用于某些数值运算时,位片索引用于该字段。位片索引将每个数值数据值表示为二进制位串。位片索引不是使用布尔标志来索引数值数据值(如在位图索引中那样),而是以二进制值表示每个值,并为二进制值中的每个数字创建一个位图,以记录哪些行的该二进制数字具有1。这是一种高度专门化的索引类型,可以显著提高以下操作的性能:
- `SUM`、`COUNT`或`AVG` Aggregate计算。(位片索引不用于`COUNT(*)`计算。)。位片索引不用于其他聚合函数。
- 指定的字段 `TOP n ... ORDER BY field`
- 在范围条件运算中指定的字段,`WHERE field > n` 或 `WHERE field BETWEEN lownum AND highnum`、
SQL优化器确定是否应该使用定义的位片索引。通常,优化器仅在处理大量(数千)行时才使用位片索引。
可以为字符串数据字段创建位片索引,但位片索引将这些数据值表示为规范数字。换句话说,任何非数字字符串(如`“abc”`)都将被索引为0。这种类型的位片索引可用于快速计数具有字符串字段值的记录,而不计算那些为空的记录。
在下面的例子中,`Salary`是位片索引的候选项:
```sql
SELECT AVG(Salary) FROM SalesPerson
```
位片索引可用于使用`WHERE`子句的查询中的聚合计算。如果`WHERE`子句包含大量记录,则这是最有效的。在下面的示例中,SQL优化器可能会使用`Salary`上的位片索引(如果已定义);如果定义了位片索引,它还会使用`REGION`上的位图索引,使用定义的位图或为`REGION`生成位图临时文件:
```sql
SELECT AVG(Salary) FROM SalesPerson WHERE Region=2
```
但是,当索引无法满足`WHERE`条件时,不使用位片索引,而必须通过读取包含要聚合的字段的表来执行。以下示例将不使用`Salary`的位片索引:
```sql
SELECT AVG(Salary) FROM SalesPerson WHERE Name LIKE '%Mc%'
```
可以为任何包含数值的字段定义位片索引。InterSystems SQL使用`Scale`参数将小数转换为位字符串,如ObjectScript `$factor`函数中所述。可以为数据类型字符串的字段定义位片索引;在这种情况下,出于位片索引的目的,非数字字符串数据值被视为`0`。
可以为系统分配的行ID为正整数值的表中的字段定义位片索引,也可以为使用`%BID`属性定义以支持位图(和位片)索引的表中的字段定义位片索引。
位片索引只能为单个字段名定义,不能为多个字段的连接定义。
不能指定`WITH DATA`子句。
下面的例子比较了位片索引和位图索引。
如果你为1、5和22行创建一个位图索引,它会为这些值创建一个索引:
```java
^gloI("bitmap",1,1)= "100"
^gloI("bitmap",5,1)= "010"
^gloI("bitmap",22,1)="001"
```
如果为第1、2和3行的值1、5和22创建位切片索引,则会首先将这些值转换为位值:
```java
1 = 00001
5 = 00101
22 = 10110
```
然后,它为这些位创建索引:
```java
^gloI("bitslice",1,1)="110"
^gloI("bitslice",2,1)="001"
^gloI("bitslice",3,1)="011"
^gloI("bitslice",4,1)="000"
^gloI("bitslice",5,1)="001"
```
在本例中,位图索引中的值22需要设置1个全局节点;位片索引中的值22需要设置3个全局节点。
请注意,插入或更新需要在所有`n`个位片中设置一个位,而不是设置单个位串。这些附加的全局设置操作可能会影响涉及填充位片索引的插入和更新操作的性能。使用`INSERT`、`UPDATE`或`DELETE`操作填充和维护位片索引比填充位图索引或常规索引慢。维护多个位片索引和/或在频繁更新的字段上维护位片索引可能具有显著的性能成本。
在易失性表(执行许多插入、更新和删除操作)中,位片索引的存储效率可能会逐渐降低。`%SYS.Maint.Bitmap`实用程序方法同时压缩位图索引和位片索引,从而提高了还原效率。
# 重建索引
可以按如下方式构建/重新构建索引:
- 使用`BUILD INDEX` SQL命令构建指定索引,或构建为表、架构或当前命名空间定义的所有索引。
- 使用管理门户重建指定类(表)的所有索引。
- 使用`%BuildIndices()`(或`%BuildIndicesAsync()`)方法,如本节所述。
当前数据库访问确定应如何重建现有索引:
- 非活动系统(在索引构建或重建期间没有其他进程访问数据)
- `READONLY`活动系统(能够在索引构建或重建期间查询数据的其他进程)
- 读写活动系统(能够在索引构建或重建期间修改数据和查询数据的其他进程)
构建索引的首选方法是使用`%BuildIndices()`方法或`%BuildIndicesAsync()`方法。
- `%Library.Persistent.%BuildIndices()`:`%BuildIndices()`作为后台进程执行,但调用方必须等待`%BuildIndices()`完成才能接收回控制。
- `%Library.Persistent.%BuildIndicesAsync()`:`%BuildIndicesAsync()`将`%BuildIndices()`作为后台进程启动,调用方立即收到控制权。`%BuildIndicesAsync()`的第一个参数是`eueToken`输出参数。其余参数与`%BuildIndices()`相同。
`%BuildIndicesAsync()`返回`%Status`值:`Success`表示`%BuildIndices()`辅助作业已成功排队;失败表示该辅助作业未成功排队。
`%BuildIndicesAsync()`向`eueToken`输出参数返回一个值,该值指示`%BuildIndices()`完成状态。要获取完成状态,请通过引用将`eueToken`值传递`给%BuildIndicesAsyncResponse()`方法。还可以指定等待布尔值。如果`wait=1`,则`%BuildIndicesAsyncResponse()`将等待,直到由`eueToken`标识的`%BuildIndices()` JOB 完成。如果`wait=0`,`%BuildIndicesAsyncResponse()`将尽快返回状态值。如果返回时`%BuildIndicesAsyncResponse() ``eueToken`不为空,则`%BuildIndices()` job尚未完成。在这种情况下,可以使用`eueToken`再次调用`%BuildIndicesAsyncResponse()`。当`%BuildIndicesAsyncResponse()``eueToken`最终为`NULL`时,返回的`%BuildIndicesAsyncResponse()``%Status`值是`%BuildIndicesAsync()`调用的job的完成状态。
## 在非活动系统上构建索引
系统自动生成方法(由`%Persistent`类提供),这些方法构建或清除为类(表)定义的每个索引。可以通过以下两种方式之一使用这些方法:
- 通过管理门户进行交互。
- 以编程方式,作为方法调用。
构建索引执行以下操作:
1. 删除索引的当前内容。
2. 扫描(读取每一行)主表,并为表中的每一行添加索引项。如果可能,使用特殊的`$SortBegin`和`$SortEnd`函数来确保高效地构建大型索引。在构建标准索引时,除了在内存中缓存数据之外,使用`$SortBegin`/`$SortEnd`还可以使用`IRISTEMP`数据库中的空间。因此,在构建非常大的标准索引时,InterSystems IRIS可能需要`IRISTEMP`中大致等于最终索引大小的空间。
注:构建索引的方法仅为使用InterSystems IRIS默认存储结构的类(表)提供。映射到遗留存储结构的类不支持索引构建,因为它假定遗留应用程序管理索引的创建。
### 使用管理门户构建索引
可以通过执行以下操作来构建表的现有索引(重建索引):
1. 从管理门户中选择系统资源管理器,然后选择SQL。使用页面顶部的切换选项选择一个命名空间;这将显示可用命名空间的列表。选择命名空间后,选择屏幕左侧的`Schema`下拉列表。这将显示当前名称空间中的模式列表,其中带有布尔标志,指示是否有任何表或视图与每个模式相关联。
2. 从此列表中选择一个架构;该架构将显示在架构框中。它的正上方是一个下拉列表,允许选择属于该模式的表、系统表、视图、过程或所有这些。选择“表”或“全部”,然后打开“表”文件夹以列出此架构中的表。如果没有表,则打开文件夹将显示空白页。(如果未选择“表”或“全部”,则打开“表”文件夹将列出整个命名空间的表。)
3. 选择其中一个列出的表。这将显示表的目录详细信息。
- 要重建所有索引:单击操作下拉列表,然后选择重建表的索引。
- 要重建单个索引:单击索引按钮以显示现有索引。每个列出的索引都有重建索引的选项。
**注意:当其他用户正在访问表的数据时,不要重建索引。要在活动系统上重建索引,请参阅在活动系统上构建索引。**
### 以编程方式构建索引
为非活动表构建索引的首选方法是使用随表的`Persistent`类提供的`%BuildIndices()`(或`%BuildIndicesAsync()`)方法。
**若要以编程方式生成一个或多个索引,请使用`%Library.Persistent.%BuildIndices()`方法。**
生成所有索引:调用`%BuildIndices()`,不带参数生成为给定类(表)定义的所有索引(为其提供值):
```java
SET sc = ##class(MyApp.SalesPerson).%BuildIndices()
IF sc=1 {
WRITE !,"成功构建索引"
} ELSE {
WRITE !,"索引构建失败",!
DO $System.Status.DisplayError(sc) QUIT
}
```
生成指定索引:调用`%BuildIndices()`,并将`$LIST`索引名作为第一个参数,为给定类(表)生成指定的已定义索引(为其提供值):
```java
SET sc = ##class(MyApp.SalesPerson).%BuildIndices($ListBuild("NameIDX","SSNKey"))
IF sc=1 {
WRITE !,"成功构建索引"
} ELSE {
WRITE !,"索引构建失败",!
DO $System.Status.DisplayError(sc) QUIT
}
```
生成除以下项之外的所有索引:调用`%BuildIndices()`,并将索引名称的`$LIST`作为第七个参数来构建(为其提供值)给定类(表)的所有已定义索引(指定索引除外):
```java
SET sc = ##class(MyApp.SalesPerson).%BuildIndices("",,,,,,$ListBuild("NameIDX","SSNKey"))
IF sc=1 {
WRITE !,"成功构建索引"
} ELSE {
WRITE !,"索引构建失败",!
DO $System.Status.DisplayError(sc) QUIT
}
```
`%BuildIndices()`方法执行以下操作:
1. 对要重建的任何(非位图)索引调用`$SortBegin`函数(这将启动对这些索引的高性能排序操作)。
2. 循环遍历类(表)的主要数据,收集索引使用的值,并将这些值添加到索引(通过适当的排序转换)。
3. 调用`$SortEnd`函数来完成索引排序过程。
如果索引已经有值,则必须使用两个参数调用`%BuildIndices()`,其中第二个参数的值为1。
为此参数指定1将导致该方法在重新生成值之前清除这些值。
例如:
```java
SET sc = ##class(MyApp.SalesPerson).%BuildIndices(,1)
IF sc=1 {
WRITE !,"成功构建索引"
} ELSE {
WRITE !,"索引构建失败",!
DO $System.Status.DisplayError(sc) QUIT
}
```
清除并重建所有的索引。
你也可以清除并重建索引的子集,例如:
```java
SET sc = ##class(MyApp.SalesPerson).%BuildIndices($ListBuild("NameIDX","SSNKey"),1)
IF sc=1 {
WRITE !,"成功构建索引"
} ELSE {
WRITE !,"索引构建失败",!
DO $System.Status.DisplayError(sc) QUIT
}
```
注意:当表的数据被其他用户访问时,不要重建索引。
若要在活动系统上重建索引,请参见在活动系统上构建索引。
## 在活动系统上构建索引
在活动系统上构建(或重建)索引时,有两个问题:
- 除非正在构建的索引对`SELECT` `Query`隐藏,否则活动`Query`可能返回不正确的结果。这是在构建索引之前使用`SetMapSelecability()`方法处理的。
- 索引构建期间对数据的活动更新不会反映在索引条目中。这是通过在生成索引时使生成操作锁定单个行来处理的。
**注意:如果应用程序在单个事务内对数据执行大量更新,则可能会出现锁表争用问题。**
### 在Readonly主动系统上构建索引
如果表当前仅用于查询操作(`READONLY`),则可以在不中断查询操作的情况下构建新索引或重建现有索引。这是通过在重建索引时使索引对查询优化器不可用来实现的。
如果要为其构建一个或多个索引的所有类当前都是`READONLY`,请使用“在读写活动系统上构建索引”中描述的相同系列操作,但有以下区别:使用`%BuildIndices()`时,设置`pLockFlag=3`(共享区锁定)。
### 在读写活动系统上构建索引
如果持久化类(表)当前正在使用并且可用于读写访问(查询和数据修改),则可以在不中断这些操作的情况下构建新索引或重建现有索引。如果要为其重建一个或多个索引的类当前可读写访问,则构建索引的首选方法是使用与表的持久类一起提供的`%BuildIndices()`(或`%BuildIndicesAsync()`)方法。
**注意:以下信息适用于动态SQL查询,而不适用于嵌入式SQL。嵌入式SQL在编译时(而不是在运行时)检查`MapSelecability`设置。因此,关闭索引的`MapSelecability`对已经编译的嵌入式SQL查询没有任何影响。因此,嵌入式SQL查询仍可能尝试使用禁用的索引,并将给出不正确的结果。**
在并发读写访问期间,需要执行以下一系列操作来构建一个或多个索引:
1. 望构建的索引对查询不可用(读取访问权限)。这是使用`SetMapSelecability()`完成的。这使得查询优化器无法使用该索引。在重建现有索引和创建新索引时都应执行此操作。例如:
```java
SET status=$SYSTEM.SQL.Util.SetMapSelectability("Sample.MyStudents","StudentNameIDX",0)
```
- 第一个参数是`Schema.Table`名称,它是`SqlTableName`,而不是持久类名称。例如,默认模式是`SQLUser`,而不是`User`。该值区分大小写。
- 第二个参数是SQL索引映射名称。这通常是索引的名称,指的是磁盘上存储索引的名称。对于新索引,这是在创建索引时将使用的名称。该值不区分大小写。
- 第三个参数是`MapSelecability`标志,其中0将索引映射定义为不可选择(`OFF`),1将索引映射定义为可选择(`ON`)。指定0。
可以通过调用`GetMapSelecability()`方法来确定索引是否不可选。如果已将索引显式标记为不可选,则此方法返回0。在所有其他情况下,它返回1;它不执行表或索引是否存在的验证检查。请注意,`Schema.Table`名称是`SqlTableName`,并且区分大小写。
`SetMapSelecability()`和`GetMapSelecability()`仅适用于当前命名空间中的索引映射。如果该表映射到多个命名空间,并且需要在每个命名空间中构建索引,则应该在每个命名空间中调用`SetMapSelecability()`。
2. 在索引构建期间建立并发操作:
- 对于新索引:在类中创建索引定义(或在类的`%Storage.SQL`中创建新的SQL Index Map规范)。编译类。此时,索引存在于表定义中;这意味着对象保存、SQL `INSERT`操作和SQL `UPDATE`操作都记录在索引中。但是,由于在步骤1中调用了`SetMapSelecability()`,因此不会为任何数据检索选择此索引映射。`SetMapSelecability()`阻止查询使用区索引,但是数据映射将被投影到SQL以使用索引全局和数据全局。对于新索引,这是合适的,因为索引尚未填充。在对表运行查询之前,需要填充区索引。
- 对于现有索引:清除任何引用该表的缓存查询。索引构建执行的第一个操作是终止索引。因此,在重新生成索引时,不能依赖任何经过优化以使用该索引的代码。
3. 使用`pLockFlag=2`(行级锁定)的持久化类(表)的`%BuildIndices()`方法构建一个或多个索引。`PLockFlag=2`标志在重建过程中在单个行上建立独占写锁,以便并发数据修改操作与构建索引操作相协调。
默认情况下,`%BuildIndices()`构建为持久类定义的所有索引;可以使用`pIgnoreIndexList`从重建中排除索引。
默认情况下,`%BuildIndices()`为所有`ID`构建索引项。但是,可以使用`pStartID`和`pEndID`来定义`ID`范围。`%BuildIndices()`将仅为该范围内(含)的ID构建索引项。例如,如果使用带有`%NOINDEX`限制的`INSERT`将一系列新记录添加到表中,则可以稍后使用具有ID范围的`%BuildIndices()`为这些新记录构建索引项。还可以使用`pStartID`和`pEndID`在节中构建极大的索引。
`%BuildIndices()`返回`%Status`值。如果`%BuildIndices()`因检索数据时出现问题而失败,系统将生成一个`SQLCODE`错误和一条消息(`%msg`),其中包含遇到错误的`%ROWID`。
4. 构建完索引后,启用映射以供查询优化器选择。将第三个参数`MapSelecability`标志设置为1,如下例所示:
```java
SET status=$SYSTEM.SQL.Util.SetMapSelectability("Sample.MyStudents","StudentNameIDX",1)
```
5. 再次清除引用该表的所有缓存查询。这将消除在此程序中创建的缓存查询,这些查询无法使用索引,因此不如使用索引的相同查询最佳。
这就完成了这个过程。索引已完全填充,查询优化器能够考虑该索引。
注意:`%BuildIndices()`只能用于重建`ID`值为正整数的表的索引。如果父表具有正整数`ID`值,还可以使用`%BuildIndices()`重建子表中的索引。对于其他表,请使用`%ValidateIndices()`方法,如验证索引中所述。因为`%ValidateIndices()`是构建索引的最慢方法,所以只有在没有其他选项的情况下才应该使用它。
文章
Louis Lu · 五月 30, 2021
本文主要总结了在InterSystems IRIS 中如何保存、查询List类型数据
假设我们设计的对象中包含姓名,同时每个姓名下可以包含多个电话。我们可以使用下面方法进行处理。
1. 传统方式
我们可以把每一个姓名和电话放在不同列中。
Class Test.Person Extends %Persistent
{
Property Name As %String;
Property Phone As %String;
}
我们使用SQL语句插入数据:
insert into Test.Person values ('a','111-111-1111');
insert into Test.Person values ('b','222-111-1111');
insert into Test.Person values ('a','111-222-1111');
insert into Test.Person values ('c','333-111-1111');
insert into Test.Person values ('b','222-222-1111');
数据在表中是这样的:
Name
Phone
a
111-111-1111
b
222-111-1111
a
111-222-1111
c
333-111-1111
b
222-222-1111
这种情况下,我们可以使用下面的sql语句将结果返回:
SELECT
distinct %exact(Name) Name,
LIST(phone %foreach(Name)) Phonestr
FROM test.person
Name
Phonestr
a
111-111-1111,111-222-1111
b
222-111-1111,222-222-1111
c
333-111-1111
我们可以为电话号码创建索引,以提高搜索速度,如下:
Index IdxP On Phone;
使用这种方式保存list数据比较简单,当是当list数据非常多时,这种方法会使表格臃肿。
2. 保存在一个字符串字段中,使用分隔符区分
这里我们将所有电话号码保存在一个字符串字段中,每个号码之间用逗号区分
Class Test.Person2 Extends %Persistent
{
Property Name As %String;
Property PhoneStr As %String;
}
填充数据后,类似于这样
Name
PhoneStr
a
111-111-1111,111-222-1111
b
222-111-1111,222-222-1111
c
333-111-1111
d
333-111-1111,222-222-1111
这种情况下我们可以用下面方法实现对每个电话的索引
Index idxP On PhoneStr(ELEMENTS);
ClassMethod PhoneStrBuildValueArray(value, ByRef array) As %Status
{
if value="" {
s array(0)=value
}else{
s list=$lfs(value,","),ptr=0
while $listnext(list,ptr,item){
s array(ptr)=item
}
}
q $$$OK
}
这里用到了一个函数
ClassMethod propertynameBuildValueArray(value, ByRef valueArray) As %Status
其中:
value – 需要拆分的内容;
valueArray – 返回array类型的值,其中包含拆分的内容,格式为 array(key1)=value1, array(key2)=value2...
这时候我们的数据是这样:
USER>zw ^Test.Person2D
^Test.Person2D=4
^Test.Person2D(1)=$lb("","a","111-111-1111,111-222-1111")
^Test.Person2D(2)=$lb("","b","222-111-1111,222-222-1111")
^Test.Person2D(3)=$lb("","c","333-111-1111")
^Test.Person2D(4)=$lb("","d","333-111-1111,222-222-1111")
索引是这样:
USER>zw ^Test.Person2I
^Test.Person2I("idxP"," 111-111-1111",1)=""
^Test.Person2I("idxP"," 111-222-1111",1)=""
^Test.Person2I("idxP"," 222-111-1111",2)=""
^Test.Person2I("idxP"," 222-222-1111",2)=""
^Test.Person2I("idxP"," 222-222-1111",4)=""
^Test.Person2I("idxP"," 333-111-1111",3)=""
^Test.Person2I("idxP"," 333-111-1111",4)=""
这种情况下我们可以通过下面的SQL 语句查找 包含电话号码 333-111-1111 的姓名
select Name from test.person2 where phonestr ['333-111-1111'
select Name from test.person2 where phonestr like '%333-111-1111%'
但是当你检查查询计划的时候,却发现它并没有使用任何索引
我们只能通过类似于下面SQL语句的写法才能使用该索引
select Name from test.person2 where for some %element(Phonestr) (%value = '333-111-1111')
类似的还有下面的写法
(%Value %STARTSWITH 'а')
(%Value [ 'a' and %Value [ 'b')
(%Value in ('c','d'))
(%Value is null)
3. 使用 %List 类型
Class Test.Person3 Extends %Persistent
{
Property Name As %String;
Property PhoneList As %List;
Index idxP On PhoneList(ELEMENTS);
ClassMethod PhoneListBuildValueArray(value, ByRef array) As %Status
{
if value="" {
s array(0)=value
}else{
s ptr=0
while $listnext(value,ptr,item){
s array(ptr)=item
}
}
q $$$OK
}
}
插入数据
insert into Test.Person3 (Name,PhoneList) select 'a', $LISTBUILD('111-111-1111','111-222-1111')
insert into Test.Person3 (Name,PhoneList) select 'b', $LISTBUILD('222-111-1111','222-222-1111')
insert into Test.Person3 (Name,PhoneList) select 'c', $LISTBUILD('333-111-1111')
insert into Test.Person3 (Name,PhoneList) select 'd', $LISTBUILD('333-111-1111','222-222-1111')
数据和索引保存为
USER>zw ^Test.Person3D
^Test.Person3D=4
^Test.Person3D(1)=$lb("","a",$lb("111-111-1111","111-222-1111"))
^Test.Person3D(2)=$lb("","b",$lb("222-111-1111","222-222-1111"))
^Test.Person3D(3)=$lb("","c",$lb("333-111-1111"))
^Test.Person3D(4)=$lb("","d",$lb("333-111-1111","222-222-1111"))
USER>zw ^Test.Person3I
^Test.Person3I("idxP","111-111-1111",1)=""
^Test.Person3I("idxP","111-222-1111",1)=""
^Test.Person3I("idxP","222-111-1111",2)=""
^Test.Person3I("idxP","222-222-1111",2)=""
^Test.Person3I("idxP","222-222-1111",4)=""
^Test.Person3I("idxP","333-111-1111",3)=""
^Test.Person3I("idxP","333-111-1111",4)=""
同样可以使用下面的SQL语句查找包含电话333-111-1111的姓名
select Name from test.person2 where for some %element(phonelist) (%value = '333-111-1111')
4 使用 List Of、Array Of 保存
不需要定义propertynameBuildValueArray函数
Class Test.Person4 Extends %Persistent
{
Property Name As %String;
Property PhoneList As list Of %String;
Index idxP On PhoneList(ELEMENTS);
}
使用同样的方式插入数据
insert into Test.Person4 (Name,PhoneList) select 'a', $LISTBUILD('111-111-1111','111-222-1111')
insert into Test.Person4 (Name,PhoneList) select 'b', $LISTBUILD('222-111-1111','222-222-1111')
insert into Test.Person4 (Name,PhoneList) select 'c', $LISTBUILD('333-111-1111')
insert into Test.Person4 (Name,PhoneList) select 'd', $LISTBUILD('333-111-1111','222-222-1111')
数据和索引保存为
USER>zw ^Test.Person4D
^Test.Person4D=4
^Test.Person4D(1)=$lb("","a",$lb("111-111-1111","111-222-1111"))
^Test.Person4D(2)=$lb("","b",$lb("222-111-1111","222-222-1111"))
^Test.Person4D(3)=$lb("","c",$lb("333-111-1111"))
^Test.Person4D(4)=$lb("","d",$lb("333-111-1111","222-222-1111"))
USER>zw ^Test.Person4I
^Test.Person4I("idxP"," 111-111-1111",1)=""
^Test.Person4I("idxP"," 111-222-1111",1)=""
^Test.Person4I("idxP"," 222-111-1111",2)=""
^Test.Person4I("idxP"," 222-222-1111",2)=""
^Test.Person4I("idxP"," 222-222-1111",4)=""
^Test.Person4I("idxP"," 333-111-1111",3)=""
^Test.Person4I("idxP"," 333-111-1111",4)=""
使用同样的SQL查询可以得到结果
select Name from test.person4 where for some %element(Phonelist) (%value = '333-111-1111')
引申话题:针对日期字段的索引
日期格式通常是yyyy-mm-dd,我们经常要求按照某年或者某月查询数据,我们可以使用propertynameBuildValueArray函数设定保存的索引方式实现这个目的
Class Test.Person5 Extends %Persistent
{
Property Name As %String;
Property DOB As %Date;
Index idxD On (DOB(KEYS), DOB(ELEMENTS));
ClassMethod DOBBuildValueArray(value, ByRef array) As %Status
{
if value="" {
s array(0)=value
}else{
s d=$zd(value,3)
s array("yy")=+$p(d,"-",1)
s array("mm")=+$p(d,"-",2)
s array("dd")=+$p(d,"-",3)
}
q $$$OK
}
}
插入数据
insert into Test.Person5 (Name,DOB)
select 'a', {d '2000-01-01'} union all
select 'b', {d '2000-01-02'} union all
select 'c', {d '2000-02-01'} union all
select 'd', {d '2001-01-02'} union all
select 'e', {d '2001-01-01'} union all
select 'f', {d '2001-02-01'}
查看数据以及索引保存的内容
USER>zw ^Test.Person5D
^Test.Person5D=6
^Test.Person5D(1)=$lb("","a",58074)
^Test.Person5D(2)=$lb("","b",58075)
^Test.Person5D(3)=$lb("","c",58105)
^Test.Person5D(4)=$lb("","d",58441)
^Test.Person5D(5)=$lb("","e",58440)
^Test.Person5D(6)=$lb("","f",58471)
USER>zw ^Test.Person5I
^Test.Person5I("idxD","dd",1,1)=""
^Test.Person5I("idxD","dd",1,3)=""
^Test.Person5I("idxD","dd",1,5)=""
^Test.Person5I("idxD","dd",1,6)=""
^Test.Person5I("idxD","dd",2,2)=""
^Test.Person5I("idxD","dd",2,4)=""
^Test.Person5I("idxD","mm",1,1)=""
^Test.Person5I("idxD","mm",1,2)=""
^Test.Person5I("idxD","mm",1,4)=""
^Test.Person5I("idxD","mm",1,5)=""
^Test.Person5I("idxD","mm",2,3)=""
^Test.Person5I("idxD","mm",2,6)=""
^Test.Person5I("idxD","yy",2000,1)=""
^Test.Person5I("idxD","yy",2000,2)=""
^Test.Person5I("idxD","yy",2000,3)=""
^Test.Person5I("idxD","yy",2001,4)=""
^Test.Person5I("idxD","yy",2001,5)=""
^Test.Person5I("idxD","yy",2001,6)=""
执行下面 SQL 可以显示所有2月出生的信息
select * from Test.Person5 where for some %element(DOB) (%key='mm' and %value = 2)
这篇文章源自 这里,作者 Vitaliy Serdtsev
文章
姚 鑫 · 五月 15, 2021
# 第一章 单元测试概述
本教程的第一部分概述了单元测试。完成本教程的这一部分后,将能够:
- 定义单元测试并区分单元测试和集成测试
- 列出单元测试的几个好处
- 描述InterSystems IRIS `%UnitTest`包和`xUnit`测试框架之间的相似性。
- 列出软件开发中测试优先方法经常声称的几个好处。
# 什么是单元测试?
单元测试是对单个代码模块的正确性的测试,例如,方法或类的测试。通常,开发人员在开发代码时为其代码创建单元测试。典型的单元测试是一种执行方法的方法,该方法测试并验证该方法是否为给定的一组输入生成了正确的输出。
单元测试不同于集成测试。集成测试验证了一组代码模块交互的正确性。单元测试仅单独验证代码模块的正确性。一组代码模块的集成测试可能会失败,即使每个模块都通过了单元测试。
# 为什么要进行单元测试?
单元测试提供了许多好处,包括:
- 提供代码模块是否正确的验证。这是单元测试的主要原因。
- 提供自动回归测试。更改代码模块后,应重新运行单元测试,以确保代码模块仍然正确。也就是说,应该使用单元测试来确保更改没有破坏代码模块。理想情况下,所有代码模块的单元测试都应该在更改任何一个模块之后运行。
- 提供文档。通常,代码模块的单元测试与代码模块一起交付。检查单元测试提供了大量有关代码模块如何工作的信息。
# XUnit测试框架
单元测试框架是为开发和执行单元测试提供支持的类包。它们可以很容易地扩展以支持更具体或专门化类型的单元测试。
XUnit系列测试框架基于原始的`Sunit`框架(用于单元测试`SmallTalk`代码),包括以下框架:
- `JUnit-Java`代码的单元测试框架。
- `NUnit-C#`、`VB.NET`和其他`.NET`语言代码的单元测试框架。
- `CppUnit-C++`代码的单元测试框架。
- `PyUnit-Python`代码的单元测试框架。
# %UnitTest和xUnit框架的结构
`%UnitTest`包和`xUnit`框架共享相同的基本结构。熟悉任何`Unit`框架的开发人员都可以毫不费力地学习使用`%UnitTest`包。`%UnitTest`和`xUnit`框架都围绕以下基本测试结构组织:
- 测试装置-为一个测试或一组测试做准备和清理工作的代码。准备测试可能包括创建数据库连接,或使用测试数据初始化数据库。清理可能包括关闭数据库连接或恢复数据库状态。
- 测试用例-测试的最小单元。验证特定的一组输入是否会产生给定模块的特定输出。
- 测试套件-设计为一起执行的测试和测试套件的集合。
- Test Runner-用于执行测试并显示其结果的实用程序。
# 测试自动化
`%UnitTest`包和`xUnit`框架都支持测试自动化。当单元测试完成执行时,它会报告测试是通过还是失败。不需要解释测试结果。这是非常重要的。可以为每个代码更改执行大量单元测试。如果必须不断地阅读和解释结果,这个过程很快就会变得非常乏味和容易出错。
许多`xUnit`框架提供了汇总测试结果的图形用户界面(GUI)。`%UnitTest`会生成一个显示测试结果的网页。它以绿色显示有关通过的测试的信息,以红色显示有关失败的测试的信息。开发人员可以一目了然地判断是否有任何测试失败。
这是由`%UnitTest`单元测试生成的测试报告。用户可以通过单击页面上的超链接深入查看提供有关测试的更多详细信息的页面。
![image](6F95C9D03A024D2CB690E5213CF37A4B)
# 测试优先方法论
敏捷软件方法论,例如测试驱动开发(TDD)和极限编程,特别强调单元测试。事实上,这些方法使用单元测试来驱动开发过程。他们提倡“测试优先”的软件开发方法。在这种方法中,开发人员在编写代码模块的一行代码之前设计并编写代码模块的单元测试。然后,开发人员创建代码模块,目标是通过单元测试。
`Test First`方法的倡导者声称该方法具有以下好处:
- 它迫使开发人员在开发任何模块之前很久就决定代码模块的正确输入和输出。
- 它集中了开发人员在创建代码模块时的注意力。开发人员关注的是在创建模块时通过单元测试的具体目标。
- 它可以防止单元测试成为事后的想法。如果首先创建单元测试,则在项目结束之前不能忽略单元测试。
- 它确保了代码的高度测试覆盖率。
注意:测试优先开发的支持者通常主张在代码模块之前执行单元测试,而不仅仅是创建单元测试。当然,在这一点上测试应该会失败。他们甚至可能不会编译。
# Red – Green – Refactor
`XUnit`和`%UnitTest`测试报告GUI报告以绿色表示通过测试,以红色表示未通过测试。下面是使用测试优先开发方法的开发节奏:
1. 红色 - 编写一个不起作用的小测试,也许一开始不会编译。
2. 绿色 - 让测试快速运行,在测试过程中犯下所有必要的错误。
3. 重构 - 消除仅在使测试正常工作时产生的所有重复。
> Kent Beck,《测试驱动的设计》
文章
姚 鑫 · 八月 9, 2021
# 第八十一章 方法关键字 - SoapRequestMessage
当多个`web方法`具有相同的`SoapAction`时使用此方法。
在默认场景中,该关键字指定请求消息的`SOAP`正文中的顶级元素的名称。
仅适用于定义为`web服务`或`web客户端`的类。
# 用法
要在请求消息的`SOAP`体中指定顶级元素的名称,请使用以下语法:
```java
Method name(formal_spec) As returnclass [ WebMethod, SoapAction = "MyAct", SoapRequestMessage="MyReqMessage" ]
{ //implementation }
```
其中`soaprequestmessage`是有效的XML标识符。
# 详解
注意:此关键字仅对包装的文档/文字`document/literal`消息有效。
对于包装的文档/文字消息,该关键字指定请求消息的`SOAP`主体中的顶部元素的名称。(默认情况下,包装文档/文字消息。
如果对同一`web服务`中的多个`web方法`使用相同的`SoapAction`值,请指定此关键字。否则,一般不需要这个关键字。
# 与WSDL的关系
`SoapRequestMessage`关键字影响`web服务`的`WSDL`的``部分。例如,考虑以下web方法:
```java
Method Add(a as %Numeric,b as %Numeric) As %Numeric [ SoapAction = MyAct,SoapRequestMessage=MyReqMessage, WebMethod ]
{
Quit a + b
}
```
对于这个web服务,WSDL包含以下内容:
```xml
```
这些元素在``部分中相应地定义。
默认情况下,如果方法没有指定`SoapRequestMessage`关键字,``部分将改为如下所示:
```xml
```
如果使用`SOAP`向导从`WSDL` IRIS `web服务`或客户端, IRIS将此关键字设置为适合该WSDL的。
# 对Message的影响
对于前面显示的`web方法`,`web服务`需要以下形式的请求消息:
```xml
12
```
相反,如果该方法没有指定`SoapRequestMessage`关键字,则该消息将如下所示:
```xml
12
```
# 第八十二章 方法关键字 - SoapTypeNameSpace
为此`web方法`使用的类型指定`XML`命名空间。仅适用于定义为`web服务`或`web客户端`的类。
# 用法
若要重写类型的默认`XML`命名空间(当该方法用作web方法时),请使用以下语法:
```java
Method name(formal_spec) As returnclass [ SoapTypeNameSpace = "soapnamespace", SoapBindingStyle = document, WebMethod ]
{ //implementation }
```
其中`soapnamespace`是命名空间`URI`。请注意,如果`URI`包含冒号(`:`),则该字符串必须加引号。也就是说,可以使用以下内容:
```java
Method MyMethod() [ SoapTypeNameSpace = "http://www.mynamespace.org", SoapBindingStyle = document, WebMethod ]
```
或以下内容:
```java
Method MyMethod() [ SoapTypeNameSpace = othervalue, SoapBindingStyle = document, WebMethod ]
```
但不包括以下内容:
```java
Method MyMethod() [ SoapTypeNameSpace = http://www.mynamespace.org, SoapBindingStyle = document, WebMethod ]
```
重要提示:对于手动创建的`web服务`,该关键字的默认值通常是合适的。当使用SOAP向导从`WSDL`生成`web客户端`或服务时,InterSystems IRIS会将该关键字设置为适合该`WSDL`;如果修改该值,`web客户端`或服务可能不再工作。
# 详解
此关键字指定此`web方法`使用的类型的XML命名空间。
注意:只有当方法使用文档样式绑定时,此关键字才有作用。也就是说,方法(或包含它的类)必须用等于`document`的`SoapBindingStyle`标记。(对于使用`rpc-style`绑定的方法,指定这个关键字是没有意义的。)
# 默认
如果省略此关键字,则此方法的类型位于由`web服务`或`客户端`类的`TYPENAMESPACE`参数指定的命名空间中。如果未指定`TYPENAMESPACE`,则类型将位于由`web服务`或客户端的`are`参数指定的命名空间中。
# 与WSDL的关系
`SoapTypeNameSpace`关键字影响`WSDL`的以下部分:
``元素中的命名空间声明。指定的命名空间(例如,`http://www.customtypes.org`)将添加到这里。例如:
```xml
...
xmlns:ns2="http://www.customtypes.org"
xmlns:s0="http://www.wbns.org"
xmlns:s1="http://webservicetypesns.org"
...
targetNamespace="http://www.wbns.org"
```
在本例中,`http://www.customtypes.org`命名空间被分配给前缀`ns2`。
请注意,`WSDL`还像往常一样声明了以下名称空间:
- `Web服务`的命名空间(`http://www.wsns.org`),在本例中,它被分配给前缀`s0`,也用作`Web服务`的目标命名空间。
- 网络服务的类型命名空间`http://www.webservicetypesns.org`),在本例中它被分配给`前缀s1`。
如果在`web服务`类中没有指定类型命名空间,则该命名空间不包含在`WSDL`中。
- ``元素,它包含一个``元素,该元素的`targetNamespace`属性等于为`SoapTypeNameSpace`指定的命名空间:
```xml
...
...
```
相反,如果没有指定`SoapTypeNameSpace`,那么`WSDL`的这一部分将如下所示。请注意,``元素的`targetNamespace`是`web服务`类型的命名空间:
```xml
...
...
```
(此外,如果在`web服务类`中没有指定类型命名空间,则`targetNamespace`将改为`web服务`的命名空间。)
# 对消息的影响
`SOAP`消息可能如下所示(为了可读性,添加了换行符和空格):
```xml
3
```
请注意,``元素位于`“http://www.customtypes.org”`命名空间中。
相反,如果没有指定`SoapTypeNameSpace`关键字,则消息可以如下所示:
```xml
3
```
# 第八十三章 方法关键字 - SqlName
覆盖投影`SQL`存储过程的默认名称。
仅当此方法被投影为`SQL`存储过程时应用。
# 用法
要覆盖方法投射为`SQL`存储过程时使用的默认名称,请使用以下语法:
```java
ClassMethod name(formal_spec) As returnclass [ SqlProc, SqlName = sqlname ]
{ //implementation }
```
其中`sqlname`是`SQL`标识符。
# 详解
如果将此方法投影为`SQL`存储过程,则使用此名称作为存储过程的名称。
# 默认
如果忽略这个关键字, IRIS确定`SQL`名称如下:
```java
CLASSNAME_METHODNAME
```
默认使用大写字母。
但是,在调用存储过程时可以使用任何情况,因为SQL是不区分大小写的。
因此,在下面的示例中,默认的`SQL name`值是`TEST1_PROC1`。
这个默认值是在`SELECT`语句中指定的:
```java
Class User.Test1 Extends %Persistent
{
ClassMethod Proc1(BO,SUM) As %INTEGER [ SqlProc ]
{
///definition not shown
}
Query Q1(KD As %String,P1 As %String,P2 As %String) As %SqlQuery
{
SELECT SUM(SQLUser.TEST1_PROC1(1,2)) AS Sumd
FROM SQLUser.Test1
}
}
```
# 第八十四章 方法关键字 - SqlProc
指定是否可以作为`SQL`存储过程调用该方法。
只有类方法(而不是实例方法)可以作为SQL存储过程调用。
# 用法
要指定该方法可以作为`SQL`存储过程调用,请使用以下语法:
```java
ClassMethod name(formal_spec) As returnclass [ SqlProc ]
{ //implementation }
```
否则,忽略该关键字或将`Not`放在该关键字之前。
# 详解
该关键字指定可以作为`SQL`存储过程调用该方法。
只有类方法(而不是实例方法)可以作为`SQL`存储过程调用。
存储过程由子类继承。
# 默认
如果忽略此关键字,则该方法作为`SQL`存储过程不可用。
文章
姚 鑫 · 八月 15, 2022
# 第二章 使用管理门户(二)
# 管理门户概述
本节介绍管理门户页面的一些常见布局元素。
注意:在管理门户中的任何位置,将光标移到菜单项上都会显示该项目的描述。
## 管理门户主页
管理门户主页的标题是 `Welcome, `。在标题旁边,功能区包含以下选项:
- 两个视图按钮,可让指定如何在菜单列中显示链接。
- 搜索栏,位于功能区的右侧。当指定一个词并按 Enter 键时,将显示包含该词的所有页面的列表;然后,可以单击要显示的目标页面,而无需浏览子菜单。
以下部分描述了主页的区域:
### 管理门户菜单栏
位于主页左边缘的菜单栏是导航门户的主要方法。
### 管理门户欢迎窗格
欢迎窗格位于主页的中心,包括经常访问的页面的快捷方式。它包含以下字段:
- 收藏夹`Favorites` — 列出选择为收藏夹的管理门户页面(请参阅操作窗格);可以单击每个页面标题直接转到该页面。
- 最近`Recent` — 列出自上次启动 IRIS 以来最近显示的页面。
- `Did you know?` — 显示提示。
- 链接 `Links` - 指向可能想要访问的页面的链接。
### 管理门户消息窗格
位于主页右侧边缘的消息窗格显示一般系统信息并提供指向系统仪表板的链接。
如果实例是镜像成员,则消息窗格还显示它所属的镜像、其状态和成员类型以及指向镜像监视器的链接。
## 管理门户标题
页眉位于管理门户中每个页面的顶部,可用于快速导航门户。
标题包含以下链接:
- 主页`Home` — 显示管理门户主页。
- 关于`About` — 显示系统概览信息。
- 帮助`Help` — 显示正在查看的页面/主题的在线文档(帮助)。
- 联系方式`Contact` — 显示全球响应中心 (WRC) 的联系方式页面。
- 注销`Logout` — 注销您并带您进入管理门户的登录页面。
- 菜单`Menu` — 根据用户担任的角色显示常见任务列表。
标头还包含有用的信息,例如:
- 服务器`Server` — 运行 IRIS 的服务器的名称。
- 命名空间`Namespace` — 当前使用的命名空间的名称。要在不同的命名空间中工作,请单击切换并选择所需的命名空间。
- 用户`User` — 登录到管理门户的用户的名称。要更改用户的密码,请单击名称。
- 许可证`Licensed to` — 出现在许可证密钥信息中的客户名称。
- 实例`Instance` — 服务器上运行的 `IRIS` 实例的名称。
此外,可以显示系统模式标签(例如,测试系统);
管理门户标题的左侧显示正在使用的产品的名称。
## 管理门户功能区
功能区位于标题正下方,并显示特定于每个页面的不同内容。例如,数据库页面(`System Explorer` > `Databases`)的功能区如下图所示:
功能区的典型内容是:
- 正在显示的管理门户页面的标题。
- 当前页面的面包屑,直接列在页面标题上方。路径中列出的每个页面都是一个活动链接,可以使用它返回到先前显示的子菜单/列表。当在页面上进行未保存的更改时,会在面包屑中附加一个星号,例如系统 > 配置 >内存和启动 —(配置设置)*。在离开未保存的更改之前,系统始终会提示进行确认。
注意:页签不会列出路径中的每个页面,并且页签中的页面并不总是与导航菜单中的页面匹配。始终可以通过单击主页返回到管理门户主页并使用搜索工具导航到特定页面,本节稍后将对此进行介绍。
- 允许在页面上执行操作的几个按钮。例如,`Databases` 页面包含按钮 `Integrity Check` 和 `Integrity Log`。
- 刷新按钮,包含有关页面上次更新时间的信息。
## 系统概述信息
单击管理门户标题上的关于时,将显示一个表格,其中包含以下信息:
- 版本 — 此 `IRIS` 实例的特定构建信息,包括平台、构建号和构建日期。
- 配置 - 此实例正在使用的配置 (`.cpf`) 文件的名称和位置。
- 数据库缓存 (`MB`) — 为数据库分配的空间例程缓存 (`MB`) — 为例程分配的空间。
- 日志文件 - 当前日志文件的名称和位置。
- `SuperServer` 端口 — 运行 `IRIS` 服务器的端口号。
- `Web`服务器端口 — 运行私有 `IRIS Web` 服务器的端口号。
- 许可证服务器地址/端口 — `IRIS` 许可证服务器的 `IP` 地址和运行它的端口号。
- 许可给 — 出现在许可密钥信息中的客户名称。
- 集群支持 - 指示此实例是否是集群的一部分。
- 镜像 — 指示此实例是否是镜像的成员。
- `Time System Started` — 上次启动 `InterSystems IRIS` 实例的日期和时间。
- 加密密钥标识符 — 如果激活加密,则为加密密钥的 `GUID`(全局唯一 `ID`)。
- `NLS` 区域设置 — 国家语言支持区域设置。
- 此会话的首选语言 - 管理门户已本地化并可显示的语言的下拉列表。可以通过从下拉列表中选择新的语言来更改显示语言。最初,浏览会话的首选语言是为浏览器指定的语言,如果不支持浏览器语言,则为英语;在特定浏览器中选择首选语言后,即使更改了浏览器语言,该浏览器中的管理门户也会使用该语言。
文章
姚 鑫 · 七月 21, 2021
# 第五章 参数定义
描述参数定义的结构。
# 介绍
参数定义定义了一个给定类的所有对象都可用的常数值。创建类定义时(或在编译前的任何时候),可以设置其类参数的值。默认情况下,每个参数的值都是空字符串,但是可以在参数定义中指定一个非空值。在编译时,为类的所有实例建立参数值。除了极少数例外,该值不能在运行时更改。
# 详解
参数定义具有以下结构:
```java
/// description
Parameter name As parameter_type [ keyword_list ] = value ;
```
- `description`描述(可选)旨在显示在“类参考”中。默认情况下,描述为空白。
- `name`(必需)是参数的名称。这必须是有效的类成员名称,并且不能与任何其他类成员名称冲突。
- `parameter_type`(可选)指定参数的用户界面类型,由Studio用于在检查器内为参数提供输入验证。
**这不是类名;参见下一节。在大多数情况下,编译器会忽略这个关键字。**
如果省略参数类型,也要省略单词`As`
- `value`(可选)指定参数的值。如果省略值,也要省略等号=
- `keyword_list`(可选)是以逗号分隔的关键字列表,用于进一步定义参数。
如果省略此列表,也要省略方括号。
# 参数的允许类型
参数类型`parameter_type` 选项可以是下列值之一:
- `BOOLEAN` — true(1)或false(0)值。
- `CLASSNAME` — 有效的类名。
- `COSCODE` — ObjectScript代码。
- `COSEXPRESSION` — 有效的ObjectScript表达式。
**如果参数是`COSEXPRESSION`类型,则在运行时计算该表达式。**
与形参`Type`关键字的大多数其他值不同,这个值影响编译器。
- `COSIDENTIFIER` — 有效的ObjectScript标识符。
- `INTEGER` — 整数值。
- `SQL` — SQL语句
- `SQLIDENTIFIER` — 有效的SQL标识符。
- `STRING` —字符串值。
- `TEXT` — 多行文本值。
- `CONFIGVALUE` -可以在类定义之外修改的参数。
与形参`Type`关键字的大多数其他值不同,这个值影响编译器。
如果参数的类型是`CONFIGVALUE`,那么可以通过`$SYSTEM.OBJ.UpdateConfigParam()`修改参数。
例如,下面的代码更改了参数`MYPARM`(在类`MyApp`中)的值。
`MyClass`的新值为`42`:
```java
set sc=$system.OBJ.UpdateConfigParam("MyApp.MyClass","MYPARM",42)
```
**注意,`$SYSTEM.OBJ.UpdateConfigParam()`影响任何新进程所使用的生成的类描述符,但不影响类定义。
如果重新编译类,InterSystems IRIS将重新生成类描述符,该描述符现在将使用包含在类定义中的这个参数的值(从而覆盖通过`$SYSTEM.OBJ.UpdateConfigParam()`所做的更改)。**
也可以省略`parameter_type`,在这种情况下`Inspector`将允许参数的任何值。
```
/// web服务的名称。
Parameter SERVICENAME = "SOAPDemo" ;
```
# 第六章 映射定义
描述投影定义的结构。
# 介绍
投影定义指示类编译器在编译或删除类定义时执行指定的操作。
投影定义投影类的名称(来自`%Projection.AbstractProjection`)实现方法称为类的编译完成后,当一个类定义中删除(因为它被删除或者因为类即将重新编译)。
# 详情
投影定义有以下结构:
```java
/// description
Projection name As projection_class (parameter_list) ;
```
- `description`(可选)用于在类引用中显示(但请注意投影目前没有显示在类引用中)。
说明默认为空。
- `Name`(必需)是投影的名称。
这必须是一个有效的类成员名,并且不能与任何其他类成员名冲突。
- `projection_class` (required)是投影类的名称,它是`%Projection.AbstractProjection`的子类。
- `parameter_list`(可选)是一个以逗号分隔的参数及其值列表。
如果指定,这些应该是`projection_class`使用的参数。
如果省略了这个列表,也省略了括号。
- `Keyword_list`(可选)是一个逗号分隔的关键字列表,这些关键字进一步定义了投影。
如果省略了这个列表,也可以省略方括号。
# 第七章 属性定义
描述属性定义的结构。注意,关系是一种属性。
# 介绍
属性包含与类实例相关的信息。可以向对象类添加属性定义。它们在其他类中没有意义。
# 详情
属性定义有以下结构:
```java
/// description
Property name As classname (parameter_list) [ keyword_list ] ;
```
或者(对于列表属性):
```java
/// description
Property name As List Of classname (parameter_list) [ keyword_list ] ;
```
或者(对于数组属性):
```java
/// description
Property name As Array Of classname (parameter_list) [ keyword_list ] ;
```
或者(对于关系属性):
```java
/// description
Relationship name As classname [ keyword_list ] ;
```
- `description`(可选)用于在类引用中显示。说明默认为空。
- `name`(必需)是属性的名称。
这必须是一个有效的类成员名,并且不能与任何其他类成员名冲突。
- `classname`(可选)是该属性所基于的类的名称。
- `parameter_list`(可选)是参数及其值的逗号分隔列表。如果指定,这些应该是由类名使用的参数,或者是对所有属性都可用的参数。
如果省略此列表,也要省略括号。
- `keyword_list`(对于关系属性是必需的,但在其他方面是可选的)是一个逗号分隔的关键字列表,用于进一步定义属性。
如果省略此列表,也要省略方括号。
注意:分片类不支持属性关系。
```java
Property SSN As %String(PATTERN = "3N1""-""2N1""-""4N") [ Required ];
```
# 第八章 查询定义
描述查询定义的结构。
# 介绍
类查询是作为类结构一部分的命名查询,可以通过动态SQL进行访问。
**可以在任何类中定义类查询;不需要将它们包含在持久类中。**
# 详解
查询定义具有以下结构:
```java
/// description
Query name(formal_spec) As classname [ keyword_list ]
{ implementation }
```
- `description`描述(可选)旨在显示在“类别参考”中。默认情况下,描述为空白。
- `name`(必需)是查询的名称。这必须是有效的类成员名称,并且不能与任何其他类成员名称冲突。
- `formal_spec`(可选)指定传递给查询的参数列表。
具体来说,这是通过关联查询类的`Execute()`方法传递给查询的参数列表。
- `classname`(必需)指定此查询使用的查询类。
**对于基于SQL的查询,该值通常为`%SQLQuery`,对于自定义查询,该值通常为%Query。**
**注意:分片类不支持自定义类查询。**
- `keyword_list`(可选)是逗号分隔的关键字列表,用于进一步定义查询。
如果省略此列表,也要省略方括号。
- `implementation` 实现(可选)是定义查询的零行或多行代码。
文章
姚 鑫 · 六月 14, 2022
# 第九章 其他参考资料(二)
# 特殊变量 (SQL)
系统提供的变量。
```sql
$HOROLOG
$JOB
$NAMESPACE
$TLEVEL
$USERNAME
$ZHOROLOG
$ZJOB
$ZPI
$ZTIMESTAMP
$ZTIMEZONE
$ZVERSION
```
SQL直接支持许多对象脚本特殊变量。这些变量包含系统提供的值。只要可以在SQL中指定文字值,就可以使用它们。
SQL特殊变量名不区分大小写。大多数可以使用缩写来指定。
Variable| Name| Abbreviation| Data Type Returned Use
---|---|---|---
$HOROLOG| $H| %String/VARCHAR |当前进程的本地日期和时间
$JOB| $J| %String/VARCHAR |当前进程的 job ID
$NAMESPACE| none| %String/VARCHAR |当前命名空间名称
$TLEVEL| $TL| %Integer/INTEGER|| 当前事务嵌套级别
$USERNAME| none| %String/VARCHAR|当前进程的用户名
$ZHOROLOG| $ZH |%Numeric/NUMERIC(21,6)|自InterSystems IRIS启动后经过的秒数
$ZJOB| $ZJ| %Integer/INTEGER|当前进程的job状态
$ZPI| none| %Numeric/NUMERIC(21,18) |数值常量PI
$ZTIMESTAMP |$ZTS| %String/VARCHAR |协调世界时间格式的当前日期和时间
$ZTIMEZONE| $ZTZ| %Integer/INTEGER| 当地时区与GMT的偏移量
$ZVERSION| $ZV| %String/VARCHAR| IRIS的当前版本
# 示例
```sql
SELECT TOP 5 Name,$H
FROM Sample.Person
```
以下示例仅在时区位于大陆内时才返回结果集:
```sql
SELECT TOP 5 Name,Home_State
FROM Sample.Person
WHERE $ZTIMEZONE BETWEEN -480 AND 480
```
# 字符串操作(SQL)
字符串操作函数和运算符。
SQL 支持多种类型的字符串操作:
- 字符串可以通过长度、字符位置或子字符串值进行操作。
- 字符串可以通过指定的分隔符或分隔符字符串来操作。
- 字符串可以通过模式匹配和单词感知搜索来测试。
- 特殊编码的字符串(称为列表)包含嵌入的子字符串标识符,而不使用分隔符。各种 `$LIST` 函数对这些与标准字符串不兼容的编码字符串进行操作。唯一的例外是 `$LISTGET` 函数和 `$LIST` 的单参数和双参数形式,它们将编码字符串作为输入,但将单个元素值作为标准字符串输出。
SQL 支持字符串函数、字符串条件表达式和字符串运算符。
ObjectScript 字符串操作区分大小写。字符串中的字母可以转换为大写、小写或混合大小写。字符串排序规则可以区分大小写,也可以不区分大小写;默认情况下,SQL 字符串排序规则是不区分大小写的 `SQLUPPER`。 SQL 提供了许多字母大小写和排序规则函数和运算符。
当为数字参数指定字符串时,大多数 SQL 函数执行以下字符串到数字的转换: 非数字字符串转换为数字 0;将数字字符串转换为规范数字;并且混合数字字符串在第一个非数字字符处被截断,然后转换为规范数字。
# 字符串连接
以下函数将子字符串连接成字符串:
- `CONCAT`:连接两个子字符串,返回一个字符串。
- `STRING`:连接两个或多个子字符串,返回单个字符串。
- `XMLAGG`:连接列的所有值,返回单个字符串。
- `LIST`:连接列的所有值,包括逗号分隔符,返回单个字符串。
- 连接运算符 (`||`) 也可用于连接两个字符串。
# 字符串长度
以下函数可用于确定字符串的长度:
- `CHARACTER_LENGTH` 和 `CHAR_LENGTH`:返回字符串中的字符数,包括尾随空格。 `NULL` 返回 `NULL`。
- `LENGTH`:返回字符串中的字符数,不包括尾随空格。 `NULL` 返回 NULL。
- `$LENGTH`:返回字符串中的字符数,包括尾随空格。 `NULL` 返回为 0。
# Truncation and Trim
以下函数可用于截断或修剪字符串。截断限制字符串的长度,删除超出指定长度的所有字符。`Trim`从字符串中删除前导和/或尾随空格。
- `Truncation`: `CONVERT`, `%SQLSTRING`, and `%SQLUPPER`.
- `Trimming`: `TRIM`, `LTRIM`, and `RTRIM`.
# 子串搜索
以下函数在字符串中搜索子字符串并返回字符串位置:
- `POSITION`:按子字符串值搜索,找到第一个匹配项,返回子字符串开始的位置。
- `CHARINDEX`:按子字符串值搜索,找到第一个匹配项,返回子字符串开始的位置。可以指定起点。
- `$FIND`:按子串值搜索,找到第一个匹配项,返回子串结束的位置。可以指定起点。
- `INSTR`:按子字符串值搜索,找到第一个匹配项,返回子字符串开始的位置。可以指定起点和子串出现。
以下函数在字符串中按位置或分隔符搜索子字符串并返回子字符串:
- `$EXTRACT`:按字符串位置搜索,返回由开始位置或开始和结束位置指定的子字符串。从字符串的开头搜索。
- `SUBSTRING`:按字符串位置搜索,返回由开始位置或开始和长度指定的子字符串。从字符串的开头搜索。
- `SUBSTR`:按字符串位置搜索,返回由起始位置或起始和长度指定的子字符串。从字符串的开头或结尾搜索。
- `$PIECE`:按分隔符搜索,返回第一个分隔的子字符串。可以指定起点或默认为字符串的开头。
- `$LENGTH`:按分隔符搜索,返回分隔子串的数量。从字符串的开头搜索。
- `$LIST`:在特殊编码的列表字符串上按子字符串计数搜索。它通过子串计数定位子串并返回子串值。从字符串的开头搜索。
- 包含运算符 (`[`) 也可用于确定子字符串是否出现在字符串中。
- `%STARTSWITH` 比较运算符将指定的字符与字符串的开头进行匹配。
# 子串搜索和替换
以下函数在字符串中搜索子字符串并将其替换为另一个子字符串。
- `REPLACE`:按字符串值搜索,用新的子字符串替换子字符串。从字符串的开头搜索。
- `STUFF`:按字符串位置和长度搜索,用新的子字符串替换子字符串。从字符串的开头搜索。
# 字符类型和 Word-Aware 比较
`%PATTERN` 比较运算符将字符串与指定的字符类型模式匹配。
文章
姚 鑫 · 六月 24, 2021
# 第十七章 加密XML文档
本章介绍如何加密XML文档。
提示:发现在此命名空间中启用`SOAP`日志记录非常有用,这样就可以收到有关任何错误的更多信息。
# 关于加密的XML文档
加密的XML文档包括以下元素:
- ``元素,其中包含由随机生成的对称密钥加密的加密数据。(使用对称密钥加密比使用公钥加密更有效。)
- 至少有一个``元素。每个``元素携带用于加密数据的对称密钥的加密副本;它还包含一个带有公钥的`X.509`证书。拥有匹配私钥的接收方可以解密对称密钥,然后解密``元素。
- (可选)其他明文元素。
```xml
MIICnDCCAYQCAWUwDQYJKo... content omitted
J2DjVgcB8vQx3UCy5uejMB ... content omitted
LmoBK7+nDelTOsC3 ... content omitted
```
要创建加密文档,请使用类`%XML.Security.EncryptedData`和`%XML.Security.EncryptedKey`。这些启用XML的类投影到适当名称空间中的有效``和``元素。
# 创建加密的XML文档
创建加密的XML文档的最简单方法如下:
1. 定义并使用可以直接投影到所需XML文档的通用容器类。
2. 创建包含要加密的XML的流。
3. 加密该流,并将其与相应的加密密钥一起写入容器类的相应属性。
4. 为容器类生成XML输出。
## 加密的前提条件
在加密文档之前,必须创建包含要将加密文档发送到的实体的证书的 IRIS凭据集。在这种情况下,不需要(也不应该拥有)关联的私钥。
## 容器类的要求
一个通用容器类必须包括以下内容:
- 类型为`%XML.Security`的属性。
被投影为``元素的`EncryptedData`。
这个属性将携带加密的数据。
- 至少一个类型为`%XML.Security`的属性。被投影为``元素的`EncryptedKey`。
这些属性将携带相应的密钥信息。
示例如下:
```java
Class XMLEncryption.Container Extends (%RegisteredObject, %XML.Adaptor)
{
Property Data As %XML.Security.EncryptedData(XMLNAME = "EncryptedData");
Property Key As %XML.Security.EncryptedKey(XMLNAME = "EncryptedKey");
Parameter NAMESPACE = "http://www.w3.org/2001/04/xmlenc#";
}
```
## 生成加密的XML文档
要生成并编写加密文档,请执行以下操作:
1. 创建包含XML文档的流。
为此,通常使用`%XML.Writer`将启用XML的对象的输出写入流。
2. 创建`%SYS.X509Credentials`的至少一个实例,将访问要向其提供加密文档的实体的InterSystems IRIS凭据集。为此,请调用此类的`GetByAlias()`类方法。例如:
```java
set credset=##class(%SYS.X509Credentials).GetByAlias("recipient")
```
若要运行此方法,必须以该凭据集的`OwnerList`中包含的用户身份登录,否则`OwnerList`必须为空。
3. 至少创建`%XML.Security.EncryptedKey`实例。若要创建此类的实例,请使用此类的`CreateX509()`类方法。例如:
```java
set enckey=##class(%XML.Security.EncryptedKey).Createx509(credset,encryptionOptions,referenceOption)
```
- `credset`是`%SYS`的实例。
`x509credentials`在刚刚创建的新窗口中打开。
- `encryptionOptions`是`$$$SOAPWSIncludeNone`(还有其他选项,但它们不适用于此场景)。
此宏在`%soap.inc`包含文件中定义。
- `referenceOption`指定了对加密元素的引用的性质。
这里使用的宏在`%soap.inc`包含文件中定义。
4. 在创建`%Library.ListOfObjects`实例,并使用其`Insert()`方法在刚创建插入`%XML.Security.EncryptedKey`实例。
5. 使用`%New()`方法创建`%XML.Security.EncryptedData`实例。例如:
```java
set encdata=##class(%XML.Security.EncryptedData).%New()
```
6. 使用`%XML.Security.EncryptedData的EncryptStream()`实例方法加密在步骤2中创建的流。例如:
```java
set status=encdata.EncryptStream(stream,encryptedKeys)
```
- stream 流是在步骤1中创建的流。
- encryptedKeys是在步骤4中创建的密钥列表。
7. 创建并更新容器类的实例。
- 将键列表写入此类的相应属性。
- 将 `%XML.Security.EncryptedData`的实例写入此类的相应属性。
8. 使用`%XML.Writer`为容器类生成输出。
例如,前面显示的`CONTAINER`类还包括以下方法:
```java
/// w ##class(XMLEncryption.Container).Demo("E:\temp\SecurityXml.txt")
ClassMethod Demo(filename = "", obj = "")
{
#Include %soap
if (obj = "") {
s obj = ##class(MyApp.Person).%OpenId(1)
}
//从此启用XML的对象创建流
set writer = ##class(%XML.Writer).%New()
set stream = ##class(%GlobalCharacterStream).%New()
set status = writer.OutputToStream(stream)
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit }
set status = writer.RootObject(obj)
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit }
do stream.Rewind()
set container = ..%New() ; 这就是我们要写出的对象
set cred = ##class(%SYS.X509Credentials).GetByAlias("servercred")
set parts =$$$SOAPWSIncludeNone
set ref = $$$KeyInfoX509Certificate
set key = ##class(%XML.Security.EncryptedKey).CreateX509(cred, parts, ref)
set container.Key = key ; 这个细节取决于类
//需要创建一个键列表(本例中仅为一个)
set keys = ##class(%Collection.ListOfObj).%New()
do keys.Insert(key)
set encdata = ##class(%XML.Security.EncryptedData).%New()
set status = encdata.EncryptStream(stream, keys)
set container.Data = encdata ; 这个细节取决于类
// 为容器写输出
set writer = ##class(%XML.Writer).%New()
set writer.Indent = 1
if (filename'="") {
set status = writer.OutputToFile(filename)
if $$$ISERR(status) {do $system.OBJ.DisplayError(status) quit}
}
set status = writer.RootObject(container)
if $$$ISERR(status) {do $system.OBJ.DisplayError(status) quit}
}
```
此方法可以接受任何启用XML的类的`OREF`;如果没有提供,则使用默认值。
# 解密加密的XML文件
## 解密的前提条件
在解密加密的`XML`文档之前,必须同时提供以下两项:
- IRIS要使用的受信任证书。
- IRIS凭据集,其私钥与加密中使用的公钥匹配。
## 解密文档
要解密加密的XML文档,请执行以下操作:
1. 创建`%XML.Reader`实例打开并使用它打开文档。
2. 获取`Document`属性,`%XML.Reader`实例。
其中包含作为DOM的XML文档。
3. 使用阅读器的`correlation()`方法将``元素或元素与类`%XML.Security.EncryptedKey`关联起来。
例如:
```java
do reader.Correlate("EncryptedKey","%XML.Security.EncryptedKey")
```
4. 遍历文档以读取``元素或多个元素。
为此,可以使用阅读器的`Next()`方法,该方法通过引用返回一个导入的对象(如果有的话)。
例如:
```java
if 'reader.Next(.ikey,.status) {
write !,"Unable to import key",!
do $system.OBJ.DisplayError(status)
quit
}
```
导入的对象`是%XML.Security.EncryptedKey`的实例。
5. 创建`%Library.ListOfObjects`的实例。
并使用它的`Insert()`方法插入`%XML.Security.EncryptedKey`的实例。
刚从文档中获得的。
6. 调用类`%XML.Security.EncryptedData`的`ValidateDocument()`方法
```java
set status=##class(%XML.Security.EncryptedData).ValidateDocument(.doc,keys)
```
第一个参数(通过引用返回)是在第2步中检索到的DOM的修改版本。
第二个参数是上一步中的键列表。
7. 可以选择使用`%XML.Writer`为修改后的DOM生成输出。
例如,前面显示的`CONTAINER`类包含以下类方法:
```java
ClassMethod DecryptDoc(filename As %String)
{
#Include %soap
set reader = ##class(%XML.Reader).%New()
set status = reader.OpenFile(filename)
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit }
set doc = reader.Document
//获取元素
do reader.Correlate("EncryptedKey","%XML.Security.EncryptedKey")
if 'reader.Next(.ikey,.status) {
write !,"无法导入密钥",!
do $system.OBJ.DisplayError(status)
quit
}
set keys = ##class(%Collection.ListOfObj).%New()
do keys.Insert(ikey)
// 以下步骤返回解密的文档
set status = ##class(%XML.Security.EncryptedData).ValidateDocument(.doc,keys)
set writer = ##class(%XML.Writer).%New()
set writer.Indent = 1
do writer.Document(doc)
quit $$$OK
}
```
文章
姚 鑫 · 六月 15, 2021
# 第七章 控制命名空间分配的外观
# 控制命名空间分配的外观
除了控制命名空间分配外,还可以控制命名空间分配在XML输出中的显示方式。具体地说,可以控制以下内容:
## 显式名称空间分配与隐式名称空间分配
将元素和属性分配给命名空间时,XML中有两种等效的表示形式,由编写器实例的`SuppressXmlns`属性控制。
为一个名为`Person`的对象生成XML输出,该对象被分配给名称空间`“http://www.person.org”`(通过前面讨论的`namespace`类参数)。
使用缺省输出(`SuppressXmlns`等于0)的示例如下:
```java
Uberoth,Amanda Q.
1952-01-13
```
另一种可能的形式完全相同,如下所示。
这是使用`SuppressXmlns`等于1生成的,它确保显式分配给名称空间的每个元素都显示为该名称空间的前缀。
```java
Uberoth,Amanda Q.
1952-01-13
```
请注意,此属性仅影响命名空间分配的显示方式;它不控制如何分配任何命名空间。如果不使用命名空间,则此参数无效。
## 为命名空间指定自定义前缀
当为对象生成XML输出时,系统会根据需要生成命名空间前缀。第一个名称空间前缀是`s01`,下一个是`s02`,依此类推。可以指定不同的前缀。为此,请在启用XML的对象本身的类定义中设置`XMLPREFIX`参数。此参数有两个效果:
- 它确保在XML输出中声明指定的前缀。也就是说,即使没有必要这样做,它也会被声明。
- 它使用该前缀,而不是在其他情况下会看到的自动生成的前缀。
# 控制空字符串(`""`)的导出方式
为对象启用XML时,需要指定将空值和空字符串投影到XML的方式
其中一个选项是在支持xml的类中将`XMLIGNORENULL`设置为`“RUNTIME”`(不区分大小写)。
在这种情况下,当使用%XML.Write的`RuntimeIgnoreNull`属性的值来确定如何处理任何等于`""`的属性,如下所示:
- **如果编写器的`RuntimeIgnoreNull`属性为0(默认值),则`XMLNIL`参数控制如何导出该属性。`XMLNIL`是一个类参数和一个属性参数;属性参数优先。**
- 如果`XMLNIL`为0(默认值),则不投影特性。也就是说,它不包含在XML文档中。
- 如果`XMLNIL`为1,并且该属性用作元素,则该属性将按如下方式导出:
```
```
- 如果`XMLNIL`为1并且特性用作属性,则不会输出特性。
- 如果编写器的`RuntimeIgnoreNull`属性为1,则该属性将导出为空元素或空属性(其导出方式与值`$char(0)`相同,后者始终导出为空元素或空导出)。
除非`XMLIGNORENULL`在启用xml的类中是`“RUNTIME”`,否则编写器的`RuntimeIgnoreNull`属性是无效的。
## 示例:`RuntimeIgnoreNull`为0(默认值)
```java
Class EmptyStrings.Export Extends (%Persistent, %XML.Adaptor)
{
Parameter XMLNAME="Test";
Parameter XMLIGNORENULL = "RUNTIME";
///把这个作为一个元素
///XMLNIL is 0, the default
Property Property1 As %String;
///把这个作为一个元素
///XMLNIL is 0, the default
Property Property2 As %String(XMLPROJECTION = "ATTRIBUTE");
///将其作为XMLNIL=1的元素
Property Property3 As %String(XMLNIL = 1);
///将其作为XMLNIL=1的属性进行项目
Property Property4 As %String(XMLNIL=1,XMLPROJECTION="ATTRIBUTE");
}
```
如果创建了这个类的一个新实例(并且没有设置任何属性的值),然后使用 `%XML.Writer`输出,如下所示:
```java
```
## 例如:`RuntimeIgnoreNull`为1
```java
```
**在本例中,因为`RuntimeIgnoreNull`为1,所以没有使用`XMLNIL`参数。
相反,`""`被导出为空属性或空元素。**
# 导出类型信息
默认情况下,XML编写器不写入类型信息。有两个选项可用于在输出中包括类型信息:
- 编写器的`OutputTypeAttribute`属性。如果此属性为1,则编写器包括其写入的对象内所有元素的XML类型信息(但不包括对象本身)。例如:
```java
Petersburg,Greta U.
1949-05-15
```
请注意,相应的命名空间将添加到XML文档的根。
- `Object()`和`RootObject()`方法的`className`参数。此参数用于指定对象的预期`ObjectScript`类型(类名)。
如果参数与实际类型相同,则编写器不包括对象的类型信息。
如果参数与实际类型不同,编写器将包括对象的实际XML类型(默认为类名)。例如,假设`Test2.PersonWithAddress`实例编写输出,并将`className`参数指定为`MyPackage.MyClass`。因为`MyPackage.MyClass`与实际的类名不同,所以编写器生成以下输出:
```java
Avery,Robert H.
Ukiah
82281
```
# 生成SOAP编码的XML
对于`%XML.Writer`,`Format`属性控制输出的整体格式。这是以下选项之一:
- `“literal”`,即默认值,在本书的大多数例子中都使用了它。
- `“encoded”`,按照SOAP 1.1标准中的描述进行编码。
- `“encoded12”`,按照SOAP 1.2标准中的描述进行编码。
## 创建内联引用
在编码格式中,任何对象值属性都被作为引用包含,被引用的对象被导出为单独的元素。
要将这些属性内联导出,而不是作为单独的元素,请将`ReferencesInline`属性设置为1。
如果格式是`“literal”`,`ReferencesInline`属性没有效果。
# 导出后控制unswizling
当导出一个支持xml的持久对象时,系统会像往常一样自动将所有需要的信息混合到内存中;该信息包括对象值属性。导出对象后,InterSystems IRIS将消除任何对象列表,但(默认情况下)不会消除单个对象引用。
对于大对象,这可能导致``错误。
在这种情况下,要使任何单个对象引用不被混合,请在支持xml的类中设置`XMLUNSWIZZLE`参数,如下所示:
```java
Parameter XMLUNSWIZZLE = 1;
```
该参数默认值为0。
# 控制元素的关闭
只包含属性的元素可以用以下任一方式表示:
```java
```
`Object()`方法始终使用第一个语法导出元素。如果需要使用此处显示的第二种语法关闭元素,请手动编写对象,如本章前面的“手动构造元素”中所述。
咨询一下,我也是初学,有没有类似于oracle的exp远程备份数据库的方法?
文章
王喆 👀 · 九月 13, 2022
背景
作为集成平台厂商,在医院同其它系统联调的时候所做的事情中,多的不是开发代码而是查消息。我先演示一下目前我正在使用的IRIS查消息的方式:
例子1:
需要看【个人信息注册服务】
我只需要在框中输入【个人信息注册】回车
点击【查看消息】,显示的是消息里面的内容,如图所示:
点击【查看流程】,显示的是IRIS消息可视化的页面,如图所示:
例子2:
需要查询患者ID为【2874621】这患者在集成平台调用了哪些服务
我只需要选择下拉选择【患者ID】,然后输入【2874621】,回车
这个是我们以用户需求的角度直观的查询到指定的消息,IRIS也有这个功能—消息查看器,它是如何使用的呢?首先,我们得知道这条消息使用了哪些Production组件,其次我们需要了解这个消息使用的实体类的结构。比如同样查这个人的消息,我需要输入request.Body.PatientID=“2874621”,选择消息类。如果我需要查多个服务的我还需要多选消息类 …… 本文不是来介绍如何使用消息查看器的,各位大概知道就好。
程序分析与设计思路
原始消息查看器的使用
我们先使用IRIS自带的【消息查看器】查患者ID为【2874621】个人信息注册服务,如图所示:
选时间,输入图中所示的条件去检索。步骤上好像也挺简单的,但是这里有两个前提条件,一是我得知道每个服务对应的消息类是哪个。二是我知道这个消息类里面患者ID对应的字段是哪个,而且这个字段不能在循环里面(好像说了3个条件 ~ 哈哈),如何处理摆在了我们的眼前。
SQL分析
使用IRIS自带的【显示查询】功能(这个如何开启使用可以看我的另外一篇文章 https://cn.community.intersystems.com/node/525741 ),如图所示:
找个位置格式化一下:
可以看到从这段SQL可以看出3点:
(1)IRIS消息记录的表是【Ens.MessageHeader】
(2)IRIS保存消息是给实体本身建立了一个表 比如上述的例子:
【BKIP_PatientInfo_MSG.PatientInfoRegister】
(3)head.MessageBodyId = BKIP_PatientInfo_MSG.PatientInfoRegister.%ID
通过这3条,我们可以得到下面这张图的信息:
倘若我们把SQL按照如上图所示的方式处理,然后反应在页面上供用户选择;患者ID也由用户选择或者输入;这样确实直观的解决了。但是,效率,上述消息查看器在用户选择时间的时候首先查到两个ID让查询就在这两个主键ID之间查找,增加了效率,我们当然可以使用。那么它是必须的么,去掉的话我们速度一定变慢?这是其一。第二点,如果患者ID是在循环中,甚至是在循环的循环中…… 消息查看器好像就没办法了(当然如果有大佬可以解决请接受我的膝盖)。
需要解决的问题
对照表,服务和消息类的对照表
如何把循环中的字段作为关键字进行查询
第一个问题就是解决方案,新建一张表维护服务和消息类的关系,第二个问题:我们要去看一下Ens.MessageHeader这张表:
好像看不出什么。。。
大家可以看看这个图
在图中红框框住的部分是一样的,同时我们进入消息可视化,如图:
我们可以不可以说这个会话ID就是代表了这个服务的这条消息呢?我们要查指定服务的指定人的消息可以理解为查这个会话ID也就是SessionID。我们把思路换一下,如果我把循环中的患者ID遍历出来存入一张表中,这一行的数据我有sessionID、患者ID、服务名。当我想查患者ID为【2874621】的个人信息注册服务得到sessionID之后,直接跳到可视化追踪,如果把患者ID换成医嘱ID也是一样的处理,其它关键字段也是一样,问题是不是迎刃而解?
程序设计(思路)
新建一个索引表 字段为 SessionID、服务名、属性名、属性值、创建时间,如图所示:
然后在每次服务被调用的时候取出我们需要的属性名、属性值和SessionID存入这张表中,如图所示:
我们在页面上进行查询的时候只需要编写如图所示的SQL,省略号代表and后面的条件。
后续的REST接口和前端的页面在此就【略】,大家可以参考我的另外一篇文章提供的思路编写提供出来Restful接口(https://cn.community.intersystems.com/node/525561 )。
总结
总的来说,我们思路是我们把原本数据量变小,把原本的多重循环的问题维护在一张单表的多行数据中去。这样把原本的多表联合查询改成了单表查询。这速度不快的飞起?目前这边只做思路分享,相信程序设计上大家肯定有自己的一套方式,我这边暂时不做过多展示。都看到这里了,给我点一个赞吧!!! 可以考虑使用ElasticSearch做全文搜索 是的,用ES是最好的。但是ES中有一个很重要的概念叫倒排索引,就是拆分词语存入倒排索引的库,方便在检索的时候分析,和我这边的把一个消息关键字段拆分成索引去检索是否有一点异曲同工捏。当然还是那句话ES是最好的,只不过我这边不需要那么高性能,而且也没试过IRIS结合ES去使用,这里只做分享哈。 还可以考虑用iknow(现在叫InterSystems NLP)实现全文检索
文章
姚 鑫 · 六月 17, 2023
# 第六十章 镜像中断程序 - 使用主 `ISCAgent` 的日志数据进行 `DR` 提升和手动故障转移
## 使用主 `ISCAgent` 的日志数据进行 `DR` 提升和手动故障转移
如果 `IRIS A` 的主机系统正在运行,但 `IRIS` 实例没有且无法重新启动,您可以使用以下过程在通过升级后使用来自 `IRIS A` 的最新日志数据更新升级的 `IRIS C IRIS A` 的 `ISCAgent`。
1. 推广 `IRIS C`,选择 `IRIS A` 作为故障转移伙伴。 `IRIS C` 被提升为故障转移成员,从 `IRIS A` 的代理获取最新的日志数据,并成为主要成员。
2. 重新启动 `IRIS A` 上的 `IRIS` 实例,它作为备份重新加入镜像。
3. 在 `IRIS A` 重新加入镜像并变为活动状态后,可以使用使用升级的 DR 异步临时替换故障转移成员中描述的过程,将所有成员返回到它们以前的角色,首先是正常关闭 `IRIS C` ,然后在 `IRIS B` 的配置参数文件的 `[MirrorMember]` 部分中设置 `ValidatedMember=0`(请参阅配置参数文件参考中的 `[MirrorMember]`),将 `IRIS B` 重新启动为 `DR` 异步,将 `IRIS B` 提升为备份,并以 `DR` 异步方式重新启动 `IRIS C`。
注意:如果 `IRIS A` 的主机系统已关闭,但 `IRIS B` 的主机系统已启动,尽管其 `IRIS` 实例未运行,请按照手动故障转移到活动备份中所述在 `IRIS B` 上运行 `^MIRROR` 例程以确定 是否`IRIS B` 在发生故障时是一个活动备份。如果是这样,使用前面的过程,但在升级期间选择 `IRIS B` 作为故障转移伙伴,允许 `IRIS C` 从 `IRIS B` 的 `ISCAgent` 获取最新的日志数据。
## 使用来自日志文件的日志数据进行 DR 提升和手动故障转移
如果 `IRIS A` 和 `IRIS B` 的主机系统都已关闭,但可以访问 `IRIS A` 的日志文件,或者 `IRIS B` 的日志文件和消息日志可用,您可以使用最新的日志数据更新 `IRIS C`从升级前的初级开始,使用以下过程。
1. 使用 `IRIS A` 或 `IRIS B` 的最新日志文件更新 `IRIS C`,如下所示:
- 如果 `IRIS A` 的日志文件可用,则将最新的镜像日志文件从 `IRIS A` 复制到 `IRIS C`,从 `IRIS C` 上的最新日志文件开始,并包括来自 `IRIS A` 的任何后续文件。例如,如果 `MIRROR -MIRRORA-20180220.001` 是 `IRIS C` 上的最新文件,复制 `MIRROR-MIRRORA-20180220.001` 和 `IRIS A` 上的任何更新文件。
- 如果 `IRIS A` 的日志文件不可用但 `IRIS B` 的日志文件和消息日志可用:
1. 确认`IRIS B`很可能已被捕获,如下所示:
a. 确认当`A`及其代理不可用时,`B`同时断开与 A的连接。可以通过在`Messages.log`文件中搜索类似于以下内容的消息来检查 `IRIS B`断开连接的时间:
```java
MirrorClient: Primary AckDaemon failed to answer status request
```
b. 通过在其 `messages.log` 文件中搜索类似于以下内容的消息,确认 IRIS B 在断开连接时是活动备份:
```java
Failed to contact agent on former primary, can't take over
```
注意:`messages.log` 文件中的如下消息表明 `IRIS B` 在断开连接时未处于活动状态:
```java
nonactive Backup is down
```
当无法确认它是否已被追上时强制提升的 `DR` 异步成为主数据库可能会导致它成为主数据库而没有镜像生成的所有日志数据。因此,一些全局更新操作可能会丢失,而其他镜像成员可能需要从备份中重建。
2. 如果可以确认 `IRIS B` 处于活动状态,请将最新的镜像日志文件从 `IRIS B` 复制到 `IRIS C`,从 `IRIS C` 上的最新日志文件开始,然后包括来自 `IRIS B` 的所有后续文件。例如,如果 `MIRROR-MIRRORA-20180220.001` 是 `InterSystems IRIS C` 上的最新文件,请从 `IRIS C` 复制 `MIRROR-MIRRORA-20180220.001` 和任何更新的文件。检查文件的权限和所有权,并在必要时更改它们以匹配现有日志文件。
2. 在不选择故障转移合作伙伴的情况下将 `IRIS C` 提升为故障转移成员。 `IRIS C` 成为主要的。
3. 当 `IRIS A` 和 `IRIS B` 的问题得到修复时,尽早并在重新启动 `IRIS` 之前,在每个成员上的 `IRIS` 实例的配置参数文件的 `[MirrorMember]` 部分中设置 `ValidatedMember = 0`(参见 `[ MirrorMember]` 在配置参数文件参考)。说明指出,此更改是必需的。完成此操作后,在每个成员上重新启动 `IRIS`,从 `IRIS A`(最近成为主成员的成员)开始。
1. 如果成员在 `IRIS` 重新启动时作为备份或 `DR` 异步加入镜像,则不需要进一步的步骤。任何在故障成员上但不在当前主成员上的日志数据都已被丢弃。
2. 如果在 `IRIS` 实例重新启动时成员无法加入镜像,如重建镜像成员中描述的引用不一致数据的消息日志消息所示,则成员上的最新数据库更改晚于存在于上的最新日志数据 `IRIS C` 成为主要时。要解决此问题,请按照该部分中的描述重建成员。
4. 在大多数情况下,`DR` 异步系统不是主要故障转移成员的合适永久主机。在 `IRIS A` 和 `IRIS B` 重新加入镜像后,使用使用升级的 `DR` 异步临时替换故障转移成员中描述的过程将所有成员返回到它们以前的角色。如果 `IRIS A` 或 `IRIS B` 作为备份重新启动,则在备份处于活动状态时从正常关闭 `IRIS C` 开始,以故障转移到备份;如果 `IRIS A` 或 `IRIS B` 都重新启动为 `DR` 异步,将其中一个提升为备份,然后在 `IRIS C` 上执行正常关闭。将另一个以前的故障转移成员提升为备份,然后将 `IRIS C` 作为 `DR` 异步重启。
文章
Guangliang Zhang · 十月 21, 2022
cache数据库自身带有系统监控Portal界面,但需要运维人员定期主动查看才能获取监控信息。当系统故障发生时,容易出现由于没有及时获取故障信息而不能及时处理,从而导致造成的影响扩大。本文研究通过解析cache数据库控制台日志(cconsole.log)进行监控信息获取并主动推送微信或短信实现cache数据库主动实时监控。
cache数据库在运行时会将所有控制台消息包括一般消息、系统错误、某些操作系统错误和网络错误都会发送到控制台日志文件,通过操作员控制台工具从其他系统远程启动的作业的成功或失败等信息也会写入控制台日志,因此通过对控制台日志的解析即可获取所需要监控信息。具体步骤方法如下:
解析控制台日志
控制台日志默认存储在install-dir\mgr路径下。
根据cache版本不同,使用的读取方法也不同。对于cache2016版本以上,系统提供了EnsLib.SQL.Snapshot类,可以直接获取日志的行和列信息,非常方便。对于cache2010及以下版本则无此方法,需要使用%File文件读取方法。
随着系统运行时间增加,控制台日志也会不断增大,造成每次检索时间加大,且不容易找出最近的监控信息,作出有效监控。较好的方式是每次解析时均从上次的节点继续进行,以便获取新的有效的监控信息。可以使用指定global来记录每次执行的最后一行ID,下次执行时从ID+1行开始解析来达到目的。对于控制台日志,EnsLib.SQL.Snapshot方法里提供了方法以获取当前行,对于cache2010及以下版本,则需要记录最后一次处理的内容,再次处理的时先找到本条内容的位置,从此位置继续处理。代码示例如下。
cache2016及以上版本代码示例:
ClassMethod GetAlertInfo() As %String{
//cconsole.log存储路径 set FilePath="D:\InterSystems\HealthShare\mgr\cconsole.log"
//使用EnsLib.SQL.Snapshot读取日志 set snap=##class(EnsLib.SQL.Snapshot).%New() set snap.MaxRowsToGet=-1 do snap.ImportFile(FilePath,," ") do Consolelogshow do snap.Clean() quit $$$OK
//处理日志内容Consolelogshow set colsCount=snap.ColCount
//获取上一次处理的最后一行行号 set snap.%CurrentRow=$g(^AlertCurrentRow("Consolelog","CurrentRow")) k ^AlertCurrentRow("Consolelog","PID") while snap.Next() { set alertLevel=snap.GetData(3) set pid=snap.GetData(2) set pid=$p(pid,"(",2) set pid=$p(pid,")",1) continue:pid=""
//0级别信息不处理 continue:((alertLevel=0))
//进程号已存在不处理,即只获取每次每个进程的第一条告警/错误信息 continue:($d(^AlertCurrentRow("Consolelog","PID",pid)))
//记录已处理的进程号 set ^AlertCurrentRow("Consolelog","PID",pid)=1
//定义并组装监控消息内容 set alertInfo="" i ((alertLevel=1)||(alertLevel=2)||(alertLevel=3)){ f i=4:1:colsCount{ set alertInfo=alertInfo_" "_snap.GetData(i) } set alertInfo=snap.GetData(1)_":"_alertLevel_":"_alertInfo continue:((alertLevel=1)&&(alertInfo'["Warning"))
//发送监控信息至信息推送平台,如微信或短信等 set rt=..sendAlertInfo(alertInfo) }
//记录当前处理的行 set ^AlertCurrentRow("Consolelog","CurrentRow")=snap.%CurrentRow } quit $$$OK}
cache2010及以下版本代码示例:
ClassMethod GetAlertInfo() As %String{ //使用%File读取日志 set file=##class(%File).%New("/cachesys/mgr/cconsole.log") set Status= file.Open("R") k ^AlertCurrentRow("Consolelog","PID")
//获取上次处理的信息内容 set LastPosition=$g(^AlertCurrentRow("Consolelog","LastPosition")) s:LastPosition="" LastPosition=0 set LastPosition=LastPosition+1 set int=0 while 'file.AtEnd{ i int=0{
//跳转到上次处理的最后位置 do file.MoveTo(LastPosition) } set file.LineTerminator=$c(10) set text= file.ReadLine(,.sc) set int=int+1
//解析日志 set pid=$p(text," ",2) set pid=$p(pid,"(",2) set pid=$p(pid,")",1) set alertLevel=$p(text," ",3) s:text'="" textLast=text
//定义并组装监控消息内容 i ((alertLevel=1)&&(alertLevel["Warning"))||(alertLevel=2)||(alertLevel=3){ set length=$length(text," ") set alert="",alertInfo="" f i=4:1:length{ set alert=$p(text," ",i) i i=4{ set alertInfo=alert }else{ set alertInfo=alertInfo_" "_alert } } set alertInfo=$p(text," ",1)_": "_alertLevel_": "_pid_":"_alertInfo set alertInfo=$zcvt(alertInfo,"O","UTF8") i '$d(^AlertCurrentRow("Consolelog","PID",pid)){
//记录已处理的进程号 set ^AlertCurrentRow("Consolelog","PID",pid)=1 //发送监控信息至信息推送平台,如微信或短信等 set rt=..sendAlertInfo(alertInfo) } } }
//记录最后处理的行内容 set ^AlertCurrentRow("Consolelog","LastPosition")=file.FindAt(1,textLast) do file.Close() quit $$$OK}
控制台日志在记录信息时,经常会记录一系列问题。记录模式为时间+进程ID+信息级别+详细信息。如图1所示。信息级别从0级到3级,问题严重程度依次增加。为0的一般为正常运行信息,也是占日志主要部分的内容。问题级别为1的为警告信息。问题级别为2的为错误信息。问题级别为3的为严重错误信息。因此信息级别为1以上均是监控需要关注的信息。需要将此部分信息及时发出。
图1 cconsole.log普通记录示例
有时候控制台日志也会有其它格式的信息出现,如图2所示。此类信息说明有比较严重的异常行为出现,需要重点关注,因为一般将其全部发出。
图2 cconsole.log特殊记录示例
监控信息推送
控制台日志会对一个进程的多个操作进行记录,如果将所有信息发出,则会造成信息量过大而找不到重点。一般将每个进程的第一个告警或错误信息发出即可。处理方式见上述示例代码。
将获取到所需要发送的监控信息调取微信(如图3)或短信(如图4)或其它信息推送终端的接口即可进行信息推送。
图3 微信告警示例 图4 短信告警示例
任务部署
将程序部署成定时任务,按照所需自定义监控频率即可实现cache数据库的主动实时监控。如图5所示。
图5 监控任务示例 确实升级后的Cache强了很多 这个极实用,推荐大伙收藏。 实用,收藏
文章
Michael Lei · 二月 14
FHIR 用例集: 打破数字医疗壁垒,实现高质量发展
--促进互联互通,改进工作流程,提高数据洞察
简介
HL7® FHIR®(快速医疗互操作性资源)是以电子方式访问、交换和管理医疗信息的国际标准。与以往的标准不同,FHIR 可让帮助行业从业者轻松构建创新应用程序,有效地收集、汇总和分析来自不同来源的各种医疗保健和管理数据。医疗机构、社保/保险公司、政府机构、生命科学公司、医疗设备制造商和医疗科技等多种主体利用 FHIR 来简化信息流、提高数据洞察力、改善临床效果和业务成果。
FHIR 基于 JSON、HTTP 和 REST 等流行的网络技术。有了 FHIR,没有医疗信息化背景的软件开发人员也能使用熟悉的开发工具和开源技术,快速、轻松地满足政府机构、临床医生、研究人员、医疗行业从业者以及各类市场主体的数据需求。
FHIR 是一种灵活、适应性强的医疗数据模型,可轻松定制,以实现各种用例的互操作性。FHIR 由称为 "资源 "的离散、可计算的数据对象组成,以实现最佳效率。通过 FHIR 资源,应用程序可以访问单个医疗记录元素,而无需检索摘要文档中包含的所有数据。
本文回顾了 FHIR 的实际应用,并提供了 InterSystems 客户如何使用 FHIR 连接不同系统、加速数字化转型和提高数据洞察力的真实案例。
FHIR 商机无限
FHIR 正在改变医疗健康数据的访问和交换。无论您是为政府、医疗机构、公共卫生机构、保险公司还是厂商工作,FHIR 都能帮助您高效地获取、检索和共享来自电子病历系统、智能医疗设备、可穿戴设备、临床试验和公共卫生监测系统等不同来源的医疗数据。
当前应用和未来的 FHIR 用例FHIR支持实现大量的不同业务场景。您可以在各种部署场景中将 FHIR 用于各种目的。下面的列表总结了 FHIR 在不同行业领域的一些当前应用和潜在的未来用例。
医疗机构
应用场景: 患者数据访问 API机会:可以基于FHIR资源和技术框架实现卫健委互联互通三年攻坚计划以及国家数据局"数据要素x医疗行业"三年行动计划中提到的相关电子健康档案共享、检验检查互认、医疗行业数据要素流通、交易等战略目标,通过基于标准的 (FHIR) API 让患者以程序化的方式访问其健康数据(病史、化验结果、治疗计划等),以及未来可能的全国统一医疗健康档案超级APP(患者端)。
应用场景: 临床决策支持机会: 使用 FHIR 改善临床决策系统的洞察力。将实时电子病历数据安全传输到第三方系统进行分析并返回建议,帮助临床医生做出明智决策。与以往的标准和方法不同,使用 FHIR,您可以将临床决策支持功能直接嵌入电子病历,以简化流程。
应用场景: 医疗机构与支付方(医保/保险公司)的合规数据交换 机会: 利用 FHIR 自动化医疗机构与支付方之间的数据交换。消除资源密集、耗时的人工流程(降低飞行检查和审计成本)。允许医疗机构直接将电子病历数据转发给支付方,无需人工干预。
使用案例: 临床试验和研究机会: 使用 FHIR 无缝共享临床试验招募和分析所需的患者数据,加快临床研究进程。
设备制造商、医疗科技公司和应用开发商
用例:远程医疗和远程监控机会: 使用 FHIR 可将患者数据从家用医疗设备安全地传输给医疗服务提供者,以便他们有效地远程监控和管理患者。
用例: 移动医疗应用程序机遇: 患者可以在手机端访问在不同医院治疗的电子病历,并且确保患者数据的隐私和安全。
用例: 慢性病管理应用程序机会: 使用 FHIR 在医疗服务提供者之间无缝共享患者数据,以实现一致的监控和协调的护理计划。
用例: 药物管理应用程序机会: 为临床医生和护理人员创建多功能药物管理应用程序。使用 FHIR 在区域全民健康信息平台之间高效共享处方信息、用药计划和药房记录。
生命科学公司、政府机构和付款人
用例:健康信息交换机会: 使用 FHIR,政府、公共卫生、保险公司等可高效开展电子健康档案/电子病历共享调阅数据,以进行质量评估、护理差距识别、理赔裁定以及开展潜在的数据交易等。
用例: 护理计划机会: 利用 FHIR,让跨机构护理团队--医生、家庭医疗工作者、社区护理人员、家庭成员等--能够无缝交换信息。让不同的医疗保健系统进行有效沟通。确保所有护理团队成员都能获得最新的患者信息。
用例: 公共卫生报告机会: 使用 FHIR 有效地汇总和共享患者数据,以进行监控和人口健康管理,从而简化公共卫生报告。利用电子病历批量检索功能。(注:该功能自 2022 年起已成为所有美国电子病历系统的强制性要求,在WHO、OECD、欧盟、亚洲、港澳台等地区也正在逐步推广普及)
以上只是部分FHIR的用例,有了FHIR,从业者可以打开无限想象空间,创建丰富多样、互联互通的数字医疗创新应用。
文章
Michael Lei · 七月 25
ISC 开发者们,我向你们致敬 👑。
多卷数据库
下面有关多卷数据的解释直接从文档搬过来的:
在InterSystems IRIS的默认配置中,数据库会使用单个 IRIS.DAT 文件保存数据。 你也可以将数据库配置为在当其达到指定大小阈值时自动保存到另外的文件(IRIS–0001.VOL、IRIS–0002.VOL 等等)中。 这些文件可能位于与 IRIS.DAT 相同的目录中和/或一组其他的目录中。
我这里想做的是设置一个较小阈值,并检查在备用目录上保存的多个扩展的数据卷。毫无疑问,这对镜像、性能 以及管理的影响是巨大的。简单来说,,前瞻性的解决方案考虑是否可以注入一个“回调”机制,并在溢出扩展之前即时地配置一个新的云存储卷。。
环境我在 2024.1 (Build 263U) 上有一个正在运行的IRIS实例,我的 $ISC_DATA_DIRECTORY 设置为一个 50Gi 的OpenEBS PVC,这是大概一个月前配置的。我向命名空间添加了一个额外的 OpenEBS PVC:
#kind: PersistentVolumeClaim
#apiVersion: v1
#metadata:
# name: jiva-iris-volume-claim
#spec:
# storageClassName: openebs-jiva-csi-default
# accessModes:
# - ReadWriteOnce
# resources:
# requests:
# storage: 50Gi
#--
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: jiva-iris-volume-claim-mv
spec:
storageClassName: openebs-jiva-csi-default
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
应用
sween@run1:~$ kubectl apply -f deezwatts-volume.yaml -n rivian
persistentvolumeclaim/jiva-iris-volume-claim-mv created
随后,通过初始化容器应用这些设置。
<snips>
volumes:
- name: task-pv-storage
persistentVolumeClaim:
claimName: jiva-iris-volume-claim
- name: task-pv-storage-mvd
persistentVolumeClaim:
claimName: jiva-iris-volume-claim-mv
<snips>
volumeMounts:
- name: task-pv-storage
mountPath: /data
- name: task-pv-storage-mvd
mountPath: /data-mvd
现在,我们将另一个磁盘卷设置为 `/data-mvd` 的多卷扩展存储
设置
以下是使用 System Management进行设置
首先在IRIS的实例中创建数据库,并设置创建数据库的一些基本属性。
我们创建了数据库“mvd”,以及主数据库文件保存路径,在点击Next后,向导页面会有一些新的设置内容:New Volume Threshold 设置触发扩展存储的数据库大小,在设置后请注意配置参数下方的提示,这个提示非常重要。否则,你又会看到相关的警告。
输入值为零则禁用新卷的自动创建。 如果不为零,当 IRIS.DAT 大小达到到此阈值时,将创建名为 IRIS-0001.VOL 的新数据库文件。 当新数据库文件再次达到阈值时,将创建 IRIS-0002.VOL文件,依此类推。 对于非零值,建议至少设置为 1 TB,以避免文件数量过多。 每个数据库被限制为最多扩展使用200 个数据库文件。
第二步,挂载新的数据库,在挂载之前无法对其进行其他配置。
现在,我们可以在数据库列表中看到 Volumes 的选项
点进Volumes后,我们可以为扩展使用多个数据库文件的数据库设置备用保存位置。扩展
由于我在这个例子中将数据库阈值设置的相当小,它会生成多个数据库文件。 为了将IRIS.DAT 中的空间耗尽,我新建了一个命名空间使用该数据库:
在该命名空间中,我调用 ZPM,从 openexchange 安装了点东西,并运行。
irisowner@iris-deezwatts-deployment-7b9bfcff8f-dssln:~$ irissession IRIS
Node: iris-deezwatts-deployment-7b9bfcff8f-dssln, Instance: IRIS
USER>zn "MVD"
MVD>zn "%SYS" d ##class(Security.SSLConfigs).Create("z") s r=##class(%Net.HttpRequest).%New(),r.Server="pm.community.intersystems.com",r.SSLConfiguration="z" d r.Get("/packages/zpm/latest/installer"),$system.OBJ.LoadStream(r.HttpResponse.Data,"c")
Load started on 06/04/2024 13:43:08
Loading file /data/IRIS/mgr/Temp/z9mu1CvnPnaGbA.xml as xml
Imported class: %ZPM.Installer
Compiling class %ZPM.Installer
Compiling routine %ZPM.Installer.1
Load finished successfully.
%SYS>zpm
=============================================================================
|| Welcome to the Package Manager Shell (ZPM). version 0.7.1 ||
|| Enter q/quit to exit the shell. Enter ?/help to view available commands ||
|| Current registry https://pm.community.intersystems.com ||
=============================================================================
zpm:%SYS>install "zpm-registry"
在数据库 UI 中查看其属性:
pod 中设置的文件夹下的内容:
irisowner@iris-deezwatts-deployment-7b9bfcff8f-dssln:/data-mvd$ ls -ltr /data-mv*
total 5140
drwxrwxrwx 2 irisowner irisowner 16384 Jun 4 11:56 lost+found
-rw-rw---- 1 irisowner irisowner 20 Jun 4 12:11 iris.dbdir
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:25 IRIS-0022.VOL
irisowner@iris-deezwatts-deployment-7b9bfcff8f-dssln:/data-mvd$ ls -ltr /data/IRIS/mgr/mvd
total 164
drwxrwxrwx 2 irisowner irisowner 4096 Jun 4 11:15 stream
-rw-rw---- 1 irisowner irisowner 63 Jun 4 12:01 iris.lck
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0001.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0002.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0003.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0004.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0005.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0006.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0007.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0008.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0009.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0010.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0012.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0015.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0018.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0016.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0019.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0020.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0017.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0014.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0013.VOL
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 12:11 IRIS-0011.VOL
-rwxrwxrwx 1 irisowner irisowner 5242880 Jun 4 13:08 IRIS.DAT
-rw-rw---- 1 irisowner irisowner 5242880 Jun 4 13:08 IRIS-0021.VOL
看来我有新玩具了!结论这篇帖子很短,可能发到讨论区更合适。不过我得回去工作了,这是我从 @jtrog 那看到新特性。期待未来在社区看到更多使用这项功能的分享和体验。我们峰会上见!