清除过滤器
文章
Michael Lei · 六月 1, 2022
糖尿病可以从医学界熟知的一些参数中发现。这样,为了帮助医学界和计算机软件系统,特别是人工智能软件,美国国家糖尿病和消化道及肾脏疾病研究所发布了一个非常有用的数据集,用于训练糖尿病检测/预测的机器学习算法。这份出版物可以在最大和最知名的ML数据库Kaggle上找到,网址是https://www.kaggle.com/datasets/mathchi/diabetes-data-set。
该糖尿病数据集有以下元数据信息(来源:https://www.kaggle.com/datasets/mathchi/diabetes-data-set):
怀孕:怀孕次数
葡萄糖: 口服葡萄糖耐量试验中2小时的血浆葡萄糖浓度 Plasma glucose concentration a 2 hours in an oral glucose tolerance test
血压: 舒张压(mm Hg)
皮肤厚度: 肱三头肌皮褶厚度(mm)
胰岛素: 2小时血清胰岛素(mu U/ml)
BMI: 体重指数 (体重 kg/(身高 m)^2)
糖尿病血统函数: 糖尿病血统函数(它提供了一些关于亲属中的糖尿病史以及这些亲属与病人的遗传关系的数据。这种对遗传影响的测量使我们了解到一个人可能有的遗传风险与糖尿病的发病有关--来源:https://machinelearningmastery.com/case-study-predicting-the-onset-of-diabetes-within-five-years-part-1-of-3/)
年龄:
结果: 类变量 (0 or 1)
实例数量: 768
属性数量: 8 + 1个类变量
对每个属性: (全部为numeric数字量化类型)
怀孕次数
口服葡萄糖耐量试验中2小时的血浆葡萄糖浓度
舒张压 (mm Hg)
三头肌皮褶厚度 (mm)
2小时血清胰岛素 (mu U/ml)
BMI指数 (体重 kg/(身高 m)^2)
糖尿病血统函数
年龄
类变量 (0 or 1)
缺失属性值: 是
类分布: (类值为1解释为 "糖尿病测试阳性")
从Kaggle获取糖尿病数据
Kaggle的糖尿病数据可以通过Health-Dataset程序加载到IRIS表中:https://openexchange.intersystems.com/package/Health-Dataset。要做到这一点,在你的module.xml项目中,设置依赖关系(Health Dataset的ModuleReference)。
Module.xml with Health Dataset application reference
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="predict-diseases.ZPM">
<Module>
<Name>predict-diseases</Name>
<Version>1.0.0</Version>
<Packaging>module</Packaging>
<SourcesRoot>src/iris</SourcesRoot>
<Resource Name="dc.predict.disease.PKG"/>
<Dependencies>
<ModuleReference>
<Name>swagger-ui</Name>
<Version>1.*.*</Version>
</ModuleReference>
<ModuleReference>
<Name>dataset-health</Name>
<Version>*</Version>
</ModuleReference>
</Dependencies>
<CSPApplication
Url="/predict-diseases"
DispatchClass="dc.predict.disease.PredictDiseaseRESTApp"
MatchRoles=":{$dbrole}"
PasswordAuthEnabled="1"
UnauthenticatedEnabled="1"
Recurse="1"
UseCookies="2"
CookiePath="/predict-diseases"
/>
<CSPApplication
CookiePath="/disease-predictor/"
DefaultTimeout="900"
SourcePath="/src/csp"
DeployPath="${cspdir}/csp/${namespace}/"
MatchRoles=":{$dbrole}"
PasswordAuthEnabled="0"
Recurse="1"
ServeFiles="1"
ServeFilesTimeout="3600"
UnauthenticatedEnabled="1"
Url="/disease-predictor"
UseSessionCookie="2"
/>
</Module>
</Document>
</Export>
预测糖尿病的前端和后端应用程序
访问 Open Exchange 应用连接 (https://openexchange.intersystems.com/package/Disease-Predictor) 并遵守以下步骤:
Clone/git 把repo pull 到任何本地目录
$ git clone https://github.com/yurimarx/predict-diseases.git
打开 该目录下Docker终端并执行:
$ docker-compose build
执行IRIS container:
$ docker-compose up -d
在管理门户中执行查询来训练AI模型: http://localhost:52773/csp/sys/exp/%25CSP.UI.Portal.SQL.Home.zen?$NAMESPACE=USER
创建用来训练的 VIEW :
CREATE VIEW DiabetesTrain AS SELECT Outcome, age, bloodpressure, bmi, diabetespedigree, glucose, insulin, pregnancies, skinthickness FROM dc_data_health.Diabetes
利用View视图来创建 AI 模型:
CREATE MODEL DiabetesModel PREDICTING (Outcome) FROM DiabetesTrain
训练模型:
TRAIN MODEL DiabetesModel
访问 http://localhost:52773/disease-predictor/index.html 来使用疾病预测器qian frontend and predict diseases like this:
幕后工作
后端预测糖尿病的类方法
InterSystems IRIS 支持执行Select 并使用上一个创建的模型来预测。
Backend ClassMethod to predict Diabetes
/// Predict Diabetes
ClassMethod PredictDiabetes() As %Status
{
Try {
Set data = {}.%FromJSON(%request.Content)
Set qry = "SELECT PREDICT(DiabetesModel) As PredictedDiabetes, "
_"age, bloodpressure, bmi, diabetespedigree, glucose, insulin, "
_"pregnancies, skinthickness "
_"FROM (SELECT "_data.age_" AS age, "
_data.bloodpressure_" As bloodpressure, "
_data.bmi_" AS bmi, "
_data.diabetespedigree_" AS diabetespedigree, "
_data.glucose_" As glucose, "
_data.insulin_" AS insulin, "
_data.pregnancies_" As pregnancies, "
_data.skinthickness_" AS skinthickness)"
Set tStatement = ##class(%SQL.Statement).%New()
Set qStatus = tStatement.%Prepare(qry)
If qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT}
Set rset = tStatement.%Execute()
Do rset.%Next()
Set Response = {}
Set Response.PredictedDiabetes = rset.PredictedDiabetes
Set Response.age = rset.age
Set Response.bloodpressure = rset.bloodpressure
Set Response.bmi = rset.bmi
Set Response.diabetespedigree = rset.diabetespedigree
Set Response.glucose = rset.glucose
Set Response.insulin = rset.insulin
Set Response.pregnancies = rset.pregnancies
Set Response.skinthickness = rset.skinthickness
Set %response.Status = 200
Set %response.Headers("Access-Control-Allow-Origin")="*"
Write Response.%ToJSON()
Return 1
} Catch err {
write !, "Error name: ", ?20, err.Name,
!, "Error code: ", ?20, err.Code,
!, "Error location: ", ?20, err.Location,
!, "Additional data: ", ?20, err.Data, !
Return 0
}
}
现在,任何网络应用都可以使用预测并显示预测结果。欢迎在frontend 文件夹查看本应用的源代码。
文章
Michael Lei · 九月 13, 2022
Globals是InterSystems IRIS的数据持久性的核心。它很灵活,允许存储JSON文档、关系数据、面向对象的数据、OLAP立方体和自定义数据模型,例如思维导图。要了解如何使用globals来存储、删除和获取思维导图数据,请遵循以下步骤:
1. 把repo Clone/git到任意本地目录
$ git clone https://github.com/yurimarx/global-mindmap.git
2. 在该目录下打开Docker 终端并执行:
$ docker-compose build
3. 启动 IRIS 容器:
$ docker-compose up -d
4. 访问 http://localhost:3000 来使用思维导图的前端并创建类似以上的思维导图
本例子的源代码
存储数据 (更多请访问: https://www.npmjs.com/package/mind-elixir):
{
topic: 'node topic',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
parent: null,
tags: ['Tag'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
}
注意parent属性,它被用来在mindmap节点之间建立父/子关系。
使用Globals 来存储思维导图的源代码
ClassMethod StoreMindmapNode
/// Store mindmap node
ClassMethod StoreMindmapNode() As %Status
{
Try {
Set data = {}.%FromJSON(%request.Content)
Set ^mindmap(data.id) = data.id /// set mindmap key
Set ^mindmap(data.id, "topic") = data.topic /// set topic subscript
Set ^mindmap(data.id, "style", "fontSize") = data.style.fontSize /// set style properties subscripts
Set ^mindmap(data.id, "style", "color") = data.style.color
Set ^mindmap(data.id, "style", "background") = data.style.background
Set ^mindmap(data.id, "parent") = data.parent /// store parent id subscript
Set ^mindmap(data.id, "tags") = data.tags.%ToJSON() /// store tags subscript
Set ^mindmap(data.id, "icons") = data.icons.%ToJSON() /// store icons subscript
Set ^mindmap(data.id, "hyperLink") = data.hyperLink /// store hyperLink subscript
Set %response.Status = 200
Set %response.Headers("Access-Control-Allow-Origin")="*"
Write "Saved"
Return $$$OK
} Catch err {
write !, "Error name: ", ?20, err.Name,
!, "Error code: ", ?20, err.Code,
!, "Error location: ", ?20, err.Location,
!, "Additional data: ", ?20, err.Data, !
Return $$$NOTOK
}
}
我们创建了一个名为^mindmap的Global。对于每个思维导图的属性,它被存储在一个Globals下标中。下标的键是mindmap的id属性。
删除思维导图节点的源代码 - kill the global
ClassMethod DeleteMindmapNode
/// Delete mindmap node
ClassMethod DeleteMindmapNode(id As %String) As %Status
{
Try {
Kill ^mindmap(id) /// delete selected mindmap node using the id (global key)
Set %response.Status = 200
Set %response.Headers("Access-Control-Allow-Origin")="*"
Write "Deleted"
Return $$$OK
} Catch err {
write !, "Error name: ", ?20, err.Name,
!, "Error code: ", ?20, err.Code,
!, "Error location: ", ?20, err.Location,
!, "Additional data: ", ?20, err.Data, !
Return $$$NOTOK
}
}
这个例子使用mindmap.id作为mindmap的Global Key,所以删除很容易: call Kill ^mindmap(<mindmap id>)
获得所有存储内容的源代码- 用 $ORDER循环globals
ClassMethod GetMindmap - return all mindmap global nodes
/// Get mindmap content
ClassMethod GetMindmap() As %Status
{
Try {
Set Nodes = []
Set Key = $Order(^mindmap("")) /// get the first mindmap node stored - the root
Set Row = 0
While (Key '= "") { /// while get child mindmap nodes
Do Nodes.%Push({}) /// create a item into result
Set Nodes.%Get(Row).style = {}
Set Nodes.%Get(Row).id = Key /// return the id property
Set Nodes.%Get(Row).hyperLink = ^mindmap(Key,"hyperLink") /// return the hyperlink property
Set Nodes.%Get(Row).icons = ^mindmap(Key,"icons") /// return icons property
Set Nodes.%Get(Row).parent = ^mindmap(Key,"parent") /// return parent id property
Set Nodes.%Get(Row).style.background = ^mindmap(Key,"style", "background") /// return the style properties
Set Nodes.%Get(Row).style.color = ^mindmap(Key,"style", "color")
Set Nodes.%Get(Row).style.fontSize = ^mindmap(Key,"style", "fontSize")
Set Nodes.%Get(Row).tags = ^mindmap(Key,"tags") /// return tags property
Set Nodes.%Get(Row).topic = ^mindmap(Key,"topic") /// return topic property (title mindmap node)
Set Row = Row + 1
Set Key = $Order(^mindmap(Key)) /// get the key to the next mindmap global node
}
Set %response.Status = 200
Set %response.Headers("Access-Control-Allow-Origin")="*"
Write Nodes.%ToJSON()
Return $$$OK
} Catch err {
write !, "Error name: ", ?20, err.Name,
!, "Error code: ", ?20, err.Code,
!, "Error location: ", ?20, err.Location,
!, "Additional data: ", ?20, err.Data, !
Return $$$NOTOK
}
}
用$Order(^mindmap("")) - empty "" - 得到第一个mindmap Global (根节点)。对于每个属性值,我们使用^mindmap(Key,<property name>)。最后,调用$Order(^mindmap(Key))来获得下一个事件。
前端
Mind-elixir和React被用来渲染和编辑mindmap,消耗使用IRIS构建的API后端。见mindmap的反应组件:
Mindmap React component - consuming IRIS REST API
import React from "react";
import MindElixir, { E } from "mind-elixir";
import axios from 'axios';
class Mindmap extends React.Component {
componentDidMount() {
this.dynamicWidth = window.innerWidth;
this.dynamicHeight = window.innerHeight;
axios.get(`http://localhost:52773/global-mindmap/hasContent`)
.then(res => {
if (res.data == "1") {
axios.get(`http://localhost:52773/global-mindmap/get`)
.then(res2 => {
this.ME = new MindElixir({
el: "#map",
direction: MindElixir.LEFT,
data: this.renderExistentMindmap(res2.data),
draggable: true, // default true
contextMenu: true, // default true
toolBar: true, // default true
nodeMenu: true, // default true
keypress: true // default true
});
this.ME.bus.addListener('operation', operation => {
console.log(operation)
if (operation.name == 'finishEdit' || operation.name == 'editStyle') {
this.saveMindmapNode(operation.obj)
} else if (operation.name == 'removeNode') {
this.deleteMindmapNode(operation.obj.id)
}
})
this.ME.init();
})
} else {
this.ME = new MindElixir({
el: "#map",
direction: MindElixir.LEFT,
data: MindElixir.new("New Mindmap"),
draggable: true, // default true
contextMenu: true, // default true
toolBar: true, // default true
nodeMenu: true, // default true
keypress: true // default true
});
this.ME.bus.addListener('operation', operation => {
console.log(operation)
if (operation.name == 'finishEdit' || operation.name == 'editStyle') {
this.saveMindmapNode(operation.obj)
} else if (operation.name == 'removeNode') {
this.deleteMindmapNode(operation.obj.id)
}
})
this.saveMindmapNode(this.ME.nodeData)
this.ME.init();
}
})
}
render() {
return (
<div id="map" style={{ height: window.innerHeight + 'px', width: '100%' }} />
);
}
deleteMindmapNode(mindmapNodeId) {
axios.delete(`http://localhost:52773/global-mindmap/delete/${mindmapNodeId}`)
.then(res => {
console.log(res);
console.log(res.data);
})
}
saveMindmapNode(node) {
axios.post(`http://localhost:52773/global-mindmap/save`, {
topic: (node.topic == undefined ? "" : node.topic),
id: node.id,
style: (node.style == undefined ? "" : node.style),
parent: (node.parent == undefined ? "" : node.parent.id),
tags: (node.tags == undefined ? [] : node.tags),
icons: (node.icons == undefined ? [] : node.icons),
hyperLink: (node.hyperLink == undefined ? "" : node.hyperLink)
})
.then(res => {
console.log(res);
console.log(res.data);
})
}
renderExistentMindmap(data) {
let root = data[0]
let nodeData = {
id: root.id,
topic: root.topic,
root: true,
style: {
background: root.style.background,
color: root.style.color,
fontSize: root.style.fontSize,
},
hyperLink: root.hyperLink,
children: []
}
this.createTree(nodeData, data)
return { nodeData }
}
createTree(nodeData, data) {
for(let i = 1; i < data.length; i++) {
if(data[i].parent == nodeData.id) {
let newNode = {
id: data[i].id,
topic: data[i].topic,
root: false,
style: {
background: data[i].style.background,
color: data[i].style.color,
fontSize: data[i].style.fontSize,
},
hyperLink: data[i].hyperLink,
children: []
}
nodeData.children.push(newNode)
this.createTree(newNode, data)
}
}
}
}
export default Mindmap;
文章
Lilian Huang · 七月 9, 2023
#Embedded Python #HL7 #InterSystems IRIS for Health
写在回复社区帖子《Python能否动态创建HL7消息》中。
前提条件和设置
使用一个启用了集成的命名空间。注意:USER命名空间默认不启用互操作性。如果以下建议创建一个新的互操作性命名空间来探索功能。
# 切换到ZN "[互操作性名称空间名称]"
# 启动交互式Python shell:Do $SYSTEM.Python.Shell()
启动脚本
#Load dependencies
import datetime as dt
import uuid
# Cache current time in CCYYMMDDHHMMss format
hl7_datetime_now=dt.datetime.now().strftime('%Y%m%d%H%M%S')
# Create HL7 Message
hl7=iris.cls("EnsLib.HL7.Message")._New()
# Set the doc type
# 2.5.1:ORU_R01 - Unsolicited transmission of an observation message
hl7.PokeDocType("2.5.1:ORU_R01")
这些信息的结构可以从管理门户中获取
创建MSH(消息头段)。
// MSH Segment
hl7.SetValueAt('OutApp','MSH:SendingApplication')
hl7.SetValueAt('OutFac','MSH:SendingFacility')
hl7.SetValueAt('InApp','MSH:ReceivingApplication')
hl7.SetValueAt('InFac','MSH:ReceivingFacility')
hl7.SetValueAt(hl7_datetime_now,'MSH:DateTimeOfMessage')
hl7.SetValueAt('ORU','MSH:MessageType.MessageCode')
hl7.SetValueAt('R01','MSH:MessageType.TriggerEvent')
hl7.SetValueAt('ORU_R01','MSH:MessageType.MessageStructure')
hl7.SetValueAt(str(uuid.uuid4()),'MSH:MessageControlID')
hl7.SetValueAt('2.5.1','MSH:ProcessingID')
编码和解码
HL7文件被格式化为段每个段被分隔符("|")和重复元素("~")划分为多个元素在一个元素内有"^"分界符和"&"子分界符。当定界符作为实际的文本内容出现时,它将被"\"和其他取代定界符的字符转义。通常,"&"是有问题的,因为它可能经常出现在信息中,导致接收系统读取时出现截断现象。HL7段有一个内置的方法,用于根据当前为信息选择的定界符来转义内容。一个常见的模式是获得对第一个段的引用
# Do this line the variable "msh" is used later
> msh=hl7.GetSegmentAt(1)
然后可以调用Escape,例如用Python的原始字符串:
> msh.Escape(r"a&b~c^d")
'a\\T\\b\\R\\c\\S\\d'
The segment can also be used to Unescape back for example:
> msh.Unescape('a\\T\\b\\R\\c\\S\\d')
'a&b~c^d'
因此,在设置预计包含分隔符的内容时,可以为信息转义这些分隔符
hl7.SetValueAt(msh.Escape(r"a&b~c^d"),'MSH:ReceivingFacility')
检索内容时可以不加转义
msh.Unescape(hl7.GetValueAt('MSH:ReceivingFacility'))
在这个例子中,只是将msh重新设置为以前的值
hl7.SetValueAt('InFac','MSH:ReceivingFacility')
仔细检查到目前为止的部分:
> hl7.GetValueAt('MSH')
'MSH|^~\\&|OutApp|OutFac|InApp|InFac|20230610100040||ORU^R01^ORU_R01|2dfab415-51aa-4c75-a7e7-a63aedfb53cc|2.5.1'
人口统计学(PID)部分
# Virtual path prefix for PID
seg='PIDgrpgrp(1).PIDgrp.PID:'
hl7.SetValueAt('1',seg+'SetIDPID')
hl7.SetValueAt('12345',seg+'PatientIdentifierList(1).IDNumber')
hl7.SetValueAt('MRN',seg+'PatientIdentifierList(1).AssigningAuthority')
hl7.SetValueAt('MR',seg+'PatientIdentifierList(1).IdentifierTypeCode')
hl7.SetValueAt(msh.Escape('Redfield'), seg+'PatientName(1).FamilyName')
hl7.SetValueAt(msh.Escape('Claire') ,seg+'PatientName(1).GivenName')
hl7.SetValueAt('19640101',seg+'DateTimeofBirth')
hl7.SetValueAt('F',seg+'AdministrativeSex')
hl7.SetValueAt(msh.Escape('Umbrella Corporation') ,seg+'PatientAddress.StreetAddress')
hl7.SetValueAt(msh.Escape('Umbrella Drive') ,seg+'PatientAddress.OtherDesignation')
hl7.SetValueAt(msh.Escape('Raccoon City') ,seg+'PatientAddress.City')
hl7.SetValueAt(msh.Escape('MO') ,seg+'PatientAddress.StateorProvince')
hl7.SetValueAt(msh.Escape('63117') ,seg+'PatientAddress.ZiporPostalCode')
仔细检查PID段的内容
> hl7.GetValueAt(seg[0:-1])
'PID|1||12345^^^MRN^MR||Redfield^Claire||19640101|F|||Umbrella Corporation^Umbrella Drive^Raccoon City^MO^63117'
订单控制部分
seg='PIDgrpgrp(1).ORCgrp(1).ORC:'
hl7.SetValueAt('RE',seg+'OrderControl')
hl7.SetValueAt('10003681',seg+'PlacerOrderNumber')
hl7.SetValueAt('99001725',seg+'FillerOrderNumber')
hl7.SetValueAt('AG104',seg+'OrderingProvider')
hl7.SetValueAt('L43',seg+'EnterersLocation')
仔细检查ORC部分的内容
> hl7.GetValueAt(seg[0:-1])
'ORC|RE|10003681|99001725|||||||||AG104|L43'
观察请求
seg='PIDgrpgrp(1).ORCgrp(1).OBR:'
hl7.SetValueAt('1',seg+'SetIDOBR')
hl7.SetValueAt('10003681',seg+'PlacerOrderNumber')
hl7.SetValueAt('99001725',seg+'FillerOrderNumber')
hl7.SetValueAt('20210428100729',seg+'ResultsRptStatusChngDateTime')
hl7.SetValueAt('F',seg+'ResultStatus')
hl7.SetValueAt('U',seg+'QuantityTiming.Priority')
OBX 观察/结果
seg='PIDgrpgrp(1).ORCgrp(1).OBXgrp(1).OBX:'
hl7.SetValueAt('1',seg+'SetIDOBX')
hl7.SetValueAt('TX',seg+'ValueType')
hl7.SetValueAt('V8132',seg+'ObservationIdentifier.Identifier')
hl7.SetValueAt(msh.Escape('G-Virus') , seg+'ObservationIdentifier.Identifier')
hl7.SetValueAt(msh.Escape('17.8 log10') ,seg+'ObservationValue')
hl7.SetValueAt(msh.Escape('RNA copies/mL') ,seg+'Units')
hl7.SetValueAt('F',seg+'ObservationResultStatus')
hl7.SetValueAt('20210428100729',seg+'DateTimeoftheObservation')
hl7.SetValueAt('AG001',seg+'ResponsibleObserver.IDNumber')
hl7.SetValueAt('Birkin',seg+'ResponsibleObserver.FamilyName')
hl7.SetValueAt('William',seg+'ResponsibleObserver.GivenName')
hl7.SetValueAt('AG001',seg+'ResponsibleObserver.IDNumber')
hl7.SetValueAt('UXL43',seg+'EquipmentInstanceIdentifier')
NTE - 注释和评论
seg='PIDgrpgrp(1).ORCgrp(1).OBXgrp(1).NTE(1):'
hl7.SetValueAt('1',seg+'SetIDNTE')
hl7.SetValueAt(msh.Escape('Expected late onset Hyphema. Contain but do not approach.') ,seg+'Comment')
向终端打印全部信息
> print(hl7.OutputToString())
MSH|^~\&|OutApp|OutFac|InApp|InFac|20230610141201||ORU^R01^ORU_R01|2dfab415-51aa-4c75-a7e7-a63aedfb53cc|2.5.1
PID|1||12345^^^MRN^MR||Redfield^Claire||19640101|F|||Umbrella Corporation^Umbrella Drive^Raccoon City^MO^63117
ORC|RE|10003681|99001725|||||||||AG104|L43
OBR|1|10003681|99001725|||||||||||||||||||20210428100729|||F||^^^^^U
OBX|1|TX|G-Virus||17.8 log10|RNA copies/mL|||||F|||20210428100729||AG001^Birkin^William||UXL43
NTE|1||Expected late onset Hyphema. Contain but do not approach.
陷阱
如果一个元素的内容包含一个诸如 "8@%SYS.Python "的值,很可能是需要用字符串值或字符串属性来代替。
例如,uuid在MSH结构中被 "str "包裹着。
原文请查看 来自 Alex Woodhead
https://community.intersystems.com/post/dynamically-creating-hl7-message-iris-embedded-python
文章
姚 鑫 · 四月 25, 2021
# 第五章 优化查询性能(四)
# 注释选项
可以在`SELECT`、`INSERT`、`UPDATE`、`DELETE`或`TRUNCATE`表命令中为查询优化器指定一个或多个注释选项。
注释选项指定查询优化器在编译SQL查询期间使用的选项。
通常,注释选项用于覆盖特定查询的系统范围默认配置。
## 语法
语法`/*#OPTIONS */`(在`/*`和`#`之间没有空格)指定了一个注释选项。
注释选项不是注释;
它为查询优化器指定一个值。
注释选项使用`JSON`语法指定,通常是`“key:value”`对,例如: `/*#OPTIONS {"optionName":value} */`。
支持更复杂的JSON语法,比如嵌套值。
注释选项不是注释;
除了`JSON`语法之外,它可能不包含任何文本。
包含非`json`文本在`/* ... */`分隔符导致`SQLCODE -153`错误。
InterSystems SQL不验证`JSON`字符串的内容。
`#OPTIONS`关键字必须用大写字母指定。
`JSON`的大括号语法中不应该使用空格。
如果SQL代码用引号括起来,比如动态SQL语句,JSON语法中的引号应该是双引号。
例如:`myquery="SELECT Name FROM Sample.MyTest /*#OPTIONS {""optName"":""optValue""} */"`.
可以在SQL代码中任何可以指定注释的地方指定`/*#OPTIONS */` comment选项。
在显示的语句文本中,注释选项总是作为注释显示在语句文本的末尾。
你可以在SQL代码中指定多个`/*#OPTIONS */` comment选项。
它们按照指定的顺序显示在返回的语句文本中。
如果为同一个选项指定了多个注释选项,则使用`last`指定的选项值。
以下的注释选项被记录在案:
- `/*#OPTIONS {"BiasAsOutlier":1} */`
- `/*#OPTIONS {"DynamicSQLTypeList":"10,1,11"}`
- `/*#OPTIONS {"NoTempFile":1} */`
## 显示
`/*#OPTIONS */` comment选项显示在SQL语句文本的末尾,而不管它们是在SQL命令中指定的位置。
一些显示的`/*#OPTIONS */` comment选项没有在SQL命令中指定,而是由编译器的预处理器生成的。
例如 `/*#OPTIONS {"DynamicSQLTypeList": ...} */`
`/*#OPTIONS */` comment选项显示在`Show Plan`语句文本、缓存的查询查询文本和SQL语句语句文本中。
为仅在`/*#OPTIONS */` comment选项中不同的查询创建一个单独的缓存查询。
# 并行查询处理
并行查询提示指示系统在多处理器系统上运行时执行并行查询处理。
这可以极大地提高某些类型查询的性能。
SQL优化器确定一个特定的查询是否可以从并行处理中受益,并在适当的时候执行并行处理。
指定并行查询提示并不强制对每个查询进行并行处理,只强制那些可能从并行处理中受益的查询。
如果系统不是多处理器系统,则此选项无效。
要确定当前系统上的处理器数量,请使用 `%SYSTEM.Util.NumberOfCPUs() `方法。
可以通过两种方式指定并行查询处理:
- 在系统范围内,通过设置`auto parallel`选项。
- 在每个查询的`FROM`子句中指定`%PARALLEL`关键字。
并行查询处理应用于`SELECT`查询。
它不应用于插入、更新或删除操作。
## 系统范围的并行查询处理
可以使用以下选项之一来配置系统范围的自动并行查询处理:
- 在管理门户中选择System Administration,然后选择Configuration,然后选择SQL和对象设置,最后选择SQL。
查看或更改在单个进程中执行查询复选框。
注意,该复选框的默认值是未选中的,这意味着并行处理在默认情况下是激活的。
- 调用`$SYSTEM.SQL.Util.SetOption()`方法,如下: `SET status=$SYSTEM.SQL.Util.SetOption("AutoParallel",1,.oldval)`.
默认值是1(自动并行处理激活)。
要确定当前的设置,调用`$SYSTEM.SQL.CurrentSettings()`,它会显示为`%PARALLEL`选项启用自动提示。
注意,更改此配置设置将清除所有名称空间中的所有缓存查询。
当激活时,自动并行查询提示指示SQL优化器对任何可能受益于这种处理的查询应用并行处理。
在IRIS 2019.1及其后续版本中,自动并行处理是默认激活的。
从IRIS 2018.1升级到IRIS 2019.1的用户需要明确激活自动并行处理。
SQL优化器用于决定是否对查询执行并行处理的一个选项是自动并行阈值。
如果激活了系统范围的自动并行处理(默认),可以使用`$SYSTEM.SQL.Util.SetOption()`方法将自动并行处理的优化阈值设置为整数值,如下所示: `SET status=$SYSTEM.SQL.Util.SetOption("AutoParallelThreshold",n,.oldval)`。
`n`阈值越高,将此特性应用于查询的可能性就越低。
此阈值用于复杂的优化计算,但可以将此值视为必须驻留在已访问映射中的元组的最小数量。
默认值为3200。
最小值为0。
要确定当前的设置,调用`$SYSTEM.SQL.CurrentSettings()`,它显示`%PARALLEL`选项的自动提示阈值。
当自动并行处理被激活时,在分片环境中执行的查询将始终使用并行处理执行,而不管并行阈值是多少。
## 针对特定查询的并行查询处理
可选的`%PARALLEL`关键字在查询的`FROM`子句中指定。
它建议跨系统的IRIS使用多个处理器(如果适用的话)并行处理查询。
这可以显著提高使用一个或多个`COUNT`、`SUM`、`AVG`、`MAX`或`MIN`聚合函数和`/`或`groupby`子句的查询的性能,以及许多其他类型的查询。
这些通常是处理大量数据并返回小结果集的查询。
例如,`SELECT AVG(SaleAmt) FROM %PARALLEL User.AllSales GROUP BY Region`都可使用并行处理。
**仅指定聚合函数、表达式和子查询的“一行”查询执行并行处理,无论是否带有`GROUP BY`子句。
但是,同时指定单个字段和一个或多个聚合函数的“多行”查询不会执行并行处理,除非它包含`GROUP BY`子句。
例如,`SELECT Name,AVG(Age) FROM %PARALLEL Sample.Person`不执行并行处理,但是 `SELECT Name,AVG(Age) FROM %PARALLEL Sample.Person GROUP BY Home_State` 执行并行处理。**
如果在运行时模式下编译指定`%PARALLEL`的查询,则所有常量都被解释为ODBC格式。
指定`%PARALLEL`可能会降低某些查询的性能。
在一个有多个并发用户的系统上运行`%PARALLEL`查询可能会降低整体性能。
在查询视图时可以执行并行处理。
但是,即使显式地指定了`%parallel`关键字,也不会对指定`%VID`的查询执行并行处理。
### `%PARALLEL`的子查询
`%PARALLEL`用于`SELECT`查询及其子查询。
插入命令子查询不能使用`%PARALLEL`。
当应用于与外围查询相关的子查询时,`%PARALLEL`将被忽略。
例如:
```sql
SELECT name,age FROM Sample.Person AS p
WHERE 30 e.dob.` 这是因为SQL优化将这种类型的连接转换为完整的外部连接。
对于完整的外部连接,`%PARALLEL`将被忽略。
- `%PARALLEL`和`%INORDER`优化不能同时使用;
如果两者都指定,`%PARALLEL`将被忽略。
- 查询引用一个视图并返回一个视图ID (`%VID`)。
- 如果表有`BITMAPEXTENT`索引,`COUNT(*)`不使用并行处理。
- `%PARALLEL`用于使用标准数据存储定义的表。
可能不支持将其与自定义存储格式一起使用。
`%PARALLEL`不支持全局临时表或具有扩展全局引用存储的表。
- `%PARALLEL`用于可以访问一个表的所有行的查询,使用行级安全(`ROWLEVELSECURITY`)定义的表不能执行并行处理。
- `%PARALLEL`用于存储在本地数据库中的数据。
它不支持映射到远程数据库的全局节点。
## 共享内存的考虑
对于并行处理,IRIS支持多个进程间队列(`IPQ`)。
每个`IPQ`处理单个并行查询。
它允许并行工作单元子流程将数据行发送回主流程,这样主流程就不必等待工作单元完成。
这使得并行查询能够尽可能快地返回第一行数据,而不必等待整个查询完成。
它还改进了聚合函数的性能。
并行查询执行使用来自通用内存堆(`gmheap`)的共享内存。
如果使用并行SQL查询执行,用户可能需要增加`gmheap`大小。
一般来说,每个`IPQ`的内存需求是`4 x 64k = 256k`。
InterSystems IRIS将一个并行SQL查询拆分为可用的`CPU`核数。
因此,用户需要分配这么多额外的`gmheap`:
```java
x x 256 =
```
注意,这个公式不是100%准确的,因为一个并行查询可以产生同样是并行的子查询。
因此,明智的做法是分配比这个公式指定的更多的额外`gmheap`。
分配足够的`gmheap`失败将导致错误报告给`messages.log`。
SQL查询可能会失败。
其他子系统尝试分配`gmheap`时也可能出现其他错误。
要查看一个实例的`gmheap`使用情况,特别是`IPQ`使用情况,请在管理门户的主页上选择System Operation,然后选择System usage,然后单击Shared Memory Heap usage链接;
![image](/sites/default/files/inline/images/2_23.png)
要更改通用内存堆或`gmheap`(有时称为共享内存堆或SMH)的大小,请从管理门户的主页选择“系统管理”,然后是“配置”,然后是“附加设置”,最后是“高级内存”;
![image](/sites/default/files/inline/images/3_19.png)
![image](/sites/default/files/inline/images/4_14.png)
## 缓存查询注意事项
如果你正在运行一个缓存的SQL查询,使用`%PARALLEL`,当这个查询被初始化时,你做了一些事情来清除缓存的查询,那么这个查询可能会从一个工人作业报告一个``错误。
导致缓存查询被清除的典型情况是调用`$SYSTEM.SQL.Purge()`或重新编译该查询引用的类。
重新编译类将自动清除与该类相关的任何缓存查询。
如果发生此错误,再次运行查询可能会成功执行。
从查询中删除`%PARALLEL`可以避免出现此错误。
## SQL语句和计划状态
使用`%PARALLEL`的SQL查询可以产生多条SQL语句。
这些SQL语句的计划状态是`Unfrozen/Parallel`。
计划状态为“已冻结”/“并行”的查询不能通过用户操作进行冻结。
# 生成报告
可以使用生成报告工具向InterSystems Worldwide Response Center (WRC) customer support提交查询性能报告,以便进行分析。
可以使用以下任意一种方式从管理门户运行生成报告工具:
1. 必须首先从WRC获得WRC跟踪号。可以使用每个管理门户页面顶部的Contact按钮从管理门户联系WRC。在WRC编号区域中输入此跟踪编号。可以使用此跟踪编号来报告单个查询或多个查询的性能。
2. 在“SQL语句”区域中,输入查询文本。右上角将显示一个X图标。可以使用此图标清除SQL语句区。查询完成后,选择保存查询按钮。系统生成查询计划并收集指定查询的运行时统计信息。无论系统范围的运行时统计信息设置如何,生成报告工具始终使用收集选项3:记录查询的所有模块级别的统计信息进行收集。由于在此级别收集统计信息可能需要时间,因此强烈建议您选中“在后台运行保存查询进程”复选框。默认情况下,此复选框处于选中状态。
当后台任务启动时,该工具显示“请等待……”,禁用页面上的所有字段,并显示一个新的视图进程按钮。
单击View Process按钮将在新选项卡中打开Process Details页面。
在流程详细信息页面,您可以查看该流程,并可以“暂停”、“恢复”或“终止”该流程。
进程的状态反映在Save查询页面上。
当流程完成时,当前保存的查询表将被刷新,View process按钮将消失,页面上的所有字段将被启用。
3. 对每个查询执行步骤2。
每个查询将被添加到当前保存的Queries表中。
注意,该表可以包含具有相同WRC跟踪号的查询,也可以包含具有不同跟踪号的查询。
完成所有查询后,继续步骤4。
对于列出的每个查询,可以选择Details链接。
该链接将打开一个单独的页面,其中显示完整的SQL语句、属性(包括WRC跟踪号和IRIS软件版本),以及包含每个模块的性能统计信息的查询计划。
- 要删除单个查询,请从“当前保存的查询”表中选中这些查询的复选框,然后单击“清除”按钮。
- 要删除与WRC跟踪编号关联的所有查询,请从当前保存的查询表中选择一行。WRC编号显示在页面顶部的WRC编号区域。如果您随后单击清除按钮,则对该WRC编号的所有查询都将被删除。
4. 使用查询复选框选择要报告给WRC的查询。要选择与WRC跟踪编号关联的所有查询,请从当前保存的查询表中选择一行,而不是使用复选框。在这两种情况下,都可以选择Generate Report按钮。生成报告工具创建一个XML文件,其中包括查询语句、具有运行时统计信息的查询计划、类定义以及与每个所选查询相关联的SQL int文件。
如果选择与单个WRC跟踪编号关联的查询,则生成的文件将具有默认名称,如`WRC12345.xml`。如果选择与多个WRC跟踪编号关联的查询,则生成的文件将具有默认名称`WRCMultiple.xml`。
将出现一个对话框,要求指定保存报告的位置。保存报告后,可以单击Mail to链接将报告发送给WRC客户支持。使用邮件客户端的附加/插入功能附加文件。
文章
Hao Ma · 一月 10, 2021
虽然 Caché 和 InterSystems IRIS 数据库的[完整性](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCDI_integrity)完全不会受到系统故障的影响,但物理存储设备故障确实会损坏其存储的数据。 因此,许多站点选择运行定期数据库完整性检查,尤其要与备份配合,以验证在发生灾难时是否可以依赖给定的备份。 系统管理员在应对涉及存储损坏的灾难时,也可能强烈需要完整性检查。 完整性检查必须读取所检查的 global 的每个块(如果尚未在缓冲区中),并且按照 global 结构指示的顺序读取。 这会花费大量时间,**但完整性检查能够以存储子系统可以承受的最快速度进行读取**。 在某些情况下,需要以这种方式运行以尽快获得结果。 在其他情况下,完整性检查需要更加保守,以避免消耗过多的存储子系统带宽。
## 行动计划
以下概述适合大多数情况。 本文其余部分中的详细讨论提供了采取其中任一行动或得出其他行动方案所需的信息。
1. 如果使用 Linux 并且完整性检查很慢,请参阅下面有关启用异步 I/O 的信息。
2. 如果完整性检查必须尽快完成,则在隔离的环境中运行;或者如果迫切需要结果,则使用多进程完整性检查来并行检查多个 global 或数据库。 进程数乘以每个进程将执行的并发异步读取数(默认为 8,如果使用 Linux 并且禁用异步 I/O 则为 1)是实时并发读取数的限制。 假定平均数是限制数量的一半,然后与存储子系统的能力进行比较。 例如,存储由 20 个驱动器条带化,每个进程的默认并发读取数为 8,则可能需要 5 个或更多进程才能利用存储子系统的全部能力 (5*8/2=20)。
3. 在平衡完整性检查速度与对生产的影响时,首先调整多进程完整性检查的进程数,然后如果需要的话,查看可调参数 SetAsyncReadBuffers。 对于长期解决方案(以及为消除误报),请参见下面的隔离完整性检查。
4. 如果已经被限制为一个进程(例如有一个极大的 global 或存在其他外部约束),并且完整性检查的速度需要上下调整,则查看下面的可调参数 SetAsyncReadBuffers。
## 多进程完整性检查
让完整性检查更快完成(以更高的速度使用系统资源)的一般解决方案是将工作分给多个并行进程。 一些完整性检查用户界面和 API 会这样做,而其他一些则使用单个进程。 对进程的分配按 global 进行,因此对单个 global 的检查始终由一个进程执行(Caché 2018.1 之前的版本按数据库而不是按 global 分配工作)。
多进程完整性检查的主要 API 是 **CheckLIst^Integrity**(有关详细信息,请参阅[文档](https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=GCDI_integrity#GCDI_integrity_verify_utility))。 它将结果收集在一个临时的 global 中,通过 Display^Integrity 来显示。 以下是使用 5 个进程检查 3 个数据库的示例。 这里如省略数据库列表参数,将检查所有数据库。
set dblist=$listbuild(“/data/db1/”,”/data/db2/”,”/data/db3/”)
set sc=$$CheckList^Integrity(,dblist,,,5)
do Display^Integrity()
kill ^IRIS.TempIntegrityOutput(+$job)
/* Note: evaluating ‘sc’ above isn’t needed just to display the results, but...
$system.Status.IsOK(sc) - ran successfully and found no errors
$system.Status.GetErrorCodes(sc)=$$$ERRORCODE($$$IntegrityCheckErrors) // 267
- ran successfully, but found errors.
Else - a problem may have prevented some portion from running, ‘sc’ may have
multiple error codes, one of which may be $$$IntegrityCheckErrors. */
像这样使用 CheckLIst^Integrity 是实现我们感兴趣的控制水平的最直接方法。 管理门户接口和完整性检查任务(内置但未安排)使用多个进程,但可能无法为我们的用途提供足够的控制。*
其他完整性检查接口,尤其是终端用户接口 ^INTEGRIT 或 ^Integrity 以及 Silent^Integrity,在单个进程中执行完整性检查。 因此,这些接口不能以最快的速度完成检查,并且它们使用的资源也较少。 但一个优点是,它们的结果是可见的,可以记录到文件或输出到终端,因为每个 global 都会被检查,而且顺序明确。
## 异步 I/O
完整性检查进程会排查 global 的每个指针块,一次检查一个,根据它指向的数据块的内容来进行验证。 数据块以异步 I/O 的方式读取,以确保每时每刻都有一定数量的读取请求供存储子系统处理,并且每次读取完成后都进行验证。
在 Linux 上,异步 I/O 只有与直接 I/O 结合时才有效,而 InterSystems IRIS 2020.3 之前的版本默认不启用直接 I/O。 这解释了大量 Linux 上完整性检查时间过长的情况。 幸运的是,可以在 Cache 2018.1、IRIS 2019.1 及以后的版本上启用直接 I/O,方法是在 .cpf 文件的 [config] 部分中设置 **wduseasyncio=1**,然后重新启动。 通常建议设置此参数,以在繁忙系统上实现 I/O 可伸缩性,并且自 Caché 2015.2 起,在非 Linux 平台上默认设置此参数。 在启用之前,确保已经为数据库缓存(global 缓冲区)配置了足够的内存,因为启用直接 I/O 后,数据库将不再被 Linux(冗余)缓存。 未启用时,完整性检查执行的读取会同步完成,不能有效利用存储。
在所有平台上,完整性检查进程一次执行的读取数默认设置为 8。 如果必须更改单个完整性检查进程从磁盘读取的速率,可以调整此参数 – 向上调会使单个进程更快完成,向下调则使用更少的存储带宽。 请记住:
* 此参数应用于每个完整性检查进程。 当使用多个进程时,进程数会使实时读取数增加。更改并行完整性检查进程数会产生较大影响,因此这通常是先做的事情。 每个进程还受到计算时间的限制(除其他限制外),因此增加此参数的值所获得的收益也有限。
* 这只在存储子系统处理并发读取的能力范围内有效。 如果数据库存储在单个本地驱动器上,再高的数值也没有用处,而在几十个驱动器上条带化的存储阵列可以并发处理几十个读取。
要从 %SYS 命名空间调整此参数,则 **do SetAsyncReadBuffers^Integrity(**value**)**。 要查看当前值,则 **write $$GetAsyncReadBuffers^Integrity()**。 更改在检查下一个 global 时生效。 目前,该设置在系统重启后不会保持,虽然可以将其添加到 SYSTEM^%ZSTART 中。
有一个相似的参数用于在磁盘上的块是连续(或接近连续)分布时控制每次读取的最大大小。 此参数很少需要用到,尽管具有高存储延迟的系统或具有较大块大小的数据库可能会从微调中受益。 该值的单位为 64KB,因此值 1 表示 64KB,4 表示 256KB 等等。0(默认值)表示让系统选择,当前选择 1 (64KB)。 此参数的 ^Integrity 函数(类似于上面提及的函数)为 **SetAsyncReadBufferSize** 和 **GetAsyncReadBufferSize**。
## 隔离完整性检查
许多站点直接在生产系统上运行定期完整性检查。 这当然是最简单的配置,但并不理想。 除了完整性检查对存储带宽的影响,并发数据库更新活动有时还可能导致误报错误(尽管检查算法内置了缓解措施)。 因此,在生产系统上运行的完整性检查所报告的错误,需要由管理员进行评估和/或重新检查。
很多时候,存在更好的选择。 可以将存储快照或备份映像挂载到另一台主机上,在那里由隔离的 Caché 或 IRIS 实例运行完整性检查。 这样不仅可以防止任何误报,而且如果存储也与生产隔离,运行完整性检查可以充分利用存储带宽并更快完成。 这种方法非常适合使用完整性检查来验证备份的模型;经过验证的备份可以有效验证截至生成备份前的生产情况。 还可以通过云和虚拟化平台更容易地从快照建立可用的隔离环境。
* * *
* 管理门户接口、完整性检查任务和 SYS.Database 的 IntegrityCheck 方法会选择相当多的进程(等于 CPU 内核数),在很多情况下缺少所需的控制。 管理门户和任务还会对任何报告错误的 global 执行完整的重新检查,以识别可能因并发更新而出现的误报。 除了完整性检查算法内置的误报缓解措施,也可能进行这种重新检查;在某些情况下,由于会花费额外的时间(重新检查在单个进程中运行,并检查整个 global),可能并不需要重新检查。 此行为将来可能会更改。
文章
Nicky Zhu · 五月 20, 2021
## 一. 企业信息库简介
企业信息库(MessageBank)是一个可选的远程归档设施,可以从多个来自不同实例的互操作性Production中收集信息、事件日志项目和搜索表项。如下图所示:
![image](/sites/default/files/inline/images/1_1_0.png)
这套环境由两种角色的实例构成:
企业信息库服务器,它本身也是一个Production,完全由Message Bank服务组成,接收来自任何数量的客户Production提交的消息、日志等。
客户端Operation(Message Bank Operation),将其添加到一个正在运行的Production中,并用企业信息库服务器的地址进行配置。如连接通畅,消息和日志即可自动转发到Message Bank并在其中存储。
为了使你能方便地看到信息库中的信息,InterSystems IRIS®提供了以下附加选项。
对于企业信息库实例,管理门户自动包括企业监控器页面,在那里你可以监控客户端Production的状态,浏览消息库,并对被监控客户的消息进行检索。
对于每个客户端实例,你在消息库实例中配置一个到企业监控器的链接。
如下所示:
![image](/sites/default/files/inline/images/1_2_0.png)
## 二. 常见应用场景
### 消息归档
在使用IRIS互操作性时,对于生产环境,为保障其有充足的磁盘空间和即时查询的效率,通常会采用消息和日志过期策略。在生产环境中只保留近期(如一个月)的信息以备回溯,过期数据将定期被清除。因此,如果有长期保留消息(如在生产环境清除周期之外还需要更长时间的回溯)的需求,则可以通过Message Bank对消息和日志进行长期保存。
### 企业消息仓库
对于集成规模较大,集成业务较多的大型企业和集团(如大型医院、医联体、医共体),往往会采用多套互操作性实例支撑数据交换和集成业务。在这种环境下,可以通过Message Bank汇聚和存储整个企业环境下的所有互操作消息和日志,为业务集中监控、跨实例业务故障分析等工作创造条件。
### 消息和日志再利用
理想条件下,实施互操作性项目之后,消息和日志中就会包括大量的业务数据,典型的包括下达的医嘱、患者信息、医疗记录等。通过对Message Bank中的数据进行分析和挖掘,能够获得有价值的业务信息。
接下来我们会为大家介绍Message Bank的搭建过程。
## 三. 搭建Message Bank
### 创建Message Bank 命名空间
在生产所用的实例之外,我们需要使用一台独立的实例用于安装和配置Message Bank(实例安装过程和License激活过程从略,请查看安装文档或联系您的支持工程师)。
在该实例上,创建一个命名空间安装Message Bank,如下所示:
![image](/sites/default/files/inline/images/1_3_0.png)
由于Message Bank本质上由Production实现,因此创建命名空间时要选上对互操作Producation的支持。
InterSystems为大家提供了可以套用的Production模版。因此,请按照以下步骤创建Production:
在刚才创建的命名空间MessageStore下创建类MessageBank.BankProduction,继承Ens.Enterprise.MsgBank.Production并将Ens.Enterprise.MsgBank.Production中的XData代码块拷贝到新建的类中,如下所示:
![image](/sites/default/files/inline/images/1_4_0.png)
保存和编译该类,并在Interoperability菜单中加载该Production。
![image](/sites/default/files/inline/images/1_5_0.png)
其中已部署了两个服务:
MsgBankService:该服务通过TCP连接从其他Production接收消息
注意该Service默认使用9192端口与其他客户端通信。
![image](/sites/default/files/inline/images/1_6_0.png)
MonitorService:该服务收集其他实例的其他Production的运行状态
此时,这个Production已经具备了从其他实例的Production收集消息和事件信息的能力,可直接启动。当然,我们还需要配置与客户端的连接。
### 为客户端Production添加消息转发Operation
假设我们已经有一个可运行的的如下所示的Production
![image](/sites/default/files/inline/images/1_7_0.png)
注意这个Production与Message Bank不在同一个实例上。
![image](/sites/default/files/inline/images/1_8_0.png)
这个Production接收XML格式的报文并根据报文类型转发到不同的BO。
要将这个Production加入Message Bank,则需要对该客户端Production添加Business Operation Ens.Enterprise.MsgBankOperation。
![image](/sites/default/files/inline/images/1_9_0.png)
对于该Operation,需要指定要连接的Message Bank的IP地址和端口。
![image](/sites/default/files/inline/images/1_10_0.png)
同时,建议开启这个Operation的“启用存档”开关,保证在Message Bank临时故障时挂起消息,在故障恢复后还能捕捉到故障期间的消息和日志。
配置完成后启用该Operation。
### 在Message Bank中加入客户端信息
上述连接建立后,客户端和Message Bank间的连接已建立,还需要配置Message Bank和客户端Production之间的程序信息(相当于注册)才能正常工作。
#### 添加客户端连接凭据
![image](/sites/default/files/inline/images/1_11_0.png)
Message Bank需要通过Web请求访问客户端信息,因此,需要配置客户端凭据,即可通过管理门户访问客户端Production的用户名和密码(对访问权限的设计和配置,可参见我们之前的文章:IRIS中的权限管理)
#### 在Message Bank上配置客户端信息
在Message Bank中的Interoperability菜单中找到“企业系统”项
![image](/sites/default/files/inline/images/1_12_0.png)
在操作页面上通过“新建连接”
![image](/sites/default/files/inline/images/1_13_0.png)
新建连接添加客户端信息。
![image](/sites/default/files/inline/images/1_14_0.png)
注意其中的服务Web应用路径为该客户端实例上Production所在的命名空间的Web Application根路径,并引用之前填写的凭据。
如配置正确,可通过企业监视器查看连接状态
![image](/sites/default/files/inline/images/1_15_0.png)
连接成功的状态如下:
![image](/sites/default/files/inline/images/1_16_0.png)
#### 在客户端上添加Message Bank连接信息(可选步骤)
如果需要在客户端上通过链接查看消息仓库的信息,则可以配置链接。
在客户端上,在被采集的Production所在的命名空间的Interoerability菜单中“消息仓库链接”配置
![image](/sites/default/files/inline/images/1_17_0.png)
输入Message Bank所在的IP、端口和Production所在的命名空间,保存并“开始”即可跳转到Messsage Bank的企业监视器。
![image](/sites/default/files/inline/images/1_18_0.png)
需要注意的是,该配置固定采用了/csp/[namespace]为Message Bank的Web Application路径,而在Message Bank实例上,这个Web Application默认的路径是/csp/healthshare/messagestorage。可通过在Message Bank上添加一个Web Application,拷贝/csp/healthshare/messagestorage的配置。
![image](/sites/default/files/inline/images/1_19_0.png)
## 四. Message Bank的实施效果
### 测试消息
在客户端的Production中触发任意流程产生消息,如下所示:
![image](/sites/default/files/inline/images/1_20_0.png)
此时通过Message Bank中的“消息仓库查看器”即可查询存储在消息仓库中的消息
![image](/sites/default/files/inline/images/1_21_0.png)
如下:
![image](/sites/default/files/inline/images/1_22_0.png)
可以注意到该消息已被同步到消息仓库。
需要注意,使用“消息仓库查看器”时,查询的是在Message Bank中存储的消息数据,使用在Message Bank上定义的Search Table或索引进行查询;如果通过“企业消息查看器”查询,则是链接到客户端的消息查看器查询,应用的是在客户端上定义的索引。
### 消息的存储
根据在源系统的消息类型的不同,传递到Message Bank后会以不同的形式保存消息。
#### 虚拟文档
对于HL7 V2等标准消息或基于XML虚拟文档的消息,在Message Bank这一侧也同样以虚拟文档的形式保存。
![image](/sites/default/files/inline/images/1_23_0.png)
特别注意其中的如下属性:
MessageBodyClassName:该类型为消息在Message Bank侧持久化的类型。
ClientBodyClassName:该类型为消息在客户端侧持久化的类型。
在本例中可以看到,客户端通过EnsLib.EDI.XML.Document类型传递的消息,在Message Bank中也是通过EnsLib.EDI.XML.Document保存。
MessageBodyId:消息在Message Bank中的物理主键
ClientBodyId:客户端侧持久化消息的物理主键
ClientSessionId:客户端会话Id
#### 结构化消息
对于基于Ens.Request等持久化类型的消息,在Message Bank这一侧则默认使用字符流来保存。
例如,对于如下的客户端结构化消息传输
![image](/sites/default/files/inline/images/1_24_0.png)
在Message Bank中的保存形式为:
![image](/sites/default/files/inline/images/1_25_0.png)
可见:
MessageBodyClassName:消息在Message Bank中以%Stream.GlobalCharacter即字符流进行保存
因此,无论是保存为EnsLib.EDI.XML.Document或是%Stream.GlobalCharacter,在Message Bank中保存的消息本身都缺乏足够的结构化特征和索引以支持对消息体的检索,我们会在下一篇教程《[互操作消息统一管理系列:SearchTable加速检索](https://cn.community.intersystems.com/post/%E4%BA%92%E6%93%8D%E4%BD%9C%E6%B6%88%E6%81%AF%E7%BB%9F%E4%B8%80%E7%AE%A1%E7%90%86%E7%B3%BB%E5%88%97%EF%BC%9Asearchtable%E5%8A%A0%E9%80%9F%E6%A3%80%E7%B4%A2)》中介绍如何通过构建Search Table来检索这些消息。
对于Message Bank相关的内容,可参见:
https://docs.intersystems.com/healthconnect20201/csp/docbook/DocBook.UI.Page.cls?KEY=EGDV_message_bank
也欢迎与我们联系获得更详细的信息。
文章
Weiwei Gu · 六月 27, 2022
Globals,这些存储数据的魔剑,已经存在了一段时间,但是没有多少人能够有效地使用它们,也没有多少人知道这个超级武器。
如果你把Globals的东西用在它们真正能发挥作用的地方,其结果可能是惊人的,要么是性能的提高,要么是整体解决方案的大幅简化 (1, 2).
Globals提供了一种特殊的存储和处理数据的方式,它与SQL表完全不同。它们在1966年首次出现在 M(UMPS)编程语言中, 该语言最初用于医学数据库。现在它仍然以同样的方式被使用,但也被其他一些以可靠性和高性能为首要任务的行业所采用:金融、交易等。
后来M(UMPS)演变为 Caché ObjectScript (COS). COS是由InterSystems公司开发的,作为M的一个超集. 其原始语言仍然被开发者社区所接受,并在一些实现中保持活力。在网络上有几个活跃的网址,比如:MUMPS Google group, Mumps User's group), effective ISO Standard等等
现代基于Globals的数据库支持交易、日志、复制、分区等。这意味着它们可以被用来构建现代的、可靠的、快速的分布式系统。
Gloabls并不将你限制于关系模型的范围内。它们让你可以自由地创建为特定任务优化的数据结构。对于许多应用来说,合理地使用好的Globals就如一颗真正的银子弹头,它所提供的速度是传统关系型应用的开发者所梦寐以求的。
作为一种存储数据的方法,globals可以在许多现代编程语言中使用,包括高级和低级语言。因此,本文将特别关注Globals本身,而不是它们曾经来自的语言。
Globals 是如何工作的
让我们先了解一下globals是如何工作的,它们有哪些优点。
我们可以从不同的角度来看待globals。在文章的这一部分,我们可将把它们看成是树形状结构或分层的数据存储空间。
简单地说,Globals是一个持久化的数组。一个自动保存在磁盘上的数组。
很难想象有什么比这更简单的方法来存储数据。在程序代码中(用COS/M语言编写),与普通关联数组的唯一区别是站在它们名字前面的^符号。
若将数据保存为Globals, 你可以不需要知道SQL,因为所有必要的命令都非常简单,在一个小时内就可以学会。
让我们从最简单的例子开始,一个有两个分支的单层的树形结构。例子是用COS(Caché ObjectScript) 写的。
Set ^a("+7926X") = "John Sidorov"
Set ^a("+7916Y") = "Sergey Smith"
当数据被插入一个Global(Set命令)时,有3件事情会自动发生:
1.将数据保存到磁盘。2.编制索引。括号里的是下标,等号右边的是а节点值。3. 排序。数据是按一个键来排序的。下一次的遍历会把 "Sergey Smith "放到第一个位置,然后是 "John Sidorov"。当从global获得一个用户列表时,数据库不会在排序上花费时间。实际上你可以请求一个从任何键开始的排序列表,甚至是一个不存在的键(输出将从这个键之后的第一个真正的键开始)。所有这些操作都以惊人的速度进行。在我的个人系统(i5-3340,16GB,HDD WD 1TB Blue)上,我设法在一个进程中达到105万次插入/秒。在多核系统上,速度可以达到几千万次/秒 的插入。
当然,记录插入速度本身并不能说明什么。例如,我们可以将数据写入文本文件--根据传言,这就是Visa的处理方式。然而,通过globals,我们得到了一个结构化和索引化的存储,你可以在工作中享受其高速和易用性。
globals最大的优势是在其中插入新节点的速度。
数据在全局中总是有索引的。单层和深入的树形遍历总是非常快的。
让我们在global中添加一些二级和三级的分支看看:
Set ^a("+7926X", "city") = "Moscow"
Set ^a("+7926X", "city", "street") = "Req Square"
Set ^a("+7926X", "age") = 25
Set ^a("+7916Y", "city") = "London"
Set ^a("+7916Y", "city", "street") = "Baker Street"
Set ^a("+7916Y", "age") = 36
显然,你可以使用globals建立多层级的“树”。由于每次插入后的自动索引,因而其对任何节点的访问几乎都是即时的。任何一级的树枝都可以按一个键进行排序。
正如你所看到的,数据可以被存储在键和值中。一个键的综合长度(所有索引的长度之和)可以达到511字节,而Caché中的值可以达到3.6MB的大小。树中的层数(维数)上限为31。
还有一件很酷的事情:你可以在不定义顶级节点的值的情况下建立一棵“树”。
Set ^b("a", "b", "c", "d") = 1
Set ^b("a", "b", "c", "e") = 2
Set ^b("a", "b", "f", "g") = 3
空的圆圈是没有值的节点。
为了更好地理解globals,让我们把它们与其他树进行比较:所谓“花园树”和“文件系统名称树”。
让我们把globals与最熟悉的层次结构进行比较:如下的Orchard tree-“生长在花园和田野中的普通树”,以及File system-文件系统。
我们可以看到,叶子和果实只生长在普通树木的枝干末端。文件系统--信息也被存储在树枝的末端,也被称为全文件名。
而下面这里是一个Global的数据结构:
不同:
1.内部节点:Global中的信息可以存储在每个节点中,而不是只存储在分支末端。2.外部节点:globals必须有定义的分支末端(有值的末端),这对文件系统和"花园树"来说不是强制性的。
关于内部节点,我们可以把global的结构看作是文件系统的名字树和花园树结构的超集。所以global的结构是一个更灵活的结构。
一般来说,global是一个结构化的树,支持在每个节点中保存数据。
为了更好地理解globals是如何工作的,让我们想象一下,如果文件系统的创建者使用与globals相同的方法来存储信息,会发生什么?
1. 如果一个文件夹中的最后一个文件被删除了,那么这个文件夹本身以及所有只包含这个被删除的文件夹的高层文件夹也会被删除。 2. 这样就根本不需要文件夹了。会有带子文件的文件和不带子文件的文件。如果你把它与普通的树作比较,每个分支都会变成一个果实。
3. 像README.txt这样的东西可能就不再需要了。所有你需要说的关于文件夹的内容都可以写在文件夹文件本身。一般来说,文件名和文件夹名是没有区别的(例如,/etc/readme可以是文件夹,也可以是文件),这意味着我们只需要操作文件就可以了。
4. 带有子文件夹和文件的文件夹可以更快地被删除。网络上有一些文章讲述了删除数百万个小文件是多么的耗时和困难(1, 2, 3). 然而,如果你创建一个基于Global的假的文件系统,它将只需要几秒钟甚至几分之一秒。当我在家里的电脑上测试删除子树时,我成功地从HDD(不是SDD)上的两级树上删除了96-341万个节点。值得一提的是,我们讨论的是删除Global树的一部分,而不是删除包含Global的整个文件。
子树的移除是globals的另一个优势:你不需要递归来做这个。它的速度快得令人难以置信。
在我们的树中,这可以通过一个Kill的命令来完成。
Kill ^a("+7926X")
下面是一个小表格,它可以让你更好地了解你可以在Global上执行的操作
Cache object script中与Globals有关的关键命令和功能
Set设置
设置(初始化)分支到一个节点(如果未定义)和节点值
Merge合并
复制一棵子树
Kill
删除一棵字树
ZKill
删除一个特定节点的值。源自该节点的子树不受影响。
$Query
对树进行全面深入的遍历
$Order
返回同一级别的下一个下标
$Data
检查一个节点是否被定义
$Increment
节点值的原子递增,以避免ACID的读和写。最新的建议是使用 $Sequence 来代替
感谢你的关注,我很乐意回答你的任何问题。
免责声明:本文反映了作者的个人观点,与InterSystems的官方立场无关。
让我们期待下一篇继续 "Globals 是存储数据的魔剑-树 :第二部分 (待翻译)
你将了解到哪些类型的数据可以显示在globals中,以及它们在哪些地方效果最好。 好文!
文章
姚 鑫 · 八月 22, 2022
# 第九章 配置数据库(一)
数据库是使用数据库向导创建的 `IRIS.DAT` 文件。 `IRIS`数据库保存称为全局变量的多维数组中的数据和称为例程的可执行内容,以及类和表定义。
全局变量和例程包括方法、类、网页、SQL、BASIC和JavaScript文件
**注意:在 `Windows` 系统上,不要对 `IRIS.DAT` 数据库文件使用文件压缩。 (通过右键单击 `Windows` 资源管理器中的文件或文件夹并选择属性,然后选择高级,然后压缩内容以节省磁盘空间来压缩文件;压缩后,文件夹名称或文件名在 `Windows` 资源管理器中呈现为蓝色。)如果压缩`IRIS.DAT` 文件,它所属的实例将无法启动,并出现误导性错误。**
`IRIS` 数据库根据需要动态扩展(假设有可用空间),但可以指定最大大小。如果使用默认的 `8KB` 块大小,数据库可以增长到 `32 TB`。
可以动态更改大多数数据库配置;可以在系统运行时创建和删除数据库以及修改数据库属性。
**注意:这些主题描述了使用管理门户手动配置数据库的过程。 `IRIS` 还包含可用于自动化数据库配置的编程工具。可以使用新选项卡类中的 `Config.Databases` 来创建和配置数据库;还可以使用 `^DATABASE` 命令行实用程序配置数据库。**
**配置数据库的另一种方法是将 `CreateDatabase`、`ModifyDatabase` 或 `DeleteDatabase` 操作与配置合并结合使用。配置合并允许通过应用声明性合并文件来自定义 `IRIS` 实例,该文件指定要应用于该实例的设置和操作。**
# Background
`IRIS` 将数据——持久多维数组(`globals`)以及可执行代码(例程)——存储在一个或多个称为数据库的物理结构中。数据库由存储在本地操作系统中的一个或多个物理文件组成。一个 `IRIS` 系统可能(并且通常确实)有多个数据库。
每个 `IRIS` 系统都维护一个数据库缓存——一个本地共享内存缓冲区,用于缓存从物理数据库中检索到的数据。这种高速缓存大大减少了访问数据所需的昂贵 `I/O` 操作的数量,并提供了 `IRIS` 的许多性能优势。
`IRIS` 应用程序通过命名空间访问数据。命名空间提供存储在一个或多个物理数据库中的数据(全局变量和例程)的逻辑视图。一个 `IRIS` 系统可能(并且通常确实)有多个命名空间。 `IRIS` 将逻辑命名空间中可见的数据映射到一个或多个物理数据库。这种映射为应用程序提供了一种强大的机制,可以在不更改应用程序逻辑的情况下更改应用程序的物理部署。
在最简单的情况下,命名空间和数据库之间存在一一对应关系,但许多系统利用定义命名空间的能力来提供对多个数据库中数据的访问。例如,一个系统可以有多个命名空间,每个命名空间提供存储在一个或多个物理数据库中的数据的不同逻辑视图。
# 数据库注意事项
## 数据库总限制
可以在单个 `IRIS` 实例中配置的数据库数量的绝对限制(如果有足够的存储空间)是 `15,998`。其他限制如下:
- 数据库的目录信息不能超过 `256 KB`。这意味着,如果数据库目录名称的平均长度较长,则实例可以拥有较少的数据库总数。以下公式描述了这种关系:
```math
maximum_DBs = 258048/ (avg_DB_path_length + 3)
```
例如,如果所有数据库目录路径的格式为 `c:\InterSystems\IRIS\mgr\DBNNNN\`,则平均长度为 `33` 个字节。因此,最大数据库数为 `7,168`,计算如下:`258048/ (33 + 3) = 7168`。
- 镜像数据库在 `15,998` 的绝对限制中计数两次。如果实例上的所有数据库都进行了镜像,则有效限制为 `7,499` 个数据库。这是因为 `IRIS` 为镜像数据库创建了两个数据库定义;一个用于目录路径 (`c:\InterSystems\IRIS\mgr\DBNNNN\`),另一个用于镜像定义 (`:mirror:MIRRORNAME:MirrorDBName`)。
- 可以同时使用的数据库数量受操作系统对打开文件数量(每个进程或系统范围)的限制的限制。 `IRIS` 将大约一半的操作系统打开文件分配留给自己和设备使用。
## 数据库配置注意事项
以下是配置数据库时要考虑的提示:
- `IRIS` 提供了一个无缝选项,可以在多个物理数据库 (`IRIS.DAT`) 文件中传播数据。因此,可以根据需要构建具有多个数据库的应用程序或通过全局或下标级映射拆分数据。
- 根据可用于管理任务(如备份、恢复、完整性检查等)的基础设施,将数据库大小保持在可管理的范围内。
- 建议将流全局变量(如果将流存储在 `IRIS.DAT` 数据库文件中)全局映射到单独的数据库,并且将流数据库配置为大 (`64 KB`) 块大小。
- 根据工作负载,考虑替代(更大)块大小可能比默认的 `8 KB` 数据库块大小更有利。
## 大数据块大小注意事项
除了 `IRIS` 支持的 `8 KB`(默认)块大小(始终启用)之外,还可以启用以下块大小:
- `16 KB (16384)`
- `32 KB (32768)`
- `64 KB (65536)`
但是,在创建使用大块的数据库时应该谨慎,因为使用它们会影响系统的性能。
在启用和使用大的块大小之前,请考虑以下几点:
- 如果应用程序工作负载主要由顺序插入或顺序读取/查询组成,那么大的块大小可以提高性能。
- 如果应用程序工作负载主要由随机插入或随机读取/查询组成,那么大的块大小可能会降低性能。
由于对于给定的数据库缓存总大小,较大的块大小会导致缓存更少的块,为了减少对随机数据库访问的影响,还应该考虑将更多的总内存用作数据库缓存。
- 对于索引类型的数据库,默认的块大小(`8 KB`)确保最佳性能;
较大的块大小可能会降低性能。
如果正在考虑为数据设置更大的块大小,那么应该考虑将索引全局变量映射到一个单独的`8 KB`块大小的数据库。
要创建一个使用不支持的块大小的数据库,请执行以下操作:
1. 使用启动设置页面(系统管理>附加设置>启动)的设置启用块大小,在配置参数文件引用的`DBSizesAllowed`条目中描述。
2. 在启动设置页面(系统管理>附加设置>启动),按照内存和启动设置中的描述,为启用的块大小配置数据库缓存。
3. 重新启动
4. 按照创建本地数据库中的说明创建数据库。
# 数据库兼容性注意事项
如创建本地数据库过程中所述,可以通过复制或移动 `IRIS.DAT` 文件将 `IRIS` 数据库复制或移动到创建它的实例之外的实例,或临时装载在另一个实例中创建的数据库在同一个系统上。还可以将数据库的备份(请参阅数据完整性指南的“备份和恢复”一章)恢复到其原始实例以外的实例。但是,为避免数据不兼容,必须满足以下要求:
- 目标(新)实例必须使用相同的字符宽度(`8`位或`Unicode`;
请参阅安装指南中的新选项卡中的字符宽度设置),并使用相同的区域设置(请参阅使用管理门户的NLS设置页面)作为创建数据库的实例。
此要求的一个例外是使用基于 `ISO 8859 Latin-1` 字符集的区域设置的 `8` 位实例与使用相应宽字符区域设置的 `Unicode` 实例兼容。例如,使用 `enu8` 语言环境在 `8` 位实例中创建的数据库可以在使用 `enuw` 语言环境的 `Unicode` 实例中使用。
- 如果源实例和目标实例位于不同字节序的系统上,则数据库必须转换为目标实例的字节序后才能使用。
根据平台的不同,多字节数据存储在最低内存地址(即首先)中的最高有效字节或最低有效字节:当最高有效字节首先存储时,称为“大端;”当首先存储最低有效字节时,它被称为“小端”。
当使用在不同端序的系统上创建的现有`IRIS.DAT`定义数据库时,请在使用数据库之前使用`cvendian`实用程。
文章
Michael Lei · 十二月 7, 2022
大家好!
这是关于使用 Docker 初始化 IRIS 实例的系列文章中的第三篇。 这次,我们将关注企业缓存协议(**E**nterprise **C**ache **P**rotocol,ECP)。
ECP 允许以一种非常简单的方式将某些 IRIS 实例配置为应用程序服务器,将其他实例配置为数据服务器。 有关详细的技术信息,请参阅官方文档。
本文旨在介绍:
* 如何编写数据服务器的初始化脚本,以及如何编写一个或多个应用程序服务器的初始化脚本。
* 如何使用 Docker 在这些节点之间建立加密连接。
为此,我们通常使用我们在以前的 Web 网关中已经看到的一些工具,以及描述 OpenSSL、envsubst 和 Config-API 等工具的镜像文章。
## 要求
ECP 不适用于 IRIS 社区版。 因此,需要访问全球响应中心才能下载容器许可证并连接到 containers.intersystems.com 注册表。
## 准备系统
系统必须与容器共享一些本地文件。 需要创建特定用户和组来避免出现“访问被拒绝”错误。
```bash
sudo useradd --uid 51773 --user-group irisowner
sudo useradd --uid 52773 --user-group irisuser
sudo groupmod --gid 51773 irisowner
sudo groupmod --gid 52773 irisuser
```
如果您还没有“iris.key”许可证,请从 WRC 下载,并将其添加到您的主目录中。
## 检索示例存储库
除“iris.key”许可证外,您需要的所有其他文件都可以在公共存储库中找到,因此,首先将其克隆:
```bash
git clone https://github.com/lscalese/ecp-with-docker.git
cd ecp-with-docker
```
## SSL 证书
为了加密应用程序服务器与数据服务器之间的通信,我们需要 SSL 证书。
可以使用现成的脚本(“gen-certificates.sh”)。 但是,您可以随意修改脚本,使证书设置与您的位置、公司等保持一致。
执行:
```bash
sh ./gen-certificates.sh
```
生成的证书现在位于“./certificates”目录中。
| 文件 | 容器 | 描述 |
| ------------------------------ | ------------- | ---------------- |
| ./certificates/CA_Server.cer | 应用程序服务器和数据服务器 | 机构服务器证书 |
| ./certificates/app_server.cer | 应用程序服务器 | IRIS 应用程序服务器实例证书 |
| ./certificates/app_server.key | 应用程序服务器 | 相关私钥 |
| ./certificates/data_server.cer | 数据服务器 | IRIS 数据服务器实例证书 |
| ./certificates/data_server.key | 数据服务器 | 相关私钥 |
## 构建镜像
首先,登录 Intersystems docker 注册表。 在构建期间,将从注册表中下载基础镜像:
```bash
docker login -u="YourWRCLogin" -p="YourICRToken" containers.intersystems.com
```
如果您不知道自己的Token,请使用您的 WRC 帐户登录 https://containers.intersystems.com/。
在此构建过程中,我们将向 IRIS 基础镜像添加一些软件实用程序:
* **gettext-base**:它将允许我们使用“envsubst”命令替换配置文件中的环境变量。
* **iputils-arping**:如果我们想要镜像数据服务器,则需要使用此实用程序。
* **ZPM**:ObjectScript 软件包管理器。
[Dockerfile](https://github.com/lscalese/ecp-with-docker/blob/master/Dockerfile):
```
ARG IMAGE=containers.intersystems.com/intersystems/iris:2022.2.0.281.0
# Don't need to download the image from WRC. It will be pulled from ICR at build time.
FROM $IMAGE
USER root
# Install iputils-arping to have an arping command. It's required to configure Virtual IP.
# Download the latest ZPM version (ZPM is included only with community edition).
RUN apt-get update && apt-get install iputils-arping gettext-base && \
rm -rf /var/lib/apt/lists/*
USER ${ISC_PACKAGE_MGRUSER}
WORKDIR /home/irisowner/demo
RUN --mount=type=bind,src=.,dst=. \
iris start IRIS && \
iris session IRIS < iris.script && \
iris stop IRIS quietly
```
此 Dockerfile 中除最后一行外没有什么特别之处。 它将 IRIS 数据服务器实例配置为最多接受 3 个应用程序服务器。 请注意,此配置需要重新启动 IRIS。 我们在构建过程中分配此参数的值,以避免稍后编写重新启动脚本。
开始构建:
```bash
docker-compose build –no-cache
```
## 配置文件
在配置 IRIS 实例(应用程序服务器和数据服务器)时,我们使用 JSON config-api 文件格式。 您会注意到这些文件包含环境变量 "${variable_name}"。 它们的值在“docker-compose.yml”文件的“environment”部分定义,我们稍后将在本文档中看到。 这些变量将在使用“envsubst”实用程序加载文件之前被替换掉。
### 数据服务器
对于数据服务器,我们将:
* 启用 ECP 服务并定义授权客户端(应用程序服务器)列表。
* 创建加密通信所需的“SSL %ECPServer”配置。
* 创建“myappdata”数据库。 它将用作来自应用程序服务器的远程数据库。
(data-serer.json)[https://github.com/lscalese/ecp-with-docker/blob/master/config-files/data-server.json]
```json
{
"Security.Services" : {
"%Service_ECP" : {
"Enabled" : true,
"ClientSystems":"${CLIENT_SYSTEMS}",
"AutheEnabled":"1024"
}
},
"Security.SSLConfigs": {
"%ECPServer": {
"CAFile": "${CA_ROOT}",
"CertificateFile": "${CA_SERVER}",
"Name": "%ECPServer",
"PrivateKeyFile": "${CA_PRIVATE_KEY}",
"Type": "1",
"VerifyPeer": 3
}
},
"Security.System": {
"SSLECPServer":1
},
"SYS.Databases":{
"/usr/irissys/mgr/myappdata/" : {}
},
"Databases":{
"myappdata" : {
"Directory" : "/usr/irissys/mgr/myappdata/"
}
}
}
```
此配置文件由“init_datasrv.sh”脚本在数据服务器容器启动时加载。 连接到数据服务器的所有应用程序服务器都必须可信。 此脚本将在 100 秒内自动验证所有连接,以限制管理门户中的手动操作。 当然,可以对其进行改进以提高安全性。
### 应用程序服务器
对于应用程序服务器,我们将:
* 启用 ECP 服务。
* 创建通信加密所需的 SSL 配置“%ECPClient”。
* 配置与数据服务器的连接信息。
* 创建远程数据库“myappdata”的配置。
* 在“USER”命名空间中创建到“myappdata”数据库的全局映射“demo.*”。 它可以让我们稍后测试 ECP 的运行。
[app-server.json](https://github.com/lscalese/ecp-with-docker/blob/master/config-files/app-server.json):
```json
{
"Security.Services" : {
"%Service_ECP" : {
"Enabled" : true
}
},
"Security.SSLConfigs": {
"%ECPClient": {
"CAFile": "${CA_ROOT}",
"CertificateFile": "${CA_CLIENT}",
"Name": "%ECPClient",
"PrivateKeyFile": "${CA_PRIVATE_KEY}",
"Type": "0"
}
},
"ECPServers" : {
"${DATASERVER_NAME}" : {
"Name" : "${DATASERVER_NAME}",
"Address" : "${DATASERVER_IP}",
"Port" : "${DATASERVER_PORT}",
"SSLConfig" : "1"
}
},
"Databases": {
"myappdata" : {
"Directory" : "/usr/irissys/mgr/myappdata/",
"Name" : "${REMOTE_DB_NAME}",
"Server" : "${DATASERVER_NAME}"
}
},
"MapGlobals":{
"USER": [{
"Name" : "demo.*",
"Database" : "myappdata"
}]
}
}
```
配置文件由“[init_appsrv.sh](https://github.com/lscalese/ecp-with-docker/blob/master/init_appsrv.sh)”脚本在应用程序服务器容器启动时加载。
## 启动容器
现在,我们可以启动容器:
* 2 个应用程序服务器。
* 1 个数据服务器。
为此,请运行:
docker-compose up –scale ecp-demo-app-server=2
请参阅 [docker-compose](https://github.com/lscalese/ecp-with-docker/blob/master/docker-compose.yml) 文件以了解详情:
```
# Variables are defined in .env file
# to show the resolved docker-compose file, execute
# docker-compose config
version: '3.7'
services:
ecp-demo-data-server:
build: .
image: ecp-demo
container_name: ecp-demo-data-server
hostname: data-server
networks:
app_net:
environment:
# List of allowed ECP clients (application server).
- CLIENT_SYSTEMS=ecp-with-docker_ecp-demo-app-server_1;ecp-with-docker_ecp-demo-app-server_2;ecp-with-docker_ecp-demo-app-server_3
# Path authority server certificate
- CA_ROOT=/certificates/CA_Server.cer
# Path to data server certificate
- CA_SERVER=/certificates/data_server.cer
# Path to private key of the data server certificate
- CA_PRIVATE_KEY=/certificates/data_server.key
# Path to Config-API file to initiliaze this IRIS instance
- IRIS_CONFIGAPI_FILE=/home/irisowner/demo/data-server.json
ports:
- "81:52773"
volumes:
# Post start script - data server initilization.
- ./init_datasrv.sh:/home/irisowner/demo/init_datasrv.sh
# Mount certificates (see gen-certificates.sh to generate certificates)
- ./certificates/app_server.cer:/certificates/data_server.cer
- ./certificates/app_server.key:/certificates/data_server.key
- ./certificates/CA_Server.cer:/certificates/CA_Server.cer
# Mount config file
- ./config-files/data-server.json:/home/irisowner/demo/data-server.json
# IRIS License
- ~/iris.key:/usr/irissys/mgr/iris.key
command: -a /home/irisowner/demo/init_datasrv.sh
ecp-demo-app-server:
image: ecp-demo
networks:
app_net:
environment:
# Hostname or IP of the data server.
- DATASERVER_IP=data-server
- DATASERVER_NAME=data-server
- DATASERVER_PORT=1972
# Path authority server certificate
- CA_ROOT=/certificates/CA_Server.cer
- CA_CLIENT=/certificates/app_server.cer
- CA_PRIVATE_KEY=/certificates/app_server.key
- IRIS_CONFIGAPI_FILE=/home/irisowner/demo/app-server.json
ports:
- 52773
volumes:
# Post start script - application server initilization.
- ./init_appsrv.sh:/home/irisowner/demo/init_appsrv.sh
# Mount certificates
- ./certificates/CA_Server.cer:/certificates/CA_Server.cer
# Path to private key of the data server certificate
- ./certificates/app_server.cer:/certificates/app_server.cer
# Path to private key of the data server certificate
- ./certificates/app_server.key:/certificates/app_server.key
# Path to Config-API file to initiliaze this IRIS instance
- ./config-files/app-server.json:/home/irisowner/demo/app-server.json
# IRIS License
- ~/iris.key:/usr/irissys/mgr/iris.key
command: -a /home/irisowner/demo/init_appsrv.sh
networks:
app_net:
ipam:
driver: default
config:
# APP_NET_SUBNET variable is defined in .env file
- subnet: "${APP_NET_SUBNET}"
```
## 我们来测试一下!
### 访问数据服务器管理门户
容器已启动。 我们从数据服务器中检查一下状态。
端口 52773 映射到本地端口 81,因此可以使用此地址 [http://localhost:81/csp/sys/utilhome.csp](http://localhost:81/csp/sys/utilhome.csp) 进行访问
使用默认登录名\密码登录,然后转到 System -> Configuration -> ECP Params(系统 -> 配置 -> ECP 参数)。 点击“ECP Application Servers”(ECP 应用程序服务器)。 如果一切正常,您应该会看到 2 个状态为“Normal”(正常)的应用程序服务器。 客户端名称的结构为 "数据服务器名称":"应用程序服务器主机名":"IRIS 实例名称"。 本例中,我们没有设置应用程序服务器主机名,因此我们将获得自动生成的主机名。
![应用程序服务器列表](https://raw.githubusercontent.com/lscalese/ecp-with-docker/master/img/app-server-list-en.png)
### 访问应用程序服务器管理门户
要连接到应用程序服务器的管理门户,首先需要获取端口号。 由于我们使用了“--scale”选项,我们无法在 docker-compose 文件中设置端口。 因此,必须使用 `docker ps` 命令检索它们:
```
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a1844f38939f ecp-demo "/tini -- /iris-main…" 25 minutes ago Up 25 minutes (unhealthy) 1972/tcp, 2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:81->52773/tcp, :::81->52773/tcp ecp-demo-data-server
4fa9623be1f8 ecp-demo "/tini -- /iris-main…" 25 minutes ago Up 25 minutes (unhealthy) 1972/tcp, 2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:49170->52773/tcp, :::49170->52773/tcp ecp-with-docker_ecp-demo-app-server_1
ecff03aa62b6 ecp-demo "/tini -- /iris-main…" 25 minutes ago Up 25 minutes (unhealthy) 1972/tcp, 2188/tcp, 53773/tcp, 54773/tcp, 0.0.0.0:49169->52773/tcp, :::49169->52773/tcp ecp-with-docker_ecp-demo-app-server_2
```
在本示例中,端口:
* 49170,用于第一个应用程序服务器 http://localhost:49170/csp/sys/utilhome.csp
* 49169,用于第二个应用程序服务器 http://localhost:49169/csp/sys/utilhome.csp
![数据服务器](https://raw.githubusercontent.com/lscalese/ecp-with-docker/master/img/data-server-status-en.png)
### 远程数据库上的读/写测试
我们在终端中执行一些读/写测试。
在第一个应用程序服务器上打开一个 IRIS 终端:
```
docker exec -it ecp-with-docker_ecp-demo-app-server_1 iris session iris
Set ^demo.ecp=$zdt($h,3,1) _ “ write from the first application server.”
```
现在,在第二个应用程序服务器上打开一个终端:
```
docker exec -it ecp-with-docker_ecp-demo-app-server_2 iris session iris
Set ^demo.ecp(2)=$zdt($h,3,1) _ " write from the second application server."
zwrite ^demo.ecp
```
您应该会看到两个服务器中的响应:
```
^demo.ecp(1)="2022-07-05 23:05:10 write from the first application server."
^demo.ecp(2)="2022-07-05 23:07:44 write from the second application server."
```
最后,在数据服务器上打开一个 IRIS 终端并执行全局 demo.ecp 读取:
```
docker exec -it ecp-demo-data-server iris session iris
zwrite ^["^^/usr/irissys/mgr/myappdata/"]demo.ecp
^["^^/usr/irissys/mgr/myappdata/"]demo.ecp(1)="2022-07-05 23:05:10 write from the first application server."
^["^^/usr/irissys/mgr/myappdata/"]demo.ecp(2)="2022-07-05 23:07:44 write from the second application server."
```
希望大家喜欢这篇文章。 欢迎您发表评论。
文章
姚 鑫 · 十一月 6, 2021
# 第六十八章 SQL命令 SAVEPOINT
在事务中标记一个点。
# 大纲
```java
SAVEPOINT pointname
```
## 参数
- `pointname` - 保存点的名称,指定为标识符。
# 描述
`SAVEPOINT`语句标记事务中的一个点。建立保存点使能够执行事务回滚到保存点,撤消在此期间完成的所有工作并释放在此期间获得的所有锁。在长期运行的事务或具有内部控制结构的事务中,通常希望能够回滚事务的一部分,而不撤消在事务期间提交的所有工作。
保存点的建立会递增`$TLEVEL`事务级别计数器。回滚到保存点会将`$TLEVEL`事务级别计数器递减到紧接在保存点之前的值。可以在一个事务内建立最多`255`个保存点。超过这个保存点数量会导致`SQLCODE-400`致命错误,这是在SQL执行期间捕获的`` 异常。终端提示符将当前事务级别显示为提示符的`TLn:`前缀,其中`n`是介于`1`和`255`之间的整数,表示当前$TLEVEL计数。
每个保存点都与一个保存点名称相关联,这是一个唯一的标识符。保存点名称不区分大小写。保存点名称可以是分隔的标识符。
- 如果指定的保存点没有点名,或者指定的点名不是有效的标识符或SQL保留字,则会发出运行时`SQLCODE-301`错误。
- 如果指定点名称以`“SYS”`开头的保存点,则会发出运行时`SQLCODE-302`错误。这些保存点名称是保留的。
保存点名称不区分大小写;因此`resetpt`,` ResetPt`和`“RESETPT”`是相同的点名。此重复项是在回滚到保存点期间检测到的,而不是在保存点期间检测到的。当指定具有重复点名的`SAVEPOINT`语句时, IRIS会递增事务级别计数器,就像点名是唯一的一样。但是,最近的点名称会覆盖保存点名称表中所有先前重复的值。因此,当指定回滚到保存点点名时, IRIS会回滚到具有该点名称的最近建立的保存点,并相应地递减事务级别计数器。但是,如果再次指定回滚到同名的保存点点名,则会生成`SQLCODE-375`错误,并显示`%msg:Cannot Rollback to Unestabled SavePoint‘name’`,整个事务将回滚,`$TLEVEL`计数恢复为`0`。
## 使用保存点
嵌入式SQL、动态SQL、ODBC和JDBC支持`SAVEPOINT`语句。在`JDBC`中,`connection.setSavepoint(Pointname)`设置一个保存点,`connection.roll back(Pointname)`回滚到指定的保存点。
如果已建立保存点,请执行以下操作:
- 回滚到保存点点名将回滚自指定保存点以来所做的工作,删除该保存点和所有中间保存点,并将`$TLEVEL`事务级别计数器递减删除的保存点数量。如果`pointname`不存在或已经回滚,此命令将回滚整个事务,将`$TLEVEL`重置为`0`,并释放所有锁。
- 回滚回滚当前事务期间完成的所有工作,回滚自`START TRANSACTION`以来完成的工作。它将`$TLEVEL`事务级别计数器重置为零,并释放所有锁。请注意,常规回滚会忽略保存点。
- `COMMIT`提交在当前事务期间完成的所有工作。它将`$TLEVEL`事务级别计数器重置为零,并释放所有锁。请注意,提交操作会忽略保存点。
在事务内发出第二个`START TRANSACTION`对保存点或`$TLEVEL`事务级别计数器没有影响。
如果事务操作未能成功完成,则会发出`SQLCODE-400`错误。
# 示例
以下嵌入式`SQL`示例创建具有两个保存点的事务:
```java
ClassMethod Savepoint()
{
n SQLCODE,%ROWCOUNT,%ROWID
&sql(
START TRANSACTION
)
&sql(
DELETE FROM Sample.Person WHERE Name = NULL
)
if SQLCODE = 100 {
w !,"没有要删除的空名称记录"
} elseif SQLCODE '= 0 {
&sql(ROLLBACK)
} else {
w !,%ROWCOUNT," 已删除Null Name记录"
}
&sql(
SAVEPOINT svpt_age1
)
&sql(
DELETE FROM Sample.Person WHERE Age = NULL
)
if SQLCODE = 100 {
w !,"没有要删除的空年龄记录"
} elseif SQLCODE '= 0 {
&sql(ROLLBACK TO SAVEPOINT svpt_age1)
} else {
w !,%ROWCOUNT," 删除空年龄记录"
}
&sql(
SAVEPOINT svpt_age2
)
&sql(
DELETE FROM Sample.Person WHERE Age > 65
)
if SQLCODE = 0 {
&sql(COMMIT)
} elseif SQLCODE = 100 {
&sql(COMMIT)
} else {
&sql(
ROLLBACK TO SAVEPOINT svpt_age2
)
w !,"退休年龄删除失败"
}
&sql(COMMIT)
&sql(COMMIT)
}
```
# ObjectScript和SQL事务
**使用`TSTART`和`TCOMMIT`的`ObjectScript`事务处理与使用SQL语句`START transaction`、`SAVEPOINT`和`COMMIT`的SQL事务处理不同,也不兼容。
ObjectScript和InterSystems SQL都提供了对嵌套事务的有限支持。
ObjectScript事务处理不与`SQL`锁控制变量交互;
特别需要关注的是`SQL`锁升级变量。
应用程序不应该尝试混合这两种事务处理类型。**
如果事务涉及`SQL`更新语句,则事务应该由`SQL START transaction`语句启动,并使用`SQL COMMIT`语句提交。
使用`TSTART/TCOMMIT`嵌套的方法可以包含在事务中,只要它们不初始化事务。
方法和存储过程通常不应该使用SQL事务控制语句,除非按照设计,它们是事务的主控制器。
文章
姚 鑫 · 六月 20, 2021
# 第十三章 将XML文档表示为DOM
`%XML.Document`类和`%XML.Node`类使可以将任意XML文档表示为DOM(文档对象模型)。然后,可以导航此对象并对其进行修改。还可以创建一个新的DOM并将其添加到其中。
**注意:使用的任何XML文档的XML声明都应该指明该文档的字符编码,并且文档应该按照声明的方式进行编码。如果未声明字符编码,InterSystems IRIS将使用本书前面的“输入和输出的字符编码”中描述的默认值。如果这些默认值不正确,请修改XML声明,使其指定实际使用的字符集。**
# 将XML文档作为DOM打开
要打开现有XML文档以用作DOM,请执行以下操作:
1. 创建`%XML.Reader`的实例。
2. 也可以指定此实例的`Format`属性,以指定要导入的文件的格式。
**默认情况下, IRIS假定XML文件为文字格式。如果文件是SOAP编码格式,则必须指明这一点,以便可以正确读取该文件。**
除非使用`Correlate()`和`Next()`,否则此属性无效。
3. 请使用`%XML.Reader`的以下方法之一。
- `OpenFile()` — 打开一个文件。
- `OpenStream()` —打开一个流。
- `OpenString()` — 打开字符串。
- `OpenURL()` — 打开URL。
在每种情况下,都可以选择为该方法指定第二个参数,以重写`Format`属性的值。
4. 访问`Document`属性,它是一个DOM。此属性是`%XML.Document`实例,它提供了可用于查找有关整个文档的信息的方法。例如,`CountNamespace()`返回DOM使用的名称空间总数。
或者,如果流包含XML文档,调用`%XML.Document`的`GetDocumentFromStream()`方法。返回`%XML.Document`的实例。
## 示例1:将文件转换为DOM
例如,下面的方法读取一个XML文件,并在表示该文档的返回`%XML.Document`的一个实例:
```java
ClassMethod GetXMLDocFromFile(file) As %XML.Document
{
s reader = ##class(%XML.Reader).%New()
s status = reader.OpenFile(file)
if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF}
s document = reader.Document
q document
}
```
## 示例2:将对象转换为DOM
以下方法接受`OREF`,并在表示该对象中返回`%XML.Document`的实例。该方法假定`OREF`是启用XML的类的实例:
```java
ClassMethod GetXMLDoc(object) As %XML.Document
{
//确保这是启用XML的类的实例
if '$IsObject(object){
w "参数不是对象"
q $$$NULLOREF
}
s classname = $CLASSNAME(object)
s isxml = $CLASSMETHOD(classname,"%Extends","%XML.Adaptor")
if 'isxml {
w "参数不是启用XML的类的实例"
q $$$NULLOREF
}
//步骤1-将对象作为XML写入流
s writer = ##class(%XML.Writer).%New()
s stream = ##class(%GlobalCharacterStream).%New()
s status = writer.OutputToStream(stream)
if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF}
s status = writer.RootObject(object)
if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF}
//步骤2-从流中提取%XML.Document
s status = ##class(%XML.Document).GetDocumentFromStream(stream,.document)
if $$$ISERR(status) {d $System.Status.DisplayError(status) q $$$NULLOREF}
quit document
}
```
# 获取DOM的名称空间
当 IRIS读取XML文档并创建DOM时,它会标识文档中使用的所有名称空间,并为每个名称空间分配一个索引号。
在`%XML.Document`实例提供了以下方法,可以使用这些方法查找有关文档中命名空间的信息:
### CountNamespace()
返回文档中的命名空间数。
### FindNamespace()
返回与给定命名空间对应的索引。
### GetNamespace()
返回给定索引的XML命名空间URI。
下面的示例方法显示一个报表,其中显示文档中使用的命名空间:
```java
ClassMethod ShowNamespaces(doc As %XML.Document)
{
s count = doc.CountNamespace()
w !, "文档中的命名空间数: "_count
for i = 1 : 1 : count {
w !, "Namespace "_i_" is "_doc.GetNamespace(i)
}
}
```
# 导航DOM的节点
要访问文档的节点,可以使用两种不同的技术:
- 使用`%XML.Document`实例的`GetNode()`方法。此方法接受一个整数,它指示从1开始的节点号。
- 调用`%XML.Document`实例的`GetDocumentElement()`方法。
此方法返回`%XML.Node`的实例,提供用于访问有关根节点的信息以及移动到其他节点的属性和方法。以下小节提供了有关使用`%XML.Node`的详细信息。
## 移动到子节点或同级节点
要移动到子节点或同级节点,请使用`%XML.Node`实例的以下方法。:
- `MoveToFirstChild()`
- `MoveToLastChild()`
- `MoveToNextSibling()`
- `MoveToPreviousSibling()`
这些方法中的每一个都移动到另一个节点(如方法名称所示)。如果是,则该方法返回TRUE。如果不是,则返回False,焦点与调用该方法之前相同。
这些方法中的每一个都有一个可选参数`skipWhitespace`。如果此参数为真,则该方法将忽略任何空格。`SkipWhitespace`的默认值为false。
## 移动到父节点
要移动到当前节点的父节点,请使用`%XML.Node`实例的`MoveToParent()`方法。
此方法接受一个可选参数`restrictDocumentNode`。如果此参数为真,则该方法不会移动到文档节点(根)。`restrictDocumentNode`的默认值为False。
## 移动到特定节点
要移动到特定节点,可以设置`%XML.Node`实例的`NodeId`属性。例如:
```java
set saveNode = node.NodeId
//..... lots of processing
//...
// restore position
set node.NodeId=saveNode
```
## 使用id属性
在某些情况下,XML文档可能包括名为`id`的属性,该属性用于标识文档中的不同节点。例如:
```java
Jack O'Neill
Samantha Carter
Daniel Jackson
```
如果(如本例所示)文档使用名为`id`的属性,则可以使用它导航到该节点。为此,可以使用文档的`GetNodeById()`方法,该方法返回`%XML.Node`的一个实例。(请注意,与大多数其他导航方法不同,此方法可从`%XML.Document`,而不是`%XML.Node`。)
文章
姚 鑫 · 七月 12, 2021
# 第一章 查询目录和驱动器
`%Library.File`(简称`%File`)为处理文件和目录提供了广泛的API。本文将介绍该API的主要功能。有关属性、方法和查询的规范列表,请参见类参考。
注意:如果指定了部分文件名或目录名,这些方法中的大多数都引用的项相对于包含正在使用的命名空间的默认全局数据库的目录。该目录在本文中称为“默认目录”。这条规则的任何例外都在文章中注明。
此外,仅当基础操作系统将文件名和目录名视为区分大小写时,这些方法才会将文件名或目录名视为区分大小写。也就是说,文件或目录名在Unix上区分大小写,但在Windows上不区分大小写。
# 查询目录和驱动器
## 列出目录的内容
`FileSet`类查询列出目录的内容。此查询按顺序接受以下参数:
1. `directory` — 指定要检查的目录的名称。
2. `wildcards` 通配符 — 指定要匹配的文件名模式(如果有)。
3. `sortby` 排序依据 — 指定如何对结果进行排序。使用以下值之一:
- `Name` 名称—文件的名称(默认)
- `Type` 类型—项目类型
- `DateCreated` 创建日期—创建文件的日期和时间
- `DateModified` 日期修改—文件上次修改的日期和时间
- `Size` 大小—文件大小
4. `includedirs` —指定如何处理给定目录中的目录。如果此参数为真(1),查询将返回任何文件之前的所有目录,并且目录名忽略通配符参数。如果此参数为false (0),通配符参数适用于文件和目录。默认值为0。
5. `delimiter` 分隔符—指定通配符参数中通配符之间的分隔符。默认值为;
此查询返回的结果集提供了以下字段:
- `Name` 名称—项目的完整路径名。
- `Type` 类型—项目的类型:`F`表示文件,`D`表示目录,`S`表示符号链接。
- `Size` 大小—文件大小,以字节为单位。对于目录和符号链接,此字段为空。
- `DateCreated` 创建日期—创建项目时的日期和时间,格式为`yyyy-mm-dd hh:mm:ss`。
- `DateModified` 日期修改—上次修改项目的日期和时间,格式为`yyyy-mm-dd hh:mm:ss`。
- `ItemName` 项目名称—项目的简称。对于文件,这是单独的文件名,没有目录。对于目录,这只是目录路径的最后一部分。
注意:Windows是目前唯一跟踪实际创建日期的平台。其他平台存储最后一次文件状态更改的日期。
下面是一个使用这个类查询的简单示例:
```java
/// desc:查看目标路径所有文件。
/// w ##class(Demo.FileDemo).ShowDir("C:\InterSystems\Cache\mgr", "*.log", "Size")
/// w ##class(Demo.FileDemo).ShowDir("E:\temp", "*.xml", "Size")
ClassMethod ShowDir(dir As %String = "", wildcard As %String = "", sort As %String = "Name")
{
s stmt = ##class(%SQL.Statement).%New()
s status = stmt.%PrepareClassQuery("%File", "FileSet")
if $$$ISERR(status) {
do $system.OBJ.DisplayError(status)
quit
}
s resultSet = stmt.%Execute(dir, wildcard, sort)
while resultSet.%Next() {
w !, resultSet.%Get("Name")
w " ", resultSet.%Get("Type")
w " ", resultSet.%Get("Size")
}
q ""
}
```
从终端对指定目录运行此方法,筛选日志文件,并按文件大小排序,结果如下所示:
```java
DHC-APP> w ##class(Demo.FileDemo).ShowDir("E:\temp", "*.xml", "Size")
E:\temp\testPerson.xml F 117
E:\temp\samplePerson.xml F 327
E:\temp\xmlnewtest.xml F 351
E:\temp\Person.xml F 259854
E:\temp\tempPerson.xml F 259854
```
又例如,下面的方法递归检查目录及其所有子目录,并写出它找到的每个文件的名称:
```java
/// w ##class(Demo.FileDemo).ShowFilesInDir("E:\temp")
ClassMethod ShowFilesInDir(directory As %String = "")
{
s stmt = ##class(%SQL.Statement).%New()
s status = stmt.%PrepareClassQuery("%File", "FileSet")
if $$$ISERR(status) {
d $system.OBJ.DisplayError(status)
q
}
s resultSet = stmt.%Execute(directory)
while resultSet.%Next() {
s name = resultSet.%Get("Name")
s type = resultSet.%Get("Type")
if (type = "F") {
w !, name
} elseif (type = "D"){
d ..ShowFilesInDir(name)
}
}
q ""
}
```
在默认目录下的终端中运行此方法会产生如下结果:
```java
DHC-APP>w ##class(Demo.FileDemo).ShowFilesInDir("E:\temp")
E:\temp\config.txt
E:\temp\game.jpg
E:\temp\Person.xml
E:\temp\ppg.txt
E:\temp\qcache.txt
E:\temp\rfc7158.html
E:\temp\rfc7158.txt
E:\temp\samplePerson.xml
E:\temp\SecurityXml.txt
E:\temp\temp1.txt
E:\temp\tempPerson.xml
E:\temp\test\Tests.xml
E:\temp\testPerson.xml
E:\temp\Testzf.dll
E:\temp\textReader.txt
E:\temp\xmlnewtest.xml
E:\temp\xmlXpath.txt
E:\temp\yaoxin.txt
E:\temp\yxtest.txt
E:\temp\yxtest_Errors.log
E:\temp\yxtest_Unsupported.log
E:\temp\汉子转拼音global.gof
```
## 列出驱动器或装载的文件系统
`Drivelist`类查询列出可用的驱动器(在Windows上)或已装载的文件系统(在Unix上)。此查询接受一个参数:
1. `fullyqualified`-如果此参数为1,则查询在每个Windows驱动器名称上都包含一个尾随反斜杠。对其他平台没有影响。默认值为0。
此查询返回的结果集提供了一个字段:
- `Drive` 驱动器—驱动器的名称(在Windows上)或装载的文件系统的名称(在Unix上)。
以下示例显示了如何使用该查询:
```java
/// w ##class(Demo.FileDemo).ShowDrives()
ClassMethod ShowDrives()
{
s stmt = ##class(%SQL.Statement).%New()
s status = stmt.%PrepareClassQuery("%File","DriveList")
if $$$ISERR(status) {
d $system.OBJ.DisplayError(status)
q
}
s resultSet = stmt.%Execute(1)
while resultSet.%Next() {
w !, resultSet.%Get("Drive")
}
q ""
}
```
在终端中运行该方法会得到如下结果:
```java
DHC-APP>w ##class(Demo.FileDemo).ShowDrives()
c:\
d:\
e:\
g:\
```
文章
姚 鑫 · 六月 11, 2021
# 第四章 添加命名空间声明
# 添加命名空间声明
## 默认行为
在`%XML.Writer`会自动插入命名空间声明,生成命名空间前缀,并在适当的地方应用前缀。例如,以下类定义:
```java
Class Sample.Person Extends (%Persistent, %Populate, %XML.Adaptor)
{
Parameter NAMESPACE = "http://www.yaoxin.com";
}
```
如果导出此类的多个对象,则会看到类似以下内容:
```java
DHC-APP> w ##class(Demo.XmlDemo).Obj2Xml(1)
yaoxin
111-11-1117
1990-04-25
889 Clinton Drive
St Louis
WI
78672
9619 Ash Avenue
Ukiah
AL
56589
濮氶懌
111-11-1115
Red
Orange
Yellow
Green
Red
Orange
Yellow
31
```
名称空间声明会自动添加到每个`` 元素。只将其添加到文档的根目录。
## 手动添加声明
可以控制何时将命名空间引入XML输出。以下方法都会影响所写入的下一个元素(但不会影响该元素之后的任何元素)。为方便起见,其中几种方法添加了标准的`W3`名称空间。
通常使用这些方法将命名空间声明添加到文档的根元素;也就是说,在调用`RootObject()`或`RootElement()`之前调用其中一个或多个方法。
注意:这些方法都没有将任何元素分配给名称空间,并且这些名称空间永远不会作为默认名称空间添加。在生成特定元素时,需要指明它使用的名称空间,如后面的“编写根元素”和“生成XML元素”中所述。
### AddNamespace()
```java
method AddNamespace(namespace As %String,
prefix As %String,
schemaLocation As %String) as %Status
```
添加指定的命名空间。这里,`Namespace`是要添加的名称空间,`Prefix`是该名称空间的可选前缀,`schemaLocation`是指示相应架构位置的可选URI。
如果未指定前缀,则会自动生成前缀(格式为S01、S02等)。
下面的示例显示了此方法的效果。首先,假设`Person`类被分配给一个名称空间(类参数中的`NAMESPACE`)。如果在未首先调用`AddNamespace()`方法的情况下生成此类实例的输出,则可能会收到如下所示的输出:
```java
Love,Bart Y.
...
```
或者,在编写根元素之前按如下方式调用`AddNamespace()`方法:
```java
set status=writer.AddNamespace("http:///www.person.org","p")
```
如果随后生成根元素,则输出如下所示:
```java
...
```
或者,假设在调用`AddNamespace()`方法时指定了第三个参数,该参数提供了关联架构的位置:
```java
set status=writer.AddNamespace("http:///www.person.org","p","http://www.MyCompany.com/schemas/person.xsd")
```
在这种情况下,如果随后生成Root元素,则输出如下所示:
```java
...
```
### AddInstanceNamespace()
```java
method AddInstanceNamespace(prefix As %String) as %Status
```
添加W3架构实例命名空间。这里的前缀是用于此命名空间的可选前缀。默认前缀为`XSI`。
```java
...
```
### AddSchemaNamespace()
```java
method AddSchemaNamespace(prefix As %String) as %Status
```
添加`W3`架构命名空间。这里的前缀是用于此命名空间的可选前缀。默认前缀为`s`。
```java
...
```
### AddSOAPNamespace()
```java
method AddSOAPNamespace(soapPrefix As %String,
schemaPrefix As %String,
xsiPrefix As %String) as %Status
```
添加`W3 SOAP`编码命名空间、`SOAP`架构命名空间和`SOAP`架构实例命名空间。此方法有三个可选参数:用于这些命名空间的前缀。默认前缀分别为`SOAP-Enc`、`s`和`XSI`。
```java
...
```
### AddSOAP12Namespace()
```java
method AddSOAP12Namespace(soapPrefix As %String,
schemaPrefix As %String,
xsiPrefix As %String) as %Status
```
添加`W3 SOAP 1.2`编码命名空间、`SOAP`架构命名空间和`SOAP`架构实例命名空间。
```java
...
```
可以使用这些方法中的多个方法。如果使用其中的多个命名空间,则受影响的元素将包含所有指定命名空间的声明。
# 编写根元素
每个XML文档必须恰好包含一个根元素。有两种方法可以创建此元素:
- 根元素可能直接对应于一个启用了InterSystems IRIS XML的对象。
在本例中,使用`RootObject()`方法,该方法将指定的启用XML的对象作为根元素写入。输出包括该对象中包含的所有对象引用。根元素获取该对象的结构,不能插入其他元素您可以指定根元素的名称,也可以使用由启用XML的对象定义的默认值。
前面的示例使用了此技术。
- 根元素可能只是一组元素的包装器(可能是一组支持XML的对象)。
在本例中,使用`RootElement()`方法,该方法插入具有指定名称的根级元素。如果此文档缩进,此方法还会增加后续操作的缩进级别。
然后调用其他方法为根元素内的一个或多个元素生成输出。在根目录中,可以按照选择的任何顺序或逻辑包含所需的元素。之后,调用`EndRootElement()`方法关闭根元素。
在这两种情况下,都可以指定要用于根元素的命名空间,只有在启用了`XML`的类没有`Namespace`参数值的情况下才会应用该命名空间。
请记住,如果文档包含文档类型声明,则该`DTD`的名称必须与根元素的名称相同。
文章
姚 鑫 · 六月 12, 2021
# 第五章 生成XML元素
# 生成XML元素
如果使用`RootElement()`启动文档的根元素,则负责生成该根元素内的每个元素。有三个选择:
## 将对象生成为元素
可以从InterSystems IRIS对象生成输出作为元素。在本例中,使用`object()`方法,该方法写入支持XML的对象。输出包括该对象中包含的所有对象引用。可以指定此元素的名称,也可以使用在对象中定义的默认值。
只能在`RootElement()`和`EndRootElement()`方法之间使用`object()`方法。
此示例为给定启用XML的类的所有已保存实例生成输出:
```java
/// desc:将表里数据输出本地文件里
/// w ##class(PHA.TEST.Xml).WriteAll("Sample.Person")
ClassMethod WriteTableAllToXml(cls As %String = "", directory As %String = "E:\temp\")
{
if '##class(%Dictionary.CompiledClass).%ExistsId(cls) {
Write !, "类不存在或未编译"
Quit
}
s check=$classmethod(cls, "%Extends", "%XML.Adaptor")
If 'check {
Write !, "类不扩展%XML.Adaptor"
Quit
}
s filename = directory_"Person"_".xml"
s writer = ##class(%XML.Writer).%New()
s writer.Indent=1
s status = writer.OutputToFile(filename)
if $$$ISERR(status) { do $System.Status.DisplayError(status) quit }
s status=writer.RootElement("SampleOutput")
if $$$ISERR(status) { do $System.Status.DisplayError(status) quit }
//获取给定类范围内对象的ID
s stmt = ##class(%SQL.Statement).%New()
s status = stmt.%PrepareClassQuery(cls,"Extent")
if $$$ISERR(status) { do $System.Status.DisplayError(status) quit }
s rset = stmt.%Execute()
while (rset.%Next()) {
//对于每个ID,写入该对象
set objid = rset.%Get("ID")
set obj = $CLASSMETHOD(cls,"%OpenId",objid)
set status = writer.Object(obj)
if $$$ISERR(status) {Do $System.Status.DisplayError(status) Quit}}
d writer.EndRootElement()
d writer.EndDocument()
q ""
}
```
此方法的输出包含给定类的所有已保存对象,这些对象嵌套在根元素中。对于`Sample.Person`,输出如下:
```java
Tillem,Robert Y.
967-54-9687
1961-11-27
3355 First Court
Reston
WY
11090
4922 Main Drive
Newton
NM
98073
Red
47
Waters,Ed X.
361-66-2801
1957-05-29
5947 Madison Drive
...
```
## 手动构建元素
以手动构造XML元素。在本例中,使用`element()`方法,该方法使用提供的名称写入元素的开始标记。然后,可以编写内容、属性和子元素。使用`EndElement()`方法指示元素的结束。
相关方法如下:
### Element()
```java
method Element(tag, namespace As %String) as %Status
```
写入开始标记。可以为元素提供命名空间,只有在启用了XML的类没有`Namespace`参数的值时才会应用该命名空间。
### WriteAttribute()
```java
method WriteAttribute(name As %String,
value As %String = "",
namespace As %String,
valueNamespace As %String = "",
global As %Boolean = 0) as %Status
```
写入属性。必须指定属性名称和值。参数命名空间是属性名称的命名空间。参数`valueNamespace`是属性值的名称空间;当值在XML模式名称空间中定义时使用。
对于GLOBAL,如果属性在关联的XML架构中是全局的,因此应该有前缀,请指定TRUE。
如果使用此方法,则必须在`Element()`(或`RootElement()`)之后直接使用它。
### WriteChars()
```java
method WriteChars(text) as %Status
```
写入字符串,执行使该字符串适合作为元素内容所需的任何必要转义。参数必须`%String`类型或`%CharacterStream`类型。
### WriteCData()
```java
method WriteCData(text) as %Status
```
参数必须`%String`类型或`%CharacterStream`类型。
### WriteBase64()
```java
method WriteBase64(binary) as %Status
```
将指定的二进制字节编码为`base-64`,并将结果文本写入元素的内容。该参数的类型必须为`%Binary`或`%BinaryStream`。
### WriteBinHex()
```java
method WriteBinHex(binary) as %Status
```
将指定的二进制字节编码为二进制,并将结果文本写入元素的内容。该参数的类型必须为`%Binary`或`%BinaryStream`。
### EndElement()
```java
method EndElement() as %Status
```
结束可以与其匹配的元素。
只能在`RootElement()`和`EndRootElement()`方法之间使用这些方法。
注意:这里描述的方法旨在使能够向XML文档编写特定的逻辑片段,但在某些情况下,可能需要更多的控制。`%XML.Writer`类提供了一个附加方法`write()`,可以使用该方法编写任意字符串。有责任确保结果是格式良好的XML文档;不提供任何验证。
示例
下面是一个示例例程:
```java
/// w ##class(Demo.XmlDemo).WriteObjXml()
ClassMethod WriteObjXml()
{
set writer=##class(%XML.Writer).%New()
set writer.Indent=1
set status=writer.OutputToDevice()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.StartDocument()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.RootElement("root")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.Element("SampleElement")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.WriteAttribute("Attribute","12345")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.Element("subelement")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.WriteChars("yao")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndElement()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.Element("subelement")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.WriteChars("xin")
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndElement()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndElement()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndRootElement()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set status=writer.EndDocument()
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
q ""
}
```
```java
DHC-APP>w ##class(Demo.XmlDemo).WriteObjXml()
yao
xin
```
### 使用%XMLL.Element
在前一节中,我们使用了Element()并指定了要生成的元素;我们还可以指定名称空间。在某些情况下,类中使用%XML.Element的实例,而不是使用元素名称。此类具有以下属性:
- Local属性指定此元素是否为其父元素的本地元素,这会影响命名空间的控制。
- Namespace属性指定此元素的命名空间。
- Tagname属性指定此元素的名称。
这里还可以使用前面描述的WriteAttribute()方法。
文章
Tete Zhang · 九月 14, 2022
从消息查看器看到清除周期以外的消息没有被正常清除
这种情况先抽查这些消息所处的会话中是否有未完成操作周期的消息(状态为除“Completed”“Error”“Discarded”之外的状态)。如有,且定期清除任务配置了“KeepIntegrity”,且该环境并不需要保留这些消息,可通过关闭清除任务中的“KeepIntegrity”配置清除这些会话和包含的消息。如果有这类消息,但是定期清除任务未配置“KeepIntegrity”,可能是定期清除任务的逻辑或消息数据问题导致清楚任务查找的时候没有覆盖这些消息,请联系WRC帮助排查具体原因。
有关定期清除任务的更多信息请参见文档
Purging Production Data | Managing Productions | InterSystems IRIS for Health 2022.1
从消息查看器看不到清除周期之外的消息,但是^%GSIZE显示有global占据了很大的磁盘空间
这种情况需要具体排查每个较大的global。可能有以下原因:
系统定义的global占用很大空间。您可以联系WRC帮助排查具体原因。
自定义的global占用很大空间。这可能是消息中嵌套的持久化数据或流数据,或者是和消息没有直接关系的独立的表里面的数据。请对创建这种global的代码进行复盘,找到创建逻辑并增加相应的数据管理逻辑。
孤立消息 (Orphan Messages)
孤立消息是指不存在配套Message Header的所有消息对象。
定期清除任务会根据Message Header里的时间信息和状态信息去判断一条消息是否符合清除条件。Message Header是在消息从一个组件发向另一个组件的时候被创建的。所以,当我们创建将被永久存储的对象之前,我们都要思考:这个对象会被保存吗?被保存后会被发送到另一个组件吗?如果不会被发送,该对象将不存在配套的Message Header,也就不会被定期清除。这种情况我们需要开发相应的自定义逻辑去定期管理该表中的数据,或确保该对象被发送到某个组件以创建Message Header。消息对象中嵌套对象或流的情况要尤其注意,对每一个嵌套的对象或流都要定义相对应的%OnDelete()删除逻辑。
在测试阶段我们可以做如下的测试:
测试前跑^%GSIZE报告并检查磁盘存储
跑一套测试消息
用清除任务删除系统上所有的消息(DaysToKeep=0)
跑^%GSIZE报告并检查磁盘存储
如果对比前后的^%GSIZE报告和磁盘空间之后,发现清除任务完成后没有遗留多余的数据,那么这就证明我们的逻辑中对消息及相关嵌套数据进行了很好的管理。反之如果发现了遗留数据,我们可以在研发测试阶段就对问题进行排查,尽量避免开放生产环境以后出现磁盘满或数据库过大的问题。
如果发现了环境中有孤立消息的问题,请联系WRC进行排查和消息清除管理。
HL7v2:孤立字段(Orphan Segment)
HL7v2在数据库中的存储逻辑如下。
EnsLib.HL7.Message对象存在以下两个global里:
^EnsLib.H.MessageD
^EnsHL7.Segment
示例:
HL7v2消息 (^EnsLib.H.MessageD global):
1: ^EnsLib.H.MessageD = 1257406
2: ^EnsLib.H.MessageD(1257406) = $lb("","","2.3:ORU_R01",0,"2019-06-03 15:28:38.819","2.3.1","C:\Support\inarchive\testoru.txt_2019-06-03_11.28.38.814","","")
3: ^EnsLib.H.MessageD(1257406,"segs") = 5
4: ^EnsLib.H.MessageD(1257406,"segs",1) = "11612,25"
5: ^EnsLib.H.MessageD(1257406,"segs",2) = "11612,26"
6: ^EnsLib.H.MessageD(1257406,"segs",3) = "11612,27"
7: ^EnsLib.H.MessageD(1257406,"segs",4) = "11612,28"
8: ^EnsLib.H.MessageD(1257406,"segs",5) = "11612,29"
其中, 125706是该HL7v2消息的Object ID。Global值"11612,25","11612,26"指向相应的HL7v2字段。
HL7v2字段 (^EnsHL7.Segment global):
1: ^EnsHL7.Segment(11612) = 30
2: ^EnsHL7.Segment(11612,25) = "|^~\&MSH|^~\&||GA0000||VAERS PROCESSOR|20010331605||ORU^R01|20010422GA03|T|2.3.1|||AL|"
3: ^EnsHL7.Segment(11612,25,0,1257406) = ""
其中,11612是创建该HL7v2消息的进程 ID (PID)。^EnsHL7.Segment(11612,25) 存储了该字段的具体数据。^EnsHL7.Segment(11612,25,0,1257406) 中的第四个值(1257406)是这个字段所属消息的Object ID。
从以上示例可以看出,HL7v2字段数据存储于^EnsHL7.Segment global。所以在^%GSIZE中看到^EnsHL7.Segment global比 ^EnsLib.H.MessageD global大是正常现象。使用平台自带的逻辑在最新版本上目前也没有已知问题会导致孤立字段。如果您持续观察^%GSIZE报告,发现^EnsHL7.Segment global的大小出现异常增长,可以联系WRC排查是否有孤立字段的情况。