JDO 中的 Datastore 查詢

本文件著重說明如何針對 App Engine Datastore 查詢使用 Java 資料物件 (JDO) 持續性架構。如需一般的查詢相關資訊,請參閱 Datastore 查詢主頁面。

「查詢」會從 Datastore 中擷取符合一組特定條件的實體。查詢會針對指定種類的實體執行作業,其可指定實體屬性值、索引鍵和祖系的篩選條件,而且傳回的「結果」可能會是零個或更多個實體。另外,查詢也能指定排序順序,按照結果的屬性值進行排序。結果包含的所有實體都有篩選器中每個具名屬性的至少一個值 (有可能是空值),並按排序順序列出,其屬性值符合所有指定的篩選條件。查詢可傳回完整實體、 投影實體,或是僅傳回實體 金鑰

典型的查詢包含以下幾項:

執行時,查詢會擷取符合所有指定篩選條件的特定種類實體,並按照指定的順序進行排序。查詢會以唯讀方式執行。

附註:為了節省記憶體並提升效能,查詢應盡可能指定傳回的結果數限制。

附註:基於索引的查詢機制支援各種查詢,適用於大多數應用程式。不過,這種機制並不支援其他資料庫技術常見的某些查詢種類,Datastore 查詢引擎尤其不支援彙整與匯總查詢。如要瞭解 Datastore 查詢的限制,請參閱 Datastore 查詢頁面。

使用 JDOQL 查詢

JDO 具有一套查詢語言,用於擷取符合一組特定條件的物件。這種語言稱為 JDOQL,會直接參照 JDO 資料類別和欄位,並針對查詢參數和結果進行型別檢查。JDOQL 與 SQL 類似,但更適合用於 App Engine Datastore 這類的物件導向資料庫 (App Engine 的 JDO API 部署項目並不直接支援 SQL 查詢)。

JDO Query 介面支援多種呼叫樣式:您可以使用 JDOQL 字串語法,在字串中指定完整的查詢,也可以在 Query 物件上呼叫方法,指定查詢的部分或全部部分。下列範例顯示了呼叫方法樣式,當中包含一個篩選器與一個排序順序,並針對篩選器中使用的值使用替代參數。傳送至 Query 物件 execute() 方法的引數值會按照指定順序替代至查詢:

import java.util.List;
import javax.jdo.Query;

// ...

Query q = pm.newQuery(Person.class);
q.setFilter("lastName == lastNameParam");
q.setOrdering("height desc");
q.declareParameters("String lastNameParam");

try {
  List<Person> results = (List<Person>) q.execute("Smith");
  if (!results.isEmpty()) {
    for (Person p : results) {
      // Process result p
    }
  } else {
    // Handle "no results" case
  }
} finally {
  q.closeAll();
}

下方是使用字串語法的相同查詢:

Query q = pm.newQuery("select from Person " +
                      "where lastName == lastNameParam " +
                      "parameters String lastNameParam " +
                      "order by height desc");

List<Person> results = (List<Person>) q.execute("Smith");

您可以混合這些定義查詢的樣式,例如:

Query q = pm.newQuery(Person.class,
                      "lastName == lastNameParam order by height desc");
q.declareParameters("String lastNameParam");

List<Person> results = (List<Person>) q.execute("Smith");

您可以多次呼叫 execute() 方法,重複使用單一 Query 例項,將參數取代為不同的值。每個呼叫都會執行查詢並傳回結果的集合。

