1. EMR/HIS 統合の課題
ベトナムのほとんどの病院は、従来のリレーショナル データベースを備えた内部 HIS/EMR システムを使用しています。 FHIR の統合には多くの問題に対処する必要があります。
| チャレンジ | 説明 | 解決策 |
|---|---|---|
| 異なるスキーマ | HIS は独自のテーブルを使用し、FHIR はリソースを使用します | データマッピング層 |
| データが標準ではありません | 名前と住所がフォーマットされていません | データクレンジング + 正規化 |
| 内部コード | HIS は独自のコード (ICD-10 VN、病院薬剤コード) を使用します。 | コンセプトマップの翻訳 |
| レガシープロトコル | HL7 v2、CDA、XML/CSV ファイル | コンバーター/変圧器 |
| パフォーマンス | HIS は数千人のユーザーにサービスを提供しています | キャッシュ、非同期処理 |
| ダウンタイム | HISは止められない | 非侵襲的な統合 |
2. 統合パターン
パターン 1: FHIR ファサード
FHIR API レイヤーを既存の HIS の前に配置します。 変化なし 彼の。 Facade は、HIS データベースを直接読み取り/書き込みします。
┌─────────────┐ ┌──────────────┐ ┌──────────┐
│ FHIR Client │────▶│ FHIR Facade │────▶│ HIS DB │
│ (App/Portal)│◀────│ (Middleware) │◀────│(Oracle/ │
└─────────────┘ └──────────────┘ │ SQL Srv) │
└──────────┘
@Component
public class HISPatientFacade implements IResourceProvider {
@Autowired
private HISPatientRepository hisRepo;
@Autowired
private PatientMapper mapper;
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Read
public Patient read(@IdParam IdType theId) {
// Đọc từ HIS database
HISBenhNhan bn = hisRepo.findByMaBN(theId.getIdPart());
if (bn == null) {
throw new ResourceNotFoundException(theId);
}
// Map sang FHIR Patient
return mapper.toFhirPatient(bn);
}
@Search
public List<Patient> searchByName(
@RequiredParam(name = Patient.SP_NAME) StringParam name) {
List<HISBenhNhan> results = hisRepo.findByTenContaining(
name.getValue());
return results.stream()
.map(mapper::toFhirPatient)
.collect(Collectors.toList());
}
}
データマッパー: HIS → FHIR
@Component
public class PatientMapper {
public Patient toFhirPatient(HISBenhNhan bn) {
Patient patient = new Patient();
// ID
patient.setId(bn.getMaBenhNhan());
// Identifier: Mã BN
patient.addIdentifier()
.setSystem("http://bv-abc.vn/fhir/sid/mabn")
.setValue(bn.getMaBenhNhan());
// Identifier: CCCD
if (bn.getSoCCCD() != null) {
patient.addIdentifier()
.setSystem("http://fhir.vn/sid/cccd")
.setValue(bn.getSoCCCD());
}
// Identifier: BHYT
if (bn.getSoBHYT() != null) {
patient.addIdentifier()
.setSystem("http://fhir.vn/sid/bhyt")
.setValue(bn.getSoBHYT());
}
// Họ tên
HumanName name = patient.addName();
name.setFamily(bn.getHo());
name.addGiven(bn.getTen());
name.setText(bn.getHoTen());
// Giới tính: HIS dùng 0/1, FHIR dùng enum
switch (bn.getGioiTinh()) {
case 0: patient.setGender(AdministrativeGender.FEMALE); break;
case 1: patient.setGender(AdministrativeGender.MALE); break;
default: patient.setGender(AdministrativeGender.UNKNOWN);
}
// Ngày sinh
patient.setBirthDate(bn.getNgaySinh());
// Địa chỉ
Address address = patient.addAddress();
address.addLine(bn.getDiaChi());
address.setDistrict(bn.getQuanHuyen());
address.setCity(bn.getTinhThanh());
// Số điện thoại
if (bn.getSoDienThoai() != null) {
patient.addTelecom()
.setSystem(ContactPoint.ContactPointSystem.PHONE)
.setValue(bn.getSoDienThoai());
}
return patient;
}
public HISBenhNhan toHISBenhNhan(Patient patient) {
HISBenhNhan bn = new HISBenhNhan();
// Map ngược từ FHIR → HIS
HumanName name = patient.getNameFirstRep();
bn.setHo(name.getFamily());
bn.setTen(name.getGivenAsSingleString());
bn.setHoTen(name.getText());
// CCCD
patient.getIdentifier().stream()
.filter(i -> "http://fhir.vn/sid/cccd".equals(i.getSystem()))
.findFirst()
.ifPresent(i -> bn.setSoCCCD(i.getValue()));
// Giới tính
switch (patient.getGender()) {
case MALE: bn.setGioiTinh(1); break;
case FEMALE: bn.setGioiTinh(0); break;
default: bn.setGioiTinh(-1);
}
bn.setNgaySinh(patient.getBirthDate());
return bn;
}
}
パターン 2: データ同期 (ETL)
スケジュール (バッチ) またはリアルタイム (CDC) に従って、HIS から FHIR サーバーにデータを同期します。
┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐
│ HIS DB │────▶│ ETL/CDC │────▶│ FHIR Server │────▶│ FHIR Apps │
│ │ │ Pipeline │ │ (HAPI FHIR) │ │ │
└──────────┘ └───────────┘ └─────────────┘ └───────────┘
# ETL Pipeline: HIS PostgreSQL → FHIR Server
import psycopg2
import requests
from datetime import datetime
FHIR_BASE = "http://localhost:8080/fhir"
def sync_patients(last_sync: datetime):
"""Đồng bộ bệnh nhân mới/cập nhật từ HIS"""
conn = psycopg2.connect(
host="his-db", dbname="his",
user="readonly", password="***"
)
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT * FROM benh_nhan
WHERE ngay_cap_nhat > %s
ORDER BY ngay_cap_nhat
""", (last_sync,))
for row in cur:
patient = map_to_fhir_patient(row)
# Upsert: PUT với ID
resp = requests.put(
f"{FHIR_BASE}/Patient/{row['ma_bn']}",
json=patient,
headers={"Content-Type": "application/fhir+json"}
)
if resp.status_code in (200, 201):
print(f"Synced Patient/{row['ma_bn']}")
else:
print(f"Error: {resp.status_code} - {resp.text}")
conn.close()
def map_to_fhir_patient(row):
return {
"resourceType": "Patient",
"id": row["ma_bn"],
"identifier": [
{
"system": "http://bv-abc.vn/fhir/sid/mabn",
"value": row["ma_bn"]
}
],
"name": [{
"family": row["ho"],
"given": [row["ten"]],
"text": row["ho_ten"]
}],
"gender": "male" if row["gioi_tinh"] == 1 else "female",
"birthDate": row["ngay_sinh"].isoformat()
}
3. HL7 v2 から FHIR への変換
多くのレガシー システムは、HL7 v2 メッセージ (ADT、ORM、ORU) を介して通信します。 FHIR は変換マッピングを提供します。
MSH|^~\&|HIS|BVDK|FHIR|GATEWAY|20250115103000||ADT^A01|MSG001|P|2.5
EVN|A01|20250115103000
PID|1||BN001^^^BVDK||Nguyen^Van A||19900515|M|||123 Nguyen Hue^^HCM^^700000^VN||0901234567
PV1|1|I|KHOA-NOI^P-301^G-01||||BS001^Tran^Thi B
// HL7 v2 → FHIR converter
public class V2ToFhirConverter {
public Bundle convertADT_A01(Message v2Message) {
ADT_A01 adt = (ADT_A01) v2Message;
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
// PID → Patient
PID pid = adt.getPID();
Patient patient = new Patient();
patient.setId(pid.getPatientID().getIDNumber().getValue());
HumanName name = patient.addName();
name.setFamily(pid.getPatientName(0).getFamilyName()
.getSurname().getValue());
name.addGiven(pid.getPatientName(0).getGivenName().getValue());
String gender = pid.getAdministrativeSex().getValue();
patient.setGender("M".equals(gender)
? AdministrativeGender.MALE
: AdministrativeGender.FEMALE);
bundle.addEntry()
.setResource(patient)
.getRequest()
.setMethod(Bundle.HTTPVerb.PUT)
.setUrl("Patient/" + patient.getId());
// PV1 → Encounter
PV1 pv1 = adt.getPV1();
Encounter encounter = new Encounter();
encounter.setStatus(Encounter.EncounterStatus.INPROGRESS);
encounter.setSubject(new Reference(
"Patient/" + patient.getId()));
// Location
String ward = pv1.getAssignedPatientLocation()
.getPointOfCare().getValue();
encounter.addLocation()
.setLocation(new Reference("Location/" + ward));
bundle.addEntry()
.setResource(encounter)
.getRequest()
.setMethod(Bundle.HTTPVerb.POST)
.setUrl("Encounter");
return bundle;
}
}
4. CDA から FHIR への変換
// CDA Document → FHIR Bundle
public Bundle convertCDAToFhir(ClinicalDocument cda) {
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
// CDA Header → Composition
Composition composition = new Composition();
composition.setStatus(Composition.CompositionStatus.FINAL);
composition.setDate(cda.getEffectiveTime().getValue());
composition.setTitle(cda.getTitle().getText());
// CDA recordTarget → Patient
RecordTarget rt = cda.getRecordTargets().get(0);
Patient patient = mapCDAPatient(
rt.getPatientRole());
composition.setSubject(new Reference(
"Patient/" + patient.getId()));
// CDA Sections → Composition sections
for (Section cdaSection : cda.getComponent()
.getStructuredBody().getComponents()) {
Composition.SectionComponent section =
composition.addSection();
section.setTitle(cdaSection.getTitle().getText());
section.getText().setDiv(
cdaSection.getText().toXml());
// Map CDA entries → FHIR resources
for (Entry entry : cdaSection.getEntries()) {
IBaseResource resource = mapCDAEntry(entry);
bundle.addEntry().setResource(resource);
section.addEntry(new Reference(resource));
}
}
bundle.addEntry().setResource(composition);
bundle.addEntry().setResource(patient);
return bundle;
}
5. ConceptMap を使用したコード変換
{
"resourceType": "ConceptMap",
"url": "http://bv-abc.vn/fhir/ConceptMap/his-gender-to-fhir",
"name": "HISGenderToFHIR",
"title": "HIS Gender Code → FHIR AdministrativeGender",
"status": "active",
"group": [
{
"source": "http://bv-abc.vn/his/gender",
"target": "http://hl7.org/fhir/administrative-gender",
"element": [
{
"code": "0",
"display": "Nữ",
"target": [
{"code": "female", "relationship": "equivalent"}
]
},
{
"code": "1",
"display": "Nam",
"target": [
{"code": "male", "relationship": "equivalent"}
]
},
{
"code": "2",
"display": "Khác",
"target": [
{"code": "other", "relationship": "equivalent"}
]
}
]
}
]
}
# Dùng $translate operation
curl 'http://localhost:8080/fhir/ConceptMap/$translate?\
system=http://bv-abc.vn/his/gender&\
code=1&\
target=http://hl7.org/fhir/administrative-gender'
6. ミドルウェアアーキテクチャ
┌──────────┐ ┌──────────┐ ┌──────────┐
│ HIS 1 │ │ HIS 2 │ │ LIS │
│ (Oracle) │ │(SQL Srv) │ │ (HL7 v2) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────┐
│ Integration Engine │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ Adapters │ │ Mappers │ │ Router │ │
│ │ (DB/v2) │ │ (→FHIR) │ │ │ │
│ └──────────┘ └──────────┘ └────────┘ │
└────────────────┬───────────────────────┘
│
▼
┌──────────────┐
│ FHIR Server │
│ (HAPI FHIR) │
└──────┬───────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Portal │ │Mobile │ │ HIE │
│BN │ │App │ │Gateway │
└────────┘ └────────┘ └────────┘
7. 統合されたベストプラクティス
- 非侵襲的 — 既存の HIS を変更せず、ファサードまたは ETL を使用します
- べき等 — 固定IDでPUT、重複なしで同期
- 増分同期 — 変更データのみを同期します (タイムスタンプベースまたは CDC)
- エラー処理 — 失敗したレコードのデッドレターキュー
- モニタリング — 同期ラグ、エラー率、スループットを追跡します
- データ品質 — FHIR サーバーにプッシュする前に検証する
- コンセプトマップ — ハードコード マッピングではなく、コード変換に使用されます
- ロールバック — 元の HIS、FHIR をセカンダリとして保持します
8. まとめ
FHIRファサード — HIS の前の FHIR API レイヤー、リアルタイム マップは HIS を編集しません
データ同期 (ETL) — HIS DB → FHIR サーバーからバッチ/CDC を同期します
HL7 v2 → FHIR — ADT/ORM/ORU メッセージを FHIR リソースに変換します
CDA → FHIR — CDA ドキュメントを FHIR バンドルに変換
コンセプトマップ — HIS 内部コードを標準コード (LOINC、SNOMED、ICD-10) に変換します。
ミドルウェア — 統合エンジンは複数のデータソースを処理します