|
摘 要JDK1.1 包括了新的数据库存取(JDBC)及组件(JavaBeans)的应用程序接口(APIs)。 这两个API结合在一起,可用来开发通用数据库代码。 通过用唯一的一个类去存取任何一种JDBC数据库 (封装于不同组件中的各个应用程序有着其具体的编码),用户就不必因为数据库结构一点点的细小变化去修改数据库编码。 一个关系数据库基本上包括一系列相互关连的表, 在每一个表中存有一类与应用系统相关的数据。 例如一个地址簿数据库中, 可能有关于人员、住址、电话号码等方面的表。 在数据库中,每一个这样的实体将被作为一系列的字符串,整数及其它原始数据类型存贮起来。数据库中,表的定义将描述每一种与实体相关的信息如何在一个表的字段中存储。 例如,你可以在一个名为“人”的表中,有两个字段别表示所存字符串为“姓”和“名”。 每一张表应当有一个或几个字段值作为标识,确保每条记录的唯一性。 这些标识或“键”可以用来连接存在于不同表中的信息。 例如你可以在“人员”表中,为每个人指定唯一的“人员号码”的键值,并在“地址” 表中的相应字段中使用同一个键值。这样,你可以通过对两个表中的“人员号码”字段值的匹配,使每一个人和他的地址关联起来。 关系数据库系统出现于七十年代,时至今日,它仍然是存储巨量数据的主要方式。因而,Java 软件工具有必要具备处理关系数据库的能力。 关系数据库要想被某个Java应用程序利用,首先需要解决两个问题。第一:需要某些 基础的中间件来建立与数据库的连接,向数据库发出SQL查询等等; 第二:操纵数据库的处理结果要与操纵任何一种Java信息一样方便——作为一个对象。前一个问题已被SUN及几个数据库产商解决;后一个问题则有待我们进一步去探究。 在为普通的程序开发业务定义大量的APIs 这项工作上,SUN一直保持着与许多软件公司的合作关系。 在JDK1.1 APIs中,JDBC的API是最早建立起来的。而且,它已得到了为数众多的应用。 这些应用中,有的是100% 的纯Java,有的则是Java和其它程序的混合体,如:用现有的ODBC 数据源进行连接( 参看图1)。JavaSoft 已将一个关于现有的JDBC驱动程序的介绍放在它的Web站点上 (http://splash.javasoft.com/jdbc/jdbc.drivers.html)。
图1 一个典型的JDBC或JDBC/ODBC 配置注意: 此图已被简化。 另外的组件已包括其中(如ODBD驱动程序) 非常明显, 这些应用的优缺点取决于你的环境和设置, 在此我不准备对它们的各种情况进行逐一论述。 在下面的内容中, 我们假定, 在你的机器中已拥有某种Java开发环境, 并且你已正确地安装并测试过某个JDBC驱动程序, 或者运用过某种JDBC驱动程序及SUN的JDBC/ODBC桥。 JDBC APIJDBC API 作为一个单独的Java 包(或类库, 即java.sql)出现, 包括有一系列的类。这些类提供了处理某个关系数据库的中间件。 本质上讲, 它们使得你可以关联某个数据库, 并向其发出查询。 你可以对这些查询结果进行处理, 检索你数据库的meta- 信息(meta-information), 并且处理在此间可能发生的各种异常情况。让我们来看一个简单的JDBC例子, 看一看应用了Java JDBC 之后, 查询会得到怎样的简化。 表1 是一个极其简单的数据库。 在清单1中的编码是一段最简单的对关系数据库进行SQL查询所需的Java语句。
String ur1="jdbc:odbc:sample"; String query="SELECT * FROM PERSON"; boolean more; try { Class.forName("sun.jdbc.odbc.jdbcOdbcDriver"); Connection con = DriverManager.getConnection(ur1,"sandor","guest"); Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query); While (more = rs,next()) { int number = rs.getInt("PERSON#"); String firstName = rs.getString("FIRST_NAME"); String lastName = rs.getString("LAST_NAME"); System.out.printIn(number + " " + firstName + " " + lastName); } rs.close(); stmt.close(); con.close(); } catch(SQLException ex) { ex.printStackTrace(); }清单1: 一个应用了JDBC的SQL查询 这段编码的含义是: 先装入SUN的JDBC/ODBC 驱动程序, 然后与被jdbc:odbc:sample 指定的数据库建立起一个关联, 最后对该数据库进行一个简单的SELECT查询。 如果没有遇到查询异常(SQLException), 程序将循环地从结果集(ResultSet)中每次抽出一条数据库记录, 并将其显示在屏幕上。 好了, 现在我们来看一看这段程序还有哪些不足? 在清单1 的这类程序中, 存在着两个根本性的错误: 1.这种编码用到了数量众多的数据库meta- 信息, 而这些信息都只能手工编码到程序中。 当你想要取一个数值时, 你必须提前知道你将取到的数值是一个整数、浮点数、还是一个双精度数。这将会使得编写一个可以处理任何一个数据库的类变得十分困难;并且每一个数据库的细小调整都会逼你去仔细地检查和修改程序。 2.数据库中的信息总是作为一个单个的RecordSet(记录集)实例来传递, 而这种实例并不是一个真正的对象。RecordSet 类(与其它的数据库类封装没什么差别)更象一个指针或游标, 借助方法, 它能够提供存取数据信息的途径。RecordSet中的实例实际上并不包括信息, 它们仅仅表示获得信息的方式。 这正说明了当你要调用另外的RecordSet方法去获取某些真实的数据信息的时候, 你必须通过RecordSet去做更多的工作(利用RecordSet.NEXT()去移动指针)。 实际上,JDBC 类的确仅仅传递这类联系松散的字段。 即使你完全了解数据库的所有内部细节, 这也没有任何价值, 因为在Java提供了存储和处理信息的方法。 所以, 理想的状态是, 有一种好的方法, 能够逐一地从数据库中抽取记录或字段(通过RecordSet), 并且将取到的信息“填”入到新生成的对象之中。 这一解决方式的关键在于关系数据库R(DB)和面向对象的数据模型(ODB)之间的相似性。RDB中的表和ODB中的类的作用很相似, 而记录和对象也有着某些相同的属性。你可以将记录看作是用来初始化某个对象的数据元素的数据组。如果你已将记录从数据库中抽取出来, 就必须调用类构造函数来生成上面所说的对象。若能够将每一条记录自动地传到适当的构造函数中, 就可以轻而易举地由记录来构造对象。 在开发一个小的应用程序时, 有可能将每一个记录传递给某个构造函数, 以此来生成新的对象。 你可以经常利用对象参照来操纵从数据库中抽取的任何数据。因为你通过RecordSet所得到的每一个对象最终都是java.lang.Object的扩充。你可以定义一个BibClass, 使其具有各类的共同属性,BigClass类操作将利用Java instanceof 算子来实时决定运行中所遇到的数据库信息, 并且通过一个大的switch选择, 跳到相应的程序段中。 你也可以定义一个相似的带有多个构造函数的BigClass, 每个构造函数有差别不太大的调用参数。 你可以用BigClass(int,int),BigClass(int,float)等等, 取决于你从RecordSet 中循环取出的数据的类型(这一方法当然将会包括许多冗余代码)。 然而, 以上两种方法都不能真正解决问题。 因为记录和构造函数在程序中的关系仍将是僵硬的。 若想得到通用的数据库编码, 必须自动地建立数据库和构造函数二者的关联。 Java 的性能对于此时的我们就如雪中送碳。清单2 中的程序片段只需一个类名就可以建造一个Java 类。 这样, 我们就可以凭借类和表的名称来识别那些可以处理从表中抽取出的记录的构造函数。 利用标准的JDBC 类, 可以容易地获得所有表的表名, 在此, 我们将要充分利用这个Java 小技巧。 只要简单地为每个数据库表开辟一个Java类, 使类名和表名相互匹配, 无论何时, 每当从表中抽取出一条记录的时候, 通过将表名传递给Class.forName(), 程序将自动生成一个对象。 Class c = class.forName("Person"); Person p = (Person)c.newInstance(); System。out.println("... just created a " + c.getName();清单2: 一个简单的Class.forName()例子 然而, 此处还有一些问题。 由于对某些特定的类来说,forName() 函数需要调用参数为void 的构造函数, 所以不能将RecordSet 变量直接传递给构造函数。 在这里,我们需要一个初始化函数, 把从数据库中抽取出的记录作为RESULTSET 参数, 将其值赋予对象的数据元素。 一个好的方法是引入超级类, 并将其作为所有数据库表相关类的通用父类。 实际上, 这个超级类在数据库查询中充当着重要的角色, 我们将在下面展示这一点。 查询数据库利用上面的方法可以由记录生成对象, 但是你仍然得用SQL语句来查询数据库, 这需要对数据库结构有深入的了解。 这还是没有解决问题, 虽然我们能够自动地匹配数据库表和类的名字, 但是还是必须手工编写SQL语句。 这就是说每次修改数据库结构后, 将不得不手工编辑这些查询语句。 不过, 我们仍然可以利用前文所述的方法来越过这个障碍。 通常而言, 查询关系数据库时, 你将会用到属于主键或索引的字段名和值。 一言弊之, 如果某人向你提供了适当的字段名和字段值, 你就可以从相应的数据库中抽取符合要求的记录( 或 字 段)。 而DatabaseMetaData 对象不但可以被用于检索一系列的表名(见上所述), 而且可以获得一系列的主键及索引字段。 上面的问题由此可以迎刃而解。 通过填入一系列适当的(字段名, 字段值)对, 可以利用相对而言少得多的代码实现对关系数据库的查询。 你可以将对子中的所有字段名和数据库中的主健及索引字段相匹配。 每当你找到了名字列表中相应的主健或索引字段, 可以根据相应的数值来生成一个SQL语句, 执行它来获取RecordSet, 并通过Class.forName() 构造机制将结果转化为对象。 实现这一想法要求可以以(名,值)对的方式对与数据库表相关的每个类的数据元素进行存取。 但是这种方法只有通过上节所述的通用父类才能趋于完美。 清单3 和4 利用伪码表示了这一方法。 Open the database connection Retrieve a list of user defined tables for each table { Check where there is a corresponding class file if(it is availabe) { load the class file Retrieve lists of key fields and indeces for this table Store these lists in hashtables for easy access } else throw an exception }清 单3: 初 始 化 数据库 连 接 的 伪 码 Take an object A containing a series of (name,value) pairs for each table T { for each (name,value) pair { if(name matches primary_key_field or index_field) store a refrence to both name and value } if all key_fields were found create a query string using key names and values else if all index_fields were found create a query string using index names and values execute the query to obtain a ResultSet For each record in the ResultSet { Create an object of the class associated with table T initialize the object using the record's contents Add the object to the results, e。g。, attach it to A } }清单4: 描述数据库查询的伪码 Java 镜像和JavabeansJava1.1 开发套件(JDK)的引入, 为我们带来了许多强大的新性能, 例如全新的用户界面接口类。 有两个新的JDK API尤其值得注意: 镜像机制(java.lang.reflect包)和JavaBeans 组件的应用程序接口(java.beans 包)。 这两个API将会帮助我们创建高明的数据库类, 使我们可以利用有关类的meta- 信息, 以此来解决开发通用数据库类中的问题。 拥有forName()和newInstance()方法的Class类, 仅仅是镜象(reflection)功能的一个简单例子。 真正重要的是,forName()字符串参数不必须是源程序中出现的字符串。 只要给出一个名字(这个名字可从任何地方取来), 你就可以载入并实例化任何一个类。 对于我们的数据库类, 我们可以直接从数据库自身的表名中得到类名。这就是说, 与数据库表相关的Java类名并不需要出现在源程序中。相应地, 当表名改变或某个表被加入到数据库中时, 不需要修改源码, 只要确信带有新名字的类已存在你的系统中。 镜像类意味着可以在实时运行中获取、存储和处理Java程序中的类信息。 它们的实例能够象任何Java对象一样被运用, 你可以象修改字符串和整数一样, 去修改类、数据类型、返回类型、方法参照和参数。 在源程序级, 这个镜像的概念看起来并没有什么价值——因为可以应用你自己的编码直接存取你所需要的有关类、方法及参数的所有信息。但是, 镜像(reflection)将会在java的编译文件中发挥作用。JavaBeans API的作用是: 通过应用程序的构造机制利用来自于全然不同的开发者或产商所编写的类。 JavaBeans 规范为类成员的名字制定一系列的条例。 以确保方法函数的名字能系统地描述它们的功能。 任何一个符合规则的Java类都可以被一个Bean的内化实例(通过镜像)检查, 以揭示其行为的重要特征——诸如对于什么样的事件类将有所响应, 以及该类将会产生什么样的事件等等。 任何符合这些规范的类都是高效的Bean, 因而是一个组件。 在理论上, 这意味着你可以从各种来源收集一系列beans, 当需要它们时可以将其其实时地绑在一起。 一个Bean的例子在下面一个名为Translation 的Bean中, 有一个构造函数和两个方法来操作一个名为“language”的属性。 这里我想强调的是, 既然你可以通过检查一个类的代码来了解它的构造函数、方法及属性, 那么Bean的内化器(Introspector)也能做到。 public class Translation extends Object { int language; public translation() { } public int getlanguage() { return(language); } public void setLanguage( int language) { this。language=language; } }清单5: 一个非常简单的Bean 一 个Bean Introspector 能够提供许多数组的PropertyDescriptor实例, 这些实例包含所有Bean的属性的类型信息, 即例子中由get/set方法所定义的类型。 你可以调用这些方法(利用reflection)来读或写这些属性。 镜像机制(reflection facilities)为我们检查原本松散的类和数据库表的完整性提供了的更好方法。 实际上, 仅仅通过类名和一个表匹配并不一定能够保证一些类内部的一致性。 一个与表相关的类显然应当具备存储数据库表中所有字段的数据元素。 一个类可能有适当的名字, 但其初始化代码可能会省略。 它可能只有一个正确的名字, 而其数据成员可能有不同的名字或者是不同的类型。 使用JDBC的DatabaseMetaData及镜像机制可以检查它们是否完全匹配! 引用一些JDBC调用去获得现实中必须的数据库信息, 通过正确的名字去检查你系统中的类, 并且通过镜像去比较表和类的属性, 这实实在在是一块香饼! 结论和启示JDBC, 镜像和JavaBeans三者的结合, 能够方便地从关系数据库中存取记录并利用记录来初始化组件(不仅仅是对象)。 为了实现上述操作, 无需修改你的数据库, 只需确认你的类符合Bean规范, 并使类属性和表字段相互匹配。Beans还有其它一些简单的技巧, 可使编程更加有趣。Beans能够提供自己的用户界面组件, 并且Beans规范还包括一个名 为Customizers的东西。 你可以引入另外的类专门地去察看、编辑和自行定制一个Bean类的实例。(关于定制Beans, 请参看下一期《 定制你的Java》)。 总之, 我们可以为数据库类编写自行定义的类, 应用程序能够从关系数据库中抽取数据, 通过实例化某个类得到新的实例, 并引入相关的图形用户界面(GUI)组件来查看和编辑数据。 所有这些都由通用代码完成, 因而能够处理任何数据库。利用Java 编写的、功能齐全的数据库查看/编辑器正迎我们而来。 张智雄 编译 稿件来源:http://www.javaworld.com/javaworld/jw-09-1997/jw-09-reflections.html |