1. HAPI FHIR の紹介
ハピ・フィル は、FHIR サーバーおよびクライアントを構築するための最も人気のあるオープン ソース Java ライブラリです。 HAPI FHIR JPA サーバーは、JPA/Hibernate を使用した永続化レイヤーを備えた完全な FHIR サーバーを提供します。
| 成分 | 説明 |
|---|---|
| HAPI FHIR コア | パーサー、モデルクラス、クライアント/サーバーフレームワーク |
| JPAサーバー | データベース永続性を備えた完全な FHIR サーバー |
| 検証 | プロファイルベースの検証エンジン |
| CLI | 移行、アップロード用のコマンドライン ツール |
| FHIR バージョン | DSTU2、STU3、R4、R4B、R5をサポート |
2. Spring Boot でプロジェクトを初期化する
Maven の依存関係
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<properties>
<hapi.fhir.version>7.4.0</hapi.fhir.version>
</properties>
<dependencies>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-starter</artifactId>
<version>${hapi.fhir.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r5</artifactId>
<version>${hapi.fhir.version}</version>
</dependency>
</dependencies>
アプリケーションのプロパティ
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/hapi_fhir
username: hapi
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: false
jdbc:
batch_size: 20
order_inserts: true
order_updates: true
hapi:
fhir:
fhir_version: R5
server_address: http://localhost:8080/fhir
allow_multiple_delete: true
allow_external_references: true
default_page_size: 20
max_page_size: 200
validation:
requests_enabled: true
responses_enabled: false
3. FHIRサーバーの構成
@Configuration
public class FhirServerConfig {
@Bean
public RestfulServer restfulServer(
ApplicationContext context,
FhirContext fhirContext) {
RestfulServer server = new RestfulServer(fhirContext);
server.setDefaultResponseEncoding(EncodingEnum.JSON);
server.setDefaultPrettyPrint(true);
// Register resource providers
server.registerProviders(
context.getBean(PatientResourceProvider.class),
context.getBean(ObservationResourceProvider.class),
context.getBean(EncounterResourceProvider.class)
);
// Register interceptors
server.registerInterceptor(new ResponseHighlighterInterceptor());
server.registerInterceptor(new CorsInterceptor());
server.registerInterceptor(context.getBean(AuditInterceptor.class));
return server;
}
@Bean
public FhirContext fhirContext() {
return FhirContext.forR5();
}
}
4. リソースプロバイダー
@Component
public class PatientResourceProvider implements IResourceProvider {
@Autowired
private PatientRepository patientRepo;
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Read
public Patient read(@IdParam IdType theId) {
return patientRepo.findById(theId.getIdPart())
.orElseThrow(() -> new ResourceNotFoundException(theId));
}
@Create
public MethodOutcome create(@ResourceParam Patient patient) {
// Validate trước khi lưu
ValidationResult result = validator.validateWithResult(patient);
if (!result.isSuccessful()) {
throw new UnprocessableEntityException(
result.toOperationOutcome());
}
Patient saved = patientRepo.save(patient);
return new MethodOutcome()
.setId(saved.getIdElement())
.setCreated(true);
}
@Search
public List<Patient> searchByName(
@RequiredParam(name = Patient.SP_NAME) StringParam name) {
return patientRepo.findByNameContaining(name.getValue());
}
@Search
public List<Patient> searchByIdentifier(
@RequiredParam(name = Patient.SP_IDENTIFIER)
TokenParam identifier) {
return patientRepo.findByIdentifier(
identifier.getSystem(), identifier.getValue());
}
@Operation(name = "$everything", idempotent = true)
public Bundle patientEverything(@IdParam IdType patientId) {
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.SEARCHSET);
Patient patient = read(patientId);
bundle.addEntry().setResource(patient);
// Thêm Encounters, Observations, Conditions...
encounterRepo.findByPatient(patientId.getIdPart())
.forEach(e -> bundle.addEntry().setResource(e));
observationRepo.findBySubject(patientId.getIdPart())
.forEach(o -> bundle.addEntry().setResource(o));
return bundle;
}
}
5. インターセプター
HAPI FHIR 使用 インターセプターパターン リクエストのライフサイクルにフックします。
@Component
public class AuditInterceptor {
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void logIncomingRequest(
RequestDetails requestDetails) {
log.info("FHIR Request: {} {} from {}",
requestDetails.getRequestType(),
requestDetails.getCompleteUrl(),
requestDetails.getAttribute("remoteAddr"));
}
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
public void auditCreate(IBaseResource resource,
RequestDetails requestDetails) {
AuditEvent audit = new AuditEvent();
audit.setAction(AuditEvent.AuditEventAction.C);
audit.setRecorded(new Date());
AuditEvent.AuditEventAgentComponent agent =
audit.addAgent();
agent.setRequestor(true);
// Set agent from auth context
AuditEvent.AuditEventEntityComponent entity =
audit.addEntity();
entity.setWhat(new Reference(
resource.getIdElement().toUnqualifiedVersionless()));
auditRepo.save(audit);
}
}
検証インターセプター
@Component
public class ValidationInterceptor {
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
public void validateResource(IBaseResource resource) {
FhirValidator validator = fhirContext.newValidator();
// Thêm profile validation
IValidatorModule module = new FhirInstanceValidator(
validationSupport);
validator.registerValidatorModule(module);
ValidationResult result = validator.validateWithResult(resource);
if (!result.isSuccessful()) {
OperationOutcome oo = (OperationOutcome)
result.toOperationOutcome();
throw new UnprocessableEntityException(
fhirContext, oo);
}
}
}
6. カスタム検索パラメータ
{
"resourceType": "SearchParameter",
"url": "http://xdev.asia/fhir/SearchParameter/patient-cccd",
"name": "cccd",
"status": "active",
"description": "Tìm kiếm bệnh nhân theo số CCCD",
"code": "cccd",
"base": ["Patient"],
"type": "token",
"expression": "Patient.identifier.where(system='http://xdev.asia/fhir/sid/cccd').value"
}
検索パラメータを登録します。
curl -X POST http://localhost:8080/fhir/SearchParameter \
-H "Content-Type: application/fhir+json" \
-d @search-parameter-cccd.json
# Reindex để áp dụng
curl -X POST http://localhost:8080/fhir/$reindex \
-H "Content-Type: application/fhir+json" \
-d '{"resourceType":"Parameters","parameter":[{"name":"url","valueString":"Patient"}]}'
7. データの一括エクスポート
# Khởi tạo export Patient và Observation
curl -X GET 'http://localhost:8080/fhir/$export' \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Content-Type: application/fhir+json" \
--data '{
"resourceType": "Parameters",
"parameter": [
{"name": "_type", "valueString": "Patient,Observation"},
{"name": "_outputFormat", "valueString": "application/fhir+ndjson"}
]
}'
# Response Header: Content-Location: http://localhost:8080/fhir/$export-poll/abc123
# Kiểm tra trạng thái
curl http://localhost:8080/fhir/\$export-poll/abc123
# Download kết quả khi hoàn tất
curl -o patients.ndjson http://localhost:8080/fhir/bulk/abc123/Patient
8. Docker のデプロイメント
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/hapi-fhir-server.jar app.jar
ENV JAVA_OPTS="-Xmx2g -Xms1g"
ENV SPRING_PROFILES_ACTIVE=production
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# docker-compose.yml
services:
fhir-server:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/hapi_fhir
- SPRING_DATASOURCE_USERNAME=hapi
- SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
- HAPI_FHIR_SERVER_ADDRESS=https://fhir.xdev.asia/fhir
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/fhir/metadata"]
interval: 30s
timeout: 10s
retries: 5
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: hapi_fhir
POSTGRES_USER: hapi
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hapi"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
9. パフォーマンスのチューニング
| 構成 | 推奨値 | 説明 |
|---|---|---|
| hibernate.jdbc.batch_size | 20-50 | バッチ挿入/更新 |
| max_page_size | 200 | 返される結果を制限する |
| 再利用_cached_search_results | 60000 (ミリ秒) | 検索結果をキャッシュする |
| JVM ヒープ | 2~4GB | リソースの数に応じて |
| 接続プール | 20-50 | 光CP接続 |
| PostgreSQL 共有バッファ | 25% RAM | データベースバッファ |
10. まとめ
HAPI FHIR JPA サーバー — Spring Boot + PostgreSQL 上に構築されたフル機能の FHIR サーバー
リソースプロバイダー — CRUD、検索、カスタム操作の実装
インターセプター — 監査、検証、セキュリティのためのリクエストのライフサイクルに接続します
カスタム検索パラメータ — ニーズに応じて検索を拡大(CCCD、健康保険)
一括データエクスポート — 大量のデータを NDJSON 形式でエクスポート
ドッカー — docker-compose を使用した実稼働デプロイメント