清除过滤器
文章
Jingwei Wang · 七月 11, 2022
InterSystems DeepSee的目的是使你能够将BI嵌入到你的应用程序中,这样你的用户就可以对他们的数据提出和回答复杂的问题。你的应用程序可以包括仪表盘,它包含图形部件。这些部件用来显示数据,由透视表和KPIs(关键绩效指标)驱动。对于一个透视表,用户可以显示一个列表,用其显示源值。
透视表、KPIs和列表是查询,在运行时执行。
数据透视表可以对运行时的输入作出反应,如用户的过滤器选择。在内部,它使用一个MDX(MultiDimensional eXpressions)查询,与DeepSee cube进行通信。一个cube由一个事实表和其索引组成。一个事实表由一组事实(行)组成,每个事实对应于一个基本记录。例如,这些事实可以代表病人或部门。DeepSee还生成了一组维度表(level tables)。所有的表都是动态维护的,根据你的配置和实现,DeepSee检测你的事务表的变化,并传播到事实表。当用户在分析器中创建透视表时,DeepSee会自动生成一个MDX查询。
KPI也可以对运行时的用户输入做出反应。在内部,它使用MDX查询(与DeepSee立方体)或SQL查询(与任何表)。在这两种情况下,你都可以手动创建查询,或从其他地方复制它。
列表显示来自用户选择的透视表行的源记录的选定值。在内部,一个列表是一个SQL查询。你可以指定要使用的字段,让DeepSee生成实际的查询。或者你可以指定整个查询。
仪表盘可以包括启动行动的按钮和其他控件。可以使用操作、设置过滤器、刷新仪表盘、打开其他仪表盘或其他URL,运行自定义代码,等等。DeepSee提供了一套标准行为,你也可以定义自定义。
DeepSee组件
要把DeepSee添加到一个应用程序中,你要添加以下一些或全部的组件。
数据连接器类(Data connector):数据连接器使你能够使用一个任意的SQL查询作为立方体或列表的来源。
cube定义类(Cube definition)一个cube定义了DeepSee透视表内使用的元素,并控制相应的事实表和索引的结构和内容。
一个cube定义指向作为它的基础使用的事务类(或数据连接器类)。
你可以有任何数量的cubes,而且你可以使用一个给定的类作为多个cubes的基础。
对于每个cube,DeepSee会生成并填充一个事实表类和其他类.
主题区类(Subject area ) : 一个主题区主要是一个过滤的cube。(它包括一个过滤器和cube体定义的不同部分的重写,如需要)。你可以在DeepSee中交替使用cube和主题区。
KPI定义类 : 当你需要自定义查询时,你会定义KPI,特别是在运行时根据用户输入确定的查询。当你需要自定义操作时,你也定义KPI,因为操作包含在KPI类中。
透视表 : 你通过拖放来创建。DeepSee会生成基础的MDX查询。
仪表盘 : 通过运行基础查询和显示结果来显示透视表和KPI。
用户门户 : 显示透视表和仪表盘。
基于高可用的推荐架构
对于任何大规模的应用,InterSystems建议你将DeepSee cube 建立在镜像服务器上的应用数据上,如下图所示。设置镜像,使应用程序的数据被镜像到镜像服务器上。在镜像服务器上,创建一个数据库,包含DeepSee立方体定义和(可选)数据。这样DeepSee就可以访问应用数据。但是,对于小规模的应用程序或演示,所有的代码和数据都可以在同一个数据库中。
主要实施步骤 - 此步骤会在之后的文章详细介绍
主要实施步骤
实施过程包括以下步骤。
建立web 应用。
从其他数据库中映射DeepSee的globals,以获得性能(可选择的,非必要步骤)。
创建cube和主题区域。这个过程包括以下步骤,你可以根据需要反复进行。
定义一个或多个cubes。在这个步骤中,你可以使用DeepSee Architect或者Studio。
建立cube。你可以使用Architect或终端。
使用DeepSee分析器来查看cubes并验证它们。
在定义好cube后,在这些cube的基础上定义任何主题区域。
创建KPIs(可选择的,非必要步骤)。
创建自定义行为(可选择的,非必要步骤)。
根据需要进行修改,以保持cubes的有效性。这样做的目的取决于数据必须是最新的,以及任何性能考虑。
创建透视表和仪表盘。
将透视表和仪表盘打包成类,以方便部署。
创建从你的应用程序到仪表盘的链接。
在这个过程中,你可能还需要做以下工作。
创建数据连接器。
配置设置。
执行本地化。
在仪表盘中使用自定义小程序。
执行其他开发任务。
安全设置。
实施工具
在实施过程中你会用到以下工具。
从管理门户的DeepSee部分提供的工具。
模型(Architech): 用来定义cube和主题区域。也可以编译cube和编译主题区域。
分析器(Analyzer) - 在验证你的模型时,使用它来检查立方体和主题区。后来你用它来创建透视表。
用户门户 :用它来定义仪表盘。
查询工具 : 使用它来创建MDX查询并查看其查询计划。
文件夹管理器 - 主要用于导出透视表和仪表盘,这样你就可以在类中打包它们的定义,你也可以用它来将资源与文件夹联系起来。
设置选项 : 使用它来指定用户门户的外观和行为,并定义可用于仪表盘的变量。
DeepSee日志 : 使用这个来查看这个命名空间的DeepSee编译日志。
Studio - 使用它来定义高级cube功能,cube元素使用的任何方法或例程,以及cube类中的任何回调方法。你还可以用它来定义KPI。
终端 - 可以用它来重建立方体和测试方法。
MDX shell(在终端运行)- 可以用它来检查cube和主题区域,创建自定义的MDX查询并查看其结果。
管理门户的其他部分 - 使用这些来做global映射,定义资源、角色和用户,以便与DeepSee一起使用,并在需要时检查DeepSee事实表。
Utility 方法 - %DeepSee.Utils包括一些方法,可以用来建立cube,同步cube,清除单元缓存,以及其他任务; %DeepSee.UserLibrary.Utils包括一些方法,可以用来以编程方式执行文件夹管理器中支持的任务。
数据连接器类(%DeepSee.DataConnector)- 使用它可以使任意的SQL查询在DeepSee立方体和列表中使用。
结果集API(%DeepSee.ResultSet)- 使用它来编程执行MDX查询并访问结果。
文章
姚 鑫 · 四月 18, 2021
# 第三章 优化表(一)
要确保InterSystems IRIS®Data Platform上的InterSystems SQL表的最高性能,可以执行多种操作。优化可以对针对该表运行的任何查询产生重大影响。本章讨论以下性能优化注意事项:
- `ExtentSize`、`Selective`和`BlockCount`用于在用数据填充表之前指定表数据估计;此元数据用于优化未来的查询。
- 运行tune Table来分析填充表中的代表表数据;生成的元数据用于优化未来的查询。
- 优化表计算的值包括扩展大小、选择性、异常值选择性、平均字段大小和块计数
- 导出和重新导入优选表统计数据
# 扩展大小、选择性和块数(ExtentSize, Selectivity, and BlockCount)
当查询优化器决定执行特定SQL查询的最有效方式时,它会考虑以下三种情况:
- 查询中使用的每个表的`ExtentSize`行计数。
- S`electivity`为查询使用的每列计算的DISTINCT值的百分比。
- 查询使用的每个SQL映射的块计数。
为了确保查询优化器能够做出正确的决策,正确设置这些值非常重要。
- 在用数据填充表之前,可以在类(表)定义期间显式设置这些统计信息中的任何一个。
- 在用代表性数据填充表之后,可以运行tune Table来计算这些统计数据。
- 运行TuneTable之后,可以通过指定显式值来覆盖计算的统计信息。
可以将显式设置的统计信息与优化表生成的结果进行比较。如果优化表所做的假设导致查询优化器的结果不是最优的,则可以使用显式设置的统计信息,而不是优化表生成的统计信息。
在Studio中,类编辑器窗口显示类源代码。在源代码的底部,它显示了Storage定义,其中包括类`ExtentSize`和每个属性的选择性(如果合适,还包括`OutlierSelectivity`)。
## ExtentSize
表的`ExtentSize`值就是表中存储的行数(大致)。
**在开发时,可以提供初始`ExtentSize`值。如果未指定`ExtentSize`,则默认值为100,000**。
通常,会提供一个粗略的估计,即在填充数据时该表的大小是多少。
有一个确切的数字并不重要。
此值用于比较扫描不同表的相对成本;
最重要的是确保关联表之间的`ExtentSize`的相对值代表一个准确的比例(也就是说,小表的值应该小,大表的值应该大)。
- `CREATE TABLE`提供了一个`%EXTENTSIZE`参数关键字来指定表中的预期行数,示例如下:
```sql
CREATE TABLE Sample.DaysInAYear (%EXTENTSIZE 366,
MonthName VARCHAR(24),Day INTEGER,
Holiday VARCHAR(24),ZodiacSign VARCHAR(24))
```
表的持久类定义可以在存储定义中指定`ExtentSize`参数:
```xml
...
200
...
```
在本例中,片段是`MyClass`类的存储定义,它为`ExtentSize`指定了200的值。
如果表有真实的(或真实的)数据,可以使用管理门户中的调优表功能自动计算和设置它的区段大小值;
## Selectivity
在InterSystems SQL表(类)中,每个列(属性)都有一个与之相关联的选择性值。
列的选择性值是在查询该列的典型值时返回的表中的行的百分比。
选择性为`1/D`,其中D是字段不同值的数目,除非检测到异常值。
选择性基于大致相等的不同值的数量。例如,假设一个表包含一个性别列,其值大致均匀分布在`“M”`和`“F”`之间。性别栏的选择值将为50%。更具区分性的特性(例如街道名称`Street Name`)的选择性值通常只有很小的百分比。
所有值都相同的字段的选择性为`100%`。为了确定这一点,优化器首先测试一小部分或几条记录,如果这些记录都具有相同的字段值,它将测试多达`100,000`条随机选择的记录,以支持非索引字段的所有值都相同的假设。如果在对`100,000`条随机选择的记录进行的测试中可能未检测到某个字段的其他值,则应手动设置选择性。
**定义为唯一(所有值都不同)的字段的选择性为1(不应与`1.0000%`的选择性混淆)。例如,`RowID`的选择性为1。**
在开发时,可以通过在存储定义中定义一个选择性参数来提供此值,该参数是表的类定义的一部分:
```xml
...
50%
...
```
若要查看类的存储定义,请在Studio中,从“视图”菜单中选择“查看存储”;Studio在类的源代码底部包含存储。

通常,需要提供在应用程序中使用时预期的选择性的估计值。与`ExtentSize`一样,拥有确切的数字并不重要。InterSystems IRIS提供的许多数据类型类将为选择性提供合理的默认值。
还可以使用`SetFieldSelectivity()`方法设置特定字段(属性)的选择值。
如果表中有真实的(或真实的)数据,则可以使用管理门户中的Tune table工具自动计算和设置其选择性值。
调优表确定一个字段是否有一个离群值,这个值比任何其他值都常见得多。
如果是这样,Tune Table将计算一个单独的离群值选择性百分比,并根据这个离群值的存在来计算选择性。
异常值的存在可能会极大地改变选择性值。
选择性用于查询优化。
在`SELECT`查询中指定的字段和在视图的`SELECT`子句中指定的字段使用相同的选择性值。
请注意,视图的行分布可能与源表不同。
这可能会影响视场选择性的精度。
## BlockCount
当编译一个持久化类时,类编译器会根据区段大小和属性定义计算每个SQL映射使用的映射块的大致数量。
可以在调优表工具的Map `BlockCount`选项卡中查看这些`BlockCount`值。
块计数在调优表中由类编译器估计。
注意,如果更改了区段大小,则必须关闭并重新打开SQL Tune Table窗口,以查看该更改反映在`BlockCount`值中。
当运行Tune Table时,它会测量每个SQL映射的实际块计数。
除非另有指定,调优表测量值将替换类编译器的近近值。
这些调优表测量值在类定义中表示为负整数,以区别于指定的`BlockCount`值。
如下面的例子所示:
```xml
-4
```
调优表测量值在调优表中表示为正整数,标识为由调优表测量。
可以在类定义中定义显式的块计数值。
可以显式地指定块计数为正整数,如下面的示例所示:
```xml
12
```
当定义一个类时,可以省略为`map`定义`BlockCount`,显式地指定一个`BlockCount`为正整数,或显式地定义`BlockCount`为`NULL`。
- 如果不指定块计数,或指定块计数为0,则类编译器估计块计数。
运行Tune Table将替换类编译器的估计值。
- 如果指定一个显式的正整数`BlockCount`,运行Tune Table不会替换此显式的`BlockCount`值。
在调优表中,显式的类定义块计数值表示为正整数,标识为在类定义中定义的。
这些块计数值不会通过随后运行Tune Table而更改。
- 如果将显式`BlockCount`指定为`NULL`,则SQL Map将使用类编译器估计的`BlockCount`值。因为`BlockCount`在类定义中是“定义的”,所以运行Tune Table不会替换这个估计的`BlockCount`值。
所有InterSystems SQL映射块的大小为2048字节(2K字节)。
在以下情况下,优化表不测量块计数:
- 如果表是由数组或列表集合投影的子表。这些类型的子表的`BlockCount`值与父表数据映射的`BlockCount`值相同。
- 如果全局映射是远程全局(不同名称空间中的全局)。取而代之的是使用在类编译期间使用的估计的`BlockCount`。
# Tune Table
Tune Table是一个实用程序,它检查表中的数据,并返回关于区段大小(表中的行数)、每个字段中不同值的相对分布以及平均字段大小(每个字段中值的平均长度)的统计信息。
它还为每个SQL映射生成块计数。
可以指定该调优表,使用此信息更新与表及其每个字段相关联的元数据。
查询优化器随后可以使用这些统计信息来确定最有效的查询执行计划。
在外部表上使用Tune Table将只计算区段大小。
调优表无法计算外部表的字段选择性值、平均字段大小或映射块计数值。
## 何时运行调优表
**应该在每个表填充了具有代表性的实际数据之后,在该表上运行tune Table。通常,在数据“激活”之前,只需要运行一次tune Table,这是应用程序开发的最后一步。Tune Table不是维护实用程序;它不应对实时数据定期运行。**
**注:在极少数情况下,运行调优表会降低SQL性能。虽然TuneTable可以在实时数据上运行,但建议在具有实际数据的测试系统上运行TuneTable,而不是在生产系统上运行。可以使用可选的系统模式配置参数来指示当前系统是测试系统还是活动系统。设置后,系统模式将显示在管理门户页面的顶部,并可由`$SYSTEM.Version.SystemMode()`方法返回。**
通常,在添加、修改或删除表数据时不应重新运行Tune Table,除非当前数据的特征发生了数量级的更改,如下所示:
- 相对表大小:Tune Table假设它正在分析具有代表性的数据子集。如果该子集是代表性子集,则该子集只能是整个数据集的一小部分。如果联接或其他关系中涉及的表的`ExtentSize`保持大致相同的相对大小,则当表中的行数发生变化时,Tune Table结果仍然是相关的。如果连接表之间的比率更改了一个数量级,则需要更新`ExtentSize`。这对于`JOIN`语句很重要,因为SQL优化器在优化表连接顺序时使用`ExtentSize`。一般来说,无论查询中指定的联接顺序如何,都会先联接较小的表,然后再联接较大的表。因此,如果`tableA`和`tableB`中的行比从`1000:2000`更改为`10000:2000`,可能在一个或多个表上重新运行tune Table,但如果更改为`2100:4000`,则不需要重新运行tune Table。
- 均匀值分布:优化表假设每个数据值的可能性都是相等的。如果它检测到离群值,它会假定除离群值之外的每个数据值的可能性都是相等的。调谐表通过分析每个字段的当前数据值来建立选择性。真实数据的可能性相等始终是一个粗略的近似值;不同数据值的数量及其相对分布的正态变化不应保证重新运行调优表。但是,字段可能值的数量(不同值与记录的比率)的数量级变化或单个字段值的总体可能性可能会导致不准确的选择性。大幅更改具有单个字段值的记录的百分比可能会导致TuneTable指定一个离群值或删除指定的离群值,从而显著改变计算的选择性。如果字段的选择性不再反映数据值的实际分布,则应重新运行调优表。
- 重大升级或新的站点安装可能需要重新运行Tune Table。
## 运行 Tune Table
运行调优表有三个接口:
- 使用Management Portal SQL interface Actions下拉列表,它允许在单个表或多个表上运行Tune Table。
- 为单个表或当前命名空间中的所有表调用`$SYSTEM.SQL.Stats.Table.GatherTableStats()`方法。
- 对单个表发出SQL命令调优表。
Tune Table清除引用正在调优的表的缓存查询。
调优表命令提供了一个recompile缓存查询选项,以使用新的调优表计算值重新生成缓存的查询。
如果表映射到只读数据库,则无法执行调优表,并生成错误消息。
在运行了调优表工具之后,生成的区段大小和选择性值将保存在类的存储定义中。
要查看存储定义,在Studio中,从“视图”菜单中选择“视图存储”;
Studio在类源代码的底部包含存储。
### 从管理门户调优表
要从管理门户运行Tune Table:
1. 选择System Explorer,然后选择SQL。
通过单击页面顶部的Switch选项选择一个名称空间,然后从显示的列表中选择一个名称空间。
(可以为每个用户设置管理门户的默认名称空间。)
2. 从屏幕左侧的下拉列表中选择模式,或者使用筛选器。
3. 执行下列操作之一:
- 优化单个表:展开表类别,然后从列表中选择一个表。选择表格后,单击操作下拉列表,然后选择调整表格信息。这将显示表的当前`ExtentSize`和选择性信息。如果从未运行过调谐表,`ExtentSize=100000`,则不会显示任何选择性、异常值选择性、异常值或平均字段大小信息(除了选择性为1的行ID),并且会按照类编译器的估计列出映射块计数信息。
从选择性选项卡中,选择调谐表按钮。这将在表上运行tune Table,根据表中的数据计算ExtentSize、选择性、异常值选择性、异常值和Average Field Size值。Map BlockCount(地图块计数)信息按Tune Table(调谐表)测量列出。
单个表上的Tune Table始终作为后台进程运行,并在完成后刷新该表。这可以防止超时问题。当此后台进程正在运行时,将显示一条正在进行的消息。在后台进程执行时,关闭按钮可用于关闭调谐表窗口。
- 优化方案中的所有表:单击操作下拉列表,然后选择优化方案中的所有表。这将显示调谐表方框。选择Finish按钮在方案中的所有表上运行Tune Table。调谐表完成后,此框显示完成按钮。选择Done(完成)退出Tune Table(调谐表)框。
SQL优化表窗口有两个选项卡:选择性和映射块计数。这些选项卡显示由调谐表生成的当前值。它们还允许手动设置与Tune Table生成的值不同的值。
选择性选项卡包含以下字段:
- 当前表扩展大小。此字段有一个编辑按钮,允许输入不同的表格扩展大小。
- “使类保持最新”复选框。对Tune Table生成的统计数据的任何更改,或由Tune Table界面或Tune Table方法中的用户输入值生成的任何更改,都会立即表示在类定义中:
- 如果未选中此框(否),则不会设置修改后的类别定义上的最新标志。这表明类定义已过期,应该重新编译。这是默认设置。
- 如果选中此框(是),类定义将保持标记为最新。在活动系统上更改统计信息时,这是首选选项,因为它降低了重新编译表类定义的可能性。
- 字段表,其中包含字段名称、选择性、备注、异常值选择性、异常值和平均字段大小等列。通过单击`Fields`表格标题,可以按该列的值进行排序。通过单击`Fields`表行,您可以手动设置该字段的选择性、异常值选择性、异常值和平均字段大小的值。
Map BlockCount选项卡包含以下字段:
- 包含SQL Map Name、BlockCount和Source of BlockCount列的映射名称表。索引的SQL映射名称是SQL索引名;这可能不同于持久类索引属性名。
- 通过单击单个map名称,可以手动设置该地图名称的`BlockCount`值。
在选择性选项卡中,可以单击优化表按钮在此表上运行优化表。
### 使用方法调整表
可以使用`$SYSTEM.SQL.Stats.Table.GatherTableStats()`方法在当前名称空间中运行Tune Table工具。
- `GatherTableStats(“Sample.MyTable”)`在单个表上运行TuneTable。
- `GatherSchemaStats(“Sample”)`在指定模式中的所有表上运行tune Table。
- `GatherTableStats(“*”)`在当前命名空间中的所有表上运行TuneTable。
使用`GatherTableStats()`方法时,可能会生成以下错误消息:
- 不存在的表:
```java
DO $SYSTEM.SQL.Stats.Table.GatherTableStats("NoSuchTable")
```
```java
No such table 'SQLUser.NoSuchTable'
```
- `View`视图:
```java
DO $SYSTEM.SQL.Stats.Table.GatherTableStats("Sample.MyView")
```
```java
'Sample.MyView' is a view, not a table. No tuning will be performed.
```
当运行`GatherTableStats(“*”)`或`GatherSchemaStats(“SchemaName”)`时,如果系统支持并行处理,系统将使用多个进程并行调优多个表。
## 在分片表上运行Tune table
如果在一个分片表上运行调优表,那么调优表操作将被转发到每个碎片,并针对该表的那个碎片运行。
调优表不会在调用它的主名称空间中执行。
如果在导出到碎片的类定义的非分片表上运行调优表,因为该表已连接到一个分片表,调优表操作将转发到每个碎片,并且它也在主名称空间中执行。
在分片表上运行Tune Table时,应该遵循以下准则:
- 优化分片主表,而不是分片本地表。
- 区段大小和块计数值是每个分片的值,而不是所有分片的总和。
- 如果使用`$SYSTEM.SQL.Stats.Table.Export()`和`$SYSTEM.SQL.Stats.Table.Import()`,则导出/导入分片主表的调优统计,而不是分片本地表。
- 调优切分表将在切分主类和切分本地类/表定义中定义调优统计。
如果手动编辑类定义中的调优表元数据,建议的过程是修改碎片主类的定义,然后重新编译碎片主类。
在编译碎片主类时,碎片主调优统计信息将被复制到类的碎片本地版本。
如果`GatherTableStats()`或`GatherSchemaStats()`指定了一个`logFile`参数,shard master实例中的日志文件有一个针对指定表的条目,例如:
- Sharded table: `TABLE: Invoking TuneTable on shards for sharded table `
- Non-sharded table: `TABLE: Invoking TuneTable on shards for mapped non-sharded table `
在每个分片实例上,在`mgr/`目录中创建一个同名的日志文件,记录这个分片上这个表的调优表信息。
如果为日志文件指定了目录路径,那么分片将忽略该路径,并且该文件始终存储在`mgr/`中。
文章
Nicky Zhu · 一月 8, 2021
我打算基于实例中的数据实现业务智能。 怎样才是设置数据库和环境来使用 DeepSee 的最佳方法呢?

