應用程式可利用查詢功能,搜尋 Datastore 中符合篩選器特定搜尋條件的實體。
總覽
應用程式可利用查詢功能,搜尋 Datastore 中符合篩選器特定搜尋條件的實體。舉例來說,記錄多個留言板的應用程式可利用查詢功能,擷取其中一個留言版的訊息,並依日期排序:
...
有些查詢比其他查詢更複雜,資料儲存庫需要預先建立的索引。這些預先建立的索引是在設定檔 index.yaml 中指定。在開發伺服器上,如果您執行的查詢需要未指定的索引,開發伺服器會自動將該索引新增至 index.yaml。但在網站中,需要尚未指定索引的查詢會失敗。因此,一般的開發週期是在開發伺服器上嘗試新的查詢,然後更新網站,以使用自動變更的 index.yaml。您可以執行 gcloud app deploy index.yaml,在不影響上傳應用程式的情況下,單獨更新 index.yaml。如果資料儲存庫中有多個實體,為這些實體建立新索引會耗費較多時間。在這種情況下,建議您先更新索引定義,再上傳使用新索引的程式碼。您可以使用管理控制台,查看索引何時建構完成。
App Engine Datastore 原生支援完全相符 (== 運算子) 和比較 (<、<=、> 和 >= 運算子) 的篩選器。這項功能支援使用布林值 AND 運算結合多個篩選器,但有一些限制 (請見下方)。
除了原生運算子外,API 還支援 != 運算子、使用布林值 OR 運算結合篩選器群組,以及 IN 運算 (測試是否等於可能值清單中的其中一個值,類似 Python 的「in」運算子)。這些作業與 Datastore 的原生作業並非一對一對應,因此有點奇怪,而且相對較慢。這些作業是透過記憶體內合併結果串流的方式實作。請注意,p != v 的實作方式為「p < v OR p > v」。(這對重複屬性很重要)。
限制事項:Datastore 會對查詢強制執行一些限制。違反這些規定會導致系統擲回例外狀況。舉例來說,目前不允許合併過多篩選器、對多個屬性使用不等式,或將不等式與不同屬性的排列順序合併。此外,參照多個屬性的篩選器有時需要設定次要索引。
不支援:Datastore 不直接支援子字串比對、不區分大小寫的比對,或所謂的全文搜尋。您可以使用運算屬性,實作不區分大小寫的比對,甚至是全文搜尋。
依屬性值進行篩選
回想一下 NDB 屬性中的 Account 類別:
通常您不會想擷取特定種類的「所有」實體,而只會想擷取某些屬性具有特定值或值範圍的實體。
屬性物件會超載一些運算子,以供傳回可用於控管查詢的篩選器運算式:舉例來說,如要尋找 userid 屬性值剛好為 42 的所有 Account 實體,您可以使用以下運算式:
(如果您確定只有一個 Account 具有該 userid,則建議您使用 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會變成檢查屬性是否 == 第一個值或第二個值,或...一直到清單中的最後一個值。 - 內含
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'))
注意:對於特定篩選器而言,這項正規化作業可能會引發組合爆炸。假設 AND 有 3 個 OR 子句,每個子句有 2 個基本子句。正規化後,這會變成 8 個 AND 子句的 OR,每個子句有 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' 的聯絡人,查詢會像這樣:
如果合併多個這類篩選器,篩選器可能會比對同一個聯絡人實體中的「不同」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 則是以屬性的「資料儲存庫名稱」建立索引。這只有在宣告的屬性類似以下時才會有所不同:
這裡的 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 修改作業 (插入、刪除或更新),僅支援查詢。