Java 理論與實踐: 在沒有數(shù)據(jù)庫的情況下進行數(shù)據(jù)庫查詢
手里有錘子的時候,看什么東西都像釘子(就像古諺語所說的那樣)。但是如果沒有錘子時該怎樣辦呢?有時,您可以去借一把錘子。然后,拿著這把借來的錘子敲打虛擬的釘子,最后歸還錘子,沒人知道這些。在本月的 Java 理論與實踐 系列中,Brian Goetz 將演示如何將 SQL 或者 XQuery 這樣的數(shù)據(jù)操縱之錘應(yīng)用于非持久存儲的數(shù)據(jù)。請在本文附帶的 討論論壇 中與作者和其他讀者分享您對本文的看法。(也可以單擊本文頂部或底部的 討論 來訪問該論壇。)
我最近仔細考察了一個項目,該項目涉及相當多的 Web 快速搜索。當爬蟲程序爬過不同的 Web 站點時,它將建立一個數(shù)據(jù)庫,該數(shù)據(jù)庫中包括它所爬過的站點和網(wǎng)頁、每一頁所包含的鏈接、每一頁的分析結(jié)果等數(shù)據(jù)。最終結(jié)果是一組報告,詳細說明經(jīng)過了哪些站點和頁面、哪些是一直鏈接的、哪些鏈接已經(jīng)斷開、哪些頁面有錯誤、計算出的頁面規(guī)格,等等。開始的時候,沒人確切知道需要什么樣的報告,或者應(yīng)當采用什么樣的格式 —— 只知道有一些內(nèi)容要報告。這表明報告開發(fā)階段會是一個反復(fù)的階段,要經(jīng)過多次反饋、修改,并且可能嘗試使用不同的結(jié)構(gòu)。惟一確定的報告要求是,報告應(yīng)當以 XML 形式展示,也可能以 HTML 形式展示。因此,開發(fā)和修改報告的過程必須是輕量級的,因為報告要求是“動態(tài)發(fā)現(xiàn)”的,而不是預(yù)先指定的。
對這個問題的“最顯而易見的”解決方法是將所有東西都放入 SQL 數(shù)據(jù)庫中 —— 頁面、鏈接、度量標準、HTTP 結(jié)果代碼、計時結(jié)果和其他元數(shù)據(jù)。這個問題可以借助關(guān)系表示來很好地解決,特別是因為這種方法不需要存儲已訪問頁面的內(nèi)容,只需要存儲它們的結(jié)構(gòu)和元數(shù)據(jù)。
到目前為止,這個項目看起來像是一個典型的數(shù)據(jù)庫應(yīng)用程序,并且它并不缺少可供選擇的持久性策略。但是,或許可以避免使用數(shù)據(jù)庫持久存儲數(shù)據(jù)的復(fù)雜性 —— 這個快速搜索工具(crawler)只訪問數(shù)萬個頁面。這個數(shù)字不是很大,因此可以將整個數(shù)據(jù)庫放在內(nèi)存中,當需要持久存儲數(shù)據(jù)時,可以通過序列化來實現(xiàn)它。(是的,加載和保存操作要花費較長的時間,但是這些操作并不經(jīng)常執(zhí)行。)懶惰反而帶來了一個好處 —— 不需要處理持久性極大地縮短了開發(fā)應(yīng)用程序的時間,因而顯著地減少了開發(fā)工作量。構(gòu)建和操縱內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)要比每次添加、提取或者分析數(shù)據(jù)時都使用數(shù)據(jù)庫容易得多。不管選擇了哪種持久存儲模型,都會限制任何觸及到數(shù)據(jù)的代碼的構(gòu)造。
內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)是一種樹型結(jié)構(gòu),如清單 1 所示,它的根是快速搜索過的各個網(wǎng)站的主頁,因此 Visitor 模式是搜索這些主頁或者從中提取數(shù)據(jù)的理想模式。(構(gòu)建一個防止陷入鏈接循環(huán) —— A 鏈接到 B、B 鏈接到 C、C 鏈接到 A —— 的基本 Visitor 類并不是很難。)
清單 1. Web 爬行器的一個簡化方案
|
這個快速搜索工具的應(yīng)用程序中有十多個 Visitor,它們所做的事情類似于選擇頁面做進一步分析、選擇不帶鏈接的頁面、列出“被鏈接最多”的頁面,等等。因為所有這些操作都很簡單,所以 Visitor 模式(如清單 2 所示)可以工作得很好,由于數(shù)據(jù)結(jié)構(gòu)可以放到內(nèi)存中,因此就算進行徹底搜索,花費也不是很大:
清單 2. 用于 Web 快速搜索工具數(shù)據(jù)庫的 Visitor 模式
|
如果不運行報告的話,Visitor 策略在訪問數(shù)據(jù)方面會做得非常好。使用數(shù)據(jù)庫進行持久存儲的一個好處是:在生成報告時,SQL 的能力就會大放光彩 —— 幾乎可以讓數(shù)據(jù)庫做任何事情。甚至用 SQL 生成報告原型也很容易 —— 運行原型報告,如果結(jié)果不是所需要的結(jié)果,那么可以修改 SQL 查詢或者編寫新的查詢,然后再試一試。如果改變的只是 SQL 查詢的話,那么這個編輯-編譯-運行周期可能很快。如果 SQL 不是存儲在程序中,那么您甚至可以跳過這個周期的編譯部分,這樣可以快速生成報告的原型。確定所需要的報告后,將它們構(gòu)建到應(yīng)用程序中就很容易了。
因此,雖然對于添加新結(jié)果、尋找特定的結(jié)果和進行特殊傳輸來說,內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)都表現(xiàn)得很不錯,但是對于報告來說,這些變成了不利條件。對于所有其自身結(jié)構(gòu)與數(shù)據(jù)庫結(jié)構(gòu)不同的報告,Visitor 都必須創(chuàng)建一個全新的數(shù)據(jù)結(jié)構(gòu),以包含報告數(shù)據(jù)。因此,每一種報告類型都需要有自己的、特定于報告的中間數(shù)據(jù)結(jié)構(gòu)來存放結(jié)果,還需要一個用來填充中間數(shù)據(jù)結(jié)構(gòu)的訪問者,以及用來將中間數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換成最終報告的后處理(post-processing)代碼。似乎需要做很多工作,尤其在大多數(shù)原型報告將被拋棄時。例如,假定您想要列出所有從其他網(wǎng)站鏈接到某個給定網(wǎng)站的頁面的報告、所有外部頁面的列表報告,以及站點上鏈接該頁面的那些頁面的列表,然后,根據(jù)鏈接的數(shù)量對報告進行歸類,鏈接最多的頁面顯示在最前面。這個計劃基本上將數(shù)據(jù)結(jié)構(gòu)從里到外翻了個個兒。為了用 Visitor 實現(xiàn)這種數(shù)據(jù)轉(zhuǎn)換,需要獲得從某個給定網(wǎng)站可以到達的外部頁面鏈接的列表,并根據(jù)被鏈接的頁面對它們進行分類,如清單 3 所示:
清單 3. Visitor 列出被鏈接最多的頁面,以及鏈接到它們的頁面
|
清單 3 中的 Visitor 生成一個映射,將每一個外部頁面與鏈接它的一組內(nèi)部頁面相關(guān)聯(lián)。為了準備該報告,還必須根據(jù)關(guān)聯(lián)頁面的大小對這些條目進行分類,然后創(chuàng)建報告。雖然沒有任何困難步驟,但是每一個報告需要的特定于報告的代碼數(shù)量卻很多,因此快速報告原型就成為一個重要的目標(因為沒有提出報告要求),試驗新報告的開銷比理想情況更高。許多報告需要多次傳遞數(shù)據(jù),以便對數(shù)據(jù)進行選擇、匯總和分類。
![]() ![]() |
![]()
|
這時,缺少一個正式的數(shù)據(jù)模型開始成為一項不利因素,該數(shù)據(jù)模型可以用于描述收集的數(shù)據(jù),并且可以用它更容易地表示選擇和聚合查詢。也許懶惰不像開始希望的那樣有效。但是,雖然這個應(yīng)用程序缺少正式數(shù)據(jù)模型,但也許我們可以將數(shù)據(jù)存儲到內(nèi)存中的數(shù)據(jù)庫,并憑借該數(shù)據(jù)庫進行查詢,通過這種方式借用一個數(shù)據(jù)模型。有兩種可能會立即出現(xiàn)在您的腦海中:開源的內(nèi)存中的 SQL 數(shù)據(jù)庫 HSQLDB 和 XQuery。我不需要數(shù)據(jù)庫提供的持久性,但是我確實需要查詢語言。
HSQLDB 是一個用 Java 語言編寫的可嵌入的數(shù)據(jù)庫引擎。它既包含適用于內(nèi)存中表的表類型,又包含適用于基于磁盤的表的表類型,設(shè)計該引擎為了將表完全嵌入到應(yīng)用程序中,消除與大多數(shù)真實數(shù)據(jù)庫相關(guān)的管理開銷。要將數(shù)據(jù)裝載到 HSQLDB,只需編寫一個 Visitor 即可,該 Visitor 將遍歷內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),并為每一個將要存儲的實體生成相應(yīng)的 INSERT 語句。然后可以對這個內(nèi)存中的數(shù)據(jù)庫表執(zhí)行 SQL 查詢,以生成報告,并在完成這些操作后拋棄這個“數(shù)據(jù)庫”。
HSQLDB 方法是一個可行方法,但您很快就發(fā)現(xiàn),我必須為對象關(guān)系的不匹配而兩次(而不是一次)受罰 —— 一次是在將樹型結(jié)構(gòu)數(shù)據(jù)庫轉(zhuǎn)換為關(guān)系數(shù)據(jù)模型時,一次是在將平面關(guān)系查詢結(jié)果轉(zhuǎn)換成結(jié)構(gòu)化的 XML 或者 HTML 結(jié)果集時。此外,將 JDBC ResultSet 后處理為 DOM 表示形式的 XML 或者 HTML 文檔也不是一項很容易的任務(wù),需要為每一個報告提供一些定制的編碼。因此雖然內(nèi)存中的 SQL 數(shù)據(jù)庫 的確 可以簡化查詢,但是從數(shù)據(jù)庫中存入和取出數(shù)據(jù)所需要的額外代碼會抵消所有節(jié)省的代碼。
![]() ![]() |
![]()
|
另一個容易得到的數(shù)據(jù)查詢方法是 XQuery。XQuery 的優(yōu)點是,它是為生成 XML 或者 HTML 文檔作為查詢結(jié)果而設(shè)計的,因此不需要對查詢結(jié)果進行后處理。這種想法很有吸引力 —— 每個報告只有一層編碼,而不是兩層或者更多層。因此第一項任務(wù)是構(gòu)建一個表示整個數(shù)據(jù)集的 XML 文檔。設(shè)計一個簡單的 XML 數(shù)據(jù)模型和編寫遍歷數(shù)據(jù)結(jié)構(gòu),并將每一個元素附加到一個 DOM 文檔中的 Visitor 很簡單。(不需要寫出這個文檔。可以將它保持在內(nèi)存中,用于查詢,然后在完成查詢時丟棄它。當?shù)讓訑?shù)據(jù)改變時,可以重新生成它。)之后,所有要做的就是編寫 XQuery 查詢,該查詢將選擇并聚集用于報告的數(shù)據(jù),并按最終需要的格式(XML 或 HTML)對它們進行格式化。查詢可以存儲在單獨的文件中,以便進行快速原型制造,因此,可支持多種報告格式。使用 Saxon 評估查詢的代碼如清單 4 中所示:
清單 4. 執(zhí)行 XQuery 查詢并將結(jié)果序列化為 XML 或 HTML 文檔的代碼
|
表示數(shù)據(jù)庫的 XML 文檔的結(jié)構(gòu)與內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)稍有不同,每一個 <site> 元素都有嵌套的 <page> 元素,每一個 <page> 元素都有嵌套的 <link> 元素,而每一個 <link> 元素都有 <link-to> 和 <link-from> 元素。實踐證明,這種表示方法對于大多數(shù)報告都很方便。
清單 5 顯示了一個示例 XQuery 報告,這個報告處理鏈接的選擇、分類和表示。它有幾個地方優(yōu)于 Visitor 方法 —— 不僅代碼少(因為查詢語言支持選擇、聚積和分類),而且所有報告的代碼 —— 選擇、聚積、分類和表示 —— 都在一個位置上。
清單 5.生成鏈接次數(shù)最多的頁面的完整報告的 XQuery 代碼
|
![]() ![]() |
![]()
|
從開發(fā)成本角度看,XQuery 方法已證實可以節(jié)約大量成本。樹型結(jié)構(gòu)對于構(gòu)建和搜索數(shù)據(jù)很理想,但對于報告,就不是很理想了。XML 方法很適合于報告(因為可以利用 XQuery 的能力),但是對于整個應(yīng)用程序的實現(xiàn),該方法還有很多不便,并會降低性能。因為數(shù)據(jù)集的大小是可管理的 —— 只有幾十兆字節(jié),所以可以將數(shù)據(jù)從一種格式轉(zhuǎn)換為從開發(fā)的角度看最方便的另一種格式。更大的數(shù)據(jù)集,比如不能完全存儲到內(nèi)存中的數(shù)據(jù)集,會要求整個應(yīng)用程序都圍繞著一個數(shù)據(jù)庫構(gòu)建。雖然有許多處理數(shù)據(jù)持久性的好工具,但是它們需要的工作都比簡單操縱內(nèi)存中數(shù)據(jù)結(jié)構(gòu)要多得多。如果數(shù)據(jù)集的大小合適,那么就可以同時利用這兩種方法的長處。
posted on 2006-08-24 17:36 Binary 閱讀(324) 評論(0) 編輯 收藏 所屬分類: j2se