本教程通过 3 个 DeepSee 架构示例来解决此问题。 首先,我们从基本架构模型开始,并重点说明其局限性。 对于复杂程度中等的业务智能应用,建议使用下一个模型,对于大多数用例而言,该模型应该足矣。 在本教程的最后,我们将说明如何增强架构的灵活性以管理高级实现。
本教程中的每个示例都介绍了新的数据库和全局映射,并讨论了为何以及何时设置它们。 在构建架构时,则重点说明更灵活的示例提供的好处。
开始前
主服务器和分析服务器
为了使数据高度可用,InterSystems 通常建议使用镜像或映射,并将 DeepSee 实现基于镜像/映射服务器。 承载数据原始副本的机器称为“主服务器”,而承载数据副本和业务智能应用程序的计算机通常称为“分析服务器”(有时称为“报告服务器”)。
拥有主服务器和分析服务器至关重要,主要原因是避免任一台服务器出现性能问题。 请查阅有关推荐架构的文档。
数据和应用程序代码
通常,将源数据和代码存储在同一数据库中仅对小型应用程序有效。 对于更大型的应用程序,建议将源数据和代码存储在两个专用数据库中,这样您就可以与运行 DeepSee 的所有命名空间共享代码,同时保持数据分离。 源数据的数据库应从生产服务器镜像。 该数据库可以为只读,也可为读写。 建议为此数据库持续启用日志功能。
源类和自定义应用程序应存储在生产和分析服务器上的专用数据库中。 请注意,这两个用于源代码的数据库不需要同步,甚至不需要运行相同的 Caché 版本。 只要定期将代码备份到其他地方,通常就不需要日志。
在本教程中,我们采用以下配置。 分析服务器上的 APP 命名空间有 APP-DATA 和 APP-CODE 作为默认数据库。 APP-DATA 数据库可以访问主服务器上的源数据数据库中的数据(源表类及其事实数据)。 APP-CODE 数据库存储 Caché 代码(.cls 和 .INT 文件)以及其他自定义代码。 数据和代码的这种分离是一种典型的架构,这允许用户,例如,有效地部署 DeepSee 代码和自定义应用程序。
在不同的命名空间上运行 DeepSee
使用 DeepSee 的业务智能实现通常在不同的命名空间中运行。 在本文中,我们将说明如何设置单个的 APP 命名空间,但是相同的过程适用于运行业务智能应用程序的所有名称空间。
文档
建议熟悉文档页面执行初始设置。 该页面的内容包括:设置 Web 应用程序,如何将 DeepSee 全局变量放置在单独的数据库中,以及 Deepeep 全局变量的替代映射列表。
* * *
在本系列的第二部分中,我们将阐述基本架构模型的实现
文章
姚 鑫 · 七月 23, 2021
# <center> 第十三章 类关键字 - ClientDataType
指定将此数据类型投影到客户端技术时使用的客户端数据类型。仅适用于数据类型类。
# 用法
要指定将此数据类型投影到客户端技术时要使用的客户端数据类型,请使用以下语法:```javaClass MyApp.MyString [ ClientDataType = clienttype ] { //class members }```
其中clienttype是下列之一:
- `BIGINT`- `BINARY`- `BINARYSTREAM`- `BOOLEAN`- `CHARACTERSTREAM`- `CURRENCY`- `DATE`- `DECIMAL`- `DOUBLE`- `FDATE`- `FTIMESTAMP`- `HANDLE`- `INTEGER`- `LIST`- `LONGVARCHAR`- `NUMERIC`- `STATUS`- `TIME`- `TIMESTAMP`- `VARCHAR` (默认)
# 详解
此关键字指定将此类投影到客户端技术时使用的客户端数据类型。每个数据类型类都必须指定一个客户端数据类型。
# 对子类的影响
这个关键字是从主超类继承的。子类可以覆盖关键字的值。
# 默认
默认的客户端数据类型是`VARCHAR`。
# <center> 第十四章 类关键字 - ClientName
能够重写此类的客户端投影中使用的默认类名。
# 用法
要在将类投影到客户端时覆盖类的默认名称,请使用以下语法:
```javaClass MyApp.MyClass [ ClientName = clientclassname ] { //class members }```
其中clientclassname是用作客户端名称的不带引号的字符串,而不是类名。
# 详解
该关键字允许在类被投影到客户端时为其定义一个替代名称(例如当使用InterSystems IRIS Java绑定时)
# 对子类的影响
此关键字不是继承的。
# 默认
如果省略此关键字,实际的类名将在客户端上使用。
# <center> 第十五章 类关键字 - CompileAfter
指定此类应在其他(指定的)类之后编译。
# 用法
要指示类编译器应该在其他类之后编译此类,请使用以下语法:
```javaClass MyApp.MyClass [ CompileAfter = classlist ] { //class members }```
其中`classlist`是下列之一:
- 类名。例如:- ```java[ CompileAfter = MyApp.Class1 ]```- 用逗号分隔的类名列表,用括号括起来。例如:
```java[ CompileAfter = (MyApp.Class1,MyApp.Class2,MyApp.Class3) ]```
# 详解
此关键字指定类编译器应该在编译指定的类后编译此类。
通常,当类之间存在编译器无法检测到的依赖关系,以致必须一个接一个地编译时,会使用此关键字。
此关键字仅影响编译顺序,不影响运行时行为。
**注意:`CompileAfter`关键字不能确保在编译这个类之前指定的类是可运行的。**
此外,`CompileAfter`关键字只影响与`System`关键字具有公共值的类。
# 对子类的影响
这个关键字继承自所有超类。如果子类为关键字指定了一个值,该值指定了在子类可以被编译之前必须被编译的附加类。
# 默认
默认情况下,不指定该关键字。
# <center> 第十六章 类关键字 - DdlAllowed
指定`DDL`语句是否可用于更改或删除类定义。仅适用于持久类。
# 用法
要通过`DDL`修改类,请使用以下语法:
```Class MyApp.Person Extends %Persistent [ DdlAllowed ] { //class members }```否则,省略此关键字或使用以下语法:
```Class MyApp.Person Extends %Persistent [ Not DdlAllowed ] { //class members }```
# 详情
此关键字指定是否可以使用`DDL`语句(如删除表、更改表、删除索引等)来更改或删除类定义。
通常,不希望让SQL用户使用`DDL`语句修改类。
# 对子类的影响
此关键字不是继承的。
# 默认
如果省略这个关键字,`DDL`语句就不能用来影响类定义。
# 注意
如果通过执行`DDL CREATE TABLE`语句来创建一个类,那么对于该类,`DdlAllowed`关键字最初将被设置为`true`。
文章
Michael Lei · 八月 20, 2021
这是一个IRIS 2020.2上的代码示例,并非InterSystems 官方支持!
本demo基于原始类描述 is based on the raw class descriptions.使用的数据类是Address, Person, Employee, Company如果要做更有吸引力的 demo, 可以添加 JSONtoString by ID的方法
用ZPM安装后从终端启动:After installation with ZPM just run from Terminal
USER>do ##class(rcc.ONAPI.demo).Run()
Adjust Parameters
host[127.0.0.1]:
port[51773]:
namespace[USER]:
user[_SYSTEM]:
pwd[SYS]:
timeout[5]:
****** connected ********
下一步, 你会得到一系列可能的Demo动作。you get a list of possible demo actions.没有输入就意味着没有动作No input means no action.菜单会一直循环直到退出The menu loops until you exit.
Populate Person by:100
100
Populate Company by:10
10
Populate Employee by:50
50
Show Person by ID:3
{"Name":"Rogers,Norbert V.","SSN":"990-11-9806","DOB":"1962-04-23","Home":{"Street":"867 Oak Street","City":"Denver","State":"NH","Zip":"64647"},"Office":{"Street":"3309 Oak Court","City":"Denver","State":"NY","Zip":"76436"},"FavoriteColors":["Green","Green"],"Age":58}
Show Company by ID:3
{"Name":"CompuComp Corp.","Mission":"Specializing in the development and manufacturing of open-source object-oriented models for additive manufacturing.","TaxID":"Y8155","Revenue":819493934,"Employees":[{"Name":"Ahmed,Sophia P.","SSN":"936-73-5161","DOB":"1933-08-08","Home":{"Street":"8758 Elm Street","City":"Fargo","State":"OH","Zip":"40652"},"Office":{"Street":"1578 Maple Street","City":"Larchmont","State":"IA","Zip":"89021"},"Spouse":{"Name":"Olsen,William A.","SSN":"912-52-4809","DOB":"2010-01-26","Home":{"Street":"2933 Main Street","City":"Bensonhurst","State":"WA","Zip":"51960"},"Office":{"Street":"4994 Ash Street","City":"Gansevoort","State":"OR","Zip":"89750"},"Spouse":{"Name":"Adam,Brian D.","SSN":"799-82-3083","DOB":"2005-11-04","Home":{"Street":"1264 Oak Avenue","City":"Chicago","State":"WV","Zip":"34943"},"Office":{"Street":"7443 Second Avenue","City":"Zanesville","State":"CO","Zip":"30478"},"Spouse":{"Name":"Cooke,Diane C.","SSN":"754-49-5729","DOB":"1984-01-12","Home":{"Street":"9624 Maple Place","City":"Albany","State":"MS","Zip":"60948"},"Office":{"Street":"4711 Second Place","City":"Youngstown","State":"VA","Zip":"31250"},"Age":36},"FavoriteColors":["Orange"],"Age":14},"FavoriteColors":["Green","Green"],"Age":10,"Title":"Senior Systems Engineer","Salary":61511},"Age":87,"Title":"Product Manager","Salary":59127},{"Name":"Ng,Elmo S.","SSN":"201-15-3259","DOB":"1954-10-14","Home":{"Street":"2437 Main Avenue","City":"Xavier","State":"KY","Zip":"10156"},"Office":{"Street":"2770 Oak Drive","City":"Tampa","State":"OH","Zip":"18459"},"Spouse":{"Name":"Eastman,Linda M.","SSN":"110-41-1818","DOB":"1980-03-19","Home":{"Street":"5309 Ash Drive","City":"Xavier","State":"RI","Zip":"36964"},"Office":{"Street":"4288 Washington Place","City":"Xavier","State":"HI","Zip":"41889"},"Spouse":{"Name":"Huff,Dave C.","SSN":"559-32-3838","DOB":"1973-05-05","Home":{"Street":"6216 First Avenue","City":"Tampa","State":"WA","Zip":"68628"},"Office":{"Street":"2896 Clinton Drive","City":"Elmhurst","State":"UT","Zip":"97796"},"FavoriteColors":["Purple"],"Age":47},"FavoriteColors":["Purple","Blue"],"Age":40},"FavoriteColors":["Red","Purple"],"Age":65,"Title":"Laboratory Marketing Manager","Salary":35888}]}
Show Employee by ID:103
{"Name":"Faust,Buzz H.","SSN":"979-41-6347","DOB":"1938-02-07","Home":{"Street":"6231 Madison Avenue","City":"Gansevoort","State":"TX","Zip":"49085"},"Office":{"Street":"6402 Main Street","City":"Elmhurst","State":"RI","Zip":"82976"},"Spouse":{"Name":"Joyce,Chad C.","SSN":"199-86-8085","DOB":"1974-11-17","Home":{"Street":"6229 Main Street","City":"Reston","State":"FL","Zip":"16922"},"Office":{"Street":"8509 Elm Blvd","City":"Bensonhurst","State":"HI","Zip":"90665"},"Spouse":{"Name":"Cooke,Diane C.","SSN":"754-49-5729","DOB":"1984-01-12","Home":{"Street":"9624 Maple Place","City":"Albany","State":"MS","Zip":"60948"},"Office":{"Street":"4711 Second Place","City":"Youngstown","State":"VA","Zip":"31250"},"Age":36},"FavoriteColors":["Purple"],"Age":45},"Age":82,"Title":"Global Administrator","Salary":13813}
Show Global PersonD by ID:4
$Data()=1
Value=$lb("","Eastman,Mary C.","887-18-3730",44711,$lb("3889 Ash Blvd","Washington","TX",67862),$lb("5709 Oak Blvd","Chicago","IL",30845),"","")
Index list for Person & Employee (n,y):y
$Employee
$Person
NameIDX
SSNKey
ZipCode
Exit Demo (n,y,*):
Populate Person by:
Populate Company by:
Populate Employee by:
Show Person by ID:
Show Company by ID:
Show Employee by ID:104
{"Name":"Novello,Emily I.","SSN":"411-35-4234","DOB":"1943-01-07","Home":{"Street":"3353 Washington Court","City":"Hialeah","State":"MT","Zip":"22403"},"Office":{"Street":"9743 Clinton Blvd","City":"Xavier","State":"OH","Zip":"89038"},"Spouse":{"Name":"Goldman,Usha T.","SSN":"465-59-4053","DOB":"1987-07-16","Home":{"Street":"2578 Second Blvd","City":"Gansevoort","State":"FL","Zip":"77552"},"Office":{"Street":"6986 Main Street","City":"Elmhurst","State":"VT","Zip":"48713"},"Spouse":{"Name":"Rogers,Norbert V.","SSN":"990-11-9806","DOB":"1962-04-23","Home":{"Street":"867 Oak Street","City":"Denver","State":"NH","Zip":"64647"},"Office":{"Street":"3309 Oak Court","City":"Denver","State":"NY","Zip":"76436"},"FavoriteColors":["Green","Green"],"Age":58},"Age":33},"FavoriteColors":["Green","White"],"Age":77,"Title":"Senior Product Specialist","Salary":96469}
Show Global PersonD by ID:
Index list for Person & Employee (n,y):
Exit Demo (n,y,*):y
****** done ********
USER>
文章
Johnny Wang · 四月 25, 2022
大家应该都已经很熟悉 InterSystems Ensemble(一个集成和应用程序开发平台),每个人都知道 Ensemble Workflow 子系统是什么以及它对于自动化人类交互的作用。 对于那些不了解 Ensemble Workflow 的人,我将简要介绍它的功能(已经熟悉的朋友可以直接跳过这一部分并学习如何使用 Angular.js 中的 Workflow 接口)。
InterSystems Ensemble
InterSystems Ensemble 是一个集成和应用程序开发平台,旨在集成异构系统、自动化业务流程和创建新的复杂应用程序,这些应用程序通过新的业务逻辑或新的用户界面增强集成应用程序的功能:EAI、SOA、BPM、BAM 甚至 BI (感谢 InterSystems DeepSee:一种用于开发分析应用程序的内置技术)。
Ensemble 具有以下关键功能:
适配器:与应用程序、技术和数据源交互的组件。 Ensemble 提供技术和应用程序集成适配器(Web 和 REST 服务、文件、FTP、电子邮件、SQL、EDI、HL7、SAP、Siebel、1S Enterprise 等)。 您可以使用适配器 SDK 创建自己的适配器。
业务服务:将来自外部系统的数据转换为 Ensemble 消息并启动业务流程和/或业务运营的组件。
业务流程:用于编排服务和操作的可执行流程,以自动化系统和/或人员之间的交互(通过工作流子系统)。 流程要么用声明性业务流程语言描述,要么用 Caché 对象脚本实现。 通过服务和操作将与外界交互的逻辑与这种交互的具体实现分开。
业务运营:负责向外部系统发送/接收消息并将 Ensemble 消息转换为与此类系统兼容的格式的组件。
消息转换:使用声明性数据转换语言将消息从一种格式转换为另一种格式的集成组件。
业务规则:允许集成解决方案的管理员在特定决策点更改 Ensemble 业务流程的行为,而无需编写代码。
工作流管理:Ensemble Workflow 子系统提供任务分配的自动化。
业务指标:允许您收集和计算 KPI。 结合仪表板,它们用于实施业务活动监控 (BAM) 解决方案。
OK,让我们回到工作流管理,仔细看看 Ensemble Workflow 子系统的功能。
工作流管理和 Ensemble 工作流子系统
根据工作流管理联盟 (www.WfMC.org) 的定义,“工作流”是完全或部分自动化的业务流程,其中文档、信息或任务根据既定规则和程序从一个参与者传递给另一个参与者。”
工作流程的关键方面:
工作流的目的是涵盖工作的“片段”
工作流是一组程序性任务执行规则
工作流用户是在工作流管理系统中处理任务的人
工作流中的角色是一组从事特定类型任务的用户。
Ensemble 中的工作流管理子系统使您能够执行以下操作:
使用 Ensemble 业务流程自动化工作流程管理
灵活配置任务分配流程
通过 Ensemble 提供的特殊工作流门户使用工作流管理系统
组织工作流管理子系统与 Ensemble 的集成业务流程的交互
使用业务活动监控子系统,Ensemble 的管理和监控工具
轻松配置和扩展工作流子系统的功能
工作流管理自动化的最简单示例是 Ensemble HelpDesk 应用程序(下图为HelpDesk 业务流程算法的片段),它可以自动化支持人员的交互,并且是标准的 Ensemble 示例集(在 Ensdemo 空间中)的一部分。 Ensemble 接收问题报告并启动 HelpDesk 业务流程。
业务流程使用 EnsLib.Workflow.TaskRequest 类的消息向具有 Demo-Development 角色的用户发送任务,该类定义了所有可能的操作(“Fixed”或“Ignored”)以及“Comment”字段。 消息的正文还包含有关错误和报告错误的用户的信息。 在此之后,相应的任务会出现在每个具有演示开发角色的用户的工作流门户中。
最初(如果未在 TaskRequest 消息中定义),任务不与任何特定用户关联(仅与角色关联),因此用户必须通过单击相应按钮来接受它。 您也可以通过单击“推迟”按钮来拒绝任务。
完成后,您可以执行此任务允许的任何操作。 在我们的例子中,我们可以在相应字段中提供评论后单击“已修复”按钮。 HelpTask 业务流程将处理此事件并向具有 Demo-Testing 角色的用户发送一条新消息,从而表明需要测试更改。 如果单击“忽略”按钮,该任务将被标记为“不是问题”,并且其处理将停止。
从这个例子可以看出,Ensemble Workflow 是一个简单直观的系统,用于组织用户的工作流。 关于 Ensemble Workflow 子系统的更多详细信息可以在 Ensemble 手册的定义工作流部分找到。
Ensemble Workflow 子系统的功能可以轻松扩展并集成到基于 InterSystems Ensemble 的外部复合应用程序中。 作为一个例子,让我们看一下在使用 Angular.js + REST API(由 Eduard Lebedyuk 编写)开发的外部复合应用程序中实现 Ensemble Workflow 的用户界面。
Angular.js 中的Ensemble工作流接口
要使 Workflow 界面与 Angular.js 一起使用,您需要在服务器上安装以下 Ensemble 应用程序:
UI in Angular.js
REST API
安装过程在指定存储库的自述文件中进行了描述。
目前(原帖里说),该应用程序具有 Ensemble Workflow 的所有必要功能:显示任务列表、附加字段和操作、排序、任务中的全文搜索。 用户可以接受/拒绝任务。 有关任务的详细信息显示在模式窗口中。 (实现只是概念证明,它还有很大的改进空间。它还以一种不得在生产中使用的方式使用 BasicAuth。目前我们已经有一个更复杂的例子)。
应用程序如下所示:
UI 使用以下库和框架:Angular.js、Twitter Bootstrap 以及 FontAwesome 图标字体。
您可以查看我们的测试服务器上运行的 HelpDesk 应用程序的用户界面。 账号:dev,密码:123
对于那些对源代码感兴趣的朋友们
以下这个小应用程序的结构:
该应用程序有 4 个 Angular 服务(RESTSrvc、SessionSrvc、UtilSrvc 和 WorklistSrvc)、3 个控制器(MainCtrl、TaskCtrl、TasksGridCtrl)、一个主页(index.csp)和 2 个模板(task.csp 和 tasks.csp)。
RESTSrvc 服务只有一个方法 getPromise,它是 $http Angular.js 服务的包装器。 RESTSrvc 的唯一目的是向服务器发送 HTTP 请求并返回这些请求的 Promise 对象。 其他服务使用 RESTSrvc 来发出请求,它们的分离本质上是一种功能性的(它其实可以写得更好)。点击下栏查看代码:
RESTSrvc
'use strict';
function RESTSrvc($http, $q) { return { getPromise: function(config) { var deferred = $q.defer(); $http(config) .success(function(data, status, headers, config) { deferred.resolve(data); }) .error(function(data, status, headers, config) { deferred.reject(data, status, headers, config); }); return deferred.promise; } }};
RESTSrvc.$inject = ['$http', '$q']; servicesModule.factory('RESTSrvc', RESTSrvc);
SessionSrvc :包含一个负责关闭会话的方法。 此应用程序中的身份验证是使用基本访问身份验证 (http://en.wikipedia.org/wiki/Basic_access_authentication) 实现的,因此不需要单独的身份验证方法,因为每个请求的标头中都有一个授权令牌。点击下栏查看代码:
SessionSrvc
'use strict';
// Session servicefunction SessionSrvc(RESTSrvc) { return { // save worklist object logout: function (baseAuthToken) { return RESTSrvc.getPromise({ method: 'GET', url: RESTWebApp.appName + '/logout', headers: { 'Authorization': baseAuthToken } }); } }};
SessionSrvc.$inject = ['RESTSrvc'];servicesModule.factory('SessionSrvc', SessionSrvc);
UtilSrvc :包含辅助方法,例如按名称获取 cookie 值、按名称获取对象属性。点击下栏查看代码:
UtilSrvc
// Utils servicefunction UtilSrvc($cookies) { return { // get cookie by name readCookie: function (name) { return $cookies[name]; },
// Function to get value of property of the object by name // Example: // var obj = {car: {body: {company: {name: 'Mazda'}}}}; // getPropertyValue(obj, 'car.body.company.name') getPropertyValue: function (item, propertyStr) { var value = item;
try { var properties = propertyStr.split('.');
for (var i = 0; i < properties.length; i++) { value = value[properties[i]];
if (value !== Object(value)) break; } } catch (ex) { console.log('Something goes wrong :/'); }
return value == undefined ? '' : value; } }};
UtilSrvc.$inject = ['$cookies'];servicesModule.factory('UtilSrvc', UtilSrvc);
WorklistSrvc :负责与任务列表数据相关的请求。点击下栏查看代码:
WorklistSrvc
'use strict';
// Worklist servicefunction WorklistSrvc(RESTSrvc) { return { // save worklist object save: function (worklist, baseAuthToken) { return RESTSrvc.getPromise({ method: 'POST', url: RESTWebApp.appName + '/tasks/' + worklist._id, data: worklist, headers: { 'Authorization': baseAuthToken } }); },
// get worklist by id get: function (id, baseAuthToken) { return RESTSrvc.getPromise({ method: 'GET', url: RESTWebApp.appName + '/tasks/' + id, headers: { 'Authorization': baseAuthToken } }); },
// get all worklists for current user getAll: function (baseAuthToken) { return RESTSrvc.getPromise({ method: 'GET', url: RESTWebApp.appName + '/tasks', headers: { 'Authorization': baseAuthToken } }); } }};
WorklistSrvc.$inject = ['RESTSrvc'];servicesModule.factory('WorklistSrvc', WorklistSrvc);
MainCtrl :负责用户身份验证的应用程序的主控制器。点击下栏查看代码:
MainCtrl
'use strict';
// Main controller// Controls the authentication. Loads all the worklists for user.function MainCtrl($scope, $location, $cookies, WorklistSrvc, SessionSrvc, UtilSrvc) { $scope.page = {}; $scope.page.alerts = []; $scope.utils = UtilSrvc; $scope.page.loading = false; $scope.page.loginState = $cookies['Token'] ? 1 : 0; $scope.page.authToken = $cookies['Token'];
$scope.page.closeAlert = function (index) { if ($scope.page.alerts.length) { $('.alert:nth-child(' + (index + 1) + ')').animate({ opacity: 0, top: "-=150" }, 400, function () { $scope.page.alerts.splice(index, 1); $scope.$apply(); }); } };
$scope.page.addAlert = function (alert) { $scope.page.alerts.push(alert);
if ($scope.page.alerts.length > 5) { $scope.page.closeAlert(0); } };
/* Authentication section */ $scope.page.makeBaseAuth = function (user, password) { var token = user + ':' + password; var hash = Base64.encode(token); return "Basic " + hash; }
// login $scope.page.doLogin = function (login, password) { var authToken = $scope.page.makeBaseAuth(login, password); $scope.page.loading = true;
WorklistSrvc.getAll(authToken).then( function (data) { $scope.page.alerts = []; $scope.page.loginState = 1; $scope.page.authToken = authToken; // set cookie to restore loginState after page reload $cookies['User'] = login.toLowerCase(); $cookies['Token'] = $scope.page.authToken;
// refresh the data on page $scope.page.loadSuccess(data); }, function (data, status, headers, config) { if (data.Error) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); } else { $scope.page.addAlert({ type: 'danger', msg: "Login unsuccessful" }); } }) .then(function () { $scope.page.loading = false; }) };
// logout $scope.page.doExit = function () { SessionSrvc.logout($scope.page.authToken).then( function (data) { $scope.page.loginState = 0; $scope.page.grid.items = null; $scope.page.loading = false; // clear cookies delete $cookies['User']; delete $cookies['Token']; document.cookie = "CacheBrowserId" + "=; Path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; document.cookie = "CSPSESSIONID" + "=; Path=" + RESTWebApp.appName + "; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; document.cookie = "CSPWSERVERID" + "=; Path=" + RESTWebApp.appName + "; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }); };
}
MainCtrl.$inject = ['$scope', '$location', '$cookies', 'WorklistSrvc', 'SessionSrvc', 'UtilSrvc'];controllersModule.controller('MainCtrl', MainCtrl);
TasksGridCtrl :一个控制器,负责任务列表和与之关联的操作。 它初始化任务列表表,包含加载任务列表和具体任务的方法,以及处理用户动作的方法(按键、表格排序、行选择、过滤)。点击下栏查看代码:
TasksGridCtrl
'use strict';
// TasksGrid controller// dependency injectionfunction TasksGridCtrl($scope, $window, $modal, $cookies, WorklistSrvc) {
// Initialize grid. // grid data: // grid title, css grid class, column names $scope.page.grid = { caption: 'Inbox Tasks', cssClass: 'table table-condensed table-bordered table-hover', columns: [{ name: '', property: 'New', align: 'center' }, { name: 'Priority', property: 'Priority' }, { name: 'Subject', property: 'Subject' }, { name: 'Message', property: 'Message' }, { name: 'Role', property: 'RoleName' }, { name: 'Assigned To', property: 'AssignedTo' }, { name: 'Time Created', property: 'TimeCreated' }, { name: 'Age', property: 'Age' }] };
// data initialization for Worklist $scope.page.dataInit = function () { if ($scope.page.loginState) { $scope.page.loadTasks(); } };
$scope.page.loadSuccess = function (data) { $scope.page.grid.items = data.children; // if we get data for other user - logout if (!$scope.page.checkUserValidity()) { $scope.page.doExit(); }
var date = new Date();
var hours = (date.getHours() > 9) ? date.getHours() : '0' + date.getHours(); var minutes = (date.getMinutes() > 9) ? date.getMinutes() : '0' + date.getMinutes(); var secs = (date.getSeconds() > 9) ? date.getSeconds() : '0' + date.getSeconds();
$('#updateTime').animate({ opacity: 0 }, 100, function () { $('#updateTime').animate({ opacity: 1 }, 1000); });
$scope.page.grid.updateTime = ' [Last Update: ' + hours; $scope.page.grid.updateTime += ':' + minutes + ':' + secs + ']';
};
// all user's tasks loading $scope.page.loadTasks = function () { $scope.page.loading = true;
WorklistSrvc.getAll($scope.page.authToken).then( function (data) { $scope.page.loadSuccess(data); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }) .then(function () { $scope.page.loading = false; }) };
// load task (worklist) by id $scope.page.loadTask = function (id) { WorklistSrvc.get(id, $scope.page.authToken).then( function (data) { $scope.page.task = data; }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }); };
// 'Accept' button handler. // Send worklist object with '$Accept' action to server. $scope.page.accept = function (id) { // nothing to do, if no id if (!id) return;
// get full worklist, set action and submit worklist. WorklistSrvc.get(id).then( function (data) { data.Task["%Action"] = "$Accept"; $scope.page.submit(data); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }); };
// 'Yield' button handler. // Send worklist object with '$Relinquish' action to server. $scope.page.yield = function (id) { // nothing to do, if no id if (!id) return;
// get full worklist, set action and submit worklist. WorklistSrvc.get(id).then( function (data) { data.Task["%Action"] = "$Relinquish"; $scope.page.submit(data); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); }); };
// submit the worklist object $scope.page.submit = function (worklist) { // send object to server. If ok, refresh data on page. WorklistSrvc.save(worklist, $scope.page.authToken).then( function (data) { $scope.page.dataInit(); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); } ); };
/* table section */
// sorting table $scope.page.sort = function (property, isUp) { $scope.page.predicate = property; $scope.page.isUp = !isUp; // change sorting icon $scope.page.sortIcon = 'fa fa-sort-' + ($scope.page.isUp ? 'up' : 'down') + ' pull-right'; };
// selecting row in table $scope.page.select = function (item) { if ($scope.page.grid.selected) { $scope.page.grid.selected.rowCss = '';
if ($scope.page.grid.selected == item) { $scope.page.grid.selected = null; return; } }
$scope.page.grid.selected = item; // change css class to highlight the row $scope.page.grid.selected.rowCss = 'info'; };
// count currently displayed tasks $scope.page.totalCnt = function () { return $window.document.getElementById('tasksTable').getElementsByTagName('TR').length - 2; };
// if AssignedTo matches with current user - return 'true' $scope.page.isAssigned = function (selected) { if (selected) { if (selected.AssignedTo.toLowerCase() === $cookies['User'].toLowerCase()) return true; } return false; };
// watching for changes in 'Search' input // if there is change, reset the selection. $scope.$watch('query', function () { if ($scope.page.grid.selected) { $scope.page.select($scope.page.grid.selected); } });
/* modal window open */
$scope.page.modalOpen = function (size, id) { // if no id - nothing to do if (!id) return;
// obtainig the full object by id. If ok - open modal. WorklistSrvc.get(id).then( function (data) { // see http://angular-ui.github.io/bootstrap/ for more options var modalInstance = $modal.open({ templateUrl: 'partials/task.csp', controller: 'TaskCtrl', size: size, backdrop: true, resolve: { task: function () { return data; }, submit: function () { return $scope.page.submit } } });
// onResult modalInstance.result.then( function (reason) { if (reason === 'save') { $scope.page.addAlert({ type: 'success', msg: 'Task saved' }); } }, function () { }); }, function (data, status, headers, config) { $scope.page.addAlert({ type: 'danger', msg: data.Error }); });
};
/* User's validity checking. */
// If we get the data for other user, logout immediately $scope.page.checkUserValidity = function () { var user = $cookies['User'];
for (var i = 0; i < $scope.page.grid.items.length; i++) { if ($scope.page.grid.items[i].AssignedTo && (user.toLowerCase() !== $scope.page.grid.items[i].AssignedTo.toLowerCase())) { return false; } else if ($scope.page.grid.items[i].AssignedTo && (user.toLowerCase() == $scope.page.grid.items[i].AssignedTo.toLowerCase())) { return true; } }
return true; };
// Check user's validity every 10 minutes. setInterval(function () { $scope.page.dataInit() }, 600000);
/* Initialize */
// sort table (by Age, asc) // to change sorting column change 'columns[<index>]' $scope.page.sort($scope.page.grid.columns[7].property, true);
$scope.page.dataInit();}
TasksGridCtrl.$inject = ['$scope', '$window', '$modal', '$cookies', 'WorklistSrvc'];controllersModule.controller('TasksGridCtrl', TasksGridCtrl);
TaskCtrl :模式窗口的控制器,包含有关任务的详细信息。 形成字段和用户操作的列表,还处理模态窗口中的按钮单击。点击下栏查看代码:
TaskCtrl
'use strict';
function TaskCtrl($scope, $routeParams, $location, $modalInstance, WorklistSrvc, task, submit) { $scope.page = { task: {} }; $scope.page.task = task; $scope.page.actions = ""; $scope.page.formFields = ""; $scope.page.formValues = task.Task['%FormValues'];
if (task.Task['%TaskStatus'].Request['%Actions']) { $scope.page.actions = task.Task['%TaskStatus'].Request['%Actions'].split(','); }
if (task.Task['%TaskStatus'].Request['%FormFields']) { $scope.page.formFields = task.Task['%TaskStatus'].Request['%FormFields'].split(','); }
// dismiss modal $scope.page.cancel = function () { $modalInstance.dismiss('cancel'); };
// perform a specified action $scope.page.doAction = function (action) { $scope.page.task.Task["%Action"] = action; $scope.page.task.Task['%FormValues'] = $scope.page.formValues;
submit($scope.page.task); $modalInstance.close(action); }
}
// resolving minification problemsTaskCtrl.$inject = ['$scope', '$routeParams', '$location', '$modalInstance', 'WorklistSrvc', 'task', 'submit'];controllersModule.controller('TaskCtrl', TaskCtrl);
app.js :包含所有应用程序模块的文件。点击下栏查看代码:
app.js
'use strict';/*Adding routes(when).[route], {[template path for ng-view], [controller for this template]}
otherwiseSet default route.
$routeParams.id - :id parameter.*/
var servicesModule = angular.module('servicesModule', []);var controllersModule = angular.module('controllersModule', []);var app = angular.module('app', ['ngRoute', 'ngCookies', 'ui.bootstrap', 'servicesModule', 'controllersModule']);
app.config(['$routeProvider', function ($routeProvider) { $routeProvider.when('/tasks', { templateUrl: 'partials/tasks.csp' }); $routeProvider.when('/tasks/:id', { templateUrl: 'partials/task.csp', controller: 'TaskCtrl' });
$routeProvider.otherwise({ redirectTo: '/tasks' });}]);
index.csp :应用程序的主页。点击下栏查看代码:
index.csp
<!doctype html>
<html> <head> <title>Ensemble Workflow</title>
<meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<!-- CSS Initialization --> <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="css/font-awesome.min.css"> <link rel="stylesheet" type="text/css" href="css/bootstrap-theme.min.css"> <link rel="stylesheet" type="text/css" href="css/custom.css">
<script language="javascript"> // REST web-app name, global variable var RESTWebApp = {appName: '#($GET(^Settings("WF", "WebAppName")))#'}; </script> </head>
<body ng-app="app" ng-controller="MainCtrl">
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="#">Ensemble Workflow</a> </div>
<div class="navbar-left"> <button ng-cloak ng-disabled="page.loginState != 1 || page.loading" type="button" class="btn btn-default navbar-btn" ng-click="page.dataInit();">Refresh Worklist</button> </div>
<div class="navbar-left"> <form role="search" class="navbar-form"> <div class="form-group form-inline"> <label for="search" class="sr-only">Search</label> <input ng-cloak ng-disabled="page.loginState != 1" type="text" class="form-control" placeholder="Search" id="search" ng-model="query"> </div> </form> </div>
<div class="navbar-right"> <form role="form" class="navbar-form form-inline" ng-show="page.loginState != 1" ng-model="user" ng-submit="page.doLogin(user.Login, user.PasswordSetter); user='';" ng-cloak> <div class="form-group"> <input class="form-control uc-inline" ng-model="user.Login" placeholder="Username" ng-disabled="page.loading"> <input type="password" class="form-control uc-inline" ng-model="user.PasswordSetter" placeholder="Password" ng-disabled="page.loading"> <button type="submit" class="btn btn-default" ng-disabled="page.loading">Sign In</button> </div> </form> </div>
<button ng-show="page.loginState == 1" type="button" ng-click="page.doExit();" class="btn navbar-btn btn-default pull-right" ng-cloak>Logout, <span class="label label-info" ng-bind="utils.readCookie('User')"></span> </button>
</div> </nav>
<div class="container-fluid">
<div style="height: 20px;"> <div ng-show="page.loading" class="progress-bar progress-bar-striped progress-condensed active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%" ng-cloak> Loading </div> </div>
<!-- Alerts --> <div ng-controller="AlertController" ng-cloak> <alert title="Click to dismiss" ng-repeat="alert in page.alerts" type="{{alert.type}}" ng-click="page.closeAlert($index, alert)">{{alert.msg}}</alert> </div>
<div ng-show="page.loginState != 1" class="attention" ng-cloak> <p>Please, Log In first.</p> </div>
<!-- Loading template --> <div ng-view> </div> </div>
</div>
<!-- Hooking scripts --> <script language="javascript" src="libs/angular.min.js"></script> <script language="javascript" src="libs/angular-route.min.js"></script> <script language="javascript" src="libs/angular-cookies.min.js"></script> <script language="javascript" src="libs/ui-bootstrap-custom-tpls-0.12.0.min.js"></script> <script language="javascript" src="libs/base64.js"></script>
<script language="javascript" src="js/app.js"></script>
<script language="javascript" src="js/services/RESTSrvc.js"></script> <script language="javascript" src="js/services/WorklistSrvc.js"></script> <script language="javascript" src="js/services/SessionSrvc.js"></script> <script language="javascript" src="js/services/UtilSrvc.js"></script>
<script language="javascript" src="js/controllers/MainCtrl.js"></script> <script language="javascript" src="js/controllers/TaskCtrl.js"></script> <script language="javascript" src="js/controllers/TasksGridCtrl.js"></script>
<script language="javascript" src="libs/jquery-1.11.2.min.js"></script> <script language="javascript" src="libs/bootstrap.min.js"></script>
</body></html>
tasks.csp :任务列表模板。点击下栏查看代码:
tasks.csp
<div class="row-fluid"> <div class="span1"> </div>
<div ng-hide="page.loginState != 1 || (page.loading && !page.totalCnt())" ng-controller="TasksGridCtrl">
<div class="panel panel-default top-buffer"> <table class="table-tasks" ng-class="page.grid.cssClass" id="tasksTable"> <caption class="text-left"> <b ng-bind="page.grid.caption"></b><b id="updateTime" ng-bind="page.grid.updateTime"></b> </caption> <thead style="cursor: pointer; vertical-align: middle;"> <tr> <th class="text-center">#</th> <!-- In the cycle prints the name of the column, specify for each column click handler and the icon (sorting) --> <th ng-repeat="column in page.grid.columns" class="text-center" ng-click="page.sort(column.property, page.isUp)"> <span ng-bind="column.name" style="padding-right: 4px;"></span> <i style="margin-top: 3px;" ng-class="page.sortIcon" ng-show="column.property == page.predicate"></i> <i style="color: #ccc; margin-top: 3px;" class="fa fa-sort pull-right" ng-show="column.property != page.predicate"></i> </th> <th class="text-center">Action</th> </tr> </thead> <tfoot> <tr> <!-- Control buttons and messages --> <td colspan="{{page.grid.columns.length + 2}}"> <p ng-hide="page.grid.items.length">There is no task(s) for current user.</p> <span ng-show="page.grid.items.length"> Showing {{page.totalCnt()}} of {{page.grid.items.length}} task(s). </span> </td> </tr> </tfoot> <tbody style="cursor: default;"> <!-- In the cycle prints the table rows (sort by specified column) --> <tr ng-repeat="item in page.grid.items | orderBy:page.predicate:page.isUp | filter:query" ng-class="item.rowCss" > <td ng-bind="$index + 1" class="text-right"></td> <!-- In the cycle prints the table cells to each row --> <td ng-repeat="column in page.grid.columns" style="text-align: {{column.align}};" ng-click="page.select(item)"> <span class="label label-info" ng-show="$first && item.New">New</span> <span ng-hide="$first" ng-bind="utils.getPropertyValue(item, column.property)"></span> </td> <td class="text-center"> <div title="Accept task" class="button button-success fa fa-plus-circle" ng-click="page.accept(item.ID)" ng-show="!page.isAssigned(item)"></div> <div title="Details" class="button button-info fa fa-search" ng-click="page.modalOpen('lg', item.ID)" ng-show="page.isAssigned(item)"></div> <div title="Yield task" class="button button-danger fa fa-minus-circle" ng-click="page.yield(item.ID)" ng-show="page.isAssigned(item)"></div> </td> </tr> </tbody> </table> </div> </div> <div class="span1"> </div></div><br>
task.csp — 模态窗口模板。点击下栏查看代码:
task.csp
<div class="modal-header"> <h3 class="modal-title">Task description</h3> </div>
<div class="modal-body"> <div class="container-fluid">
<div class="row top-buffer"> <div class="col-xs-12 col-md-6"> <div class="form-group"> <label for="subject">Subject</label> <input id="subject" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Request['%Subject'];" readonly> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for="timeCreated">Time created</label> <input id="timeCreated" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].TimeCreated;" readonly> </div> </div> </div>
<div class="row"> <div class="col-md-12"> <div class="form-group"> <label for="message">Message</label> <textarea id="message" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Request['%Message'];" rows="3" readonly></textarea> </div> </div> </div>
<div class="row"> <div class="col-md-6"> <div class="form-group"> <label for="role">Role</label> <input id="role" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Role.Name;" readonly> </div> </div>
<div class="col-md-3"> <div class="form-group"> <label for="assignedTo">Assigned to</label> <input id="assignedTo" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].AssignedTo;" readonly> </div> </div>
<div class="col-md-3"> <div class="form-group"> <label for="priority">Priority</label> <input id="priority" type="text" class="form-control task-info-input" ng-model="page.task.Task['%Priority'];" readonly> </div> </div> </div>
<div class="row" ng-show="page.formFields"> <div class="delimeter col-md-6 el-centered"> </div> </div>
<div class="row" ng-repeat="formField in page.formFields"> <div class="col-md-12"> <div class="form-group"> <label for="form{{$index}}" ng-bind="formField"></label> <input id="form{{$index}}" type="text" class="form-control task-info-input" ng-model="page.formValues[formField]"> </div> </div> </div>
</div>
</div>
<div class="modal-footer"> <button ng-repeat="action in page.actions" class="btn btn-primary top-buffer" ng-click="page.doAction(action)" ng-bind="action"></button>
<button class="btn btn-success top-buffer" ng-click="page.doAction('$Save')">Save</button> <button class="btn btn-warning top-buffer" ng-click="page.cancel()">Cancel</button> </div>
此外,您可以自由地将我们的 REST API 用于您的 UI,尤其是考虑到它非常简单。点击下栏查看代码:
The URL map of our REST API
<Routes> <Route Url="/logout" Method="GET" Call="Logout"/> <Route Url="/tasks" Method="GET" Call="GetTasks"/> <Route Url="/tasks/:id" Method="GET" Call="GetTask"/> <Route Url="/tasks/:id" Method="POST" Call="PostTask"/> <Route Url="/test" Method="GET" Call="Test"/></Routes>
本文翻译自 Habrahabr InterSystems 博客(俄语)
<Routes> <Route Url="/logout" Method="GET" Call="Logout"/> <Route Url="/tasks" Method="GET" Call="GetTasks"/> <Route Url="/tasks/:id" Method="GET" Call="GetTask"/> <Route Url="/tasks/:id" Method="POST" Call="PostTask"/> <Route Url="/test" Method="GET" Call="Test"/></Routes>
<div class="modal-header"> <h3 class="modal-title">Task description</h3> </div>
<div class="modal-body"> <div class="container-fluid">
<div class="row top-buffer"> <div class="col-xs-12 col-md-6"> <div class="form-group"> <label for="subject">Subject</label> <input id="subject" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Request['%Subject'];" readonly> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for="timeCreated">Time created</label> <input id="timeCreated" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].TimeCreated;" readonly> </div> </div> </div>
<div class="row"> <div class="col-md-12"> <div class="form-group"> <label for="message">Message</label> <textarea id="message" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Request['%Message'];" rows="3" readonly></textarea> </div> </div> </div>
<div class="row"> <div class="col-md-6"> <div class="form-group"> <label for="role">Role</label> <input id="role" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].Role.Name;" readonly> </div> </div>
<div class="col-md-3"> <div class="form-group"> <label for="assignedTo">Assigned to</label> <input id="assignedTo" type="text" class="form-control task-info-input" ng-model="page.task.Task['%TaskStatus'].AssignedTo;" readonly> </div> </div>
<div class="col-md-3"> <div class="form-group"> <label for="priority">Priority</label> <input id="priority" type="text" class="form-control task-info-input" ng-model="page.task.Task['%Priority'];" readonly> </div> </div> </div>
<div class="row" ng-show="page.formFields"> <div class="delimeter col-md-6 el-centered"> </div> </div>
<div class="row" ng-repeat="formField in page.formFields"> <div class="col-md-12"> <div class="form-group"> <label for="form{{$index}}" ng-bind="formField"></label> <input id="form{{$index}}" type="text" class="form-control task-info-input" ng-model="page.formValues[formField]"> </div> </div> </div>
</div>
</div>
<div class="modal-footer"> <button ng-repeat="action in page.actions" class="btn btn-primary top-buffer" ng-click="page.doAction(action)" ng-bind="action"></button>
<button class="btn btn-success top-buffer" ng-click="page.doAction('$Save')">Save</button> <button class="btn btn-warning top-buffer" ng-click="page.cancel()">Cancel</button> </div>
'use strict';/*Adding routes(when).[route], {[template path for ng-view], [controller for this template]}
otherwiseSet default route.
$routeParams.id - :id parameter.*/
var servicesModule = angular.module('servicesModule', []);var controllersModule = angular.module('controllersModule', []);var app = angular.module('app', ['ngRoute', 'ngCookies', 'ui.bootstrap', 'servicesModule', 'controllersModule']);
app.config(['$routeProvider', function ($routeProvider) { $routeProvider.when('/tasks', { templateUrl: 'partials/tasks.csp' }); $routeProvider.when('/tasks/:id', { templateUrl: 'partials/task.csp', controller: 'TaskCtrl' });
$routeProvider.otherwise({ redirectTo: '/tasks' });}]);
欢迎下载Ensembleworkflow 小程序
文章
Nicky Zhu · 二月 3, 2021
下一篇:
[案例: 建立只能使用SQL的用户](https://cn.community.intersystems.com/post/%E6%A1%88%E4%BE%8B-%E5%BB%BA%E7%AB%8B%E5%8F%AA%E8%83%BD%E4%BD%BF%E7%94%A8sql%E7%9A%84%E7%94%A8%E6%88%B7)
IRIS通过认证(Authentication)与授权(Authorization)两项机制控制外部用户对系统及应用、数据资源的可访问性。因此。如需要进行权限控制,则需要通过配置认证和授权进行。
## IRIS中的认证 {#2}
认证可以验证任何试图连接到InterSystems IRIS®的用户的身份。一旦通过认证,用户就与IRIS建立了通信,从而可以使用其数据和工具。有许多不同的方法可以验证用户的身份;每种方法都称为验证机制。IRIS 通常被配置为只使用其中一种方式。
支持的认证方式
* 实例认证:通过用户名/密码对登录平台,即密码认证
* LDAP:通过第三方LDAP服务器(如Windows Active Directory )完成认证
* 操作系统认证:建立操作系统用户-平台用户映射,使用操作系统用户登录平台
* Kerberos:使用Kerberos协议进行认证
* 代理认证:使用自定义的代码实现认证过程
### 系统服务与认证 {#2.1}
在安装时,IRIS会启动一系列系统级的服务用与控制与外部用户或系统的交互,这些服务都绑定了默认的认证机制

图中红框标出的即为系统安装后会自动启用并需经认证才可使用的系统服务,认证手段可配置。
例如,如果变更%Service_Console的身份验证方法,取消密码方法,用户就不能通过输入用户名密码登入Terminal。
通过Portal的菜单 系统管理 > 安全 > 服务 可访问该设置。
### 账户控制参数 {#2.2}
通过系统管理 > 安全 > 系统安全 > 系统范围的安全参数中的选项可对于用户名/密码认证手段的行为进行更多的约束。

* 非活动限制 - 指定用户账户不活跃的最大天数,它被定义为成功登录之间的时间。当达到此限制时,该帐户将被禁用。值为0(0)表示对登录之间的天数没有限制。[对于最低安全级别的安装,默认为0,对于正常和锁定的安装,默认为90]。
* 无效登录限制 (0-64) - 指定连续不成功的登录尝试的最大次数。在达到此限制后,要么禁用账户,要么对每次尝试进行递增的时间延迟;行动取决于如果达到登录限制字段则禁用账户的值。值为0(零)表示对无效登录的次数没有限制。[默认为5]
* 如果达到登录限制,则禁用账户 - 如果选中,则指定达到无效登录次数(在前一字段中指定)将导致用户账户被禁用。
* 密码有效期天数(0-99999) - 指定密码过期的频率以及用户更改密码的频率(天数)。当初始设置时,指定密码过期的天数。0(0)表示密码永远不会过期。不会影响已设置了下次登录时更改密码字段的用户。[默认为0]
需要特别注意的是,密码有效性、过期和禁用账户等设置会影响IRIS实例的所有账户,包括IRIS超级管理员账户。如触发了控制策略,则在更新这些帐户的信息之前,可能无法进行各种操作,这可能导致意外的结果。如超级管理员账户被锁定,则需要通过紧急模式启动实例再进行修改。
对于系统可用的认证手段的配置和其他可用的安全配置,请参见[Security Administration Guide](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS "安全管理向导")
## IRIS中的授权 {#3}
### 授权模型 {#3.1}
InterSystems公司的授权模式采用基于角色的访问控制。
* Users – 用户
* Roles – 角色
* Privileges – 权限
* Resources – 资源 | Permissions – 许可
在这种模式下,用户拥有与分配给各自用户身份的角色相关的权限。

* 一个角色是一个命名的特权集合
* 一个用户可以拥有一个以上的角色
* 权限分配给角色,角色分配给用户
其中,Roles就是权限的集合,而权限提供对资源的特定类型的访问的许可。
* 可控资源: 数据库,服务,应用(包括Web应用 )和其他
* 可选用的许可: Read, Write or Use,其中执行代码需要数据库的读权限
### 资源的定义 {#3.2}
资源是一项相对抽象的概念,用来指代IRIS中的数据库,服务,应用等可被访问的对象。例如,对于数据库,在建立时默认采用%DB_%DEFAULT指代,也可自定义资源(数据库资源必须以%DB_开头):

对于Web应用,默认不需要通过资源控制,即所有可登录用户都可访问(但该用户进程不一定能访问到数据,还需参照是否具有对数据库的访问权限)。如通过分配资源进行控制,则登录用户还需具有资源才能访问这个Web应用:

因此,一项权限实际上是指对某个资源的一些特定操作的集合。
例如,对于数据库UserDB具有读写操作许可的权限A,对于Web应用/csp/sys具有使用操作许可的权限B。如果我们将这两项权限都赋给角色RoleA,那么这个角色就同时拥有A权限和B权限,从而能够访问数据库UserDB和访问Web应用/csp/sys。
### SQL授权 {#3.3}
除了对数据库进行授权外,IRIS作为一个数据平台,需要对外提供数据访问。因此,IRIS也提供了SQL授权对用户可执行的SQL进行细粒度的权限控制。
SQL的授权可以分配给角色或用户。但通常在企业环境中,用户数量会很多,仍然需要对SQL用户进行分组,根据分组规划角色,通过角色进行授权的控制,才能有效降低维护授权所需的工作量。
SQL的授权针对SQL类型,可分为库、表级授权。
对于Create table、drop view、truncate table这一类的DDL,使用库级授权,即用户可在特定的库中执行建表、删除视图等经过授权的操作。如下:

对于Select、update等DML,则使用表级授权,使用户能够通过DML访问特定的表中的数据。如下:

除通过Portal操作之外,对于SQL的授权,还可使用IRIS SQL中的额GRANT语句,例如:
`GRANT * ON Schema Test TO TestRole`
这个SQL即可以将当前操作数据库下Schema Test中的所有表的所有权限都赋给TestRole这个角色。
关于GRANT语句的用法,可参见[GRANT指令](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_grant)
以上即为IRIS中进行权限控制所需掌握的概念和内容,在后续文章中,我们会结合实例向大家介绍其使用。
下一篇:
[案例: 建立只能使用SQL的用户](https://cn.community.intersystems.com/post/%E6%A1%88%E4%BE%8B-%E5%BB%BA%E7%AB%8B%E5%8F%AA%E8%83%BD%E4%BD%BF%E7%94%A8sql%E7%9A%84%E7%94%A8%E6%88%B7)
推荐阅读
[Security Administration Guide](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS "安全管理向导") - https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS
文章
姚 鑫 · 六月 13, 2021
# 第六章 控制名称空间的使用
# 控制名称空间的使用
如将对象投射到`XML`中所述,可以将类分配给名称空间,以便相应的XML元素属于该名称空间,还可以控制类的属性是否也属于该名称空间。
将类中的对象导出为XML时,`%XML.Write`提供其他选项,例如指定元素是否为其父级的本地元素。本节包括以下主题:
- 默认情况下,`%XML.Writer`如何处理命名空间
- 如何指定本地元素是否合格
- 如何指定元素是否为其父元素的本地元素
- 如何指定属性是否合格
- 命名空间分配方式的摘要
注意:在InterSystems IRIS XML支持中,可以按类指定名称空间。通常,每个类都有自己的命名空间声明;但是,通常只需要一个或少量的命名空间。还可以在逐个类的基础上指定相关信息(而不是以某种全局方式)。这包括控制元素是否为其父元素的本地元素以及子元素是否合格的设置。为简单起见,建议使用一致的方法。
## 名称空间的默认处理
若要将启用XML的类分配给命名空间,请设置该类的`Namespace`参数,如将对象投影到XML中所述。在`%XML.Writer`会自动插入命名空间声明,生成命名空间前缀,并在适当的地方应用前缀。例如,以下类定义:
```java
Class GXML.Objects.WithNamespaces.Person Extends (%Persistent, %Populate, %XML.Adaptor)
{
Parameter NAMESPACE = "http://www.person.com";
Property Name As %Name [ Required ];
Property DOB As %Date(FORMAT = 5, MAXVAL = "+$h") [ Required ];
Property GroupID As %Integer(MAXVAL=10,MINVAL=1,XMLPROJECTION="ATTRIBUTE");
}
```
```java
Uberoth,Amanda Q.
1952-01-13
```
请注意以下事项:
- 名称空间声明被添加到每``元素。
- 默认情况下,``元素的局部元素(``和``)是限定的。
该名称空间被添加为默认名称空间,因此应用于这些元素。
- ``元素的属性(`GroupID`)默认是不限定的。
这个属性没有前缀,因此被认为是未限定的。
- 这里显示的前缀是自动生成的。
(请记住,当对象分配给名称空间时,只指定名称空间,而不是前缀。)
- 此输出不会在写入器中设置任何与名称空间相关的属性,也不会在写入器中使用任何与名称空间相关的方法。
### 命名空间分配的上下文效应
为支持xml的对象分配的名称空间取决于该对象是在顶层导出还是作为另一个对象的属性导出。
一个名为`Address`的类。
假设使用`NAMESPACE`参数将`Address`类分配给名称空间`“http://www.address.org”`。
如果你直接导出`Address`类的一个对象,你可能会收到如下输出:
```java
8280 Main Avenue
Washington
VT
15355
```
注意, `` 元素及其所有元素都在同一个名称空间(`“http://www.address.org”`)中。
相反,假设`Person`类的属性是`Address`对象。
使用`NAMESPACE`参数将`Person`类分配给名称空间`“http://www.person.org”`。
如果导出`Person`类的一个对象,将收到如下输出:
```java
Zevon,Samantha H.
1964-05-24
8280 Main Avenue
Washington
VT
15355
```
注意,``元素位于其父对象(`“http://www.person.org”`)的名称空间中。
但是,``的元素位于名称空间`“http://www.address.org”`中。
## 控制局部元素是否限定
在顶层导出对象时,通常将其视为全局元素。然后根据启用XML的对象的`ELEMENTQUALIFIED`参数的设置处理其本地元素。如果未设置此类参数,则改用编写器属性`ElementQualified`的值;默认情况下,文本格式为1,编码格式为0。
下面的示例显示一个默认设置为`ElementQualified`的对象,即1:
```java
Pybus,Gertrude X.
1986-10-19
```
该名称空间被添加到``元素中作为默认名称空间,因此应用于元素``和``子元素。
我们修改了写入器定义并将`ElementQualified`属性设置为0。
在本例中,相同的对象如下所示:
```java
Pybus,Gertrude X.
1986-10-19
```
在本例中,名称空间被添加到带有前缀的``元素中,该前缀用于``元素,但不用于其子元素。
## 控制一个元素是否局部于它的父元素
默认情况下,当使用`object()`方法生成一个元素并且该元素具有命名空间时,该元素不是其父元素的本地元素。相反,可以强制元素属于其父元素的命名空间。为此,可以使用`object()`方法的可选本地参数;这是第四个参数。
### 本地参数为0(默认值)
在这里的示例中虑将`NAMESPACE`类参数指定为`“http://www.person.com”`的`Person`类。
如果打开根元素,然后使用`Object()`生成`Person`,则``元素位于`“http://www.person.com”`名称空间中。
以下例子:
```java
Adam,George L.
1947-06-29
```
如果我们将``元素更深地嵌套到其他元素中,也会出现类似的结果。
```java
Adam,George L.
1947-06-29
```
### 局部参数设置为1
为了强制``元素成为其父元素的本地元素,我们将`local`参数设置为1。
如果我们这样做并再次生成前面的输出,我们将收到以下少嵌套版本的输出:
```java
Adam,George L.
1947-06-29
```
注意,现在``元素在`“http://www.rootns.org”`名称空间中,这是它的父元素的名称空间。
类似地,嵌套程度更高的版本应该是这样的:
```java
Adam,George L.
1947-06-29
```
## 控制属性是否限定
导出对象时,默认情况下其属性不合格。要使它们合格,请将编写器属性`AttributeQualified`设置为1。下面的示例显示`AttributeQualified`等于0(或尚未设置)的编写器生成的输出:
```java
Leiberman,Amanda E.
1988-10-28
```
相反,下面的示例显示使用`AttributeQualified`等于1的编写器生成的同一对象:
```java
Leiberman,Amanda E.
1988-10-28
```
在这两种情况下,元素都是不合格的。
## 命名空间分配摘要
本节介绍如何为`XML`输出中的任何给定元素确定命名空间。
### 顶级元素
对于与在顶级导出的InterSystems IRIS类相对应的元素,适用以下规则:
1. 如果为类指定了`Namespace`参数,则元素位于该命名空间中。
2. 如果未指定该参数,则元素位于在生成元素的输出方法(`RootObject()`、`RootElement()`、`Object()`或`Element()`)中指定的命名空间中。
3. 如果未在输出方法中指定命名空间,则元素位于编写器的`DefaultNamespace`属性指定的命名空间中。
4. 如果`DefaultNamespace`属性为空,则元素不在任何命名空间中。
### 低层元素
要导出的类的子元素受该类的`ELEMENTQUALIFIED`参数影响。如果未设置`ELEMENTQUALIFIED`,则改用编写器属性`ElementQualified`的值;默认情况下,文本格式为1,编码格式为0。
如果元素符合给定类的条件,则该类的子元素将按如下方式分配给命名空间:
1. 如果为父对象指定了`Namespace`参数,则子元素将显式分配给该命名空间。
2. 如果未指定该参数,子元素将显式分配给在生成元素的输出方法(`RootObject()`、`RootElement()`、`Object()`或`Element()`)中指定的命名空间。
3. 如果未在输出方法中指定命名空间,则子元素将显式分配给由编写器的`DefaultNamespace`属性指定的命名空间。
4. 如果`DefaultNamespace`属性为空,则子元素不会显式分配给任何命名空间。
文章
Hao Ma · 十一月 20, 2022
## 发布您自己的软件
首先:要发布您的软件,您要支持这个”[命名规范](https://community.intersystems.com/post/objectscript-package-manager-naming-convention)。其中和zmp最相关的是包名和l类名的设计,你要定义成这样:
**company.project.subpackage.TheClass.cls**
如果您的Package Name定义是: Company.Project, 有大写字母,对不起,是无法用zpm打包的。
[这个链接](https://community.intersystems.com/post/zpm-simple-implementation-cookbook)给了最简单的例子,但还不详细,我来总结一下:
发布您的软件前,有几件事情要了解:
1. zpm的注册中心并不存代码,存的只是一个到您代码的链接。因此,您得找地方放您的代码。当前最常用的是github。
2. 文件目录的结构
举例:有一个class定义是 `com.tony.Test1.cls`, 你的目录应该这么组织,假设您要放在 `/myDemo`, 那么class应该在`/myDemo/src/com/tony/Test1.cls`。这是使用VSCode组织代码的默认方式,**只有保证这样的目录结构,您才可能用zpm加载代码到iris**.
让我来做个简单的例子。
首先,有这样的class:
```java
Class com.tony.Test1
{ Property p1;
}
```
我的文件目录设置
```sh
$ ls -l /external/myDemo/src
total 4
-rw-r--r-- 1 irisowner irisowner 40 Nov 12 10:00 Test1.cls
$
```
这时候我来使用zpm打包测试
### module.xml的生成和加载
第一步,生成module.xml
我们看看最简单的用zpm generate命令生成module的例子:
```sh
zpm:USER>generate /external/myDemo/project1
Enter module name: project1
Enter module version: 1.0.0 =>
Enter module description:
Enter module keywords:
Enter module source folder: src =>
Existing Web Applications:
/csp/user
/terminal
/terminalsocket
Enter a comma separated list of web applications or * for all:
Dependencies:
Enter module:version or empty string to continue:
zpm:USER>
```
`zpm generate`会把/external/myDemo/project1目录下的文件打包,在这个目录下创建一个module.xml文件, 是这样的:
```sh
project1
1.0.0
module
src
```
先等等解释这个xml, 让我们先执行第2步。
第2步: 把文件load到iris
```sh
zpm:USER>load /external/myDemo/
[USER|firstdemo] Reload START (/external/myDemo/)
[USER|firstdemo] Reload SUCCESS
[firstdemo] Module object refreshed.
[USER|firstdemo] Validate START
[USER|firstdemo] Validate SUCCESS
[USER|firstdemo] Compile START
[USER|firstdemo] Compile SUCCESS
[USER|firstdemo] Activate START
[USER|firstdemo] Configure START
[USER|firstdemo] Configure SUCCESS
[USER|firstdemo] Activate SUCCESS
zpm:USER>
```
您去iris里看看, 确认class已经被loaded
第3步: 删除(optional, 但你可能会用到这个命令)
```sh
zpm:USER>uninstall firstdemo
[USER|firstdemo] Clean START
[USER|firstdemo] Unconfigure START
[USER|firstdemo] Unconfigure SUCCESS
Deleting class com.tony.Test1
[USER|firstdemo] Clean SUCCESS
zpm:USER>
```
这时候您应该可以发现com.tong.Test1类已经从Iris删除了。
对上面的例子总结一下:
1. 打包是对一个文件夹打包
2. 使用zpm把软件包加载进iris是先找包里面的module.xml文件。通过module.xml里定义的信息来知道包的名字,版本,打包的内容等等。 这个module.xml是在打包的时候用`zpm generate`创建的, 但您也可以自己手工创建,比如copy其他包的module.xml改改, 有时候会更快捷,尤其是您对zpm命令不是很熟悉的时候, 对很多打包的需求,比如后面会提到的定义依赖等等,直接改module.xml比`zpm generate`容易多了。
3. 让我来说说可能的问题:
module.xml中``定义了包名*com.PKG*, 加载数据包到iris很成功。但是,如果您还记得,刚刚例子里打包的是class是`com.tony.Test1`,那么如果你再定义一个新的类,叫`com.tiedan.Test1`, 用`zpm generate`一个新module, 名字叫project2(注意第一个打包的module名字是project1), project2的"Resource Name"也还是"com.PKG, 它能正确加载吗?
不会, 你会被告知:"ERROR! Resource 'com.PKG' is already defined as part of module 'project1'; cannot also be listed in module 'project2'"
解决方法:手工将module.xml里面的Resource Name改成“com.tony.PKG"和"com.tiedan.PKG", 这样两个包都能成功加载了。
还有个小问题,Mac用户可能会看到这样的提示:
>zpm:USER>load /external/myDemo/project1
>
>[USER|project1] Reload START (/external/myDemo/project1/)
>[project1] Reload FAILURE
>ERROR! Unable to import file '/external/myDemo/project1/src/com/.DS_Store' as this is not a supported type.
>zpm:USER>
这是说打包的文件夹下面有.DS_Store文件,而zpm不认识。zpm会把里面认识的文件, 比如.cls文件成功加载, 然后告诉你"Roload FAILURE"
好吧, 到这里我们知道怎么打包和把包加载到iris里, 接着看看什么文件可以被打包。
### Package可以包含的文件类型
这时候要好好了解module.xml的内容的细节了, 请阅读[技术文档的module.xml部分](https://github.com/intersystems/ipm/wiki/03.-Module.xml#elements)。
其中的resource部分,阐明了您可以打包的内容:
第一部分:可以被加载到iris的文件类型。
> Use the following suffixes for different types of resources:
>
> .PKG - Package
> .CLS - Class
> .INC - Include
> .MAC - Routine
> .LOC - LocalizedErrorMessages
> .GBL - Global
> .DFI - DeepSee Item
第二部分:jar包
```xml
Copies content of lib folder to Target
Copies just desired file to Target
```
第三部分:UnitTest
module.xml的例子里给出的UnitTest部分是这个样子
```xml
```
说实话,我还没研究怎么使用`zpm generate`可以做到这一点,能想到的就是手工去修改module.xml文件。
第四部分:Web Application
执行`zpm generate`的时候, 会列出当前命名空间可以使用的Web Application列表。让我重新执行一下打包的第一步,看看结果是什么样子
```sh
zpm:USER>generate /external/myDemo/project1
Enter module name: project11
Enter module version: 1.0.0 =>
Enter module description:
Enter module keywords:
Enter module source folder: src =>
Existing Web Applications:
/csp/user
/terminal
/terminalsocket
Enter a comma separated list of web applications or * for all: /csp/user
Enter path to csp files for /csp/user:
Dependencies:
Enter module:version or empty string to continue:
zpm:USER>
```
得出的module.xml里多了如下内容:
```xml
src
```
其中,打包时提问`Enter path to csp files for /csp/user:。 这里,您需要填入的是当前要load的csp文件。比如:您有一个tony.csp要加载,那么您可以在要打包的目录下创建一个子目录“cspfiles", 把tony.csp放在cspfiles目录里, 回答提问的使用这样
`Enter path to csp files for /csp/user:/cspfiles`
> 这里用的是相对路径,但格式是绝对路径的格式,我把它看成一个bug。
module.xml中还有其他很多配置的内容,我在后面会介绍包的依赖的部分。
### 软件包的Package
用package命令,在iris里将软件打包。打包的结果是得倒一个project1-1.0.0.tgz的文件。 `package -v`显示verbose信息,您可以清楚的看到project1-1.0.0.tgz和module.xml的存放位置。
```sh
zpm:USER>project1 package -v
[USER|project1] Reload START (/external/myDemo/project1/)
Skipping preload - directory does not exist.
Load of directory started on 11/20/2022 13:15:17 '*'
Loading file /external/myDemo/project1/src/com/tony/Test1.cls as udl
Load finished successfully.
[USER|project1] Reload SUCCESS
[project1] Module object refreshed.
[USER|project1] Validate START
[USER|project1] Validate SUCCESS
[USER|project1] Compile START
Compilation started on 11/20/2022 13:15:17 with qualifiers 'd-lck'
Compiling class com.tony.Test1
Compiling routine com.tony.Test1.1
Compilation finished successfully in 0.008s.
[USER|project1] Compile SUCCESS
[USER|project1] Activate START
[USER|project1] Configure START
[USER|project1] Configure SUCCESS
Studio project created/updated: project1.PRJ
[USER|project1] Activate SUCCESS
[USER|project1] Package START
Exporting 'com.tony.Test1.cls' to '/usr/irissys/mgr/Temp/dirKBwoaM/project1-1.0.0/src/com/tony/Test1.cls'
Exported to /usr/irissys/mgr/Temp/dirKBwoaM/project1-1.0.0/module.xml
Module exported to:
/usr/irissys/mgr/Temp/dirKBwoaM/project1-1.0.0/
Module package generated:
/usr/irissys/mgr/Temp/dirKBwoaM/project1-1.0.0.tgz
[USER|project1] Package SUCCESS
zpm:USER>
```
### 软件包的Publish
我们并没有权限把软件包直接发布到官方的registry去。您需要去InterSystems的[OpenExchange页面](https://openexchange.intersystems.com),提交您的软件包。如下图填入软件包的信息,Github URL, 注意勾选右下角的"Publish in Package Manager" 。

后面, 我会介绍怎么创建自己team的私服, 用zpm publish可以简单的把iris的软件包发布到私服上去,这对一个开发团队共享软件包并方便部署应该是更有吸引力些。 马老师,zpm可以打包出任务计划里的指定任务吗? 正常导出用sql, 执行`call %sys.task_tasklist()`。内部并没有一个表,而是直接存成global形式,而且结构还挺复杂真找还是能找到,那么你可以把这个global打包到zpm。
个人以为这是个错误的方法。导入导出global就不是正常该干的事,尤其是对于%SYS库。
如果我做, 我会写个定义task的程序,打在安装包里,到处都能用。
文章
姚 鑫 · 二月 26, 2021
# 第四十八章 Caché 变量大全 ^$LOCK 变量
提供锁名信息。
# 大纲
```
^$|nspace|LOCK(lock_name,info_type,pid)
^$|nspace|L(lock_name,info_type,pid)
```
# 参数
- `|nspace|`或`[nspace]` [nspace]可选-扩展SSVN引用,显式名称空间名称或隐含名称空间。必须计算为带引号的字符串,该字符串括在方括号(`[“nspace”]`)或竖线(`|“nspace”|`)中。命名空间名称不区分大小写;它们以大写字母存储和显示。
- lock_name 计算结果为包含锁定变量名称(带下标或无下标)的字符串的表达式。如果是文字,则必须指定为带引号的字符串。
- info_type 可选-解析为所有大写字母指定为带引号字符串的关键字的表达式。info_type指定返回关于`lock_name`的哪种类型的信息。可用选项有“所有者”、“标志”、“模式”和“计数”。作为独立函数调用`^$LOCK`时需要。
- pid 可选-用于“计数”关键字。一个整数,指定锁所有者的进程标识。如果指定,最多为“计数”返回一个列表元素。如果省略(或指定为0),将为持有指定锁的每个所有者返回一个列表元素。`pid`对其他info_type关键字没有影响。
# 描述
`^$LOCK`结构化系统变量返回有关当前命名空间或本地系统上指定命名空间中的锁的信息。可以通过两种方式使用`^$LOCK`:
- info_type作为独立函数返回指定锁的信息。
- `$DATA`、`$ORDER`或`$QUERY`函数没有info_type作为参数。
注意:`^$LOCK`从本地系统的锁表中检索锁表信息。它不会从远程服务器上的锁表中返回信息。
## ECP环境中的`^$LOCK。`
- 本地系统:为本地系统持有的锁调用`^$LOCK`时,`^$LOCK`的行为与没有ECP时相同,只有一个例外:info_type的`“FLAGS”`返回一个星号(*),表示锁处于ECP环境中。这意味着一旦锁被释放,远程系统间IRIS实例就能够持有锁。
- 应用服务器:当通过ECP(数据服务器或另一个应用服务器)在一个应用服务器上为另一个服务器上持有的锁调用`^$LOCK`时,`^$LOCK`不返回任何信息。请注意,这是与锁不存在时相同的行为。
- 数据服务器:当在数据服务器上为应用服务器持有的锁调用`^$LOCK`时,`^$LOCK`的行为与本地系统略有不同,如下所示:
- “Owner”:如果锁由连接到调用`^$Lock`的数据服务器的应用程序服务器持有,则`^$Lock(LOCKNAME,“OWNER”)`为持有该锁的实例返回`“ECPConfigurationName:MachineName:InstanceName”`,但不标识持有该锁的特定进程。
- `“FLAGS”`:如果锁由连接到调用`^$LOCK`的数据服务器的应用程序服务器持有,则独占锁的`^$LOCK(lockname,“FLAGS”)`将返回`“Z”`标志。这个`“Z”`表示除了ECP环境之外,在InterSystems IRIS中不再使用的一种遗留锁。
- `“mode”`:如果锁由连接到调用`^$lock`的数据服务器的应用程序服务器持有,则独占锁的`^$lock(lockname,“mode”)`返回`“ZAX”`而不是`“X”`。`ZA`是一种遗留锁,除ECP环境外,在系统间IRIS中不再使用。对于共享锁,`^$lock(lockname,“mode”)`返回`“S”`,与本地锁相同。
# 参数
## nspace
此可选参数允许您使用扩展的SSVN引用在另一个名称空间中指定全局变量。可以显式指定名称空间名称,将其命名为带引号的字符串文字或变量,或者通过指定隐式名称空间。命名空间名称不区分大小写。可以使用方括号语法`[“ USER”]`或环境语法`|“ USER” |`。 nspace分隔符前后不允许有空格。
可以使用以下方法测试是否定义了名称空间:
```java
WRITE ##class(%SYS.Namespace).Exists("USER"),!
WRITE ##class(%SYS.Namespace).Exists("LOSER")
```
可以使用`$NAMESPACE`特殊变量来确定当前的名称空间。更改当前名称空间的首选方法是`NEW $NAMESPACE`,然后`SET $NAMESPACE =“nspacename”`。
## lock_name
该表达式的计算结果为包含锁定变量名称(带下标或未下标)的字符串。使用`LOCK`命令定义一个锁变量(通常是全局变量)。
## info_type
当将`^$LOCK`用作独立函数时,需要一个`info_type`关键字;当将`^$LOCK`用作另一个函数的参数时,则是一个可选参数。 info_type必须以大写字母指定为带引号的字符串。
- `“OWNER”`返回锁所有者的进程ID(pid)。如果该锁是共享锁,则以逗号分隔列表的形式返回该锁的所有所有者的进程ID。如果指定的锁不存在,则`^$LOCK`返回空字符串。
- `“FLAGS”`返回锁的状态。它可以返回以下值:`“D”`-处于挂起挂起状态; `“P”`-处于锁定挂起状态; `“N”`-这是一个节点锁定,后代未锁定; `“Z”`-此锁处于ZAX模式; `“L”`-锁丢失,服务器不再具有此锁; `“*”`-这是一个远程锁定。如果指定的锁处于正常锁状态或不存在,则`^$LOCK`返回空字符串。
- `“MODE”`返回当前节点的锁定模式。对于排他锁定模式,它返回`“X”`,对于共享锁定模式,它返回`“S”`,对于`ZALLOCATE`锁定模式,它返回`“ZAX”`。如果指定的锁不存在,则`^$LOCK`返回空字符串。
- `“COUNTS”`返回锁的锁计数,指定为二进制列表结构。对于排他锁,列表包含一个元素;对于共享锁,列表包含每个锁所有者的元素。可以使用pid参数仅返回指定锁定所有者的list元素。每个元素都包含所有者的pid,独占模式增量计数和共享模式增量计数。如果独占模式和共享模式的增量计数均为0(或`“”`),则锁定处于`“ZAX”`模式。增量计数后可以跟一个`“D”`,以指示该锁已在当前事务中解锁,但是其释放被延迟(`“D”`),直到事务被提交或回滚为止。如果指定的锁不存在,则`^$LOCK`返回空字符串。
必须使用所有大写字母指定`info_type`关键字。指定无效的`info_type`关键字会生成``错误。
## pid
锁所有者的进程ID。仅在使用`“COUNTS”`关键字时有意义。用于将“ ”返回值限制为(最多)一个列表元素。 pid在所有平台上均指定为整数。如果pid与`lock_name ^$LOCK`的所有者的进程ID匹配,则返回该所有者的`“COUNTS”`列表元素;如果pid与`lock_name ^$LOCK`的所有者的进程ID不匹配,则返回空字符串。将pid指定为0表示与省略pid相同; `^$LOCK`返回所有`“COUNTS”`列表元素。pid参数与`“OWNER”`,`“FLAGS”`或`“MODE”`关键字一起使用,但被忽略。
# 示例
下面的示例显示由info_type关键字返回的排他锁的值:
```java
/// d ##class(PHA.TEST.SpecialVariables).LOCK()
ClassMethod LOCK()
{
LOCK ^B(1,1) ; define lock
WRITE !,"lock owner: ",^$LOCK("^B(1,1)","OWNER")
WRITE !,"lock flags: ",^$LOCK("^B(1,1)","FLAGS")
WRITE !,"lock mode: ",^$LOCK("^B(1,1)","MODE")
WRITE !,"lock counts: "
ZZDUMP ^$LOCK("^B(1,1)","COUNTS")
LOCK -^B(1,1) ; delete lock
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK()
lock owner: 17824
lock flags:
lock mode: X
lock counts:
0000: 0B 01 04 04 A0 45 03 04 01 02 01 ....??E.....
```
下面的示例显示在递增和递减独占锁时,info_type `“COUNTS”`返回的值如何变化:
```java
/// d ##class(PHA.TEST.SpecialVariables).LOCK1()
ClassMethod LOCK1()
{
LOCK ^B(1,1) ; define exclusive lock
ZZDUMP ^$LOCK("^B(1,1)","COUNTS")
LOCK +^B(1,1) ; increment lock
ZZDUMP ^$LOCK("^B(1,1)","COUNTS")
LOCK +^B(1,1) ; increment lock again
ZZDUMP ^$LOCK("^B(1,1)","COUNTS")
LOCK -^B(1,1) ; decrement lock
ZZDUMP ^$LOCK("^B(1,1)","COUNTS")
LOCK -^B(1,1) ; decrement lock again
ZZDUMP ^$LOCK("^B(1,1)","COUNTS")
LOCK -^B(1,1) ; delete exclusive lock
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK1()
0000: 0B 01 04 04 A0 45 03 04 01 02 01 ....??E.....
0000: 0B 01 04 04 A0 45 03 04 02 02 01 ....??E.....
0000: 0B 01 04 04 A0 45 03 04 03 02 01 ....??E.....
0000: 0B 01 04 04 A0 45 03 04 02 02 01 ....??E.....
0000: 0B 01 04 04 A0 45 03 04 01 02 01 ....??E.....
```
下面的示例显示在递增和递减共享锁时,info_type`“COUNTS”`返回的值如何变化
```java
/// d ##class(PHA.TEST.SpecialVariables).LOCK2()
ClassMethod LOCK2()
{
LOCK ^S(1,1)#"S" ; define shared lock
ZZDUMP ^$LOCK("^S(1,1)","COUNTS")
LOCK +^S(1,1)#"S" ; increment lock
ZZDUMP ^$LOCK("^S(1,1)","COUNTS")
LOCK +^S(1,1)#"S" ; increment lock again
ZZDUMP ^$LOCK("^S(1,1)","COUNTS")
LOCK -^S(1,1)#"S" ; decrement lock
ZZDUMP ^$LOCK("^S(1,1)","COUNTS")
LOCK -^S(1,1)#"S" ; decrement lock again
ZZDUMP ^$LOCK("^S(1,1)","COUNTS")
LOCK -^S(1,1)#"S" ; delete shared lock
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK2()
0000: 0B 01 04 04 A0 45 02 01 03 04 01 ....??E.....
0000: 0B 01 04 04 A0 45 02 01 03 04 02 ....??E.....
0000: 0B 01 04 04 A0 45 02 01 03 04 03 ....??E.....
0000: 0B 01 04 04 A0 45 02 01 03 04 02 ....??E.....
0000: 0B 01 04 04 A0 45 02 01 03 04 01 ....??E.....
```
以下示例显示如何将`^$lock`用作`$DATA`、`$ORDER`和`$QUERY`函数的参数。
## 作为$DATA的参数
`$DATA(^$|nspace|LOCK(lock_name))`
`^$lock`作为`$DATA`的参数返回一个整数值,该值指定锁定名称是否作为节点存在于`^$lock`中。下表显示了`$DATA`可以返回的整数值。
Value| Meaning
---|---
0 |锁信息不存在
10 |锁信息存在
请注意,在此上下文中使用的`$DATA`只能返回0或10,其中10表示指定的锁存在。它不能确定锁是否有后代,也不能返回1或11。
下面的示例测试当前命名空间中是否存在锁名。第一次写入返回10(锁名存在),第二次写入返回0(锁名不存在):
```java
/// d ##class(PHA.TEST.SpecialVariables).LOCK3()
ClassMethod LOCK3()
{
LOCK ^B(1,2) ; define lock
WRITE !,$DATA(^$LOCK("^B(1,2)"))
LOCK -^B(1,2) ; delete lock
WRITE !,$DATA(^$LOCK("^B(1,2)"))
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK3()
10
0
```
## 作为`$ORDER`的参数
`$ORDER(^$|nspace|LOCK(lock_name),direction)`
`^$lock`作为`$ORDER`的参数,按排序顺序将下一个或上一个`^$lock`锁名节点返回到指定的锁名。如果不存在这样的锁名作为`^$lock`节点,`$ORDER`将返回空字符串。
锁以区分大小写的字符串排序顺序返回。使用数字排序规则以下标树顺序返回命名锁的下标。
Direction参数指定是返回下一个锁名称还是返回上一个锁名称。如果不提供方向参数,InterSystems IRIS会将排序序列中的下一个锁名返回到您指定的锁名。
以下子例程在Samples名称空间中搜索锁,并将锁名称存储在名为locket的本地数组中。
```java
/// d ##class(PHA.TEST.SpecialVariables).LOCK4()
ClassMethod LOCK4()
{
LOCKARRAY
SET lname=""
FOR I=1:1 {
SET lname=$ORDER(^$|"SAMPLES"|LOCK(lname))
QUIT:lname=""
SET LOCKET(I)=lname
WRITE !,"the lock name is: ",lname
}
WRITE !,"All lock names listed"
QUIT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK4()
the lock name is: ^%SYS("CSP","Daemon")
All lock names listed
```
## 作为`$QUERY`的参数
`$QUERY(^$|nspace|LOCK(lock_name))`
`^$lock`作为`$query`的参数,将排序序列中的下一个锁名返回到您指定的锁名。如果没有将下一个锁名定义为`^$lock`中的节点,则`$query`将返回空字符串。
锁以区分大小写的字符串排序顺序返回。使用数字排序规则以下标树顺序返回命名锁的下标。
在下面的示例中,在当前命名空间中(按随机顺序)创建了五个全局锁名称。
```java
/// d ##class(PHA.TEST.SpecialVariables).LOCK5()
ClassMethod LOCK5()
{
LOCK (^B(1),^A,^D,^A(1,2,3),^A(1,2))
WRITE !,"lock name: ",$QUERY(^$LOCK(""))
WRITE !,"lock name: ",$QUERY(^$LOCK("^C"))
WRITE !,"lock name: ",$QUERY(^$LOCK("^A(1,2)"))
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).LOCK5()
lock name: ^$LOCK("^%SYS(""CSP"",""Daemon"")")
lock name: ^$LOCK("^D")
lock name: ^$LOCK("^A(1,2,3)")
```
`$QUERY`将所有全局锁变量名(带下标或无下标)视为字符串,并按字符串排序顺序检索它们。因此,`$QUERY(^$LOCK(“”))`按排序顺序检索第一个锁名:`^$LOCK(“^A”)`或排序序列中位置较高的InterSystems IRIS定义的锁。`$QUERY(^$LOCK(“^C”))`检索排序序列中不存在的`^C`:`^$LOCK(“^D”)`之后的下一个锁名。`$QUERY(^$LOCK(“^A(1,2)”))`检索排序规则序列中它后面的`^$LOCK(“^A(1,2,3)”)`。 想知道锁产生的原因大概有哪些?应该怎么避免呢? 我有个想法,希望结合应用场景来介绍,这样能够明白使用场景。
文章
姚 鑫 · 七月 27, 2022
# 第九章 REST 服务安全
如果 `REST` 服务正在访问机密数据,应该对服务使用身份验证。如果需要为不同的用户提供不同级别的访问权限,还要指定端点所需的权限。
# 为 `REST` 服务设置身份验证
可以对 `IRIS REST` 服务使用以下任何形式的身份验证:
- `HTTP` 身份验证标头 — 这是 `REST` 服务的推荐身份验证形式。
- `Web` 会话身份验证 — 其中用户名和密码在 URL 中的问号后面指定。
- `OAuth 2.0` 身份验证 - 请参阅以下小节。
## `REST` 应用程序和 `OAuth 2.0`
要通过 `OAuth 2.0` 对 `REST` 应用程序进行身份验证,请执行以下所有操作:
- 将包含 `REST` 应用程序的资源服务器配置为 `OAuth 2.0` 资源服务器。
- 允许对 `%Service.CSP` 进行委派身份验证。
- 确保将 `Web` 应用程序(用于 `REST` 应用程序)配置为使用委托身份验证。
- 在 `%SYS` 命名空间中创建一个名为 `ZAUTHENTICATE` 的例程。 提供了一个示例例程 `REST.ZAUTHENTICATE.mac`,可以复制和修改它。此例程是 GitHub (https://github.com/intersystems/Samples-Security) 上 Samples-Security 示例的一部分。可以按照“下载用于 IRIS 的示例”中的说明下载整个示例,但在 `GitHub` 上打开例程并复制其内容可能更方便。
在例程中,修改 `applicationName` 的值并根据需要进行其他更改。
## 指定使用 `REST` 服务所需的权限
为了指定执行代码或访问数据所需的权限, 技术使用基于角色的访问控制 (`RBAC`)。
如果需要为不同的用户提供不同级别的访问权限,请执行以下操作来指定权限:
- 修改规范类以指定使用 `REST` 服务或 `REST` 服务中的特定端点所需的权限;然后重新编译。权限是与资源名称组合的权限(例如读取或写入)。
- 使用管理门户:
- 定义在规范类中引用的资源。
- 定义提供权限集的角色。例如,角色可以提供对端点的读取访问权限或对不同端点的写入访问权限。一个角色可以包含多组权限。
- 将用户置于其任务所需的所有角色中。
此外,可以使用 `%CSP.REST` 类的 `SECURITYRESOURCE` 参数来执行授权。
# 指定权限
可以为整个 `REST` 服务指定权限列表,也可以为每个端点指定权限列表。为此:
1. 要指定访问服务所需的权限,请编辑规范类中的 `OpenAPI XData` 块。对于 `info` 对象,添加一个名为 `x-ISC_RequiredResource` 的新属性,其值是以逗号分隔的已定义资源列表及其访问模式 (`resource:mode`),这是访问 `REST` 服务的任何端点所必需的。
下面显示了一个示例:
```java
"swagger":"2.0",
"info":{
"version":"1.0.0",
"title":"Swagger Petstore",
"description":"A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification",
"termsOfService":"http://swagger.io/terms/",
"x-ISC_RequiredResource":["resource1:read","resource2:read","resource3:read"],
"contact":{
"name":"Swagger API Team"
},
...
```
2. 要指定访问特定端点所需的权限,请将 `x-ISC_RequiredResource` 属性添加到定义该端点的操作对象,如下例所示:
```java
"post":{
"description":"Creates a new pet in the store. Duplicates are allowed",
"operationId":"addPet",
"x-ISC_RequiredResource":["resource1:read","resource2:read","resource3:read"],
"produces":[
"application/json"
],
...
```
3. 编译规范类。此操作重新生成调度类。
## 使用 `SECURITYRESOURC` 参数
作为附加的授权工具,分派 `%CSP.REST` 子类的类具有 `SECURITYRESOURCE` 参数。 `SECURITYRESOURCE` 的值要么是资源及其权限,要么只是资源(在这种情况下,相关权限是使用)。系统检查用户是否对与 `SECURITYRESOURCE` 关联的资源具有所需的权限。
注意:如果调度类为 `SECURITYRESOURCE` 指定了一个值,并且 `CSPSystem` 用户没有足够的权限,那么这可能会导致登录尝试失败时出现意外的 `HTTP` 错误代码。为防止这种情况发生, 建议您将指定资源的权限授予 `CSPSystem` 用户。
文章
姚 鑫 · 七月 11, 2022
# 第二章 嵌入式Python概述(二)
# 从 Python 调用 IRIS API
如果使用的是嵌入式 `Python` 并且需要与 `IRIS` 交互,可以使用 `Python shell` 中的 `iris` 模块,或者使用 `Python` 编写的 `IRIS` 类中的方法。要遵循本节中的示例,可以使用 `ObjectScript` 命令 `do ##class(%SYS.Python).Shell()` 从终端会话启动 `Python shell`。
当启动终端会话时,将被放置在 `IRIS` 的 `USER` 命名空间中,将看到提示 `USER>`。但是,如果从 `GitHub` 加载了示例类,则需要在 `SAMPLES` 命名空间中才能访问它们。
在终端中,更改为 `SAMPLES` 命名空间,然后启动 `Python shell`,如下所示:
```java
USER>set $namespace = "SAMPLES"
SAMPLES>do ##class(%SYS.Python).Shell()
Python 3.9.5 (default, Jul 19 2021, 17:50:44) [MSC v.1927 64 bit (AMD64)] on win32
Type quit() or Ctrl-D to exit this shell.
>>>
```
当从终端会话启动 `Python shell` 时,`Python shell` 继承与终端相同的上下文,例如,当前命名空间和用户。局部变量不被继承。
## 使用类
要从 `Python` 访问 `IRIS` 类,请使用 `iris` 模块来实例化要使用的类。然后,可以像访问 `Python` 类一样使用访问它的属性和方法。
注意:可能习惯于在 `Python` 中导入模块,然后再使用它,例如:
```java
>>> import iris
```
但是,在使用 `%SYS.Python` 类的 `Shell()` 方法运行 `Python shell `时,不需要显式导入 `iris` 模块。继续使用该模块。
以下示例使用系统类 `%Library.File` 的 `ManagerDirectory()` 方法打印 `IRIS` 管理器目录的路径:
```java
>>> lf = iris.cls('%Library.File')
>>> print(lf.ManagerDirectory())
C:\InterSystems\IRIS\mgr\
```
此示例使用系统类 `%SYSTEM.CPU` 的 `Dump()` 方法来显示有关正在运行 `IRIS` 实例的服务器的信息:
```java
>>> cpu = iris.cls('%SYSTEM.CPU')
>>> cpu.Dump()
-- CPU Info for node MYSERVER ----------------------------------------------
Architecture: x86_64
Model: Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz
Vendor: Intel
# of threads: 4
# of cores: 2
# of chips: 1
# of threads per core: 2
# of cores per chip: 2
MT supported: 1
MT enabled: 1
MHz: 2904
------------------------------------------------------------------------------
```
此示例使用 `GitHub` 上 `Samples-Data` 存储库中的 `Sample.Company` 类。虽然可以使用任何命名空间中以百分号 (`%`) 开头的类(如 `%SYS.Python` 或 `%Library.File`)来访问 `Sample.Company` 类,但如前所述,必须位于 `SAMPLES` 命名空间中。
`Sample.Company` 的类定义如下:
```java
Class Sample.Company Extends (%Persistent, %Populate, %XML.Adaptor)
{
/// The company's name.
Property Name As %String(MAXLEN = 80, POPSPEC = "Company()") [ Required ];
/// The company's mission statement.
Property Mission As %String(MAXLEN = 200, POPSPEC = "Mission()");
/// The unique Tax ID number for the company.
Property TaxID As %String [ Required ];
/// The last reported revenue for the company.
Property Revenue As %Integer;
/// The Employee objects associated with this Company.
Relationship Employees As Employee [ Cardinality = many, Inverse = Company ];
}
```
此类扩展 `%Library.Persistent`(通常缩写为 `%Persistent`),这意味着此类的对象可以持久保存在 `IRIS` 数据库中。该类还具有多个属性,包括 `Name` 和 `TaxID`,这两个属性都是保存对象所必需的。
尽管不会在类定义中看到它们,但持久类带有许多用于操作此类对象的方法,例如 `%New()`、`%Save()`、`%Id()` 和 `%OpenId()`。但是,`Python` 方法名称中不允许使用百分号 (`%`),因此请改用下划线 (`_`)。
下面的代码创建一个新的 `Company` 对象,设置所需的 `Name` 和 `TaxID` 属性,然后将公司保存在数据库中:
```java
>>> myCompany = iris.cls('Sample.Company')._New()
>>> myCompany.Name = 'Acme Widgets, Inc.'
>>> myCompany.TaxID = '123456789'
>>> status = myCompany._Save()
>>> print(status)
1
>>> print(myCompany._Id())
22
```
上面的代码使用 `_New()` 方法创建类的实例,并使用 `_Save()` 将实例保存在数据库中。 `_Save()` 方法返回一个状态码。在这种情况下,`1` 表示保存成功。当保存一个对象时, `IRIS` 会为其分配一个唯一 `ID`,可以在以后使用该 `ID` 从存储中检索该对象。 `_Id()` 方法返回对象的 `ID`。
使用类的 `_OpenId()` 方法将对象从持久存储中检索到内存中进行处理:
```java
>>> yourCompany = iris.cls("Sample.Company")._OpenId(22)
>>> print(yourCompany.Name)
Acme Widgets, Inc.
```
将以下代码添加到类定义中会创建一个 `Print()` 方法,该方法打印当前公司的 `Name` 和 `TaxID`。将 `Language` 关键字设置为 `python` 会告诉类编译器该方法是用 `Python` 编写的。
```java
Method Print() [ Language = python ]
{
print ('\nName: ' + self.Name + ' TaxID: ' + self.TaxID)
}
```
给定一个 `Company` 对象,可以调用它的 `Print()` 方法,如下所示:
```java
>>> yourCompany.Print()
Name: Acme Widgets, Inc. TaxID: 123456789
```
文章
Nicky Zhu · 五月 7, 2022
我们的一位客户五一期间向使用IRIS搭建的数据流推送一家三甲医院数年的历史数据,导致实施的同事们经历了一系列噩梦,包括但不限与:
1. 由于未通知实施团队有这样规模的数据推送,数据推送过程与全库备份任务重叠。尽管实例和数据流正常运行,但备份任务与数据流争抢IO,导致备份任务不能在预期时间内完成,实施童鞋五一加班处理问题。
2. 为了节省磁盘空间,服务器上部署了定期删除IRIS备份文件的任务,原本能够保持一周的全备+增量备份,但在本次数据暴增的情况下,新的备份尚未完成而旧的全备已被删除,导致问题发生时没有可用于恢复的备份。
3. 由于这次数据推送前未进行数据质量校验,推送的数据全部不合规,但已经历了较长的数据流进行处理全部入库;同时由于备份文件已被删除,无法通过恢复数据库的方法回滚,导致实施童鞋不得不逐条从生产环境三个库的数百张表中挑出问题数据逐一删除,从五一放假结束至今还未完成善后工作。大家可以设想一下,如果备份还在,那么恢复备份就可以了。
因此,我们希望再次提醒各位在前线奋斗的亲们:
1. 善待你的备份。尽管对于大型医院或医疗集团来说,两周的全备+增量备份策略下,备份文件会占据数个TB的存储空间。但在需要回滚时,这几个T的空间能救命。
2. 保持可用的测试环境。尤其是对于可能出现随机数据需求的客户,随机产生数据需求意味着随机出现测试需求。
3. 验证新数据的合规性,永远不要假设新数据一定合规。未经测试的新数据必然毫无悬念地导致新问题。
4. 对于任何批量数据处理任务,请务必提前规划,错开资源(CPU、内存、IO)的抢占,避免抢不到资源的任务饿饭。
5. 保持与最终客户的频繁沟通,所有对于生产环境进行的改动都应该经过项目组评估。虽然客户是上帝,但命运有时很顽皮,生产环境的安全保障也需要客户的合作。
最后,大家都知道InterSystems的IRIS在多数客户的场景下都不需要搭建负载均衡集群,这家客户也不例外,数据流中的数层结点上部署的都是单实例IRIS,通过Mirror实现高可用。在这次新数据的上传过程中,IRIS的数据流自然经历了突如其来的爆发式数据压力,以其中一个实例的消息量为例:
该用户在实例上保存30天的数据,可见在经历了五一的消息暴增之后,该客户的每日平均消息量已超过3300万条每天(实际上我们已经查到其中数天单日消息增量已超过5000万条),而该客户平时的消息量不过数十万条每天。
这次IRIS经洪峰而不倒固然可喜可贺,但相信在需要在客户面前经历各种千夫所指的PM、实施、开发与测试同事一定不希望经历这种惊喜。
Good luck.
值得一篇专门的文章介绍客户应该怎么使用备份以及数据推流
文章
Michael Lei · 六月 23, 2021
> 注(2019 年 6 月):许多内容发生了变化,[最新的详细信息请参见此处](https://community.intersystems.com/post/unpacking-pbuttons-yape-update-notes-and-quick-guides)
> 注(2018 年 9 月):自本帖首次发布以来,内容已经有了很大改动,我建议使用 Docker 容器版本,以容器形式运行的项目以及详细信息仍然在 [GitHub 的同一个地址发布](https://github.com/murrayo/yape),您可以下载、运行并根据需要进行修改。
与客户合作进行性能评估、容量规划和故障排除时,我经常解包和查看来自 pButtons 的 Caché 和操作系统指标。 [我不久前发布了一个帖子,介绍了一个用来解包 pButtons 指标的实用工具](https://community.intersystems.com/post/extracting-pbuttons-data-csv-file-easy-charting)(该实用工具使用 unix shell、perl 和 awk 脚本编写),而不是费力地浏览 html 文件,再将需要绘制的部分剪切并粘贴到 excel 中。 虽然这是一个*有用的省时工具*,但还不够完善... 我还使用脚本自动绘制指标图表,以便快速查看并包含在报告中。 但是,这些绘图脚本不容易维护,并且当需要站点特定的配置(例如 iostat 或 Windows perfmon 的磁盘列表)时会变得特别混乱,所以我从未公开发布过绘图实用工具。 不过我现在可以很高兴地说,已经有了简单得多的解决方案。
当我与 Fabian 一起在客户站点查看系统性能时,有了意外发现,[他向我展示了使用实用的 Python 绘图模块所做的工作](https://community.intersystems.com/post/visualizing-data-jungle-part-i-lets-make-graph)。 这是一个比我使用的脚本更灵活、更容易维护的解决方案。 集成 Python 模块进行文件管理和绘制图表的简便性,包括可以分享的交互式 html,意味着输出可以有更大用处。 以 Fabian 的帖子为基础,我编写了 __Yape__,旨在快速简单地提取客户的多种格式的 pButtons 文件,然后绘制图表。 该项目已[在 GitHub 上发布](https://github.com/murrayo/yape),您可以下载、运行并根据需要进行修改。
## 概述
目前,此过程有_两个_步骤。
### 步骤 1. `extract_pButtons.py`
从 pButtons 提取感兴趣的部分并写入到 .csv 文件,以便使用 Excel 打开或使用 `graph_pButtons.py` 进行绘图处理。
### 步骤 2. `graph_pButtons.py`
绘制步骤 1 中创建的文件的图表。 目前,输出可以是 `.png` 形式的线形图或点阵图,也可以是带有平移、缩放、打印等选项的`交互式 .html`。
GitHub 上的 _Readme.md_ 详细介绍了如何设置和运行这两个 python 脚本,并且将是最新的参考。
## 其他说明
例如:使用向输出和输入目录添加前缀的选项,可以轻松遍历包含一组(例如一个星期)pButtons html 文件的目录,并针对每个 pButtons 文件都输出到一个单独目录。
for i in `ls *.html`; do ./extract_pButtons.py $i -p ${i}_; done
for i in `ls *.html`; do ./graph_pButtons.py ./${i}_metrics -p ${i}_; done
在短期内,当我继续撰写有关 [Caché 容量规划和性能](https://community.intersystems.com/post/intersystems-data-platforms-capacity-planning-and-performance-series-index)的系列文章时,我将使用由这些实用工具创建的图表。
我已经在 OSX 上进行了测试,但没有在 Windows 上测试。 您应该能够在 Windows 上安装和运行 Python,请留下您在 Windows 下的经验反馈。 例如,我猜想必须对文件路径斜杠进行更改。
> 注:直到几周前,我都没有用 Python 编写过任何东西,所以如果您是 Python 专家,那么代码中可能会有一些内容并不是最佳做法。 但是,我几乎每天都使用这些脚本,因此我将继续进行改进。 我希望我的 Python 技能会有所提高 — 但是如果您看到一些应该纠正的地方,请随意“教导”我!
如果您发现这些脚本有用,请告诉我,并不时回来看看以获取新功能和更新。
文章
Michael Lei · 十二月 7, 2022
Intersystems IRIS for Health 对 FHIR 行业标准提供了出色的支持。主要特点是:1.FHIR 服务器2. FHIR数据库3. REST 和 ObjectScript API 用于 FHIR 资源(患者、问卷、疫苗等)的 CRUD 操作
本文演示了如何使用这些功能,并展示了用于创建和查看表单类型的 FHIR 资源的Angula前端。
第 1 步 - 使用 InterSystems IRIS for Health 部署您的 FHIR 服务器
要创建 FHIR 服务器,您必须将以下说明添加到 iris.script 文件中(来自:https://openexchange.intersystems.com/package/iris-fhir-template)
zn "HSLIB" set namespace= "FHIRSERVER" Set appKey = "/fhir/r4" Set strategyClass = "HS.FHIRServer.Storage.Json.InteractionsStrategy" set metadataPackages = $lb ( "hl7.fhir.r4.core@4.0.1" ) set importdir= "/opt/irisapp/src" //Install a Foundation namespace and change to it Do ##class (HS.HC.Util.Installer).InstallFoundation(namespace) zn namespace // Install elements that are required for a FHIR-enabled namespace Do ##class (HS.FHIRServer.Installer).InstallNamespace() // Install an instance of a FHIR Service into the current namespace Do ##class (HS.FHIRServer.Installer).InstallInstance(appKey, strategyClass, metadataPackages) // Configure FHIR Service instance to accept unauthenticated requests set strategy = ##class (HS.FHIRServer.API.InteractionsStrategy).GetStrategyForEndpoint(appKey) set config = strategy.GetServiceConfigData() set config.DebugMode = 4 do strategy.SaveServiceConfigData(config) zw ##class (HS.FHIRServer.Tools.DataLoader).SubmitResourceFiles( "/opt/irisapp/fhirdata/" , "FHIRServer" , appKey) do $System .OBJ.LoadDir( "/opt/irisapp/src" , "ck" ,, 1 ) zn "%SYS" Do ##class (Security.Users).UnExpireUserPasswords( "*" ) zn "FHIRSERVER" zpm "load /opt/irisapp/ -v" : 1 : 1 //zpm "install fhir-portal" halt
使用实用程序类 HS.FHIRServer.Installer,您可以创建 FHIR 服务器。
第 2 步 - 使用 FHIR REST 或 ObjectScript API 读取、更新、删除和查找 FHIR 数据
我喜欢使用 ObjectScript 类 HS.FHIRServer.Service 来执行所有 CRUD 操作。
要从资源类型(表单)中获取所有 FHIR 数据:
/// Retreive all the records of questionnaire ClassMethod GetAllQuestionnaire() As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" set request.RequestMethod = "GET" do fhirService.DispatchRequest(request, .pResponse) set json = pResponse.Json set resp = [] set iter = json.entry. %GetIterator () while iter. %GetNext (.key, .value) { do resp. %Push (value.resource) } write resp. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on get all questionnairies" } Quit tSC }
要从 FHIR 数据存储库中获取特定数据项(调查表):
/// Retreive a questionnaire by id ClassMethod GetQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "GET" do fhirService.DispatchRequest(request, .pResponse) write pResponse.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on get the questionnaire" } Quit tSC }
要创建新的 FHIR 资源事件(新的调查表):
/// Create questionnaire ClassMethod CreateQuestionnaire() As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" set request.RequestMethod = "POST" set data = {}. %FromJSON ( %request.Content ) set data.resourceType = "Questionnaire" set request.Json = data do fhirService.DispatchRequest(request, .response) write response.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on create questionnaire" } Return tSC }
要更新 FHIR 资源(表单):
/// Update a questionnaire ClassMethod UpdateQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "PUT" set data = {}. %FromJSON ( %request.Content ) set data.resourceType = "Questionnaire" set request.Json = data do fhirService.DispatchRequest(request, .response) write response.Json. %ToJSON () } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on update questionnaire" } Return tSC }
要删除 FHIR 资源事件(表单):
/// Delete a questionnaire by id ClassMethod DeleteQuestionnaire(id As %String ) As %Status { set tSC = $$$OK Set %response.ContentType = ..#CONTENTTYPEJSON Set %response.Headers ( "Access-Control-Allow-Origin" )= "*" Try { set fhirService = ##class (HS.FHIRServer.Service).EnsureInstance(..#URL) set request = ##class (HS.FHIRServer.API.Data.Request). %New () set request.RequestPath = "/Questionnaire/" _id set request.RequestMethod = "DELETE" do fhirService.DispatchRequest(request, .pResponse) } Catch Err { set tSC = 1 set message = {} set message.type= "ERROR" set message.details = "Error on delete the questionnaire" } Quit tSC }
如您所见,您想要创建使用 POST,更新使用 PUT,删除使用 DELETE,查询使用 GET 动词。
第 3 步 - 创建 Angular 客户端以使用您的 FHIR 服务器应用程序
我使用 PrimeNG 创建了一个角度应用程序并安装了包 npm install --save @types/fhir。此包具有映射到 TypeScript 的所有 FHIR 类型。
Angular控制器类:
import { Component, OnInit, ViewEncapsulation } from '@angular/core' ; import { ActivatedRoute, Router } from '@angular/router' ; import { Period, Questionnaire } from 'fhir/r4' ; import { ConfirmationService, MessageService, SelectItem } from 'primeng/api' ; import { QuestionnaireService } from './questionnaireservice' ; const QUESTIONNAIREID = 'questionnaireId' ; @Component({ selector: 'app-questionnaire', templateUrl: './questionnaire.component.html', providers: [MessageService, ConfirmationService], styleUrls: ['./questionnaire.component.css'], encapsulation: ViewEncapsulation.None }) export class QuestionnaireComponent implements OnInit { public questionnaire: Questionnaire ; public questionnairies: Questionnaire[] ; public selectedQuestionnaire: Questionnaire ; public questionnaireId: string ; public sub: any ; public publicationStatusList: SelectItem[] ; constructor( private questionnaireService: QuestionnaireService, private router: Router, private route: ActivatedRoute, private confirmationService: ConfirmationService, private messageService: MessageService){ this.publicationStatusList = [ {label: 'Draft', value: 'draft'}, {label: 'Active', value: 'active'}, {label: 'Retired', value: 'retired'}, {label: 'Unknown', value: 'unknown'} ] } ngOnInit() { this.reset() ; this.listQuestionnaires() ; this.sub = this.route.params.subscribe(params => { this.questionnaireId = String(+params[QUESTIONNAIREID]) ; if (!Number.isNaN(this.questionnaireId)) { this.loadQuestionnaire(this.questionnaireId) ; } }) ; } private loadQuestionnaire(questionnaireId) { this.questionnaireService.load(questionnaireId).subscribe(response => { this.questionnaire = response ; this.selectedQuestionnaire = this.questionnaire ; if (!response.effectivePeriod) { this.questionnaire.effectivePeriod = <Period>{} ; } }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load questionnaire.' }) ; }) ; } public loadQuestions() { if (this.questionnaire && this.questionnaire.id) { this.router.navigate(['/question', this.questionnaire.id]) ; } else { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Choose a questionnaire.' }) ; } } private listQuestionnaires() { this.questionnaireService.list().subscribe(response => { this.questionnairies = response ; this.reset() ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on load the questionnaries.' }) ; }) ; } public onChangeQuestionnaire() { if (this.selectedQuestionnaire && !this.selectedQuestionnaire.id) { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' }) ; } else { if (this.selectedQuestionnaire && this.selectedQuestionnaire.id) { this.loadQuestionnaire(this.selectedQuestionnaire.id) ; } } } public reset() { this.questionnaire = <Questionnaire>{} ; this.questionnaire.effectivePeriod = <Period>{} ; } public save() { if (this.questionnaire.id && this.questionnaire.id != "" ) { this.questionnaireService.update(this.questionnaire).subscribe( (resp) => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire saved.' }) ; this.listQuestionnaires() this.loadQuestionnaire(this.questionnaire.id) ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on save the questionnaire.' }) ; } ) ; } else { this.questionnaireService.save(this.questionnaire).subscribe( (resp) => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire saved.' }) ; this.listQuestionnaires() this.loadQuestionnaire(resp.id) ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on save the questionnaire.' }) ; } ) ; } } public delete(id: string) { if (!this.questionnaire || !this.questionnaire.id) { this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Select a questionnaire.' }) ; } else { this.confirmationService.confirm({ message: ' Do you confirm?', accept: () => { this.questionnaireService.delete(id).subscribe( () => { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Questionnaire deleted.' }) ; this.listQuestionnaires() ; this.reset() ; }, error => { console.log(error) ; this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Error on delete questionnaire.' }) ; } ) ; } }) ; } } }
Angular HTML 文件
<p-toast [style]= "{marginTop: '80px', width: '320px'}" ></p-toast> <p-card> <div class = "p-fluid p-formgrid grid" > <div class = "field col-12 lg:col-12 md:col-12" > <p-dropdown id= "dropquestions1" [options]= "questionnairies" [(ngModel)]= "selectedQuestionnaire" (onChange)= "onChangeQuestionnaire()" placeholder= "Select a Questionnaire" optionLabel= "title" [filter]= "true" [showClear]= "true" ></p-dropdown> </div> </div> <p-tabView> <p-tabPanel leftIcon= "fa fa-question" header= "Basic Data" > <div class = "p-fluid p-formgrid grid" > <div class = "field col-3 lg:col-3 md:col-12" > <label for = "txtname" >Name</label> <input class = "inputfield w-full" id= "txtname" required type= "text" [(ngModel)]= "questionnaire.name" pInputText placeholder= "Name" > </div> <div class = "field col-7 lg:col-7 md:col-12" > <label for = "txttitle" >Title</label> <input class = "inputfield w-full" id= "txttitle" required type= "text" [(ngModel)]= "questionnaire.title" pInputText placeholder= "Title" > </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtdate" >Date</label> <p-inputMask id= "txtdate" mask= "9999-99-99" [(ngModel)]= "questionnaire.date" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtstatus" >Status</label> <p-dropdown [options]= "publicationStatusList" [(ngModel)]= "questionnaire.status" ></p-dropdown> </div> <div class = "field col-3 lg:col-3 md:col-12" > <label for = "txtpublisher" >Publisher</label> <input class = "inputfield w-full" id= "txtpublisher" required type= "text" [(ngModel)]= "questionnaire.publisher" pInputText placeholder= "Publisher" > </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtstartperiod" >Start Period</label> <p-inputMask id= "txtstartperiod" mask= "9999-99-99" [(ngModel)]= "questionnaire.effectivePeriod.start" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-2 lg:col-2 md:col-12" > <label for = "txtendperiod" >End Period</label> <p-inputMask id= "txtendperiod" mask= "9999-99-99" [(ngModel)]= "questionnaire.effectivePeriod.end" placeholder= "9999-99-99" slotChar= "yyyy-mm-dd" ></p-inputMask> </div> <div class = "field col-12 lg:col-12 md:col-12" > <label for = "txtcontent" >Description</label> <p-editor [(ngModel)]= "questionnaire.description" [style]= "{'height':'100px'}" ></p-editor> </div> </div> <div class = "grid justify-content-end" > <button pButton pRipple type= "button" label= "New Record" (click)= "reset()" class = "p-button-rounded p-button-success mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Save" (click)= "save()" class = "p-button-rounded p-button-info mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Delete" (click)= "delete(questionnaire.id)" class = "p-button-rounded p-button-danger mr-2 mb-2" ></button> <button pButton pRipple type= "button" label= "Questions" (click)= "loadQuestions()" class = "p-button-rounded p-button-info mr-2 mb-2" ></button> </div> </p-tabPanel> </p-tabView> </p-card> <p-confirmDialog #cd header= "Atenção" icon= "pi pi-exclamation-triangle" > <p-footer> <button type= "button" pButton icon= "pi pi-times" label= "Não" (click)= "cd.reject()" ></button> <button type= "button" pButton icon= "pi pi-check" label= "Sim" (click)= "cd.accept()" ></button> </p-footer> </p-confirmDialog>
角度服务类
import { Injectable } from '@angular/core' ; import { HttpClient, HttpHeaders } from '@angular/common/http' ; import { Observable } from 'rxjs' ; import { environment } from 'src/environments/environment' ; import { take } from 'rxjs/operators' ; import { Questionnaire } from 'fhir/r4' ; @Injectable({ providedIn: 'root' }) export class QuestionnaireService { private url = environment.host2 + 'questionnaire' ; constructor(private http: HttpClient) { } public save(Questionnaire: Questionnaire): Observable<Questionnaire> { return this.http.post<Questionnaire>(this.url, Questionnaire).pipe(take( 1 )) ; } public update(Questionnaire: Questionnaire): Observable<Questionnaire> { return this.http.put<Questionnaire>(`${this.url}/${Questionnaire.id}`, Questionnaire).pipe(take( 1 )) ; } public load(id: string): Observable<Questionnaire> { return this.http.get<Questionnaire>(`${this.url}/${id}`).pipe(take( 1 )) ; } public delete(id: string): Observable<any> { return this.http.delete(`${this.url}/${id}`).pipe(take( 1 )) ; } public list(): Observable<Questionnaire[]> { return this.http.get<Questionnaire[]>(this.url).pipe(take( 1 )) ; } }
第 4 步 - 实际应用
1. 转到 https://openexchange.intersystems.com/package/FHIR-Questionnaires 应用程序。
2. clone/git pull repo 到任何本地目录
$ git clone https://github.com/yurimarx/fhir-questions.git
3、在该目录下打开终端,运行:
$ docker-compose up -d
4.打开网络应用程序: http://localhost:52773/fhirquestions/index.html
截图: