利用 JDBC 将数据提取到 XML 之中

遗留数据可以轻松迈进 XML 时代

XML 最适合存储数据,因此在某些时候,肯定会有人要求您拉取数据库中的信息,并像在 XML 中那样操作这些信息。本教程将帮助您学习使用 JDBC 访问数据库、使用 SQL 拉取信息,随后您可以利用这些信息,使用预先确定的映射构建一个 XML 文档。

Nicholas Chase, 作者, Web 站点开发人员, Freelance

Nicholas Chase 的照片Nicholas Chase 曾经参与过多家公司的网站开发,这些公司包括 Lucent Technologies、Sun Microsystems、Oracle Corporation 和 Tampa Bay Buccaneers。Nick 曾经做过高中物理教师、低放射性废弃设备管理员、在线科幻杂志编辑、多媒体工程师、Oracle 教员。最近,他担任过福罗里达州克利尔沃特的 Site Dynamics Interactive Communications 的首席信息官。他出版了三部有关 Web 开发的书籍,其中包括 Java and XML From Scratch (Que)。他喜欢聆听读者的意见,您可以通过 nicholas@nicholaschase.com 与他取得联系。



2012 年 2 月 27 日 (最初于 2001 年 9 月 28 日)

简介

我是否应该学习这份教程?

本教程设计用于协助那些需要从数据库中提取信息并将其置入 XML 文档的 JavaTM 开发人员。

本教程假设您已经大体熟悉了 Java 和 XML,并且对文档对象模型 (DOM) 较为熟悉。您应熟悉 Java 编程,但无需为了掌握本教程中介绍的技术而事先了解使用 JDBCTM 进行数据库连接的知识。本教程简要提及了 SQL 的基本知识。本教程不要求您具备 GUI 编程知识,因为应用程序输入/输出是通过命令行处理的。参考资料 部分中的链接包括有关 XML 和 DOM 基础知识以及具体 SQL 背景信息的教程参考。

本教程的内容

XML 最适合存储数据,因此在某些时候,肯定会有人要求您拉取数据库中的信息,并像在 XML 中那样操作这些信息。JDBC 是一种使用 Java 访问数据库的独立于供应商的方法。本教程将介绍如何实例化和使用 JDBC 驱动程序来连接到一个数据库,从而检索信息。此外还将介绍 SQL 的基础知识,以及如何创建和使用 JDBC 查询的结果。

本教程的目标是从数据库中提取数据,并创建一个 DOM 文档。DOM 文档的结构由 XML 映射文件决定,这展现了为此目的使用 XML 文件的一种方法。

工具

即便您只是阅读了本教程中的示例,而未亲自动手尝试过,本教程仍然能够帮助您理解主题。如果您希望在学习教程的过程中动手尝试示例,那么请确保您已经安装了以下工具,而且这些工具能够正常工作:

本教程中使用的规范

本教程使用了几种规范来强调文中的部分资料:

  • 需要键入的文本使用 bold monospace 字体显示。某些代码示例中使用粗体来强调随附文本中引用的标记或元素。
  • 强调/斜体 用于强调窗口、对话框和特性名称。
  • monospace 字体表示文件和路径名称。
  • 在这份教程中,我们忽略了与讨论内容部相关的代码片段,使用省略号 (...) 取而代之。

通过 JDBC 访问数据库

JDBC 是什么?

如果您已经学习了 “使用 JDBC 将 XML 数据插入数据库” 教程,那么可以直接跳到 SELECT 语句剖析 一节。

就在不久之前,要与数据库交互,开发人员必须使用针对该数据库的特定 API。这使得创建独立于数据库的应用程序非常困难,甚至完全不可能。

JDBC 类似于 ODBC(即开放数据库连接),提供了访问数据库的标准 API 中介。从左侧的资料中可以了解到,可以使用标准 JDBC 命令,JDBC 驱动程序会将其转为针对数据库的原生 API。

本教程的任何内容均未提及特定数据库,因为选择哪种数据库是无关紧要的。所有命令均为标准 JDBC 命令,驱动程序可将其转为适合您选择的任意数据库的原生命令。由于这种 API 独立性,因此您在一定程度上可以轻松使用另外一种数据库,除了驱动程序名称和可能需要更改的连接 URL (在 创建连接 时使用)之外,您不需要更改应用程序中的任何内容。

有关为您的数据库下载恰当的 JDBC 驱动程序的信息,请参阅 参考资料 部分。可用的 JDBC 驱动程序超过 150 种,实际上适合任何数据库。

甚至还有适用于不具备可用 JDBC 驱动程序的数据库的选项。

图 1. 无 JDBC 驱动程序的数据库的示例
无 JDBC 驱动程序的数据库的示例

JBDC-ODBC 桥接

只要有 ODBC 驱动程序可用,那么针对某种数据库的特定 JDBC 驱动程序就并非必不可少;您可以使用 JDBC-ODBC 桥接 取而代之。应用程序将调用桥接,桥接将命令转为 ODBC,ODBC 驱动程序将其转为原生 API。

出于多种原因,并不推荐使用 JDBC-ODBC 桥接来访问数据库,其中包括性能和配置方面的原因(命令必须通过两个 API 传递,必须在每一个客户端上安装和配置 ODBC 驱动程序),但是,如果无纯粹的 JDBC 驱动程序可用,那么使用桥接来进行测试和开发也是一种可以接受的做法。

如果您选择使用桥接,那么在 Windows 系统中应该选择 Start>Settings>Control Panel>ODBC Data Sources 来创建一个系统数据源名称 (DSN)。记下 DSN 的名称,在 创建连接 时需要使用此名称。

图 2. 具有 JDBC 和驱动程序的数据库的示例
具有 JDBC 和驱动程序的数据库的示例

设置数据库和驱动程序

create table products (
   product_id   numeric primary key,
   product_name varchar(50),
   base_price   numeric,
   size         numeric,
   unit         varchar(10),
   lower        numeric,
   upper        numeric,
   unit_price   numeric )

首先,请确保已安装了您选择使用的所有数据库,并且它们都能正常运行,此外还要确保驱动程序是可用的。可以从 http://industry.java.sun.com/products/jdbc/drivers 下载 JDBC 驱动程序。

创建数据库之后,再创建必要的表。本教程仅使用一个表,即 products。其结构如左侧所示。通过采用适合您的数据库的恰当步骤来创建表。

注意:在一般情况下,此数据应至少会拆分到两个相关的表中;为了简便起见,本示例仅使用了一个表。

访问数据库的流程

使用 Java 与数据库交互通常涉及以下步骤:

  1. 载入数据库驱动程序。这可以是 JDBC 驱动程序,也可以是 JDBC-ODBC 桥接。
  2. 为数据库创建一个 Connection
  3. 创建一个 Statement 对象。此对象实际上会执行 SQL 或存储过程。
  4. 创建一个 ResultSet,并使用所执行的查询的结果填充它(如果目标是检索数据或直接更新数据)。
  5. 检索或更新 ResultSet 中的数据。

java.sql 包中包含 JDBC 2.0 Core API,它是 JavaTM 2 SDK, Standard Edition 发行版本的一部分,用于访问数据库。javax.sql 包是作为 Java 2 SDK, Enterprise Edition 的一部分发布的,其中包含 JDBC 2.0 Optional Package API。

本教程仅使用 JDBC 2.0 Core API 中的类。

实例化驱动程序

为了访问数据库,首先要加载 JDBC 驱动程序。在任意给定时间,如果有多种驱动程序可用,DriverManager 会尝试创建与它所知道的每一种驱动程序之间的连接,从而确定需要使用哪种驱动程序。应用程序将使用成功连接的第一个驱动程序。DriverManager 可以通过两种方式得知驱动程序的存在。

第一种方式是直接使用 Class.forName() 加载,如本例中所示。载入驱动程序类之后,它将向 DriverManager 进行注册,如下所示:

public class Pricing extends Object {

   public static void main (String args[]){

      //For the JDBC-ODBC bridge, use
      //driverName = "sun.jdbc.odbc.JdbcOdbcDriver"
      String driverName = "JData2_0.sql.$Driver";

      try {
         Class.forName(driverName);
      } catch (ClassNotFoundException e) {
         System.out.println("Error creating class: "+e.getMessage());
      }
   }
}

获得一个成功加载的驱动程序之后,应用程序即可连接到数据库。

DriverManager 可用于定位一个驱动程序的第二种方法就是循环获取 sql.drivers 系统属性中找到的任何驱动程序。sql.drivers 属性是可能存在的驱动程序的冒号分隔列表。在加载类之前,将始终动态检查此列表,因此如果您希望使用某种特殊驱动程序,请确保 sql.drivers 属性为空,或者从您需要的驱动程序开始。

创建连接

加载驱动程序之后,应用程序即可连接到数据库。

DriverManager 将通过静态 getConnection() 方法建立连接,该方法将获取数据库的 URL 作为参数。URL 通常表示为:

jdbc:<sub-protocol>:databasename

然而,引用 URL 可以使用活动驱动程序能够理解的任何格式编写。请查询您的 JDBC 驱动程序的文档,以便确定 URL 格式。

一种需要使用子协议的场景就是通过 ODBC 进行连接。如果直接通过 ODBC 访问 DSN 为 pricing 的示例数据库,那么 URL 应该是:

odbc:pricing

这就意味着,要通过 JDBC 进行连接,URL 将是:

jdbc:odbc:pricing

实际连接创建如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class Pricing extends Object {

   public static void main (String args[]){

      //For the JDBC-ODBC bridge, use
      //driverName = "sun.jdbc.odbc.JdbcOdbcDriver"
      //and
      //connectURL = "jdbc:odbc:pricing"
      String driverName = "JData2_0.sql.$Driver";
      String connectURL = "jdbc:JDataConnect://127.0.0.1/pricing";
      Connection db = null;	   
      try {
         Class.forName(driverName);
         db = DriverManager.getConnection(connectURL);
      } catch (ClassNotFoundException e) {
         System.out.println("Error creating class: "+e.getMessage());
      } catch (SQLException e) {
         System.out.println("Error creating connection: "+e.getMessage());
      }
   }
}

成功创建连接后,即可执行任何所需的数据库操作(例如,插入或更新数据)。

关闭连接

由于 StatementConnection 是对象,因此 Java 将对其执行垃圾收集,释放它们占用的数据库资源。这可能会误导您认为这代表您无需为关闭这些对象而担忧,但实际上并非如此。

Java 应用程序本身完全有可能拥有大量可用资源,也就是说垃圾收集的频率较低。此外,在 Java 应用程序拥有大量资源时,可用的数据库资源可能受到限制。Java 对象也许会占用大量数据库资源,而这些对象很有可能被应用程序关闭。

无论是否存在错误,都有必要确保这些对象已关闭,因此应该为已有的 try-catch 块添加一个 finally 块。

...
      Connection db = null;	  
      try {
         Class.forName(driverName);
         db = DriverManager.getConnection(connectURL);
      } catch (ClassNotFoundException e) {
         System.out.println("Error creating class: "+e.getMessage());
      } catch (SQLException e) {
         System.out.println("Error creating connection: "+e.getMessage());
      } finally {
         System.out.println("Closing connections...");
         try {
            db.close();
         } catch (SQLException e) {
            System.out.println("Can't close connection.");
         }
      }
   }
}

具有讽刺意味的是,close() 方法本身可能会引发一个 SQLException,因此需要自己的 try-catch 块。


通过 JDBC 从数据库中提取信息

SELECT 语句剖析

建立了数据库连接之后,应用程序便可开始检索数据。在 SQL 数据库中,数据(通常)是使用 SELECT 语句检索的。SELECT 语句具有几个基本组成部分。例如:

SELECT product_id, product_name FROM products WHERE product_id < 1000 ORDER BY product_id

此语句可细分如下:

  • SELECT product_id, product_nameselect 子句,定义将返回一个表(或者一组表)中的哪些列。如需返回全部列,则可以使用 SELECT *
  • FROM productsfrom 子句,定义了包含要返回的数据的表。您可以从多个表中进行选择。这也称为 join,需要谨慎设计 where 子句
  • WHERE product_id < 1000where 子句,决定将返回哪些可用行。
  • ORDER BY product_idorder by 子句 决定了数据的返回顺序。

SELECT 语句也可将多行组织在一起,并返回一个汇总值。例如,以下语句将返回在 2001 年 9 月 15 日获得的收入超过 1000 美元的所有产品的总收入:

SELECT product_id, sum(quantity*price) as revenue FROM orders WHERE order_date = '9/15/01' GROUP BY product_id HAVING revenue > 1000

SELECT 语句由 Statement 对象执行。

创建语句

创建 Statement 对象非常简单,只需使用 Connection's createStatement() 方法,确保捕获潜在的 SQLException 即可。这一部分中使用的大多数类都将引发 SQLException,因此其余代码将转入这个 try-catch 块。

...
import java.sql.Statement;

public class Pricing extends Object {
...
      } catch (SQLException e) {
         System.out.println("Error creating connection: "+e.getMessage());
      }
	  
	  //Create the Statement object, used to execute the SQL statement
      Statement statement = null;
      try {
         statement = db.createStatement();
      } catch (SQLException e) {
         System.out.println("SQL Error: "+e.getMessage());
      }   finally {
         System.out.println("Closing connections...");
         try {
            db.close();
         } catch (SQLException e) {
            System.out.println("Can't close connection.");
         }
      }
   }
}

这与创建 PreparedStatements使用 CallableStatement 调用存储过程 类似。

执行语句

为了实际检索数据,必须执行 Statement。这通常涉及传递一个 SELECT 语句,这将创建作为 ResultSet 返回的一组数据,如下所示。

...
import java.sql.ResultSet;

