James W. Cooper
IBM T.J. Watson 研究中心
摘自:Java 1.1 中面向对象编程的原理
摘要
本文分析了Java DataBase Connectivity (JDBC),一个在 Java 中以面向对象的方法来连接数据库的技术。它是对 ODBC API 进行的一种面向对象的封装和重新设计,它易于学习和使用,并且它能够使您编写不依靠供给商的代码,用以查询和操纵数据库。JDBC 提供了一些相当低层的方法来存取数据库,同时也在相当高层提供了功能强大的对象来处理数据库。
什么是数据库?
数据库是以某种文件结构存储的一系列信息表,这种文件结构使您能够访问这些表、选择表中的列、对表进行排序以及根据各种标准选择行。数据库通常有多个 索引与这些表中的许多列相关联,所以我们能尽可能快地访问这些表。
在进行计算时,数据库比其它类型的结构更常用。您会发现在员工记录和薪资系统中数据库处于核心地位,在旅行计划系统中以及在产品生产和销售的整个过程中都可以发现数据库。
以员工记录为例,您可以设想一个含有员工姓名、地址、工资、扣税以及津贴等内容的表。让我们考虑一下这些内容可能如何组织在一起。您可以设想一个表包含员工姓名、地址和电话号码。您希望保存的其它信息可能包括工资、工资范围、上次加薪时间、下次加薪时间、员工业绩评定等内容。
这些内容是否应保存在一个表格中?几乎可以肯定不应该如此。不同类别的员工的工资范围可能没有区别;这样,您可以仅将员工类型储存在员工记录表中,而将工资范围储存在另一个表中,通过类型编号与这个表关联。考虑以下情况:
Key Lastname SalaryType SalaryType Min Max
1 Adams 2 1 30000 45000
2 Johnson 1 2 45000 60000
3 Smyth 3 3 60000 75000
4 Tully 1
5 Wolff 2
SalaryType 列中的数据引用第二个表。我们可以想象出许多种这样的表,如用于存储居住城市和每个城市的税值、健康计划扣除金额等的表。每个表都有一个主键列(如上面两个表中最左边的列)和若干数据列。在数据库中建立表格既是一门艺术,也是一门科学。这些表的结构由它们的 范式指出。我们通常说表属于第一、第二或第三范式,简称为 1NF、2NF 或 3NF。
第一范式:表中的每个表元应该只有一个值(永远不可能是一个数组)。(1NF)
第二范式:满足 1NF,并且每一个副键列完全依靠于主键列。这表示主键和该行中的剩余表元之间是 1 对 1 的关系。(2NF)
第三范式:满足 2NF,并且所有副键列是互相独立的。任何一个数据列中包含的值都不能从其他列的数据计算得到。(3NF)
现在,几乎所有的数据库都是基于“第三范式 (3NF)”创建的。这意味着通常都有相当多的表,每个表中的信息列都相对较少。
从数据库中获取数据
假设我们希望生成一个包含员工及其工资范围的表,在我们设计的一个练习中将使用这个表。这个表格不是直接存在在数据库中,但可以通过向数据库发出一个查询来构建它。我们希望得到如下所示的一个表:
Name Min Max
Adams $45,000.00 $60,000.00
Johnson $30,000.00 $45,000.00
Smyth $60,000.00 $75,000.00
Tully $30,000.00 $45,000.00
Wolff $45,000.00 $60,000.00
或者,按照工资递增的顺序排序
Name Min Max
Tully $30,000.00 $45,000.00
Johnson $30,000.00 $45,000.00
Wolff $45,000.00 $60,000.00
Adams $45,000.00 $60,000.00
Smyth $60,000.00 $75,000.00
我们发现,获得这些表的查询形式如下所示
SELECT DISTINCTROW Employees.Name, SalaryRanges.Min,
SalaryRanges.Max FROM Employees INNER JOIN SalaryRanges ON Employees.SalaryKey = SalaryRanges.SalaryKey
ORDER BY SalaryRanges.Min;
这种语言称为结构化查询语言,即 SQL(一般读作 "sequel"),而且它是几乎目前所有数据库都可以使用的一种语言。这几年已颁布了若于 SQL 标准,而且大多数 PC 数据库支持大部分 ANSI 标准。SQL-92 标准被认为是一种基础标准,而且已更新多次。然而,没有一种数据库可以完美地支持后来的 SQL 版本,而且大多数数据库都提供了多种 SQL 扩展,以支持他们数据库独有的性能。
数据库的种类
由于 PC 已成为主要的办公工具,因此,已开发出在 PC 上的大量流行的数据库,这些数据库都是可以自治理的。它们包括初级数据库,如 Microsoft Works,还包括更为复杂的数据库,如 Approach、 dBase、Borland Paradox、Microsoft Access 和 FoxBase。
另一类 PC 数据库包括那些可由许多 PC 客户机通过服务器访问的数据库。其中包括 IBM DB/2、Microsoft SQL Server、 Oracle、Sybase、SQLBase 和 XDB。所有这些数据库产品都支持多种相对类似的 SQL 方言,因此,所有数据库最初看起来好象可以互换。当然,它们 不能互换的原因是每种数据库都有不同的性能特征,而且每一种都有不同的用户界面和编程接口。您可能会想,既然它们都支持 SQL,对它们进行的编程也应该相似,但这是绝对错误的,因为每种数据库都使用其自己方式接收 SQL 查询,并使用其自己的方式返回结果。这就自然引出了一种新一代的标准:ODBC
ODBC
假如我们能够以某种方式编写不依靠于特定厂商的数据库的代码,并且能够不改变自己的调用程序即可从这些数据库中得到相同的结果,那将是一件很好的事。假如我们可以仅为所有这些数据库编写一些封装,使它们具有相似的编程接口,这种对数据库编程独立于供给商的特性将很轻易实现。
Microsoft 于 1992 年首先尝试了这一技巧,该公司发布了一个规范,称为对象数据库连接性。这被认为是在 Windows 环境下连接所有数据库的答案。与所有软件的第一个版本相同,它也经历了一些发展的困扰,在 1994 年推出了另一个版本,该版本运行速度更快,而且更为稳定。它也是第一个 32 位的版本。另外,ODBC 开始向 Windows 之外的其它平台发展,到目前为止,它在 PC 和工作站领域已十分普遍。几乎每个主要的数据库厂商都提供 ODBC 驱动程序。
然而,ODBC 并不是我们最初想象的灵丹妙药。许多数据库厂商都将 ODBC 作为其标准接口之外的“备选接口”,而且对 ODBC 的编程微不足道。与其它 Windows 编程一样,它包括句柄、指针和选项,使其难以把握。最后一点,ODBC 不是中立的标准。它由 Microsoft 公司开发,并由该公司不断改进,而微软同时也推出了我们所有人所使用的极具竞争性的软件平台,这使得ODBC的未来难以猜测。
什么是 JDBC?
JDBC 是一组首字母缩写,曾经代表“Java DataBase Connectivity”,但现在它本身已成为一个商标符号。它是对 ODBC API 进行的一种面向对象的封装和重新设计,它易于学习和使用,并且它真正能够使您编写不依靠厂商的代码,用以查询和操纵数据库。尽管它与所有 Java API 一样,都是面向对象的,但它并不是很高级别的对象集,在本章的剩余部分,我们将提出更高级别的方法。
除 Microsoft 之外,多数厂商都采用了 JDBC,并为其数据库提供了 JDBC 驱动程序;这使您可轻松地真正编写几乎完全不依靠数据库的代码。另外,JavaSoft 和 Intersolv 已开发了一种称为 JDBC-ODBC Bridge 的产品,可使您连接还没有直接的 JDBC 驱动程序的数据库。支持 JDBC 的所有数据库必须至少可以支持 SQL-92 标准。这在很大程度上实现了跨数据库和平台的可移植性。
安装和使用 JDBC
JDBC 的类都被归到 java.sql 包中,在安装 Java JDK 1.1 或更高版本时会自动安装。然而,假如您想使用 JDBC-ODBC 桥,还必须安装两个另外的程序包。首先,假如您使用 Windows 95,则必须将您的 ODBC 驱动程序升级为 32 位驱动程序,您可从 Microsoft 的网站下载。这个驱动程序在 Microsoft 的网站上很难找到;请搜索 DataAcc.exe 并进行下载和安装。
JDBC-ODBC 驱动程序可从 Sun 的 Java 网站 (http://java.sun.com) 轻松地找到并下载。在您扩充并安装了这个驱动程序后,必须执行下列步骤:
将 jdbc-odbcclasses; 路径添加到您的 PATH 环境变量中。
将 jdbc-odbcclasses; 路径添加到您的 CLASSPATH 环境变量中。
在 Windows 95 环境下,将它们放入 autoexec.bat 文件中,重新引导,以使所有设置生效。
在 Windows NT 环境下,将它们添加到“控制面板”中“系统”对象的“环境”选项卡中,退出并重新登录,以使其生效。
JDBC 驱动程序的类型
Java 程序连接数据库的方法实际上有四种:
JDBC-ODBC 桥和 ODBC 驱动程序 -- 在这种方式下,这是一个本地解决方案,因为 ODBC 驱动程序和桥代码必须出现在用户的每台机器中。从根本上说这是一个临时解决方案。
本机代码和 Java 驱动程序 -- 它用另一个本地解决方案(该平台上的 Java 可调用的本机代码)取代 ODBC 和 JDBC-ODBC 桥。
JDBC 网络的纯 Java 驱动程序 -- 由 Java 驱动程序翻译的 JDBC 形成传送给服务器的独立协议。然后,服务器可连接任何数量的数据库。这种方法使您可能从客户机 Applet 中调用服务器,并将结果返回到您的 Applet。在这种情况下,中间件软件提供商可提供服务器。
本机协议 Java 驱动程序 -- Java 驱动程序直接转换为该数据库的协议并进行调用。这种方法也可以通过网络使用,而且可以在 Web 浏览器的 Applet 中显示结果。在这种情况下,每个数据库厂商将提供驱动程序。
假如您希望编写代码来处理 PC 客户机数据库,如 dBase、Foxbase 或 Access,则您可能会使用第一种方法,并且拥有用户机器上的所有代码。更大的客户机-服务器数据库产品(如 IBM 的 DB2)已提供了第 3 级别的驱动程序。
两层模型和三层模型
当数据库和查询它的应用程序在同一台机器上,而且没有服务器代码的干预时,我们将生成的程序称为 两层模型。一层是应用程序,而另一层是数据库。在 JDBC-ODBC 桥系统中通常是这种情况。
当一个应用程序或 applet 调用服务器,服务器再去调用数据库时,我们称其为三层模型。当您调用称为“服务器”的程序时通常是这种情况。
编写 JDBC 代码访问数据库
现在,我们将开始看一下如何编写 Java 程序来访问数据库。我们要使用的数据库是一个称为 groceries.mdb 的 Microsoft Access 数据库。此数据库中的数据由三个本地杂货店中一些常见商品的价格组成。食品表如下所示:
FoodKey FoodName
1 Apples
2 Oranges
3 Hamburger
4 Butter
5 Milk
6 Cola
7 Green beans
杂货店表如下所示:
StoreKey StoreName
1 Stop and Shop
2 Village Market
3 Waldbaum′s
杂货店定价表仅由这三个表格中的键值和价格组成:
FSKey StoreKey FoodKey Price
1 1 1 $0.27
2 2 1 $0.29
3 3 1 $0.33
4 1 2 $0.36
5 2 2 $0.29
6 3 2 $0.47
7 1 3 $1.98
8 2 3 $2.45
9 3 3 $2.29
10 1 4 $2.39
11 2 4 $2.99
12 3 4 $3.29
13 1 5 $1.98
14 2 5 $1.79
15 3 5 $1.89
16 1 6 $2.65
17 2 6 $3.79
18 3 6 $2.99
19 1 7 $2.29
20 2 7 $2.19
21 3 7 $1.99
用 ODBC 注册您的数据库
在 Windows 95 或 NT 环境下访问 ODBC 数据库之前,必须使用控制面板中的ODBC 驱动程序对它进行注册。在 Windows 95 环境下,就是“控制面板”程序中的 ODBC 图标。在 Windows NT 环境下,您会在“开始”菜单中找到此程序。(假如找不到,您需要安装上述的 ODBC 驱动程序,即 WX1350.exe)。
双击 ODBC 图标,然后单击“添加”,如图 1 所示。然后选择数据库驱动程序(此处使用 Microsoft Access),然后单击“确定”。在“数据源名”和“描述”中分别键入数据源名称 (Groceries) 和数据库说明 (Grocery prices)(这两项都不需要和文件名相关),然后单击“选取”,找到数据库,并选择该数据库。找到该数据库后,屏幕将如图 2 所示。单击“确定”,然后单击“关闭”来关闭面板。
图 1:ODBC 控制面板设置屏幕。
图 2:在 ODBC 控制面板中选择数据库和说明。
连接数据库
所有与数据库有关的对象和方法都在 java.sql 包中,因此在使用 JDBC 的程序中必须加入 "import java.sql.* "。 JDBC 要连接 ODBC 数据库,您必须首先加载 JDBC-ODBC 桥驱动程序
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
该语句加载驱动程序,并创建该类的一个实例。然后,要连接一个特定的数据库,您必须创建 Connect 类的一个实例,并使用 URL 语法连接数据库。
String url = "jdbc:odbc:Grocery prices";
Connection con = DriverManager.getConnection(url);
请注重,您使用的数据库名是您在 ODBC 设置面板中输入的“数据源”名称。
URL 语法可能因数据库类型的不同而变化极大。
jdbc:subprotocol:subname
第一组字符代表连接协议,并且始终是 jdbc。还可能有一个子协议,在此处,子协议被指定为 odbc。它规定了一类数据库的连通性机制。假如您要连接其它机器上的数据库服务器,可能也要指定该机器和一个子目录:
jdbc:bark//doggie/elliott
最后,您可能要指定用户名和口令,作为连接字符串的一部分:
jdbc:bark//doggie/elliot;UID=GoodDog;PWD=woof
访问数据库
一旦连接到数据库,就可以请求表名以及表列的名称和内容等信息,而且您可以运行 SQL 语句来查询数据库或者添加或修改其内容。可用来从数据库中获取信息的对象有:
DatabaseMetaData 有关整个数据库的信息:表名、表的索引、数据库产品的名称和版本、数据库支持的操作。
ResultSet 关于某个表的信息或一个查询的结果。您必须逐行访问数据行,但是您可以任何顺序访问列。
ResultSetMetaData 有关 ResultSet 中列的名称和类型的信息。
尽管每个对象都有大量的方法让您获得数据库元素的极为具体的信息,但在每个对象中都有几种主要的方法使您可获得数据的最重要信息。然而,假如您希望看到比此处更多的信息,建议您学习文档以获得其余方法的说明。
ResultSet
ResultSet 对象是 JDBC 中最重要的单个对象。从本质上讲,它是对一个一般宽度和未知长度的表的一种抽象。几乎所有的方法和查询都将数据作为 ResultSet 返回。ResultSet 包含任意数量的命名列,您可以按名称访问这些列。它还包含一个或多个行,您可以按顺序自上而下逐一访问。在您使用 ResultSet 之前,必须查询它包含多少个列。此信息存储在 ResultSetMetaData 对象中。
//从元数据中获得列数
ResultSetMetaData rsmd;
rsmd = results.getMetaData();
numCols = rsmd.getColumnCount();
当您获得一个 ResultSet 时,它正好指向第一行之前的位置。您可以使用 next() 方法得到其他每一行,当没有更多行时,该方法会返回 false。由于从数据库中获取数据可能会导致错误,您必须始终将结果集处理语句包括在一个 try 块中。
try
{
rsmd = results.getMetaData();
numCols = rsmd.getColumnCount();
boolean more = results.next();
while (more)
{
for (i = 1; i <= numCols; i++)
System.out.print(results.getString(i)+" ");
System.out.println();
more = results.next();
}
results.close();
}
catch(Exception e)
{System.out.println(e.getMessage());}
您可以多种形式获取 ResultSet 中的数据,这取决于每个列中存储的数据类型。另外,您可以按列序号或列名获取列的内容。请注重,列序号从 1 开始,而不是从 0 开始。ResultSet 对象的一些最常用方法如下所示。
getInt(int);
将序号为 int 的列的内容作为整数返回。
getInt(String);
将名称为 String 的列的内容作为整数返回。
getFloat(int);
将序号为 int 的列的内容作为一个 float 型数返回。
getFloat(String);
将名称为 String 的列的内容作为 float 型数返回。
getDate(int);
将序号为 int 的列的内容作为日期返回。
getDate(String);
将名称为 String 的列的内容作为日期返回。
next();
将行指针移到下一行。假如没有剩余行,则返回 false。
close();
关闭结果集。
getMetaData();
返回 ResultSetMetaData 对象。
ResultSetMetaData
您使用 getMetaData() 方法从 ResultSet 中获取 ResultSetMetaData 对象。您可以使用此对象获得列的数目和类型以及每一列的名称。
getColumnCount();
返回 ResultSet 中的列数。
getColumnName(int);
返回列序号为 int 的列名。
getColumnLabel(int);
返回此列暗含的标签。
isCurrency(int);
假如此列包含带有货币单位的一个数字,则返回 true。
isReadOnly(int);
假如此列为只读,则返回 true。
isAutoIncrement(int);
假如此列自动递增,则返回 true。这类列通常为键,而且始终是只读的。
getColumnType(int);
返回此列的 SQL 数据类型。这些数据类型包括
BIGINT
BINARY
BIT
CHAR
DATE
DECIMAL
DOUBLE
FLOAT
INTEGER
LONGVARBINARY
LONGVARCHAR
NULL
NUMERIC
OTHER
REAL
SMALLINT
TIME
TIMESTAMP
TINYINT
VARBINARY
VARCHAR
DatabaseMetaData
DatabaseMetaData 对象可为您提供整个数据库的信息。您主要用它获取数据库中表的名称,以及表中列的名称。由于不同的数据库支持不同的 SQL 变体,因此,也有多种方法查询数据库支持哪些 SQL 方法。
getCatalogs()
返回该数据库中的信息目录列表。使用 JDBC-ODBC Bridge 驱动程序,您可以获得用 ODBC 注册的数据库列表。这很少用于 JDBC-ODBC 数据库。
getTables(catalog, schema, tableNames, columnNames) 返回表名与 tableNames 相符而且列名与 columnNames 相符的所有表的说明。
getColumns(catalog, schema, tableNames, columnNames) 返回表名与 tableNames 相符而且列名与 columnNames 相符的所有表列说明。
getURL();
获得您所连接的 URL 名称。
getDriverName();
获得您所连接的数据库驱动程序的名称。
获取有关表的信息
您可以使用 DataBaseMetaData 的 getTables() 方法来获取数据库中表的信息。这个方法有如下 4 个 String 参数:
results =
dma.getTables(catalog, schema, tablemask, types[]);
其中参数的意义是:
catalog
要在其中查找表名的目录名。对于 JDBC-ODBC 数据库以及许多其他数据库而言,可将其设置为 null。这些数据库的目录项实际上是它在文件系统中的绝对路径名称。
schema
要包括的数据库“方案”。许多数据库不支持方案,而对另一些数据库而言,它代表数据库所有者的用户名。一般将它设置为 null。
tablemask
一个掩码,用来描述您要检索的表的名称。假如您希望检索所有表名,则将其设为通配符 %。请注重,SQL 中的通配符是 % 符号,而不是一般 PC 用户的 * 符号。
types[]
这是描述您要检索的表的类型的 String 数组。数据库中通常包括许多用于内部处理的表,而对作为用户的您没什么价值。假如它是空值,则您会得到所有这些表。假如您将其设为包含字符串“TABLES”的单元素数组,您将仅获得对用户有用的表格。
用于从数据库中获取表名的简单代码相当于获取 DatabaseMetaData 对象,并从其中检索表名:
con = DriverManager.getConnection(url);
//获取数据库的元数据
dma =con.getMetaData();
//将数据库中的表的名称转储出来
String[] types = new String[1];
types[0] = "TABLES"; //设置查询类型
//请注重通配符是 % 符号(而不是“*”)
results = dma.getTables(null, null, "%", types);
然后,我们可以打印出表名,正如我们上面所做的那样:
boolean more = results.next();
while (more)
{
for (i = 1; i <= numCols; i++)
System.out.print(results.getString(i)+" ");
System.out.println();
more = results.next();
}
如前文所述,将所有代码包括在 try 块中。
执行 SQL 查询
我们已经理解了 JDBC 的基本对象,现在就可以执行 SQL 查询了。查询是作为 Statement 对象的方法执行的,您很轻易从 Connection 对象获得Statement对象:
String query = "SELECT FoodName FROM Food;";
ResultSet results;
try
{
Statement stmt = con.createStatement();
results = stmt.executeQuery(query);
}
catch (Exception e)
{System.out.println("query exception");}
请注重,这个简单的查询返回 Food 表中的整个 FoodName 列。您使用像这样的简单查询获取整个列的内容。请注重,查询的查询本身是一个 ResultSet,您可以用我们上面刚讨论过的方法对它进行处理。
打印 ResultSet
因为我们总是要从 ResultSets 中打印数据,我们可以设计一种简单的方法,将整个 ResultSet 转储出来,包括表名称元数据。该子程序如下所示:
private void dumpResults(String head)
{
//这是打印列标头和每列的内容的
//通用方法
System.out.println(head);
try
{
//从元数据中获取列数
rsmd = results.getMetaData();
numCols = rsmd.getColumnCount();
//打印列名
for (i = 1; i<= numCols; i++)
System.out.print(rsmd.getColumnName(i)+" ");
System.out.println();
//打印列内容
boolean more = results.next();
while (more)
{
for (i = 1; i <= numCols; i++)
System.out.print(results.getString(i)+" ";
System.out.println();
more = results.next();
}
}
catch(Exception e)
{System.out.println(e.getMessage());}
}
一个简单的 JDBC 程序
我们已经学习了 JDBC 的所有基本功能,现在我们可以编写一个简单的程序,该程序打开数据库,打印它的表名以及某一表列的内容,然后对该数据库执行查询。此程序如下所示:
import java.net.URL;
import java.sql.*;
import java.util.*;
class JdbcOdbc_test
{
ResultSet results;
ResultSetMetaData rsmd;
DatabaseMetaData dma;
Connection con;
int numCols, i;
//--
public JdbcOdbc_test()
{
String url = "jdbc:odbc:Grocery prices";
String query = "SELECT DISTINCTROW FoodName FROM Food " + "WHERE (FoodName like ′C%′);";
try
{
//加载 JDBC-ODBC 桥驱动程序
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
//连接数据库
con = DriverManager.getConnection(url);
//获取数据库的元数据
dma =con.getMetaData();
System.out.println("Connected to:"+dma.getURL());
System.out.println("Driver "+dma.getDriverName());
//将数据库中的表的名称转储出来
String[] types = new String[1];
types[0] = "TABLES";
//请注重通配符是 % 符号(而不是“*”)
results = dma.getTables(null, null, "%", types);
dumpResults("--Tables--");
results.close();
}
catch (Exception e)
{System.out.println(e);}
//获取表列的名称
System.out.println("--Column Names--");
try {
results =
dma.getColumns(null, null, "FoodPrice", null);
ResultSetMetaData rsmd = results.getMetaData();
int numCols = rsmd.getColumnCount();
while (results.next() )
String cname = results.getString("COLUMN_NAME");
System.out.print(cname + " ");
System.out.println();
results.close();
}
catch (Exception e)
{System.out.println(e);}
//列出一个列的内容 -- 这是一个查询
try {
Statement stmt = con.createStatement();
results =
stmt.executeQuery("SELECT FOODNAME FROM FOOD;");
}
catch (Exception e)
{System.out.println("query exception");}
dumpResults("--Contents of FoodName column--");
//尝试实际的 SQL 语句
try
{
Statement stmt = con.createStatement();
results = stmt.executeQuery(query);
}
catch (Exception e)
{System.out.println("query exception");}
dumpResults("--Results of Query--");
}
该程序打印出的结果如下所示:
C:ProjectsobjectJavachapter19>java JdbcOdbc_test
Connected to:jdbc:odbc:Grocery prices
Driver JDBC-ODBC Bridge (ODBCJT32.DLL)
--Tables--
TABLE_QUALIFIER TABLE_OWNER TABLE_NAME TABLE_TYPE REMARKS
groceries null Food TABLE null
groceries null FoodPrice TABLE null
groceries null Stores TABLE null
--Column Names--
FSKey StoreKey FoodKey Price
--Contents of FoodName column--
FOODNAME
Apples
Oranges
Hamburger
Butter
Milk
Cola
Green beans
--Results of Query--
FoodName
Cola
构建更高级别的 JDBC 对象
从上面的例子可以明显看出,假如可以将我们使用过的一些方法封装在几个更高级别对象中,那将非常有帮助,我们不仅可以封装 try 程序块,而且可以更简单地访问 ResultSet 方法。
在这一部分中,我们将构建一个新的 resultSet 对象,该对象封装了 JDBC ResultSet 对象,并以 String 数组的形式返回一行数据。我们发现您始终需要从 ResultSetMetaData 对象中获取列的序号和名称,因此,创建一个封装元数据的新对象就非常合理。
另外,我们经常需要按名称或整数索引提取某行的元素,假如不必总是将这些访问语句包括 try 块中,那将大有帮助。最后一点,假如我们需要整行的内容,则更方便的做法是将整行以String 数组形式返回。在下面所示的 resultSet 对象中,我们致力于实现这些目标:
class resultSet
{
//这个类是 JDBC ResultSet 对象的更高级抽象
ResultSet rs;
ResultSetMetaData rsmd;
int numCols;
public resultSet(ResultSet rset)
{
rs = rset;
try
{
//同时获取元数据和列数
rsmd = rs.getMetaData();
numCols = rsmd.getColumnCount();
}
catch (Exception e)
{System.out.println("resultset error"
+e.getMessage());}
}
//--
public String[] getMetaData()
{
//返回包含所有列名或其他元数据的
//一个数组
String md[] = new String[numCols];
try
{
for (int i=1; i<= numCols; i++)
md[i-1] = rsmd.getColumnName(i);
}
catch (Exception e)
{System.out.println("meta data error"+
e.getMessage());}
return md;
}
//--
public boolean hasMoreElements()
{
try{
return rs.next();
}
catch(Exception e){return false;}
}
//--
public String[] nextElement()
{
//将行的内容复制到字符串数组中
String[] row = new String[numCols];
try
{
for (int i = 1; i <= numCols; i++)
row[i-1] = rs.getString(i);
}
catch (Exception e)
{System.out.println("next element error"+
e.getMessage());}
return row;
}
//--
public String getColumnValue(String columnName)
{
String res = "";
try
{
res = rs.getString(columnName);
}
catch (Exception e)
{System.out.println("Column value error:"+
columnName+e.getMessage());}
return res;
}
//--
public String getColumnValue(int i)
{
String res = "";
try
{
res = rs.getString(i);
}
catch (Exception e)
{System.out.println("Column value error:"+
columnName+e.getMessage());}
return res;
}
//--
public void finalize()
{
try{rs.close();}
catch (Exception e)
{System.out.println(e.getMessage());}
}
}
通过简单使用 new 操作符就地创建一个 ResultSet 对象,我们很轻易将任何 ResultSet 对象封装在此类中:
ResultSet results = .. //按通常的方法获得ResultsSet
//利用它创建一个更有用的对象
resultSet rs = new resultSet(results);
并很轻易在任何 JDBC 程序中使用这个对象。
构建一个 Database 对象
我们沿 00 链向上移的另一部分努力是创建一个 Database 对象,它将封装下列对象的行为:Connection、Statement 和 DatabaseMetaData 对象, 以及我们刚刚构建的 SQL 查询和 resultSet。我们的 Database 对象答应我们创建连接、获取表名、在数据库中移动以及更简单地获得行和列的值。请注重, Execute 方法返回一个 resultSet 对象,您可以直接对它进行操作。
class Database
{
//这是一个将 JDBC 数据库的所有功能封装在单个对象中的类
Connection con;
resultSet results;
ResultSetMetaData rsmd;
DatabaseMetaData dma;
String catalog;
String types[];
public Database(String driver)
{
types = new String[1];
types[0] = "TABLES"; //初始化类型
try{Class.forName(driver);} //加载 JDBC-ODBC 桥驱动程序
catch (Exception e)
{System.out.println(e.getMessage());}
}
//--
public void Open(String url, String cat)
{
catalog = cat;
try {con = DriverManager.getConnection(url);
dma =con.getMetaData(); //获取元数据
}
catch (Exception e)
{System.out.println(e.getMessage());}
}
//--
public String[] getTableNames()
{
String[] tbnames = null;
Vector tname = new Vector();
//将表名添加到一个 Vector 中,
//因为我们不知道有多少个表
try {
results =
new resultSet(dma.getTables(catalog, null,
"%", types));
while (results.hasMoreElements())
tname.addElement(results.getColumnValue("TABLE_NAME"));
}
catch (Exception e) {System.out.println(e);}
//将表名复制到一个 String 数组中
tbnames = new String[tname.size()];
for (int i=0; i< tname.size(); i++)
tbnames[i] = (String)tname.elementAt(i);
return tbnames;
}
//--
public String[] getTableMetaData()
{
// 返回表类型的信息
results = null;
try{
results =
new resultSet(dma.getTables(catalog, null,
"%", types));
}
catch (Exception e)
{System.out.println(e.getMessage());}
return results.getMetaData();
}
//--
public String[] getColumnMetaData(String tablename)
{
//返回一个列的数据
results = null;
try {
results =
new resultSet(dma.getColumns(catalog, null,
tablename, null));
}
catch (Exception e)
{System.out.println(e.getMessage());}
return results.getMetaData();
}
//--
public String[] getColumnNames(String table)
{
//返回一个列名数组
String[] tbnames = null;
Vector tname = new Vector();
try {
results =
new resultSet(dma.getColumns(catalog, null,
table, null));
while (results.hasMoreElements() )
tname.addElement(results.getColumnValue("COLUMN_NAME"));
}
catch (Exception e) {System.out.println(e);}
tbnames = new String[tname.size()];
for (int i=0; i< tname.size(); i++)
tbnames[i] = (String)tname.elementAt(i);
return tbnames;
}
//--
public String getColumnValue(String table,
String columnName)
{
//返回给定列的值 String res = null;
try
{
if (table.length()>0)
results =
Execute("Select " + columnName +
" from " + table +
" order by "+columnName);
if (results.hasMoreElements())
res = results.getColumnValue(columnName);
}
catch (Exception e)
{System.out.println("Column value error" +
columnName+ e.getMessage());}
return res;
}
//--
public String getNextValue(String columnName)
{
// 使用存储的 resultSet
//返回该列的下一个值
String res = "";
try
{
if (results.hasMoreElements())
res = results.getColumnValue(columnName);
}
catch (Exception e)
{System.out.println("next value error"+
columnName+ e.getMessage());}
return res;
}
//--
public resultSet Execute(String sql)
{
//对此数据库执行一个 SQL 查询
results = null;
try
{
Statement stmt = con.createStatement();
results = new resultSet(stmt.executeQuery(sql));
}
catch (Exception e)
{System.out.println("execute error"+
e.getMessage());}
return results;
}
}
一个可视化的数据库程序
为了对我们本章学习的内容进行总结,我们编写一个简单的 GUI 程序,它可以显示数据库的表名、列名和列内容。我们还将包括一个文本区域,您可以在其中键入一个要对数据库执行的 SQL 查询。在 Companion CD-ROM 上的 chapter20 子目录中,可以找到本程序(称为 dbFrame.java)所使用的 resultSet 和 Database 类。程序的显示界面如图 3 所示。
图 3:用来显示用 JDBC 连接的数据库中的数据的 dbFrame.java 程序。
在本程序中,默认数据库 (groceries.mdb) 的表名显示在左侧的栏中。当您单击其中一个表名时,列名就会显示在中间的栏中。最后,当您单击中间栏中的某一行时,该行的内容就会显示在右侧的栏中。
本程序的要害只是接收列表选择,然后清除并填充正确的列表框:
public void itemStateChanged(ItemEvent e)
{
Object obj = e.getSource();
if (obj == Tables) //放入列名
showColumns();
if (obj == Columns) //放入列的内容
showData();
}
//--
private void loadList(List list, String[] s)
{
//清除并填充指定的列表框
list.removeAll();
for (int i=0; i< s.length; i++)
list.add(s[i]);
}
//--
private void showColumns()
{
//显示列名
String cnames[] =
db.getColumnNames(Tables.getSelectedItem());
loadList(Columns, cnames);
}
//--
private void showData()
{
String colname = Columns.getSelectedItem();
String colval =
db.getColumnValue(Tables.getSelectedItem(),
colname);
Data.setVisible(false);
Data.removeAll();
Data.setVisible(true);
colval =
db.getNextValue(Columns.getSelectedItem());
while (colval.length()>0)
{
Data.add(colval);
colval =
db.getNextValue(Columns.getSelectedItem());
}
}
执行查询
显示画面底部的文本区域使您可键入所需的任何 SQL 查询。演示程序中构建的一个查询如下所示:
String queryText =
"SELECT DISTINCTROW FoodName, StoreName, Price "+
"FROM (Food INNER JOIN FoodPrice ON "+
"Food.FoodKey = FoodPrice.FoodKey) " +
"INNER JOIN Stores ON "+
"FoodPrice.StoreKey = Stores.StoreKey "+
"WHERE (((Food.FoodName)=′Oranges′)) "+
" ORDER BY FoodPrice.Price;";
此查询简单地列出每个杂货店的桔子价格。
当您单击 Run Query 按钮时,它将执行此查询,并将 resultSet 对象传送给一个对话框进行显示:
public void actionPerformed(ActionEvent e)
{
Object obj = e.getSource();
if (obj == Quit)
System.exit(0);
if (obj == Search)
clickedSearch();
}
//--
private void clickedSearch()
{
resultSet rs = db.Execute(query.getText());
String cnames[] = rs.getMetaData();
queryDialog q = new queryDialog(this, rs);
q.show();
}
查询结果对话框
查询对话框获得 resultSet 对象,并将每一行放入一个 String 数组中,然后将这些 String 数组放入一个 Vector 中,这样就可以在 paint() 子程序运行期间快速访问这些行。
private void makeTables()
{
//将每一行放入一个 String 数组中,并将
//这些字符串数组全部放入一个 Vector 中
tables = new Vector();
String t[] = results.getMetaData();
tables.addElement( t);
while (results.hasMoreElements())
tables.addElement(results.nextElement());
}
我们通过 Graphics 的 drawString() 方法将数据绘制在一个 Panel 中。就像在 Printer 对象中一样,我们必须自己跟踪 x 和 y 的位置。
public void paint(Graphics g)
{
String s[];
int x=0;
//计算字体的高度
int y =g.getFontMetrics().getHeight();
//估算列的高度
int deltaX = (int)1.5f*
(g.getFontMetrics().stringWidth("wwwwwwwwwwwwww"));
//遍历表矢量
for (int i=0; i< tables.size(); i++)
{
s = (String[])tables.elementAt(i);
//绘制字符串数组中的每一行
for (int j =0; j< s.length; j++)
{
String st= s[j];
g.drawString(st, x, y);
x += deltaX; //移到下一列
}
x = 0; //开始一个新行
y += g.getFontMetrics().getHeight();
//列标签与列数据之间的额外空间
if (i == 0) y += g.getFontMetrics().getHeight();
}
}
内建查询的 queryDialog 如图 4 所示。
图 4:dbFrame 程序中 显示的 queryDialog,其中显示的是默认查询的结果。
示例文件
groceries.zip
dbFrame.zip
jdbc-odbc Bridge
小结
在本文中,我们讨论了数据库以及检验数据库并对数据库执行查询的方法。我们已经看到,JDBC 提供了一种与平台和数据库无关的、面向对象的方法来访问这些数据,我们还学习了 JDBC 的主要对象:ResultSet、ResultSetMetaData 和 DatabaseMetaData。在用这些对象编写了一个简单的程序之后,我们设计了更高级别的 resultSet 和 Database 对象,我们用它们构建了一个简单的可视化界面来显示数据库信息。
假如您熟悉数据库的强大功能,就会熟悉到 SQL 语言可使您执行比我们此处所述操作更强大的任务。例如,您可以创建新表、添加、更改或删除表的行、列或单个表元。使用 JDBC,所有这一切都变得通用和易于处理。
假如您使用的是特定平台的数据库驱动程序,如 JDBC-ODBC Bridge,则您在编写应用程序时会受到限制,因为 applet 不能连接在另一台计算机上运行的这个桥。其他客户机-服务器数据库,如 IBM 的 DB2,答应您使用 applet 中的 JDBC 与其连接。
要了解本网站上有关 Java 和数据库连接性的更多信息,请访问: http://www.cnet.com/Content/Builder/Programming/Databases/?st.bl.fd.prog1.feat