James W. Cooper IBM T.J. Watson 研究中心 1998 年 5 月 |
<!-- TABLE CELLPADDING=2 CELLSPACING=0 BORDER=0 WIDTH=125> <TR VALIGN=CENTER> <TD BGCOLOR="#3333CC"> <FONT FACE="HELVETICA, HELV, ARIAL" COLOR="#FFFFFF" SIZE="-1"><b>Download it now!</b></font></TD> </TR> <TR BGCOLOR="#CCCCCC"> <TD VALIGN=BOTTOM BGCOLOR="#FFFFFF"> <FONT FACE="HELVETICA, HELV, ARIAL" SIZE="-1"> <A HREF="ftp://www6.software.ibm.com/software/developerWorks/library/jdbc-objects.pdf"><B>PDF</B></A> </FONT> <FONT FACE="HELVETICA, HELV, ARIAL" SIZE="-2"> (209 KB) <BR> <A HREF="http://www.adobe.com/prodindex/acrobat/readstep.html" ><B>Free Acrobat™ Reader</B></A> </FONT> </TD></TR></TABLE --> |
在进行计算时,数据库比其它类型的结构更常用。您会发现在员工记录和薪资系统中数据库处于核心地位,在旅行计划系统中以及在产品生产和销售的整个过程中都可以发现数据库。
以员工记录为例,您可以设想一个含有员工姓名、地址、工资、扣税以及津贴等内容的表。让我们考虑一下这些内容可能如何组织在一起。您可以设想一个表包含员工姓名、地址和电话号码。您希望保存的其它信息可能包括工资、工资范围、上次加薪时间、下次加薪时间、员工业绩评定等内容。
这些内容是否应保存在一个表格中?几乎可以肯定不应该如此。不同类别的员工的工资范围可能没有区别;这样,您可以仅将员工类型储存在员工记录表中,而将工资范围储存在另一个表中,通过类型编号与这个表关联。考虑以下情况:
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 客户机通过服务器访问的数据库。其中包括 IBM DB/2、Microsoft SQL Server、 Oracle、Sybase、SQLBase 和 XDB。所有这些数据库产品都支持多种相对类似的 SQL 方言,因此,所有数据库最初看起来好象可以互换。当然,它们 不能互换的原因是每种数据库都有不同的性能特征,而且每一种都有不同的用户界面和编程接口。您可能会想,既然它们都支持 SQL,对它们进行的编程也应该相似,但这是绝对错误的,因为每种数据库都使用其自己方式接收 SQL 查询,并使用其自己的方式返回结果。这就自然引出了一种新一代的标准:ODBC
Microsoft 于 1992 年首先尝试了这一技巧,该公司发布了一个规范,称为对象数据库连接性。这被认为是在 Windows 环境下连接所有数据库的答案。与所有软件的第一个版本相同,它也经历了一些发展的困扰,在 1994 年推出了另一个版本,该版本运行速度更快,而且更为稳定。它也是第一个 32 位的版本。另外,ODBC 开始向 Windows 之外的其它平台发展,到目前为止,它在 PC 和工作站领域已十分普遍。几乎每个主要的数据库厂商都提供 ODBC 驱动程序。
然而,ODBC 并不是我们最初想象的灵丹妙药。许多数据库厂商都将 ODBC 作为其标准接口之外的“备选接口”,而且对 ODBC 的编程微不足道。与其它 Windows 编程一样,它包括句柄、指针和选项,使其难以掌握。最后一点,ODBC 不是中立的标准。它由 Microsoft 公司开发,并由该公司不断改进,而微软同时也推出了我们所有人所使用的极具竞争性的软件平台,这使得ODBC的未来难以预测。
除 Microsoft 之外,多数厂商都采用了 JDBC,并为其数据库提供了 JDBC 驱动程序;这使您可轻松地真正编写几乎完全不依赖数据库的代码。另外,JavaSoft 和 Intersolv 已开发了一种称为 JDBC-ODBC Bridge 的产品,可使您连接还没有直接的 JDBC 驱动程序的数据库。支持 JDBC 的所有数据库必须至少可以支持 SQL-92 标准。这在很大程度上实现了跨数据库和平台的可移植性。
JDBC-ODBC 驱动程序可从 Sun 的 Java 网站 (http://java.sun.com) 轻松地找到并下载。在您扩充并安装了这个驱动程序后,必须执行下列步骤:
当一个应用程序或 applet 调用服务器,服务器再去调用数据库时,我们称其为三层模型。当您调用称为“服务器”的程序时通常是这种情况。
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 图标,然后单击“添加”,如图 1 所示。然后选择数据库驱动程序(此处使用 Microsoft Access),然后单击“确定”。在“数据源名”和“描述”中分别键入数据源名称 (Groceries) 和数据库说明 (Grocery prices)(这两项都不需要和文件名相关),然后单击“选取”,找到数据库,并选择该数据库。找到该数据库后,屏幕将如图 2 所示。单击“确定”,然后单击“关闭”来关闭面板。
图 1:ODBC 控制面板设置屏幕。
图 2:在 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
DatabaseMetaData | 有关整个数据库的信息:表名、表的索引、数据库产品的名称和版本、数据库支持的操作。 |
ResultSet | 关于某个表的信息或一个查询的结果。您必须逐行访问数据行,但是您可以任何顺序访问列。 |
ResultSetMetaData | 有关 ResultSet 中列的名称和类型的信息。 |
尽管每个对象都有大量的方法让您获得数据库元素的极为详细的信息,但在每个对象中都有几种主要的方法使您可获得数据的最重要信息。然而,如果您希望看到比此处更多的信息,建议您学习文档以获得其余方法的说明。
//从元数据中获得列数 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 对象。 |
getColumnCount(); |
返回 ResultSet 中的列数。 | ||
getColumnName(int); |
返回列序号为 int 的列名。 | ||
getColumnLabel(int); |
返回此列暗含的标签。 | ||
isCurrency(int); |
如果此列包含带有货币单位的一个数字,则返回 true。 | ||
isReadOnly(int); |
如果此列为只读,则返回 true。 | ||
isAutoIncrement(int); |
如果此列自动递增,则返回 true。这类列通常为键,而且始终是只读的。 | ||
getColumnType(int); |
返回此列的 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(); |
获得您所连接的数据库驱动程序的名称。 |
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 块中。
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,您可以用我们上面刚讨论过的方法对它进行处理。
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());} }
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:\Projects\objectJava\chapter19>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
在这一部分中,我们将构建一个新的 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 程序中使用这个对象。
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 (