清除过滤器
文章
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 全局变量的替代映射列表。
* * *
在本系列的第二部分中,我们将阐述基本架构模型的实现
文章
姚 鑫 · 二月 5
# 第十五章 K - L 开头的术语
### 日志记录 (journaling)
**系统**
一种功能,系统管理员可以选择启用,导致 `IRIS` 在日志文件中记录所有或选定全局的更改。如果发生系统故障,可以将这些更改向前滚动。也就是说,在恢复期间,可以将整个事务重新应用到数据库。另请参见写入镜像日志记录 (`Write Image Journaling`)。
# 以 K 开头的术语
### 密钥分发中心 (`KDC`)
**系统**
密钥分发中心(`Key Distribution Center,KDC`)是 `Kerberos` 安装的一部分,是确保所有参与方正确认证的中央 `Kerberos` 服务器。具体来说,`KDC` 是可信第三方 `Kerberos` 服务器的一部分,负责生成构成票据授予票据(`TGT`)和服务票据(`Service Ticket`)基础的密钥。在 `Windows` 系统中,密钥分发中心是 `Windows` 域控制器(`Domain Controller,DC`)的一部分,有时也称为该名称。这两个缩写的相似性纯属巧合。
### Kerberos
**系统**
`Kerberos` 是由麻省理工学院(`MIT`)的 `Athena` 项目开发的可信第三方认证系统。它通过建立一个认证信息数据库,允许对用户或应用程序(统称为主体,`principals`)进行认证。该数据库是安全的(因此是可信的),并且与执行认证的任何两个主体分离(这就是为什么它是第三方系统)。`Kerberos` 设计用于不一定安全的网络环境,如互联网。自 `1980` 年代末以来,它已在大型商业和教育机构中广泛使用。
### 键(唯一索引) (key (unique index))
**对象(Objects)**
键是唯一索引的另一种名称。
### 键(加密) (key (encryption))
**系统**
用于加密或解密数据的一个大数,与加密算法配合使用。
### 密钥加密密钥 (key-encryption key)
**系统**
在`IRIS` 数据库加密中,涉及的第二个密钥。第一个密钥用于加密数据库,而密钥加密密钥——第二个密钥——用于加密(因此保护)第一个密钥。当数据库加密密钥被激活时,它会使用密钥加密密钥进行解密并加载到内存中以供使用。
### 关键字(类定义) (keyword (class definition))
**对象(Objects)**
关键字在类定义中定义了一个特定的特性。也称为类关键字。
### 关键字(系统元素) (keyword (system element))
**系统**
关键字也可能指`IRIS` 系统的一部分,如函数名称或运算符。
# 以 L 开头的术语
### 语言配置 (language configuration)
**系统**
一组四个表:字符集、排序序列、`$X/$Y` 动作表和模式匹配;定义国家语言支持的设备无关方面。它是设备相关国家语言支持特性的对应部分,即输入/输出转换。
### 许可证 (license)
**系统**
`InterSystems` 与其客户之间的协议,定义了可供客户使用的`IRIS` 软件组件及每个组件可用的用户数量。客户必须持有许可证才能运行 `IRIS`。许可证信息通过产品激活密钥分发,并存储在系统上的名为 `iris.key` 的文件中。
### 列表 (list)
**对象(Objects)**
一种有序的集合,使用槽号访问数据。每个列表在 `SQL` 中被投射为单个列表字段。
### 区域设置 (locale)
**系统**
指定用户语言、国家及任何其他特殊变体偏好的参数。区域设置指定用于数据输入、输出和处理的用户可见惯例,如数字和日期的表示方式,以及星期和月份的名称。
文章
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的程序,打在安装包里,到处都能用。
文章
Jeff Liu · 六月 30, 2024
Purpose of this article
有两篇很棒的有关删除消息关联的孤儿记录的内容以及如何处理孤儿的问题的WRC议最佳实践文章Ensemble Orphaned Messages | InterSystems Developer Community | Best DeleteHelper - A Class to Help with Deleting Referenced Persistent Classes (intersystems.com)本文并不是要取代 Intersystems 专业人员撰写的这些文章,而是要在此基础上介绍我们如何利用这些信息和其他讨论(包括我们实际清理这些数据的方法)来帮助我们的数据库变得更加紧凑。
情况说明:
我们的备份越来越多。年初的时候,我们遇到过一台服务器被强制故障的情况,需要进行还原。由于数据库庞大,即使复制这个数据库也需要很长时间,更不用说还原重建shadow服务器了。因此,我们不得不决定最终解决这一增长问题。最初的原因已经确定
开箱即用的任务或者在某些时候假定已运行,但没有勾选信息体。这是因为在查询其中一个消息体时,我们得到了来自 10 多年前的 ID 1。该任务是最佳实践中提到的默认 Ens.Util.Tasks.Purge。这就引出了流程中的提示 1
理解你的数据
您在数据库中存储了哪些数据?您是否有必须保存在记录表中的数据?对于您的事务数据,我指的是与message header相关联的报文,您可以使用以下方法进行查询
SELECT Distinct(MessageBodyClassName) from Ens.MessageHeader
最初,我会查看报文类,打开这些报文类,了解这些报文存储在哪个global中
这样,您就能在一定程度上了解数据的存储位置。
注意,如果您直接保存到数据流(如 %LIBRARY.GLOBALBINARYSTREAM)中,则表明存在孤儿,因为这些孤儿应保存到数据流容器中,我们稍后将介绍这一点
Running Global size report运行 GSize 是一目了然查看数据库大小的工具之一,它可以显示数据在数据库中的存储位置。在终端运行以下步骤
do ^%GSIZE
Directory name: NAMESAPCE/ =>
All Globals? No => YES
Show details => NO
这可以说明数据在哪里被用掉,从而为您提供指导。
步骤 2 阻止未来的孤儿
最佳实践指南中对此进行了很好的阐述,但我们还是对其中的步骤进行了细分。
查找直接发送缓存流的类,并将代码迁移到使用流容器的类。
查找未处理的嵌入类--添加手动删除和删除辅助工具
查看 hl7 消息的几个 %OnSaves
步骤 3 清理孤儿数据
每天运行清理任务
清理持久化流和其他杂项孤儿数据
使用直接流类的班级
Suriya Narayanan 指出,不应直接使用任何 %libary 类。如果不包含数据流,数据流最终会进入 ^Ens.Stream。这些数据不会以良好的引用方式存储,因此您必须在全局中查找您想要保留的最后数据。
在我们的场景中,是 BP 向操作发送消息,而不是将流添加到容器中。
set HTMLDocument=##class(%Library.GlobalBinaryStream).%New()
set tSC=..SendRequestSync(..DocmanRouterName,HTMLDocument, .aResponse,20,"")
//needed sent instead as container
set requestContainer = ##class(Ens.StreamContainer).%New()
set tSc=requestContainer.StreamSet(HTMLDocument)
set tSC=..SendRequestSync(..DocmanRouterName,requestContainer, .aResponse,20,"")
%Saves
并非所有在发送前 %save 的类都会导致问题,只有在没有发送的情况下才会。这种情况可能发生在修改经常保存的 hl7 副本时,也可能发生在临时保存但没有发送的情况下。下面是一个对 hl7 进行操作后没有保存的数据流示例,因此创建了 orpahans。有时,在清除了孤儿和监视器后,我就不会再创建孤儿了,下面的 %save 对象从未被使用过,因此是一个 orpahan,因为只发送了 hl7。在此之前有一个 %new 对象
嵌入式对象
下面的删除辅助文档就是一个例子。XML 信息在这方面是出了名的
Class Messages.XML.GenericWif.fileParameters Extends (%Persistent, %XML.Adaptor)
{
Property revisionNumber As %String;
Property primaryLink As Messages.XML.GenericWif.primaryLink;
Property additionalIndexes As Messages.XML.GenericWif.additionalIndexes;
在填充对象后,只有 Messages.XML.GenericWif.fileParameters 会被删除。
1) 进入每个对象,添加类方法和 OnDelete SQL 触发器,检查是否存在子对象并将其删除。可以使用 objectscript 或 sql 进行检查
ClassMethod %OnDelete(oid As %ObjectIdentity) As %Status [ Private ]
{
// Delete the property object references.
Set tSC = $$$OK, tThis = ##class(Messages.XML.GenericWif.fileParameters).%Open(oid)
If $ISOBJECT(tThis.primaryLink) Set tSC = ##class(Messages.XML.GenericWif.primaryLink).%DeleteId(tThis.primaryLink.%Id())
If $ISOBJECT(tThis.additionalIndexes) Set tSC = ##class(Messages.XML.GenericWif.additionalIndexes).%DeleteId(tThis.additionalIndexes.%Id())
Quit tSC
}
/// Callback/Trigger for SQL delete
Trigger OnDelete [ Event = DELETE ]
{
// Delete the property object references. {%%ID} holds the id of the record being deleted.
Set tID={%%ID}
Set tThis = ##class(Messages.XML.GenericWif.fileParameters).%OpenId(tID)
If $ISOBJECT(tThis.primaryLink) Do ##class(Messages.XML.GenericWif.primaryLink).%DeleteId(tThis.primaryLink.%Id())
If $ISOBJECT(tThis.additionalIndexes) Do ##class(Messages.XML.GenericWif.additionalIndexes).%DeleteId(tThis.additionalIndexes.%Id())
Quit
}
对于 objectscript,您可以打开其中一个 id,我们在测试系统或非生产系统上复制了该 id,例如,键入查询一条最旧的记录,然后在 sql 中查询其他表,例如 Messages_XML_GenericWif.primaryLink。然后,您可以查看是否可以打开其中的子 id。例如,您添加了代码,删除了包含嵌入式消息的 fileParameters 的 id 1。
set a = ##class(Messages.XML.GenericWif.primaryLink).%OpenId(2)
zw a
//this output the info. Now delete parent
set tSC=##class(Messages.XML.GenericWif.fileParameters).%DeleteId(1)
set a = ##class(Messages.XML.GenericWif.primaryLink).%OpenId(2)
zw a
//a should be ""
选项 2 是 deleteHelper。它的作用是在代码的 .int 中添加 %onDelete 类方法,而不是 sql。您只需在消息类的扩展中添加 deleteSuper 类,即
Class Messages.BoltonRenal.Pathology.Outbound.PathologyResult Extends (Ens.Request, SRFT.Utility.DeleteHelper.OnDeleteSuper)
{
Property requestingClinician As %String;
Property department As Messages.BoltonRenal.Pathology.Outbound.Department;
// the on deletesuper is a class like this in the link
include Ensemble
/// A class to help assist in "deep" deleting of an instance, including references to other persistent classes.
/// <br><br>
/// To use simply add as a Super Class in your persistent class<br>
/// The class defines a Generator %OnDelete method that will generate code for your class,
/// deleting, if needed, references (inclduing collections) to other persistent classes<br>
ClassMethod %OnDelete(oid As %ObjectIdentity) As %Status [ CodeMode = objectgenerator, Private, ServerOnly = 1 ]
{
// a list ($ListBuild format) of "simple" (non-collection) property names we'll want to delete the references of (because they're Persistent)
Set delPropNames = ""
如果您转到routine、消息和 .int,就会发现嵌入的对象开始被删除,例如:
清理消息
在这一切之后,我们编写了自己的类来清除 150 天之外的信息。我们的想法如下
在数据查找表中设置要删除的自定义邮件类型(升级版可以将其存储在表格中,使代码更简洁
手动设置删除最旧的 ens email正文。
创建全局数据以存储结果。
手动清理stream和其他残留物
再次提醒,请自行承担删除风险
我们的想法是,你有自己的报文正文类型列表(你的基础可能是 HL7 和报文正文详细信息)。
代码会在其中循环,并从报文头中获取最小 ID(对于报文正文,您需要查看保存到 messagebody 的最旧自定义报文),然后通过以下方式删除
if msgBodyName="Ens.MessageBody"{
set tMinMsgId=..MinBodyID
}else
{
set tMinMsgId=..GetMinimumIDForMessage(rs.MessageBodyClassName)
}
// Min ID is just basically this query set minIDQuery="SELECT TOP(1) MessageBodyID FROM Ens.MessageHeader where MessageBodyClassName=?"
And deletes it. Has added code around it to log
SET tSC1=$CLASSMETHOD(className,"%DeleteId",tResult.ID)
您可以尝试删除每种类型的数量。在运行 hl7 和报文体时,我们删除了 500000 个,而在运行报文体时,我们删除了 7200000 个。
同样,先在实时数据库的复刻环境(非生产或开发系统)上进行测试
您不希望删除过多内容导致journal过大,因此需要反复试验。
如使用后丢失所需数据,我不承担任何责任。
清理流数据
我们有一个小任务,可以用来直接杀死global stream。更新数字并运行 .NET Framework 3.0。要非常清楚你应该保留的最新数据流编号是多少,而且只有当你剩下的数据流是连续的时才有效
ClassMethod StreamPurge() As %Status
{
set i=1
while (i<200000){
k ^CacheStream(i)
s i=i+1
}
q $$$OK
}
文章
姚 鑫 · 二月 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)”)`。 想知道锁产生的原因大概有哪些?应该怎么避免呢? 我有个想法,希望结合应用场景来介绍,这样能够明白使用场景。
文章
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 技能会有所提高 — 但是如果您看到一些应该纠正的地方,请随意“教导”我!
如果您发现这些脚本有用,请告诉我,并不时回来看看以获取新功能和更新。
文章
姚 鑫 · 七月 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.
值得一篇专门的文章介绍客户应该怎么使用备份以及数据推流
文章
姚 鑫 · 七月 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` 用户。
文章
Michael Lei · 九月 20, 2023
根据剑桥词典的解释,令牌化数据是“用令牌(=代表第一个数据的不同数据)替换隐私数据,以防止隐私信息被不被允许做的人看到”(https://dictionary.cambridge.org/pt/dicionario/ingles/tokenize)。如今,一些公司,尤其是金融和医疗保健领域的公司,正在将其数据令牌/代币化作为满足网络安全和数据隐私(GDPR、CCPA、HIPAA 和 LGPD)要求的重要策略。但是,为什么不使用加密呢?保护敏感数据的令牌化过程比数据加密更常用,原因如下:
更好的性能:在密集的操作处理中动态加密和解密数据会提高性能并需要更多的处理器能力。
测试:可以标记生产数据库并复制到测试数据库,并维护适合更真实的单元和功能测试的测试数据。
更好的安全性:如果黑客破解或获得密钥,所有加密数据都将可用,因为加密是一个可逆过程。令牌化过程是不可逆的。如果您需要从令牌化数据中获取原始数据,则需要维护一个安全且独立的数据库来链接到原始数据和令牌化数据。
令牌化架构
令牌化架构需要两个数据库:应用程序数据库(App DB)用于存储令牌化数据和来自业务的其他数据;令牌数据库(Token Database)用于存储原始值和令牌化值,因此当您需要时,您的应用程序可以获得原始值以向用户显示。还有一个 令牌生成器 REST API 用于标记敏感数据、存储到令牌数据库中并返回票证。应用程序将票据、令牌化数据和其他数据存储在应用程序数据库中。参见架构图:
分词器应用程序
了解它在令牌化应用程序中的工作原理: https://openexchange.intersystems.com/package/Tokenizator。
该应用程序是一个用于标记化的 REST API:
对任何数值显示为“*”号。示例:信用卡 4450 3456 1212 0050 至 4450 **** **** 0050。
任何真实的IP地址都是假值。示例:192.168.0.1 到 168.1.1.1。
任何人的数据都是假的。示例:地址为北京海淀的张三到地址为西藏拉萨的仓央嘉措。
任何数值为假数值。示例:300.00 到 320.00。
任何信用卡数据都是虚假的信用卡号码。示例:4450 3456 1212 0050 至 4250 2256 4512 5050。
任何值到哈希值。示例:dfgdgasdrrrdd123 的系统架构师。
正则表达式的任何值。示例:使用正则表达式规则 [AZ]{2}-\\d{5}-[az]{5} 将 EI-54105-tjfdk 转换为 AI-44102-ghdfg。
如果您需要任何其他选项,请在 github 项目上提交问题。
要对值进行标记并在之后获取原始值,请按照以下步骤操作:
打开您的 Postman 或从您的应用程序使用此 API。
使用 STARS、PERSON、NUMBER、CREDITCARD、HASH、IPADDRESS 和 REGEX 方法对此敏感数据样本创建 令牌化请求:
方法:POST
网址: http://localhost:8080/token/tokenize
正文(JSON):
[
{
"tokenType":"STARS",
"originalValueString":"545049405679",
"settings": {
"starsPosition":"1",
"starsQuantity":"4"
}
},
{
"tokenType":"IPADDRESS",
"originalValueString":"192.168.0.1",
"settings": {
"classType":"CLASS_B",
"ipSize":"4"
}
},
{
"tokenType":"PERSON",
"originalValueString":"Yuri Marx Pereira Gomes",
"settings": {
"localeLanguage":"en",
"localeCountry":"US",
"withAddress":"true",
"withEmail":"true"
}
},
{
"tokenType":"NUMBER",
"originalValueNumber":300.0,
"settings": {
"minRange":"100.0",
"maxRange":"400.0"
}
},
{
"tokenType":"CREDITCARD",
"originalValueString":"4892879268763190",
"settings": {
"type":"VISA"
}
},
{
"tokenType":"HASH",
"originalValueString":"System Architect"
},
{
"tokenType":"REGEX",
"originalValueString":"EI-54105-tjfdk",
"settings": {
"regex":"[A-Z]{2}-\\d{5}-[a-z]{5}"
}
}
]
查看结果。您将获得一个令牌化的数值 (tokenizedValueString) 并将其存储到本地数据库中。
从响应中复制票证(将其与令牌化化值一起存储到本地数据库中)
现在凭票证即可获得原数值。创建一个请求以使用票证获取原始值:
方法:获取
网址: http://localhost:8080/token/query-ticket/TICKET-VALUE-HERE
查看您的原始值
生成的所有令牌都存储到 InterSystems IRIS Cloud SQL 中,以便您能够可靠地获得原始值。
欢迎体验!
文章
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
截图:
文章
Lilian Huang · 九月 7, 2023
您好!社区的各位老师,
在我的上一篇文章中,我们学习了以下主题:
什么是 Docker?
Docker 的一些好处
Docker 是如何工作的?
Docker 镜像
Docker容器
Docker 镜像存储库
InterSystems 的 Docker 镜像存储库
Docker安装
Docker 基本命令
使用 Docker 运行 IRIS 社区版
Docker 桌面图形用户界面
在本文中,我们将讨论以下主题:
使用 Docker Compose 文件( YAML 文件)
Docker 文件的使用(用于构建 Docker 镜像)
Docker 卷的使用
那么让我们开始吧。
1.使用Docker Compose文件( YAML文件)
Docker Compose 是一款旨在帮助定义和共享多容器应用程序的工具。使用 Compose,我们可以创建一个 YAML 文件来定义服务,并且通过单个命令,我们可以启动或拆除所有内容。
使用 Compose 的一大优势是能够在文件中定义应用程序堆栈并将其保存在项目存储库的根目录中。您还可以让其他人轻松地为您的项目做出贡献。 他们只需要克隆您的存储库并启动撰写应用程序。
在我的上一篇文章中,我们使用下面提到的命令来创建并启动带有 InterSystems 社区镜像的容器:
docker run -d -p 52773:52773 intersystemsdc/iris-community
此时,让我们修改此命令并添加容器名称、映射更多端口并设置重新启动选项:
docker run -d -p 52773:52773 -p 53773:53773 -p 1972:1972 --name iris --restart=always intersystemsdc/iris-community
让我为您分解一下上述命令:
# docker run command to create and start the container docker run # -d -an option used to start container in deattach mode -d # -p -an option is used to map the ports -p 52773:52773 -p 53773:53773 -p 1972:1972 # name of the container --name iris # set the restart option to always --restart=always # base image intersystemsdc/iris-community
创建撰写文件
在根文件夹中,创建一个名为-----100-----的文件,并写入上述命令,如下所示:
#specify docker-compose version version: '3.6' #services/container details services: #Name of the container iris: #Base Image image: intersystemsdc/iris-community #set restart option restart: always #port mapping ports: - 55036 :1972 - 55037 :52773 - 53773 :53773
docker run 命令和 docker-compose 文件的映射如下所示:Docker-compose 文件快照如下所示:
为了运行 docker-compose 文件代码,我们将使用 docker-compose up 命令:
docker-compose up -d
-----101----- 或 -----102-----:在后台运行命令并将控制返回到终端的选项。
容器已启动。让我们运行“docker ps”命令来列出正在运行的容器
正如您所看到的,我们使用 docker-compose 文件得到了相同的结果。
创建并启动多个容器
在 docker-compose 的帮助下,我们不仅可以运行多个容器,还可以组织并向其添加更多命令。
例如,在下面的 docker-compose 文件中,我们运行 MongoDB 容器和 iris 容器:
#specify docker-compose version version: '3.6' #services/container details services: #Name of the container iris: #Base Image image: intersystemsdc/iris-community #set restart option restart: always #port mapping ports: - 55036:1972 - 55037:52773 - 53773:53773 #start MongoDB container mongodb: image: mongo ports: - 27017:27017
让我们运行 docker-compose up 命令
MongoDB 和 iris 容器现已创建并启动。
2.Docker 文件
Docker可以通过读取-----103-----的指令来自动构建镜像。 -----103----- 是一个文本文档,其中包含用户可以在命令行上调用来组装图像的所有命令。
所以,我们的第一个问题很简单。什么是 Dockerfile? Docker 使用它来构建镜像本身。 Dockerfile 本质上是如何创建镜像的构建指令。
与简单存储二进制映像相比,Dockerfile 的优点是自动构建将确保您拥有可用的最新版本。从安全角度来看这是一件好事,因为您希望确保没有安装任何易受攻击的软件。
常用 Docker 文件命令
您可以在下面找到一些最常用的 docker 命令。请注意,所有 docker 命令都必须大写字母。
从
第一个是 FROM 命令,它告诉您映像基于什么。正是这种多层方法使得 Docker 如此高效和强大。在本例中,使用了 iris-community Docker 映像,它再次引用 Dockerfile 来自动化构建过程。
FROM intersystemsdc/iris-community
工作目录
该命令用于设置一个工作目录,我们将在其中复制文件。例如,下面提到的命令将设置 /opt/irisbuild 作为工作目录:
WORKDIR /opt/irisbuild
复制
COPY 命令就像听起来一样简单。它可以将文件复制到容器中。通常,我们复制自定义配置文件、应用程序源文件、数据文件等。
#coping Installer.cls to a root of workdir. Don't miss the dot, here. COPY Installer.cls . #copy source files from src folder to src folder in workdir in the docker container. COPY src src #copy source files from data/fhir folder to fhirdata folder in the docker container. COPY data/fhir fhirdata
环境电压
这会设置环境变量,这些变量可以在 Dockerfile 及其调用的任何脚本中使用。-----105-----指令将环境变量-----106-----定义为值-----107-----
ENV USER_ID "SYSTEM" ENV USER_PASSWORD "MANAGER"
跑步
-----108-----命令用于在镜像构建过程中执行命令。
#here we give the rights to irisowner user and group which are run IRIS. RUN chown ${ISC_PACKAGE_MGRUSER} : ${ISC_PACKAGE_IRISGROUP} /opt/irisapp #start IRIS and run script in iris.script file RUN iris start IRIS \ && iris session IRIS < /tmp/iris.script
用户
默认情况下,容器以 root 身份运行,这使它们能够完全控制主机系统。随着容器技术的成熟,可能会出现更安全的默认选项。目前,要求 root 对其他人来说是危险的,并且可能不适用于所有环境。您的映像应使用 -----109----- 指令来指定容器运行的非 root用户。如果您的软件没有创建自己的用户,您可以在 Dockerfile 中创建用户和组。
#here we switch user to a root to create a folder and copy files in docker. USER root WORKDIR /opt/irisapp #switching user from root to irisowner, to copy files USER irisowner COPY src src
更多细节请阅读Docker官方文档
3.Docker卷
Docker卷是一个完全由Docker管理的独立文件系统。它作为标准文件或目录存在于数据保存的主机上。
使用 Docker 卷的目的是将数据保留在容器外部,以便可用于备份或共享。
Docker 卷依赖于 Docker 的文件系统,是 Docker 容器和服务保存数据的首选方法。当容器启动时,Docker 会加载只读镜像层,在镜像堆栈顶部添加读写层,并将卷挂载到容器文件系统上。
我们使用 -----110----- 或 -----111----- 标志来允许我们将本地文件挂载到容器中。
卷是保存 Docker 容器生成和使用的数据的首选机制。虽然绑定挂载取决于主机的目录结构和操作系统,但卷完全由 Docker 管理。与绑定挂载相比,卷有几个优点:
卷比绑定安装更容易备份或迁移。
您可以使用 Docker CLI 命令或 Docker API 管理卷。
卷适用于 Linux 和 Windows 容器。
卷可以在多个容器之间更安全地共享。
卷驱动程序允许您将卷存储在远程主机或云提供商上,以加密卷的内容或添加其他功能。
新卷的内容可以由容器预先填充。
Docker Desktop 上的卷比 Mac 和 Windows 主机上的绑定挂载具有更高的性能。
此外,卷通常是比将数据保留在容器的可写层中更好的选择,因为卷在使用时不会增加容器的大小。此外,卷的内容存在于给定容器的生命周期之外。
我们可以在 docker-compose 文件的 services 部分下提及卷。
#specify docker-compose version version: '3.6' #services/container details services: #Name of the container iris: #Base Image image: intersystemsdc/iris-community #set restart option restart: always #port mapping ports: - 55036 :1972 - 55037 :52773 - 53773 :53773 #create a volume volumes: - ./:/irisdev/app
我们已经创建了 irisdev/app 卷。
概括
Docker 是一个功能强大的工具,允许开发人员和 IT 团队在容器化环境中创建、部署和运行应用程序。通过提供可移植性、一致性、可扩展性、资源效率和安全性,Docker 可以轻松地跨不同环境和基础设施部署应用程序。随着容器化的日益普及,Docker正在成为现代软件开发和部署的重要工具。在本文中,我们学习了如何使用 Docker compose(一个 YAML 文件,指定应用程序中每个容器的配置选项)、Docker 文件(用于构建 Docker 镜像)和 Docker 卷(一种用于共享数据的持久数据存储机制) Docker 容器和主机之间的数据。)
感谢您的阅读!