FHIR バンドルを使用した FHIR リソースの管理

このページでは、FHIR リソースのコレクションである FHIR バンドルと、HIR リソースに対して行うオペレーションを実行して、FHIR リソースを管理する方法について説明します。

ExecuteBundle メソッドは、FHIR の標準バッチ/トランザクション インタラクション(DSTU2STU3R4)と履歴オペレーションを実装します。

FHIR バンドル

FHIR バンドルにはエントリの配列が含まれ、それぞれが Observation や Patient などのリソースに対するオペレーション(作成、更新、削除など)を表します。詳しくは、バンドル リソースの要素についての詳細な説明をご覧ください。

FHIR バンドルの実行時に、バンドルタイプによってバンドルでのオペレーションの実行方法が決定されます。使用できるバンドルのタイプは次のとおりです。

  • batch: 複数の独立したリクエストとしてオペレーションが実行されます。
  • transaction: 相互に依存する複数のリクエストとしてオペレーションが実行されます。
  • history: エントリをリソースの履歴に挿入します。

たとえば、トランザクション バンドルに Patient リソースと Observation リソースの作成が含まれているとします。Patient リソースの作成リクエストが失敗した場合、Observation リソースは作成されません。

バンドルタイプが batch のときにオペレーションが失敗した場合、Cloud Healthcare API でバンドル内の残りのオペレーションが実行されます。バンドルタイプが transaction のときにオペレーションが失敗した場合、Cloud Healthcare API でオペレーションの実行が停止され、トランザクションがロールバックされます。

履歴バンドル

履歴バンドルは FHIR 標準のカスタム拡張機能で、同期などのバックアップと復元のユースケースに対応しています。履歴バンドルを使用すると、FHIR リソースの履歴に対してリソース バージョンの挿入または置き換えを行うことができます。リソース バージョンを削除できるのは、Resource-purge メソッドを使用する場合のみです。history バンドルは、バンドルあたり 100 エントリの上限で単一のトランザクションとして実行されます。history バンドルのリソース バージョンのタイムスタンプが FHIR ストアの最新バージョンより大きい場合、最新バージョンがそれに応じて更新されます。history バンドルが正常に挿入されると、空のレスポンスが返されます。それ以外の場合は、失敗を示す OperationOutcome が返されます。

履歴バンドルのサポートはデフォルトでは有効になっていません。FHIR ストア管理者は、FHIR ストアの構成enableHistoryModificationstrue に設定する必要があります。FHIR ストアの構成で disableResourceVersioningtrue に設定されている場合、履歴バンドルを使用できません。

履歴バンドルは、fhir.history メソッドから返される際の形式と同じ形式で提供されます。各バンドル エントリが有効であるには、リソース ID、変更タイムスタンプ、ステータスが必要です。また、すべてのエントリに同じリソース ID を指定する必要があります。リソース ID は、resource.id フィールドまたは request.url フィールドで指定します。フィールドが指定されている場合、指定されたリソース ID は同じになります。リソースのタイムスタンプは、リソースの meta.lastUpdated フィールドまたは response.lastModified フィールドで指定します。

バンドルを実行するための権限の付与

バンドルを実行するには、datasets.fhirStores.fhir.executeBundle権限のロールが必要です。この権限を付与するには、healthcare.fhirResourceReaderロールを使用します。この権限を付与するための手順については、ポリシーの変更をご覧ください。

履歴バンドルを実行するには、datasets.fhirStores.fhir.import 権限のロールも必要です。

Cloud Healthcare API では、バンドル内の各オペレーションの権限を確認します。healthcare.fhirResources.create 権限は持っているものの、healthcare.fhirResources.update 権限は持っていない場合、healthcare.fhirResources.create オペレーションが含まれるバンドルのみを実行できます。

バンドルを実行する

FHIR バンドルを実行するには、projects.locations.datasets.fhirStores.fhir.executeBundleメソッドを使用します。

