Xem bản video
1. Thách thức Tích hợp EMR/HIS
Hầu hết bệnh viện Việt Nam sử dụng hệ thống HIS/EMR nội bộ với cơ sở dữ liệu relational truyền thống. Tích hợp FHIR đòi hỏi xử lý nhiều vấn đề.
| Thách thức | Mô tả | Giải pháp |
|---|---|---|
| Schema khác biệt | HIS dùng bảng riêng, FHIR dùng resources | Data mapping layer |
| Dữ liệu không chuẩn | Tên, địa chỉ không theo format | Data cleansing + normalization |
| Mã nội bộ | HIS dùng mã riêng (ICD-10 VN, mã thuốc BV) | ConceptMap translation |
| Legacy protocols | HL7 v2, CDA, file XML/CSV | Converter/transformer |
| Performance | HIS phục vụ hàng ngàn user | Caching, async processing |
| Downtime | Không thể dừng HIS | Non-invasive integration |
2. Integration Patterns
Pattern 1: FHIR Facade
Đặt một lớp FHIR API phía trước HIS hiện tại, không thay đổi HIS. Facade đọc/ghi trực tiếp database 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());
}
}
Data Mapper: 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;
}
}
Pattern 2: Data Sync (ETL)
Đồng bộ dữ liệu từ HIS sang FHIR Server theo lịch (batch) hoặc real-time (CDC).
┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐
│ 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 to FHIR Conversion
Nhiều hệ thống cũ giao tiếp qua HL7 v2 messages (ADT, ORM, ORU). FHIR cung cấp mapping chuyển đổi.
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 to FHIR Transformation
// 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. Code Translation với 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. Middleware Architecture
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 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. Best Practices Tích hợp
- Non-invasive — Không thay đổi HIS hiện tại, dùng Facade hoặc ETL
- Idempotent — PUT với ID cố định, sync lại không bị trùng
- Incremental sync — Chỉ đồng bộ data thay đổi (timestamp-based hoặc CDC)
- Error handling — Dead letter queue cho records thất bại
- Monitoring — Track sync lag, error rate, throughput
- Data quality — Validate trước khi đẩy vào FHIR Server
- ConceptMap — Dùng cho code translation, không hardcode mapping
- Rollback — Giữ bản gốc HIS, FHIR là secondary
8. Tổng kết
FHIR Facade — Lớp API FHIR phía trước HIS, map realtime, không sửa HIS
Data Sync (ETL) — Đồng bộ batch/CDC từ HIS DB → FHIR Server
HL7 v2 → FHIR — Chuyển đổi ADT/ORM/ORU messages sang FHIR resources
CDA → FHIR — Transform CDA documents sang FHIR Bundle
ConceptMap — Translate mã nội bộ HIS sang mã chuẩn (LOINC, SNOMED, ICD-10)
Middleware — Integration engine xử lý nhiều nguồn dữ liệu