4. RESTful交互与交换契约
FHIR 的 RESTful 交互不是一组“能调通就行”的接口,而是一份交换契约:客户端与服务端对每个动词的语义、返回码、幂等性和并发行为达成一致,系统间才能安全地交换临床数据。FHIR 定义了八类核心交互——read、search、create、update、patch、delete、history,以及 batch/transaction。理解它们的关键,是分清两件事:这个操作对资源状态做了什么,以及重复执行一次会不会产生额外副作用。
一、八类交互的语义与返回码
先看单资源交互。read 是“按已知标识取回当前版本”,请求 GET [base]/Patient/123,成功返回 200 与资源正文;若资源不存在返回 404,若已删除返回 410。search 是“按条件找资源”,请求 GET [base]/Patient?name=张&birthdate=ge1980-01-01,返回 200 与一个 Bundle(type=searchset),其中 total 表示匹配总数,entry 是结果页。search 是只读的,天然幂等。
create 是“新建资源”,请求 POST [base]/Patient,正文为资源 JSON。服务端分配 id 后返回 201,并在 Location 头给出新资源地址,如 Location: [base]/Patient/456/_history/1。create 不是幂等的:同一份正文提交两次,通常会得到两个不同 id 的资源。update 是“以客户端指定的 id 整体替换资源”,请求 PUT [base]/Patient/456,成功返回 200(或 201,若该 id 原本不存在且服务端允许 upsert)。update 是幂等的:同一份正文重复 PUT,资源最终状态一致。
patch 是“局部修改”,请求 PATCH [base]/Patient/456,正文可用 JSON Patch、FHIRPath Patch 或 XML Patch。它只改动指定字段,适合“只更新联系电话”这类场景。delete 是“删除”,请求 DELETE [base]/Patient/456,成功返回 204;FHIR 推荐逻辑删除而非物理删除,即把资源标记为已删除并保留历史,再次 read 返回 410。delete 幂等:重复删除同一资源,结果都是“该资源不存在”。
history 是“版本审计”,请求 GET [base]/Patient/456/_history 返回该资源所有版本的 Bundle,GET [base]/Patient/456/_history/2 返回第 2 版。它让“谁在什么时候改了什么”可追溯,是并发控制的基础。batch/transaction 是“批量提交”:请求 POST [base],正文是一个 type=batch 或 type=transaction 的 Bundle。batch 中各条目相互独立,各自成败;transaction 则要求全成功或全回滚,且条目之间可用 fullUrl 与 urn:uuid 建立引用,服务端按依赖顺序处理。
二、幂等性:重试安全的底线
幂等性指“同一请求执行一次与执行多次,服务端最终状态相同”。在医疗集成中,网络超时、消息重投、断点续传都很常见,幂等设计直接决定数据会不会被重复写入。read、search、update、patch、delete、history 都是幂等的;create 和 batch/transaction 不是。对非幂等操作,工程上通常用条件创建或客户端生成标识来补救:例如用 PUT /Patient/456 代替 create,由客户端先确定 id;或在 create 时携带 If-None-Exist 头,如 If-None-Exist: identifier=http://hospital.example/mrn|A1001,服务端发现已存在同标识资源时返回 200 而非新建,从而把 create 变成“存在即返回”的幂等语义。
三、并发控制:用版本号避免覆盖
两个医生同时修改同一份 Patient,后提交者可能覆盖前者。FHIR 用乐观锁解决:每个资源版本带一个 meta.versionId,更新时客户端在请求头带上 If-Match: W/"2"。服务端比对当前版本,若一致则更新并生成版本 3;若已被他人改为版本 3,则返回 412 Precondition Failed,客户端需重新 read 后再合并。删除同样可带 If-Match,防止误删已被修改的资源。history 交互则提供了版本回溯能力,配合 _since 参数还能做增量同步。
四、batch 与 transaction 的选择
两者都用 Bundle 承载,但语义不同。batch 适合“一批互不依赖的独立操作”,例如一次提交 20 条 Observation,其中 3 条校验失败,其余 17 条仍成功,返回的 Bundle 中每条 entry 带各自的 response.status。transaction 适合“必须整体一致”的场景,例如同时创建 Patient、Encounter 和 Observation,三者通过 urn:uuid 互相引用;只要有一条失败,服务端必须回滚全部,返回 4xx/5xx,客户端不会看到半成品数据。选择原则很简单:条目之间有引用依赖或业务上必须同生共死,用 transaction;只是批量提速、彼此独立,用 batch。
五、一个可复用的交互契约检查表
| 交互 | HTTP 方法与路径 | 成功码 | 幂等 | 关键请求头/参数 |
|---|---|---|---|---|
| read | GET /[type]/[id] | 200 | 是 | — |
| search | GET /[type]?[参数] | 200 | 是 | _count、_sort、_include |
| create | POST /[type] | 201 | 否 | If-None-Exist |
| update | PUT /[type]/[id] | 200/201 | 是 | If-Match |
| patch | PATCH /[type]/[id] | 200/204 | 是 | Content-Type: application/json-patch+json |
| delete | DELETE /[type]/[id] | 204 | 是 | If-Match |
| history | GET /[type]/[id]/_history | 200 | 是 | _since、_count |
| batch | POST / | 200 | 否 | Bundle.type=batch |
| transaction | POST / | 200/201 | 否 | Bundle.type=transaction |