手里有錘子的時候,看什么東西都像釘子(就像古諺語所說的那樣)。但是如果沒有錘子時該怎樣辦呢?有時,您可以去借一把錘子。然后,拿著這把借來的錘子敲打虛擬的釘子,最后歸還錘子,沒人知道這些。在本月的 Java 理論與實踐 系列中,Brian Goetz 將演示如何將 SQL 或者 XQuery 這樣的數據操縱之錘應用于非持久存儲的數據。請在本文附帶的 討論論壇 中與作者和其他讀者分享您對本文的看法。(也可以單擊本文頂部或底部的 討論 來訪問該論壇。)
我最近仔細考察了一個項目,該項目涉及相當多的 Web 快速搜索。當爬蟲程序爬過不同的 Web 站點時,它將建立一個數據庫,該數據庫中包括它所爬過的站點和網頁、每一頁所包含的鏈接、每一頁的分析結果等數據。最終結果是一組報告,詳細說明經過了哪些站點和頁面、哪些是一直鏈接的、哪些鏈接已經斷開、哪些頁面有錯誤、計算出的頁面規格,等等。開始的時候,沒人確切知道需要什么樣的報告,或者應當采用什么樣的格式 —— 只知道有一些內容要報告。這表明報告開發階段會是一個反復的階段,要經過多次反饋、修改,并且可能嘗試使用不同的結構。惟一確定的報告要求是,報告應當以 XML 形式展示,也可能以 HTML 形式展示。因此,開發和修改報告的過程必須是輕量級的,因為報告要求是“動態發現”的,而不是預先指定的。
不需要數據庫
對這個問題的“最顯而易見的”解決方法是將所有東西都放入 SQL 數據庫中 —— 頁面、鏈接、度量標準、HTTP 結果代碼、計時結果和其他元數據。這個問題可以借助關系表示來很好地解決,特別是因為這種方法不需要存儲已訪問頁面的內容,只需要存儲它們的結構和元數據。
到目前為止,這個項目看起來像是一個典型的數據庫應用程序,并且它并不缺少可供選擇的持久性策略。但是,或許可以避免使用數據庫持久存儲數據的復雜性 —— 這個快速搜索工具(crawler)只訪問數萬個頁面。這個數字不是很大,因此可以將整個數據庫放在內存中,當需要持久存儲數據時,可以通過序列化來實現它。(是的,加載和保存操作要花費較長的時間,但是這些操作并不經常執行。)懶惰反而帶來了一個好處 —— 不需要處理持久性極大地縮短了開發應用程序的時間,因而顯著地減少了開發工作量。構建和操縱內存中的數據結構要比每次添加、提取或者分析數據時都使用數據庫容易得多。不管選擇了哪種持久存儲模型,都會限制任何觸及到數據的代碼的構造。
內存中的數據結構是一種樹型結構,如清單 1 所示,它的根是快速搜索過的各個網站的主頁,因此 Visitor 模式是搜索這些主頁或者從中提取數據的理想模式。(構建一個防止陷入鏈接循環 —— A 鏈接到 B、B 鏈接到 C、C 鏈接到 A —— 的基本 Visitor 類并不是很難。)
清單 1. Web 爬行器的一個簡化方案
public class Site {
Page homepage;
Collection<Page> pages;
Collection<Link> links;
}
public class Page {
String url;
Site site;
PageMetrics metrics;
}
public class Link {
Page linkFrom;
Page linkTo;
String anchorText;
}
|
這個快速搜索工具的應用程序中有十多個 Visitor,它們所做的事情類似于選擇頁面做進一步分析、選擇不帶鏈接的頁面、列出“被鏈接最多”的頁面,等等。因為所有這些操作都很簡單,所以 Visitor 模式(如清單 2 所示)可以工作得很好,由于數據結構可以放到內存中,因此就算進行徹底搜索,花費也不是很大:
清單 2. 用于 Web 快速搜索工具數據庫的 Visitor 模式
public interface Visitor {
public void visitSite(Site site);
public void visitLink(Link link);
}
|
噢,忘記報告了
如果不運行報告的話,Visitor 策略在訪問數據方面會做得非常好。使用數據庫進行持久存儲的一個好處是:在生成報告時,SQL 的能力就會大放光彩 —— 幾乎可以讓數據庫做任何事情。甚至用 SQL 生成報告原型也很容易 —— 運行原型報告,如果結果不是所需要的結果,那么可以修改 SQL 查詢或者編寫新的查詢,然后再試一試。如果改變的只是 SQL 查詢的話,那么這個編輯-編譯-運行周期可能很快。如果 SQL 不是存儲在程序中,那么您甚至可以跳過這個周期的編譯部分,這樣可以快速生成報告的原型。確定所需要的報告后,將它們構建到應用程序中就很容易了。
因此,雖然對于添加新結果、尋找特定的結果和進行特殊傳輸來說,內存中的數據結構都表現得很不錯,但是對于報告來說,這些變成了不利條件。對于所有其自身結構與數據庫結構不同的報告,Visitor 都必須創建一個全新的數據結構,以包含報告數據。因此,每一種報告類型都需要有自己的、特定于報告的中間數據結構來存放結果,還需要一個用來填充中間數據結構的訪問者,以及用來將中間數據結構轉換成最終報告的后處理(post-processing)代碼。似乎需要做很多工作,尤其在大多數原型報告將被拋棄時。例如,假定您想要列出所有從其他網站鏈接到某個給定網站的頁面的報告、所有外部頁面的列表報告,以及站點上鏈接該頁面的那些頁面的列表,然后,根據鏈接的數量對報告進行歸類,鏈接最多的頁面顯示在最前面。這個計劃基本上將數據結構從里到外翻了個個兒。為了用 Visitor 實現這種數據轉換,需要獲得從某個給定網站可以到達的外部頁面鏈接的列表,并根據被鏈接的頁面對它們進行分類,如清單 3 所示:
清單 3. Visitor 列出被鏈接最多的頁面,以及鏈接到它們的頁面
public class InvertLinksVisitor {
public Map<Page, Set<Page>> map = ...;
public void visitLink(Link link) {
if (link.linkFrom.site.equals(targetSite)
&& !link.linkTo.site.equals(targetSite)) {
if (!map.containsKey(link.linkTo))
map.put(link.linkTo, new HashSet<Page>());
map.get(link.linkTo).add(link.linkFrom);
}
}
}
|
清單 3 中的 Visitor 生成一個映射,將每一個外部頁面與鏈接它的一組內部頁面相關聯。為了準備該報告,還必須根據關聯頁面的大小對這些條目進行分類,然后創建報告。雖然沒有任何困難步驟,但是每一個報告需要的特定于報告的代碼數量卻很多,因此快速報告原型就成為一個重要的目標(因為沒有提出報告要求),試驗新報告的開銷比理想情況更高。許多報告需要多次傳遞數據,以便對數據進行選擇、匯總和分類。
我的數據模型王國
這時,缺少一個正式的數據模型開始成為一項不利因素,該數據模型可以用于描述收集的數據,并且可以用它更容易地表示選擇和聚合查詢。也許懶惰不像開始希望的那樣有效。但是,雖然這個應用程序缺少正式數據模型,但也許我們可以將數據存儲到內存中的數據庫,并憑借該數據庫進行查詢,通過這種方式借用一個數據模型。有兩種可能會立即出現在您的腦海中:開源的內存中的 SQL 數據庫 HSQLDB 和 XQuery。我不需要數據庫提供的持久性,但是我確實需要查詢語言。
HSQLDB 是一個用 Java 語言編寫的可嵌入的數據庫引擎。它既包含適用于內存中表的表類型,又包含適用于基于磁盤的表的表類型,設計該引擎為了將表完全嵌入到應用程序中,消除與大多數真實數據庫相關的管理開銷。要將數據裝載到 HSQLDB,只需編寫一個 Visitor 即可,該 Visitor 將遍歷內存中的數據結構,并為每一個將要存儲的實體生成相應的 INSERT 語句。然后可以對這個內存中的數據庫表執行 SQL 查詢,以生成報告,并在完成這些操作后拋棄這個“數據庫”。
噢,忘記了關系數據庫有多煩人
HSQLDB 方法是一個可行方法,但您很快就發現,我必須為對象關系的不匹配而兩次(而不是一次)受罰 —— 一次是在將樹型結構數據庫轉換為關系數據模型時,一次是在將平面關系查詢結果轉換成結構化的 XML 或者 HTML 結果集時。此外,將 JDBC ResultSet 后處理為 DOM 表示形式的 XML 或者 HTML 文檔也不是一項很容易的任務,需要為每一個報告提供一些定制的編碼。因此雖然內存中的 SQL 數據庫 的確 可以簡化查詢,但是從數據庫中存入和取出數據所需要的額外代碼會抵消所有節省的代碼。
讓 XQuery 來拯救您
另一個容易得到的數據查詢方法是 XQuery。XQuery 的優點是,它是為生成 XML 或者 HTML 文檔作為查詢結果而設計的,因此不需要對查詢結果進行后處理。這種想法很有吸引力 —— 每個報告只有一層編碼,而不是兩層或者更多層。因此第一項任務是構建一個表示整個數據集的 XML 文檔。設計一個簡單的 XML 數據模型和編寫遍歷數據結構,并將每一個元素附加到一個 DOM 文檔中的 Visitor 很簡單。(不需要寫出這個文檔。可以將它保持在內存中,用于查詢,然后在完成查詢時丟棄它。當底層數據改變時,可以重新生成它。)之后,所有要做的就是編寫 XQuery 查詢,該查詢將選擇并聚集用于報告的數據,并按最終需要的格式(XML 或 HTML)對它們進行格式化。查詢可以存儲在單獨的文件中,以便進行快速原型制造,因此,可支持多種報告格式。使用 Saxon 評估查詢的代碼如清單 4 中所示:
清單 4. 執行 XQuery 查詢并將結果序列化為 XML 或 HTML 文檔的代碼
String query = readFile(queryFile + ".xq");
Configuration c = new Configuration();
StaticQueryContext qp = new StaticQueryContext(c);
XQueryExpression xe = qp.compileQuery(query);
DynamicQueryContext dqc = new DynamicQueryContext(c);
dqc.setContextNode(new DocumentWrapper(document, z.getName(), c));
List result = xe.evaluate(dqc);
FileOutputStream os = new FileOutputStream(fileName);
XMLSerializer serializer = new XMLSerializer (os, format);
serializer.asDOMSerializer();
for(Iterator i = result.iterator(); i.hasNext(); ) {
Object o = i.next();
if (o instanceof Element)
serializer.serialize((Element) o);
else if (o instanceof Attr) {
Element e = document.createElement("scalar");
e.setTextContent(((Attr) o).getNodeValue());
serializer.serialize(e);
}
else {
Element e = document.createElement("scalar");
e.setTextContent(o.toString());
serializer.serialize(e);
}
}
os.close();
|
表示數據庫的 XML 文檔的結構與內存中的數據結構稍有不同,每一個 <site> 元素都有嵌套的 <page> 元素,每一個 <page> 元素都有嵌套的 <link> 元素,而每一個 <link> 元素都有 <link-to> 和 <link-from> 元素。實踐證明,這種表示方法對于大多數報告都很方便。
清單 5 顯示了一個示例 XQuery 報告,這個報告處理鏈接的選擇、分類和表示。它有幾個地方優于 Visitor 方法 —— 不僅代碼少(因為查詢語言支持選擇、聚積和分類),而且所有報告的代碼 —— 選擇、聚積、分類和表示 —— 都在一個位置上。
清單 5.生成鏈接次數最多的頁面的完整報告的 XQuery 代碼
<html>
<head><title>被鏈接最多的頁面</title></head>
<body>
<ul>
{
let $links := //link[link-to/@siteUrl ne $targetSite
and link-from/@siteUrl eq $targetSite]
for $page in distinct-values($links/link-to/@url)
let $linkingPages := $links[link-to/@url eq $page]/link-from/@url
order by count($linkingPages)
return
<li>Page {$page}, {count($linkingPages)} links
<ul> {
for $p in $linkingPages return <li>Linked from {$p/@url}</li>
}
</ul></li>
}
</ul> </body> </html>
|
結束語
從開發成本角度看,XQuery 方法已證實可以節約大量成本。樹型結構對于構建和搜索數據很理想,但對于報告,就不是很理想了。XML 方法很適合于報告(因為可以利用 XQuery 的能力),但是對于整個應用程序的實現,該方法還有很多不便,并會降低性能。因為數據集的大小是可管理的 —— 只有幾十兆字節,所以可以將數據從一種格式轉換為從開發的角度看最方便的另一種格式。更大的數據集,比如不能完全存儲到內存中的數據集,會要求整個應用程序都圍繞著一個數據庫構建。雖然有許多處理數據持久性的好工具,但是它們需要的工作都比簡單操縱內存中數據結構要多得多。如果數據集的大小合適,那么就可以同時利用這兩種方法的長處。