以下のサンプルでは、BUNDLE.json は JSON でエンコードされた FHIR バンドルへのパスとファイル名です。また、リクエストの本文にバンドルを含めることもできます。

次のサンプル バンドルは、患者リソースの作成と別の患者リソースの削除を行います。

{
  "resourceType": "Bundle",
  "id": "bundle-transaction",
  "meta": {
    "lastUpdated": "2018-03-11T11:22:16Z"
  },
  "type": "transaction",
  "entry": [
    {
      "resource": {
        "resourceType": "Patient",
        "name": [
          {
            "family": "Smith",
            "given": [
              "Darcy"
            ]
          }
        ],
        "gender": "female",
        "address": [
          {
            "line": [
              "123 Main St."
            ],
            "city": "Anycity",
            "state": "CA",
            "postalCode": "12345"
          }
        ]
      },
      "request": {
        "method": "POST",
        "url": "Patient"
      }
    },
    {
      "request": {
        "method": "DELETE",
        "url": "Patient/1234567890"
      }
    }
  ]
}

次のサンプルは、バンドルを実行する方法を示しています。

curl

バンドルを実行するには、POSTリクエストを行い、次の情報を指定します。

  • 親データセットと FHIR ストアの名前とロケーション
  • ローカルマシン上のバンドル ファイルの場所
  • アクセス トークン

次のサンプルは、curl を使用した POST リクエストを示しています。

curl -X POST \
    -H "Content-Type: application/fhir+json; charset=utf-8" \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    --data @BUNDLE_FILE.json \
    "https://healthcare.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/datasets/DATASET_ID/fhirStores/FHIR_STORE_ID/fhir"

個々のオペレーションの結果に関係なく、バッチバンドルの実行後に、サーバーは batch-response タイプの Bundle リソースの JSON エンコード表現を返します。Bundle リソースには、リクエストのエントリごとに 1 つのエントリが含まれます。エントリの処理により、結果は成功かエラーになります。

トランザクション バンドルが成功した場合、サーバーはオペレーションが成功したリクエスト中の各エントリごとに 1 つのエントリを含む、transaction-responseタイプの Bundle リソースの JSON エンコード表現を返します。

トランザクション バンドルの実行中にエラーが発生した場合、レスポンスの本文にバンドルは含まれません。代わりに、エラーの理由を説明する JSON エンコードされた OperationOutcome リソースが含まれます。ロールバックされた正常なオペレーションは、レスポンスでは報告されません。

次のサンプル バンドルは、上記の例を正常に実行した際の出力です。最初のエントリは、Patient を作成するオペレーションの成功を示していて、新しいリソースの ID が含まれています。2 番目のエントリは、削除オペレーションが成功したことを示しています。

{
  "entry": [
    {
      "response": {
        "location": projects/PROJECT_ID/locations/LOCATION/datasets/DATASET_ID/fhirStores/FHIR_STORE_ID/fhir/RESOURCE/RESOURCE_ID,
        "status": "201 Created"
      }
    },
    {
      "response": {
        "status": "200 OK"
      }
    }
  ],
  "resourceType": "Bundle",
  "type": "transaction-response"
}

PowerShell

バンドルを実行するには、POSTリクエストを行い、次の情報を指定します。

  • 親データセットと FHIR ストアの名前とロケーション
  • ローカルマシン上のバンドル ファイルの場所
  • アクセス トークン

次のサンプルは、Windows PowerShell を使用した POST リクエストを示しています。

$cred = gcloud auth application-default print-access-token
$headers = @{ Authorization = "Bearer $cred" }

