原文在这里
FHIR标准提供灵活的扩展与再约束机制 - Profile。到底Profile机制如何工作?什么样的扩展需要用到Profile?怎么建立Profile?
FHIR核心资源模型 - FHIR Core
FHIR发布的资源模型是按80/20原则设计的 - 最常用那80%的用例中需要的数据会被涵盖在FHIR核心资源模型中,这些数据需求可能只是所有用例需要数据的20%;通过对核心资源的扩展和再约束,可以让它们适用于不常见的20%用例和未被涵盖的80%的数据需求。
FHIR发布的资源模型是FHIR核心资源模型(FHIR Core),它们有如下特点:
-
对象模型 - 有继承关系。所有资源都继承自DomainResource
-
为了保证最大的适用度,资源中的绝大多数属性的最小基数都是0,意味着它们都可以为空
-
资源的属性可以通过code(值集)、coding、CodeableConcept(术语)进行取值范围约束
-
为了防止医疗错误,所有的属性都没有且不能有默认值
当需要对核心资源模型进行扩展和再约束时,FHIR提供了profile机制对所做的扩展和再约束进行画像(描述),这就是profile的意思。
这里把对FHIR资源模型的用户自定义扩展区分为“扩展”和“再约束”,差异是:
扩展指需要要增加新的资源属性
再约束指修改核心资源模型的属性的基数、值集绑定、必须支持、切片等
扩展并不是在FHIR核心资源模型上直接增加新的属性,而是通过定义类型为Extension的StructureDefinition来实现的;再约束同样不是直接修改核心资源模型的属性特性,也是通过定义一个StructureDefinition实现的。整体思路是用户做的扩展和再约束都不会对发布的FHIR核心资源模型进行任何的修改,从而保证了标准的一致性。
注:profile机制不仅可以扩展和再约束FHIR资源模型,还可以扩展值集(ValueSet)、查询参数(SearchParameter)、操作(OperationDefinition)等,本文只介绍profile对资源模型等扩展和再约束。
这套机制到底是怎么做到不对资源模型进行修改而满足用户自定义需求的呢?
扩展
先看FHIR资源的扩展。
上面提到FHIR资源模型继承于DomainResource,这是一个FHIR发布的抽象资源,它里面定义了所有资源都需要的属性,包括:
- 用于资源描述text属性
- 可以包含其它资源的contained属性
- 用于放扩展属性的extension属性
- 用于放扩展属性,但这些扩展属性会改变资源含义的modifierExtension属性(由于它改为了资源的含义,因此这类扩展即使不认识也不能忽略,这个属性我们会写篇文章单独讨论)
这其中extension属性的基数是0…*,意味着所有资源无需做对模型做扩展就可以将需要自定义的任意数量的新属性放在extension中,即FHIR核心资源天生就可以扩展。即便没有profile,FHIR标准依然可以无障碍地交换数据。
那么为什么我们还要用profile机制来定义扩展呢?是因为不同用户对相同资源扩展的内容不一样,在共享交换的时候,要互相理解扩展的内容需要有一个一致的、计算机可解读的方法,这就是profile。有了profile,交换的双方可以据此对FHIR资源数据进行校验。
还是难于理解?我们举个例子,看看美国FHIR核心Profile里定义的患者扩展,它扩展了一个名为birthsex的属性,用于记录患者出生时的性别:
1. 首先定义了一个名为birthsex的Extension (Extension是一个FHIR发布的结构,但不是资源),它的值(value)绑定了出生时性别代码值集:
这个extension的定义实际上是一个StructureDefinition(也是FHIR资源)实例,是这样的:
![
在这里插入图片描述](https://img-blog.csdnimg.cn/0d84aeda34d94632803f7b39fd636764.png)
{
"resourceType" : "StructureDefinition",
"id" : "us-core-birthsex",
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
"name" : "USCoreBirthSexExtension",
"kind" : "complex-type",
"abstract" : false,
"type" : "Extension", //类型为Extension (扩展)
"baseDefinition" : "http://hl7.org/fhir/StructureDefinition/Extension",
"derivation" : "constraint",
...
"differential" : {
"element" : [{
"id" : "Extension",
"path" : "Extension",
"min" : 0,
"max" : "1"
},
{
"id" : "Extension.url",
"path" : "Extension.url",
"fixedUri" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex"
},
{
"id" : "Extension.value[x]",
"path" : "Extension.value[x]",
"min" : 1,
"max" : "1",
"type" : [{
"code" : "code"
}],
"binding" : { //定义值集绑定
"strength" : "required",
"description" : "Code for sex assigned at birth",
"valueSet" : "http://hl7.org/fhir/us/core/ValueSet/birthsex"
}
}
...
]
}
}
2. 将这个扩展关联到患者资源,并且定义birthsex可以出现的基数为0…1,即最多出现一次:
我们提到Patient资源根本不需要扩展就可以将birthsex放在自己的extension属性里,而profile的作用就是告诉其他人:我扩展的Patient放了什么,可以出现几次…
所以,US Core对Patient资源所做的“扩展”,其实也是下面这样定义了一个StructureDefinition,在里面将需要扩展的资源类型(Patient)和扩展(Extension结构)关联在一起,这就是profile机制:
{
"resourceType" : "StructureDefinition",
"id" : "us-core-patient",
"name" : "USCorePatientProfile",
...
"kind" : "resource",
"type" : "Patient", // 类型为Patient资源
"baseDefinition" : "http://hl7.org/fhir/StructureDefinition/Patient",
"differential" : { //定义差异
"element" : [
{
"id" : "Patient.extension:birthsex", // 扩展的birthsex的完整路径
"path" : "Patient.extension", // FHIR path路径
"sliceName" : "birthsex",
"short" : "Birth Sex Extension",
"min" : 0, //最小基数
"max" : "1", //最大基数
"type" : [{
"code" : "Extension",
"profile" : ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex"] //birthsex扩展结构的profile地址
}],
"mustSupport" : false,
...
},
...
]
}
}
那使用这个profile的patient实例是什么样的?像下面这样,它依然是一个标准的Patient资源实例,但在meta.profile里说明了是用什么profile进行扩展/再约束的,在extension里插入birthsex的结构:
{
"resourceType" : "Patient",
"id" : "example-targeted-provenance",
"meta" : {
"profile" : [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
]
},
"extension" : [
{
"id" : "birthsex",
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
"valueCode" : "F"
}
],
"identifier" : [
{
"system" : "http://hospital.smarthealthit.org",
"value" : "1032702"
}
],
"active" : true,
"name" : [
{
"family" : "Shaw",
"given" : [
"Amy",
"V."
]
}
],
"telecom" : [
{
"system" : "phone",
"value" : "555-555-5555"
}
],
"gender" : "female",
"birthDate" : "1987-02-20"
}
拿到这个patient资源实例数据的系统,就可以通过meta.profile的url获得profile详情并基于它对实例数据进行校验。
再约束
再约束是什么?
- FHIR核心资源模型的大多数属性的最小基数都是0,在实际用例里,我们可能需要限制很多属性必须要有值,而不能为空;而有时我们希望那些不需要的属性不要出现。这都需要我们修改属性出现的基数,例如从0…1改为1…1,确保其不能为空;从0…*改为0…0,确保其不出现。
- 有一些复杂需求要求特定的属性结构中的数据复合某种逻辑,例如要求Patient的name属性(是一个HumanName类型)中姓或名其中之一不能为空,这就不是通过修改基数能满足的。
- 另外,FHIR核心资源模型的很多属性都绑定了值集或术语,这些值集和术语可能和本地需要的不同,我们需要改为绑定本地值集或术语。
- 除此之外,我们还可能需要修改“切片”。最典型的就是Observation资源,它可以通过value属性表达单值的观察结果、也可以通过component数组属性表达多值的观察结果。如果需要记录包含了收缩压和舒张压的血压,我们用Observation资源表达时,需要限定component属性中仅出现收缩压和舒张压两项,且值是Quantity类型(带单位的数值型)。这相当于需要约束component数组属性只能切出特定的两片,不允许任何其他的切片。
- 甚至你可能需要替换特定属性的数据类型。
这些都是对FHIR核心资源模型的进一步约束,称之为再约束。
与扩展类似,再约束也是通过定义StructureDefinition资源来表达进一步约束的逻辑,而非直接修改资源模型。我们来看几个例子。
先看US Core对Patient的再约束,它对identifier、name和gender的基数做了修改,将最小基数从0改为1,即不能为空;另外,它修改了address.state的取值范围,将其绑定到了美国邮政USPS的2位字母的州代码值集上。
同样,这些修改都在US Core的Patient profile里,如下。注意Patient.identifier和Patient.gender的“min“为1,Patient.address.state的“binding.valueset”为"http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state":
{
"resourceType" : "StructureDefinition",
"id" : "us-core-patient",
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient",
"name" : "USCorePatientProfile",
"kind" : "resource",
"abstract" : false,
"type" : "Patient",
"baseDefinition" : "http://hl7.org/fhir/StructureDefinition/Patient",
"derivation" : "constraint",
...
"differential" : {
"element" : [
{
"id" : "Patient.identifier",
"extension" : [{
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement",
"valueBoolean" : true
}],
"path" : "Patient.identifier",
"min" : 1,
...
},
{
"id" : "Patient.gender",
"extension" : [{
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement",
"valueBoolean" : true
}],
"path" : "Patient.gender",
"min" : 1,
"type" : [{
"code" : "code"
}],
"mustSupport" : true,
"binding" : {
"strength" : "required",
"valueSet" : "http://hl7.org/fhir/ValueSet/administrative-gender"
},
...
},
{
"id" : "Patient.address.state",
"extension" : [{
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement",
"valueBoolean" : true
}],
"path" : "Patient.address.state",
"binding" : {
"strength" : "extensible",
"description" : "Two Letter USPS alphabetic codes.",
"valueSet" : "http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state"
},
...
}]
}
}
再看一个切片的例子 - US Core的血压体征profile,它基于Observation资源,但限制component数组属性只能有2个切片:舒张压(diastolic)和收缩压(systolic),每个切片的code是固定的,且每个切片的值类型为数量(quantity):
仔细看一下这个profile的定义,可以看到:
- Observation.component 这个数组属性的最小基数是2,即至少有2个切片(component)
- Observation.component:systolic 这个数组元素的基数是1…1,即收缩压必须出现
- Observation.component:systolic.code 基数是1…1(必须出现),且code必须是loinc的8480-6
- Observation.component:diastolic 这个数组元素的基数是1…1,即收缩压必须出现
- Observation.component:diastolic.code 基数是1…1(必须出现),且code必须是loinc的8462-4
{
"resourceType" : "StructureDefinition",
"id" : "us-core-blood-pressure",
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure",
"name" : "USCoreBloodPressureProfile",
"type" : "Observation",
"baseDefinition" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs",
...
"differential" : {
"element" : [
{
"id" : "Observation.component",
"extension" : [{
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement",
"valueBoolean" : true
}],
"path" : "Observation.component",
"slicing" : {
"discriminator" : [{
"type" : "pattern",
"path" : "code"
}],
"ordered" : false,
"rules" : "open"
},
"min" : 2,
"max" : "*",
"mustSupport" : true
},
{
"id" : "Observation.component:systolic",
"extension" : [{
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement",
"valueBoolean" : true
}],
"path" : "Observation.component",
"sliceName" : "systolic",
"short" : "(USCDI) Systolic Blood Pressure",
"min" : 1,
"max" : "1",
"mustSupport" : true
},
{
"id" : "Observation.component:systolic.code",
"extension" : [{
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement",
"valueBoolean" : true
}],
"path" : "Observation.component.code",
"short" : "(USCDI) Systolic Blood Pressure Code",
"min" : 1,
"max" : "1",
"patternCodeableConcept" : {
"coding" : [{
"system" : "http://loinc.org",
"code" : "8480-6"
}]
},
"mustSupport" : true
},
{
"id" : "Observation.component:diastolic",
"extension" : [{
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement",
"valueBoolean" : true
}],
"path" : "Observation.component",
"sliceName" : "diastolic",
"short" : "(USCDI) Diastolic Blood Pressure",
"min" : 1,
"max" : "1",
"mustSupport" : true
},
{
"id" : "Observation.component:diastolic.code",
"extension" : [{
"url" : "http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement",
"valueBoolean" : true
}],
"path" : "Observation.component.code",
"short" : "(USCDI) Diastolic Blood Pressure Code",
"min" : 1,
"max" : "1",
"patternCodeableConcept" : {
"coding" : [{
"system" : "http://loinc.org",
"code" : "8462-4"
}]
},
"mustSupport" : true
},
...
]
}
}
注意:
-
可能你在上面的profile截图里注意到有很多红底白字的
,它是必须支持 (must support),也是再约束的一部分。与不能为空意思不同,它的意思是这些标记为必须支持的属性,发送者应该确保这些属性被发送,接受者必须确保理解并处理这些属性、不能忽略。即便发送方发现标记为必须支持的属性值是空的,也需要给出空值的原因。不能为空是通过再约束最小基数实现的,而必须支持是上面profile里出现多次的"mustSupport" : true。 -
FHIR资源的属性类型很多都是复杂的结构 - 例如人名的类型为HumanName,这个结构也是可以通过extension扩展的。
如何开发和发布profile?
Profile是可以逐级开发的,例如美国在FHIR核心Patient资源模型上做的US Core Patient profile,美国的所有医院/用户可以在此基数上进一步做额外的扩展和再约束,避免重复的工作。如果你发现可用的profile,也可以直接拿来用,或者在它的基础上改造。
对于profile的开发,从上面的介绍可以看到profile就是一个StructureDefinition的FHIR资源实例,序列化为一个JSON或XML文档,所以任何的文本编辑器都可以编写。但使用文本编辑器需要自己检查所有项目是否填写准确、所有的数据拼写是否正确。FHIR生态下已经有很多免费的工具可以开发profile,帮助语法提示和拼写检查。如果是小规模开发,可以考虑Forge,它是一个可视化的profile编辑器;如果是大规模开发,可以考虑FSH。
而profile让交换的对方知道才有意义,所以profile应该是可访问的,最好能把它们发布出去。另外,通常会对很多资源进行profile,而且要让别人理解profile,最好还有对profile的用例说明、资源示例等,就像FHIR官网那样。那你就需要实施规范(ImplementationGuide,它也是FHIR资源),将所有的profile、资源示例、用例说明等汇总在一起,方便统一发布出去。而且FHIR还有实施指南注册和发布网站,可以免费注册和发布。