IBM,DW,NoSQL,數據建模
[2] IBM,DW,NoSQL,數據建模
關系數據庫已經統治數據存儲30 多年了,但是無模式(或NoSQL)數據庫的逐漸流行表明變化正在發生。盡管 RDBMS 為在傳統的客戶端服務器架構中存儲數據提供了一個堅實的基礎,但它不能輕松地(或便宜地)擴展到多個節點。在高度可伸縮的 Web 應用程序(比如 Facebook 和 Twitter)的時代,這是一個非常不幸的弱點。
盡管關系數據庫的早期替代方案(還記得面向對象的數據庫嗎?)不能解決真正緊急的問題,NoSQL 數據庫(比如 Google 的 Bigtable 和 Amazon 的 SimpleDB)卻作為對 Web 的高可伸縮性需求的直接響應而崛起。本質上,NoSQL 可能是一個殺手問題的殺手應用程序—隨著 Web 2.0 的演變,Web 應用程序開發人員可能會遇到更多,而不是更少這樣的應用程序。
在這期 Java 開發 2.0 中,我將向您介紹無模式數據建模,這是經過關系思維模式訓練的許多開發人員使用 NoSQL 的主要障礙。您將了解到,從一個域模型(而不是關系模型)入手是簡化您的改變的關鍵。如果您使用 Bigtable(如我的示例所示),您可以借助 Gaelyk:Google App Engine 的一個輕量級框架擴展。
NoSQL:一種新的思維方式?
當開發人員談論非關系或 NoSQL 數據庫時,經常提到的第一件事是他們需要改變思維方式。我認為,那實際上取決于您的初始數據建模方法。如果您習慣通過首先建模數據庫結構(即首先確定表及其關聯關系)來設計應用程序,那么使用一個無模式數據存儲(比如 Bigtable)來進行數據建模則需要您重新思考您的做事方式。但是,如果您從域模型開始設計您的應用程序,那么 Bigtable 的無模式結構將看起來更自然。
非關系數據存儲沒有聯接表或主鍵,甚至沒有外鍵這個概念(盡管這兩種類型的鍵以一種更松散的形式出現)。因此,如果您嘗試將關系建模作為一個 NoSQL 數據庫中的數據建模的基礎,那么您可能最后以失敗告終。從域模型開始將使事情變得簡單;實際上,我已經發現,域模型下的無模式結構的靈活性正在重新煥發生機。
從關系數據模型遷移到無模式數據模型的相對復雜程度取決于您的方法:即您從基于關系的設計開始還是從基于域的設計開始。當您遷移到 CouchDB 或 Bigtable 這樣的數據庫時,您的確會喪失 Hibernate(至少現在)這樣的成熟的持久存儲平臺的順暢感覺。另一方面,您卻擁有能夠親自構建它的“綠地效果”。在此過程中,您將深入了解無模式數據存儲。
實體和關系
無模式數據存儲賦予您首先使用對象來設計域模型的靈活性(Grails 這樣的較新的框架自動支持這種靈活性)。您的下一步工作是將您的域映射到底層數據存儲,這在使用 Google App Engine 時再簡單不過了。
在文章“Java 開發 2.0:針對 Google App Engine 的 Gaelyk”中,我介紹了 Gaelyk ——一個基于 Groovy的框架,該框架有利于使用 Google 的底層數據存儲。那篇文章的主要部分關注如何利用 Google 的 Entity對象。下面的示例(來自那篇文章)將展示對象實體如何在 Gaelyk 中工作。
清單1.使用 Entity的對象持久存儲
def ticket =newEntity("ticket") ticket.officer = params.officer ticket.license = params.plate ticket.issuseDate = offensedate ticket.location = params.location ticket.notes = params.notes ticket.offense = params.offense
這種對象持久存儲方法很有效,但容易看出,如果您頻繁使用票據實體—例如,如果您正在各種 servlet 中創建(或查找)它們,那么這種方法將變得令人厭煩。使用一個公共 servlet(或 Groovlet)來為您處理這些任務將消除其中一些負擔。一種更自然的選擇——我將稍后展示——將是建模一個 Ticket對象。
返回比賽
我不會重復 Gaelyk 簡介中的那個票據示例,相反,為保持新鮮感,我將在本文中使用一個賽跑主題,并構建一個應用程序來展示即將討論的技術。
如圖 1 中的“多對多”圖表所示,一個 Race擁有多個 Runner,一個 Runner可以屬于多個 Race。
圖1.比賽和參賽者
如果我要使用一個關系表結構來設計這個關系,至少需要 3 個表:第 3 表將是鏈接一個“多對多”關系的聯接表。所幸我不必局限于關系數據模型。相反,我將使用 Gaelyk(和 Groovy代碼)將這個“多對多”關系映射到 Google 針對 Google App Engine 的 Bigtable 抽象。事實上,Gaelyk 允許將 Entity當作 Map,這使得映射過程相當簡單。
無模式數據存儲的好處之一是無須事先知道所有事情,也就是說,與使用關系數據庫架構相比,可以更輕松地適應變化。(注意,我并非暗示不能更改架構;我只是說,可以更輕松地適應變化。)我不打算定義我的域對象上的屬性—我將其推遲到 Groovy的動態特性(實際上,這個特性允許創建針對 Google 的 Entity對象的域對象代理)。相反,我將把我的時間花費在確定如何查找對象并處理關系上。這是 NoSQL 和各種利用無模式數據存儲的框架還沒有內置的功能。
Model 基類
我將首先創建一個基類,用于容納 Entity對象的一個實例。然后,我將允許一些子類擁有一些動態屬性,這些動態屬性將通過 Groovy的方便的 setProperty方法添加到對應的 Entity實例。setProperty針對對象中實際上不存在的任何屬性設置程序調用。(如果這聽起來聳人聽聞,不用擔心,您看到它的實際運行后就會明白。)
清單2展示了位于我的示例應用程序的一個 Model實例的第一個 stab:
清單2.一個簡單的 Model 基類
packagecom.b50.nosqlimportcom.google.appengine.api.datastore. DatastoreServiceFactoryimportcom.google. appengine.api.datastore.EntityabstractclassModel { def entitystaticdef datastore = DatastoreServiceFactory.datastoreServicepublicModel() publicModel(params) { this.@entity=newEntity(this.getClass().simpleName) params.each{ key,val ->this.setProperty key,val } }def getProperty(String name) else" } }voidsetProperty(String name,value)"= value}def save()}
注意抽象類如何定義一個構造函數,該函數接收屬性的一個 Map ——我總是可以稍后添加更多構造函數,稍后我就會這么做。這個設置對于 Web 框架十分方便,這些框架通常采用從表單提交的參數。Gaelyk 和 Grails 將這樣的參數巧妙地封裝到一個稱為 params的對象中。這個構造函數迭代這個 Map并針對每個“鍵/值”對調用 setProperty方法。
檢查一下 setProperty方法就會發現“鍵”設置為底層 entity的屬性名稱,而對應的“值”是該 entity的值。
Groovy技巧
如前所述,Groovy的動態特性允許我通過 get和 set Property方法捕獲對不存在的屬性的方法調用。這樣,清單 2 中的 Model的子類不必定義它們自己的屬性—它們只是將對一個屬性的所有調用委托給這個底層 entity對象。
清單 2 中的代碼執行了一些特定于 Groovy的操作,值得一提。首先,可以通過在一個屬性前面附加一個 @來繞過該屬性的訪問器方法。我必須對構造函數中的 entity對象引用執行上述操作,否則我將調用 setProperty方法。很明顯,在這個關頭調用 setProperty將打破這種模式,因為 setProperty方法中的 entity變量將是 null。
其次,構造函數中的調用 this.getClass().simpleName將設置 entity的“種類”—— simpleName屬性將生成一個不帶包前綴的子類名稱(注意,simpleName的確是對 getSimpleName的調用,但 Groovy允許我不通過對應的 JavaBeans 式的方法調用來嘗試訪問一個屬性)。
最后,如果對 id屬性(即,對象的鍵)進行一個調用,getProperty方法很智能,能夠詢問底層 key以獲取它的 id。在 Google App Engine 中,entities的 key屬性將自動生成。
Race 子類
定義 Race子類很簡單,如清單 3 所示:
清單3.一個 Race 子類
packagecom.b50.nosqlclassRaceextendsModel }
當一個子類使用一列參數(即一個包含多個“鍵/值”對的 Map)實例化時,一個對應的 entity將在內存中創建。要持久存儲它,只需調用 save方法。
清單4.創建一個 Race 實例并將其保存到 GAE 的數據存儲
importcom.b50.nosql.Runnerdef iparams= [:] def formatter =newSimpleDateFormat("MM/dd/yyyy") def rdate = formatter.parse("04/17/2010") iparams["name"] ="Charlottesville Marathon" iparams["date"] = rdate iparams["distance"] =26.2asdouble def race =newRace(iparams)race.save()
清單4 是一個 Groovlet,其中,一個 Map(稱為 iparams)創建為帶有 3 個屬性——一次比賽的名稱、日期和距離。(注意,在 Groovy中,一個空白 Map通過 [:]創建。)Race的一個新實例被創建,然后通過 save方法存儲到底層數據存儲。
可以通過 Google App Engine 控制臺來查看底層數據存儲,確保我的數據的確在那里,如圖 2 所示:
圖2.查看新創建的Race
查找程序方法生成持久存儲的實體
現在我已經存儲了一個 Entity,擁有查找它的能力將有所幫助。接下來,我可以添加一個“查找程序”方法。在本例中,我將把這個“查找程序”方法創建為一個類方法(static)并且允許通過名稱查找這些 Race(即基于 name屬性搜索)。稍后,總是可以通過其他屬性添加其他查找程序。
我還打算對我的查找程序采用一個慣例,即指定:任何名稱中不帶單詞 all的查找程序都企圖找到一個實例。名稱中包含單詞 all的查找程序(如 findAllByName)能夠返回一個實例 Collection或 List。清單 5 展示了 findByName查找程序:
清單5.一個基于 Entity名稱搜索的簡單查找程序
static def findByName(name) {def query =newQuery(Race.class.simpleName) query.addFilter("name", Query.FilterOperator.EQUAL, name) def preparedQuery =this.datastore.prepare(query) if (preparedQuery.countEntities()>1) else}
這個簡單的查找程序使用 Google App Engine 的 Query和 PreparedQuery類型來查找一個類型為“Race”的實體,其名稱(完全)等同于傳入的名稱。如果有超過一個 Race符合這個標準,查找程序將返回一個列表的第一項,這是分頁限制 1(withLimit(1))所指定的。
對應的 findAllByName與上述方法類似,但添加了一個參數,指定您想要的實體個數,如清單 6 所示:
清單 6.通過名稱找到全部實體
static def findAllByName(name, pagination=10) { def query =newQuery(Race.class.getSimpleName()) query.addFilter("name", Query.FilterOperator.EQUAL, name) def preparedQuery =this.datastore.prepare(query) def entities = preparedQuery.asList(withLimit(pagination asint)) return entities.collect {newRace(it as Entity) } }
與前面定義的查找程序類似,findAllByName通過名稱找到 Race實例,但是它返回所有 Race。順便說一下,Groovy的 collect方法非常靈活:它允許刪除創建 Race實例的對應的循環。注意,Groovy還支持方法參數的默認值;這樣,如果我沒有傳入第 2 個值,pagination將擁有值 10。