Invoke-RestMethod `
  -Method Post `
  -Headers $headers `
  -ContentType: "application/fhir+json" `
  -InFile BUNDLE_FILE.json `
  -Uri "https://healthcare.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/datasets/DATASET_ID/fhirStores/FHIR_STORE_ID/fhir" | ConvertTo-Json

個々のオペレーションの結果に関係なく、バッチバンドルの実行後に、サーバーは batch-response タイプの Bundle リソースの JSON エンコード表現を返します。Bundle リソースには、リクエストのエントリごとに 1 つのエントリが含まれます。エントリの処理により、結果は成功かエラーになります。

トランザクション バンドルが成功した場合、サーバーはオペレーションが成功したリクエスト中の各エントリごとに 1 つのエントリを含む、transaction-responseタイプの Bundle リソースの JSON エンコード表現を返します。

トランザクション バンドルの実行中にエラーが発生した場合、レスポンスの本文にバンドルは含まれません。代わりに、エラーの理由を説明する JSON エンコードされた OperationOutcome リソースが含まれます。ロールバックされた正常なオペレーションは、レスポンスでは報告されません。

次のサンプル バンドルは、上記の例を正常に実行した際の出力です。最初のエントリは、Patient を作成するオペレーションの成功を示していて、新しいリソースの ID が含まれています。2 番目のエントリは、削除オペレーションが成功したことを示しています。

{
  "entry": [
    {
      "response": {
        "etag": "ETAG",
        "lastModified": "2020-08-03T04:12:47.312669+00:00",
        "location": "projects/PROJECT_ID/locations/LOCATION/datasets/DATASET_ID/fhirStores/FHIR_STORE_ID/fhir/RESOURCE/RESOURCE_ID",
        "status": "201 Created"
      }
    },
    {
      "response": {
        "etag": "ETAG",
        "lastModified": "2020-08-03T04:12:47.312669+00:00",
        "status": "200 OK"
      }
    }
  ],
  "resourceType": "Bundle",
  "type": "transaction-response"
}

Go

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"

	healthcare "google.golang.org/api/healthcare/v1"
)

// fhirExecuteBundle executes an FHIR bundle.
func fhirExecuteBundle(w io.Writer, projectID, location, datasetID, fhirStoreID string) error {
	ctx := context.Background()

	healthcareService, err := healthcare.NewService(ctx)
	if err != nil {
		return fmt.Errorf("healthcare.NewService: %w", err)
	}

	fhirService := healthcareService.Projects.Locations.Datasets.FhirStores.Fhir

	payload := map[string]interface{}{
		"resourceType": "Bundle",
		"type":         "transaction",
		"entry": []map[string]interface{}{
			{
				"resource": map[string]interface{}{
					"resourceType": "Patient",
					"active":       true,
				},
				"request": map[string]interface{}{
					"method": "POST",
					"url":    "Patient",
				},
			},
		},
	}
	jsonPayload, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("json.Encode: %w", err)
	}

	parent := fmt.Sprintf("projects/%s/locations/%s/datasets/%s/fhirStores/%s", projectID, location, datasetID, fhirStoreID)

	call := fhirService.ExecuteBundle(parent, bytes.NewReader(jsonPayload))
	call.Header().Set("Content-Type", "application/fhir+json;charset=utf-8")
	resp, err := call.Do()
	if err != nil {
		return fmt.Errorf("executeBundle: %w", err)
	}
	defer resp.Body.Close()

	respBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("could not read response: %w", err)
	}

	if resp.StatusCode > 299 {
		return fmt.Errorf("create: status %d %s: %s", resp.StatusCode, resp.Status, respBytes)
	}
	fmt.Fprintf(w, "%s", respBytes)

	return nil
}

Java

import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.healthcare.v1.CloudHealthcare;
import com.google.api.services.healthcare.v1.CloudHealthcareScopes;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Collections;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;

public class FhirStoreExecuteBundle {
  private static final String FHIR_NAME = "projects/%s/locations/%s/datasets/%s/fhirStores/%s";
  private static final JsonFactory JSON_FACTORY = new GsonFactory();
  private static final NetHttpTransport HTTP_TRANSPORT = new NetHttpTransport();

  public static void fhirStoreExecuteBundle(String fhirStoreName, String data)
      throws IOException, URISyntaxException {
    // String fhirStoreName =
    //    String.format(
    //        FHIR_NAME, "your-project-id", "your-region-id", "your-dataset-id", "your-fhir-id");
    // String data = "{\"resourceType\": \"Bundle\",\"type\": \"batch\",\"entry\": []}"

    // Initialize the client, which will be used to interact with the service.
    CloudHealthcare client = createClient();
    HttpClient httpClient = HttpClients.createDefault();
    String baseUri = String.format("%sv1/%s/fhir", client.getRootUrl(), fhirStoreName);
    URIBuilder uriBuilder = new URIBuilder(baseUri).setParameter("access_token", getAccessToken());
    StringEntity requestEntity = new StringEntity(data);

    HttpUriRequest request =
        RequestBuilder.post()
            .setUri(uriBuilder.build())
            .setEntity(requestEntity)
            .addHeader("Content-Type", "application/fhir+json")
            .addHeader("Accept-Charset", "utf-8")
            .addHeader("Accept", "application/fhir+json; charset=utf-8")
            .build();

    // Execute the request and process the results.
    HttpResponse response = httpClient.execute(request);
    HttpEntity responseEntity = response.getEntity();
    if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
      System.err.print(
          String.format(
              "Exception executing FHIR bundle: %s\n", response.getStatusLine().toString()));
      responseEntity.writeTo(System.err);
      throw new RuntimeException();
    }
    System.out.print("FHIR bundle executed: ");
    responseEntity.writeTo(System.out);
  }

  private static CloudHealthcare createClient() throws IOException {
    // Use Application Default Credentials (ADC) to authenticate the requests
    // For more information see https://cloud.google.com/docs/authentication/production
    GoogleCredentials credential =
        GoogleCredentials.getApplicationDefault()
            .createScoped(Collections.singleton(CloudHealthcareScopes.CLOUD_PLATFORM));

    // Create a HttpRequestInitializer, which will provide a baseline configuration to all requests.
    HttpRequestInitializer requestInitializer =
        request -> {
          new HttpCredentialsAdapter(credential).initialize(request);
          request.setConnectTimeout(60000); // 1 minute connect timeout
          request.setReadTimeout(60000); // 1 minute read timeout
        };

    // Build the client for interacting with the service.
    return new CloudHealthcare.Builder(HTTP_TRANSPORT, JSON_FACTORY, requestInitializer)
        .setApplicationName("your-application-name")
        .build();
  }

  private static String getAccessToken() throws IOException {
    GoogleCredentials credential =
        GoogleCredentials.getApplicationDefault()
            .createScoped(Collections.singleton(CloudHealthcareScopes.CLOUD_PLATFORM));

    return credential.refreshAccessToken().getTokenValue();
  }
}

Node.js

サンプル バンドル ファイルは、コードサンプルの GitHub リポジトリで入手できます。

const google = require('@googleapis/healthcare');
const healthcare = google.healthcare({
  version: 'v1',
  auth: new google.auth.GoogleAuth({
    scopes: ['https://www.googleapis.com/auth/cloud-platform'],
  }),
  headers: {'Content-Type': 'application/fhir+json'},
});
const fs = require('fs');

async function executeFhirBundle() {
  // TODO(developer): uncomment these lines before running the sample
  // const cloudRegion = 'us-central1';
  // const projectId = 'adjective-noun-123';
  // const datasetId = 'my-dataset';
  // const fhirStoreId = 'my-fhir-store';
  // const bundleFile = 'bundle.json';
  const parent = `projects/${projectId}/locations/${cloudRegion}/datasets/${datasetId}/fhirStores/${fhirStoreId}`;

  const bundle = JSON.parse(fs.readFileSync(bundleFile));

  const request = {parent, requestBody: bundle};
  const resource =
    await healthcare.projects.locations.datasets.fhirStores.fhir.executeBundle(
      request
    );
  console.log('FHIR bundle executed');
  console.log(resource.data);
}

executeFhirBundle();

Python

サンプル バンドル ファイルは、コードサンプルの GitHub リポジトリで入手できます。

def execute_bundle(
    project_id,
    location,
    dataset_id,
    fhir_store_id,
    bundle,
):
    """Executes the operations in the given bundle.

    See https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/healthcare/api-client/v1/fhir
    before running the sample."""
    # Imports Python's built-in "os" module
    import os

    # Imports the google.auth.transport.requests transport
    from google.auth.transport import requests

    # Imports a module to allow authentication using a service account
    from google.oauth2 import service_account

    # Gets credentials from the environment.
    credentials = service_account.Credentials.from_service_account_file(
        os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
    )
    scoped_credentials = credentials.with_scopes(
        ["https://www.googleapis.com/auth/cloud-platform"]
    )
    # Creates a requests Session object with the credentials.
    session = requests.AuthorizedSession(scoped_credentials)

    # URL to the Cloud Healthcare API endpoint and version
    base_url = "https://healthcare.googleapis.com/v1"

    # TODO(developer): Uncomment these lines and replace with your values.
    # project_id = 'my-project'  # replace with your GCP project ID
    # location = 'us-central1'  # replace with the parent dataset's location
    # dataset_id = 'my-dataset'  # replace with the parent dataset's ID
    # fhir_store_id = 'my-fhir-store' # replace with the FHIR store ID
    # bundle = 'bundle.json'  # replace with the bundle file
    url = f"{base_url}/projects/{project_id}/locations/{location}"

    resource_path = "{}/datasets/{}/fhirStores/{}/fhir".format(
        url, dataset_id, fhir_store_id
    )

    headers = {"Content-Type": "application/fhir+json;charset=utf-8"}

    with open(bundle) as bundle_file:
        bundle_file_content = bundle_file.read()

    response = session.post(resource_path, headers=headers, data=bundle_file_content)
    response.raise_for_status()

    resource = response.json()

    print(f"Executed bundle from file: {bundle}")
    print(json.dumps(resource, indent=2))

    return resource

PATCH リクエストを作成する

FHIR バンドルを使用して、FHIR リソースに対して JSON PATCH リクエストを実行できます。詳細については、FHIR バンドルでの PATCH リクエストの実行をご覧ください。

バンドル内に作成されたリソースへの参照の解決

トランザクション バンドルのリソースには、ターゲット システム内に存在しないものの、バンドルの実行中に作成されたリソースへの参照を含めることができます。Cloud Healthcare API は、entry.fullUrlフィールドを使用してリソース間の関連付けを解決します。バンドル内にある別のリソースの entry.fullUrl 値に一致する参照は、ストア内の対応するリソースの ID に書き換えられます。これは、バンドル内のオペレーションの順序に関係なく成功します。

Cloud Healthcare API は、次の形式の fullUrl を受け入れます。

  • urn:uuid:UUID
  • urn:oid:OID
  • 任意の URL
  • RESOURCE_TYPE/RESOURCE_ID 形式のリソース名(例: Patient/123)。fullUrl はバンドルのローカル プレースホルダであるため、この形式の使用はおすすめしません。ストア内のリソースの名前が同じであっても、バンドル内のリソースが作成オペレーションの結果として別の名前に解決された場合、混乱を招く可能性があります。

次のサンプル バンドルでは、Patient リソースを参照する Patient リソースと Observation リソースを作成します。

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry":[
    {
      "request": {
        "method":"POST",
        "url":"Patient"
      },
      "fullUrl": "urn:uuid:05efabf0-4be2-4561-91ce-51548425acb9",
      "resource": {
        "resourceType":"Patient",
        "gender":"male"
      }
    },
    {
      "request": {
        "method":"POST",
        "url":"Observation"
      },
      "resource": {
        "resourceType":"Observation",
        "subject": {
          "reference": "urn:uuid:05efabf0-4be2-4561-91ce-51548425acb9"
        },
        "status":"preliminary",
        "code": {
          "text":"heart rate"
        }
      }
    }
  ]
}

次のサンプルは、バンドルを実行する方法を示しています。

curl

サンプル バンドル ファイルは、コードサンプルの GitHub リポジトリで入手できます。

バンドルを実行するには、POST リクエストを行い、次の情報を指定します。

  • 親データセットと FHIR ストアの名前とロケーション
  • Cloud Storage 内のバンドル ファイルのロケーション
  • アクセス トークン

次のサンプルは、curl を使用した POST リクエストを示しています。

curl -X POST \
    -H "Content-Type: application/fhir+json; charset=utf-8" \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    --data @BUNDLE_FILE.json \
    "https://healthcare.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/datasets/DATASET_ID/fhirStores/FHIR_STORE_ID/fhir"

次のサンプル バンドルは、上記の例を正常に実行した際の出力です。最初のエントリは、Patient を作成するオペレーションの成功を示していて、新しいリソースの ID が含まれています。2 番目のエントリは、Observation を作成するオペレーションの成功を示していて、新しいリソースの ID が含まれています。

{
  "entry": [
    {
      "response": {
        "etag": "ETAG1",
        "lastModified": "2020-08-04T16:14:14.273976+00:00",
        "location": "https://healthcare.googleapis.com/v1/projects/PROJECT_ID/locations/REGION/datasets/REGION/fhirStores/FHIR_STORE_ID/fhir/Patient/PATIENT_ID/_history/HISTORY_ID",
        "status": "201 Created"
      }
    },
    {
      "response": {
        "etag": "ETAG",
        "lastModified": "2020-08-04T16:14:14.273976+00:00",
        "location": "https://healthcare.googleapis.com/v1/projects/PROJECT_ID/locations/REGION/datasets/REGION/fhirStores/FHIR_STORE_ID/fhir/Observation/OBSERVATION_ID/_history/HISTORY_ID",
        "status": "201 Created"
      }
    }
  ],
  "resourceType": "Bundle",
  "type": "transaction-response"
}

PowerShell

サンプル バンドル ファイルは、コードサンプルの GitHub リポジトリで入手できます。

バンドルを実行するには、POST リクエストを行い、次の情報を指定します。

  • 親データセットと FHIR ストアの名前とロケーション
  • Cloud Storage 内のバンドル ファイルのロケーション
  • アクセス トークン

次のサンプルは、Windows PowerShell を使用した POST リクエストを示しています。

$cred = gcloud auth application-default print-access-token
$headers = @{ Authorization = "Bearer $cred" }

Invoke-RestMethod `
  -Method Post `
  -Headers $headers `
  -ContentType: "application/fhir+json" `
  -InFile BUNDLE_FILE.json `
  -Uri "https://healthcare.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/datasets/DATASET_ID/fhirStores/FHIR_STORE_ID/fhir" | ConvertTo-Json

次のサンプル バンドルは、上記の例を正常に実行した際の出力です。最初のエントリは、Patient を作成するオペレーションの成功を示していて、新しいリソースの ID が含まれています。2 番目のエントリは、Observation を作成するオペレーションの成功を示していて、新しいリソースの ID が含まれています。

{
  "entry": [
    {
      "response": {
        "etag": "ETAG1",
        "lastModified": "2020-08-04T16:14:14.273976+00:00",
        "location": "https://healthcare.googleapis.com/v1/projects/PROJECT_ID/locations/REGION/datasets/REGION/fhirStores/FHIR_STORE_ID/fhir/Patient/PATIENT_ID/_history/HISTORY_ID",
        "status": "201 Created"
      }
    },
    {
      "response": {
        "etag": "ETAG",
        "lastModified": "2020-08-04T16:14:14.273976+00:00",
        "location": "https://healthcare.googleapis.com/v1/projects/PROJECT_ID/locations/REGION/datasets/REGION/fhirStores/FHIR_STORE_ID/fhir/Observation/OBSERVATION_ID/_history/HISTORY_ID",
        "status": "201 Created"
      }
    }
  ],
  "resourceType": "Bundle",
  "type": "transaction-response"
}