public class Pricing extends Object {
...
      //Create the Statement object, used to execute the SQL statement
      Statement statement = null;
      //Create the ResultSet object, which ultimately holds the data retrieved
      ResultSet resultset = null;
      try {
         statement = db.createStatement();
         //Execute the query to populate the ResultSet
         resultset = statement.executeQuery("SELECT * FROM products");
      } catch (SQLException e) {
         System.out.println("SQL Error: "+e.getMessage());
      }	 finally {
...

如果 SQL 语句存在问题,比如引用了并不存在的表,那么应用程序会引发一个 SQLException。否则,无论是否找到了任何数据,应用程序都将继续执行。

测试数据

图 3. 数据集中的 ResultSet 指针位置
数据集中的 ResultSet 指针位置

创建 ResultSet 时,它拥有一个引用数据集内相对位置的 “指针”。在 ResultSet 语句返回之后(即便表是空的),此指针也将定位在第一行 “上方”。

为了到达实际数据的第一行,应用程序将调用 next() 方法。该方法会返回一个布尔值,指示新位置是否有一个行。如果未找到任何数据,next() 会返回 false

...
         resultset = statement.executeQuery("select * from products");	
         
         //Check for data by moving the cursor to the first record (if there is one)
         if (resultset.next()) {
            System.out.println("Data exists.");
         } else {
            System.out.println("No data exists.");
         }
      } catch (SQLException e) {
...

稍后在 循环遍历数据 中,将使用类似的技术。

数据类型

确定存在数据之后,便可使用 ResultSetgetXXX() 方法来检索数据。实际上并不存在称为 getXXX() 的方法,但有一组方法是以这种形式表示的,因为 ResultSet 可能会返回多种类型的数据。例如,如果数据单元是数据库中的整型数据,ResultSet 可能会将其作为数字返回给应用程序(通过 getDouble()getInt()getFloat() 等方法);也可能会将其作为 String 返回(通过 getString());或者将其作为数组返回(通过 getArray())。较大的数据单元甚至可以作为 InputStream (通过 getBinaryStream())或者 Reader(通过 getCharacterStream())返回。

需要牢记的一个要点就是 Java 支持大多数数据库中找到的数据类型,但仅有某些数据类型可作为与原有类型不同的类型检索。例如,getString() 可以检索原本作为任何数据库类型的数据,但日期和时间值仅能通过 getDate()(或者 getTime())、getObject()getString())检索。

按名称检索数据

您可以通过两种方式检索数据本身:按照名称和按照索引。为了按照名称检索,可以使用之前讨论的 getXXX() 方法之一进行检索。这些方法将获取 intString 形式的参数。稍后将具体介绍 按照索引检索数据,就目前而言,应用程序将按照列名称检索字段。

最终,所有这些数据均将作为纯文本的形式包含在一个 XML 文件之中,因此将对所有值使用 getString()

...
         if (resultset.next()) {
            //Output data by referencing the ResultSet columns by name
            System.out.print(resultset.getString("product_id")+"|");
            System.out.print(resultset.getString("product_name")+" | ");
            System.out.print(resultset.getString("base_price")+" | ");
            System.out.print(resultset.getString("size")+" | ");
            System.out.print(resultset.getString("unit")+" | ");
            System.out.print(resultset.getString("lower")+" | ");
            System.out.print(resultset.getString("upper")+" | ");
            System.out.println(resultset.getString("unit_price"));
         } else {
            System.out.println("No data exists.");	 
         }
...

编译和运行应用程序将显示第一行数据,即一家在线美食零售商的批量定价信息。

图 4. 按名称检索数据,第一行数据
按名称检索数据,第一行数据

循环遍历数据

每次只显示一行是一种非常有用的做法,但更好的方法是循环遍历数据,并将每一条记录分别显示在独立的一行之中。

为此,应用程序将转至下一行并输出数据,随后再转到下一行。在指针经过最后一行之后,next() 将返回 false,循环将终止。

...
         resultset = statement.executeQuery("select * from products");	
         //Execute the loop as long as there is a next record
         while (resultset.next()) {
            //Output data by referencing the ResultSet columns by name
            System.out.print(resultset.getString("product_id")+" | ");
            System.out.print(resultset.getString("product_name")+" | ");
            System.out.print(resultset.getString("base_price")+" | ");
            System.out.print(resultset.getString("size")+" | ");
            System.out.print(resultset.getString("unit")+" | ");
            System.out.print(resultset.getString("lower")+" | ");
            System.out.print(resultset.getString("upper")+" | ");
            System.out.println(resultset.getString("unit_price"));
         }
      } catch (SQLException e) {
...

运行此应用程序将返回表中的所有行。

图 5. 循环遍历数据将显示表中的所有行
循环遍历数据将显示表中的所有行

按索引检索数据

当然,按照列名称检索数据可能非常不便,特别是在涉及到许多列的情况下。此外这种做法也会加大通用化和自定义检索的难度。为了通过能够更加轻松自定义的方式更迅速地检索数据,可以使用各列的索引编号。

第一列的索引为 1、第二列为 2,依此类推。我们知道上一节中的示例有 8 列,因此可将其改写如下:

...
         while (resultset.next()) {
            for (int i=1; i <= 8; i++) {
               //Output each column by its index
               System.out.print(resultset.getString(i)+" | ");
            }
               //Output a line feed at the end of the row
               System.out.println("");
            }
...
图 6. 按索引检索数据的示例
按索引检索数据的示例

使用上述技术将使数据检索过程完全通用化,因而可迁移至其他类似的任务和/或应用程序。

通用检索

本教程的最终目标是创建一个基于完全独立于应用程序的映射文件的数据检索和操作过程。因此,应用程序需要能够在无需预先了解数据结构的前提下检索数据。换句话说,应用程序需要具备一种单独确定 ResultSet 中有多少个列的方法。

为了实现这个目标,我们将利用 ResultSetMetaData 类。与数据库的元数据类似,它能访问各列的编号和类型,以及列名称和其他信息。

ResultSetMetaData 可用于输出与某条记录有关的所有信息,即便是该记录拥有的惟一信息就是 ResultSet 函数本身时也是如此。

...
import java.sql.ResultSetMetaData;

public class Pricing extends Object {

...
      Statement statement = null;
      ResultSet resultset = null;
      //Create the ResultSetMetaData object, which will hold information about
      //the ResultSet
      ResultSetMetaData resultmetadata = null;
      try {
         statement = db.createStatement();
         resultset = statement.executeQuery("select * from products");	
		 
         //Get the ResultSet information
         resultmetadata = resultset.getMetaData();
         //Determine the number of columns in the ResultSet
         int numCols = resultmetadata.getColumnCount();
		 
         while (resultset.next()) {
            for (int i=1; i <= numCols; i++) {
               //For each column index, determine the column name
               String colName = resultmetadata.getColumnName(i);
               //Get the column value
               String colVal = resultset.getString(i);
               //Output the name and value
               System.out.println(colName+"="+colVal);
            }
            //Output a line feed at the end of the row
            System.out.println(" ");
         }
...

现在,运行应用程序不仅能提供数据,还会提供列名称,在为数据创建 XML 文档时,这是必不可少的:

图 7. 通用数据检索的示例
通用数据检索的示例

ResultSet 选项

至此,应用程序使用的一直是利用默认属性创建的 ResultSet,但是您可以调整这些属性,从而控制数据获取方向以及 ResultSet 是否会显示其他用户做出的更改等方面。

例如,在创建 ResultSet 时,可采用使之能够滚动或用于直接更新数据的方式。它将自顶向下地获取数据,并且不受其他数据库用户做出的更改的影响。

属性实际上是在最终创建 ResultSetStatement 上设置的:

Statement statement = db.createStatement(
                                         ResultSet.TYPE_SCROLL_INSENSITIVE,
                                         ResultSet.FETCH_FORWARD
                                         );

请注意,将多个自定义属性传递给 ResultSet 方法可能会影响应用程序的性能。

PreparedStatement

提高应用程序性能的一种方法就是使用 PreparedStatement

PreparedStatement 类似于 Statement,惟一区别是前者是使用 SQL 语句作为参数创建的。在大多数情况下,此类 SQL 语句都将由数据库预先编译(或者至少进行缓存)。例如,如果应用程序的结构如下,那么 SELECT 语句应预先编译:

...
import java.sql.PreparedStatement;

public class Pricing extends Object {
...	  
      //Create the PreparedStatement
      PreparedStatement statement = null;
      ResultSet resultset = null;
      ResultSetMetaData resultmetadata = null;
      try {

         //Compile or cache the SQL within the database and prepare it for execution
         statement = db.prepareStatement("select * from products");
         //Execute the SQL above to populate the ResultSet
         resultset = statement.executeQuery();	
	  
         //Get the ResultSet Information
         resultmetadata = resultset.getMetaData();
...

PreparedStatement 或许在特定查询反复执行时最为有用。在此类情况下,在运行时设置参数的能力非常有用。

设置 IN 参数

在重用 StatementPreparedStatement 的大多数情况下,每次使用都存在细微的差异,例如记录范围有所不同。在这些情况下,您就需要使用 IN 参数。举例来说,为了检索一个要在运行时确定的 product_id 范围,可以使用:

...
 statement = db.prepareStatement("select * from products where "+
                                 "product_id < ? and product_id > ?");
 statement.setInt(1, 5);
 statement.setInt(2, 10);
		 
 resultset = statement.executeQuery();	
...

setXXX() 方法与 getXXX() 方法匹配,不同之处在于它们包括参数编号和要为参数设置的值。例如,setInt(2, 10) 将语句中的第二个 ? 替换为整数 10,从而以如下形式执行语句:

select * from products where product_id < 5 and product_id > 10

在使用 CallableStatement 调用存储过程时,您可以使用类似的技巧。

使用 CallableStatement 调用存储过程

大多数现代数据库都允许开发人员在数据库内创建存储过程。这种存储过程可以简单到仅有单独一条 SQL 语句,也可以像迷你应用程序一样复杂。无论是哪种情况,有些时候很有必要从 Java 调用这些过程来生成一组待检索的数据。

CallableStatement 类扩展了 PreparedStatement,以便允许开发人员为数据库查询指定参数。随后 CallableStatement 将返回 ResultSet(或者 ResultSet)。

创建 CallableStatement 与创建 PreparedStatement 极为相似,但使用了调用语句,而非 SQL 语句。随后驱动程序会将调用语句转为原生调用。

statement = db.prepareCall("{call product_listing }");

注意:CallableStatementPreparedStatement 之间的差别之一就是 CallableStatement 除了通常创建的 ResultSet 之外,CallableStatement 还会提供 OUT 参数。

检测空值

本教程中的数据涉及商品的批量定价。因此,某些数量范围的上限为空,表示无更高的批量折扣可用。

这种做法虽然可行,但会导致构建最终 XML 文档的难度加大,因为空值可能会造成问题。为了解决这个问题,您可以使用 wasNull() 方法来确定特定数据段是否为空,以及是否使用其他内容取代它。这个示例展示了使用 “and up” 替换空值的情况。

...
         while (resultset.next()) {
            //Output each row
            for (int i=1; i <= numCols; i++) {
               //For each column, get name and value information
               String colName = resultmetadata.getColumnName(i);
               String colVal = resultset.getString(i);
               //Determine if the last column accessed was null
               if (resultset.wasNull()) {
                  colVal = "and up";
               }
               //Output the information
               System.out.println(colName+"="+colVal);
            }
            System.out.println(" ");
         }
...

请注意,wasNull() 无参数。该方法处理从 ResultSet 中检索到的最后一列,因此必须先调用 getXXX()

图 8. 检测空值的示例
检测空值的示例

此时,应用程序检索了恰当的数据和列名称。您已经为构建 XML 文档做好了准备。


设置映射

映射的工作原理

本教程的目标是展示如何使用映射文件,通过来自数据库的数据创建 XML 文档。换句话说,映射文件决定了要检索哪些数据以及数据在 XML 文件中的最终表示形式。

实现这一目标的一种方法就是使用从数据库中提取的数据创建一个临时 XML 文档,随后根据映射文件将数据转换为新格式。映射文件决定了要提取哪些数据、新文件的名称和结构以及哪些数据将存储在何处。

结构

映射文件包含几部分信息:

  • 原始数据,采用 data 元素的形式。为了最大限度地提高灵活性,这里将采用 SQL 语句的形式。通过这种方法,您就可以使用映射文件指定从多个表中提取数据。
  • 新文档的整体结构。这将采用 root 元素的形式,该元素通过属性指定目标根元素的名称和表示数据库行的元素的名称。
  • 数据元素的名称和内容。这些信息包含在一系列 element 元素之中。element 包含新元素的名称以及任何 attributecontent 元素。这两个元素指定了应该添加的数据,对于属性来说,则指定了它应该调用什么。例如,如果 description 元素应该具有一个 product_number 属性,该属性代表 product_id 列,而 product_name 作为内容,映射文件应将其表示为:
    <element name="description">
        <attribute name="product_number">product_id</attribute>
        <content>product_name</content>
    </element>

映射文件

最终的映射文件如下所示:

<?xml version="1.0"?>
<mapping>
   <data sql="select * from products" />
   <root name="pricingInfo" rowName="product">
      <element name="description">
          <attribute name="product_number">product_id</attribute>
          <content>product_name</content>
      </element>
      <element name="quantity">
          <content>lower</content>
      </element>
      <element name="size">
          <content>size</content>
      </element>
      <element name="sizeUnit">
          <content>unit</content>
      </element>
      <element name="quantityPrice">
          <content>unit_price</content>
      </element>
   </root>
</mapping>

使用 SQL 结果创建 XML 文档

算法

创建新 XML 文档的过程如下:

  1. 解析映射文件,以便获得必要的信息,包括要检索的数据在内。
  2. 检索源查询。这允许根据映射文件动态检索数据。
  3. 将数据存储到文档对象中。随后我们将从这个临时文档中拉取数据,以便根据映射创建目标文档。
  4. 检索数据映射,使应用程序可以使用它。
  5. 循环遍历原始数据。分析每一行数据,并将其重新映射到新结构。
  6. 检索元素映射。映射文件定义了从临时文档中按照怎样的顺序拉取哪些数据。
  7. 为新文档添加元素。检索数据之后,将其添加到新文档的新名称下。
  8. 为新文档添加属性。最后,将任何属性添加到恰当的元素中。

解析映射文件

创建新文档的第一步是检索映射,这只能通过解析映射文件完成。请注意,由于此文件也包括对最终将检索的数据的引用,因此您必须先解析此文件,然后才能执行数据库操作。

...
import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

public class Pricing extends Object {

  public static void main (String args[]){

    //Create the Document object   
    Document mapDoc = null;
    try {
      //Create the DocumentBuilderFactory
      DocumentBuilderFactory dbfactory = 
               DocumentBuilderFactory.newInstance();
      //Create the DocumentBuilder
      DocumentBuilder docbuilder = dbfactory.newDocumentBuilder();
      //Parse the file to create the Document
      mapDoc = docbuilder.parse("mapping.xml");
    } catch (Exception e) {
      System.out.println("Problem creating document: "+e.getMessage());
    }

    //For the JDBC-ODBC bridge, use
    //driverName = "sun.jdbc.odbc.JdbcOdbcDriver"
    //and
    //connectURL = "jdbc:odbc:pricing"
    String driverName = "JData2_0.sql.$Driver";
    String connectURL = "jdbc:JDataConnect://127.0.0.1/pricing";
    Connection db = null;
...

检索源查询

接下来检索 data 元素的 sql 属性中存储的源查询。

...
import org.w3c.dom.Element;
import org.w3c.dom.Node;
...
         System.out.println("Problem creating document: "+e.getMessage());
      }

      //Retrieve the root element 
      Element mapRoot = mapDoc.getDocumentElement();
        //Retrieve the (only) data element and cast it to Element
        Node dataNode = mapRoot.getElementsByTagName("data").item(0);
        Element dataElement = (Element)dataNode;
        //Retrieve the sql statement
        String sql = dataElement.getAttribute("sql");

        //Output the SQL statement
        System.out.println(sql);

      //For the JDBC-ODBC bridge, use
      //driverName = "sun.jdbc.odbc.JdbcOdbcDriver"
      //and
      //connectURL = "jdbc:odbc:pricing"
      String driverName = "JData2_0.sql.$Driver";
      String connectURL = "jdbc:JDataConnect://127.0.0.1/pricing";
      Connection db = null;
...

首先确定根元素,随后检索 data 节点。由于仅有一个 data 元素,因此您可以直接检索。类似的技术也可以用于通过多个依序运行的查询构建文档。

最后,将 Node 强制转换为 Element,以便获得 Attribute 值。

删除之前的输出语句,运行应用程序,显示 SQL 语句作为输出。

图 9. SQL 语句作为输出
SQL 语句作为输出

将数据存储到文档对象中

从数据库中成功提取数据之后,数据将存储在临时的 Document 之中。通用方法是为每个数据行创建一个 row 元素,将每列表示为根据该列命名的一个元素,将数据本身作为元素的内容。

...
   public static void main (String args[]){

      Document mapDoc = null;
      //Define a new Document object
      Document dataDoc = null;
      try {
         //Create the DocumentBuilderFactory
         DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
         //Create the DocumentBuilder
         DocumentBuilder docbuilder = dbfactory.newDocumentBuilder();
         //Parse the file to create the Document
         mapDoc = docbuilder.parse("mapping.xml");

         //Instantiate a new Document object
         dataDoc = docbuilder.newDocument();
      } catch (Exception e) {
         System.out.println("Problem creating document: "+e.getMessage());
      }
...
      ResultSetMetaData resultmetadata = null;
 
      //Create a new element called "data"	  
      Element dataRoot = dataDoc.createElement("data");

      try {
         statement = db.createStatement();
         resultset = statement.executeQuery("select * from products");	

         resultmetadata = resultset.getMetaData();
         int numCols = resultmetadata.getColumnCount();

         while (resultset.next()) {
            //For each row of data
            //Create a new element called "row"
            Element rowEl = dataDoc.createElement("row"); 

            for (int i=1; i <= numCols; i++) {
               //For each column, retrieve the name and data
               String colName = resultmetadata.getColumnName(i);
               String colVal = resultset.getString(i);
               //If there was no data, add "and up"
               if (resultset.wasNull()) {
                  colVal = "and up";
               }
               //Create a new element with the same name as the column
               Element dataEl = dataDoc.createElement(colName);
               //Add the data to the new element
               dataEl.appendChild(dataDoc.createTextNode(colVal));
               //Add the new element to the row
               rowEl.appendChild(dataEl);
            }

            //Add the row to the root element			
            dataRoot.appendChild(rowEl);

         }

      } catch (SQLException e) {
         System.out.println("SQL Error: "+e.getMessage());
      }	 finally {
         System.out.println("Closing connections...");
         try {
            db.close();
         } catch (SQLException e) {
            System.out.println("Can't close connection.");
         }
      }

      //Add the root element to the document
      dataDoc.appendChild(dataRoot);
   }
}

具体来说,参见上面的代码示例,首先创建一个空文档以及根元素 data。对于数据库中的每一行,创建一个 row 元素,为每一列创建一个独立的元素,并将其添加到行中。最后,将各 row 元素添加到根,将根添加到 Document

检索数据映射

获得数据之后,便可以开始将其映射到新结构:从解析后的映射文档中检索映射。先检索根元素和行元素中的信息,随后检索元素映射本身。

...
import org.w3c.dom.NodeList;
...
      dataDoc.appendChild(dataRoot);

      //Retrieve the root element (also called "root")
      Element newRootInfo = 
               (Element)mapRoot.getElementsByTagName("root").item(0);
      //Retrieve the root and row information
      String newRootName = newRootInfo.getAttribute("name");
      String newRowName = newRootInfo.getAttribute("rowName");
      //Retrieve information on elements to be built in the new document
      NodeList newNodesMap = mapRoot.getElementsByTagName("element");

   }
}

有了这些信息之后,就可以构建新的 Document

循环遍历原始数据

每个原始行均作为一个 row 元素存储在临时文件之中。您需要循环遍历这些元素,将其作为 NodeList 检索。

...
      NodeList newNodesMap = mapRoot.getElementsByTagName("element");

      //Retrieve all rows in the old document
      NodeList oldRows = dataRoot.getElementsByTagName("row");
      for (int i=0; i < oldRows.getLength(); i++){

         //Retrieve each row in turn
         Element thisRow = (Element)oldRows.item(i);

      }  
...

检索元素映射

既然已经获得了数据和映射信息,下面就可以开始构建新的 Document。对于每一行,循环遍历映射,确定从临时 Document 中检索数据列的顺序,并确定在将其添加到新的 Document 中时应对其采用什么样的名称。

...
    for (int i=0; i < oldRows.getLength(); i++){

      //Retrieve each row in turn	   
      Element thisRow = (Element)oldRows.item(i);

      for (int j=0; j < newNodesMap.getLength(); j++) {

        //For each node in the new mapping, retrieve the information
        //First the new information...  
        Element thisElement = (Element)newNodesMap.item(j); 
        String newElementName = thisElement.getAttribute("name");	

        //Then the old information
        Element oldElement = 
               (Element)thisElement.getElementsByTagName("content").item(0);
        String oldField = oldElement.getFirstChild().getNodeValue();

      } 

    }  
...

对于 newNodesMap 中的每一个元素,应用程序都会检索新元素名称,随后会检索要检索的旧元素的名称。

向新文档添加元素

将新元素添加到文档之中非常简单,只需使用恰当的名称创建新元素,随后检索恰当的数据并将其设置为元素的内容即可。

...
   public static void main (String args[]){

      Document mapDoc = null;
      Document dataDoc = null;
      //Create the new Document
      Document newDoc = null;
      try {
         //Create the DocumentBuilderFactory
         DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
         //Create the DocumentBuilder
         DocumentBuilder docbuilder = dbfactory.newDocumentBuilder();
         //Parse the file to create the Document
         mapDoc = docbuilder.parse("mapping.xml");
         //Instantiate a new Document object
         dataDoc = docbuilder.newDocument();

         //Instantiate the new Document
         newDoc = docbuilder.newDocument();
      } catch (Exception e) {
         System.out.println("Problem creating document: "+e.getMessage());
      }

...	  
      //Retrieve the root element (also called "root")
      Element newRootInfo = (Element)mapRoot.getElementsByTagName("root").item(0);
      //Retrieve the root and row information
      String newRootName = newRootInfo.getAttribute("name");
      String newRowName = newRootInfo.getAttribute("rowName");
      //Retrieve information on elements to be built in the new document
      NodeList newNodesMap = mapRoot.getElementsByTagName("element");

      //Create the final root element with the name from the mapping file
      Element newRootElement = newDoc.createElement(newRootName);

      NodeList oldRows = dataRoot.getElementsByTagName("row");
      for (int i=0; i < oldRows.getLength(); i++){

         //For each of the original rows	   
         Element thisRow = (Element)oldRows.item(i);

         //Create the new row
         Element newRow = newDoc.createElement(newRowName);
         			   
         for (int j=0; j < newNodesMap.getLength(); j++) {
			   
            //Get the mapping information for each column
            Element thisElement = (Element)newNodesMap.item(j); 
            String newElementName = thisElement.getAttribute("name");	

            Element oldElement = 
               (Element)thisElement.getElementsByTagName("content").item(0);
            String oldField = oldElement.getFirstChild().getNodeValue();

            //Get the original values based on the mapping information
            Element oldValueElement = 
               (Element)thisRow.getElementsByTagName(oldField).item(0);
            String oldValue = 
               oldValueElement.getFirstChild().getNodeValue();

            //Create the new element
            Element newElement = newDoc.createElement(newElementName);
            newElement.appendChild(newDoc.createTextNode(oldValue));
            //Add the new element to the new row
            newRow.appendChild(newElement);

         }
         //Add the new row to the root
         newRootElement.appendChild(newRow);
      }
      //Add the new root to the document
      newDoc.appendChild(newRootElement);
   }
}

首先,需要创建新的 Document,随后创建新的根元素,其名称会获取自映射信息。对于临时 Document 中的每一行,可以使用映射中指定的 newRowName 为新行创建一个元素。

对于每一行,循环遍历映射中指定的各新元素,并检索原始数据。在上一节中,您已经按顺序检索了 content 元素的文本。现在您可以使用此信息来确定将按照什么样的顺序从临时行中检索哪些节点。获得了旧数据和新名称之后,可以创建新元素并将其添加到行中。

最后,将新行添加到根元素,将根元素添加到 Document。最后要添加的内容就是属性。

为新文档添加属性

Document 已经接近完成。您已经添加了新元素,但尚未添加可能指定的任何属性。按照与添加新元素本身相似的方式添加这些属性。然而,一个元素有可能具有多项属性,因此在代码中必须考虑到这个问题。完成这项任务的最简单的方法就是检索 attribute 元素,并将其置入 NodeList,随后遍历列表,处理各元素。对于每一个所需属性,通过原始 Document 确定字段名称,并确定其在新 Document 中的名称。

...
       Element newElement = newDoc.createElement(newElementName);
       newElement.appendChild(newDoc.createTextNode(oldValue));

       //Retrieve list of new elements
        NodeList newAttributes = 
               thisElement.getElementsByTagName("attribute");
        for (int k=0; k < newAttributes.getLength(); k++) {
           //For each new attribute
           //Get the mapping information
           Element thisAttribute = (Element)newAttributes.item(k);
           String oldAttributeField = 
               thisAttribute.getFirstChild().getNodeValue();
           String newAttributeName = thisAttribute.getAttribute("name");

           //Get the original value
           oldValueElement = 
               (Element)thisRow.getElementsByTagName(oldAttributeField).item(0);
           String oldAttributeValue = 
               oldValueElement.getFirstChild().getNodeValue();
		
           //Create the new attribute		
           newElement.setAttribute(newAttributeName, oldAttributeValue);
          }
        //Add the element to the new row
        newRow.appendChild(newElement);
      }
      //Add the new row to the root
      newRootElement.appendChild(newRow);
...

最终文档

这个过程结束时,newDoc 包含采用新格式的旧信息。之后即可在另一个应用程序中使用它,或者使用 XSLT 或其他方法实现进一步的转换。

<pricingInfo>
   <product>
      <description product_number="1">Filet Mignon</description>
      <quantity>1</quantity>
      <size>1</size>
      <sizeUnit>item</sizeUnit>
      <quantityPrice>40</quantityPrice>
   </product>
   <product>
      <description product_number="2">Filet Mignon</description>
      <quantity>11</quantity>
      <size>1</size>
      <sizeUnit>item</sizeUnit>
      <quantityPrice>30</quantityPrice>
   </product>
   <product>
      <description product_number="3">Filet Mignon</description>
      <quantity>101</quantity>
      <size>1</size>
      <sizeUnit>item</sizeUnit>
      <quantityPrice>20</quantityPrice>
   </product>
   <product>
      <description product_number="4">Prime Rib</description>
      <quantity>1</quantity>
      <size>1</size>
      <sizeUnit>lb</sizeUnit>
      <quantityPrice>20</quantityPrice>
   </product>
   <product>
      <description product_number="5">Prime Rib</description>
      <quantity>101</quantity>
      <size>1</size>
      <sizeUnit>lb</sizeUnit>
      <quantityPrice>15</quantityPrice>
   </product>
...

JDBC 数据提取总结

结束语

本教程详细地讨论了如何使用 JDBC 连接到一个数据库并提取数据,随后将这些数据用于创建 XML 文件。讨论包括使应用程序通用化,以便能够配合任何查询、任何结构一起使用。使用元数据可以实现这一目标的一部分。

XML 目标文件的结构由 XML 映射文件决定。本教程还提供了读取映射文件并使用它将临时 XML 文件转为所需结构的细节。


下载

描述名字大小
教程的示例代码extractcodefiles.zip10KB

参考资料

学习

获得产品和技术

讨论

  • 查看 developerWorks 博客,并加入 developerWorks 中文社区,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客、书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=795850
ArticleTitle=利用 JDBC 将数据提取到 XML 之中
publish-date=02272012