應用程式可利用查詢功能,搜尋 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 的所有 Account 實體。
在這些運算中,!= 和 IN 這兩個運算是以結合其他運算的方式進行實作,有點複雜 (如 != 和 IN)。
您可以指定多個篩選器:
這會結合指定的篩選器引數,傳回 userid 值大於或等於 40 且小於 50 的所有 Account 實體。
注意:如先前所述,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
變成檢查等於清單中第一個值、第二個值,依此類推至最後一個值的屬性。 AND
內含OR
的情況,等同於將多個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'))
注意:對於特定篩選器而言,這項正規化作業可能會引發組合爆炸。請考慮 AND
的 3 個 OR
子句,每個子句有 2 個基本子句。經過標準化後,這會變成 OR
的 8 個 AND
子句,每個子句有 3 個基本子句:也就是 6 個項變成 24 個。
指定排序順序
您可以使用 order()
方法,指定查詢傳回結果的順序。這個方法會使用各種引數,這些引數可能是屬性物件 (依遞增順序排序) 或其否定形式 (表示遞減順序)。例如:
這會擷取所有 Greeting
實體,並依 content
屬性的遞增值排序。具有相同內容屬性的連續實體會依據 date
屬性的遞減值排序。您可以使用多個 order()
呼叫來達到相同效果:
注意:將篩選器與 order()
搭配使用時,Datastore 會拒絕特定組合。特別是,使用不等式篩選器時,第一個排序順序 (如有) 必須指定與篩選器相同的屬性。此外,有時您可能需要設定次要索引。
祖系查詢
您可以透過祖系查詢對資料儲存庫進行同步一致的查詢,不過相同祖系的實體每秒只能寫入一次。以資料儲存庫中的客戶資料與其相關聯的購買資料為例,以下簡單比較祖系查詢與非祖系查詢兩者之間的取捨與結構。
在下列非祖系範例中,每個 Customer
在資料儲存庫中都有一個實體,每個 Purchase
在資料儲存庫中也都有一個實體,且 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'
為 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
,但資料儲存庫名稱為 t
。
這些方法也適用排序查詢結果:
查詢疊代器
查詢過程的狀態資訊會保留在疊代器物件中 (大多數應用程式不會直接使用這些物件;通常呼叫 fetch(20)
比操作疊代器物件更為簡單。)取得這類物件的兩種基本方法如下:
- 在
Query
物件上使用 Python 內建的iter()
函式 - 呼叫
Query
物件的iter()
方法
第一個支援使用 Python for
迴圈 (會隱含呼叫 iter()
函式) 迴圈查詢。
第二種方法是使用 Query
物件的 iter()
方法,可將選項傳遞至疊代器,以影響其行為。舉例來說,如要在 for
迴圈中使用僅鍵查詢,您可以編寫以下內容:
查詢疊代器還提供其他實用方法:
方法 | 說明 |
---|---|
__iter__()
| 屬於 Python 的迭代器通訊協定。 |
next()
| 傳回下一個結果,如果沒有則會引發例外狀況 StopIteration 。 |
has_next()
| 如果後續 next() 呼叫會傳回結果,則會傳回 True ;如果會觸發 StopIteration ,則會傳回 False 。會阻斷至此問題的答案已知,並緩衝結果 (如有),直到您使用 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
旗標表示可能還有其他結果;UI 可以使用這項資訊,例如抑制「下一頁」按鈕或連結。如要要求後續頁面,請將一個 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 呼叫,重新執行部分查詢,以便擷取指向批次中間的游標。
如要使用游標在查詢結果中往前翻頁,請建立反向查詢:
針對各個實體呼叫函式 (「對應」)
假設您需要取得與查詢傳回的 Message
實體相對應的 Account
實體。您可以這樣寫:
不過,這種作業方式相當沒有效率:您必須先等待擷取實體,然後使用該實體,接著再等待下個實體,然後再使用該實體。大多數的時間都是在等待。另一種方法是編寫回呼函式,並將其對應至查詢結果:
這個版本的執行速度會比上述簡單的 for
迴圈快上一些,因為它可支援部分並行作業。不過,由於 callback()
中的 get()
呼叫仍為同步,因此效益並沒有大幅提升。這時可使用非同步 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 僅支援查詢。