實體、屬性和金鑰

Datastore 中的資料物件稱為「實體」。實體有一或多個命名的「屬性」,每個屬性可以有一或多個值。相同種類的實體不需要具有相同屬性,而實體的特定屬性值並不全都需要屬於相同的資料類型 (如有必要,應用程式可在本身的資料模型建立及強制執行這類限制)。

Datastore 支援各種屬性值的資料類型。其中包括:

  • 整數
  • 浮點數
  • 字串
  • 日期
  • 二進位資料

如需這些類型的完整清單,請參閱屬性和值類型一節。

Datastore 中的每個實體都有專門用來識別該實體的「金鑰」。索引鍵由下列元件組成:

  • 實體的「命名空間」,可允許多租戶架構
  • 實體的類型,可將實體分類以便進行 Datastore 查詢
  • 個別實體的ID,可以是以下兩者之一:
    • 「索引鍵名稱」字串
    • 整數「數字 ID」
  • 選用的祖系路徑,可將實體置於 Datastore 階層之中

應用程式可使用實體的索引鍵從 Datastore 擷取個別實體,或依據實體的索引鍵或屬性值發出查詢,以擷取一或多個實體。

Java App Engine SDK 在 com.google.appengine.api.datastore 套件中提供簡易的 API,可直接支援 Datastore 功能。本文件中的所有範例均以這個低階 API 為基礎;您可以選擇在應用程式中直接使用這個 API,或以其為基礎來建置自己的資料管理層。

Datastore 本身不會對實體結構強制執行任何限制,例如特定屬性是否具有特定類型的值;這項工作是由應用程式負責。

種類及 ID

每個 Datastore 實體都屬於特定「種類」,可將實體分門別類,方便查詢。舉例來說,人力資源應用程式可以使用屬於 Employee 種類的實體來代表公司的每位員工。您在 Java Datastore API 中建立實體時,可透過 Entity() 建構函式的引數來指定實體的種類。以兩條底線 (__) 開頭的種類名稱均為保留名稱,不得使用。

以下範例示範了如何建立 Employee 種類的實體、填入屬性值,並將其儲存至 Datastore:

Entity employee = new Entity("Employee", "asalieri");
employee.setProperty("firstName", "Antonio");
employee.setProperty("lastName", "Salieri");
employee.setProperty("hireDate", new Date());
employee.setProperty("attendedHrTraining", true);

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
datastore.put(employee);

除了種類以外,建立每個實體時還會指派實體的「ID」。因為 ID 屬於實體索引鍵的一部分,所以會與實體永久關聯,且無法變更。ID 可利用兩種方式指派:

  • 您的應用程式可以指定自身的實體「索引鍵名稱」字串。
  • 您可以讓 Datastore 自動指派整數的「數字 ID」給實體。

如要指派索引鍵名稱給實體,請在建立實體時,將名稱當做第二個引數提供給建構函式:

Entity employee = new Entity("Employee", "asalieri");

如要讓 Datastore 自動指派數字 ID,請省略這個引數:

Entity employee = new Entity("Employee");

指派 ID

Datastore 可使用兩種不同的自動 ID 政策設定自動產生 ID:

  • default 政策可產生隨機順序的未使用 ID,近乎均勻分佈。每個 ID 最長可達 16 個十進位數字。
  • legacy 政策可建立一系列非連續的較小整數 ID。

如果您要向使用者顯示實體 ID,並/或依據其順序顯示,最理想的方式就是使用手動分配。

Datastore 會產生順序隨機且近乎均勻分佈的未使用 ID。每個 ID 最長可達 16 個十進位數字。

祖系路徑

Cloud Datastore 中的實體會形成階層結構空間,與檔案系統的目錄結構相似。建立實體時,可選擇將其他實體指定為「父項」;新實體則為父系實體的「子項」(請注意,不同於檔案系統,父項實體不需要實際存在)。沒有父項的實體則是「根實體」。實體與父項實體之間具有永久關聯性,一旦實體建立後就無法變更。Cloud Datastore 絕對不會將相同的數字 ID 指派給父項相同的兩個實體,也不會指派給兩個根實體 (即沒有父項的實體)。

實體的父項、父項的父項等以此類推,全都是這個實體的「祖系」;實體的子項、子項的子項等等,則都是其「子系」。根實體及其所有子系都屬於相同的「實體群組」。從根實體開始,再從父項到子項,最後到指定實體的實體序列,即構成該實體的「祖系路徑」。識別實體的完整索引鍵,包含連串種類-ID 組合序列,其中指定了實體的祖系路徑,最後則以該實體本身的種類-ID 組合做為結尾:

[Person:GreatGrandpa, Person:Grandpa, Person:Dad, Person:Me]