JDOQL 字串語法支援以字串與數值指定常值;其他所有值類型都必須使用參數替代。查詢字串中的常值可以使用單引號 (') 或雙引號 (") 括住。以下是使用字串常值的範例:

Query q = pm.newQuery(Person.class,
                      "lastName == 'Smith' order by height desc");

篩選器

「屬性篩選器」指定

  • 屬性名稱
  • 比較運算子
  • 屬性值
例如:

Filter propertyFilter =
    new FilterPredicate("height", FilterOperator.GREATER_THAN_OR_EQUAL, minHeight);
Query q = new Query("Person").setFilter(propertyFilter);
Query q = pm.newQuery(Person.class);
q.setFilter("height <= maxHeight");

屬性值必須由應用程式提供,不能參考或以其他屬性計算而來。如果實體具有指定名稱的屬性,且屬性值與篩選器中指定的值比較,符合比較運算子描述的方式,則該實體符合篩選器的條件。

比較運算子可以為以下任何一種:

運算子 含義
== 等於
< 小於
<= 小於或等於
> 大於
>= 大於或等於
!= 不等於

查詢主頁面所述,單一查詢無法針對多個屬性使用不等式篩選器 (<<=>>=!=)。但允許針對相同屬性使用多個不等式篩選器,例如用於查詢某個範圍的值。contains() 篩選器可對應至 SQL 中的 IN 篩選器,系統則會使用下列語法支援這些篩選器:

// Query for all persons with lastName equal to Smith or Jones
Query q = pm.newQuery(Person.class, ":p.contains(lastName)");
q.execute(Arrays.asList("Smith", "Jones"));

不等於 (!=) 運算子實際上會執行兩項查詢:一項查詢中的其他篩選器皆未變更,「不等於」篩選器會替換為「小於」(<) 篩選器,另一項則是會替換為「大於」(>) 篩選器。然後系統會將結果合併並排序。查詢中只能有一個「不等於」篩選器,且一個查詢中不能有任何其他的不等式篩選器。

contains() 運算子也會執行多個查詢:指定清單中的各個項目各有一項查詢,而其他篩選器均未經過變更,且 contains() 篩選器已替換為「等於」(==) 篩選器。結果會依照清單中項目的順序進行合併。如果查詢中包含多個 contains() 篩選器,則會以多項查詢執行,每項查詢會對應至 contains() 清單中每個可能的值「組合」

包含 not-equal (!=) 或 contains() 運算子的單一查詢上限為 30 個子查詢。

如要進一步瞭解 !=contains() 查詢如何轉譯為 JDO/JPA 架構中的多個查詢,請參閱「 使用 != 和 IN 篩選器進行查詢」一文。

在 JDOQL 字串語法中,您可以使用 && (邏輯「and」) 和 || (邏輯「or」) 運算子分隔多個篩選器:

q.setFilter("lastName == 'Smith' && height < maxHeight");

系統不支援否定 (邏輯「not」) 運算子。請注意,只有在分隔的篩選器具有相同的屬性名稱 (也就是可以組合為單一 contains() 篩選器) 時,才能使用 || 運算子:

// Legal: all filters separated by || are on the same property
Query q = pm.newQuery(Person.class,
                      "(lastName == 'Smith' || lastName == 'Jones')" +
                      " && firstName == 'Harold'");

// Not legal: filters separated by || are on different properties
Query q = pm.newQuery(Person.class,
                      "lastName == 'Smith' || firstName == 'Harold'");

排序順序

查詢「排序順序」指定

  • 屬性名稱
  • 排序方向 (遞增或遞減)

例如:

// Order alphabetically by last name:
Query q1 = new Query("Person").addSort("lastName", SortDirection.ASCENDING);

// Order by height, tallest to shortest:
Query q2 = new Query("Person").addSort("height", SortDirection.DESCENDING);

例如:

// Order alphabetically by last name:
Query q = pm.newQuery(Person.class);
q.setOrdering("lastName asc");

// Order by height, tallest to shortest:
Query q = pm.newQuery(Person.class);
q.setOrdering("height desc");

如果查詢包含多個排序順序,他們將按照指定的順序排序。下列範例會先將「last name」遞增排列,再將「height」遞減排列:

Query q =
    new Query("Person")
        .addSort("lastName", SortDirection.ASCENDING)
        .addSort("height", SortDirection.DESCENDING);
Query q = pm.newQuery(Person.class);
q.setOrdering("lastName asc, height desc");

如果未指定排序順序,則會按照從 Datastore 擷取的順序傳回結果。

附註:由於 Datastore 執行查詢的方式,如果查詢指定以不等式篩選條件篩選某個屬性,同時指定其他屬性的排序順序,則在不等式篩選條件中使用的屬性,順序必須在其他屬性之前。

範圍

查詢可指定傳回特定「範圍」的結果給應用程式。範圍會指出完整結果集中應最先及最後傳回的結果。系統會透過數字索引識別結果,其中 0 代表該組合中的首個結果。舉例來說,範圍為 5, 10 會傳回第 6 到第 10 個結果:

q.setRange(5, 10);

附註:使用範圍可能會影響效能,因為 Datastore 必須在擷取後捨棄開始位移之前的所有結果。舉例來說,範圍為 5, 10 的查詢會從 Datastore 擷取十個結果,捨棄前五個,並將剩下的五個傳回給應用程式。

以金鑰為基礎的查詢

實體金鑰可以是查詢篩選器或排序順序的主體。Datastore 會考量這類查詢的完整索引鍵值,包括實體的祖系路徑、類型,以及應用程式指派的索引鍵名稱字串或系統指派的數字 ID。由於系統中的所有實體都有專屬的鍵,因此您可以透過鍵查詢輕鬆擷取特定類型的實體,例如資料儲存庫內容的批次轉儲。與 JDOQL 範圍不同的是,這個方法不會因為實體數目的多寡而影響效率。

在比較不等式時,金鑰會根據以下標準進行排序:

  1. 祖系路徑
  2. 實體種類
  3. ID (索引鍵名稱或數字 ID)

祖系路徑元素的比較方式類似:依序先比較種類 (字串),再比較索引鍵名稱或數字 ID。種類和索引鍵名稱都是按位元組值排序的字串;數字 ID 是按數字排序的整數。若父項和種類相同的實體混用索引鍵名稱和數字 ID,則會先列出採用數字 ID 的實體,再列出採用索引鍵名稱的實體。

在 JDO 中,您可以透過物件的主要金鑰欄位參考查詢的實體金鑰。如要將鍵用作查詢篩選器,請將參數類型 Key 指定給 declareParameters() 方法。以下程式碼可以搜尋最喜愛某種食物的所有 Person 實體,並假設 PersonFood 之間存在 一對一非從屬關聯性

Food chocolate = /*...*/;

Query q = pm.newQuery(Person.class);
q.setFilter("favoriteFood == favoriteFoodParam");
q.declareParameters(Key.class.getName() + " favoriteFoodParam");

List<Person> chocolateLovers = (List<Person>) q.execute(chocolate.getKey());

「純索引鍵查詢」僅傳回結果實體的索引鍵而非實體本身,與擷取整個實體相比,延遲時間及成本都較低。

Query q = new Query("Person").setKeysOnly();
Query q = pm.newQuery("select id from " + Person.class.getName());
List<String> ids = (List<String>) q.execute();

先執行僅有金鑰的查詢,並從結果中獲取實體的子集,通常較為經濟實惠。您不需執行一般查詢而得到多餘的實體。

Extent

JDO「extent」代表 Datastore 中屬於特定類別的每個物件。您可以將所需類別傳送至持續性管理程式的 getExtent() 方法來建立 extent。Extent 介面可擴充 Iterable 介面,用於存取結果,並視需求分批擷取結果。存取完結果之後,您就能呼叫 extent 的 closeAll() 方法。

以下範例逐一處理 Datastore 中的每個 Person 物件:

import java.util.Iterator;
import javax.jdo.Extent;

// ...

Extent<Person> extent = pm.getExtent(Person.class, false);
for (Person p : extent) {
  // ...
}
extent.closeAll();

依查詢刪除實體

如果您發出的查詢目標是刪除符合查詢篩選器的所有實體,可以使用 JDO 的「依查詢刪除」功能少編寫一些程式碼。以下程式碼會刪除身高超過指定高度的所有人:

Query q = pm.newQuery(Person.class);
q.setFilter("height > maxHeightParam");
q.declareParameters("int maxHeightParam");
q.deletePersistentAll(maxHeight);

您會發現唯一的差異在於,我們呼叫的是 q.deletePersistentAll() 而非 q.execute()。上方提及的所有篩選器、排序順序與索引規則和限制均適用於選取或刪除結果集的查詢。不過,請注意,如果您使用 pm.deletePersistent() 刪除這些 Person 實體,查詢所刪除實體的任何相依子項也會遭到刪除。如要進一步瞭解從屬子項,請參閱「 JDO 中的實體關係」頁面。

查詢游標

在 JDO 中,您可以使用擴充功能和 JDOCursorHelper 類別,搭配 JDO 查詢使用游標。擷取清單形式的結果或使用疊代器時可使用游標。如要取得游標,您可以將結果清單或迭代器傳遞至靜態方法 JDOCursorHelper.getCursor()

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jdo.Query;
import com.google.appengine.api.datastore.Cursor;
import org.datanucleus.store.appengine.query.JDOCursorHelper;


Query q = pm.newQuery(Person.class);
q.setRange(0, 20);

List<Person> results = (List<Person>) q.execute();
// Use the first 20 results

Cursor cursor = JDOCursorHelper.getCursor(results);
String cursorString = cursor.toWebSafeString();
// Store the cursorString

// ...

// Query q = the same query that produced the cursor
// String cursorString = the string from storage
Cursor cursor = Cursor.fromWebSafeString(cursorString);
Map<String, Object> extensionMap = new HashMap<String, Object>();
extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, cursor);
q.setExtensions(extensionMap);
q.setRange(0, 20);

