前言: 數(shù)據(jù)庫工程師眾所周知的一個事實是,當(dāng)對數(shù)據(jù)庫里的文本字段進(jìn)行l(wèi)ike檢索的時候,任何數(shù)據(jù)索引都是不起作用的,這樣也就導(dǎo)致系統(tǒng)會承擔(dān)額外的開銷和負(fù)載壓力,對于龐大的數(shù)據(jù)記錄,對其中的文本字段進(jìn)行關(guān)鍵字匹配,就肯定會存在非常嚴(yán)重的效率障礙和性能障礙。因此,基于文本的全文索引技術(shù)也就逐漸興起。
全文索引的技術(shù)原理并不復(fù)雜,對段落性的文本內(nèi)容進(jìn)行逐詞分解,并針對詞出現(xiàn)頻率,出現(xiàn)位置進(jìn)行標(biāo)記,按照詞本身的編碼順序存儲為索引文件。這樣,在針對關(guān)鍵詞進(jìn)行檢索的時候,就不會遍歷所有的文本數(shù)據(jù)記錄,而是根據(jù)索引文件進(jìn)行有序查找,這里面一個顯見的事實是,通過有序索引查找關(guān)鍵詞,對于海量的數(shù)據(jù)記錄而言,也只需要很少次數(shù)的指針跳轉(zhuǎn),(數(shù)量為X的索引記錄,查詢特定記錄的指針跳轉(zhuǎn)次數(shù)最多為Log2(x)。)即可完成搜索,而無須完整遍歷整個數(shù)據(jù)表或文件集。
但是全文索引技術(shù)的實現(xiàn)卻并不簡單,針對中文的尤其如此,英文文本中,空格是天然的分詞標(biāo)記,而中文段落卻無法通過這樣簡單的途徑分詞,因此基于常用語詞典和一些語言識別規(guī)則的分詞技術(shù)成為一種非常高的技術(shù)門檻,幸好,很多商業(yè)公司提供了非常成熟的商業(yè)產(chǎn)品,使我等可以坐享其成,快速搭建全文搜索的平臺。
ORACLE INTERMEDIA介紹
ORACLE Intermedia是ORACLE公司官方發(fā)布的用來管理多媒體數(shù)據(jù)的數(shù)據(jù)庫管理模塊,通過它可以進(jìn)行有效的視頻,音頻,圖片等文件的統(tǒng)一存儲,調(diào)用和相關(guān)處理;同時其中也包括一個Oracle Intermdedia Text功能模塊,能夠?qū)Χ喾N格式文檔進(jìn)行分詞索引處理,也提供了使用自然語法或高級查詢方法進(jìn)行跨文本查詢的途徑,可以查詢word, PDF,RTF等格式的文件和數(shù)據(jù)。
Oracle Intermedia 的索引效率和查詢效率,據(jù)一些公開數(shù)據(jù)上看要遠(yuǎn)高于Microsoft的Index Server,而且本身具有平臺無關(guān)特性,另外作為數(shù)據(jù)庫產(chǎn)品,可以很好的和數(shù)據(jù)庫應(yīng)用進(jìn)行整合,這一點也是純粹的文件索引系統(tǒng)所無法實現(xiàn)的。當(dāng)然,作為通用的數(shù)據(jù)庫產(chǎn)品,Oracle不可能針對全文索引做到最大限度的優(yōu)化,因此對于高并發(fā)大容量的搜索引擎應(yīng)用,Oracle的方案可能就無法滿足,這一點也是必須提前聲明的。
全文索引實現(xiàn)步驟
步驟1:查看Oracle Intermedia是否正確安裝。Oracle Intermdeia是Oracle的一個附帶模塊,安裝過程中選擇即可。
步驟2:設(shè)置詞法解析器
oracle根據(jù)不同語言,有不同的詞法解析器,以下說明我們可能用到的三個
basic_lexer,針對英語環(huán)境,以空格為分詞標(biāo)記,同時能分辨一些“噪音”單詞,如 “if”, “is”等。
chinese_vgram_lexer,專用的漢語分析器,按字為單元分析中文,算法簡單,可以一網(wǎng)打盡中文用詞,但是效率差強人意。
chinese_lexer,可以識別大部分常用短語和詞匯,不會產(chǎn)生大量冗余數(shù)據(jù),有很好的實用性,但是語言支持只能為UTF-8編碼,不支持zhs16gbk字符集。
以ctxsys用戶登陸系統(tǒng),執(zhí)行:
begin ctx_ddl.create_preference('my_lexer','chinese_vgram_lexer'); end;
這里假設(shè)我們的語法解析器命名為my_lexer,這個名稱也可以根據(jù)實際應(yīng)用變化。
步驟3:建立索引字段
我的測試用例保存在system空間,表名為my_docs,字段名為doc,字段類型為blob,存儲標(biāo)準(zhǔn)word doc文件。
仍舊保持ctxsys帳戶登陸,執(zhí)行如下操作
create index system.myindex on system.my_docs(doc) indextype is ctxsys.context parameters(‘lexer’,’my_lexer’) ;
步驟4:同步操作(sync)及優(yōu)化操作
以system 登陸,同步操作執(zhí)行
exec ctx_ddl.sync_index('myindex');
創(chuàng)建同步定時任務(wù)代碼如下
VARIABLE jobno number;
BEGIN
DBMS_JOB.SUBMIT(:jobno,'ctx_ddl.sync_index(''myindex'');',
SYSDATE, 'SYSDATE + (1/24/4)');
commit;
END;
/
以system登陸,優(yōu)化索引操作執(zhí)行
exec ctx_ddl.optimize_index('myindex','FULL');
創(chuàng)建優(yōu)化定時任務(wù)代碼如下
VARIABLE jobno number;
BEGIN
DBMS_JOB.SUBMIT(:jobno,'ctx_ddl.optimize_index(''myindex'',''FULL'');',
SYSDATE, 'SYSDATE + 1');
commit;
END;
/
步驟5:測試
select id from my_docs where contains(doc,'關(guān)鍵字')>0
總結(jié):
該學(xué)習(xí)筆記內(nèi)容大部分可以通過搜索引擎找到,并非本人原創(chuàng)內(nèi)容,本文全部經(jīng)個人在windows平臺下,在oracle 9i下測試完成,留檔記錄,為日后的項目和產(chǎn)品開發(fā)做技術(shù)準(zhǔn)備。 全文索引—CONTAINS語法
全文索引—CONTAINS語法
全文索引——CONTAINS 語法
我們通常在 WHERE 子句中使用 CONTAINS ,就象這樣:SELECT * FROM table_name WHERE CONTAINS(fullText_column,'search contents')。
我們通過例子來學(xué)習(xí),假設(shè)有表 students,其中的 address 是全文本檢索的列。
1. 查詢住址在北京的學(xué)生
SELECT student_id,student_name
FROM students
WHERE CONTAINS( address, 'beijing' )
remark: beijing是一個單詞,要用單引號括起來。
2. 查詢住址在河北省的學(xué)生
SELECT student_id,student_name
FROM students
WHERE CONTAINS( address, '"HEIBEI province"' )
remark: HEBEI province是一個詞組,在單引號里還要用雙引號括起來。
3. 查詢住址在河北省或北京的學(xué)生
SELECT student_id,student_name
FROM students
WHERE CONTAINS( address, '"HEIBEI province" OR beijing' )
remark: 可以指定邏輯操作符(包括 AND ,AND NOT,OR )。
4. 查詢有 '南京路' 字樣的地址
SELECT student_id,student_name
FROM students
WHERE CONTAINS( address, 'nanjing NEAR road' )
remark: 上面的查詢將返回包含 'nanjing road','nanjing east road','nanjing west road' 等字樣的地址。
? ?? ?? ? A NEAR B,就表示條件: A 靠近 B。
5. 查詢以 '湖' 開頭的地址
SELECT student_id,student_name
FROM students
WHERE CONTAINS( address, '"hu*"' )
remark: 上面的查詢將返回包含 'hubei','hunan' 等字樣的地址。
? ?? ?? ? 記住是 *,不是 %。
6. 類似加權(quán)的查詢
SELECT student_id,student_name
FROM students
WHERE CONTAINS( address, 'ISABOUT (city weight (.8), county wright (.4))' )
remark: ISABOUT 是這種查詢的關(guān)鍵字,weight 指定了一個介于 0~1之間的數(shù),類似系數(shù)(我的理解)。表示不同條件有不同的側(cè)重。
7. 單詞的多態(tài)查詢
SELECT student_id,student_name
FROM students
WHERE CONTAINS( address, 'FORMSOF (INFLECTIONAL,street)' )
remark: 查詢將返回包含 'street','streets'等字樣的地址。
? ?? ?? ?對于動詞將返回它的不同的時態(tài),如:dry,將返回 dry,dried,drying 等等。
Lucene不是一個完整的全文索引應(yīng)用,而是是一個用Java寫的全文索引引擎工具包,它可以方便的嵌入到各種應(yīng)用中實現(xiàn)針對應(yīng)用的全文索引/檢索功能。
Lucene的作者:Lucene的貢獻(xiàn)者Doug Cutting是一位資深全文索引/檢索專家,曾經(jīng)是V-Twin搜索引擎(Apple的Copland操作系統(tǒng)的成就之一)的主要開發(fā)者,后在Excite擔(dān)任高級系統(tǒng)架構(gòu)設(shè)計師,目前從事于一些INTERNET底層架構(gòu)的研究。他貢獻(xiàn)出的Lucene的目標(biāo)是為各種中小型應(yīng)用程序加入全文檢索功能。
Lucene的發(fā)展歷程:早先發(fā)布在作者自己的www.lucene.com,后來發(fā)布在SourceForge,2001年年底成為APACHE基金會jakarta的一個子項目:http://jakarta.apache.org/lucene/
已經(jīng)有很多Java項目都使用了Lucene作為其后臺的全文索引引擎,比較著名的有:
- J ive:WEB論壇系統(tǒng);
- Eyebrows:郵件列表HTML歸檔/瀏覽/查詢系統(tǒng),本文的主要參考文檔“TheLucene search engine: Powerful, flexible, and free”作者就是EyeBrows系統(tǒng)的主要開發(fā)者之一,而EyeBrows已經(jīng)成為目前APACHE項目的主要郵件列表歸檔系統(tǒng)。
- Cocoon:基于XML的web發(fā)布框架,全文檢索部分使用了Lucene
-
Eclipse:基于Java的開放開發(fā)平臺,幫助部分的全文索引使用了Lucene
對于中文用戶來說,最關(guān)心的問題是其是否支持中文的全文檢索。但通過后面對于Lucene的結(jié)構(gòu)的介紹,你會了解到由于Lucene良好架構(gòu)設(shè)計,對中文的支持只需對其語言詞法分析接口進(jìn)行擴(kuò)展就能實現(xiàn)對中文檢索的支持。
Lucene的API接口設(shè)計的比較通用,輸入輸出結(jié)構(gòu)都很像數(shù)據(jù)庫的表==>記錄==>字段,所以很多傳統(tǒng)的應(yīng)用的文件、數(shù)據(jù)庫等都可以比較方便的映射到Lucene的存儲結(jié)構(gòu)/接口中。總體上看:可以先把Lucene當(dāng)成一個支持全文索引的數(shù)據(jù)庫系統(tǒng)。
比較一下Lucene和數(shù)據(jù)庫:
Lucene | 數(shù)據(jù)庫 |
索引數(shù)據(jù)源:doc(field1,field2...) doc(field1,field2...) |
索引數(shù)據(jù)源:record(field1,field2...) record(field1..) |
Document:一個需要進(jìn)行索引的“單元” 一個Document由多個字段組成 |
Record:記錄,包含多個字段 |
Field:字段 | Field:字段 |
Hits:查詢結(jié)果集,由匹配的Document組成 | RecordSet:查詢結(jié)果集,由多個Record組成 |
全文檢索 ≠ like "%keyword%"
通常比較厚的書籍后面常常附關(guān)鍵詞索引表(比如:北京:12, 34頁,上海:3,77頁……),它能夠幫助讀者比較快地找到相關(guān)內(nèi)容的頁碼。而數(shù)據(jù)庫索引能夠大大提高查詢的速度原理也是一樣,想像一下通過書后面的索引查找的速度要比一頁一頁地翻內(nèi)容高多少倍……而索引之所以效率高,另外一個原因是它是排好序的。對于檢索系統(tǒng)來說核心是一個排序問題。
由于數(shù)據(jù)庫索引不是為全文索引設(shè)計的,因此,使用like "%keyword%"時,數(shù)據(jù)庫索引是不起作用的,在使用like查詢時,搜索過程又變成類似于一頁頁翻書的遍歷過程了,所以對于含有模糊查詢的數(shù)據(jù)庫服務(wù)來說,LIKE對性能的危害是極大的。如果是需要對多個關(guān)鍵詞進(jìn)行模糊匹配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。
所以建立一個高效檢索系統(tǒng)的關(guān)鍵是建立一個類似于科技索引一樣的反向索引機(jī)制,將數(shù)據(jù)源(比如多篇文章)排序順序存儲的同時,有另外一個排好序的關(guān)鍵詞列表,用于存儲關(guān)鍵詞==>文章映射關(guān)系,利用這樣的映射關(guān)系索引:[關(guān)鍵詞==>出現(xiàn)關(guān)鍵詞的文章編號,出現(xiàn)次數(shù)(甚至包括位置:起始偏移量,結(jié)束偏移量),出現(xiàn)頻率],檢索過程就是把模糊查詢變成多個可以利用索引的精確查詢的邏輯組合的過程。從而大大提高了多關(guān)鍵詞查詢的效率,所以,全文檢索問題歸結(jié)到最后是一個排序問題。
由此可以看出模糊查詢相對數(shù)據(jù)庫的精確查詢是一個非常不確定的問題,這也是大部分?jǐn)?shù)據(jù)庫對全文檢索支持有限的原因。Lucene最核心的特征是通過特殊的索引結(jié)構(gòu)實現(xiàn)了傳統(tǒng)數(shù)據(jù)庫不擅長的全文索引機(jī)制,并提供了擴(kuò)展接口,以方便針對不同應(yīng)用的定制。
可以通過一下表格對比一下數(shù)據(jù)庫的模糊查詢:
Lucene全文索引引擎
數(shù)據(jù)庫
索引
將數(shù)據(jù)源中的數(shù)據(jù)都通過全文索引一一建立反向索引
對于LIKE查詢來說,數(shù)據(jù)傳統(tǒng)的索引是根本用不上的。數(shù)據(jù)需要逐個便利記錄進(jìn)行GREP式的模糊匹配,比有索引的搜索速度要有多個數(shù)量級的下降。
匹配效果
通過詞元(term)進(jìn)行匹配,通過語言分析接口的實現(xiàn),可以實現(xiàn)對中文等非英語的支持。
使用:like "%net%" 會把netherlands也匹配出來,
多個關(guān)鍵詞的模糊匹配:使用like "%com%net%":就不能匹配詞序顛倒的xxx.net..xxx.com
匹配度
有匹配度算法,將匹配程度(相似度)比較高的結(jié)果排在前面。
沒有匹配程度的控制:比如有記錄中net出現(xiàn)5詞和出現(xiàn)1次的,結(jié)果是一樣的。
結(jié)果輸出
通過特別的算法,將最匹配度最高的頭100條結(jié)果輸出,結(jié)果集是緩沖式的小批量讀取的。
返回所有的結(jié)果集,在匹配條目非常多的時候(比如上萬條)需要大量的內(nèi)存存放這些臨時結(jié)果集。
可定制性
通過不同的語言分析接口實現(xiàn),可以方便的定制出符合應(yīng)用需要的索引規(guī)則(包括對中文的支持)
沒有接口或接口復(fù)雜,無法定制
結(jié)論
高負(fù)載的模糊查詢應(yīng)用,需要負(fù)責(zé)的模糊查詢的規(guī)則,索引的資料量比較大
使用率低,模糊匹配規(guī)則簡單或者需要模糊查詢的資料量少
全文檢索和數(shù)據(jù)庫應(yīng)用最大的不同在于:讓
最相關(guān)的
頭100條結(jié)果滿足98%以上用戶的需求
Lucene的創(chuàng)新之處:
大部分的搜索(數(shù)據(jù)庫)引擎都是用B樹結(jié)構(gòu)來維護(hù)索引,索引的更新會導(dǎo)致大量的IO操作,Lucene在實現(xiàn)中,對此稍微有所改進(jìn):不是維護(hù)一個索引文件,而是在擴(kuò)展索引的時候不斷創(chuàng)建新的索引文件,然后定期的把這些新的小索引文件合并到原先的大索引中(針對不同的更新策略,批次的大小可以調(diào)整),這樣在不影響檢索的效率的前提下,提高了索引的效率。
Lucene和其他一些全文檢索系統(tǒng)/應(yīng)用的比較:
Lucene | 其他開源全文檢索系統(tǒng) | |
增量索引和批量索引 | 可以進(jìn)行增量的索引(Append),可以對于大量數(shù)據(jù)進(jìn)行批量索引,并且接口設(shè)計用于優(yōu)化批量索引和小批量的增量索引。 | 很多系統(tǒng)只支持批量的索引,有時數(shù)據(jù)源有一點增加也需要重建索引。 |
數(shù)據(jù)源 | Lucene沒有定義具體的數(shù)據(jù)源,而是一個文檔的結(jié)構(gòu),因此可以非常靈活的適應(yīng)各種應(yīng)用(只要前端有合適的轉(zhuǎn)換器把數(shù)據(jù)源轉(zhuǎn)換成相應(yīng)結(jié)構(gòu)), | 很多系統(tǒng)只針對網(wǎng)頁,缺乏其他格式文檔的靈活性。 |
索引內(nèi)容抓取 | Lucene的文檔是由多個字段組成的,甚至可以控制那些字段需要進(jìn)行索引,那些字段不需要索引,近一步索引的字段也分為需要分詞和不需要分詞的類型: ?? 需要進(jìn)行分詞的索引,比如:標(biāo)題,文章內(nèi)容字段 ?? 不需要進(jìn)行分詞的索引,比如:作者/日期字段 |
缺乏通用性,往往將文檔整個索引了 |
語言分析 | 通過語言分析器的不同擴(kuò)展實現(xiàn): 可以過濾掉不需要的詞:an the of 等, 西文語法分析:將jumps jumped jumper都?xì)w結(jié)成jump進(jìn)行索引/檢索 非英文支持:對亞洲語言,阿拉伯語言的索引支持 |
缺乏通用接口實現(xiàn) |
查詢分析 | 通過查詢分析接口的實現(xiàn),可以定制自己的查詢語法規(guī)則: 比如: 多個關(guān)鍵詞之間的 + - and or關(guān)系等 |
|
并發(fā)訪問 | 能夠支持多用戶的使用 |
關(guān)于亞洲語言的的切分詞問題(Word Segment)
對于中文來說,全文索引首先還要解決一個語言分析的問題,對于英文來說,語句中單詞之間是天然通過空格分開的,但亞洲語言的中日韓文語句中的字是一個字挨一個,所有,首先要把語句中按“詞”進(jìn)行索引的話,這個詞如何切分出來就是一個很大的問題。
首先,肯定不能用單個字符作(si-gram)為索引單元,否則查“上海”時,不能讓含有“海上”也匹配。
但一句話:“北京天安門”,計算機(jī)如何按照中文的語言習(xí)慣進(jìn)行切分呢?
“北京 天安門” 還是“北 京 天安門”?讓計算機(jī)能夠按照語言習(xí)慣進(jìn)行切分,往往需要機(jī)器有一個比較豐富的詞庫才能夠比較準(zhǔn)確的識別出語句中的單詞。
另外一個解決的辦法是采用自動切分算法:將單詞按照2元語法(bigram)方式切分出來,比如:
"北京天安門" ==> "北京 京天 天安 安門"。
這樣,在查詢的時候,無論是查詢"北京" 還是查詢"天安門",將查詢詞組按同樣的規(guī)則進(jìn)行切分:"北京","天安安門",多個關(guān)鍵詞之間按與"and"的關(guān)系組合,同樣能夠正確地映射到相應(yīng)的索引中。這種方式對于其他亞洲語言:韓文,日文都是通用的。
基于自動切分的最大優(yōu)點是沒有詞表維護(hù)成本,實現(xiàn)簡單,缺點是索引效率低,但對于中小型應(yīng)用來說,基于2元語法的切分還是夠用的。基于2元切分后的索引一般大小和源文件差不多,而對于英文,索引文件一般只有原文件的30%-40%不同,
|
自動切分 | 詞表切分 |
實現(xiàn) | 實現(xiàn)非常簡單 | 實現(xiàn)復(fù)雜 |
查詢 | 增加了查詢分析的復(fù)雜程度, | 適于實現(xiàn)比較復(fù)雜的查詢語法規(guī)則 |
存儲效率 | 索引冗余大,索引幾乎和原文一樣大 | 索引效率高,為原文大小的30%左右 |
維護(hù)成本 | 無詞表維護(hù)成本 | 詞表維護(hù)成本非常高:中日韓等語言需要分別維護(hù)。 還需要包括詞頻統(tǒng)計等內(nèi)容 |
適用領(lǐng)域 | 嵌入式系統(tǒng):運行環(huán)境資源有限 分布式系統(tǒng):無詞表同步問題 多語言環(huán)境:無詞表維護(hù)成本 |
對查詢和存儲效率要求高的專業(yè)搜索引擎 |
目前比較大的搜索引擎的語言分析算法一般是基于以上2個機(jī)制的結(jié)合。關(guān)于中文的語言分析算法,大家可以在Google查關(guān)鍵詞"wordsegment search"能找到更多相關(guān)的資料。
下載:http://jakarta.apache.org/lucene/
注意:Lucene中的一些比較復(fù)雜的詞法分析是用JavaCC生成的(JavaCC:JavaCompilerCompiler,純Java的詞法分析生成器),所以如果從源代碼編譯或需要修改其中的QueryParser、定制自己的詞法分析器,還需要從https://javacc.dev.java.net/下載javacc。
lucene的組成結(jié)構(gòu):對于外部應(yīng)用來說索引模塊(index)和檢索模塊(search)是主要的外部應(yīng)用入口
org.apache.Lucene.search/ | 搜索入口 |
org.apache.Lucene.index/ | 索引入口 |
org.apache.Lucene.analysis/ | 語言分析器 |
org.apache.Lucene.queryParser/ | 查詢分析器 |
org.apache.Lucene.document/ | 存儲結(jié)構(gòu) |
org.apache.Lucene.store/? | 底層IO/存儲結(jié)構(gòu) |
org.apache.Lucene.util/ | 一些公用的數(shù)據(jù)結(jié)構(gòu) |
簡單的例子演示一下Lucene的使用方法:
索引過程:從命令行讀取文件名(多個),將文件分路徑(path字段)和內(nèi)容(body字段)2個字段進(jìn)行存儲,并對內(nèi)容進(jìn)行全文索引:索引的單位是Document對象,每個Document對象包含多個字段Field對象,針對不同的字段屬性和數(shù)據(jù)輸出的需求,對字段還可以選擇不同的索引/存儲字段規(guī)則,列表如下:方法 | 切詞 | 索引 | 存儲 | 用途 |
---|---|---|---|---|
Field.Text(String name, String value) | Yes | Yes | Yes | 切分詞索引并存儲,比如:標(biāo)題,內(nèi)容字段 |
Field.Text(String name, Reader value) | Yes | Yes | No | 切分詞索引不存儲,比如:META信息, 不用于返回顯示,但需要進(jìn)行檢索內(nèi)容 |
Field.Keyword(String name, String value) | No | Yes | Yes | 不切分索引并存儲,比如:日期字段 |
Field.UnIndexed(String name, String value) | No | No | Yes | 不索引,只存儲,比如:文件路徑 |
Field.UnStored(String name, String value) | Yes | Yes | No | 只全文索引,不存儲 |
public class IndexFiles {
//使用方法:: IndexFiles [索引輸出目錄] [索引的文件列表] ...
public static void main(String[] args) throws Exception {
String indexPath = args[0];
IndexWriter writer;
//用指定的語言分析器構(gòu)造一個新的寫索引器(第3個參數(shù)表示是否為追加索引)
writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);
for (int i=1; i<args.length; i++) {
System.out.println("Indexing file " + args[i]);
InputStream is = new FileInputStream(args[i]);
//構(gòu)造包含2個字段Field的Document對象
//一個是路徑path字段,不索引,只存儲
//一個是內(nèi)容body字段,進(jìn)行全文索引,并存儲
Document doc = new Document();
doc.add(Field.UnIndexed("path", args[i]));
doc.add(Field.Text("body", (Reader) new InputStreamReader(is)));
//將文檔寫入索引
writer.addDocument(doc);
is.close();
};
//關(guān)閉寫索引器
writer.close();
}
}
索引過程中可以看到:
- 語言分析器提供了抽象的接口,因此語言分析(Analyser)是可以定制的,雖然lucene缺省提供了2個比較通用的分析器SimpleAnalyser和StandardAnalyser,這2個分析器缺省都不支持中文,所以要加入對中文語言的切分規(guī)則,需要修改這2個分析器。
- Lucene并沒有規(guī)定數(shù)據(jù)源的格式,而只提供了一個通用的結(jié)構(gòu)(Document對象)來接受索引的輸入,因此輸入的數(shù)據(jù)源可以是:數(shù)據(jù)庫,WORD文檔,PDF文檔,HTML文檔……只要能夠設(shè)計相應(yīng)的解析轉(zhuǎn)換器將數(shù)據(jù)源構(gòu)造成成Docuement對象即可進(jìn)行索引。
- 對于大批量的數(shù)據(jù)索引,還可以通過調(diào)整IndexerWrite的文件合并頻率屬性(mergeFactor)來提高批量索引的效率。
檢索過程和結(jié)果顯示:
搜索結(jié)果返回的是Hits對象,可以通過它再訪問Document==>Field中的內(nèi)容。
假設(shè)根據(jù)body字段進(jìn)行全文檢索,可以將查詢結(jié)果的path字段和相應(yīng)查詢的匹配度(score)打印出來,
public class Search {在整個檢索過程中,語言分析器,查詢分析器,甚至搜索器(Searcher)都是提供了抽象的接口,可以根據(jù)需要進(jìn)行定制。
public static void main(String[] args) throws Exception {
String indexPath = args[0], queryString = args[1];
//指向索引目錄的搜索器
Searcher searcher = new IndexSearcher(indexPath);
//查詢解析器:使用和索引同樣的語言分析器
Query query = QueryParser.parse(queryString, "body",
new SimpleAnalyzer());
//搜索結(jié)果使用Hits存儲
Hits hits = searcher.search(query);
//通過hits可以訪問到相應(yīng)字段的數(shù)據(jù)和查詢的匹配度
for (int i=0; i<hits.length(); i++) {
System.out.println(hits.doc(i).get("path") + "; Score: " +
hits.score(i));
};
}
}
簡化的查詢分析器
個人感覺lucene成為JAKARTA項目后,畫在了太多的時間用于調(diào)試日趨復(fù)雜QueryParser,而其中大部分是大多數(shù)用戶并不很熟悉的,目前LUCENE支持的語法:
Query ::= ( Clause )*
Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")")
中間的邏輯包括:and or + - &&||等符號,而且還有"短語查詢"和針對西文的前綴/模糊查詢等,個人感覺對于一般應(yīng)用來說,這些功能有一些華而不實,其實能夠?qū)崿F(xiàn)目前類似于Google的查詢語句分析功能其實對于大多數(shù)用戶來說已經(jīng)夠了。所以,Lucene早期版本的QueryParser仍是比較好的選擇。
添加修改刪除指定記錄(Document)
Lucene提供了索引的擴(kuò)展機(jī)制,因此索引的動態(tài)擴(kuò)展應(yīng)該是沒有問題的,而指定記錄的修改也似乎只能通過記錄的刪除,然后重新加入實現(xiàn)。如何刪除指定的記錄呢?刪除的方法也很簡單,只是需要在索引時根據(jù)數(shù)據(jù)源中的記錄ID專門另建索引,然后利用IndexReader.delete(Termterm)方法通過這個記錄ID刪除相應(yīng)的Document。
根據(jù)某個字段值的排序功能
lucene缺省是按照自己的相關(guān)度算法(score)進(jìn)行結(jié)果排序的,但能夠根據(jù)其他字段進(jìn)行結(jié)果排序是一個在LUCENE的開發(fā)郵件列表中經(jīng)常提到的問題,很多原先基于數(shù)據(jù)庫應(yīng)用都需要除了基于匹配度(score)以外的排序功能。而從全文檢索的原理我們可以了解到,任何不基于索引的搜索過程效率都會導(dǎo)致效率非常的低,如果基于其他字段的排序需要在搜索過程中訪問存儲字段,速度回大大降低,因此非常是不可取的。
但這里也有一個折中的解決方法:在搜索過程中能夠影響排序結(jié)果的只有索引中已經(jīng)存儲的docID和score這2個參數(shù),所以,基于score以外的排序,其實可以通過將數(shù)據(jù)源預(yù)先排好序,然后根據(jù)docID進(jìn)行排序來實現(xiàn)。這樣就避免了在LUCENE搜索結(jié)果外對結(jié)果再次進(jìn)行排序和在搜索過程中訪問不在索引中的某個字段值。
這里需要修改的是IndexSearcher中的HitCollector過程:
...
scorer.score(new HitCollector() {
private float minScore = 0.0f;
public final void collect(int doc, float score) {
if (score > 0.0f && // ignore zeroed buckets
(bits==null || bits.get(doc))) { // skip docs not in bits
totalHits[0]++;
if (score >= minScore) {
/* 原先:Lucene將docID和相應(yīng)的匹配度score例入結(jié)果命中列表中:
* hq.put(new ScoreDoc(doc, score)); // update hit queue
* 如果用doc 或 1/doc 代替 score,就實現(xiàn)了根據(jù)docID順排或逆排
* 假設(shè)數(shù)據(jù)源索引時已經(jīng)按照某個字段排好了序,而結(jié)果根據(jù)docID排序也就實現(xiàn)了
* 針對某個字段的排序,甚至可以實現(xiàn)更復(fù)雜的score和docID的擬合。
*/
hq.put(new ScoreDoc(doc, (float) 1/doc ));
if (hq.size() > nDocs) { // if hit queue overfull
hq.pop(); // remove lowest in hit queue
minScore = ((ScoreDoc)hq.top()).score; // reset minScore
}
}
}
}
}, reader.maxDoc());
更通用的輸入輸出接口
雖然lucene沒有定義一個確定的輸入文檔格式,但越來越多的人想到使用一個標(biāo)準(zhǔn)的中間格式作為Lucene的數(shù)據(jù)導(dǎo)入接口,然后其他數(shù)據(jù),比如PDF只需要通過解析器轉(zhuǎn)換成標(biāo)準(zhǔn)的中間格式就可以進(jìn)行數(shù)據(jù)索引了。這個中間格式主要以XML為主,類似實現(xiàn)已經(jīng)不下4,5個:
數(shù)據(jù)源: WORD PDF HTML DB other
\ | | | /
XML中間格式
|
Lucene INDEX
目前還沒有針對MSWord文檔的解析器,因為Word文檔和基于ASCII的RTF文檔不同,需要使用COM對象機(jī)制解析。這個是我在Google上查的相關(guān)資料:http://www.intrinsyc.com/products/enterprise_applications.asp
另外一個辦法就是把Word文檔轉(zhuǎn)換成text:http://www.winfield.demon.nl/index.html
索引過程優(yōu)化
索引一般分2種情況,一種是小批量的索引擴(kuò)展,一種是大批量的索引重建。在索引過程中,并不是每次新的DOC加入進(jìn)去索引都重新進(jìn)行一次索引文件的寫入操作(文件I/O是一件非常消耗資源的事情)。
Lucene先在內(nèi)存中進(jìn)行索引操作,并根據(jù)一定的批量進(jìn)行文件的寫入。這個批次的間隔越大,文件的寫入次數(shù)越少,但占用內(nèi)存會很多。反之占用內(nèi)存少,但文件IO操作頻繁,索引速度會很慢。在IndexWriter中有一個MERGE_FACTOR參數(shù)可以幫助你在構(gòu)造索引器后根據(jù)應(yīng)用環(huán)境的情況充分利用內(nèi)存減少文件的操作。根據(jù)我的使用經(jīng)驗:缺省Indexer是每20條記錄索引后寫入一次,每將MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
搜索過程優(yōu)化
lucene支持內(nèi)存索引:這樣的搜索比基于文件的I/O有數(shù)量級的速度提升。
http://www.onjava.com/lpt/a/3273
而盡可能減少IndexSearcher的創(chuàng)建和對搜索結(jié)果的前臺的緩存也是必要的。
Lucene面向全文檢索的優(yōu)化在于首次索引檢索后,并不把所有的記錄(Document)具體內(nèi)容讀取出來,而起只將所有結(jié)果中匹配度最高的頭100條結(jié)果(TopDocs)的ID放到結(jié)果集緩存中并返回,這里可以比較一下數(shù)據(jù)庫檢索:如果是一個10,000條的數(shù)據(jù)庫檢索結(jié)果集,數(shù)據(jù)庫是一定要把所有記錄內(nèi)容都取得以后再開始返回給應(yīng)用結(jié)果集的。所以即使檢索匹配總數(shù)很多,Lucene的結(jié)果集占用的內(nèi)存空間也不會很多。對于一般的模糊檢索應(yīng)用是用不到這么多的結(jié)果的,頭100條已經(jīng)可以滿足90%以上的檢索需求。
如果首批緩存結(jié)果數(shù)用完后還要讀取更后面的結(jié)果時Searcher會再次檢索并生成一個上次的搜索緩存數(shù)大1倍的緩存,并再重新向后抓取。所以如果構(gòu)造一個Searcher去查1-120條結(jié)果,Searcher其實是進(jìn)行了2次搜索過程:頭100條取完后,緩存結(jié)果用完,Searcher重新檢索再構(gòu)造一個200條的結(jié)果緩存,依此類推,400條緩存,800條緩存。由于每次Searcher對象消失后,這些緩存也訪問那不到了,你有可能想將結(jié)果記錄緩存下來,緩存數(shù)盡量保證在100以下以充分利用首次的結(jié)果緩存,不讓Lucene浪費多次檢索,而且可以分級進(jìn)行結(jié)果緩存。
Lucene的另外一個特點是在收集結(jié)果的過程中將匹配度低的結(jié)果自動過濾掉了。這也是和數(shù)據(jù)庫應(yīng)用需要將搜索的結(jié)果全部返回不同之處。
- 支持中文的Tokenizer:這里有2個版本,一個是通過JavaCC生成的,對CJK部分按一個字符一個TOKEN索引,另外一個是從SimpleTokenizer改寫的,對英文支持?jǐn)?shù)字和字母TOKEN,對中文按迭代索引。
- 基于XML數(shù)據(jù)源的索引器:XMLIndexer,因此所有數(shù)據(jù)源只要能夠按照DTD轉(zhuǎn)換成指定的XML,就可以用XMLIndxer進(jìn)行索引了。
- 根據(jù)某個字段排序:按記錄索引順序排序結(jié)果的搜索器:IndexOrderSearcher,因此如果需要讓搜索結(jié)果根據(jù)某個字段排序,可以讓數(shù)據(jù)源先按某個字段排好序(比如:PriceField),這樣索引后,然后在利用這個按記錄的ID順序檢索的搜索器,結(jié)果就是相當(dāng)于是那個字段排序的結(jié)果了。
Luene的確是一個面對對象設(shè)計的典范
- 所有的問題都通過一個額外抽象層來方便以后的擴(kuò)展和重用:你可以通過重新實現(xiàn)來達(dá)到自己的目的,而對其他模塊而不需要;
- 簡單的應(yīng)用入口Searcher, Indexer,并調(diào)用底層一系列組件協(xié)同的完成搜索任務(wù);
- 所有的對象的任務(wù)都非常專一:比如搜索過程:QueryParser分析將查詢語句轉(zhuǎn)換成一系列的精確查詢的組合(Query),通過底層的索引讀取結(jié)構(gòu)IndexReader進(jìn)行索引的讀取,并用相應(yīng)的打分器給搜索結(jié)果進(jìn)行打分/排序等。所有的功能模塊原子化程度非常高,因此可以通過重新實現(xiàn)而不需要修改其他模塊。?
- 除了靈活的應(yīng)用接口設(shè)計,Lucene還提供了一些適合大多數(shù)應(yīng)用的語言分析器實現(xiàn)(SimpleAnalyser,StandardAnalyser),這也是新用戶能夠很快上手的重要原因之一。
這些優(yōu)點都是非常值得在以后的開發(fā)中學(xué)習(xí)借鑒的。作為一個通用工具包,Lunece的確給予了需要將全文檢索功能嵌入到應(yīng)用中的開發(fā)者很多的便利。
此外,通過對Lucene的學(xué)習(xí)和使用,我也更深刻地理解了為什么很多數(shù)據(jù)庫優(yōu)化設(shè)計中要求,比如:
- 盡可能對字段進(jìn)行索引來提高查詢速度,但過多的索引會對數(shù)據(jù)庫表的更新操作變慢,而對結(jié)果過多的排序條件,實際上往往也是性能的殺手之一。
- 很多商業(yè)數(shù)據(jù)庫對大批量的數(shù)據(jù)插入操作會提供一些優(yōu)化參數(shù),這個作用和索引器的merge_factor的作用是類似的,
- 20%/80%原則:查的結(jié)果多并不等于質(zhì)量好,尤其對于返回結(jié)果集很大,如何優(yōu)化這頭幾十條結(jié)果的質(zhì)量往往才是最重要的。
- 盡可能讓應(yīng)用從數(shù)據(jù)庫中獲得比較小的結(jié)果集,因為即使對于大型數(shù)據(jù)庫,對結(jié)果集的隨機(jī)訪問也是一個非常消耗資源的操作。
參考資料:
Apache: Lucene Project
http://jakarta.apache.org/lucene/
Lucene開發(fā)/用戶郵件列表歸檔
Lucene-dev@jakarta.apache.org
Lucene-user@jakarta.apache.org
The Lucene search engine: Powerful, flexible, and free
http://www.javaworld.com/javaworld/jw-09-2000/jw-0915-Lucene_p.html
Lucene Tutorial
http://www.darksleep.com/puff/lucene/lucene.html
Notes on distributed searching with Lucene
http://home.clara.net/markharwood/lucene/
中文語言的切分詞
http://www.google.com/search?sourceid=navclient&hl=zh-CN&q=chinese+word+segment
搜索引擎工具介紹
http://searchtools.com/
Lucene作者Cutting的幾篇論文和專利
http://lucene.sourceforge.net/publications.html?
Lucene的.NET實現(xiàn):dotLucene
http://sourceforge.net/projects/dotlucene/
Lucene作者Cutting的另外一個項目:基于Java的搜索引擎Nutch
http://www.nutch.org/ ? http://sourceforge.net/projects/nutch/
關(guān)于基于詞表和N-Gram的切分詞比較
http://china.nikkeibp.co.jp/cgi-bin/china/news/int/int200302100112.html
2005-01-08 Cutting在Pisa大學(xué)做的關(guān)于Lucene的講座:非常詳細(xì)的Lucene架構(gòu)解說
XML數(shù)據(jù)源對象是一個ActiveX控件,允許你在XML文件和HTML頁面之間操作數(shù)據(jù)。本文將向你展示如何從各種XML數(shù)據(jù)源中提取數(shù)據(jù),以及如何使用JavaScript顯示這些數(shù)據(jù)。?
XML數(shù)據(jù)源對象DSO是一個微軟ActiveX控件,構(gòu)建在微軟IE4以后的版本上。這個對象允許你把一個外部的XML文件或者嵌入HTML文件中的內(nèi)容提取到HTML頁面中。?
你可以在一個Web頁面中使用XML?-?DSO從一個外部XML文件中選取內(nèi)容,從嵌入Web頁面的XML中提取XML數(shù)據(jù),然后使用JavaScript操作這些數(shù)據(jù)。然而,并不建議在Internet中使用這個對象,因為DSO只能工作在MSIE?4以上的瀏覽器中,因此這可能會帶來一些兼容性問題。?所以,在企業(yè)內(nèi)部網(wǎng)使用XML-DSO是很合適的。?
開始
為了初始化XML?-?DSO對象,我們使用<OBJECT>標(biāo)記。?用于XML-DSO的CLASSID是:
CLSID:550dda30-0541-11d2-9ca9-0060b0ec3d39
這ID唯一標(biāo)識XML-DSO。使用下面的代碼在一個Web頁面中初始化這個控件:?
<OBJECT?ID="SomeID"?CLASSID="CLSID:550dda30-0541-11d2-9ca9-0060b0ec3d39"></OBJECT>
雖然大部分對象需要許多參數(shù)與之相關(guān)聯(lián),但是XML-DSO不需要任何參數(shù)。?
使用一個XML數(shù)據(jù)島析取數(shù)據(jù)
首先,通過使用<XML>標(biāo)記包含一個XML數(shù)據(jù)島。其次,給它分配一個ID,xmldb?--以備以后使用。?數(shù)據(jù)實際上是使用HTML標(biāo)記:<ALT>,<SPAN>,<DIV>等等提取的。代碼列表1中的代碼使用了<SPAN>標(biāo)記。datasrc屬性指定了你想從中提取數(shù)據(jù)的那個數(shù)據(jù)島。datafld屬性指定了你想要的數(shù)據(jù)的XML標(biāo)記。所以,第一個<SPAN>提取名稱,而第二<SPAN>提取性別。?
代碼列表1:
<!--?example1.htm?-->
<html>
<head>
<title>XML?DSO-example1.htm</title>
</head>
<body?bgcolor="#FFFFFF">
<xml?id="xmldb">
<db>
<member>
<name>Premshree?Pillai<name>
<sex>male</sex>
</member>
<member>
<name>Vinod</name>
<sex>male</sex>
</member>
</db>
</xml>
<span?datasrc="#xmldb"?datafld="name"<</span>
<br>
<span?datasrc="#xmldb"?datafld="sex"></span>
</body>
</html>??
注意這段代碼沒有初始化一個XML-DSO對象。這是因為XML數(shù)據(jù)島的使用中已經(jīng)隱式地創(chuàng)建了一個。輸出應(yīng)為:?
Premshree?Pillai
male
注意在XML數(shù)據(jù)島中有兩個<name>和<sex>標(biāo)記。使用這個方法,你只能提取這些標(biāo)記中的第一個實例。代碼列表2中的代碼使用<TABLE>標(biāo)記提取所有的實例:?
輸出將是:?
?
?????Name?????Sex?
?????-----------------------------------
?????Premshree?Pillai??male?
?????Vinod??????male?
在代碼列表2中,<TABLE>標(biāo)記使用<TD>標(biāo)記內(nèi)的<DIV>標(biāo)記提取數(shù)據(jù)。表格將自動重復(fù)<member>(<name>和<sex>的母標(biāo)記)的每個實例。?
代碼列表2:
<!--?example2.htm?-->
<html>
<head>
<title>XML?DSO-example2.htm</title>
</head>
<body?bgcolor="#FFFFFF">
<xml?id="xmldb">
<db>
<member>
<name>Premshree?Pillai<name>
<sex>male</sex>
</member>
<member>
<name>Vinod</name>
<sex>male</sex>
</member>
</db>
</xml>
<table?datasrc="#xmldb"?border="1">
<thead>
<th>Name</th>
<th>Sex</th>
</thead>
<tr>
<td><div?datafld="name"></div></td>
<td><div?datafld="sex"></div></td>
</tr>
</table>
</body>
</html>
使用外部XML文件提取數(shù)據(jù)
為了使用XML-DSO加載一個外部XML文件,你必須顯式的包含這個對象并且使用一些JavaScript。?
首先創(chuàng)建一個XML-DSO對象,使用ID?myXML。添加寬度和高度屬性到<OBJECT>標(biāo)記中,然后設(shè)置它們的值為0。這保證XML-DSO對象不會占據(jù)你的Web頁面的任何空間。?
其次,使用datasrc創(chuàng)建一個象myXML一樣的表--類似于代碼列表2中一樣。代碼使用<DIV>標(biāo)記(在TD標(biāo)記之)提取數(shù)據(jù),使用datafld作為第一欄的信息,并且使用URL作為第二欄。添加<SCRIPT>標(biāo)記,因為在這里,外部的XML使用Java腳本顯式地聲明你想要加載的XML文件。?
設(shè)置變量xmlDso為myXML.XMLDocument。myXML引用你已經(jīng)創(chuàng)建的對象。接下來,使用XML-DSO的load()方法加載example3.xml。文件example3.xml連接到對象myXML上。?
<!--?example3.xml?-->
<?xml?version="1.0"??>
<ticker>
<item>
<message>JavaScript?Ticker?using?XML?DSO</message>
<URL>http://someURL.com</URL>
</item>
</ticker>?
現(xiàn)在,研究一下下面的HTML頁面:?
<!--?example3.htm?-->
<html>
<head>
<title>XML?DSO-example3.htm</title>
<script?language="JavaScript">
function?load()?{
var?xmlDso=myXML.XMLDocument;
xmlDso.load("example3.xml");
}
</script>
</head>
<body?bgcolor="#FFFFFF"?onLoad="load()">
<o(jì)bject?id="myXML"?CLASSID="clsid:550dda30-0541-11d2-9ca9-0060b0ec3d39"?
width="0"?height="0"></object>
<table?datasrc="#myXML"?border="1">
<thead>
<th>Message</th>
<th>URL</th>
</thead>
<tr>
<td><div?datafld="message"></div></td>
<td><div?datafld="URL"></div></td>
</tr>
</table>
</body>
</html>?
輸出應(yīng)是:?
Message?URL
JavaScript?Ticker?using?XML?DSO?http://someURL.com
上面的腳本非常特殊化。下面給出一個更一般的腳本:?
<script?language="JavaScript">
var?xmlDso;
function?load(xmlFile,?objName)?{
eval('xmlDso='+objName+'.XMLDocument');
xmlDso.load(xmlFile);
}
</script>
Now,?to?load?any?XML?file?use:?
load("SomeXMLFile.xml","anyXmlDsoObject");
使用XML-DSO和JavaScript
假設(shè)你有一個包含姓名、電子郵件地址和電話號碼的XML文件。你想使用它構(gòu)建一個應(yīng)用程序,顯示每個人的檔案--一次顯示一個。用戶將使用"Next"和"Previous"按鈕瀏覽每個人的數(shù)據(jù)。Javascript可以幫助你實現(xiàn)這個目的。?
下面的代碼使用記錄集方法把文件中所有的數(shù)據(jù)保存到一個變量memberSet中。moveNext()方法指向下一個數(shù)據(jù)項(下一行)。腳本然后載入XML文件example4.xml,把記錄保存到變量memberSet中。第一個記錄將被顯示,但是memberSet.moveNext()指向文件中相對于前一個指定數(shù)據(jù)的下一個記錄。?
<!--?example4.xml?-->
<?xml?version="1.0"??>
<myDB>
<member>
<name>Premshree?Pillai</name>
<sex>male</sex>
</member>
<member>
<name>Vinod</name>
<sex>male</sex>
</member>
<member>
<name>Santhosh</name>
<sex>male</sex>
</member>
</myDB>?
這里是相應(yīng)的HTML文件:?
<!--?example4.htm?-->
<html>
<head>
<title>XML?DSO-example4.htm</title>
<script?language="JavaScript">
function?load()?{
var?xmlDso=myDB.XMLDocument;
xmlDso.load("example4.xml");
/*?Get?the?complete?record?set?*/
var?memberSet=myDB.recordset;
/*?Go?to?next?data?*/
memberSet.moveNext();
}
</script>
</head>
<body?bgcolor="#FFFFFF"?onLoad="load()">
<o(jì)bject?id="myDB"?CLASSID="clsid:550dda30-0541-11d2-9ca9-0060b0ec3d39"?
width="0"?height="0"></object>
<span?datasrc="#myDB"?datafld="name"></span>
</body>
</html>?
輸出應(yīng)是:?
Vinod
下面給出更多使用JavaScript操作XML-DSO的方法:?
·?movePrevious():?指向前一個數(shù)據(jù)項。?
·?moveFirst():?指向第一個數(shù)據(jù)項。?
·?moveLast():?指向最后一個數(shù)據(jù)項。?
·?EOF:?這個屬性用來檢測我們是否已經(jīng)到達(dá)數(shù)據(jù)記錄的底部。
使用XML-DSO和JavaScript
假設(shè)你有一個包含姓名、電子郵件地址和電話號碼的XML文件。你想使用它構(gòu)建一個應(yīng)用程序,顯示每個人的檔案--一次顯示一個。用戶將使用"Next"和"Previous"按鈕瀏覽每個人的數(shù)據(jù)。Javascript可以幫助你實現(xiàn)這個目的。?
下面的代碼使用記錄集方法把文件中所有的數(shù)據(jù)保存到一個變量memberSet中。moveNext()方法指向下一個數(shù)據(jù)項(下一行)。腳本然后載入XML文件example4.xml,把記錄保存到變量memberSet中。第一個記錄將被顯示,但是memberSet.moveNext()指向文件中相對于前一個指定數(shù)據(jù)的下一個記錄。?
<!--?example4.xml?-->
<?xml?version="1.0"??>
<myDB>
<member>
<name>Premshree?Pillai</name>
<sex>male</sex>
</member>
<member>
<name>Vinod</name>
<sex>male</sex>
</member>
<member>
<name>Santhosh</name>
<sex>male</sex>
</member>
</myDB>??
這里是相應(yīng)的HTML文件:?
<!--?example4.htm?-->
<html>
<head>
<title>XML?DSO-example4.htm</title>
<script?language="JavaScript">
function?load()?{
var?xmlDso=myDB.XMLDocument;
xmlDso.load("example4.xml");
/*?Get?the?complete?record?set?*/
var?memberSet=myDB.recordset;
/*?Go?to?next?data?*/
memberSet.moveNext();
}
</script>
</head>
<body?bgcolor="#FFFFFF"?onLoad="load()">
<o(jì)bject?id="myDB"?CLASSID="clsid:550dda30-0541-11d2-9ca9-0060b0ec3d39"?
width="0"?height="0"></object>
<span?datasrc="#myDB"?datafld="name"></span>
</body>
</html>?
輸出應(yīng)是:?
Vinod
下面給出更多使用JavaScript操作XML-DSO的方法:?
·?movePrevious():?指向前一個數(shù)據(jù)項。?
·?moveFirst():?指向第一個數(shù)據(jù)項。?
·?moveLast():?指向最后一個數(shù)據(jù)項。?
·?EOF:?這個屬性用來檢測我們是否已經(jīng)到達(dá)數(shù)據(jù)記錄的底部。?
initTicker()首先檢查是否有IE?4+。如果瀏覽器是IE4+,這個XML文件被作為一個參數(shù)被傳遞并載入。如果定時器失敗了,那么調(diào)用xmlDsoTicker()函數(shù)。xmlDsoTicker()除了xmlFile參數(shù)以外,和initTicker()有相同的參數(shù),因為XML文件已經(jīng)被載入。xmlDsoTicker()檢查變量counter(初始值為maxMsgs)是否小于maxMsgs-1。如果是,moveNext()方法指向tickerSet中下一個數(shù)據(jù)項。?
HTML頁面的BODY包含下面的代碼:?
<a?href=""?datasrc="#ticker"?datafld="URL"?class="tickerStyle">
<span?datasrc="#ticker"?datafld="message"></span>
</a>?
1、數(shù)據(jù)交換?
用XML在應(yīng)用程序和公司之間作數(shù)據(jù)交換已不是什么秘密了,毫無疑問應(yīng)被列為第一位。那么為什么XML在這個領(lǐng)域里的地位這么重要呢?原因就是XML使用元素和屬性來描述數(shù)據(jù)。在數(shù)據(jù)傳送過程中,XML始終保留了諸如父/子關(guān)系這樣的數(shù)據(jù)結(jié)構(gòu)。幾個應(yīng)用程序可以共享和解析同一個XML文件,不必使用傳統(tǒng)的字符串解析或拆解過程。相反,普通文件不對每個數(shù)據(jù)段做描述(除了在頭文件中),也不保留數(shù)據(jù)關(guān)系結(jié)構(gòu)。使?用XML做數(shù)據(jù)交換可以使應(yīng)用程序更具有彈性,因為可以用位置(與普通文件一樣)或用元素名(從數(shù)據(jù)庫)來存取XML數(shù)據(jù)。?
2、Web服務(wù)?
Web服務(wù)是最令人激動的革命之一,它讓使用不同系統(tǒng)和不同編程語言的人們能夠相互交流和分享數(shù)據(jù)。其基礎(chǔ)在于Web服務(wù)器用XML在系統(tǒng)之間交換數(shù)據(jù)。交換數(shù)據(jù)通常用XML標(biāo)記,能使協(xié)議取得規(guī)范一致,比如在簡單對象處理協(xié)議(Simple?Object?Access?Protocol,?SOAP)平臺上。SOAP可以在用不同編程語言構(gòu)造的對象之間傳遞消息。這意味著一個C#對象能夠與一個Java對象進(jìn)行通訊。這種通訊甚至可以發(fā)生在運行于不同操作系統(tǒng)上的對象之間。DCOM,?CORBA或Java?RMI只能在緊密耦合的對象之間傳遞消息,SOAP則可在松耦合對象之間傳遞消息。?
3、內(nèi)容管理?
XML只用元素和屬性來描述數(shù)據(jù),而不提供數(shù)據(jù)的顯示方法。這樣,XML就提供了一個優(yōu)秀的方法來標(biāo)記獨立于平臺和語言的內(nèi)容。使用象XSLT這樣的語言能夠輕易地將XML文件轉(zhuǎn)換成各種格式文件,比如HTML,?WML,?PDF,?flat?file,?EDI,?等等。XML具有的能夠運行于不同系統(tǒng)平臺之間和轉(zhuǎn)換成不同格式目標(biāo)文件的能力使得它成為內(nèi)容管理應(yīng)用系統(tǒng)中的優(yōu)秀選擇。??
4、Web集成?
現(xiàn)在有越來越多的設(shè)備也支持XML了。使得Web開發(fā)商可以在個人電子助理和瀏覽器之間用XML來傳遞數(shù)據(jù)。為什么將XML文本直接送進(jìn)這樣的設(shè)備去呢?這樣作的目的是讓用戶更多地自己掌握數(shù)據(jù)顯示方式,更能體驗到實踐的快樂。常規(guī)的客戶/服務(wù)(C/S)方式為了獲得數(shù)據(jù)排序或更換顯示格式,必須向服務(wù)器發(fā)出申請;而XML則可以直接處理數(shù)據(jù),不必經(jīng)過向服務(wù)器申請查詢-返回結(jié)果這樣的雙向“旅程”,同時在設(shè)備也不需要配制數(shù)據(jù)庫。甚至還可以對設(shè)備上的XML文件進(jìn)行修改并將結(jié)果返回給服務(wù)器。想像一下,一臺具有互聯(lián)網(wǎng)功能并支持XML的電冰箱將會給市場帶來多么大的沖擊吧。你從此不必早起去取牛奶了!?
5、配制?
許多應(yīng)用都將配制數(shù)據(jù)存儲在各種文件里,比如.INI文件。雖然這樣的文件格式已經(jīng)使用多年并一直很好用,但是XML還是以更為優(yōu)秀的方式為應(yīng)用程序標(biāo)記配制數(shù)據(jù)。使用NET里的類,如XmlDocument和XmlTextReader,將配制數(shù)據(jù)標(biāo)記為XML格式,能使其更具可讀性,并能方便地集成到應(yīng)用系統(tǒng)中去。使用XML配制文件的應(yīng)用程序能夠方便地處理所需數(shù)據(jù),不用象其他應(yīng)用那樣要經(jīng)過重新編譯才能修改和維護(hù)應(yīng)用系統(tǒng)。如前所述,這里提到的五種使用XML的途徑不包括全部場合。
1、首先,我們在機(jī)器里新建一個用戶名為text(這里任意的用戶名都可以)。
2、注銷Administrator用戶。改用text用戶進(jìn)入系統(tǒng)。
3、點擊“開始”—“運行”。鍵入regedit。可以打開注冊表編輯器。
4、 找到以下路徑:HKEY_CURRENT_USER\\Software\\Microsoft\\Internet Explorer。選中“Internet Explorer”。點擊表單欄的“注冊表”。導(dǎo)出注冊表文件。
5、這時就把text用戶下的IE設(shè)置給導(dǎo)出來了。因為text用戶是新加入的。所以他的IE設(shè)置并沒有被更改過。
6、現(xiàn)在我們切換回Administrator用戶登錄系統(tǒng)。
7、點擊“打開”—“運行”。鍵入regedit。依然進(jìn)入注冊表編輯器。
8、點擊表單欄的“注冊表”,導(dǎo)入注冊表。把剛才保存的那個注冊表文件導(dǎo)入進(jìn)去就可以了。
現(xiàn)在我們再打開IE。發(fā)現(xiàn)一切都已經(jīng)恢復(fù)成系統(tǒng)默認(rèn)的樣子了。
經(jīng)實驗本方法非常實用。為了安全起見,大家還需要進(jìn)行必要的殺毒等措施。
??????? 定義:
????????? 將常用的或很復(fù)雜的工作,預(yù)先用SQL語句寫好并用一個指定的名稱存儲起來,?? 那么以后要叫數(shù)據(jù)庫提供與已定義好的存儲過程的功能相同的服務(wù)時,只需調(diào)用execute,即可自動完成命令。
??????? 講到這里,可能有人要問:這么說存儲過程就是一堆SQL語句而已啊?
????????? Microsoft公司為什么還要添加這個技術(shù)呢?
??????? 那么存儲過程與一般的SQL語句有什么區(qū)別呢?
??????? 存儲過程的優(yōu)點:
????????? 1.存儲過程只在創(chuàng)造時進(jìn)行編譯,以后每次執(zhí)行存儲過程都不需再重新編譯,而一般SQL語句每執(zhí)行一次就編譯一次,所以使用存儲過程可提高數(shù)據(jù)庫執(zhí)行速度。
????????? 2.當(dāng)對數(shù)據(jù)庫進(jìn)行復(fù)雜操作時(如對多個表進(jìn)行Update,Insert,Query,Delete時),可將此復(fù)雜操作用存儲過程封裝起來與數(shù)據(jù)庫提供的事務(wù)處理結(jié)合一起使用。
????????? 3.存儲過程可以重復(fù)使用,可減少數(shù)據(jù)庫開發(fā)人員的工作量
????????? 4.安全性高,可設(shè)定只有某此用戶才具有對指定存儲過程的使用權(quán)
??????? 存儲過程的種類:
????????? 1.系統(tǒng)存儲過程:以sp_開頭,用來進(jìn)行系統(tǒng)的各項設(shè)定.取得信息.相關(guān)管理工作,
????????? 如?? sp_help就是取得指定對象的相關(guān)信息
????????? 2.擴(kuò)展存儲過程?? 以XP_開頭,用來調(diào)用操作系統(tǒng)提供的功能
????????? exec?? master..xp_cmdshell?? 'ping?? 10.8.16.1'
????????? 3.用戶自定義的存儲過程,這是我們所指的存儲過程
????????? 常用格式
????????? Create?? procedure?? procedue_name
????????? [@parameter?? data_type][output]
????????? [with]{recompile|encryption}
????????? as
????????? sql_statement
??????? 解釋:??
??????? output:表示此參數(shù)是可傳回的
??????? with?? {recompile|encryption}
??????? recompile:表示每次執(zhí)行此存儲過程時都重新編譯一次
??????? encryption:所創(chuàng)建的存儲過程的內(nèi)容會被加密
??????? 如:
????????? 表book的內(nèi)容如下
????????? 編號?? 書名?? 價格
????????? 001?? C語言入門?? $30
????????? 002?? PowerBuilder報表開發(fā)?? $52
????????? 實例1:查詢表Book的內(nèi)容的存儲過程
????????? create?? proc?? query_book
????????? as??
????????? select?? *?? from?? book
????????? go
????????? exec?? query_book
????????? 實例2:加入一筆記錄到表book,并查詢此表中所有書籍的總金額
????????? Create?? proc?? insert_book
????????? @param1?? char(10),@param2?? varchar(20),@param3?? money,@param4?? money?? output
????????? with?? encryption?? ---------加密
????????? as
????????? insert?? book(編號,書名,價格)?? Values(@param1,@param2,@param3)
????????? select?? @param4=sum(價格)?? from?? book
????????? go
????????? 執(zhí)行例子:??
????????? declare?? @total_price?? money??
????????? exec?? insert_book?? '003','Delphi?? 控件開發(fā)指南',$100,@total_price
????????? print?? '總金額為'+convert(varchar,@total_price)
????????? go
??????? 存儲過程的3種傳回值:
????????? 1.以Return傳回整數(shù)
????????? 2.以output格式傳回參數(shù)
????????? 3.Recordset
??????? 傳回值的區(qū)別:
????????? output和return都可在批次程式中用變量接收,而recordset則傳回到執(zhí)行批次的客戶端中??
??????? 實例3:設(shè)有兩個表為Product,Order,其表內(nèi)容如下:
????????? Product
????????? 產(chǎn)品編號?? 產(chǎn)品名稱?? 客戶訂數(shù)??
????????? 001?? 鋼筆?? 30??
????????? 002?? 毛筆?? 50??
????????? 003?? 鉛筆?? 100??
????????? Order??
????????? 產(chǎn)品編號?? 客戶名?? 客戶訂金
????????? 001?? 南山區(qū)?? $30
????????? 002?? 羅湖區(qū)?? $50
????????? 003?? 寶安區(qū)?? $4
??????? 請實現(xiàn)按編號為連接條件,將兩個表連接成一個臨時表,該表只含編號.產(chǎn)品名.客戶名.訂金.總金額,
??????? 總金額=訂金*訂數(shù),臨時表放在存儲過程中
??????? 代碼如下:
????????? Create?? proc?? temp_sale
????????? as
????????? select?? a.產(chǎn)品編號,a.產(chǎn)品名稱,b.客戶名,b.客戶訂金,a.客戶訂數(shù)*?? b.客戶訂金?? as總金額
????????? into?? #temptable?? from?? Product?? a?? inner?? join?? Order?? b?? on?? a.產(chǎn)品編號=b.產(chǎn)品編號
????????? if?? @@error=0??
????????? print?? 'Good'
????????? else
????????&n bsp; print?? 'Fail'
????????? go
存儲過程介紹??
一、先介紹一下什么是存儲過程??
存儲過程是利用SQL?? Server所提供的Tranact-SQL語言所編寫的程序。Tranact-SQL語言是SQL?? Server提供專為設(shè)計數(shù)據(jù)庫應(yīng)用程序的語言,它是應(yīng)用程序和SQL?? Server數(shù)據(jù)庫間的主要程序式設(shè)計界面。它好比Oracle數(shù)據(jù)庫系統(tǒng)中的Pro-SQL和Informix的數(shù)據(jù)庫系統(tǒng)能夠中的Informix-4GL語言一樣。這類語言主要提供以下功能,讓用戶可以設(shè)計出符合引用需求的程序:??
1)、變量說明??
2)、ANSI兼容的SQL命令(如Select,Update….)??
3)、一般流程控制命令(if…else…、while….)??
4)、內(nèi)部函數(shù)??
二、存儲過程的書寫格??
CREATE?? PROCEDURE?? [擁有者.]存儲過程名[;程序編號]??
[(參數(shù)#1,…參數(shù)#1024)]??
[WITH??
{RECOMPILE?? |?? ENCRYPTION?? |?? RECOMPILE,?? ENCRYPTION}??
]??
[FOR?? REPLICATION]??
AS?? 程序行??
其中存儲過程名不能超過128個字。每個存儲過程中最多設(shè)定1024個參數(shù)??
(SQL?? Server?? 7.0以上版本),參數(shù)的使用方法如下:??
@參數(shù)名?? 數(shù)據(jù)類型?? [VARYING]?? [=內(nèi)定值]?? [OUTPUT]??
每個參數(shù)名前要有一個“@”符號,每一個存儲過程的參數(shù)僅為該程序內(nèi)部使用,參數(shù)的類型除了IMAGE外,其他SQL?? Server所支持的數(shù)據(jù)類型都可使用。??
[=內(nèi)定值]相當(dāng)于我們在建立數(shù)據(jù)庫時設(shè)定一個字段的默認(rèn)值,這里是為這個參數(shù)設(shè)定默認(rèn)值。[OUTPUT]是用來指定該參數(shù)是既有輸入又有輸出值的,也就是在調(diào)用了這個存儲過程時,如果所指定的參數(shù)值是我們需要輸入的參數(shù),同時也需要在結(jié)果中輸出的,則該項必須為OUTPUT,而如果只是做輸出參數(shù)用,可以用CURSOR,同時在使用該參數(shù)時,必須指定VARYING和OUTPUT這兩個語句。??
例子:??
CREATE?? PROCEDURE?? order_tot_amt?? @o_id?? int,@p_tot?? int?? output?? AS??
SELECT?? @p_tot?? =?? sum(Unitprice*Quantity)??
FROM?? orderdetails??
WHERE?? ordered=@o_id??
例子說明:??
該例子是建立一個簡單的存儲過程order_tot_amt,這個存儲過程根據(jù)用戶輸入的定單ID號碼(@o_id),由定單明細(xì)表(orderdetails)中計算該定單銷售總額[單價(Unitprice)*數(shù)量(Quantity)],這一金額通過@p_tot這一參數(shù)輸出給調(diào)用這一存儲過程的程序??
三、在SQL?? Server中執(zhí)行存儲過程??
在SQL?? Server的查詢分析器中,輸入以下代碼:??
declare?? @tot_amt?? int??
execute?? order_tot_amt?? 1,@tot_amt?? output??
select?? @tot_amt??
以上代碼是執(zhí)行order_tot_amt這一存儲過程,以計算出定單編號為1的定單銷售金額,我們定義@tot_amt為輸出參數(shù),用來承接我們所要的結(jié)果?
今天在 DRM中報錯ora-01830把sql語句輸出作了以下的實驗,發(fā)現(xiàn)是時間多了一個.0
后來的辦法是先把這個時間轉(zhuǎn)成to_char,再轉(zhuǎn)成to_date
SQL> select to_date('2005-10-01 12:01:01.0','yyyy-mm-dd hh24:mi:ss') from dual;
select to_date('2005-10-01 12:01:01.0','yyyy-mm-dd hh24:mi:ss') from dual
ORA-01830: 日期格式圖片在轉(zhuǎn)換整個輸入字符串之前結(jié)束
SQL> select to_date('2005-10-01 12:01:01.0','yyyy-mm-dd hh24:mi:sssss') from dual;
select to_date('2005-10-01 12:01:01.0','yyyy-mm-dd hh24:mi:sssss') from dual
ORA-01836: 小時與日中的秒發(fā)生沖突
SQL> select to_date('2005-10-01 12:01:01.0','yyyy-mm-dd hh24:mi:ss.sssss') from dual;
select to_date('2005-10-01 12:01:01.0','yyyy-mm-dd hh24:mi:ss.sssss') from dual
ORA-01836: 小時與日中的秒發(fā)生沖突
SQL> select to_date('2005-10-01 12:01:01.0','yyyy-mm-dd hh24:mi:ff') from dual;
select to_date('2005-10-01 12:01:01.0','yyyy-mm-dd hh24:mi:ff') from dual
ORA-01821: 日期格式無法識別
------------------------------------------------------------------
必須保證傳入的字符串和要轉(zhuǎn)換的格式精確匹配
SQL> SELECT TO_DATE('11-10-1996-13:51:21','DD/MM/YYYY-HH24') A FROM dual;
ERROR:
ORA-01830: date format picture ends before converting entire input string.
SQL> SELECT TO_DATE('11-10-1996-13:51:21','DD/MM/YYYY-HH24:MI:SS') B FROM dual;
--------------------------------------
以上是轉(zhuǎn)載的
后來我是這么做的哈:
SELECT中將其他表的日期TO_CHAR下,然后再將值在INSERT時TO_DATE!
具體的做法如下:
SELECT TO_CHAR(parameter,'YYYY-MM-DD HH24:MI:SS') AS TIME
FROM TABLE_NAME_1;
...
...
INSERT INTO TABLE_NAME_2
(COLUME_NAME_1)
VALUE (TO_DATE('"+TIME+"','YYYY-MM-DD HH24:MI:SS'));
然后就OK了,呵呵,看來要學(xué)的還真多!
關(guān)鍵字: Java, JDBC, Connection Pool, Database, 數(shù)據(jù)庫連接池, sourcecode
??雖然 J2EE 程序員一般都有現(xiàn)成的應(yīng)用服務(wù)器所帶的JDBC 數(shù)據(jù)庫連接池,不過對于開發(fā)一般的 Java Application 、 Applet 或者 JSP、velocity 時,我們可用的JDBC 數(shù)據(jù)庫連接池并不多,并且一般性能都不好。 Java 程序員都很羨慕 Windows ADO ,只需要 new Connection 就可以直接從數(shù)據(jù)庫連接池中返回 Connection。并且 ADO Connection 是線程安全的,多個線程可以共用一個 Connection, 所以 ASP 程序一般都把 getConnection 放在 Global.asa 文件中,在 IIS 啟動時建立數(shù)據(jù)庫連接。ADO 的 Connection 和 Result 都有很好的緩沖,并且很容易使用。
其實我們可以自己寫一個JDBC 數(shù)據(jù)庫連接池。寫 JDBC connection pool 的注意事項有:
1. 有一個簡單的函數(shù)從連接池中得到一個 Connection。
2. close 函數(shù)必須將 connection 放回 數(shù)據(jù)庫連接池。
3. 當(dāng)數(shù)據(jù)庫連接池中沒有空閑的 connection, 數(shù)據(jù)庫連接池必須能夠自動增加 connection 個數(shù)。
4. 當(dāng)數(shù)據(jù)庫連接池中的 connection 個數(shù)在某一個特別的時間變得很大,但是以后很長時間只用其中一小部分,應(yīng)該可以自動將多余的 connection 關(guān)閉掉。
5. 如果可能,應(yīng)該提供debug 信息報告沒有關(guān)閉的 new Connection 。
如果要 new Connection 就可以直接從數(shù)據(jù)庫連接池中返回 Connection, 可以這樣寫( Mediator pattern ) (以下代碼中使用了中文全角空格):
public class EasyConnection implements java.sql.Connection{
private Connection m_delegate = null;
public EasyConnection(){
m_delegate = getConnectionFromPool();
}
public void close(){
putConnectionBackToPool(m_delegate);
}
public PreparedStatement prepareStatement(String sql) throws SQLException{
m_delegate.prepareStatement(sql);
}
//...... other method
}
看來并不難。不過不建議這種寫法,因為應(yīng)該盡量避免使用 Java Interface, 關(guān)于 Java Interface 的缺點我另外再寫文章討論。大家關(guān)注的是 Connection Pool 的實現(xiàn)方法。下面給出一種實現(xiàn)方法。
import java.sql.*;
import java.lang.reflect.*;
import java.util.*;
import java.io.*;
public class SimpleConnetionPool {
private static LinkedList m_notUsedConnection = new LinkedList();
private static HashSet m_usedUsedConnection = new HashSet();
private static String m_url = "";
private static String m_user = "";
private static String m_password = "";
static final boolean DEBUG = true;
static private long m_lastClearClosedConnection = System.currentTimeMillis();
public static long CHECK_CLOSED_CONNECTION_TIME = 4 * 60 * 60 * 1000; //4 hours
static {
initDriver();
}
private SimpleConnetionPool() {
}
private static void initDriver() {
Driver driver = null;
//load mysql driver
try {
driver = (Driver) Class.forName("com.mysql.jdbc.Driver").newInstance();
installDriver(driver);
} catch (Exception e) {
}
//load postgresql driver
try {
driver = (Driver) Class.forName("org.postgresql.Driver").newInstance();
installDriver(driver);
} catch (Exception e) {
}
}
public static void installDriver(Driver driver) {
try {
DriverManager.registerDriver(driver);
} catch (Exception e) {
e.printStackTrace();
}
}
public static synchronized Connection getConnection() {
clearClosedConnection();
while (m_notUsedConnection.size() > 0) {
try {
ConnectionWrapper wrapper = (ConnectionWrapper) m_notUsedConnection.removeFirst();
if (wrapper.connection.isClosed()) {
continue;
}
m_usedUsedConnection.add(wrapper);
if (DEBUG) {
wrapper.debugInfo = new Throwable("Connection initial statement");
}
return wrapper.connection;
} catch (Exception e) {
}
}
int newCount = getIncreasingConnectionCount();
LinkedList list = new LinkedList();
ConnectionWrapper wrapper = null;
for (int i = 0; i < newCount; i++) {
wrapper = getNewConnection();
if (wrapper != null) {
list.add(wrapper);
}
}
if (list.size() == 0) {
return null;
}
wrapper = (ConnectionWrapper) list.removeFirst();
m_usedUsedConnection.add(wrapper);
m_notUsedConnection.addAll(list);
list.clear();
return wrapper.connection;
}
private static ConnectionWrapper getNewConnection() {
try {
Connection con = DriverManager.getConnection(m_url, m_user, m_password);
ConnectionWrapper wrapper = new ConnectionWrapper(con);
return wrapper;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
static synchronized void pushConnectionBackToPool(ConnectionWrapper con) {
boolean exist = m_usedUsedConnection.remove(con);
if (exist) {
m_notUsedConnection.addLast(con);
}
}
public static int close() {
int count = 0;
Iterator iterator = m_notUsedConnection.iterator();
while (iterator.hasNext()) {
try {
( (ConnectionWrapper) iterator.next()).close();
count++;
} catch (Exception e) {
}
}
m_notUsedConnection.clear();
iterator = m_usedUsedConnection.iterator();
while (iterator.hasNext()) {
try {
ConnectionWrapper wrapper = (ConnectionWrapper) iterator.next();
wrapper.close();
if (DEBUG) {
wrapper.debugInfo.printStackTrace();
}
count++;
} catch (Exception e) {
}
}
m_usedUsedConnection.clear();
return count;
}
private static void clearClosedConnection() {
long time = System.currentTimeMillis();
//sometimes user change system time,just return
if (time < m_lastClearClosedConnection) {
time = m_lastClearClosedConnection;
return;
}
//no need check very often
if (time - m_lastClearClosedConnection < CHECK_CLOSED_CONNECTION_TIME) {
return;
}
m_lastClearClosedConnection = time;
//begin check
Iterator iterator = m_notUsedConnection.iterator();
while (iterator.hasNext()) {
ConnectionWrapper wrapper = (ConnectionWrapper) iterator.next();
try {
if (wrapper.connection.isClosed()) {
iterator.remove();
}
} catch (Exception e) {
iterator.remove();
if (DEBUG) {
System.out.println("connection is closed, this connection initial StackTrace");
wrapper.debugInfo.printStackTrace();
}
}
}
//make connection pool size smaller if too big
int decrease = getDecreasingConnectionCount();
if (m_notUsedConnection.size() < decrease) {
return;
}
while (decrease-- > 0) {
ConnectionWrapper wrapper = (ConnectionWrapper) m_notUsedConnection.removeFirst();
try {
wrapper.connection.close();
} catch (Exception e) {
}
}
}
/**
* get increasing connection count, not just add 1 connection
* @return count
*/
public static int getIncreasingConnectionCount() {
int count = 1;
int current = getConnectionCount();
count = current / 4;
if (count < 1) {
count = 1;
}
return count;
}
/**
* get decreasing connection count, not just remove 1 connection
* @return count
*/
public static int getDecreasingConnectionCount() {
int count = 0;
int current = getConnectionCount();
if (current < 10) {
return 0;
}
return current / 3;
}
public synchronized static void printDebugMsg() {
printDebugMsg(System.out);
}
public synchronized static void printDebugMsg(PrintStream out) {
if (DEBUG == false) {
return;
}
StringBuffer msg = new StringBuffer();
msg.append("debug message in " + SimpleConnetionPool.class.getName());
msg.append("\r\n");
msg.append("total count is connection pool: " + getConnectionCount());
msg.append("\r\n");
msg.append("not used connection count: " + getNotUsedConnectionCount());
msg.append("\r\n");
msg.append("used connection, count: " + getUsedConnectionCount());
out.println(msg);
Iterator iterator = m_usedUsedConnection.iterator();
while (iterator.hasNext()) {
ConnectionWrapper wrapper = (ConnectionWrapper) iterator.next();
wrapper.debugInfo.printStackTrace(out);
}
out.println();
}
public static synchronized int getNotUsedConnectionCount() {
return m_notUsedConnection.size();
}
public static synchronized int getUsedConnectionCount() {
return m_usedUsedConnection.size();
}
public static synchronized int getConnectionCount() {
return m_notUsedConnection.size() + m_usedUsedConnection.size();
}
public static String getUrl() {
return m_url;
}
public static void setUrl(String url) {
if (url == null) {
return;
}
m_url = url.trim();
}
public static String getUser() {
return m_user;
}
public static void setUser(String user) {
if (user == null) {
return;
}
m_user = user.trim();
}
public static String getPassword() {
return m_password;
}
public static void setPassword(String password) {
if (password == null) {
return;
}
m_password = password.trim();
}
}
class ConnectionWrapper implements InvocationHandler {
private final static String CLOSE_METHOD_NAME = "close";
public Connection connection = null;
private Connection m_originConnection = null;
public long lastAccessTime = System.currentTimeMillis();
Throwable debugInfo = new Throwable("Connection initial statement");
ConnectionWrapper(Connection con) {
Class[] interfaces = {java.sql.Connection.class};
this.connection = (Connection) Proxy.newProxyInstance(
con.getClass().getClassLoader(),
interfaces, this);
m_originConnection = con;
}
void close() throws SQLException {
m_originConnection.close();
}
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
Object obj = null;
if (CLOSE_METHOD_NAME.equals(m.getName())) {
SimpleConnetionPool.pushConnectionBackToPool(this);
}
else {
obj = m.invoke(m_originConnection, args);
}
lastAccessTime = System.currentTimeMillis();
return obj;
}
}
使用方法
public class TestConnectionPool{1.前言
public static void main(String[] args) {
SimpleConnetionPool.setUrl(DBTools.getDatabaseUrl());
SimpleConnetionPool.setUser(DBTools.getDatabaseUserName());
SimpleConnetionPool.setPassword(DBTools.getDatabasePassword());
Connection con = SimpleConnetionPool.getConnection();
Connection con1 = SimpleConnetionPool.getConnection();
Connection con2 = SimpleConnetionPool.getConnection();
//do something with con ...
try {
con.close();
} catch (Exception e) {}
try {
con1.close();
} catch (Exception e) {}
try {
con2.close();
} catch (Exception e) {}
con = SimpleConnetionPool.getConnection();
con1 = SimpleConnetionPool.getConnection();
try {
con1.close();
} catch (Exception e) {}
con2 = SimpleConnetionPool.getConnection();
SimpleConnetionPool.printDebugMsg();
}
}
數(shù)據(jù)庫應(yīng)用,在許多軟件系統(tǒng)中經(jīng)常用到,是開發(fā)中大型系統(tǒng)不可缺少的輔助。但如果對數(shù)據(jù)庫資源沒有很好地管理(如:沒有及時回收數(shù)據(jù)庫的游標(biāo)(ResultSet)、Statement、連接 (Connection)等資源),往往會直接導(dǎo)致系統(tǒng)的穩(wěn)定。這類不穩(wěn)定因素,不單單由數(shù)據(jù)庫或者系統(tǒng)本身一方引起,只有系統(tǒng)正式使用后,隨著流量、用戶的增加,才會逐步顯露。
在基于Java開發(fā)的系統(tǒng)中,JDBC是程序員和數(shù)據(jù)庫打交道的主要途徑,提供了完備的數(shù)據(jù)庫操作方法接口。但考慮到規(guī)范的適用性,JDBC只提供了最直接的數(shù)據(jù)庫操作規(guī)范,對數(shù)據(jù)庫資源管理,如:對物理連接的管理及緩沖,期望第三方應(yīng)用服務(wù)器(Application Server)的提供。
本文,以JDBC規(guī)范為基礎(chǔ),介紹相關(guān)的數(shù)據(jù)庫連接池機(jī)制,并就如果以簡單的方式,實現(xiàn)有效地管理數(shù)據(jù)庫資源介紹相關(guān)實現(xiàn)技術(shù)。
2.連接池技術(shù)背景
2.1 JDBC
JDBC是一個規(guī)范,遵循JDBC接口規(guī)范,各個數(shù)據(jù)庫廠家各自實現(xiàn)自己的驅(qū)動程序(Driver),如下圖所示:

?wèi)?yīng)用在獲取數(shù)據(jù)庫連接時,需要以URL的方式指定是那種類型的Driver,在獲得特定的連接后,可按照固定的接口操作不同類型的數(shù)據(jù)庫,如: 分別獲取Statement、執(zhí)行SQL獲得ResultSet等,如下面的例子 :
import java.sql.*;
…
DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
Connection dbConn = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:oracle","username","password");
Statement st = dbConn.createStatement();
ResultSet rs = st.executeQuery("select * from demo_table");
…some data source operation in here
rs.close();
st.close();
dbConn.close();
在完成數(shù)據(jù)操作后,還一定要關(guān)閉所有涉及到的數(shù)據(jù)庫資源。這雖然對應(yīng)用程序的邏輯沒有任何影響,但是關(guān)鍵的操作。上面是個簡單的例子,如果攙和眾多的if-else、exception,資源的管理也難免百密一疏。如同C中的內(nèi)存泄漏問題,Java系統(tǒng)也同樣會面臨崩潰的惡運。所以數(shù)據(jù)庫資源的管理依賴于應(yīng)用系統(tǒng)本身,是不安全、不穩(wěn)定的一種隱患。
2.2 JDBC連接池
在標(biāo)準(zhǔn)JDBC對應(yīng)用的接口中,并沒有提供資源的管理方法。所以,缺省的資源管理由應(yīng)用自己負(fù)責(zé)。雖然在JDBC規(guī)范中,多次提及資源的關(guān)閉/回收及其他的合理運用。但最穩(wěn)妥的方式,還是為應(yīng)用提供有效的管理手段。所以,JDBC為第三方應(yīng)用服務(wù)器(Application Server)提供了一個由數(shù)據(jù)庫廠家實現(xiàn)的管理標(biāo)準(zhǔn)接口:連接緩沖(connection pooling)。引入了連接池( Connection Pool )的概念 ,也就是以緩沖池的機(jī)制管理數(shù)據(jù)庫的資源。
JDBC最常用的資源有三類:
— Connection: 數(shù)據(jù)庫連接。
— Statement: 會話聲明。
— ResultSet: 結(jié)果集游標(biāo)。
分別存在以下的關(guān)系 :
這是一種“爺—父—子”的關(guān)系,對Connection的管理,就是對數(shù)據(jù)庫資源的管理。舉個例子: 如果想確定某個數(shù)據(jù)庫連接(Connection)是否超時,則需要確定其(所有的)子Statement是否超時,同樣,需要確定所有相關(guān)的ResultSet是否超時;在關(guān)閉Connection前,需要關(guān)閉所有相關(guān)的Statement和ResultSet。
因此,連接池(Connection Pool)所起到的作用,不僅僅簡單地管理Connection,還涉及到 Statement和ResultSet。
2.3 連接池(ConnectionPool)與資源管理
ConnectionPool以緩沖池的機(jī)制,在一定數(shù)量上限范圍內(nèi),控制管理Connection,Statement和ResultSet。任何數(shù)據(jù)庫的資源是有限的,如果被耗盡,則無法獲得更多的數(shù)據(jù)服務(wù)。
在大多數(shù)情況下,資源的耗盡不是由于應(yīng)用的正常負(fù)載過高,而是程序原因。
在實際工作中,數(shù)據(jù)資源往往是瓶頸資源,不同的應(yīng)用都會訪問同一數(shù)據(jù)源。其中某個應(yīng)用耗盡了數(shù)據(jù)庫資源后,意味其他的應(yīng)用也無法正常運行。因此,ConnectionPool的第一個任務(wù)是限制:每個應(yīng)用或系統(tǒng)可以擁有的最大資源。也就是確定連接池的大小(PoolSize)。
ConnectionPool的第二個任務(wù):在連接池的大小(PoolSize)范圍內(nèi),最大限度地使用資源,縮短數(shù)據(jù)庫訪問的使用周期。許多數(shù)據(jù)庫中,連接(Connection)并不是資源的最小單元,控制Statement資源比Connection更重要。以O(shè)racle為例:
每申請一個連接(Connection)會在物理網(wǎng)絡(luò)(如 TCP/IP網(wǎng)絡(luò))上建立一個用于通訊的連接,在此連接上還可以申請一定數(shù)量的Statement。同一連接可提供的活躍Statement數(shù)量可以達(dá)到幾百。 在節(jié)約網(wǎng)絡(luò)資源的同時,縮短了每次會話周期(物理連接的建立是個費時的操作)。但在一般的應(yīng)用中,多數(shù)按照2.1范例操作,這樣有10個程序調(diào)用,則會產(chǎn)生10次物理連接,每個Statement單獨占用一個物理連接,這是極大的資源浪費。 ConnectionPool可以解決這個問題,讓幾十、幾百個Statement只占用同一個物理連接, 發(fā)揮數(shù)據(jù)庫原有的優(yōu)點。
通過ConnectionPool對資源的有效管理,應(yīng)用可以獲得的Statement總數(shù)到達(dá) :
(并發(fā)物理連接數(shù))×(每個連接可提供的Statement數(shù)量)
例如某種數(shù)據(jù)庫可同時建立的物理連接數(shù)為 200個,每個連接可同時提供250個Statement,那么ConnectionPool最終為應(yīng)用提供的并發(fā)Statement總數(shù)為: 200 × 250 = 50,000個。這是個并發(fā)數(shù)字,很少有系統(tǒng)會突破這個量級。所以在本節(jié)的開始,指出資源的耗盡與應(yīng)用程序直接管理有關(guān)。
對資源的優(yōu)化管理,很大程度上依靠數(shù)據(jù)庫自身的JDBC Driver是否具備。有些數(shù)據(jù)庫的JDBC Driver并不支持Connection與Statement之間的邏輯連接功能,如SQLServer,我們只能等待她自身的更新版本了。
對資源的申請、釋放、回收、共享和同步,這些管理是復(fù)雜精密的。所以,ConnectionPool另一個功能就是,封裝這些操作,為應(yīng)用提供簡單的,甚至是不改變應(yīng)用風(fēng)格的調(diào)用接口。
3.簡單JDBC連接池的實現(xiàn)
根據(jù)第二章中原理機(jī)制,Snap-ConnectionPool(一種簡單快速的連接池工具,可在www.snapbug.net下載)按照部分的JDBC規(guī)范,實現(xiàn)了連接池所具備的對數(shù)據(jù)庫資源有效管理功能。
3.1 體系描述
在JDBC規(guī)范中,應(yīng)用通過驅(qū)動接口(Driver Interface)直接方法數(shù)據(jù)庫的資源。為了有效、合理地管理資源,在應(yīng)用與JDBC Driver之間,增加了連接池: Snap-ConnectionPool。并且通過面向?qū)ο蟮臋C(jī)制,使連接池的大部分操作是透明的。參見下圖,Snap-ConnectionPool的體系:

圖中所示,通過實現(xiàn)JDBC的部分資源對象接口( Connection, Statement, ResultSet ),在 Snap-ConnectionPool內(nèi)部分別產(chǎn)生三種邏輯資源對象: PooledConnection, PooledStatement和 PooledResultSet。它們也是連接池主要的管理操作對象,并且繼承了JDBC中相應(yīng)的從屬關(guān)系。這樣的體系有以下幾個特點:
— 透明性。在不改變應(yīng)用原有的使用JDBC驅(qū)動接口的前提下,提供資源管理的服務(wù)。應(yīng)用系統(tǒng),如同原有的 JDBC,使用連接池提供的邏輯對象資源。簡化了應(yīng)用程序的連接池改造。
— 資源封裝。復(fù)雜的資源管理被封裝在? Snap-ConnectionPool內(nèi)部,不需要應(yīng)用系統(tǒng)過多的干涉。管理操作的可靠性、安全性由連接池保證。應(yīng)用的干涉(如:主動關(guān)閉資源),只起到優(yōu)化系統(tǒng)性能的作用,遺漏操作不會帶來負(fù)面影響。
— 資源合理應(yīng)用。按照J(rèn)DBC中資源的從屬關(guān)系,Snap-ConnectionPool不僅對Connection進(jìn)行緩沖處理,對Statement也有相應(yīng)的機(jī)制處理。在2.3已描述,合理運用Connection和Statement之間的關(guān)系,可以更大限度地使用資源。所以,Snap-ConnectionPool封裝了Connection資源,通過內(nèi)部管理PooledConnection,為應(yīng)用系統(tǒng)提供更多的Statement資源。
— 資源連鎖管理。Snap-ConnectionPool包含的三種邏輯對象,繼承了JDBC中相應(yīng)對象之間的從屬關(guān)系。在內(nèi)部管理中,也依照從屬關(guān)系進(jìn)行連鎖管理。例如:判斷一個Connection是否超時,需要根據(jù)所包含的Statement是否活躍;判斷Statement也要根據(jù)ResultSet的活躍程度。
3.2 連接池集中管理ConnectionManager
ConnectionPool是Snap-ConnectionPool的連接池對象。在Snap-ConnectionPool內(nèi)部,可以指定多個不同的連接池(ConnectionPool)為應(yīng)用服務(wù)。ConnectionManager管理所有的連接池,每個連接池以不同的名稱區(qū)別。通過配置文件適應(yīng)不同的數(shù)據(jù)庫種類。如下圖所示:
通過ConnectionManager,可以同時管理多個不同的連接池,提供通一的管理界面。在應(yīng)用系統(tǒng)中通過ConnectionManager和相關(guān)的配置文件,可以將凌亂散落在各自應(yīng)用程序中的數(shù)據(jù)庫配置信息(包括:數(shù)據(jù)庫名、用戶、密碼等信息),集中在一個文件中。便于系統(tǒng)的維護(hù)工作。
3.3 連接池使用范例
對2.1的標(biāo)準(zhǔn)JDBC的使用范例,改為使用連接池,結(jié)果如下:
import java.sql.*;
import net.snapbug.util.dbtool.*;
…
..ConnectionPool dbConn = ConnectionManager
.getConnectionPool("testOracle" );
Statement st = dbConn.createStatement();
ResultSet rs = st.executeQuery(
“select * from demo_table” );
…
some data source operation
in herers.close();st.close();
在例子中,Snap-ConnectionPool封裝了應(yīng)用對Connection的管理。只要改變JDBC獲取Connection的方法,為獲取連接池(ConnectionPool)(粗體部分),其他的數(shù)據(jù)操作都可以不做修改。按照這樣的方式,Snap-ConnectionPool可幫助應(yīng)用有效地管理數(shù)據(jù)庫資源。如果應(yīng)用忽視了最后資源的釋放: rs.close() 和 st.close(),連接池會通過超時(time-out)機(jī)制,自動回收。
4.小結(jié)
無論是Snap-ConnectionPool還是其他的數(shù)據(jù)庫連接池,都應(yīng)當(dāng)具備一下基本功能:
-對源數(shù)據(jù)庫資源的保護(hù)
-充分利用發(fā)揮數(shù)據(jù)庫的有效資源
-簡化應(yīng)用的數(shù)據(jù)庫接口,封閉資源管理。
-對應(yīng)用遺留資源的自動回收和整理,提高資源的再次利用率。
在這個前提下,應(yīng)用程序才能投入更多的精力于各自的業(yè)務(wù)邏輯中。數(shù)據(jù)庫資源也不再成為系統(tǒng)的瓶頸。
| |||||||||
日 | 一 | 二 | 三 | 四 | 五 | 六 | |||
---|---|---|---|---|---|---|---|---|---|
29 | 30 | 1 | 2 | 3 | 4 | 5 | |||
6 | 7 | 8 | 9 | 10 | 11 | 12 | |||
13 | 14 | 15 | 16 | 17 | 18 | 19 | |||
20 | 21 | 22 | 23 | 24 | 25 | 26 | |||
27 | 28 | 29 | 30 | 31 | 1 | 2 | |||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
歡迎探討,努力學(xué)習(xí)Java哈
常用鏈接
留言簿(3)
隨筆分類
- Java(11)
- JSP
- MyEclipse(1)
- Oracle 10g(3)
- Resin
- Spring
- SQL(4)
- Struts(1)
- Tomcat
- XML(2)
- 個人日志(1)
- 學(xué)習(xí)(5)
- 工作(1)
- 搜索引擎(7)
- 灌水(1)
- 貼圖
- 軟件工程
隨筆檔案
文章分類
文章檔案
Lansing's Download
Lansing's Link
我的博客
搜索
最新評論

- 1.?re: 關(guān)于ODBC數(shù)據(jù)源連接文本
- 評論內(nèi)容較長,點擊標(biāo)題查看
- --棱語明
- 2.?re: Oracle 10g TO_DATE() ora-01830 領(lǐng)悟共勉[未登錄]
- 評論內(nèi)容較長,點擊標(biāo)題查看
- --訪客
- 3.?re: Oracle 10g TO_DATE() ora-01830 領(lǐng)悟共勉
- 人才啊!
- --歲月無聲
- 4.?re: Struts框架技術(shù)在J2EE中的研究和應(yīng)用[未登錄]
- dfetetgfgf
- --aa
- 5.?re: 關(guān)于ODBC數(shù)據(jù)源連接文本 [未登錄]
-
“坐在巷口的那對男女”呵呵 @blackbat
- --lansing