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

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

ExecuteBundle メソッドは、FHIR の標準のバッチ / トランザクション インタラクションを実装します(DST2STU3、および R4 をご覧ください)。

FHIR バンドル

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

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

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

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

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

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

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

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"
	"io/ioutil"

	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: %v", 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: %v", 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: %v", err)
	}
	defer resp.Body.Close()

	respBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("could not read response: %v", 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.jackson2.JacksonFactory;
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 JacksonFactory();
  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 = "{}/projects/{}/locations/{}".format(base_url, project_id, 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, "r") 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("Executed bundle from file: {}".format(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"
}