根實體的祖系路徑是空白路徑,其金鑰只包含實體本身的種類與 ID。

[Person:GreatGrandpa]

本概念以下圖說明:

顯示實體群組中根實體與子實體的關係

如要指定實體的父項,請在建立子系實體時,提供父系實體的金鑰做為 Entity() 建構函式的引數。您可以呼叫父系實體的 getKey() 方法來取得金鑰:

Entity employee = new Entity("Employee");
datastore.put(employee);

Entity address = new Entity("Address", employee.getKey());
datastore.put(address);

如果新實體也擁有索引鍵名稱,請提供索引鍵名稱做為 Entity() 建構函式的第二個引數,並提供父系實體的索引鍵做為第三個引數:

Entity address = new Entity("Address", "addr1", employee.getKey());

交易和實體群組

每次嘗試建立、更新或刪除實體時,都是在交易的背景下進行。單一交易可包含的前述作業數量不受限制。為了維持資料一致性,交易一定會使其中包含的變動整組適用於 Datastore,其中如有任何變動失敗,則整組變動均不適用。此外,在相同交易內執行的所有同步一致性讀取作業 (祖系查詢或「get」) 將出現一致的資料快照。

如前所述,實體群組是一組實體,透過祖系連接至共同根元素。將資料組織為實體群組,可限制執行的交易:

  • 一項交易所存取的所有資料,最多只能存在於 25 個實體群組中。
  • 如果您想在交易中使用查詢,必須將資料歸納成實體群組,才可指定能夠比對出正確資料的祖系篩選條件。
  • 單一實體群組的寫入總處理量大約是每秒一次交易。之所以會有這項限制,原因在於 Datastore 會針對橫跨大範圍地理區域的每個實體執行免主機的同步複製作業,以利發揮高度的可靠性和容錯能力。

在許多應用程式中,當您想取得不相關資料的廣泛檢視畫面時,可以使用最終一致性 (即跨多個實體群組的非祖系查詢,有時可能會傳回稍微過時的資料),然後在查看或編輯一組高度相關資料時,使用同步一致性 (祖系查詢或單一實體的 get)。在此類應用程式中,針對各組高度相關的資料使用獨立實體群組,通常會有不錯的效果。詳情請參閱「建立同步一致性結構」。

屬性和值類型

與實體相關聯的資料值由一或多個「屬性」組成。每個屬性都有一個名稱及一或多個值。一個屬性可能會有多個類型的值,而兩個實體的相同屬性可能會有不同類型的值。屬性可能已建立索引或未建立索引 (排序或篩選屬性「P」的查詢將忽略「P」未建立索引的實體)。一個實體最多可有 20,000 個已建立索引的屬性。

支援的值類型如下:

值類型 Java 類型 排序順序 附註
整數 short
int
long
java.lang.Short
java.lang.Integer
java.lang.Long
數字 以長整數形式儲存,然後轉換為欄位類型

超出範圍值溢出
浮點數 float
double
java.lang.Float
java.lang.Double
數字 64 位元雙精度,
IEEE 754
布林值 boolean
java.lang.Boolean
false<true
文字字串 (短) java.lang.String Unicode 最多 1500 個位元組

超過 1500 個位元組的值會擲回 IllegalArgumentException
文字字串 (長) com.google.appengine.api.datastore.Text 最多 1 MB

不會建立索引
位元組字串 (短) com.google.appengine.api.datastore.ShortBlob 位元組順序 最多 1500 個位元組

超過 1500 個位元組的值會擲回 IllegalArgumentException
位元組字串 (長) com.google.appengine.api.datastore.Blob 最多 1 MB

不會建立索引
日期與時間 java.util.Date 依時間順序
地理點 com.google.appengine.api.datastore.GeoPt 依照緯度、
然後經度
郵遞地址 com.google.appengine.api.datastore.PostalAddress Unicode
電話號碼 com.google.appengine.api.datastore.PhoneNumber Unicode
電子郵件地址 com.google.appengine.api.datastore.Email Unicode
Google 帳戶使用者 com.google.appengine.api.users.User 電子郵件地址
依 Unicode 順序
即時通訊控點 com.google.appengine.api.datastore.IMHandle Unicode
連結 com.google.appengine.api.datastore.Link Unicode
類別 com.google.appengine.api.datastore.Category Unicode
評分 com.google.appengine.api.datastore.Rating 數字
Datastore 索引鍵 com.google.appengine.api.datastore.Key
或參考物件 (當做子項)
依路徑元素
(種類、ID、
種類、ID...)
最多 1500 個位元組

超過 1500 個位元組的值會擲回 IllegalArgumentException
Blobstore 金鑰 com.google.appengine.api.blobstore.BlobKey 位元組順序
嵌入實體 com.google.appengine.api.datastore.EmbeddedEntity 未建立索引
空值 null

