成效改善提示

本文說明提升應用程式成效的幾個技巧。在某些情況下,我們會使用其他 API 或通用 API 的範例來說明這些技巧背後的概念。不過,同樣的概念也能應用在 Google Pay API for Passes。

使用 gzip 壓縮

要減少每個要求占用的頻寬,最簡便的方法就是使用 gzip 壓縮檔。雖然此方法需要額外的 CPU 作業時間進行解壓縮,但相對可省下可觀的網路成本。

如果要接收以 gzip 編碼的回應,您必須執行下列兩項操作:設定 Accept-Encoding 標頭,並修改使用者代理程式,使其包含 gzip 字串。以下是啟用 gzip 壓縮的正確 HTTP 標頭格式範例:

Accept-Encoding: gzip
User-Agent: my program (gzip)

使用部分資源

另一種提高 API 呼叫成效的方式,就是只收發您有興趣的資料。這麼做可避免讓您的應用程式傳輸、剖析及儲存不需要的欄位,進而更有效地使用網路、CPU 以及記憶體等資源。

部分要求分為兩類:

  • 部分回應:您可以在這類要求中使用 fields 要求參數指定回應中要包含哪些欄位。
  • 修補:這類更新要求可讓您透過使用 PATCH HTTP 動詞,僅傳送想要變更的欄位。

接下來的幾節將進一步說明如何提出部分要求。

部分回應

根據預設,伺服器會在處理要求後傳回完整的資源表示法。為改善成效,您可以要求伺服器只傳送您真正需要的欄位,並改為取得「部分回應」

如要提出僅傳回部分回應的要求,請使用 fields 要求參數來指定您想要傳回的欄位。您可以將此參數搭配任何會傳回回應資料的要求使用。

請注意,fields 參數只會影響回應資料,不會影響您需要傳送的資料 (如果有的話)。如要減少您在修改資源時傳送的資料量,請使用修補要求。

範例

以下範例顯示 fields 參數與某個通用 (虛構) 的「Demo」API 的搭配用法。

簡易要求:這個 HTTP GET 要求會省略 fields 參數,並傳回完整的資源。

https://www.googleapis.com/demo/v1

完整資源回應:完整資源資料包括下列欄位 (為節省篇幅,此處省略許多其他欄位)。

{
  "kind": "demo",
  ...
  "items": [
  {
    "title": "First title",
    "comment": "First comment.",
    "characteristics": {
      "length": "short",
      "accuracy": "high",
      "followers": ["Jo", "Will"],
    },
    "status": "active",
    ...
  },
  {
    "title": "Second title",
    "comment": "Second comment.",
    "characteristics": {
      "length": "long",
      "accuracy": "medium"
      "followers": [ ],
    },
    "status": "pending",
    ...
  },
  ...
  ]
}

僅傳回部分回應的要求:以下是對相同資源發出的要求,其中使用了 fields 參數,以大幅減少傳回的資料量。

https://www.googleapis.com/demo/v1?fields=kind,items(title,characteristics/length)

部分回應:在上方的要求回應中,伺服器傳回的回應只包含種類資訊,以及經過簡化的項目陣列 (只包含個別項目的 HTML 標題和長度特性資訊)。

200 OK
{
  "kind": "demo",
  "items": [{
    "title": "First title",
    "characteristics": {
      "length": "short"
    }
  }, {
    "title": "Second title",
    "characteristics": {
      "length": "long"
    }
  },
  ...
  ]
}

請注意,回應是一個 JSON 物件,且此物件只包含選定的欄位及包住這些欄位的父項物件。

以下將詳述如何設定 fields 參數的格式,接著再進一步說明回應中實際傳回的內容。

欄位參數語法摘要

