應用程式可利用查詢功能,搜尋 Datastore 中符合篩選器特定搜尋條件的實體。
總覽
應用程式可利用查詢功能,搜尋 Datastore 中符合篩選器特定搜尋條件的實體。舉例來說,記錄多個留言板的應用程式可利用查詢功能,擷取其中一個留言版的訊息,並依日期排序:
...
...
有些查詢比其他查詢複雜,Datastore 需要預先建構這些查詢的索引。這些預先建構的索引會在設定檔 index.yaml 中指定。在開發伺服器上,如果您執行需要索引的查詢,但您尚未指定索引,開發伺服器會自動將索引新增至 index.yaml。但在您的網站中,需要尚未指定索引的查詢會失敗。
因此,典型的開發週期是先在開發伺服器上嘗試執行新的查詢,然後再更新網站,以便使用自動變更的 index.yaml。
若只要更新 index.yaml 而不上傳應用程式,您可以執行
gcloud app deploy index.yaml。如果您的資料儲存庫包含許多實體,就需要較長的時間為實體建立新索引;在此情況下,最好先更新索引定義再上傳使用新索引的程式碼。您可以使用管理控制台,瞭解索引何時建構完成。
App Engine Datastore 原生支援完全相符的篩選條件 (== 運算子) 和比較條件 (<、<=、> 和 >= 運算子)。這項功能支援使用布林值 AND 運算合併多個篩選器,但有一些限制 (詳情請參閱下文)。
除了原生運算子外,API 還支援 != 運算子 (使用布林值 OR 運算結合多組篩選器),以及檢查是否等於其中一個可能清單值的 IN 運算 (類似 Python 的「in」運算子)。這些作業不會 1:1 對應至 Datastore 的原生作業,因此相對來說有點奇怪且緩慢。這些項目是透過結果串流的記憶體內合併作業實作。請注意,p != v 會實作為「p < v OR p > v」。(這對重複屬性很重要)。
限制事項:Datastore 會對查詢強制執行一些限制。違反這些規則會導致例外狀況。舉例來說,目前不允許合併過多篩選條件、針對多個屬性使用不等式,或合併不等式與不同屬性的排序順序。此外,如果篩選條件參照多個屬性,有時也需要設定次要索引。
不支援:Datastore 不直�����援子���串���對、不區分大小寫比對,以及所謂的全文搜尋。您可以使用計算屬性,實作不區分大小寫的比對,甚至是全文搜尋。
依屬性值進行篩選
從 NDB 屬性回呼 Account 類別:
通常您不會想擷取特定種類的「所有」實體,而是只擷取具有特定值或值範圍的實體。
屬性物件會超載一些運算子,以供傳回可用於控管查詢的篩選器運算式:舉例來說,如要尋找 userid 屬性值剛好為 42 的所有 Account 實體,您可以使用以下運算式:
(如果確認該 userid 只有一個 Account,您可能會想要使用 userid 做為金鑰。
Account.get_by_id(...)
的運算速度比 Account.query(...).get() 更快。)
NDB 支援下列運算:
property == value
property < value
property <= value
property > value
property >= value
property != value
property.IN([value1, value2])
如要篩選不等式,您可以使用類似以下的語法:
這會找出 userid 屬性大於或等於 40 的所有帳戶實體。
在這些運算中,!= 和 IN 這兩個運算是以結合其他運算的方式進行實作,有點複雜 (如 != 和 IN)。
您可以指定多個篩選器:
這會合併指定的篩選器引數,傳回 userid 值大於或等於 40 且小於 50 的所有帳戶實體。
注意: 如先前所述,Datastore 會拒絕針對多個屬性使用不等式篩選條件的查詢。
相對於在單一運算式中指定完整的查詢篩選器,您可能會發現逐步建構查詢的方式會更為方便,例如:
query3 等於前一個範例中的 query 變數。請注意,查詢物件是不可變動的,因此 query2 的建構作業不會影響 query1,而 query3 的建構作業也不會影響 query1 或 query2。
!= 和 IN 運算
從 NDB 屬性回呼 Article 類別:
!= (不等於) 和 IN (成員資格) 運算是使用 OR 運算結合其他篩選器進行實作。就前者來說,
property != value
會實作為
(property < value) OR (property > value)
例如:
等同於
附註:這個查詢不會搜尋未包含「perl」標記的 Article 實體,這點可能超乎某些人的意料。而是找出至少有一個標記不等於「perl」的所有實體。
舉例來說,即使下列實體含有「perl」標記,仍會納入結果:
不過,以下網址不會納入:
您無法查詢不含「perl」標記的實體。
同樣地,下列的 IN 運算
property IN [value1, value2, ...]
會檢查屬性值是否包含在可能值的清單中,並會以下列形式進行實作:
(property == value1) OR (property == value2) OR ...
例如:
等同於
附註:
使用 OR 刪除重複結果的查詢:即使這些實體可能符合兩個或兩個以上的子查詢,結果串不會出現相同實體超過一次。
查詢重複屬性
前一節中定義的 Article 類別,也是查詢重複屬性的範例。特別是類似如下的篩選器
即使 Article.tags 是重複屬性,仍使用單一值。您無法將重複屬性與清單物件進行比對 (Datastore 無法辨別),而類似如下的篩選器
相對於搜尋標記值包含在 ['python', 'ruby', 'php'] 清單的 Article 實體,此查詢搜尋的是 tags 值 (視為清單) 包含「至少其中一個值」的實體。
在重複屬性上查詢 None 值會導致行為未定義,請勿這麼做。
結合 AND 與 OR 運算
您可以任意巢狀化 AND 和 OR 作業。
例如:
然而,由於 OR 本身的實作方式,這種形式的���詢會過於複雜,可能會發生例外狀況的失敗情形。如果將這些篩選器正規化,使運算式樹狀結構頂端 (最多) 有單一 OR 運算,且下方只有單一層級的 AND 運算,就能提升安全性。
如要執行這類正規化作業,您必須記住布林值邏輯的規則,以及 != 和 IN 篩選器的實際導入方式:
- 將
!=和IN運算子展開至原始形式,其中!=變成檢查小於或大於該值的屬性,而IN變成檢查等於清單中第一個值、第二個值,依此類推至最後一個值的屬性。 - 內含
OR的AND等於套用至原始AND運算元的數個AND,並以單一OR運算元取代原始OR。OR舉例來說,AND(a, b, OR(c, d))相當於OR(AND(a, b, c), AND(a, b, d))。 - 如果
AND的運算元本身就是AND運算,則可將巢狀AND的運算元併入封閉的AND。例如,AND(a, b, AND(c, d))等同於AND(a, b, c, d)。 - 如果
OR的運算元本身就是OR運算,則可將巢狀OR的運算元併入封閉的OR。例如,OR(a, b, OR(c, d))等同於OR(a, b, c, d)。
如果使用比 Python 更簡單的標記法分階段將這些轉換套用到篩選器範例中,得到的結果如下:
- 對
IN和!=運算子使用規則 1:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', OR(tags < 'perl', tags > 'perl')))) - 對
AND運算最內層的OR巢狀結構套用第 2 個規則:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', OR(AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl')))) - 對另一個
OR運算內含的OR巢狀結構使用第 4 個規則:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl'))) - 對
AND運算內含的其他OR巢狀結構使用第 2 個規則:OR(AND(tags == 'python', tags == 'ruby'), AND(tags == 'python', tags == 'jruby'), AND(tags == 'python', AND(tags == 'php', tags < 'perl')), AND(tags == 'python', AND(tags == 'php', tags > 'perl')))
- 使用第 3 個規則收合其他巢狀結構的
AND運算:OR(AND(tags == 'python', tags == 'ruby'), AND(tags == 'python', tags == 'jruby'), AND(tags == 'python', tags == 'php', tags < 'perl'), AND(tags == 'python', tags == 'php', tags > 'perl'))
注意:對於特定篩選器而言,這項正規化作業可能會引發組合爆炸。請考慮 3 個 OR 子句,每個子句有 2 個基本子句。ANDOR經過正規化後,這會變成 8 個 OR 子句,每個子句有 3 個基本子句,也就是 6 個字詞會變成 24 個。AND
指定排序順序
您可以使用 order() 方法,指定查詢傳回結果的順序。這個方法會使用各種引數,這些引數可能是屬性物件 (依遞增順序排序) 或其否定形式 (表示遞減順序)。例如:
這會擷取所有 Greeting 實體,並依 content 屬性的遞增值排序。如果連續實體具有相同的內容屬性,系統會依 date 屬性的遞減值排序。您可以多次呼叫 order(),達到相同效果:
注意:將篩選器與 order() 搭配使用時,Datastore 會拒絕特定組合。特別是使用不等式篩選器時,第一個排序順序 (如有) 必須指定與篩選器相同的屬性。此外,有時您需要設定次要索引。
祖系查詢
您可以透過祖系查詢對資料儲存庫進行同步一致的查詢,不過相同祖系的實體每秒只能寫入一次。以資料儲存庫中的客戶資料與其相關聯的購買資料為例,以下簡單比較祖系查詢與非祖系查詢兩者之間的取捨與結構。
在下列非祖系範例中,每個 Customer 在 Datastore 中都有一個實體,每個 Purchase 在 Datastore 中也有一個實體,且 KeyProperty 指向客戶。
如要找出屬於該客戶的所有購買資料,您可以使用以下查詢:
在此情況下,資料存放區可提供高寫入總處理量,但僅提供最終一致性。如果新增了交易,您可能會取得過時的資料。使用祖系查詢可避���這種行為發生。
如果是進行客戶與購買資料的祖系查詢,則仍會使用相同的結構,但會有兩個不同的實體:客戶部分相同,不過,建立購買交易時,您不再需要指定購買交易的 KeyProperty()。這是因為使用祖系查詢時,您會呼叫建立購買實體時的客戶實體金鑰。
每一筆購買資料都有一個金鑰,而客戶同樣也有專屬的金鑰,但購買資料的金鑰都會內嵌 customer_entity 的金鑰。請注意,每個祖先每秒只能寫入一次。以下範例會建立具有祖先的實體:
如要查詢特定客戶的購買資料,請使用以下查詢。
查詢屬性
查詢物件具有下列唯讀資料屬性:
| 屬性 | 類型 | 預設值 | 說明 |
|---|---|---|---|
| kind | str | None | 種類名稱 (通常為類別名稱) |
| ancestor | Key | None | 查詢的指定祖系 |
| filters | FilterNode | None | 篩選器運算式 |
| orders | Order | None | 排序順序 |
列印查詢物件 (或對其呼叫 str() 或 repr()) 會產生格式良好的字串表示法:
篩選結構化屬性值
查詢可以直接篩選結構化屬性的欄位值。
舉例來說,如要查詢所有有地址且城市為「'Amsterdam'」的聯絡人,查詢條件會如下所示:
如果結合多個此類篩選器來進行查詢,篩選器可能會找出相同 Contact 實體的「不同」Address 子實體。例如:
可能會找到城市為「'Amsterdam'」的聯絡人地址,以及街道為「'Spear St'」的另一個 (不同) 地址。不過至少就等式篩選器而言,您可以建立查詢來傳回單一子實體符合多個值的結果:
如果使用這項技術,系統會在查詢中忽略屬性值等於 None 的子實體。如果屬性具有預設值,您必須明確將其設為 None 以在查詢中忽略這些屬性,否則查詢會包含要求該屬性值等於預設值的篩選條件。舉例來說,如果 Address 模型有個 country 屬性為 default='us',上述範例僅會傳回國家/地區為 'us' 的聯絡人;如要考量其他國家/地區值的聯絡人,您需要使用 Address(city='San Francisco', street='Spear St',
country=None) 的篩選條件。
屬性值等於 None 的任何子實體會遭到忽略。因此,篩選 None 的子實體屬性值沒有意義。
使用字串命名的屬性
有時候,您可能會想依據字串指定的屬性名稱來篩選或排序查詢。舉例來說,如果您讓使用者輸入類似 tags:python 的搜尋查詢,改為類似下列查詢可能會比較方便:
Article.query(Article."tags" == "python") # does NOT work
如果模型是 Expando,則篩選器可以使用 GenericProperty,類別 Expando 會將其用於動態屬性:
如果模型不是 Expando,使用 GenericProperty 也沒問題,但如要確保只使用定義的屬性名稱,也可以使用 _properties 類別屬性:
或使用 getattr() ��類別取得:
兩者的差異在於 getattr() 使用屬性的「Python 名稱」,而 _properties 則以屬性的「Datastore 名稱」建立索引。這只有在宣告的屬性類似以下時才會有所不同:
這裡的 Python 名稱為 title,但 Datastore 名稱為 t。
這些方法也適用排序查詢結果:
查詢疊代器
查詢過程的狀態資訊會保留在疊代器物件中 (大多數應用程式不會直接使用這些函式;一般來說,呼叫 fetch(20) 比操控疊代器物件更簡單)。取得這類物件的基本方法有兩種:
- 在
Query物件上使用 Python 的內建iter()函式 - 呼叫
Query物件的iter()方法
第一個支援使用 Python for 迴圈 (會隱含呼叫 iter() 函式),對查詢進行迴圈。
第二種方式是使用 Query 物件的 iter() 方法,將選項傳遞至疊代器,藉此影響疊代器的行為。舉例來說,如要在 for 迴圈中使用僅限鍵的查詢,可以編寫下列程式碼:
查詢疊代器還提供其他實用方法:
| 方法 | 說明 |
|---|---|
__iter__()
| Python 疊代器通訊協定的一部分。 |
next()
| 傳回下一個結果,或在沒有結果時引發 StopIteration 例外狀況。 |
has_next()
| 如果後續的 next() 呼叫會傳回結果,則傳回 True;如果會引發 StopIteration,則傳回 False。Blocks until the answer to this question is known and buffers the result (if any) until you retrieve it with next().
|
probably_has_next()
| 與 has_next() 類似,但使用速度較快 (有時不準確) 的捷徑。可能會傳回偽陽性 ( True,但 next() 實際上會引發 StopIteration),
但絕不會傳回偽陰性 (False,但 next() 實際上會傳回結果)。
|
cursor_before()
| 傳回查詢游標,代表傳回的是最後一個結果之前的小點。 如果沒有游標可用 (特別是未傳遞 produce_cursors 查詢選項時),就會引發例外狀況。
|
cursor_after()
| 傳回查詢游標,代表的是傳回最後一個結果之後的小點。 如果沒有游標可用 (特別是未傳遞 produce_cursors 查詢選項時),就會引發例外狀況。
|
index_list()
| 傳回已執行查詢所使用的索引清單,包括主要、複合、種類和單一屬性索引。 |
查詢游標
「查詢游標」是小型的不透明資料結構,代表查詢的繼續執行點。這項功能可一次向使用者顯示一頁結果,也能處理可能需要停止及繼續的長時間工作。一般來說,您會搭配查詢的 fetch_page() 方法使用這些函式。這項功能與 fetch() 有點類似,但會傳回三元組 (results, cursor, more)。傳回的 more 旗標表示可能還有更多結果;使用者介面可使用此旗標,例如隱藏「下一頁」按鈕或連結。如要要求後續頁面,請將一次 fetch_page() 呼叫傳回的游標傳送至下一個呼叫。如果傳遞無效的游標,系統會引發 BadArgumentError。請注意,驗證作業僅會檢查其值是否採用 base64 編碼。您必須進行任何進一步的必要驗證。
因此,為了讓使用者查看符合查詢的所有實體,將這些實體擷取在同一頁,使用的程式碼可能類似:
...
請留意,此處使用 urlsafe() 和 Cursor(urlsafe=s) 進行游標的序列化與取消序列化作業。透過此方式,您可以在回應要求時將游標傳送給網路用戶端,然後在後續要求中從該用戶端接收到游標。
注意:即使已經沒有其他結果,fetch_page() 方法通常還是會傳回游標,但這並非絕對:傳回的游標值可能為 None。另請注意,由於 more 標記是使用迭代器的 probably_has_next() 方法實作,因此即使下一頁空白,在極少數情況下仍可能會傳回 True。
部分 NDB 查詢不支援查詢游標,但您可以修正這些查詢。
如果查詢使用 IN、OR 或 !=,則「除非」依照鍵排序,否則查詢結果無法使用游標。如果應用程式未依鍵排序結果並呼叫 fetch_page(),則會收到 BadArgumentError。如果
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name).fetch_page(N)
發生錯誤,請將其變更為
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name, User.key).fetch_page(N)
相對於「分頁」瀏覽查詢結果,您可以使用查詢的 iter() 方法取得確切位置的游標。
如要執行這項操作,請將 produce_cursors=True 傳遞至 iter();當迭代器位於正確位置時,請呼叫其 cursor_after(),取得緊接在該位置之後的游標。(或者,同樣地,呼叫 cursor_before() 取得游標前方的內容)。請注意,呼叫 cursor_after() 或 cursor_before() 可能會發出封鎖 Datastore 呼叫,重新執行部分查詢,以便擷取指向批次中間的游標。
如要使用游標在查詢結果中往前翻頁,請建立反向查詢:
針對各個實體呼叫函式 (「對應」)
假設您需要取得對應至查詢傳回實體的 Account 實體。Message您可以撰寫類似下列的內容:
不過,這種作業方式相當沒有效率:您必須先等待擷取實體,然後使用該實體,接著再等待下個實體,然後再使用該實體。大多時間都在等待。另一種方式是編寫回呼函式,對應至查詢結果:
這個版本的執行速度會比上述簡單的 for
迴圈快一些,因為可以進行部分並行作業。
不過,由於 callback() 中的 get() 呼叫仍為同步,因此增益並不大。適合用來非同步擷取。
GQL
GQL 是類似 SQL 的語言,可從 App Engine Datastore 中擷取實體或金鑰。雖然 GQL 的功能與傳統關聯式資料庫的查詢語言不同,但 GQL 的語法與 SQL 類似。������ GQL 語法的相關說明,請參閱 GQL 參考資料。
您可以使用 GQL 建構查詢。這類似於使用 Model.query() 建立查詢,但這會使用 GQL 語法來定義查詢篩選器與排序。使用說明如下:
ndb.gql(querystring)傳回Query物件 (與Model.query()傳回的類型相同)。 所有常見的方法在這些Query物件上都可使用 (包括fetch()、map_async()和filter()等)。Model.gql(querystring)是ndb.gql("SELECT * FROM Model " + querystring)的簡寫。 一般而言,querystring 會是類似"WHERE prop1 > 0 AND prop2 = TRUE"的字串。- 如要查詢包含結構化屬性的模型,可以在 GQL 語法中使用
foo.bar參照子屬性。 - GQL 支援類似 SQL 的參數繫結。應用程式可定義查詢並且將值繫結到查詢中:
或是
呼叫查詢的
bind()函式會傳回新的查詢,不會變更原始查詢。 - 如果模型類別會覆寫
_get_kind()類別方法,GQL 查詢應使用該函式傳回的種類,而非類別名稱。 - 如果模型中的屬性會覆寫名稱 (例如:
foo = StringProperty('bar')),GQL 查詢應使用覆寫的屬性名稱 (在本例中為bar)。
如果查詢有部分值為使用者提供的變數,則請務必使用參數繫結功能。以免遭到語法駭客攻擊。
查詢尚未匯入 (或更廣義來說,尚未定義) 的模型會發生錯誤。
除非模型是 Expando,否則使用模型類別未定義的屬性名稱會導致錯誤。
為查詢的 fetch() 指定限制或位移,會覆寫 GQL 的 OFFSET 和 LIMIT 子句所設定的限制或位移。請勿合併使用 GQL 的 OFFSET和 LIMIT,以及 fetch_page()。請注意,App Engine 對查詢設下的 1,000 項結果上限,適用於位移和限制。
如果您習慣使用 SQL,請注意,使用 GQL 時可能會出現錯誤的假設。GQL 會翻譯為 NDB 的原生查詢 API。 這與一般的物件關聯對應工具 (例如 SQLAlchemy 或 Django 的資料庫支援) 不同,因為 API 呼叫會先轉換為 SQL,再傳輸至資料庫伺服器。GQL 不支援對 Datastore 進行修改 (插入、刪除或更新);GQL 僅支援查詢。