重要事項:強烈建議您避免將 users.User 儲存為屬性值,因為這會包含電子郵件地址和專屬 ID。如果使用者變更電子郵件地址,但您還是使用先前儲存的 user.User 來比對新的 user.User 值,兩者將無法配對。請改為使用 User 的「使用者 ID 值」做為使用者的穩定專屬 ID。

針對文字字串和未編碼的二進位資料 (位元組字串),Datastore 支援兩種值類型:

  • 短字串 (不超過 1,500 個位元組) 會建立索引,可用於查詢篩選器條件和排序順序。
  • 長字串 (不超過 1 MB) 不會建立索引,也不能用於查詢篩選器和排序順序。
注意:長位元組字串類型在 Datastore API 中的名稱為 Blob。這類型與 Blobstore API 中使用的 blob 無關。

當查詢的屬性具有混合類型的值時,Datastore 會根據內部表示法使用確定性排序:

  1. 空值
  2. 固定點數
    • 整數
    • 日期和時間
    • 評分
  3. 布林值
  4. 位元組序列
    • 位元組字串
    • Unicode 字串
    • Blobstore 索引鍵
  5. 浮點數
  6. 地理點
  7. Google 帳戶使用者
  8. Datastore 索引鍵

長文字字串、長位元組字串和嵌入實體不會建立索引,因此未定義排序。

處理實體

應用程式可使用 Datastore API 建立、擷取、更新及刪除實體。如果應用程式知道實體的完整索引鍵 (或是可從父系索引鍵、種類和 ID 中取得),即可在該實體上使用索引鍵直接作業。應用程式也可以透過 Datastore 查詢取得實體索引鍵,詳情請參閱「Datastore 查詢」頁面。

Java Datastore API 會使用 DatastoreService 介面的方法處理實體。您可以呼叫靜態方法 DatastoreServiceFactory.getDatastoreService() 取得 DatastoreService 物件:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

建立實體

您可以建構 Entity 類別的例項,藉此建立新實體,並將實體類型做為引數提供給 Entity() 建構函式。

視需求填入實體的屬性後,將實體以引數的形式傳送給 DatastoreService.put() 方法,即可在資料儲存庫中儲存實體。您可以將實體的索引鍵名稱當做第二個引數傳送給建構函式,藉此為實體指定索引鍵名稱。

Entity employee = new Entity("Employee", "asalieri");
// Set the entity properties.
// ...
datastore.put(employee);

如果您沒有提供索引鍵名稱,Datastore 會自動為實體的索引鍵產生數字 ID:

Entity employee = new Entity("Employee");
// Set the entity properties.
// ...
datastore.put(employee);

擷取實體

如要擷取指定金鑰所標示的實體,請將 Key 物件傳遞至 DatastoreService.get() 方法:

// Key employeeKey = ...;
Entity employee = datastore.get(employeeKey);

更新實體

如要更新現有實體,請修改實體物件的屬性,然後將物件傳送至 DatastoreService.put() 方法。物件資料會覆寫現有實體,每次呼叫 put() 時,系統會將整個物件傳送至 Datastore。

刪除實體

如果有實體的金鑰,您就可以透過 DatastoreService.delete() 方法刪除該實體:

// Key employeeKey = ...;
datastore.delete(employeeKey);

重複屬性

您可以在單一屬性中儲存多個值。

Entity employee = new Entity("Employee");
ArrayList<String> favoriteFruit = new ArrayList<String>();
favoriteFruit.add("Pear");
favoriteFruit.add("Apple");
employee.setProperty("favoriteFruit", favoriteFruit);
datastore.put(employee);

// Sometime later
employee = datastore.get(employee.getKey());
@SuppressWarnings("unchecked") // Cast can't verify generic type.
    ArrayList<String> retrievedFruits = (ArrayList<String>) employee
    .getProperty("favoriteFruit");

嵌入實體

有時您會發現將實體當做另一個實體的屬性來嵌入是一種方便的做法。比方說,這種做法很適合用於在實體中建立屬性值的階層結構。Java 類別 EmbeddedEntity 可讓您執行這項操作:

// Entity employee = ...;
EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();

embeddedContactInfo.setProperty("homeAddress", "123 Fake St, Made, UP 45678");
embeddedContactInfo.setProperty("phoneNumber", "555-555-5555");
embeddedContactInfo.setProperty("emailAddress", "test@example.com");

employee.setProperty("contactInfo", embeddedContactInfo);

如果索引中包含嵌入實體,您就可以查詢子屬性。如果索引排除了嵌入實體,則會一併將所有子屬性排除於在外。您可以選擇建立索引鍵與嵌入實體的關聯,但 (和完善的實體不同) 索引鍵並非必要,而且即使有索引鍵,也不能用來擷取實體。

