Chuyển đến nội dung chính

レッスン 18: 実践 — HAPI FHIR を使用した FHIR サーバーの構築

Spring Boot を使用した HAPI FHIR JPA サーバーのセットアップ、PostgreSQL バックエンド、インデックス作成と検索パラメーターの構成、リソース検証、インターセプター パターン、カスタム操作 ($everything、$validate)、一括データ エクスポート、Docker デプロイメント、パフォーマンス チューニング。

🏗️ アーキテクチャ — レッスン 18 レッスン 18: 実践 — FHIR サーバーの構築 HAPI FHIR を使用

HL7 FHIR - 基本から高度な医療データ標準

パート 6: 実践 - FHIR システムの構築

xdev.asia

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_size20-50バッチ挿入/更新
max_page_size200返される結果を制限する
再利用_cached_search_results60000 (ミリ秒)検索結果をキャッシュする
JVM ヒープ2~4GBリソースの数に応じて
接続プール20-50光CP接続
PostgreSQL 共有バッファ25% RAMデータベースバッファ

10. まとめ

  • HAPI FHIR JPA サーバー — Spring Boot + PostgreSQL 上に構築されたフル機能の FHIR サーバー

  • リソースプロバイダー — CRUD、検索、カスタム操作の実装

  • インターセプター — 監査、検証、セキュリティのためのリクエストのライフサイクルに接続します

  • カスタム検索パラメータ — ニーズに応じて検索を拡大(CCCD、健康保険)

  • 一括データエクスポート — 大量のデータを NDJSON 形式でエクスポート

  • ドッカー — docker-compose を使用した実稼働デプロイメント