Vincent

          Vicent's blog
          隨筆 - 74, 文章 - 0, 評(píng)論 - 5, 引用 - 0
          數(shù)據(jù)加載中……

          Java 理論與實(shí)踐: 在沒有數(shù)據(jù)庫的情況下進(jìn)行數(shù)據(jù)庫查詢

          手里有錘子的時(shí)候,看什么東西都像釘子(就像古諺語所說的那樣)。但是如果沒有錘子時(shí)該怎樣辦呢?有時(shí),您可以去借一把錘子。然后,拿著這把借來的錘子敲打虛擬的釘子,最后歸還錘子,沒人知道這些。在本月的 Java 理論與實(shí)踐 系列中,Brian Goetz 將演示如何將 SQL 或者 XQuery 這樣的數(shù)據(jù)操縱之錘應(yīng)用于非持久存儲(chǔ)的數(shù)據(jù)。請(qǐng)?jiān)诒疚母綆У?討論論壇 中與作者和其他讀者分享您對(duì)本文的看法。(也可以單擊本文頂部或底部的 討論 來訪問該論壇。)

          我最近仔細(xì)考察了一個(gè)項(xiàng)目,該項(xiàng)目涉及相當(dāng)多的 Web 快速搜索。當(dāng)爬蟲程序爬過不同的 Web 站點(diǎn)時(shí),它將建立一個(gè)數(shù)據(jù)庫,該數(shù)據(jù)庫中包括它所爬過的站點(diǎn)和網(wǎng)頁、每一頁所包含的鏈接、每一頁的分析結(jié)果等數(shù)據(jù)。最終結(jié)果是一組報(bào)告,詳細(xì)說明經(jīng)過了哪些站點(diǎn)和頁面、哪些是一直鏈接的、哪些鏈接已經(jīng)斷開、哪些頁面有錯(cuò)誤、計(jì)算出的頁面規(guī)格,等等。開始的時(shí)候,沒人確切知道需要什么樣的報(bào)告,或者應(yīng)當(dāng)采用什么樣的格式 —— 只知道有一些內(nèi)容要報(bào)告。這表明報(bào)告開發(fā)階段會(huì)是一個(gè)反復(fù)的階段,要經(jīng)過多次反饋、修改,并且可能嘗試使用不同的結(jié)構(gòu)。惟一確定的報(bào)告要求是,報(bào)告應(yīng)當(dāng)以 XML 形式展示,也可能以 HTML 形式展示。因此,開發(fā)和修改報(bào)告的過程必須是輕量級(jí)的,因?yàn)閳?bào)告要求是“動(dòng)態(tài)發(fā)現(xiàn)”的,而不是預(yù)先指定的。

          不需要數(shù)據(jù)庫

          對(duì)這個(gè)問題的“最顯而易見的”解決方法是將所有東西都放入 SQL 數(shù)據(jù)庫中 —— 頁面、鏈接、度量標(biāo)準(zhǔn)、HTTP 結(jié)果代碼、計(jì)時(shí)結(jié)果和其他元數(shù)據(jù)。這個(gè)問題可以借助關(guān)系表示來很好地解決,特別是因?yàn)檫@種方法不需要存儲(chǔ)已訪問頁面的內(nèi)容,只需要存儲(chǔ)它們的結(jié)構(gòu)和元數(shù)據(jù)。

          到目前為止,這個(gè)項(xiàng)目看起來像是一個(gè)典型的數(shù)據(jù)庫應(yīng)用程序,并且它并不缺少可供選擇的持久性策略。但是,或許可以避免使用數(shù)據(jù)庫持久存儲(chǔ)數(shù)據(jù)的復(fù)雜性 —— 這個(gè)快速搜索工具(crawler)只訪問數(shù)萬個(gè)頁面。這個(gè)數(shù)字不是很大,因此可以將整個(gè)數(shù)據(jù)庫放在內(nèi)存中,當(dāng)需要持久存儲(chǔ)數(shù)據(jù)時(shí),可以通過序列化來實(shí)現(xiàn)它。(是的,加載和保存操作要花費(fèi)較長(zhǎng)的時(shí)間,但是這些操作并不經(jīng)常執(zhí)行。)懶惰反而帶來了一個(gè)好處 —— 不需要處理持久性極大地縮短了開發(fā)應(yīng)用程序的時(shí)間,因而顯著地減少了開發(fā)工作量。構(gòu)建和操縱內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)要比每次添加、提取或者分析數(shù)據(jù)時(shí)都使用數(shù)據(jù)庫容易得多。不管選擇了哪種持久存儲(chǔ)模型,都會(huì)限制任何觸及到數(shù)據(jù)的代碼的構(gòu)造。

          內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)是一種樹型結(jié)構(gòu),如清單 1 所示,它的根是快速搜索過的各個(gè)網(wǎng)站的主頁,因此 Visitor 模式是搜索這些主頁或者從中提取數(shù)據(jù)的理想模式。(構(gòu)建一個(gè)防止陷入鏈接循環(huán) —— A 鏈接到 B、B 鏈接到 C、C 鏈接到 A —— 的基本 Visitor 類并不是很難。)


          清單 1. Web 爬行器的一個(gè)簡(jiǎn)化方案
          												
          																		
          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;
          }
          
          												
          										

          這個(gè)快速搜索工具的應(yīng)用程序中有十多個(gè) Visitor,它們所做的事情類似于選擇頁面做進(jìn)一步分析、選擇不帶鏈接的頁面、列出“被鏈接最多”的頁面,等等。因?yàn)樗羞@些操作都很簡(jiǎn)單,所以 Visitor 模式(如清單 2 所示)可以工作得很好,由于數(shù)據(jù)結(jié)構(gòu)可以放到內(nèi)存中,因此就算進(jìn)行徹底搜索,花費(fèi)也不是很大:


          清單 2. 用于 Web 快速搜索工具數(shù)據(jù)庫的 Visitor 模式
          												
          																		
          public interface Visitor {
              public void visitSite(Site site);
              public void visitLink(Link link);
          }
          
          												
          										

          噢,忘記報(bào)告了

          如果不運(yùn)行報(bào)告的話,Visitor 策略在訪問數(shù)據(jù)方面會(huì)做得非常好。使用數(shù)據(jù)庫進(jìn)行持久存儲(chǔ)的一個(gè)好處是:在生成報(bào)告時(shí),SQL 的能力就會(huì)大放光彩 —— 幾乎可以讓數(shù)據(jù)庫做任何事情。甚至用 SQL 生成報(bào)告原型也很容易 —— 運(yùn)行原型報(bào)告,如果結(jié)果不是所需要的結(jié)果,那么可以修改 SQL 查詢或者編寫新的查詢,然后再試一試。如果改變的只是 SQL 查詢的話,那么這個(gè)編輯-編譯-運(yùn)行周期可能很快。如果 SQL 不是存儲(chǔ)在程序中,那么您甚至可以跳過這個(gè)周期的編譯部分,這樣可以快速生成報(bào)告的原型。確定所需要的報(bào)告后,將它們構(gòu)建到應(yīng)用程序中就很容易了。

          因此,雖然對(duì)于添加新結(jié)果、尋找特定的結(jié)果和進(jìn)行特殊傳輸來說,內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)都表現(xiàn)得很不錯(cuò),但是對(duì)于報(bào)告來說,這些變成了不利條件。對(duì)于所有其自身結(jié)構(gòu)與數(shù)據(jù)庫結(jié)構(gòu)不同的報(bào)告,Visitor 都必須創(chuàng)建一個(gè)全新的數(shù)據(jù)結(jié)構(gòu),以包含報(bào)告數(shù)據(jù)。因此,每一種報(bào)告類型都需要有自己的、特定于報(bào)告的中間數(shù)據(jù)結(jié)構(gòu)來存放結(jié)果,還需要一個(gè)用來填充中間數(shù)據(jù)結(jié)構(gòu)的訪問者,以及用來將中間數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換成最終報(bào)告的后處理(post-processing)代碼。似乎需要做很多工作,尤其在大多數(shù)原型報(bào)告將被拋棄時(shí)。例如,假定您想要列出所有從其他網(wǎng)站鏈接到某個(gè)給定網(wǎng)站的頁面的報(bào)告、所有外部頁面的列表報(bào)告,以及站點(diǎn)上鏈接該頁面的那些頁面的列表,然后,根據(jù)鏈接的數(shù)量對(duì)報(bào)告進(jìn)行歸類,鏈接最多的頁面顯示在最前面。這個(gè)計(jì)劃基本上將數(shù)據(jù)結(jié)構(gòu)從里到外翻了個(gè)個(gè)兒。為了用 Visitor 實(shí)現(xiàn)這種數(shù)據(jù)轉(zhuǎn)換,需要獲得從某個(gè)給定網(wǎng)站可以到達(dá)的外部頁面鏈接的列表,并根據(jù)被鏈接的頁面對(duì)它們進(jìn)行分類,如清單 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 生成一個(gè)映射,將每一個(gè)外部頁面與鏈接它的一組內(nèi)部頁面相關(guān)聯(lián)。為了準(zhǔn)備該報(bào)告,還必須根據(jù)關(guān)聯(lián)頁面的大小對(duì)這些條目進(jìn)行分類,然后創(chuàng)建報(bào)告。雖然沒有任何困難步驟,但是每一個(gè)報(bào)告需要的特定于報(bào)告的代碼數(shù)量卻很多,因此快速報(bào)告原型就成為一個(gè)重要的目標(biāo)(因?yàn)闆]有提出報(bào)告要求),試驗(yàn)新報(bào)告的開銷比理想情況更高。許多報(bào)告需要多次傳遞數(shù)據(jù),以便對(duì)數(shù)據(jù)進(jìn)行選擇、匯總和分類。





          回頁首


          我的數(shù)據(jù)模型王國(guó)

          這時(shí),缺少一個(gè)正式的數(shù)據(jù)模型開始成為一項(xiàng)不利因素,該數(shù)據(jù)模型可以用于描述收集的數(shù)據(jù),并且可以用它更容易地表示選擇和聚合查詢。也許懶惰不像開始希望的那樣有效。但是,雖然這個(gè)應(yīng)用程序缺少正式數(shù)據(jù)模型,但也許我們可以將數(shù)據(jù)存儲(chǔ)到內(nèi)存中的數(shù)據(jù)庫,并憑借該數(shù)據(jù)庫進(jìn)行查詢,通過這種方式借用一個(gè)數(shù)據(jù)模型。有兩種可能會(huì)立即出現(xiàn)在您的腦海中:開源的內(nèi)存中的 SQL 數(shù)據(jù)庫 HSQLDB 和 XQuery。我不需要數(shù)據(jù)庫提供的持久性,但是我確實(shí)需要查詢語言。

          HSQLDB 是一個(gè)用 Java 語言編寫的可嵌入的數(shù)據(jù)庫引擎。它既包含適用于內(nèi)存中表的表類型,又包含適用于基于磁盤的表的表類型,設(shè)計(jì)該引擎為了將表完全嵌入到應(yīng)用程序中,消除與大多數(shù)真實(shí)數(shù)據(jù)庫相關(guān)的管理開銷。要將數(shù)據(jù)裝載到 HSQLDB,只需編寫一個(gè) Visitor 即可,該 Visitor 將遍歷內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),并為每一個(gè)將要存儲(chǔ)的實(shí)體生成相應(yīng)的 INSERT 語句。然后可以對(duì)這個(gè)內(nèi)存中的數(shù)據(jù)庫表執(zhí)行 SQL 查詢,以生成報(bào)告,并在完成這些操作后拋棄這個(gè)“數(shù)據(jù)庫”。

          噢,忘記了關(guān)系數(shù)據(jù)庫有多煩人

          HSQLDB 方法是一個(gè)可行方法,但您很快就發(fā)現(xiàn),我必須為對(duì)象關(guān)系的不匹配而兩次(而不是一次)受罰 —— 一次是在將樹型結(jié)構(gòu)數(shù)據(jù)庫轉(zhuǎn)換為關(guān)系數(shù)據(jù)模型時(shí),一次是在將平面關(guān)系查詢結(jié)果轉(zhuǎn)換成結(jié)構(gòu)化的 XML 或者 HTML 結(jié)果集時(shí)。此外,將 JDBC ResultSet 后處理為 DOM 表示形式的 XML 或者 HTML 文檔也不是一項(xiàng)很容易的任務(wù),需要為每一個(gè)報(bào)告提供一些定制的編碼。因此雖然內(nèi)存中的 SQL 數(shù)據(jù)庫 的確 可以簡(jiǎn)化查詢,但是從數(shù)據(jù)庫中存入和取出數(shù)據(jù)所需要的額外代碼會(huì)抵消所有節(jié)省的代碼。





          回頁首


          讓 XQuery 來拯救您

          另一個(gè)容易得到的數(shù)據(jù)查詢方法是 XQuery。XQuery 的優(yōu)點(diǎn)是,它是為生成 XML 或者 HTML 文檔作為查詢結(jié)果而設(shè)計(jì)的,因此不需要對(duì)查詢結(jié)果進(jìn)行后處理。這種想法很有吸引力 —— 每個(gè)報(bào)告只有一層編碼,而不是兩層或者更多層。因此第一項(xiàng)任務(wù)是構(gòu)建一個(gè)表示整個(gè)數(shù)據(jù)集的 XML 文檔。設(shè)計(jì)一個(gè)簡(jiǎn)單的 XML 數(shù)據(jù)模型和編寫遍歷數(shù)據(jù)結(jié)構(gòu),并將每一個(gè)元素附加到一個(gè) DOM 文檔中的 Visitor 很簡(jiǎn)單。(不需要寫出這個(gè)文檔??梢詫⑺3衷趦?nèi)存中,用于查詢,然后在完成查詢時(shí)丟棄它。當(dāng)?shù)讓訑?shù)據(jù)改變時(shí),可以重新生成它。)之后,所有要做的就是編寫 XQuery 查詢,該查詢將選擇并聚集用于報(bào)告的數(shù)據(jù),并按最終需要的格式(XML 或 HTML)對(duì)它們進(jìn)行格式化。查詢可以存儲(chǔ)在單獨(dú)的文件中,以便進(jìn)行快速原型制造,因此,可支持多種報(bào)告格式。使用 Saxon 評(píng)估查詢的代碼如清單 4 中所示:


          清單 4. 執(zhí)行 XQuery 查詢并將結(jié)果序列化為 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(); 
          
          												
          										

          表示數(shù)據(jù)庫的 XML 文檔的結(jié)構(gòu)與內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)稍有不同,每一個(gè) <site> 元素都有嵌套的 <page> 元素,每一個(gè) <page> 元素都有嵌套的 <link> 元素,而每一個(gè) <link> 元素都有 <link-to> 和 <link-from> 元素。實(shí)踐證明,這種表示方法對(duì)于大多數(shù)報(bào)告都很方便。

          清單 5 顯示了一個(gè)示例 XQuery 報(bào)告,這個(gè)報(bào)告處理鏈接的選擇、分類和表示。它有幾個(gè)地方優(yōu)于 Visitor 方法 —— 不僅代碼少(因?yàn)椴樵冋Z言支持選擇、聚積和分類),而且所有報(bào)告的代碼 —— 選擇、聚積、分類和表示 —— 都在一個(gè)位置上。


          清單 5.生成鏈接次數(shù)最多的頁面的完整報(bào)告的 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>
          
          												
          										





          回頁首


          結(jié)束語

          從開發(fā)成本角度看,XQuery 方法已證實(shí)可以節(jié)約大量成本。樹型結(jié)構(gòu)對(duì)于構(gòu)建和搜索數(shù)據(jù)很理想,但對(duì)于報(bào)告,就不是很理想了。XML 方法很適合于報(bào)告(因?yàn)榭梢岳?XQuery 的能力),但是對(duì)于整個(gè)應(yīng)用程序的實(shí)現(xiàn),該方法還有很多不便,并會(huì)降低性能。因?yàn)閿?shù)據(jù)集的大小是可管理的 —— 只有幾十兆字節(jié),所以可以將數(shù)據(jù)從一種格式轉(zhuǎn)換為從開發(fā)的角度看最方便的另一種格式。更大的數(shù)據(jù)集,比如不能完全存儲(chǔ)到內(nèi)存中的數(shù)據(jù)集,會(huì)要求整個(gè)應(yīng)用程序都圍繞著一個(gè)數(shù)據(jù)庫構(gòu)建。雖然有許多處理數(shù)據(jù)持久性的好工具,但是它們需要的工作都比簡(jiǎn)單操縱內(nèi)存中數(shù)據(jù)結(jié)構(gòu)要多得多。如果數(shù)據(jù)集的大小合適,那么就可以同時(shí)利用這兩種方法的長(zhǎng)處。

          posted on 2006-08-24 17:36 Binary 閱讀(324) 評(píng)論(0)  編輯  收藏 所屬分類: j2se

          主站蜘蛛池模板: 彭阳县| 长岛县| 林芝县| 长丰县| 保德县| 崇州市| 康保县| 来凤县| 肥西县| 无棣县| 石泉县| 都匀市| 阜平县| 二连浩特市| 梧州市| 临沭县| 宜城市| 郎溪县| 新乡市| 穆棱市| 嘉善县| 舞阳县| 卢湾区| 东乡| 二手房| 彭水| 囊谦县| 永德县| 安达市| 防城港市| 永春县| 隆子县| 丽水市| 毕节市| 陵水| 绥化市| 无锡市| 闽侯县| 铜鼓县| 大竹县| 寻甸|