fields 要求參數值的格式約略以 XPath 語法為基礎。以下提供支援之語法的摘要,其他範例如下一節所示。

  • 使用以逗號分隔的清單來選取多個欄位。
  • 使用 a/ba 欄位的巢狀結構內選取 b 欄位;使用 a/b/cb 的巢狀結構內選取 c 欄位。

    例外狀況:如果 API 回應使用「data」包裝函式,也就是回應是放置在 data 物件 (看起來像 data: { ... }) 的巢狀結構中,請勿在 fields 規格中加入「data」。若包含的資料物件具有類似 data/a/b 的欄位規格,則會造成錯誤。請改為只使用 fields 規格,例如 a/b

  • 透過在括號「( )」中放入運算式,使用子選取器要求一組在陣列或物件中的特定子欄位。

    例如:fields=items(id,author/email) 只會傳回項目陣列中各個元素的項目 ID 和作者的電子郵件地址。您也可以指定單一子欄位,其中 fields=items(id) 等於 fields=items/id

  • 必要時,在欄位選取項目中使用萬用字元。

    例如:fields=items/pagemap/* 會選取 PageMap 中的所有物件。

更多使用 fields 參數的範例

以下範例包含 fields 參數值會如何影響回應的說明。

注意:和所有查詢參數值一樣,fields 參數值也必須經過網址編碼。為方便閱讀,本文中的範例會省略編碼。

找出您要傳回的欄位,或「選取欄位」
fields 要求參數值是一個以逗號分隔的欄位清單,每個欄位是根據回應的根來指定。因此,如果您執行的是列出作業,回應會是一個集合,其中通常包含資源陣列。如果您執行的作業傳回單一資源,欄位會根據該資源來指定。如果您選取的欄位是一個陣列 (或陣列的一部分),則伺服器會傳回該陣列中所有元素的選定部分。

以下提供幾個集合層級的範例:
範例 作用
items 傳回項目陣列中的所有元素,包括每個元素中的所有欄位,但不包含其他欄位。
etag,items 同時傳回 etag 欄位和項目陣列中的所有元素。
items/title 只傳回項目陣列中所有元素的 title 欄位。

每當傳回巢狀欄位時,回應都會包含涵蓋該欄位的父項物件。如未同時選定其他子欄位,父項欄位將不會含有任何其他子欄位。
context/facets/label 只傳回 facets 陣列所有成員的 label 欄位,該欄位本身是放置在 context 物件下的巢狀結構中。
items/pagemap/*/title 針對項目陣列中的每個元素,只傳回屬於 pagemap 子項的所有物件的 title 欄位 (如果有的話)。

