Konzept: Die Ressource hat zwei Namen
Eine FHIR-Ressource trägt zwei Namen, die nichts miteinander zu tun haben. Wer sie verwechselt, baut eine Schnittstelle, die monatelang funktioniert und dann an einem Dienstagvormittag für ein Fünftel aller Patienten aufhört.
Resource.id | Resource.identifier | |
|---|---|---|
| Was es ist | die Adresse der Ressource auf diesem Server | der Name des Gegenstands in der Welt |
| Wer vergibt es | der Server, der sie speichert | die Organisation, die den Gegenstand verwaltet |
| Geltungsbereich | ein Server, ein Ressourcentyp | der Namensraum in identifier.system |
| Datentyp | primitiver Typ id — Zeichenvorrat eingeschränkt | komplexer Typ Identifier, value ist ein string |
| Beim Kopieren auf einen anderen Server | ändert sich | bleibt gleich |
| Darf gedeutet werden | nein | ja — dafür ist er da |
| v2-Entsprechung | keine | PID-3 (CX) |
Die letzte Zeile links ist der Grund, warum die Verwechslung überhaupt passiert: HL7 v2 kennt nichts, was der logischen Id entspricht. In v2 gibt es nur Geschäftsidentifier. Wer aus dieser Welt kommt und ein Feld sieht, das „id” heißt und einen Patienten eindeutig bezeichnet, greift danach — es ist die nächstliegende Entsprechung zu PID-3.1, und sie ist falsch.
Was der Standard über die id sagt
„Each resource has an
idelement which contains the ‘logical id’ of the resource assigned by the server responsible for storing it.”
Assigned by the server responsible for storing it. Nicht vom Sender, nicht vom Quellsystem.
„The logical id is unique within the space of all resources of the same type on the same server.”
Nicht weltweit, nicht im Verbund. Zwei Server dürfen für denselben Patienten verschiedene id führen, und beide haben recht.
„Logical ids are always opaque, and external systems need not and should not attempt to determine their internal structure.”
Opak. Kein System darf aus einer id etwas ablesen — und die Umkehrung, die darin mitsteckt: wenn niemand sie deuten darf, darf sie auch niemand mit Bedeutung bauen. Eine id, die man konstruiert, damit man sie später wiedererkennt, ist keine id mehr; sie ist ein Geschäftsidentifier, der sich als Adresse verkleidet hat.
Dazu die technische Einschränkung, an der es in der Praxis bricht:
Regex:
[A-Za-z0-9\-\.]{1,64}· „Logical ids (and therefore locations) are case sensitive.”
Der Zeichenvorrat der id ist kleiner als der jedes realen Krankenhaus-Identifiers. Nicht erlaubt sind unter anderem Unterstrich, Schrägstrich, Leerzeichen, Doppelpunkt, Raute, Klammern, Pluszeichen, Umlaute und alles außerhalb von ASCII.
In HL7 v2 ist der Geschäftswert opak — man darf ihn nicht deuten. In FHIR ist die logische Id opak — man darf sie nicht bauen. Beide Fehler sind derselbe: Man legt Bedeutung an einen Ort, der laut Spezifikation keine trägt.
Wer die Id vergibt — create, update, update as create
create — POST [base]/Patient. Der Server vergibt:
„If an
idis provided, the server SHALL ignore it.”
SHALL ignore ist die stärkste Form. Ein Server, der eine mitgeschickte id beim POST übernimmt, ist nicht konform.
update — PUT [base]/Patient/[id]. Der Client bestimmt die Adresse, zweimal — in der URL und im Rumpf:
„If no
idelement is provided, or the id disagrees with the id in the URL, the server SHALL respond with an HTTP400error code.”
update as create ist nicht festgelegt, sondern Serverentscheidung, angesagt über CapabilityStatement.rest.resource.updateCreate:
„Servers MAY choose to allow clients to
PUTa resource to a location that does not yet exist on the server.”
Genau hier entsteht die Versuchung: Wer updateCreate hat, kann die id frei wählen und spart pro Nachricht einen Suchlauf. Das ist erlaubt — und verschiebt die Zuständigkeit für die Adressvergabe stillschweigend vom Server zum Sender.
Wofür updateCreate gedacht ist, nennt die Spezifikation selbst: ein Client, der „is reproducing an existing data model on the server, and needs to keep original ids in order to retain ongoing integrity”, push-basiertes pub/sub, ein „agreed identification model” — mit ausdrücklichem Hinweis auf Sicherheitsimplikationen.
Das Muster dahinter:
updateCreateträgt, wenn die Id keine Geschäftsbedeutung hat, sondern nur eine Adresse ist, über die der Client verfügt. Client-vergebene UUIDs erfüllen das, eine Patientennummer nicht.
Die saubere Alternative: conditional update
Für „ich kenne die id nicht, aber den Geschäftsidentifier” gibt es einen eigenen Mechanismus:
PUT [base]/Patient?identifier=<system>|<value>
| Trefferlage | Verhalten |
|---|---|
kein Treffer, keine id im Rumpf | Server legt an |
| genau ein Treffer | Server aktualisiert die passende Ressource |
genau ein Treffer, id im Rumpf passt nicht | 400 |
| mehrere Treffer | 412 Precondition Failed |
Zwei Dinge daran zählen. Erstens: Der Sender nennt den Patienten mit dem Namen, der ihm gehört, und der Server übersetzt in seine Adresse — die Zuständigkeiten bleiben, wo sie hingehören, und der Zeichenvorrat der Nummer wird gleichgültig, weil sie nie in eine id-Position gerät.
Zweitens: Die 412-Zeile ist der Ort, an dem die Namensraumfrage wiederkommt. Mehrere Treffer heißt, das Paar aus system und value war nicht eindeutig. FHIR beantwortet die Frage „welcher Identifier führt?” nicht — es erzwingt nur, dass sie gestellt wird, und antwortet mit einem Fehlercode, wenn niemand sie vorher beantwortet hat. Der Vorteil ist nicht, dass das Problem verschwindet, sondern dass es laut wird.
Belegstand: Der conditional update ist in R4 als trial use gekennzeichnet. Wer ihn im Schnittstellenkonzept zusagt, sollte die Unterstützung im
CapabilityStatementdes Gegenübers nachweisen, nicht voraussetzen.
Drei Identitätsbegriffe, drei Reichweiten
| Konstrukt | Reichweite | v2-Verwandter |
|---|---|---|
Bundle.entry.fullUrl | innerhalb dieses Bundles | PID-1 (Set ID) |
Resource.id | auf diesem Server | — (existiert in v2 nicht) |
Identifier (system + value) | in der Welt | PID-3 (CX) |
Ein Identitätsbegriff ist nur so viel wert wie sein Geltungsbereich. Wer eine Kennung darüber hinaus benutzt, hat keine Abkürzung gefunden, sondern eine unausgesprochene Zusatzannahme eingebaut. Bei
PID-1fällt das sofort auf. BeiResource.idnicht — weil sie in der URL steht, dauerhaft aussieht und sich anfühlt wie ein Primärschlüssel.
Die Ebenenleiter in FHIR
| # | Sprosse | Was auf ihr behauptet wird | v2-Gegenstück |
|---|---|---|---|
| 1 | Wert | „der Nachname lautet Ehrlich” | Wert |
| 2 | Element | „dies ist ein Nachname und keine Adresse” | Feld / Komponente |
| 3 | Ressource | „dies ist eine Person und kein Aufenthalt” | Segment |
| 4 | Interaktion | „diese Ressource liegt unter dieser Adresse” | Anordnung / Eventtyp |
| 5 | Referenzgeflecht | „dieser Aufenthalt gehört zu diesem Patienten” | Struktur |
| 6 | Profil | „so sieht ein Patient in diesem Land aus” | Bezugsdokument |
Sprosse 4 ist neu und hat kein sauberes v2-Gegenstück. Sie existiert, weil FHIR ein Protokoll ist: Ein Teil der Aussage steckt nicht im Rumpf, sondern in HTTP-Methode und URL. POST /Patient und PUT /Patient/x können denselben Rumpf tragen und Verschiedenes sagen.
Ein FHIR-Defekt kann außerhalb der Ressource liegen und trotzdem in ihr sichtbar werden. Wer eine fehlerhafte Ressource vorgelegt bekommt und nur hineinschaut, sieht die Hälfte.
Portalbezug
Ein Adapter, der Patientennummern als logische Id verwendet, spart pro Nachricht eine Abfrage und funktioniert in jedem Test. Er bricht an dem Tag, an dem der erste Wert auftritt, den der Datentyp nicht zulässt — bei einer Fusion, einem zweiten Haus, einem geänderten Nummernkreis. Vorher ist der Defekt nicht abwesend, sondern unbeobachtet.
Beispiel (synthetisch)
Ein Kommunikationsserver bildet eine ADT^A01 auf ein Transaction-Bundle ab. So sieht es aus, wenn die Patientennummer als Adresse benutzt wird:
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [{
"fullUrl": "https://portal.synth.example/fhir/Patient/SUED_778123",
"resource": {
"resourceType": "Patient",
"id": "SUED_778123",
"identifier": [{
"type": {"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v2-0203", "code": "MR"}]},
"system": "urn:oid:1.2.3.4.5",
"value": "SUED_778123"
}],
"name": [{"use": "official", "family": "Ehrlich", "given": ["Marlene"]}]
},
"request": {"method": "PUT", "url": "Patient/SUED_778123"}
}]
}Der Wert steht dreimal, und er ist an einer Stelle richtig und an zwei falsch:
| Stelle | Bewertung |
|---|---|
identifier[0].value | richtig. Genau dafür ist das Element da; der Unterstrich ist unproblematisch |
Patient.id | ungültig. Verstößt gegen [A-Za-z0-9\-\.]{1,64} |
request.url | ungültig, aus demselben Grund — und hier entsteht die Aussage „diese Ressource liegt unter dieser Adresse” |
fullUrl | Folgefehler |
Sauber gebaut — die Nummer bleibt, wo sie hingehört, der Server vergibt die Adresse:
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [{
"fullUrl": "urn:uuid:3f2a8c14-7b91-4d0e-9a56-11c4d8e07b23",
"resource": {
"resourceType": "Patient",
"identifier": [{
"type": {"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v2-0203", "code": "MR"}]},
"system": "https://fhir.klinikverbund-synth.example/sid/patientennummer-sued",
"value": "SUED_778123"
}],
"name": [{"use": "official", "family": "Ehrlich", "given": ["Marlene"]}]
},
"request": {
"method": "PUT",
"url": "Patient?identifier=https%3A%2F%2Ffhir.klinikverbund-synth.example%2Fsid%2Fpatientennummer-sued%7CSUED_778123"
}
}]
}Drei Unterschiede, jeder für sich tragend: kein Patient.id im Rumpf · ein echter, hausspezifischer Namensraum statt eines Platzhalter-OID aus der Projektvorlage · die Nummer steht in einem Suchparameterwert, wird prozentkodiert und unterliegt keiner Formregel.
Einordnung: Kategorie 2, und die Reihenfolge, die daraus folgt
Der abgelehnte Aufruf ist ein harter Fehler — der Empfänger bricht ab, mit 400 und OperationOutcome. Die naheliegende Alternative ist 4 (Mapping-Fehler), und ihre Bedingung 2 ist sogar erfüllt: Ein Geschäftswert wird auf ein Zielelement abgebildet, für das er nicht taugt, per vereinbartem Bezugsdokument. Ihre Bedingung 1 scheitert — der Sender erzeugt eine gegen die Basisspezifikation ungültige Ressource, also sind nicht beide Seiten konform.
Die Kategorien 3 bis 6 setzen alle voraus, dass die Nachricht angenommen und verarbeitet wurde. Wird sie abgelehnt, ist die Einordnung 1 oder 2 — unabhängig davon, wie interessant die Ursache ist. Die Ursache beschreibt die Reparatur, nicht die Kategorie.
Daraus die Reihenfolge im Raster: Erste Frage ist nicht „wo liegt der Fehler?”, sondern „ist die Nachricht angenommen worden?”
Und eine Eigenschaft, die Kategorie 2 von allen anderen unterscheidet: Ihr Auftreten hängt von der Datenlage ab, nicht vom Vertrag. Dieselbe Abbildung kann zehn Wochen fehlerfrei laufen und an dem Tag brechen, an dem der erste Wert auftritt, der die Typprüfung verletzt.
Zwei Sätze, die über diesen Fall hinausreichen
Nicht der Wert ist falsch, sondern seine Position. Ein Wert hat keine Gültigkeit an sich, sondern nur relativ zu dem Element, in dem er steht — derselbe Wert kann in derselben Nachricht gleichzeitig einwandfrei und ungültig sein.
Erlaubt ist nicht dasselbe wie vorgesehen. Dass eine Spezifikation ein Verhalten zulässt, ist kein Argument dafür, es zu benutzen.
Und für die Abwägung zwischen zwei Reparaturwegen: Ein harter Fehler kostet Zeit, ein stiller kostet Daten. Wer einen 400 durch eine Wertanpassung ersetzt, die stille Fehlzuordnungen erzeugt, hat die Fehlerklasse verschlechtert, nicht das Problem gelöst.
Transfer
Für ein Schnittstellenkonzept folgt daraus eine Zeile, die in den meisten fehlt: Wer vergibt die logische Id, und aus welchem Zeichenvorrat stammen die Werte, die in Adresspositionen geraten? Beides ist in fünf Minuten prüfbar und wird in der Regel nie gemessen.
Für eine Migration folgt daraus die unbequemere Konsequenz: Resource.id überlebt einen Serverwechsel nicht. Jede Beziehung, die ausschließlich über logische Ids geführt wird, muss danach neu hergestellt werden — es sei denn, die Geschäftsidentifier wurden mitgeführt.
Fragen an die Kunden-IT
- Ist die Patientennummer über den gesamten Verbund eindeutig — oder nur innerhalb des jeweiligen Hauses?
- Welcher Namensraum (
identifier.system) wird je Haus verwendet, und wer hat ihn vergeben? - Unterstützt der Zielserver
conditionalUpdate, und steht das imCapabilityStatement? - Welchen Zeichenvorrat haben Ihre Patientennummern tatsächlich — nicht laut Vorgabe, sondern gemessen am Bestand?
- Falls logische Ids client-seitig vergeben werden: Auf welcher vertraglichen Grundlage, und was passiert bei einem Serverwechsel?
Lektüre & Belege
- FHIR R4, Resource — logische Id, Opazität, Geltungsbereich
- FHIR R4, Datentypen — Formregel und Case-Sensitivität
- FHIR R4, RESTful API — create/update, update as create, conditional update
- Die Referenz ist ein Versprechen — dieselbe Unterscheidung, eine Ebene weiter
- Der Identifier ist ein Paar — die v2-Seite: Wert und Namensraum
- Der Namensraum im Wert — dort wandert der Namensraum in den Wert, hier der Wert in die Adresse
- Die sechs Fehlerkategorien · Sandbox — Sequenz 1 führt die Id-Vergabe ausführbar vor