利用 IRIS for Health 实施 openEHR
一听到“openEHR”,你是否就感到不寒而栗?“原型”这个词是否让你起鸡皮疙瘩?
通过本文克服你的恐惧,利用 InterSystems IRIS for Health 的强大功能,轻松掌握 OpenEHR!
什么是 openEHR?
openEHR 是一项开放且与供应商无关的规范,旨在以语义丰富且长期可持续的方式表示、存储和交换临床信息。与许多互操作性标准定义固定消息结构的做法不同,openEHR 通过多层建模方法将临床知识与技术实现分离。
从本质上讲,openEHR 基于三个基本概念:
- 参考模型(RM)
该模型定义了健康记录中使用的核心结构,例如组合(Compositions)、条目(Entries)、观察结果(Observations)、评估(Evaluations)和操作(Actions)。参考模型(RM)刻意设计为通用且与技术无关。 - 原型
可机读的模型(以 ADL 表示),用于定义特定概念(如血压测量值或出院小结)的详细临床语义。原型对参考模型施加约束,并提供可重用的临床词汇。 - 模板(OPT文件)
基于原型构建的特化形式。模板将原型定制为适用于特定用例、系统或表单的形式(例如,生命体征模板或区域出院摘要)。模板消除了可选性,并生成系统可安全实施的操作性定义。
下面来看一个基于组合(Composition)的 RAW 文件示例(JSON 格式):
剧透
{
"_type": "COMPOSITION",
"archetype_node_id": "openEHR-EHR-COMPOSITION.diagnostic_summary.v1",
"archetype_details": {
"_type": "ARCHETYPED",
"archetype_id": {
"_type": "ARCHETYPE_ID",
"value": "openEHR-EHR-COMPOSITION.diagnostic_summary.v1"
},
"template_id": {
"_type": "TEMPLATE_ID",
"value": "ES - AP - Diagnoses and treatment"
},
"rm_version": "1.0.4"
},
"uid": {
"_type": "OBJECT_VERSION_ID",
"value": "diag-1003::ehr.local::1"
},
"name": {
"_type": "DV_TEXT",
"value": "Summary of diagnoses and treatment – Moderate COPD"
},
"language": {
"_type": "CODE_PHRASE",
"terminology_id": {
"_type": "TERMINOLOGY_ID",
"value": "ISO_639-1"
},
"code_string": "es"
},
"territory": {
"_type": "CODE_PHRASE",
"terminology_id": {
"_type": "TERMINOLOGY_ID",
"value": "ISO_3166-1"
},
"code_string": "ES"
},
"category": {
"_type": "DV_CODED_TEXT",
"value": "event",
"defining_code": {
"_type": "CODE_PHRASE",
"terminology_id": {
"_type": "TERMINOLOGY_ID",
"value": "openehr"
},
"code_string": "433"
}
},
"composer": {
"_type": "PARTY_IDENTIFIED",
"name": "Dr. García"
},
"context": {
"_type": "EVENT_CONTEXT",
"start_time": {
"_type": "DV_DATE_TIME",
"value": "2025-11-22T08:45:00Z"
},
"setting": {
"_type": "DV_CODED_TEXT",
"value": "primary medical care",
"defining_code": {
"_type": "CODE_PHRASE",
"terminology_id": {
"_type": "TERMINOLOGY_ID",
"value": "openehr"
},
"code_string": "228"
}
}
},
"content": [
{
"_type": "SECTION",
"archetype_node_id": "openEHR-EHR-SECTION.diagnoses_and_treatments.v1",
"name": {
"_type": "DV_TEXT",
"value": "Respiratory diagnosis and treatment"
},
"items": [
{
"_type": "EVALUATION",
"archetype_node_id": "openEHR-EHR-EVALUATION.problem_diagnosis.v1",
"name": {
"_type": "DV_TEXT",
"value": "Moderate COPD"
},
"archetype_details": {
"_type": "ARCHETYPED",
"archetype_id": {
"_type": "ARCHETYPE_ID",
"value": "openEHR-EHR-EVALUATION.problem_diagnosis.v1"
},
"template_id": {
"_type": "TEMPLATE_ID",
"value": "ES - AP - Diagnoses and treatment"
},
"rm_version": "1.0.4"
},
"language": {
"_type": "CODE_PHRASE",
"terminology_id": {
"_type": "TERMINOLOGY_ID",
"value": "ISO_639-1"
},
"code_string": "es"
},
"encoding": {
"_type": "CODE_PHRASE",
"terminology_id": {
"_type": "TERMINOLOGY_ID",
"value": "IANA_character-sets"
},
"code_string": "UTF-8"
},
"subject": {
"_type": "PARTY_SELF"
},
"data": {
"_type": "ITEM_TREE",
"archetype_node_id": "at0001",
"name": {
"_type": "DV_TEXT",
"value": "Diagnostic data"
},
"items": [
{
"_type": "ELEMENT",
"archetype_node_id": "at0002",
"name": {
"_type": "DV_TEXT",
"value": "Diagnosis description"
},
"value": {
"_type": "DV_TEXT",
"value": "Moderate COPD with exertional dyspnoea."
}
},
{
"_type": "ELEMENT",
"archetype_node_id": "at0003",
"name": {
"_type": "DV_TEXT",
"value": "Diagnosis code"
},
"value": {
"_type": "DV_CODED_TEXT",
"value": "J44.1",
"defining_code": {
"_type": "CODE_PHRASE",
"terminology_id": {
"_type": "TERMINOLOGY_ID",
"value": "ICD-10"
},
"code_string": "J44.1"
}
}
},
{
"_type": "ELEMENT",
"archetype_node_id": "at0004",
"name": {
"_type": "DV_TEXT",
"value": "Onset date"
},
"value": {
"_type": "DV_DATE_TIME",
"value": "2020-09-15T00:00:00Z"
}
}
]
}
},
{
"_type": "EVALUATION",
"archetype_node_id": "openEHR-EHR-EVALUATION.medication_summary.v1",
"name": {
"_type": "DV_TEXT",
"value": "Inhaled treatment for COPD"
},
"archetype_details": {
"_type": "ARCHETYPED",
"archetype_id": {
"_type": "ARCHETYPE_ID",
"value": "openEHR-EHR-EVALUATION.medication_summary.v1"
},
"template_id": {
"_type": "TEMPLATE_ID",
"value": "ES - AP - Diagnoses and treatment"
},
"rm_version": "1.0.4"
},
"language": {
"_type": "CODE_PHRASE",
"terminology_id": {
"_type": "TERMINOLOGY_ID",
"value": "ISO_639-1"
},
"code_string": "es"
},
"encoding": {
"_type": "CODE_PHRASE",
"terminology_id": {
"_type": "TERMINOLOGY_ID",
"value": "IANA_character-sets"
},
"code_string": "UTF-8"
},
"subject": {
"_type": "PARTY_SELF"
},
"data": {
"_type": "ITEM_TREE",
"archetype_node_id": "at0001",
"name": {
"_type": "DV_TEXT",
"value": "Associated medication tree"
},
"items": [
{
"_type": "ELEMENT",
"archetype_node_id": "at0002",
"name": {
"_type": "DV_TEXT",
"value": "Medication associated with the diagnosis"
},
"value": {
"_type": "DV_TEXT",
"value": "Inhaled salbutamol 100 mcg as needed; inhaled tiotropium 18 mcg every 24 hours."
}
}
]
}
}
]
}
]
}
这种分层建模方法使 openEHR 系统能够保持多年——甚至数十年——的稳定性,同时允许临床模型独立于底层软件平台进行演进。
openEHR 生态系统的一个关键组成部分是AQL(原型查询语言),这是用于检索存储在 openEHR 存储库中的临床数据的标准查询语言。
原型查询语言(AQL)
AQL 受 SQL 启发,但针对 openEHR 信息模型进行了适配。AQL 并非查询关系型表,而是允许开发人员利用原型路径,查询openEHR 组合(Compositions)中的结构化临床内容。
AQL 的主要特征
-
基于路径的查询:AQL使用原型路径(类似于 XPath)来导航组合的内部结构,例如:
/content[openEHR-EHR-OBSERVATION.blood_pressure.v1]/data/events/time - 临床语义感知:查询引用的是临床概念(原型、条目类型、数据点),而非数据库列名。
- 灵活的 WHERE 子句:AQL支持对 Composition 内的值进行过滤,例如收缩压 > 140 或与特定代码匹配的诊断。
- 多组合查询:可跨多个组合检索数据,例如某位患者随时间推移的所有观察结果。
- 供应商中立:原则上,任何支持 AQL 的 openEHR 实现都应接受相同的查询。
下面来看一个 AQL 的示例:
SELECT
c/uid/value AS composition_id,
o/data[at0001]/events[at0006]/data[at0003]/value/magnitude AS systolic,
o/data[at0001]/events[at0006]/data[at0004]/value/magnitude AS diastolic
FROM
EHR e
CONTAINS COMPOSITION c
CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v1]
WHERE
e/ehr_id/value = '12345'
AND
o/data[at0001]/events[at0006]/time/value > '2023-01-01T00:00:00Z'
OpenEHR 与 FHIR 的比较
从技术角度来看,OpenEHR 和 FHIR 非常非常相似。信息基于文档格式、REST API 等。让我们来看看 FHIR 和 OpenEHR 的主要概念及其关联:
.png)
要在 IRIS for Health 中实现 openEHR,我们需要什么?
原始组合存储
- 将接收到的组合数据以原始 JSON 或 XML格式原样存储。
- 任何转换操作均不得修改语义内容。
- 在本示例中,我们将使用 JSON 格式,但还有多种其他选项。
REST API 服务
我们的实现必须公开以下 REST API:
电子健康记录(EHR)服务
用于创建和查询患者的“病历”。
- POST /ehr:创建新的电子健康记录(EHR)。
- GET /ehr/{ehr_id}:检索EHR 元数据。
- GET /ehr?subject_id=?:根据外部标识符定位电子健康记录(EHR)。
组合服务
用于存储患者的临床信息。
- POST /ehr/{id}/composition:提交一个 RAW 格式的新组合。在可能的情况下,根据 OPT 进行验证
- GET /composition/{version_uid}:检索特定版本。
- GET /ehr/{id}/compositions:列出某份电子健康记录(EHR)的组合。
- DELETE /composition/{uid}:将组合标记为已删除(逻辑删除)。
AQL 查询端点
- POST /query/aql:接收 AQL 查询,将其转换为 IRIS SQL 或基于 JSON-path 的查找,并以 openEHR 规范 JSON 格式返回结果。
原始组合验证
我们必须强制基于 OPT2 文件对 RAW 组合进行验证,不能将收到的任何 JSON 数据直接保存到我们的存储库中。
在 IRIS for Health 中实现 openEHR
要实现 openEHR 服务器中所有可用功能需要一定时间,因此我将重点关注核心功能:
使用 Web 应用程序部署 REST API 服务
发布 REST API 非常简单,我们只需要两个组件:一个继承自 %CSP.REST 的类,以及 Web 应用程序列表中的一条新记录。让我们来看一下我们继承的 %CSP.REST 类的声明:
.png)
如您所见,我们已为存储库定义了所有必需的最小路由。其中包括组合管理、用于 RAW 验证的 OPT2 文件,以及 AQL 查询的执行。
.png)
在本示例中,我们不会定义任何安全配置,但建议使用 JWT 身份验证。
好,我们已经有了自己的类和 %CSP.REST 类,还有什么需要做的吗?
RAW 验证
openEHR 绝非新生事物,因此不难想象,已有多种库可用于支持 RAW 验证等功能。 在本示例中,我们使用了Arche库并对其进行了定制。Arche是一个用 Java 开发的开源库,用于根据 OPT2 文件验证 rawcompositions。该验证器是一个 JAR 文件,通过外部语言服务器(在 Docker 镜像部署中默认启用)进行配置,并在使用 JavaGateway 功能保存 raw 之前被调用:
.png)
set javaGate = $system.external.getJavaGateway()
set result = javaGate.invoke("org.validator.openehr.Cli", "validate", filePath, optPath)如果 raw 通过验证,JSON 文档将被保存到数据库中。
JSON 原始数据存储
虽然可以使用 DocDB,但我们希望利用 SQL 数据库的性能优势。与 openEHR 相关的一个最大问题是文档查询性能不佳,因此我们将对组合进行预处理,提取所有组合类型共有的信息,以提升查询效率。
我们的 Composition 类定义了以下属性:
Class OPENEHR.Object.Composition Extends (%Persistent, %XML.Adaptor) [ DdlAllowed ]
{
/// Description
Property ehrId As %Integer;
Property compositionUid As %String(MAXLEN = 50);
Property compositionType As %String;
Property startTime As %DateTime;
Property endTime As %Date;
Property archetypes As list Of %String(MAXLEN = 50000);
Property doc As %String(MAXLEN = 50000);
Property deleted As %Boolean [ InitialExpression = 0 ];
Index compositionUidIndex On compositionUid;
Index ehrIdIndex On ehrId;
Index ExampleIndex On archetypes(ELEMENTS);
}- ehrId:患者的电子健康记录标识符。
- compositionUid:组合标识符。
- compositionType:保存的组合类型。
- startTime:组合的创建时间。
- archetypes:组合中包含的原型列表。
- doc:JSON 格式的文档。
- deleted:表示软删除的布尔值。
这些索引将用于优化查询。
AQL 支持
正如前文所述,AQL 是一种基于路径的查询语言。我们该如何在 IRIS for Health 中模拟这种行为呢?欢迎了解 JSON_TABLE!
什么是 JSON_TABLE?
JSON_TABLE 函数返回一个表,该表通过将JSON值映射到列中,可在 SQL 查询中使用。从 JSON 值到列的映射以SQL/JSON 路径语言表达式形式编写。
作为表值函数,JSON_TABLE 返回的表可在SELECT语句的FROM子句中使用,以访问存储在 JSON 值中的数据;该表不会在不同查询之间保留。 在单个 FROM 子句中可以多次调用 JSON_TABLE,并且可以与其他表值函数并列使用。
我们已用 Python 实现了一个 ClassMethod 来将 AQL 转换为 SQL,但存在一个问题:AQL 基于原型的相对路径,而非绝对路径,因此我们需要确定每个原型的绝对路径,并将其与 AQL 的相对路径进行关联。
如何获取绝对路径?非常简单!当用户将组合的 OPT2 文件保存到 IRIS 时,我们就能获取到它!一旦获得绝对路径,我们会将其保存到该组合专用的 CSV 文件中(可保存在全局变量中或通过其他方式保存),因此, 我们只需从该特定组合文件中获取绝对路径;或者,如果 AQL 未定义该组合,则可在现有的 CSV 文件中搜索该 AQL 中原型的绝对路径。
让我们看看它是如何工作的。以下是一个通过特定 ICD-10 代码获取所有诊断的 AQL 示例:
SELECT
c/uid/value AS comp_uid,
c/context/start_time/value AS comp_start_time,
dx/data[at0001]/items[at0002]/value/value AS diagnosis_text,
dx/data[at0001]/items[at0003]/value/defining_code/code_string AS diagnosis_code
FROM EHR e
CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.diagnostic_summary.v1]
CONTAINS SECTION s[openEHR-EHR-SECTION.diagnoses_and_treatments.v1]
CONTAINS EVALUATION dx[openEHR-EHR-EVALUATION.problem_diagnosis.v1]
WHERE dx/data[at0001]/items[at0003]/value/defining_code/code_string
MATCHES {'E11', 'I48.0'}
ORDER BY c/context/start_time/value DESC
OPENEHR.Utils.AuxiliaryFunctions 类中的 Transform函数将其转换为:
SELECT comp_uid, comp_start_time, diagnosis_text, diagnosis_code
FROM (
SELECT c.compositionUid AS comp_uid,
jt_root.comp_start_time AS comp_start_time,
jt_n1.diagnosis_text AS diagnosis_text,
jt_n1.diagnosis_code AS diagnosis_code
FROM OPENEHR_Object.Composition AS c,
JSON_TABLE( c.doc, '$' COLUMNS ( comp_start_time VARCHAR(4000) PATH
'$.context.start_time.value' ) ) AS jt_root,
JSON_TABLE( c.doc, '$.content[*]?(@._type=="SECTION" && @.archetype_node_id==
"openEHR-EHR-SECTION.diagnoses_and_treatments.v1").items[*]?
(@._type=="EVALUATION" && @.archetype_node_id=="openEHR-EHR-EVALUATION.problem_diagnosis.v1")'
COLUMNS (
diagnosis_text VARCHAR(4000) PATH '$.data[*]?(@.archetype_node_id=="at0001").items[*]?
(@.archetype_node_id=="at0002").value.value',
diagnosis_code VARCHAR(255) PATH '$.data[*]?(@.archetype_node_id=="at0001").items[*]?
(@.archetype_node_id=="at0003").value.defining_code.code_string' ) ) AS jt_n1
WHERE ('openEHR-EHR-COMPOSITION.diagnostic_summary.v1' %INLIST (c.archetypes)
AND 'openEHR-EHR-EVALUATION.problem_diagnosis.v1' %INLIST (c.archetypes))
AND (jt_n1.diagnosis_code LIKE '%E11%' OR jt_n1.diagnosis_code LIKE '%I48.0%') ) U
ORDER BY comp_start_time DESC让我们尝试使用 AQL 调用我们的 REST API:
.png)
成功!
现在再试一个数值比较:
SELECT
c/uid/value AS comp_uid,
c/context/start_time/value AS comp_start_time,
a/items[at0024]/value/magnitude AS creatinine_value,
a/items[at0024]/value/units AS creatinine_units
FROM EHR e
CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.lab_results_and_medications.v1]
CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.laboratory_test_result.v1]
CONTAINS CLUSTER a[openEHR-EHR-CLUSTER.laboratory_test_analyte.v1]
WHERE a/items[at0001]/value/value = 'Creatinina (mg/dL)'
AND a/items[at0024]/value/magnitude BETWEEN 1.2 AND 1.8
ORDER BY c/context/start_time/value DESC
转换为:
SELECT comp_uid, comp_start_time, ldl_value, ldl_units
FROM (
SELECT c.compositionUid AS comp_uid, jt_root.comp_start_time AS comp_start_time,
jt_n1.ldl_value AS ldl_value, jt_n1.ldl_units AS ldl_units
FROM OPENEHR_Object.Composition AS c,
JSON_TABLE( c.doc, '$' COLUMNS ( comp_start_time VARCHAR(4000)
PATH '$.context.start_time.value' ) ) AS jt_root,
JSON_TABLE( c.doc, '$.content[*]?(@._type=="OBSERVATION" &&
@.archetype_node_id=="openEHR-EHR-OBSERVATION.laboratory_test_result.v1")
.data.events[*]?(@._type=="POINT_EVENT").data.items[*]?(@._type=="CLUSTER" &&
@.archetype_node_id=="openEHR-EHR-CLUSTER.laboratory_test_analyte.v1")'
COLUMNS (
ldl_value NUMERIC PATH '$.items[*]?
(@.archetype_node_id=="at0024").value.magnitude',
ldl_units VARCHAR(64) PATH '$.items[*]?
(@.archetype_node_id=="at0024").value.units',
_w1 VARCHAR(4000) PATH '$.items[*]?
(@.archetype_node_id=="at0001").value.value' ) ) AS jt_n1
WHERE ('openEHR-EHR-COMPOSITION.lab_results_and_medications.v1' %INLIST (c.archetypes)
AND 'openEHR-EHR-OBSERVATION.laboratory_test_result.v1' %INLIST (c.archetypes)
AND 'openEHR-EHR-CLUSTER.laboratory_test_analyte.v1' %INLIST (c.archetypes))
AND jt_n1._w1 = 'LDL (mg/dL)' AND jt_n1.ldl_value <= 130 )
U ORDER BY comp_start_time DESC.png)
又一次响亮的成功!
结论
正如您从本示例中所见,InterSystems IRIS for Health 为您提供了实施和部署 openEHR 存储库所需的所有工具。如果您有任何疑问,请随时留言!
免责声明
相关代码仅作为概念验证,其目的并非实现具备全部功能的存储库,而是为了证明使用 InterSystems IRIS for Health 完全可以实现这一目标。祝您使用愉快!