以下提供幾個資源層級的範例:
範例 作用
title 傳回所要求資源的 title 欄位。
author/uri 傳回所要求資源中 author 物件的 uri 子欄位。
links/*/href
傳回屬於 links 子項之所有物件的 href 欄位。
使用「子選取項目」只要求特定欄位部分。
根據預設,如果您的要求指定特定的欄位,伺服器會傳回整個物件或陣列元素。您可以指定回應中僅包含哪些特定欄位。執行這項操作的方法是使用「( )」子選取項目語法,如下列範例所示。
範例 作用
items(title,author/uri) 只傳回項目陣列中每個元素的 title 值以及作者的 uri 值。

處理部分回應

伺服器處理包含 fields 查詢參數的有效要求後,會傳回一個 HTTP 200 OK 狀態碼,以及所要求的資料。如果 fields 查詢參數發生錯誤或無效,伺服器會傳回 HTTP 400 Bad Request 狀態碼和錯誤訊息,指出使用者選取欄位時發生的錯誤 (例如 "Invalid field selection a/b")。

以下是前述簡介一節中顯示的部分回應範例。此要求使用 fields 參數來指定要傳回的欄位。

https://www.googleapis.com/demo/v1?fields=kind,items(title,characteristics/length)

部分回應看起來會像這樣:

200 OK
{
  "kind": "demo",
  "items": [{
    "title": "First title",
    "characteristics": {
      "length": "short"
    }
  }, {
    "title": "Second title",
    "characteristics": {
      "length": "long"
    }
  },
  ...
  ]
}

注意:如果 API 支援資料分頁的查詢參數 (例如 maxResultsnextPageToken),請使用這些參數將各筆查詢的結果縮減至方便管理的大小。否則,可能無法透過部分回應獲得成效改善。

修補 (部分更新)

修改資源時,您也可以避免傳送不必要的資料。如果只要為您正在變更的特定欄位傳送更新資料,請使用 HTTP PATCH 動詞。相較於舊版的 GData 部分更新實作方式,本文中的「修補」一詞的語意有所改變,且變得更加簡單。

下方的簡短範例將示範如何使用修補功能,在進行小規模更新時將您需要傳送的資料量減到最小。

範例

此範例將示範如何使用簡單的修補要求,只更新一個通用 (虛構) 的「Demo」API 資源標題。這項資源同時包含註解、一組特性、狀態以及許多其他欄位,但此要求只會傳送 title 欄位,因為這是唯一進行修改的欄位:

PATCH https://www.googleapis.com/demo/v1/324
Authorization: Bearer your_auth_token
Content-Type: application/json

{
  "title": "New title"
}

回應:

200 OK
{
  "title": "New title",
  "comment": "First comment.",
  "characteristics": {
    "length": "short",
    "accuracy": "high",
    "followers": ["Jo", "Will"],
  },
  "status": "active",
  ...
}

伺服器會傳回 200 OK 狀態碼,以及更新後資源的完整表示法。由於修補要求只包含 title 欄位,因此這個值是唯一與以往不同的值。

注意:如果您將部分回應 fields 參數與修補搭配使用,就可以進一步提高更新要求的效率。修補要求只能縮減要求的大小,而部分回應則能縮減回應的大小。因此,若要同時減少兩個方向傳送的資料量,請將修補要求與 fields 參數搭配使用。

修補要求的語意

修補要求的主體只包含您想要修改的資源欄位。您必須在指定欄位時加入任何其歸屬的父物件,而這些歸屬的父物件將會連同部分回應一併傳回。修改後的資料會在傳送時合併至父物件的資料中 (如果有父物件存在的話)。

  • 新增:如要新增一個還不存在的欄位,請指定新的欄位及其欄位值。
  • 修改:如要變更現有欄位的值,請指定該欄位並設為新的值。
  • 刪除:如要刪除欄位,請指定該欄位並將其設為 null。例如 "comment": null。您也可以將可變動的物件設為 null,以便刪除整個物件。如果您使用的是 Java API 用戶端程式庫,請改用 Data.NULL_STRING,詳情請參閱 JSON null 的相關說明。

有關陣列的注意事項:含有陣列的修補要求會以您提供的陣列取代現有的陣列。您無法個別修改、新增或刪除陣列中的項目。

在「讀取 - 修改 - 寫入」週期中使用修補功能

建議您可以從擷取欲修改之資料的部分回應開始著手,這在資源使用 ETag 時尤其重要。這是因為您必須提供 If-Match HTTP 標頭目前的 ETag 值,才能成功更新資源。取得資料之後,您便可以接著修改要變更的值,並透過修補要求傳回修改過的部分表示法。以下是一個假設 Demo 資源使用 ETag 的範例:

GET https://www.googleapis.com/demo/v1/324?fields=etag,title,comment,characteristics
Authorization: Bearer your_auth_token

以下是部分回應:

200 OK
{
  "etag": "ETagString"
  "title": "New title"
  "comment": "First comment.",
  "characteristics": {
    "length": "short",
    "level": "5",
    "followers": ["Jo", "Will"],
  }
}

以下修補要求以上述回應為根據。這個要求同時使用 fields 參數來限制修補回應傳回的資料,如下所示:

PATCH https://www.googleapis.com/demo/v1/324?fields=etag,title,comment,characteristics
Authorization: Bearer your_auth_token
Content-Type: application/json
If-Match: "ETagString"
{
  "etag": "ETagString"
  "title": "",                  /* Clear the value of the title by setting it to the empty string. */
  "comment": null,              /* Delete the comment by replacing its value with null. */
  "characteristics": {
    "length": "short",
    "level": "10",              /* Modify the level value. */
    "followers": ["Jo", "Liz"], /* Replace the followers array to delete Will and add Liz. */
    "accuracy": "high"          /* Add a new characteristic. */
  },
}

伺服器以 200 OK HTTP 狀態碼回應,並在回應中提供更新後資源的部分表示法:

200 OK
{
  "etag": "newETagString"
  "title": "",                 /* Title is cleared; deleted comment field is missing. */
  "characteristics": {
    "length": "short",
    "level": "10",             /* Value is updated.*/
    "followers": ["Jo" "Liz"], /* New follower Liz is present; deleted Will is missing. */
    "accuracy": "high"         /* New characteristic is present. */
  }
}

直接建構修補要求

部分修補要求必須以您先前擷取的資料作為建構依據。舉例來說,如果您想要在陣列中新增一個項目,卻又不想失去任何現有的陣列元素,則您必須先取得現有的資料。同樣地,如果 API 使用 ETag,您必須透過要求傳送先前的 ETag 值,才能成功更新資源。

注意:使用 ETag 時,您可以使用 "If-Match: *" HTTP 標頭強制執行修補。如此一來,您就不需要在寫入之前先進行讀取。

然而,在其他情況下,您可以直接建構修補要求,而不必先擷取現有的資料。舉例來說,您可以輕鬆建構一個用來更新欄位值或新增欄位的修補要求。範例如下:

PATCH https://www.googleapis.com/demo/v1/324?fields=comment,characteristics
Authorization: Bearer your_auth_token
Content-Type: application/json

{
  "comment": "A new comment",
  "characteristics": {
    "volume": "loud",
    "accuracy": null
  }
}

此要求會將註解欄位設為新的值,或是以新的值覆寫註解欄位中現有的值。同樣地,如果有 volume 特性存在,則會覆寫掉它的值;否則就會建立該特性。如果有設定好的精確度欄位存在,則會移除該欄位。

處理修補回應

API 會在處理完有效的修補要求之後,傳回 200 OK HTTP 回應碼以及修改後資源的完整表示法。如果 API 使用 ETag,伺服器會在成功處理完修補要求時更新 ETag 的值 (如同 PUT)。

修補要求會傳回整個資源表示法,除非您使用 fields 參數來減少傳回的資料量。

如果修補要求產生的新資源狀態,且該狀態語法或語意上無效,則伺服器會傳回 400 Bad Request 或 422 Unprocessable Entity HTTP 狀態碼,且資源狀態維持不變。舉例來說,如果您嘗試刪除必要欄位的值,則伺服器會傳回錯誤。

PATCH HTTP 動詞不受支援時可使用的替代標記法

如果您的防火牆禁止使用 HTTP PATCH 要求,請執行 HTTP POST 要求,並將替換標頭設定為 PATCH,如下所示:

POST https://www.googleapis.com/...
X-HTTP-Method-Override: PATCH
...

修補和更新之間的差異

就實務層面而言,當您在為使用 HTTP PUT 動詞的更新要求傳送資料時,您只需傳送必填或選填欄位即可;如果您傳送了由伺服器設定的欄位值,這些值將會遭到忽略。雖然這看起來像是執行部分更新的另一種方法,但此方法仍有一些限制。使用 HTTP PUT 動詞進行更新時,如果您沒有提供必要參數,要求將會失敗;此外,如果您沒有提供選擇性參數,要求將會清除先前設定的資料。

這是為什麼使用修補較為安全。您只需為您想要變更的欄位提供資料,而您省略的欄位將不會被清除。這項規則的唯一例外是重複元素或陣列:如果您全數省略,欄位會保持原狀;如果您提供了任何一個重複元素或陣列,則整組元素或陣列都會替換為您提供的組合。

建議的要求頻率上限

我們可能會限制您呼叫 API 的頻率。建議您將要求次數保持在每秒 20 次以下。

向 Google Pay for Passes 發出批次要求

Google Pay for Passes API 支援 API 呼叫批次處理作業,以減少用戶端必須建立的連線數,如批次詳細資料一文所述。

以下是使用 Google Pay for Passes 程式庫批次處理要求的程式碼範例:

Java

//Set up client batch
Walletobjects client = new Walletobjects.Builder(httpTransport, jsonFactory, credentials)
        .setApplicationName(config.getApplicationName())
        .build();
BatchRequest batch = client.batch();

