|
概述 數(shù)據(jù)庫(kù)程序設(shè)計(jì)通常需要從大量的外部數(shù)據(jù)源中獲取數(shù)據(jù)進(jìn)行處理。這這篇文章中,作 者提出了在數(shù)據(jù)庫(kù)中使用DatabaseMetaData——JDBC源數(shù)據(jù)接口——和執(zhí)行預(yù)編譯的SQ L實(shí)現(xiàn)將純文本數(shù)據(jù)轉(zhuǎn)換成實(shí)際的數(shù)據(jù)類型的技術(shù)。它提供了一種在運(yùn)行時(shí)動(dòng)態(tài)發(fā)現(xiàn)數(shù)據(jù) 庫(kù)的具體數(shù)據(jù)類型和翻譯外部文本數(shù)據(jù)的方法。 正文 假設(shè)您需要寫一個(gè)將文本文件中的數(shù)據(jù)轉(zhuǎn)換成數(shù)據(jù)庫(kù)的表并進(jìn)行存儲(chǔ)的數(shù)據(jù)庫(kù)應(yīng)用程序 ,例如這個(gè)表包含的是航班信息,包括票號(hào)、購(gòu)買日期、出發(fā)日期、離港/到港位置以及 票價(jià)等在數(shù)據(jù)庫(kù)中,每一段信息都有一一個(gè)指定的數(shù)據(jù)類型與之相對(duì)應(yīng)。例如數(shù)值型、 文本型、日期型或者貨幣型等等。 程序必須從文本文件中讀出幾段這樣的票務(wù)信息,將這些信息轉(zhuǎn)換成合適的數(shù)據(jù)類型并 創(chuàng)建一個(gè)相應(yīng)的表以存儲(chǔ)這些轉(zhuǎn)換過的數(shù)據(jù)。為了實(shí)現(xiàn)這種轉(zhuǎn)換,程序需要知道每段數(shù) 據(jù)在數(shù)據(jù)庫(kù)中對(duì)應(yīng)的數(shù)據(jù)類型。簡(jiǎn)單的方法是在程序中通過硬編碼進(jìn)行類型的轉(zhuǎn)換—— 將數(shù)據(jù)類型作為編程時(shí)就預(yù)先知道的靜態(tài)信息。 通過采用這種簡(jiǎn)單的方式,您必須要在另外一個(gè)程序中用類似的代碼來實(shí)現(xiàn)同一個(gè)數(shù)據(jù) 庫(kù)中另外一個(gè)表(如具有客戶信息的custumer表)的數(shù)據(jù)生成工作,盡管您可以重復(fù)使 用以前的許多程序代碼,但您必須重新設(shè)計(jì)所有的數(shù)據(jù)庫(kù)數(shù)據(jù)類型轉(zhuǎn)換的代碼。 如果您需要生成的表非常多時(shí),您就會(huì)非常厭倦并且也很浪費(fèi)精力了。但這也僅僅是只 有一個(gè)數(shù)據(jù)庫(kù)的情況。如果將數(shù)據(jù)遷移到其它的數(shù)據(jù)庫(kù)系統(tǒng),您必須重寫所有的代碼以 實(shí)現(xiàn)不同數(shù)據(jù)庫(kù)數(shù)據(jù)類型的對(duì)應(yīng)。在不同的數(shù)據(jù)庫(kù)間實(shí)現(xiàn)數(shù)據(jù)類型的對(duì)應(yīng)和轉(zhuǎn)換在數(shù)據(jù) 庫(kù)編程中一直是一個(gè)非常討厭的事。 創(chuàng)建可重用的代碼 怎樣避免這些重復(fù)的工作呢?您寫代碼時(shí)可以將數(shù)據(jù)庫(kù)名、表名和列名看成參數(shù)。問題 是是不是有辦法對(duì)數(shù)據(jù)類型也可以進(jìn)行類似的處理,這樣您就可以寫一個(gè)類或者一個(gè)類 集完成任何數(shù)據(jù)庫(kù)間任何數(shù)據(jù)類型的對(duì)應(yīng)和表的生成?答案是在運(yùn)行之前不要確定數(shù)據(jù) 類型,在運(yùn)行時(shí)根據(jù)java.sql 包中的DatabaseMetaData接口來實(shí)現(xiàn)它,通過在這個(gè)接口 中寫入代碼,您可以避免對(duì)數(shù)據(jù)類型進(jìn)行硬編碼,這樣就可以寫通用的可重用代碼了。 DatabaseMetaData接口為數(shù)據(jù)庫(kù)提供了元數(shù)據(jù)(metadata)信息。Metadata是描述數(shù)據(jù) 的數(shù)據(jù)。例如,航班數(shù)據(jù)庫(kù)表包含票務(wù)信息,它是數(shù)據(jù)。在這種情況下,元數(shù)據(jù)會(huì)包含 諸如表中有多少列、每個(gè)列的數(shù)據(jù)類型、是不是一個(gè)列可以為空等等信息。這就是關(guān)于 數(shù)據(jù)的數(shù)據(jù)本篇關(guān)注的焦點(diǎn)是每個(gè)列的數(shù)據(jù)類型。 在后面的討論中,我會(huì)向您介紹一種以層次方式進(jìn)行組織的三個(gè)java類構(gòu)成的可重用庫(kù) 。每層都對(duì)下層進(jìn)行了封裝,最下層離數(shù)據(jù)庫(kù)最近。相應(yīng)地,最上層離離應(yīng)用程序最近 了。雖然這些庫(kù)是為灌入數(shù)據(jù)庫(kù)的表而設(shè)計(jì)的,但只要您在代碼上進(jìn)行小規(guī)模的改動(dòng), 您就可以建立自己的數(shù)據(jù)類型獨(dú)立的數(shù)據(jù)庫(kù)查詢和更新。 沿用下面的情況 假設(shè)你需要在一個(gè)organization數(shù)據(jù)庫(kù)中灌入一個(gè)稱為emp的表,這里是一個(gè)在第一行包 含表,然后第二行是一系列的列名,隨后的是每行都是相應(yīng)的列對(duì)應(yīng)的數(shù)據(jù)的樣板數(shù)據(jù) 文件: emp hiredate sal ename empno 1996-09-01 1250.00 jackson 7123 1980-01-01 2500.50 walsh 7124 1985-01-01 12345.67 gates 7125 所有的輸入是純ASCII文本,但這些數(shù)據(jù)會(huì)轉(zhuǎn)換成如下的數(shù)據(jù)庫(kù)特定數(shù)據(jù)類型: 字段 數(shù)據(jù)庫(kù)類型 -------- ------------- hiredate date sal decimal ename ASCII text empno number emp表的數(shù)據(jù)結(jié)構(gòu)如下圖的頂行所示: 從輸入列到數(shù)據(jù)庫(kù)表列的映射 不是表中所有的列都需要立刻灌入,在這個(gè)例子中,僅僅灌入標(biāo)記為true的列,需要灌 入的列有empno、ename、 hiredate和sal。 在數(shù)據(jù)文件中的列的順序也不需要一定與數(shù)據(jù)庫(kù)中列的順序一致。一個(gè)索引數(shù)組維護(hù)了 文件中的列到數(shù)據(jù)庫(kù)中的列的對(duì)應(yīng)。在這個(gè)例子中,活動(dòng)列(標(biāo)記為true的)的順序數(shù) 組就是索引數(shù)組。如果I是輸入文件中一個(gè)列的位置(hiredate:0、sal:1等等),那 么activeColumnOrder[i]是這些列在表中的相應(yīng)位置。(activeColumn[0]是5,意思是 說hiredate是emp表中的第五列)。 建立庫(kù)類層次 我們的庫(kù)中包含三個(gè)層次: 一個(gè)TableColumns類,它距離數(shù)據(jù)庫(kù)最近,它負(fù)責(zé)發(fā)現(xiàn)和管理數(shù)據(jù)庫(kù)表列的信息。 一個(gè)TableMediator類,它用來準(zhǔn)備使用TableColumns所管理的數(shù)據(jù)庫(kù)表信息對(duì)表進(jìn)行灌 入。 一個(gè)TableBuilder類,它離數(shù)據(jù)庫(kù)最遠(yuǎn),當(dāng)然離應(yīng)用程序最近。它負(fù)責(zé)從輸入的數(shù)據(jù)文 件中讀取數(shù)據(jù)然后使用TableMediator類將數(shù)據(jù)灌入指定的表。 下圖說明這種層次關(guān)系: 應(yīng)用分層 這些類都收集在稱為tablebuild的包中。 這些Java源程序可以在這里( http://www./javatips/javatip82/tablebuild.zip) 第一層:TableColumns類 TableColumns類負(fù)責(zé)通過查詢給定數(shù)據(jù)庫(kù)的元數(shù)據(jù)實(shí)例發(fā)現(xiàn)數(shù)據(jù)庫(kù)表的給定列信息。它 將列信息存儲(chǔ)在兩個(gè)并列的數(shù)組中:一個(gè)稱為columnNames的字符串?dāng)?shù)組,一個(gè)稱為col umnTypeCodes的短整型數(shù)組。 一個(gè)給定表的所有列的類型可以通過DatabaseMetaData的getColumns方法獲得: public abstract ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException getColumns方法需要四個(gè)參數(shù):一個(gè)類目名(catalog name)、一個(gè)方案名(schema n ame)、一個(gè)表名(table name)和一個(gè)列名(column name)。它們中,最后三個(gè)帶有 Pattern后綴的(如上面的代碼)很有意思,因?yàn)樗鼈冊(cè)试S你搜索所有的匹配模式表達(dá)式 的目標(biāo)字符串,也就是說,您通過這些參數(shù)指定查詢準(zhǔn)則。如下面所指出的: 尋找所有的滿足下面的準(zhǔn)則的列:它們屬于一個(gè)給定的類目、并且屬于匹配給定方案模 式的任何一個(gè)方案、并且匹配給定表名模式的任何一個(gè)表。當(dāng)然,它們的名字也要匹配 給定的列名模式。 模式使用類似SQL語(yǔ)句的語(yǔ)法進(jìn)行指定,它通過LIKE從句指定匹配名。獨(dú)特的地方是,它 通過在模式表達(dá)式中使用下劃線以匹配目標(biāo)字符串中的任何字符,通過百分號(hào)匹配目標(biāo) 字符串中任何數(shù)量的連續(xù)字符。例如,模式表達(dá)式j(luò)_b會(huì)匹配"job"和"jab",而模式表達(dá) 式j(luò)%b會(huì)匹配任何以"j"開始,以"b"結(jié)束中間包含任意數(shù)量(包括0)字符的目標(biāo)字符串 。 下面是在TableColumns 構(gòu)造方法中對(duì)getColumns方法的調(diào)用,其中dbMeta是給定數(shù)據(jù)庫(kù) 的DatabaseMetaData的一個(gè)實(shí)例。 ResultSet rset = dbMeta.getColumns(null, null, table.toUpperCase(), "%") ; 注意,catalog名和schema模式參數(shù)的值是null??罩当硎具@個(gè)參數(shù)可以從搜索準(zhǔn)則中省 略。另外,要注意我們將指定表名的大寫傳給表名模式。還不清楚這是JDBC的需要還是 JDBC驅(qū)動(dòng)器的偏好。我在發(fā)現(xiàn)大寫的表名能夠讓代碼工作以前,花了很多時(shí)間去檢查我 的代碼哪里出了問題。最后,"%"分配給列名模式以獲得所有的列。它可以解釋為“取得 給定表的所有列”。 GetColumns方法返回的java.sql.ResultSet結(jié)果集為每一個(gè)匹配的列返回一行。每行包 括18個(gè)描述域,這就是結(jié)果集列。與我們的類相關(guān)的域包括列名、列類型碼,它們是列 的第4和5個(gè)域。列類型碼是java.sql.Types 類中表示列中SQL類型的一個(gè)常量。 在這個(gè)例子中,通過表名對(duì)emp的getColumns調(diào)用會(huì)返回所有8個(gè)字段的一個(gè)描述。在結(jié) 果集中每行一個(gè)。列的類型碼已經(jīng)被取出并且存儲(chǔ)在稱為columnTypeCodes的類型數(shù)組中 ,這樣就可以被TableMediator類所調(diào)用。我們會(huì)在下面檢查這個(gè)過程。 第二層:TableMediator類 TableMediator類封裝了下面的功能: 創(chuàng)建一個(gè)預(yù)備語(yǔ)句(一個(gè)帶有參數(shù)的預(yù)編譯SQL語(yǔ)句,這些參數(shù)可以在運(yùn)行時(shí)提供)以灌 入表行。語(yǔ)句的參數(shù)對(duì)應(yīng)活動(dòng)列的值。 為一個(gè)活動(dòng)列的集合執(zhí)行這個(gè)預(yù)備語(yǔ)句。 TableMediator類使用TableColumns類以獲得給定表的完全列信息,然后將這些信息與給 定的活動(dòng)列集進(jìn)行比較以實(shí)現(xiàn)前面第一個(gè)圖中說明的配置。 PrepareRowInserts方法創(chuàng)建所需的預(yù)備語(yǔ)句。在這個(gè)例子中,預(yù)備語(yǔ)句象下面的樣子: insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (?, ?, ?, ?, ?, ?, ?, ?) 讓您的庫(kù)可移植的第一個(gè)關(guān)鍵因素是使用DatabaseMetaData接口來發(fā)現(xiàn)列的類型。如在 TableColumns類中所看到的。第二個(gè)是使用通用的java.sql.PreparedStatement 接口的 setObject方法: ps.setObject(activeColumnOrder[i], columnValues[i]); 這行代碼出現(xiàn)在TableMediator類的insertRow方法中,ps是先前創(chuàng)建的PreparedStatem ent實(shí)例。 SetObject方法需要兩個(gè)參數(shù):第一個(gè)是與之相聯(lián)系的預(yù)備語(yǔ)句中的參數(shù)的位置;第二個(gè) 是這個(gè)參數(shù)的值。通常,第二個(gè)參數(shù)可以是任何類型的對(duì)象;這段代碼中是字符串。JD BC驅(qū)動(dòng)器承擔(dān)將這個(gè)字符串轉(zhuǎn)換成合適的數(shù)據(jù)庫(kù)類型的責(zé)任。 在這個(gè)語(yǔ)句中,我們沒有使用事先發(fā)現(xiàn)的列類型。然而,我們確實(shí)需要值為空的列類型 ——例如要向數(shù)據(jù)庫(kù)中灌入沒有提供任何值的列。在這種情形,我們會(huì)在PreparedStat ement接口中使用一個(gè)setNull方法。這里是在TableMediator類的insertRow方法中使用 它的方法: ps.setNull(i, tc.columnTypeCodes[i]); 這個(gè)語(yǔ)句將預(yù)備語(yǔ)句參數(shù)I的值設(shè)置為空,但這個(gè)設(shè)置需要附帶傳遞對(duì)應(yīng)列的數(shù)據(jù)類型作 為第二個(gè)參數(shù)。 第三層:TableBuilder類 一旦建立了TableColumns 和TableMediator類,使用TableBuilder類將輸入數(shù)據(jù)灌入表 的代碼就很簡(jiǎn)單明了了。 TableBuilder封裝了下面的功能: 從文本文件中讀取第一行以確定表名并創(chuàng)建一個(gè)TableMediator類的實(shí)例。 從文本文件的第二行讀取列名,并要求TableMediator實(shí)例為這些列設(shè)置活動(dòng)列列表。 要求TableMediator實(shí)例準(zhǔn)備將列插入列表。 從文本文件的隨后的行中讀取列數(shù)據(jù)并要求TableMediator實(shí)例將這些數(shù)據(jù)插入表的下一 行。 使用tablebuild庫(kù) 給定tablebuild庫(kù),很容易寫一個(gè)可以灌數(shù)據(jù)庫(kù)表的應(yīng)用程序。下面是一個(gè)應(yīng)用實(shí)例: import java.sql.*; import java.io.*; import tablebuild.*; public class PopulateTable { public static final String usage = "usage: java PopulateTable " + ""; public static void main(String[] args) throws SQLException, ClassNotFoundException, IOException { if (args.length != 1) { System.err.println(usage); System.exit(1); } Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); Connection conn = DriverManager.getConnection( "jdbc:odbc:orgdb"); BufferedReader br = new BufferedReader( new FileReader(args[0])); // tab-delimited input data file TableBuilder tb = new TableBuilder(conn, br, " "); tb.buildTableInfo(); tb.buildActiveColumns(); tb.buildTable(); } } 這個(gè)例子使用JDBC-ODBC橋驅(qū)動(dòng)Microsoft Access數(shù)據(jù)庫(kù),數(shù)據(jù)源被命名為orgdb。我主 要選擇這個(gè)橋來說明是因?yàn)樗峁┝颂峁┐蟛糠諮DBC能力的參考實(shí)現(xiàn)。 為了完成這個(gè)例子,您可以通過上面的應(yīng)用程序向orgdb數(shù)據(jù)源的emp表灌入到數(shù)據(jù)。如 果輸入文件名是empfile,您可以這樣運(yùn)行這個(gè)應(yīng)用程序: java PopulateTable empfile 這樣就可以將給定數(shù)據(jù)灌入到表的hiredate、sal、ename和empno列。并將輸入數(shù)據(jù)的值 轉(zhuǎn)換成所需的數(shù)據(jù)庫(kù)指定數(shù)據(jù)類型。 如果您有一個(gè)新的數(shù)據(jù)源,您不需要修改應(yīng)用程序和tablebuild類。只要維護(hù)相同的輸 入格式。也就是說,第一行包含表名,第二行包含列名,隨后的行包含一些行的所有列 數(shù)據(jù),列的分隔符可以是任何東西,由于它是可以在TableBuilder類中通過參數(shù)進(jìn)行設(shè) 置的。 測(cè)試您的JDBC驅(qū)動(dòng)程序 本文中,您已經(jīng)知道了創(chuàng)建一個(gè)數(shù)據(jù)類型獨(dú)立的類庫(kù)向數(shù)據(jù)庫(kù)的表中灌入列數(shù)據(jù)的方法 。其關(guān)鍵是將所有的數(shù)據(jù)類型的管理工作留給JDBC的java.sql.DatabaseMetaData和jav a.sql.PreparedStatement接口的驅(qū)動(dòng)器實(shí)現(xiàn)。您可以使用本文中的說明方法完成數(shù)據(jù)庫(kù) 的插入操作,當(dāng)然也可以實(shí)現(xiàn)數(shù)據(jù)庫(kù)查詢和更新。 一個(gè)重要的警告:動(dòng)態(tài)類型發(fā)現(xiàn)和類型轉(zhuǎn)換的能力最終依賴于您所使用的JDBC驅(qū)動(dòng)器的 實(shí)現(xiàn)。例如,有些JDBC驅(qū)動(dòng)器可能不支持PreparedStatement的setObject方法的類型轉(zhuǎn) 換能力。另外,不同的JDBC驅(qū)動(dòng)器對(duì)DatabaseMetaData接口的實(shí)現(xiàn)是不同的。在JDBC編 程中的通用規(guī)則是,當(dāng)沒有實(shí)現(xiàn)JDBC規(guī)范中所允許的處理能力時(shí),就該懷疑JDBC驅(qū)動(dòng)程 序了。大多數(shù)情況下,一個(gè)好的驅(qū)動(dòng)器一定兼容JDBC規(guī)范,所以也就一定具有這個(gè)功能 。< |
|
|