List<Person> results = (List<Person>) q.execute();
// Use the next 20 results

如要進一步瞭解查詢游標,請參閱 Datastore 查詢頁面。

Datastore 的讀取政策和呼叫期限

您可以針對 PersistenceManager 例項使用設定時發出的所有呼叫,設定讀取政策 (同步一致性與最終一致性) 和 Datastore 呼叫期限。您也可以覆寫個別 Query 物件的這些選項。(但請注意,如果您透過金鑰擷取實體,則無法覆寫這些選項的設定。)

為 Datastore 查詢選取最終一致性後,用於收集結果的索引也會透過最終一致性的方式存取。查詢有時會傳回不符合查詢條件的實體,即使採用同步一致的讀取政策也是如此。(如果查詢使用祖系篩選器,您可以使用交易來確保結果集一致。)

如要覆寫單一查詢的讀取政策,請呼叫其 addExtension() 方法:

Query q = pm.newQuery(Person.class);
q.addExtension("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");

可能的值為 "EVENTUAL""STRONG";預設值為 "STRONG",除非在設定檔 jdoconfig.xml 中另行設定。

如要覆寫單一查詢的 Datastore 呼叫期限,請呼叫其 setDatastoreReadTimeoutMillis() 方法:

q.setDatastoreReadTimeoutMillis(3000);

值為以毫秒為單位表示的時間間隔。