//Pre insert class
Services.VerticalType verticalType = Services.VerticalType.OFFER;
String classUid = String.format("%s_CLASS_%s", verticalType.toString(), UUID.randomUUID().toString());
String classId = String.format("%s.%s" , config.getIssuerId(), classUid);
OfferClass theClass = rsc.makeOfferClassResource(classId);
client.offerclass().insert(theClass).execute();

// Batch 10 object inserts
for (int i = 0; i < 10; ++i){

    // your objectUid should be a hash based off of pass metadata, for the demo we will use pass-type_object_uniqueid
    String objectUid = String.format("%s_OBJECT_%s", verticalType.toString(), UUID.randomUUID().toString());

    // check Reference API for format of "id", for example offer:(https://developers.google.com/pay/passes/reference/v1/offerobject/insert).
    // Must be alphanumeric characters, ".", "_", or "-".
    String objectId = String.format("%s.%s", config.getIssuerId(), objectUid);
    OfferObject anObject = rsc.makeOfferObjectResource((classId), objectId);
    client.offerobject().insert(anObject).queue(batch, new JsonBatchCallback(){

        @Override
        public void onSuccess(OfferObject t, HttpHeaders responseHeaders) throws IOException {
            System.out.println("Success!");

        }

        @Override
        public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) throws IOException {
            System.out.println("Error Message:" + e.getMessage());

        }
    });
}
//Execute batch
batch.execute();

Python

# Generating Credentials for auth request
credentials = service_account.Credentials.from_service_account_file(
    config.SERVICE_ACCOUNT_FILE,
        scopes = config.SCOPES)
authed_session = AuthorizedSession(credentials)
# Pre insert class
classUid = 'OFFER' + '_CLASS_'+ str(uuid.uuid4())
config.offer_class['id'] = '%s.%s' % (config.ISSUER_ID,classUid)
response = authed_session.post('https://www.googleapis.com/walletobjects/v1/offerClass?strict=true', json=config.offer_class)
# Build a batch of 10 object insert requests
data = ''
for i in range(9):
  objectUid = 'OFFER' + '_OBJECT_'+ str(uuid.uuid4())
  objectId = '%s.%s' % (config.ISSUER_ID,objectUid)
  data += '--batch_mybatch\n'
  data += 'Content-Type: application/json\n\n'
  data += 'POST /walletobjects/v1/offerObject/\n\n'
  config.offer_object['id'] = objectId
  data += json.dumps(config.offer_object) + '\n\n'
data += '--batch_mybatch--'
# execute batch
response = authed_session.post('https://walletobjects.googleapis.com/batch', data=data, headers={'Content-Type': 'multipart/mixed; boundary=batch_mybatch'})
print('Response:')
print(response.content.decode('UTF-8'))

PHP

// Create an Google_Client which facilitates REST call
$client = new Google_Client();
// do OAuth2.0 via service account file.
// See https://developers.google.com/api-client-library/php/auth/service-accounts#authorizingrequests
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . SERVICE_ACCOUNT_FILE); // for Google_Client() initialization for server-to-server
$client->useApplicationDefaultCredentials();
// Set application name.
$client->setApplicationName(APPLICATION_NAME);
// Set Api scopes.
$client->setScopes(array(SCOPES));
// Pre insert class
$vertical= "OFFER";
$service = new Google_Service_Walletobjects($client);
$classUid = $vertical."_CLASS_".uniqid('', true);
$classId = sprintf("%s.%s" , ISSUER_ID, $classUid);
$offerClass = ResourceDefinitions::makeOfferClassResource($classId);
$response = $service->offerclass->insert($offerClass);
//Initialize service
$service = new Google_Service_Walletobjects($client);
$service->getClient()->setUseBatch(true);
//Get batch
$batch = $service->createBatch();
// Batch 10 object inserts
for($i=0; $i<=10; $i++){
   $vertical= "OFFER";
   $objectUid= $vertical."_OBJECT_".uniqid('', true)
   $objectId = sprintf("%s.%s", ISSUER_ID, $objectUid);
   $offerObject = ResourceDefinitions::makeOfferObjectResource($classId, $objectId);
   $batch->add($service->offerobject->insert($offerObject));
}
// Execute batch
$results = $batch->execute();
var_export($results);