您可以使用 setPropertiesFrom() 方法從現有實體複製嵌入實體的屬性,而不是手動填入這些屬性:

// Entity employee = ...;
// Entity contactInfo = ...;
EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();

embeddedContactInfo.setKey(contactInfo.getKey()); // Optional, used so we can recover original.
embeddedContactInfo.setPropertiesFrom(contactInfo);

employee.setProperty("contactInfo", embeddedContactInfo);

您稍後可以使用相同的方法從嵌入實體復原原本的實體:

Entity employee = datastore.get(employeeKey);
EmbeddedEntity embeddedContactInfo = (EmbeddedEntity) employee.getProperty("contactInfo");

Key infoKey = embeddedContactInfo.getKey();
Entity contactInfo = new Entity(infoKey);
contactInfo.setPropertiesFrom(embeddedContactInfo);

批次作業

DatastoreService 方法 put()get()delete() (以及其 AsyncDatastoreService 對應項目) 都有批次版本,可接受可迴圈物件 (put()Entity 類別,get()delete()Key 類別),並使用該物件在單一 Datastore 呼叫中對多個實體執行作業:

Entity employee1 = new Entity("Employee");
Entity employee2 = new Entity("Employee");
Entity employee3 = new Entity("Employee");
// ...

List<Entity> employees = Arrays.asList(employee1, employee2, employee3);
datastore.put(employees);

這些批次作業會將所有實體或索引鍵分組為實體群組,然後在每個實體群組上並行執行要求的作業。這類批次呼叫僅會產生單次服務呼叫的負擔,因此比個別呼叫每個實體的速度還快。如果涉及多個實體群組,則會在伺服器端針對所有群組並行執行作業。

產生金鑰

應用程式可以使用 KeyFactory 類別,為實體建立 Key 物件,這些實體來自已知的元件,例如實體的類型和 ID。如果實體沒有父項,請將種類與 ID (金鑰名稱字串或數字 ID) 傳送至靜態方法 KeyFactory.createKey(),藉以建立金鑰。以下範例會為種類為 Person 且金鑰名稱為 "GreatGrandpa" 或數字 ID 為 74219 的實體建立金鑰:

Key k1 = KeyFactory.createKey("Person", "GreatGrandpa");
Key k2 = KeyFactory.createKey("Person", 74219);

如果金鑰包含路徑元件,您可以使用輔助類別 KeyFactory.Builder 建立路徑。這個類別的 addChild 方法可將單一實體新增至路徑,並傳回建構函式本身,讓您可以將一系列呼叫鏈結在一起,進從根實體開始,一次為一個實體建構路徑。建構完整路徑之後,請呼叫 getKey 以擷取產生的金鑰:

Key k =
    new KeyFactory.Builder("Person", "GreatGrandpa")
        .addChild("Person", "Grandpa")
        .addChild("Person", "Dad")
        .addChild("Person", "Me")
        .getKey();

KeyFactory 類別也包含靜態方法 keyToStringstringToKey,用於在金鑰及其字串表示法之間進行轉換:

String personKeyStr = KeyFactory.keyToString(k);

// Some time later (for example, after using personKeyStr in a link).
Key personKey = KeyFactory.stringToKey(personKeyStr);
Entity person = datastore.get(personKey);

金鑰的字串表示法為「可在網路上安全使用」:這種表示法並不包含在 HTML 或網址中被視為特殊的字元。

使用空白清單

Datastore 先前沒有可適當表示空白清單的屬性表示法。Java SDK 的解決方式是將空白集合儲存為空值,但也因此無法區分空值和空白清單。為了維持回溯相容性,會繼續以這種方法做為預設行為,運作方式摘要如下:

  • 空值屬性以空值寫入 Datastore
  • 空白集合以空值寫入 Datastore
  • 從 Datastore 讀取的空值會被當做空值
  • 空白集合會被當成空值讀取。

但是,如果您變更預設行為,Java 適用的 SDK 就會支援儲存空白清單。建議您對變更應用程式預設行為一事深思熟慮之後,再開啟空白清單的支援

如果要變更預設行為以能使用空白清單,請在應用程式初始化期間,按照下列方式設定 DATASTORE_EMPTY_LIST_SUPPORT 屬性:

System.setProperty(DatastoreServiceConfig.DATASTORE_EMPTY_LIST_SUPPORT, Boolean.TRUE.toString());

將此屬性如上所示設為 true 之後,運作方式如下:

  • 空值屬性以空值寫入 Datastore
  • 空白集合以空白清單寫入 Datastore
  • 從 Datastore 讀取的空值會被當做空值
  • 從 Datastore 讀取時,會以空白集合傳回空白清單。