一、前言:为什么需要理解 FHIR 查询?
在医疗健康信息系统中,FHIR(Fast Healthcare Interoperability Resources)已成为事实上的数据交换标准。无论是设备管理、任务审批、还是患者服务,我们常常需要回答这样的问题:
- "有没有编码为
DEV-12345的设备?" - "当前机构下是否存在待审核的、关联该设备的任务?"
这些问题看似简单,但在 FHIR 架构中涉及资源模型设计、搜索参数规范、客户端实现细节等多个层面。
二、FHIR 查询基础:URL 参数如何工作?
2.1 FHIR 搜索的基本语法
FHIR 使用 RESTful 风格的 URL 进行资源查询,基本格式为:
http
GET [base]/[ResourceType]?[searchParameter]=[value]
例如:
http
GET /fhir/Device?identifier=DEV-12345
GET /fhir/Task?status=requested
每个资源类型(如 Device, Task)都定义了一组标准搜索参数(Search Parameters),这些参数决定了你可以按哪些字段过滤数据。
2.2 标识符(Identifier)查询详解
identifier 是 FHIR 中最常用的搜索参数之一,用于匹配资源的业务编码。
情况一:仅按值匹配(Value-only)
当资源的 identifier 字段没有 system 时:
json
{
"identifier": [{ "value": "DEV-12345" }]
}
查询方式:
http
GET /fhir/Device?identifier=DEV-12345
情况二:按系统+值精确匹配(System + Value)
当 identifier 包含命名空间(system)时:
json
{
"identifier": [{
"system": "http://example.org/device-id",
"value": "DEV-12345"
}]
}
查询方式(使用 | 分隔):
http
GET /fhir/Device?identifier=http://example.org/device-id|DEV-12345
✅ 最佳实践 :如果知道
system,优先使用完整格式,避免不同命名空间下的值冲突。
2.3 引用关系(Reference)与机构过滤
FHIR 资源常通过引用(Reference)关联其他资源。例如:
json
{
"owner": {
"reference": "Organization/abc-123-def"
}
}
理论上,应使用标准搜索参数 owner 查询:
http
GET /fhir/Device?owner=Organization/abc-123-def
但并非所有服务器都实现了标准参数 。有些系统会提供自定义参数 (如 organization=abc-123-def)以简化业务逻辑。
⚠️ 关键点 :必须通过实测确认服务器支持的参数名和格式。
三、典型场景分析:如何查询"设备"与"任务"?
3.1 场景一:查询设备编码为 XXX 的设备
步骤 1:确认设备资源结构
假设设备资源如下:
json
{
"resourceType": "Device",
"identifier": [{
"system": "http://example.org/device-id",
"value": "DEV-12345"
}],
"owner": { "reference": "Organization/org-789" }
}
步骤 2:构造查询 URL
-
如果只需查设备是否存在:
httpGET /fhir/Device?identifier=http://example.org/device-id|DEV-12345 -
如果还需限定机构(假设服务器支持
organization参数):httpGET /fhir/Device?identifier=http://example.org/device-id|DEV-12345&organization=org-789
🔍 验证方法 :用
curl或 Postman 手动测试,观察返回结果是否符合预期。
3.2 场景二:查询"关联某设备"的任务
这是更复杂的场景。任务(Task)本身不直接存储设备编码,常见实现方式有两种:
方式 A:任务包含 identifier 冗余字段
json
{
"resourceType": "Task",
"identifier": [{ "value": "DEV-12345" }], // 冗余存储设备编码
"status": "requested"
}
此时可直接查询:
http
GET /fhir/Task?identifier=DEV-12345&status=requested
方式 B:任务通过 focus 引用设备
json
{
"focus": { "reference": "Device/device-uuid" }
}
此时需使用链式查询(Chained Parameter):
http
GET /fhir/Task?focus:Device.identifier=http://example.org/device-id|DEV-12345
💡 现实情况 :许多系统采用方式 A(冗余存储),因为实现简单且查询高效。
3.3 场景三:增加机构过滤条件
无论查设备还是任务,若需限定"当前机构",需确认:
-
机构信息存储在哪个字段?
- Device: 通常在
owner - Task: 可能在
for,requester, 或自定义扩展
- Device: 通常在
-
服务器支持哪些搜索参数?
- 标准参数:
owner,for - 自定义参数:如
organization
- 标准参数:
通过实测发现,某系统支持:
http
GET /fhir/Task?identifier=DEV-12345&organization=org-789&status=requested
这说明该系统扩展了 organization 参数,接受纯机构 ID(无前缀)。
四、HAPI FHIR 客户端实现详解
有了清晰的 HTTP 接口认知,现在将其转化为类型安全、可维护的 Java 代码。
4.1 环境准备
- 依赖 :
hapi-fhir-client(版本 ≥ 6.0) - 资源模型 :R4(
org.hl7.fhir.r4.model.*)
📌 注意:HAPI FHIR 6.x 调整了 DSL 语法,本文使用最新稳定写法。
4.2 查询设备:按编码 + 机构
步骤 1:分析需求
- 输入:设备业务编码、机构 ID
- 输出:Device 资源或 null
步骤 2:确定搜索参数
identifier:有 system → 用systemAndIdentifierorganization:自定义字符串参数 → 用StringClientParam
步骤 3:编写代码
java
import ca.uhn.fhir.rest.gclient.StringClientParam;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Device;
public Device getDeviceByBusinessCode(String businessCode, String orgId) {
if (businessCode == null || orgId == null) {
throw new IllegalArgumentException("Business code and organization ID are required");
}
IGenericClient client = createFhirClient();
String identifierSystem = "http://example.org/device-id"; // 替换为实际 system
// 自定义搜索参数:organization
StringClientParam ORG_PARAM = new StringClientParam("organization");
Bundle bundle = client.search()
.forResource(Device.class)
// 精确匹配 identifier (system + value)
.where(Device.IDENTIFIER.exactly().systemAndIdentifier(identifierSystem, businessCode))
// 按机构过滤(传入纯 ID)
.where(ORG_PARAM.matches().values(orgId))
.count(1) // 性能优化
.returnBundle(Bundle.class)
.execute();
List<Device> devices = extractResourcesFromBundle(bundle, Device.class);
return devices.isEmpty() ? null : devices.get(0);
}
关键点说明:
StringClientParam("organization"):显式声明自定义参数名.matches().values(orgId):HAPI 6.x+ 的正确语法- 不加
Organization/前缀:根据服务器要求传纯 ID
4.3 查询任务:按设备编码 + 机构 + 状态
步骤 1:分析任务结构
identifier仅有 value(无 system)- 状态需为
requested - 机构通过自定义
organization参数过滤
步骤 2:编写代码
java
public Task getPendingTaskByDeviceCode(String deviceCode, String orgId) {
if (deviceCode == null || orgId == null) {
throw new IllegalArgumentException("Device code and organization ID are required");
}
IGenericClient client = createFhirClient();
StringClientParam ORG_PARAM = new StringClientParam("organization");
Bundle bundle = client.search()
.forResource(Task.class)
// 注意:Task.identifier 无 system,用 identifier(value)
.where(Task.IDENTIFIER.exactly().identifier(deviceCode))
.where(ORG_PARAM.matches().values(orgId))
.where(Task.STATUS.exactly().code("requested"))
.count(1)
.returnBundle(Bundle.class)
.execute();
List<Task> tasks = extractResourcesFromBundle(bundle, Task.class);
return tasks.isEmpty() ? null : tasks.get(0);
}
与设备查询的区别:
- 使用
Task.IDENTIFIER(而非Device.IDENTIFIER) - 调用
.identifier(deviceCode)(因无 system) - 增加状态过滤
.where(Task.STATUS.exactly().code("requested"))
五、常见陷阱与最佳实践
5.1 陷阱一:混淆资源类型的搜索常量
java
// ❌ 错误:查询 Task 却用 Device 的常量
.where(Device.IDENTIFIER.exactly()...)
虽然可能生成相同 URL,但:
- 代码语义错误
- 难以维护
- 违反类型安全
✅ 正确 :始终使用目标资源的常量(Task.IDENTIFIER)
5.2 陷阱二:忽略 identifier 的 system 差异
- Device 的 identifier 有 system → 用
systemAndIdentifier - Task 的 identifier 无 system → 用
identifier(value)
混用会导致查询失败。
5.3 陷阱三:给自定义参数加资源前缀
java
// ❌ 错误:服务器要求纯 ID
.organization("Organization/abc-123")
✅ 正确 :根据实测结果,传 abc-123
5.4 最佳实践
| 实践 | 说明 |
|---|---|
| 先手动测试 URL | 用 curl 验证参数有效性 |
| 区分标准 vs 自定义参数 | 文档化自定义行为 |
| 使用类型安全 DSL | 避免硬编码字符串 |
| 参数非空校验 | 防御性编程 |
| 限制返回数量 | 加 .count(1) 提升性能 |
| 方法命名清晰 | 如 getPendingTaskByDeviceCode |