概述 数据库程序设计通常需要从大量的外部数据源中获取数据进行处理。这这篇文章中,作 者提出了在数据库中使用DatabaseMetaData——JDBC源数据接口——和执行预编译的SQ L实现将纯文本数据转换成实际的数据类型的技术。它提供了一种在运行时动态发现数据 库的具体数据类型和翻译外部文本数据的方法。 正文 假设您需要写一个将文本文件中的数据转换成数据库的表并进行存储的数据库应用程序 ,例如这个表包含的是航班信息,包括票号、购买日期、出发日期、离港/到港位置以及 票价等在数据库中,每一段信息都有一一个指定的数据类型与之相对应。例如数值型、 文本型、日期型或者货币型等等。 程序必须从文本文件中读出几段这样的票务信息,将这些信息转换成合适的数据类型并 创建一个相应的表以存储这些转换过的数据。为了实现这种转换,程序需要知道每段数 据在数据库中对应的数据类型。简单的方法是在程序中通过硬编码进行类型的转换—— 将数据类型作为编程时就预先知道的静态信息。 通过采用这种简单的方式,您必须要在另外一个程序中用类似的代码来实现同一个数据 库中另外一个表(如具有客户信息的custumer表)的数据生成工作,尽管您可以重复使 用以前的许多程序代码,但您必须重新设计所有的数据库数据类型转换的代码。 如果您需要生成的表非常多时,您就会非常厌倦并且也很浪费精力了。但这也仅仅是只 有一个数据库的情况。如果将数据迁移到其它的数据库系统,您必须重写所有的代码以 实现不同数据库数据类型的对应。在不同的数据库间实现数据类型的对应和转换在数据 库编程中一直是一个非常讨厌的事。 创建可重用的代码 怎样避免这些重复的工作呢?您写代码时可以将数据库名、表名和列名看成参数。问题 是是不是有办法对数据类型也可以进行类似的处理,这样您就可以写一个类或者一个类 集完成任何数据库间任何数据类型的对应和表的生成?答案是在运行之前不要确定数据 类型,在运行时根据java.sql 包中的DatabaseMetaData接口来实现它,通过在这个接口 中写入代码,您可以避免对数据类型进行硬编码,这样就可以写通用的可重用代码了。 DatabaseMetaData接口为数据库提供了元数据(metadata)信息。Metadata是描述数据 的数据。例如,航班数据库表包含票务信息,它是数据。在这种情况下,元数据会包含 诸如表中有多少列、每个列的数据类型、是不是一个列可以为空等等信息。这就是关于 数据的数据本篇关注的焦点是每个列的数据类型。 在后面的讨论中,我会向您介绍一种以层次方式进行组织的三个java类构成的可重用库 。每层都对下层进行了封装,最下层离数据库最近。相应地,最上层离离应用程序最近 了。虽然这些库是为灌入数据库的表而设计的,但只要您在代码上进行小规模的改动, 您就可以建立自己的数据类型独立的数据库查询和更新。 沿用下面的情况 假设你需要在一个organization数据库中灌入一个称为emp的表,这里是一个在第一行包 含表,然后第二行是一系列的列名,随后的是每行都是相应的列对应的数据的样板数据 文件: 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文本,但这些数据会转换成如下的数据库特定数据类型: 字段 数据库类型 -------- ------------- hiredate date sal decimal ename ASCII text empno number emp表的数据结构如下图的顶行所示: 从输入列到数据库表列的映射 不是表中所有的列都需要立刻灌入,在这个例子中,仅仅灌入标记为true的列,需要灌 入的列有empno、ename、 hiredate和sal。 在数据文件中的列的顺序也不需要一定与数据库中列的顺序一致。一个索引数组维护了 文件中的列到数据库中的列的对应。在这个例子中,活动列(标记为true的)的顺序数 组就是索引数组。如果I是输入文件中一个列的位置(hiredate:0、sal:1等等),那 么activeColumnOrder[i]是这些列在表中的相应位置。(activeColumn[0]是5,意思是 说hiredate是emp表中的第五列)。 建立库类层次 我们的库中包含三个层次: 一个TableColumns类,它距离数据库最近,它负责发现和管理数据库表列的信息。 一个TableMediator类,它用来准备使用TableColumns所管理的数据库表信息对表进行灌 入。 一个TableBuilder类,它离数据库最远,当然离应用程序最近。它负责从输入的数据文 件中读取数据然后使用TableMediator类将数据灌入指定的表。 下图说明这种层次关系: 应用分层 这些类都收集在称为tablebuild的包中。 这些Java源程序可以在这里( ![]() 第一层:TableColumns类 TableColumns类负责通过查询给定数据库的元数据实例发现数据库表的给定列信息。它 将列信息存储在两个并列的数组中:一个称为columnNames的字符串数组,一个称为col umnTypeCodes的短整型数组。 一个给定表的所有列的类型可以通过DatabaseMetaData的getColumns方法获得: public abstract ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException getColumns方法需要四个参数:一个类目名(catalog name)、一个方案名(schema n ame)、一个表名(table name)和一个列名(column name)。它们中,最后三个带有 Pattern后缀的(如上面的代码)很有意思,因为它们允许你搜索所有的匹配模式表达式 的目标字符串,也就是说,您通过这些参数指定查询准则。如下面所指出的: 寻找所有的满足下面的准则的列:它们属于一个给定的类目、并且属于匹配给定方案模 式的任何一个方案、并且匹配给定表名模式的任何一个表。当然,它们的名字也要匹配 给定的列名模式。 模式使用类似SQL语句的语法进行指定,它通过LIKE从句指定匹配名。独特的地方是,它 通过在模式表达式中使用下划线以匹配目标字符串中的任何字符,通过百分号匹配目标 字符串中任何数量的连续字符。例如,模式表达式j_b会匹配"job"和"jab",而模式表达 式j%b会匹配任何以"j"开始,以"b"结束中间包含任意数量(包括0)字符的目标字符串 。 下面是在TableColumns 构造方法中对getColumns方法的调用,其中dbMeta是给定数据库 的DatabaseMetaData的一个实例。 ResultSet rset = dbMeta.getColumns(null, null, table.toUpperCase(), "%") ; 注意,catalog名和schema模式参数的值是null。空值表示这个参数可以从搜索准则中省 略。另外,要注意我们将指定表名的大写传给表名模式。还不清楚这是JDBC的需要还是 JDBC驱动器的偏好。我在发现大写的表名能够让代码工作以前,花了很多时间去检查我 的代码哪里出了问题。最后,"%"分配给列名模式以获得所有的列。它可以解释为“取得 给定表的所有列”。 GetColumns方法返回的java.sql.ResultSet结果集为每一个匹配的列返回一行。每行包 括18个描述域,这就是结果集列。与我们的类相关的域包括列名、列类型码,它们是列 的第4和5个域。列类型码是java.sql.Types 类中表示列中SQL类型的一个常量。 在这个例子中,通过表名对emp的getColumns调用会返回所有8个字段的一个描述。在结 果集中每行一个。列的类型码已经被取出并且存储在称为columnTypeCodes的类型数组中 ,这样就可以被TableMediator类所调用。我们会在下面检查这个过程。 第二层:TableMediator类 TableMediator类封装了下面的功能: 创建一个预备语句(一个带有参数的预编译SQL语句,这些参数可以在运行时提供)以灌 入表行。语句的参数对应活动列的值。 为一个活动列的集合执行这个预备语句。 TableMediator类使用TableColumns类以获得给定表的完全列信息,然后将这些信息与给 定的活动列集进行比较以实现前面第一个图中说明的配置。 PrepareRowInserts方法创建所需的预备语句。在这个例子中,预备语句象下面的样子: insert into emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) values (?, ?, ?, ?, ?, ?, ?, ?) 让您的库可移植的第一个关键因素是使用DatabaseMetaData接口来发现列的类型。如在 TableColumns类中所看到的。第二个是使用通用的java.sql.PreparedStatement 接口的 setObject方法: ps.setObject(activeColumnOrder[i], columnValues[i]); 这行代码出现在TableMediator类的insertRow方法中,ps是先前创建的PreparedStatem ent实例。 SetObject方法需要两个参数:第一个是与之相联系的预备语句中的参数的位置;第二个 是这个参数的值。通常,第二个参数可以是任何类型的对象;这段代码中是字符串。JD BC驱动器承担将这个字符串转换成合适的数据库类型的责任。 在这个语句中,我们没有使用事先发现的列类型。然而,我们确实需要值为空的列类型 ——例如要向数据库中灌入没有提供任何值的列。在这种情形,我们会在PreparedStat ement接口中使用一个setNull方法。这里是在TableMediator类的insertRow方法中使用 它的方法: ps.setNull(i, tc.columnTypeCodes[i]); 这个语句将预备语句参数I的值设置为空,但这个设置需要附带传递对应列的数据类型作 为第二个参数。 第三层:TableBuilder类 一旦建立了TableColumns 和TableMediator类,使用TableBuilder类将输入数据灌入表 的代码就很简单明了了。 TableBuilder封装了下面的功能: 从文本文件中读取第一行以确定表名并创建一个TableMediator类的实例。 从文本文件的第二行读取列名,并要求TableMediator实例为这些列设置活动列列表。 要求TableMediator实例准备将列插入列表。 从文本文件的随后的行中读取列数据并要求TableMediator实例将这些数据插入表的下一 行。 使用tablebuild库 给定tablebuild库,很容易写一个可以灌数据库表的应用程序。下面是一个应用实例: 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桥驱动Microsoft Access数据库,数据源被命名为orgdb。我主 要选择这个桥来说明是因为它提供了提供大部分JDBC能力的参考实现。 为了完成这个例子,您可以通过上面的应用程序向orgdb数据源的emp表灌入到数据。如 果输入文件名是empfile,您可以这样运行这个应用程序: java PopulateTable empfile 这样就可以将给定数据灌入到表的hiredate、sal、ename和empno列。并将输入数据的值 转换成所需的数据库指定数据类型。 如果您有一个新的数据源,您不需要修改应用程序和tablebuild类。只要维护相同的输 入格式。也就是说,第一行包含表名,第二行包含列名,随后的行包含一些行的所有列 数据,列的分隔符可以是任何东西,由于它是可以在TableBuilder类中通过参数进行设 置的。 测试您的JDBC驱动程序 本文中,您已经知道了创建一个数据类型独立的类库向数据库的表中灌入列数据的方法 。其关键是将所有的数据类型的管理工作留给JDBC的java.sql.DatabaseMetaData和jav a.sql.PreparedStatement接口的驱动器实现。您可以使用本文中的说明方法完成数据库 的插入操作,当然也可以实现数据库查询和更新。 一个重要的警告:动态类型发现和类型转换的能力最终依赖于您所使用的JDBC驱动器的 实现。例如,有些JDBC驱动器可能不支持PreparedStatement的setObject方法的类型转 换能力。另外,不同的JDBC驱动器对DatabaseMetaData接口的实现是不同的。在JDBC编 程中的通用规则是,当没有实现JDBC规范中所允许的处理能力时,就该怀疑JDBC驱动程 序了。大多数情况下,一个好的驱动器一定兼容JDBC规范,所以也就一定具有这个功能 。< |
|