Voyage系列2: API介紹
在本章中,我們將瀏覽 Voyage 的 API。
3.1 創建一個存儲庫
在 Voyage 中,所有持久化對象都存儲在一個倉庫(repository)中。所使用的倉庫類型決定了對象的存儲后端。
要使用 Voyage 的內存層,需要創建 VOMemoryRepository 的一個實例,如下所示:
repository := VOMemoryRepository new
在本文中,我們將使用 MongoDB 后端。要啟動一個新的 MongoDB 倉庫或連接到一個已有的倉庫,創建 VOMongoRepository 的一個實例,并以主機名和數據庫名為參數。例如,要連接到主機 mongo.db.url 上的數據庫 databaseName,請執行以下代碼:
repository := VOMongoRepository
host: 'mongo.db.url'
database: 'databaseName'.
或者,使用 host:port:database: 消息可以指定要連接的端口。最后,如果需要認證,可以使用 host:database:username:password: 或 host:port:database:username:password: 消息來完成。
3.2 單例模式和實例模式
Voyage 可以工作在兩種不同的模式下:
-
單例模式:在鏡像中有一個唯一的倉庫,它作為一個單例保存所有數據。當您使用這種模式時,可以采用“行為完整”的編程方法,其中實例響應特定的詞匯表(關于詞匯表和用法的更多詳情見下文)。
-
實例模式:您可以在鏡像中擁有不確定數量的倉庫。當然,這種模式要求您明確指定要使用哪些倉庫。
默認情況下,Voyage 工作在實例模式:返回的實例必須作為參數傳遞給所有數據庫 API 操作。為了避免總是保留這個實例,一個方便的替代方案是使用單例模式。單例模式消除了在所有數據庫操作中傳遞倉庫作為參數的需求。要使用單例模式,執行以下代碼:
VOMongoRepository enableSingleton.
Note 只有一個倉庫可以作為單例,因此執行這一行代碼將移除任何其他已存在的單例模式下的倉庫!在本文檔中,我們覆蓋了單例模式下的 Voyage,但使用實例模式也是非常直接的。更多信息請參見
VORepository的persistence協議。
3.3 Voyage API
以下兩張表展示了 Voyage API 的一個代表性子集。這些方法定義在 Object 和 Class 上,但只有當消息的接收者(或其實例)是 Voyage 根對象時,它們才會真正執行工作。有關完整的 Voyage API,請參見這兩個類上的 voyage-model-core-extensions 持久化協議。
首先,我們展示單例模式:
save |
將一個對象存儲進倉庫中(插入或更新) |
remove |
將一個對象從倉庫中刪除 |
removeAll |
將一個類的所有對象從倉庫中刪除 |
selectAll |
檢索某一類型的所有對象 |
selectOne: |
檢索與參數匹配的第一個對象 |
selectMany: |
檢索與參數匹配的所有對象 |
接下來是實例模式,在實例模式中,消息的接收者總是執行操作的倉庫。
save: |
將一個對象存儲進倉庫中(插入或更新) |
remove: |
從倉庫中刪除一個對象 |
removeAll: |
從倉庫中刪除一個類的所有對象 |
selectAll: |
檢索某一類型的所有對象 |
selectOne:where: |
檢索與where子句匹配的第一個對象 |
selectMany:where: |
檢索與where子句匹配的所有對象 |
3.4 重置或斷開數據庫連接
在已部署的應用中,不應該需要關閉或重置與數據庫的連接。此外,當鏡像關閉并稍后重新打開時,Voyage 會重新建立連接。
然而,在開發過程中,可能需要重置數據庫連接以反映更改。這在更改數據庫的存儲選項時尤為重要(參見第3.12節)。執行重置的操作如下:
VORepository current reset.
如果需要斷開與數據庫的連接,操作如下:
VORepository setRepository: nil.
3.5 測試和單例
當我們想要測試動作是否真的在 Voyage 倉庫中保存或移除一個對象時,我們應該確保運行測試不會影響到可能正在使用的數據庫。這非常重要,因為我們面對的是單例(Singleton),它充當全局變量。我們應該確保測試是在專門為測試設置的倉庫中進行的,并且它們不會影響其他倉庫。
這里是一個典型的解決方案:在設置過程中,我們存儲當前的倉庫,設置一個新的倉庫,并且這個新的臨時倉庫將用于測試。
Testcase subclass: #SuperHeroTest
instanceVariableNames: 'oldRepository'
classVariableNames: ''
package: 'MyVoyageTests'
SuperHeroTest >> setUp
oldRepository := VORepository current.
VORepository setRepository: VOMemoryRepository new.
在清理階段,我們恢復保存的倉庫,并丟棄新創建的臨時倉庫。
SuperHeroTest >> tearDown
VORepository setRepository: oldRepository
3.6 存儲對象
要存儲對象,對象的類需要被聲明為倉庫的根。所有倉庫根都是進入數據庫的入口點。Voyage 存儲的不僅僅是包含字面量的對象。完整的對象樹也可以使用 Voyage 進行存儲,并且這是透明完成的。換句話說,存儲對象樹時不需要特別處理。然而,當存儲對象圖時,必須注意打破循環。在本節中,我們討論這樣的基本對象存儲;而在第 3.12 節“增強存儲”中,我們將展示如何增強和/或修改對象的持久化方式。
3.7 基本存儲
假設我們想要存儲一個關聯(即一對對象)。為此,我們需要聲明 Association 類可以作為我們倉庫的根進行存儲。為了表達這一點,我們定義類方法 isVoyageRoot 以返回 true。
Association class >> isVoyageRoot
^ ture
我們還可以通過 voyageCollectionName 類方法定義用于存儲文檔的集合名稱。默認情況下,Voyage 為每個根類創建一個 MongoDB 集合,集合名稱為類的名稱。
接下來,要存儲一個 association 對象,我們只需要給它發送save消息:
anAssociation := #answer->42.
anAssociation save.
這將在數據庫中生成一個包含以下結構的文檔的集合:
{
"_id" : ObjectId("a05feb630000000000000000"),
"#instanceOf" : "Association",
"#version" : NumberLong("3515916499"),
"key" : 'answer',
"value" : 42
}
存儲的數據保留了一些額外信息,以便在加載時能夠正確地重建對象:
-
instanceOf記錄了存儲實例的類。此信息非常重要,因為集合可以包含 Voyage 根類的子類實例。 -
version保留了提交的對象版本的標記。此屬性由 Voyage 內部用于在應用程序中刷新緩存數據。沒有版本字段,應用程序將不得不通過頻繁查詢數據庫來刷新對象。
請注意,Voyage 生成的文檔不會直接通過 Voyage 自身可見,因為 Voyage 的目標是抽象出文檔結構。要查看實際的文檔,您需要直接訪問數據庫。對于 MongoDB,這可以通過 Mongo Browser 實現,它是作為 Voyage 的一部分加載的(World -> Tools -> Mongo Browser)。其他用于 MongoDB 的選項包括使用 mongo 命令行界面或圖形界面工具,如 RoboMongo(跨平臺)或 MongoHub(適用于 Mac)。
3.8 嵌入對象
對象可以非常簡單,只是字面量的關聯,也可以更復雜:對象可以包含其他對象,從而形成對象的樹。保存這樣的對象就像向他們發送save消息一樣簡單。例如,假設我們想存儲矩形,并且每個矩形包含兩個點。為了實現這一點,我們指定Rectangle類是一個文檔根,如下所示:
Rectangle class >> isVoyageRoot
^ true
這允許矩形被保存到數據庫中,例如,如下代碼片段所示:
aRectangle := 42@1 corner: 10@20
aRectangle save.
這將在數據庫的矩形集合中添加一個具有以下結構的文件:
{
"_id" : ObjectId("ef72b5810000000000000000"),
"#instanceOf" : "Rectangle",
"#version" : NumberLong("2460645040"),
"origin" : {
"#instanceOf" : "Point",
"x" : 42,
"y" : 1
},
"corner" : {
"#instanceOf" : "Point",
"x" : 10,
"y" : 20
}
}
3.9 引用其它的根類
有時對象是包含其他根對象的樹。例如,您可能希望將用戶和角色作為根對象保存在不同的集合中,并且一個用戶有一個角色的集合。如果嵌入的對象(角色)是根對象,Voyage 會存儲對這些對象的引用而不是將它們包含在文檔中。
回到我們的矩形示例,假設我們希望將點保存在單獨的集合中。換句話說,現在點將被引用而不是嵌入。
在我們為 Point 類添加 isVoyageRoot,并保存矩形之后,在矩形集合中,我們得到如下文檔:
{
"_id" : ObjectId("7c5e772b0000000000000000"),
"#instanceOf" : "Rectangle",
"#version" : 423858205,
"origin" : {
"#collection" : "point",
"#instanceOf" : "Point",
"_id" : ObjectId("7804c56c0000000000000000")
},
"corner" : {
"#collection" : "point",
"#instanceOf" : "Point",
"_id" : ObjectId("2a731f310000000000000000")
}
}
此外,在點集合中,我們還會得到以下兩個實體:
{
"_id" : ObjectId("7804c56c0000000000000000"),
"#version" : NumberLong("4212049275"),
"#instanceOf" : "Point",
"x" : 42,
"y" : 1
}
{
"_id" : ObjectId("2a731f310000000000000000"),
"#version" : 821387165,
"#instanceOf" : "Point",
"x" : 10,
"y" : 20
}
3.10 在圖中打破循環
當要存儲的對象包含嵌入對象的圖而不是樹時,即當嵌入對象之間的引用存在循環時,必須打破這些循環。如果不這樣做,存儲對象將導致無限循環。最直接有效的解決方案是將引起循環的一個對象聲明為 Voyage 根對象。這在存儲時有效地打破了循環,避免了無限循環。
例如,在矩形示例中,假設我們在矩形內部有一個標簽,并且該標簽包含一段文本。這段文本也保留了對其所包含的標簽的引用。換句話說,標簽和文本之間存在循環引用。為了持久化矩形,必須打破這種循環。為此,要么將標簽聲明為 Voyage 根對象,要么將文本聲明為 Voyage 根對象。
打破循環的另一種解決方案是避免聲明新的 Voyage 根對象,可以將對象的一些字段聲明為瞬態(transient),并定義如何在加載時重建圖。這個話題將在后面討論。
3.11 在 Mongo 中存儲日期的實例
Mongo 的一個已知問題是它不區分 Date 和 DateAndTime,因此即使你存儲的是 Date 實例,你也會檢索到 DateAndTime 實例。在物化對象時,你需要手動將其轉換回 Date。
3.12 增強存儲
通過向類添加Magritte描述,可以改變對象的存儲方式。本節首先探討對象存儲格式的配置選項,隨后深入講解更進階的概念——例如屬性的加載與保存,這類技術可用于解決嵌入式對象中的循環引用問題。
配置存儲
繼續使用矩形示例,但使用嵌入的點,我們添加以下存儲要求:
-
我們需要使用一個名為
rectanglesForTest的不同集合,而不是rectangle。 -
我們只在該集合中存儲
Rectangle類的實例,因此instanceOf信息是冗余的。 -
origin和corner屬性也只會是 Point,因此他倆的instanceOf信息也是冗余的。
要實現這一點,我們使用帶有特定指令的 Magritte 描述來聲明類的屬性,并描述 origin 和 corner 屬性。
mongoContainer方法的定義包含兩個要點:首先通過pragma指令<mongoContainer>聲明該類所使用的數據容器;其次返回一個經過特定配置的VOMongoContainer實例——該實例被設置為使用數據庫中的rectanglesForTest集合,且僅存儲Rectangle類型的對象實例。
需要注意的是,這兩行配置并非必須同時指定。開發人員可以僅聲明使用rectanglesForTest集合,或只限定該集合專用于存儲Rectangle實例,這兩種單獨配置方式同樣有效。
Rectangle class>>mongoContainer
<mongoContainer>
^ VOMongoContainer new
collectionName: 'rectanglesForTest';
kind: Rectangle;
yourself
另外兩個方法通過pragma指令<mongoDescription>進行聲明,并返回經過配置的Mongo描述對象。這些描述對象分別設置了對應的屬性名稱和類型,具體實現如下:
Rectangle class>>mongoOrigin
<mongoDescription>
^ VOMongoToOneDescription new
attributeName: 'origin';
kind: Point;
yourself
Rectangle class>>mongoCorner
<mongoDescription>
^ VOMongoToOneDescription new
attributeName: 'corner';
kind: Point;
yourself
在重置資源庫時,需執行以下操作:
VORepository current reset
此時,保存在rectanglesForTest集合中的矩形對象,其存儲結構將大致呈現如下形式:
{
"_id" : ObjectId("ef72b5810000000000000000"),
"#version" : NumberLong("2460645040"),
"origin" : {
"x" : 42,
"y" : 1
},
"corner" : {
"x" : 10,
"y" : 20
}
}
屬性描述的其他配置選項包括:
-
beEager用于聲明被引用的實例需要立即加載(默認設置為延遲加載)。 -
beLazy聲明被引用的實例將采用延遲加載。 -
convertNullTo:在檢索到值為Null(nil)的對象時,將返回傳入代碼塊的執行結果作為替代。
對于集合類型的屬性,需要返回VOMongoToManyDescription而非VOMongoToOneDescription。上述所有配置選項在此情況下依然適用,其中kind:配置項用于指定該集合所包含值的類型。
VOMongoToManyDescription 提供了若干額外配置選項:
-
kindCollection:用于指定屬性中所包含集合的類別。 -
convertNullToEmpty在檢索到值為 Null(nil)的集合時,會返回一個空集合。
3.13 屬性的自定義加載與保存
可以為對象屬性的數據庫讀寫操作編寫特定的轉換邏輯。這種機制可用于解決對象圖中的循環引用問題,而無需聲明額外的Voyage根對象。要聲明此類自定義邏輯,需要定義一個包含Smalltalk代碼塊的MAPluggableAccessor,分別用于從對象讀取屬性值以及向對象寫入屬性值。需要注意的是,這些訪問器的命名可能違反直覺:read:訪問器定義的是將存儲到數據庫中的值,而write:訪問器定義的則是將數據庫檢索值轉換為對象屬性值的邏輯。這是因為對象-文檔映射器在讀取對象存儲到數據庫時使用read:訪問器,在基于數據庫取值將數據寫入內存對象時使用write:訪問器。
定義自定義訪問器可實現如下場景:例如將Amount對象中包含的Currency對象,以其三字母縮寫形式(如EUR、USD、CLP等)存入數據庫。當加載該數據表示時,需將其轉換回Currency對象(例如通過實例化新的Currency對象)。具體實現方式如下:
Amount class>>mongoCurrency
<mongoDescription>
^ VOMongoToOneDescription new
attributeName: 'currency';
accessor: (MAPluggableAccessor
read: [ :amount | amount currency abbreviation ]
write: [ :amount :value | amount currency: (Currency
fromAbbreviation: value) ]);
yourself
此外,可通過在屬性描述符或容器描述符中添加postLoad:動作,為某個屬性或包含它的對象定義加載后操作。該操作是一個單參數代碼塊,會在對象加載到內存后執行,并以被加載的對象作為參數。
最后,通過返回VOMongoTransientDescription實例作為屬性描述符,可以將屬性排除在存儲(及檢索)范圍之外。這為待保存的對象圖提供了截斷點,例如當對象包含不應持久化至數據庫的引用數據時。該機制也可用于破除存儲對象圖中的循環引用。但需要注意的是,從數據庫檢索對象圖時,包含這些對象的屬性將被設置為nil。為解決此問題,可在屬性描述符或容器描述符中指定加載后操作,將這些屬性設置為正確的值。
以下示例聲明了'currencyMetaData'屬性將被排除在存儲范圍之外。
Amount class>>mongoCurrencyMetaData
<mongoDescription>
^VOTransientDescription new
attributeName: 'currencyMetaData';
yourself
3.14 關于OID的幾點說明
MongoDB的對象標識符(OID)是一個充當主鍵的唯一字段。這種12字節的BSON類型數據由以下部分構成:
-
一個4字節數值,表示自Unix紀元以來經過的秒數,
-
一個3字節的機器標識符,
-
一個2字節的進程ID,
-
以及一個從隨機值開始的3字節計數器。
被添加到Mongo根集合的對象會獲得一個唯一標識符,即OID實例。當您創建此類對象后,通過向其發送voyageId消息獲取OID時,將返回該標識符。OID實例變量的值是一個與Mongo的ObjectId對應的大型正整數。
可以創建并使用自定義的OID實現,并將這些對象存入Mongo數據庫。但不建議這樣做,因為您可能無法再通過OID(使用voyageId)查詢這些對象——這是由于Mongo要求特定的格式。若執意使用自定義格式,應通過Mongo控制臺驗證其格式,例如使用如下查詢。若返回錯誤提示“Error: invalid object id: length”,則表示無法通過ID查詢該對象。
> db.Trips.find({"person._id" : ObjectId("190372")})
Fri Aug 28 14:21:10.815 Error: invalid object id: length
MongoDB格式的OID還有一個額外優勢:它們按創建日期和時間排序,因此您無需額外操作即可獲得一個帶索引的"creationDateAndTime"屬性(因為OID的_id字段上存在不可刪除的索引)。
3.15 Voyage中的查詢操作
Voyage支持通過數據庫查詢選擇性檢索對象實例。使用內存層時,查詢采用標準的Smalltalk代碼塊;使用MongoDB后端時,則通過MongoDB查詢語言執行搜索。為指定這些查詢,MongoDB采用JSON結構,而在Voyage中可通過兩種方式構建查詢:根據復雜程度的不同,MongoDB查詢既可編寫為代碼塊形式,也可編寫為字典形式。本節將首先探討這兩種查詢創建方式,最后闡述如何執行這些查詢。
3.16 使用代碼塊或mongoQuery進行基礎對象檢索
查詢數據庫最直接的方式是:使用內存層時采用代碼塊,使用MongoDB后端時采用MongoQuery。由于代碼塊屬于標準Smalltalk用法,本次討論我們將重點聚焦于MongoQuery的使用。
MongoQuery并非Voyage本身的組成部分,而是Voyage用于與MongoDB交互的MongoTalk層的一部分。MongoTalk由Nicolas Petton開發,提供了訪問MongoDB的所有底層操作。在特定限制下,MongoQuery能夠將常規Pharo代碼塊轉換為符合數據庫預期格式的JSON查詢。本質上,MongoQuery是一種用于創建MongoDB查詢的嵌入式領域特定語言。使用MongoQuery時,查詢語句看起來像是普通的Pharo表達式(但該語言的功能比原生Smalltalk更為受限)。
使用MongoQuery時,查詢中可使用以下運算符:
< <= > >= = ~= |
常規比較運算符 |
& |
AND運算符 |
| |
OR運算符 |
not |
NOT運算符 |
at: |
訪問嵌入的文檔 |
where: |
執行Javascript查詢 |
例如,以下查詢將選擇數據庫中所有名稱為John的記錄:
[ :each | each nam = 'John' ]
稍復雜的查詢示例:查找數據庫中所有名稱是John且訂單數量大于10的記錄。
[ :each | (each name = 'John') & (each orders > 10) ]
需要注意的是,這種查詢方式僅適用于對象屬性值的查詢,不適用于對其他對象引用的查詢。對于引用查詢,應沿用關系型數據庫的傳統方式通過ID構建查詢(我們接下來會討論)。但遵循MongoDB設計理念的最佳解決方案是:重新審視對象模型,避免使用外鍵形式的關系表達。
3.17 查詢包含其他根文檔元素的查詢
在No-SQL數據庫中,無法跨集合查詢(即實現SQL中的JOIN操作)。您有兩種選擇:按前文建議修改數據模型,或編寫應用層代碼模擬JOIN行為。后一種方案可通過以下方式實現:對已通過先前查詢返回的對象發送voyageId消息,然后使用該ID匹配另一個對象。以下示例演示了如何將顏色color與參考顏色refCol進行匹配:
[ :each | (each at: 'color._id') = refCol voyageId ]
3.18 使用at:消息訪問嵌入式文檔
由于MongoDB能夠存儲任意復雜度的文檔,常見的情況是一個文檔由多個嵌入式文檔組成,例如:
{
"origin" : {
"x" : 42,
"y" : 1
},
"corner" : {
"x" : 10,
"y" : 20
}
}
在此情況下,若要通過嵌入式文檔元素搜索對象,需使用at:消息及字段分隔符“.”。例如,要選擇所有原點x值等于42的矩形,查詢語句如下所示。
[ :each | (each at: 'origin.x') = 42 ]
3.19 使用where:消息執行Javascript比較
對于超出MongoQuery甚至MongoDB查詢語言能力的查詢需求,MongoDB提供了通過$where操作符直接編寫Javascript查詢的方式。在MongoQuery中也可通過發送where:消息實現:
以下示例我們使用Javascript表達式重寫之前的查詢:
[ :each | each where: 'this.origin.x == 42' ]
關于$where操作符更完整的使用文檔請參閱MongoDB的官方文檔。
3.20 使用JSON查詢
當MongoQuery無法滿足查詢表達需求時,可改用JSON查詢。JSON查詢是MongoDB查詢的內部表示形式,在Voyage中可直接創建。簡而言之:JSON結構被映射為鍵值對構成的字典。其中鍵為字符串,值可以是基本類型、集合或另一個JSON結構(即嵌套字典)。創建查詢時,我們只需構建符合這些要求的字典即可。
注意 JSON查詢嚴格限定于使用MongoDB后端的情況。其他后端(例如內存層)不提供對JSON查詢的支持。
例如,首個使用MongoQuery的示例若以字典形式編寫,其代碼如下:
{ 'name' -> 'John' } asDictionary
字典鍵值對采用AND語義進行組合。篩選名稱值為John且訂單數量大于10的記錄可編寫如下:
{
'name' -> 'John'.
'orders' -> { '$gt' : 10 } asDictionary
} asDictionary
要構建"大于"條件語句,需要創建新字典并使用MongoDB的$gt查詢選擇器來表達大于關系。有關可用查詢選擇器的完整列表,請參閱MongoDB查詢選擇器官方文檔。
通過OID查詢對象
若已知文檔的ObjectId,可通過該值創建OID實例并進行查詢。
{('_id' -> (OID value: 16r55CDD2B6E9A87A520F000001))} asDictionary.
請注意,以下兩種寫法是等價的:
OID value: 26555050698940995562836590593. "dec"
OID value: 16r55CDD2B6E9A87A520F000001. "hex"
注意 如果您擁有位于根集合中的對象實例,可以通過獲取其voyageId并在查詢中使用該ObjectId。
使用點標記法訪問嵌入式文檔
在JSON查詢中訪問嵌入式文檔的值時,需使用點標記法。例如,查詢原點x值為42的矩形可表示為:
{
'origin.x' -> {'$eq' : 42} asDictionary
} asDictionary
在查詢中表達OR條件
要表達OR條件,需要創建一個鍵為'$or'、值為條件表達式的字典。以下示例演示如何選擇名稱是John且訂單數超過10的對象,或者名稱不是John且訂單數不超過10的對象:
{ '$or' :
{
{
'name' -> 'John'.
'orders' -> { '$gt': 10 } asDictionary
} asDictionary.
{
'name' -> { '$ne': 'John'} asDictionary.
'orders' -> { '$lte': 10 } asDictionary
} asDictionary.
}.
} asDictionary.
突破MongoQuery的功能限制
使用JSON查詢可以實現MongoQuery不支持的功能,例如使用正則表達式。以下查詢用于搜索所有fullname.lastName以字母D開頭的文檔:
{
'fullname.lastName' -> {
'$regexp': '^D.*'.
'$options': 'i'.
} asDictionary.
} asDictionary.
正則表達式的選項i表示不區分大小寫。更多選項請參閱$regex操作符的文檔說明。
此示例僅簡要展示了JSON查詢的強大功能。實際上可以構建更多不同類型的查詢,完整運算符及用法請參閱MongoDB操作符文檔。
3.21 執行查詢
Voyage提供了一組執行搜索的方法。為演示這些方法的使用,我們將采用先前介紹的已存儲Point示例。請注意,除非特別說明,本節所有查詢均可使用MongoQuery或JSON查詢兩種方式編寫。
3.22 基礎對象檢索
以下方法提供基礎的對象檢索功能。
-
selectAll獲取對應數據庫集合中的所有文檔。例如,Point selectAll將返回所有Point對象。 -
selectOne獲取與查詢條件匹配的單個文檔。該方法對應detect:操作,接收查詢規范作為參數(可以是MongoQuery或JSON查詢)。例如:Point selectOne: [:each | each x = 42]或等價的Point selectOne: { 'x' -> 42 } asDictionary。 -
selectMany獲取所有與查詢條件匹配的文檔。該方法對應select:操作,與上述方法類似,接收查詢規范作為參數。
3.23 限制對象檢索與排序
查詢數據庫的方法看似與Collection層次結構中的等效方法相似。然而,常規集合能完全在內存中操作,而Voyage集合查詢通常需要定制化以優化內存消耗和/或訪問速度。這是因為每個集合可能包含數百萬個文檔,超出Pharo的內存限制,同時數據庫搜索的性能也遠高于Pharo中的等效代碼。
查詢的第一項優化在于限制返回的結果數量。系統會從所有匹配的文檔集合中,返回以指定參數為起始索引的子集。這可用于僅獲取查詢結果的前N個匹配項,或以更小的數據塊遍歷查詢結果,如下文簡單分頁示例所示。
-
selectMany:limit:從數據庫中獲取與查詢條件匹配的對象集合,最多返回指定數量的結果。例如:Point selectMany: [:each | each x = 42] limit: 10 -
selectMany:limit:offset:從數據庫中獲取與查詢條件匹配的對象集合。返回結果從查詢結果的偏移量位置+1處開始,最多返回限定數量的對象。例如,若上述查詢匹配到25個點,則通過Point selectMany: [:each | each x = 42] limit: 20 offset: 10可返回最后15個點(本例中任何大于15的limit參數均可實現此效果)。
可實施的第二項定制是對結果進行排序。為此,VOOrder類提供了用于指定升序或降序排列的常量。
-
selectAllSortBy:獲取所有文檔,并按參數中的規范進行排序(該參數需為JSON查詢)。例如,Point selectAllSortBy: { #x -> VOOrder ascending} asDictionary會按x值升序返回所有點。 -
selectMany:sortBy:獲取所有與查詢條件匹配的文檔并對其進行排序。例如,要返回x值為42且按y值降序排列的點:Point selectMany: { 'x' -> 42 } asDictionary sortBy: { #y -> VOOrder descending } asDictionary。 -
selectMany:sortBy:limit:offset:為上述查詢提供限定返回數量和偏移量的參數設置。
3.24 簡單分頁器示例
通常,您可能只想顯示集合中特定范圍內的對象,例如前25個,或從第25到第50個,依此類推。這里我們展示一個簡單的分頁器,它通過使用selectMany:limit:offset:方法來實現這一功能。
首先我們創建一個名為Paginator的類。要實例化它,需要提供一個Voyage根對象(aClass)和一個查詢條件(aCondition)。
Object subclass: #Paginator
instanceVariableNames: 'collectionClass where pageCount'
classVariableNames: ''
package: 'DemoPaginator'
Paginator class>>on: aClass where: aCondition
^ self basicNew
initializeOn: aClass where: aCondition
Paginator>>initializeOn: aClass where: aCondition
self initialize.
collectionClass := aClass.
where := aCondition
接著我們定義計算邏輯:根據頁面大小和實體總數來獲取總頁數。
Paginator>>pageSize
^ 25
Paginator>>pageCount
^ pageCount ifNil: [ pageCount := self calculatePageCount ]
Paginator>>calculatePageCount
| count pages |
count := self collectionClass count: self where.
pages := count / self pageSize.
count \\ self pageSize > 0
ifTrue: [ pages := pages + 1].
^ count
隨后,用于獲取指定頁面元素的查詢按如下方式實現:
Paginator>>page: aNumber
^ self collectionClass
selectMany: self where
limit: self pageSize
offset: (aNumber - 1) * self pageSize
3.25 創建與刪除索引
MongoDB中有許多實用功能在Voyage中并未提供,但仍可通過Pharo環境進行操作,其中最重要的就是索引管理功能。
3.26 使用OSProcess創建索引
目前尚無法通過Voyage直接創建和刪除索引,但可以通過OSProcess來實現這一功能。
例如,假設存在一個名為myDB的數據庫,其中包含名為Trips的集合。行程數據中包含一個嵌入的收據集合,而收據具有名為description的屬性。以下操作將在description字段上創建索引:
OSProcess command:
'/{pathToMongoDB}/MongoDB/bin/mongo --eval ',
'"db.getSiblingDB(''myDB'').Trips.',
'createIndex({''receipts.description'':1})"'
要刪除Trips集合上的所有索引,可以按以下方式操作:
OSProcess command:
'/{pathToMongoDB}/MongoDB/bin/mongo --eval ',
'"db.getSiblingDB(''myDB'').Trips.dropIndexes()"'
3.27 驗證索引的使用情況
要確認查詢是否確實使用了索引,可以在mongo控制臺中使用".explain()"方法。例如,如果我們按上述方法在description字段上添加索引,然后運行查詢并添加.explain(),就能看到僅掃描了部分文檔。
> db.Trips.find({"receipts.description":"a"})
.explain("executionStats")
{
"cursor" : "BtreeCursor receipts.receiptDescription_1",
"isMultiKey" : true,
"n" : 2,
"nscannedObjects" : 2,
"nscanned" : 2,
"nscannedObjectsAllPlans" : 2,
"nscannedAllPlans" : 2,
[...]
}
移除索引后,系統會掃描所有文檔(此示例中共有246個文檔):
> db.Trips.find({"receipts.description":"a"}
..explain("executionStats")
{
"cursor" : "BasicCursor",
"isMultiKey" : false,
"n" : 2,
"nscannedObjects" : 246,
"nscanned" : 246,
"nscannedObjectsAllPlans" : 246,
"nscannedAllPlans" : 246,
[...]
}
3.28 結論
本章我們介紹了Voyage——一個持久化編程框架。Voyage的優勢在于其對象文檔映射器與MongoDB后端支持。我們演示了如何存儲和刪除數據庫中的對象,以及如何優化存儲格式。隨后深入探討了數據庫查詢操作,展示了兩種構建查詢的方式并詳述了查詢執行機制。最后,盡管Voyage未直接提供索引構建支持,我們仍展示了在MongoDB數據庫中創建索引的方法。
浙公網安備 33010602011771號