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