使用 FHIR 套裝組合管理 FHIR 資源

本頁說明如何執行 FHIR 套裝組合來管理 FHIR 資源。FHIR 套裝組合是一組 FHIR 資源,以及要對這些 FHIR 資源執行的作業。

ExecuteBundle 方法會實作 FHIR 標準批次/交易互動 (DSTU2STU3R4R5) 和記錄作業。

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 儲存庫設定中,將 enableHistoryModifications 設為 true。如果 disableResourceVersioning 在 FHIR 儲存庫設定中設為 true,您就無法使用歷史記錄套件。

系統會以與 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 組合的路徑和檔案名稱。您也可以在要求主體中加入套裝組合。

下列範例 Bundle 會建立 Patient 資源,並刪除另一個 Patient 資源:

{
  "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"

無論個別作業的結果為何,執行批次套件後,伺服器都會傳回 Bundle 資源的 JSON 編碼表示法,類型為 batch-responseBundle 資源包含要求中的每個項目,以及處理項目的結果,可能是成功和錯誤結果的組合。

如果交易套裝組合成功,伺服器會傳回 Bundle 資源的 JSON 編碼表示法 (類型為 transaction-response),其中包含要求中每個項目的項目,以及作業的成功結果。

如果執行交易套件時發生錯誤,回應主體不會包含套件。而是包含 JSON 編碼的 OperationOutcome 資源,說明錯誤原因。如果作業成功但已復原,則回應中不會回報這類作業。

以下範例組合包是成功執行上述範例的輸出內容。第一個項目表示建立 Patient 的作業是否成功,並包含新資源的 ID。第二個項目表示刪除作業成功。

{
  "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 資源包含要求中的每個項目,以及處理項目的結果,可能是成功和錯誤結果的組合。

如果交易套裝組合成功,伺服器會傳回 Bundle 資源的 JSON 編碼表示法 (類型為 transaction-response),其中包含要求中每個項目的項目,以及作業的成功結果。

如果執行交易套件時發生錯誤,回應主體不會包含套件。而是包含 JSON 編碼的 OperationOutcome 資源,說明錯誤原因。如果作業成功但已復原,則回應中不會回報這類作業。

以下範例組合包是成功執行上述範例的輸出內容。第一個項目表示建立 Patient 的作業是否成功,並包含新資源的 ID。第二個項目表示刪除作業成功。

{
  "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
  • 任何網址
  • 格式為 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。第二個項目表示建立 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。第二個項目表示建立 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"
}