一、范例查询 我们的终极目标是一个能够满足所有潜在用户的Intranet。为此,我们必须提高Intranet用户访问数据库的灵活性,一种可能的方案是采用所谓的即席查询(Ad-hoc Query)。
“即席”两个字在这里的含义是“不作特殊准备地,随意、自由地”。即席查询允许用户象数据库管理员一样,自由地访问数据库。也许,最灵活的方式是让用户在Web页面的文本输入框中直接输入SQL命令,然后由应用发送该SQL命令查询数据库。然而,虽然这种方式很灵活,但要实施得好很困难,存在许多问题。
首先,这种方式不安全。如果不对用户进行大量的培训,不在应用中对用户输入的SQL命令进行严格的检验,用户可能有意无意地破坏系统运行。另外,即使进行了培训,要求用户总是能够构造出高效的SQL查询也不切实际。
然而,这些问题并不能完全阻碍我们构造出有效的Intranet即席查询系统。一般地,Intranet内的用户比网络之外的用户可信度高。为此,我们可以采用灵活性稍差但仍不失高效的方案——范例查询(Query-By-Example,QBE)。范例查询的使用简单、灵活,不需要对用户进行大量的培训,同时它也比直接使用SQL的方式更安全。
在范例查询系统中,我们提供给用户的界面与数据库结构之间有着密切的对应关系。每一个查询项目有一个相应的用户界面控件。例如,假设有一个雇员信息数据库,我们用一个列表框允许用户选择雇员所在的部门,用一个文本框允许用户输入薪金范围限制查询结果。
二、数据库抽象 对于一些程序员来说,数据库操作有时就象是一堆散乱的连接字符串、SQL命令和结果集。Java的面向对象特色可以让数据源具有更好的可管理性。接下来我们将用Java技术构造一个浏览器界面的QBE系统。这个系统以几个核心类为基础,核心类允许JSP页面在更高的层次上操作数据库,避免大量地编写底层SQL代码。
数据库最基本的元素之一是表。在数据库中,表是数据记录的容器,比如用来容纳雇员名字和薪水信息。下面的DBTable类描述的就是数据库里面的表。DBTable类的公用方法负责处理最底层的细节。比如,addChildTable方法用来建立表的父-子关系,addConstraint方法用来过滤表的输出。
【Listing 1:DBTable.java,描述数据库的表】
import java.util.*;
public class DBTable {
String pkey; // 主键
String name; // 表的名字
Vector columns; // 结果集包含的列
Hashtable col_desc; // 各个列的描述
Vector children; // 子表
Vector constraints; // 所有约束
/* 创建一个新的、未经初始化的表*/
protected DBTable() {
columns = new Vector();
children = new Vector();
constraints = new Vector();
col_desc = new Hashtable();
}
/* 创建一个新的表,指定名字和主键*/
public DBTable(String name, String pkey) {
this();
this.name = name;
this.pkey = pkey;
}
/* 返回主键 */
public String getPrimaryKey() {
return pkey;
}
/* 创建一个新的约束,设置它的值,
* 并把它加入表的约束列表
*/
public void addConstraint(String column,
int op, String value) {
Constraint c = new Constraint();
c.column = column;
c.op = op;
c.value = value;
constraints.add(c);
}
/* 把结果集限制为单个记录的简便方法 */
public void constrainByPrimaryKey(String value) {
addConstraint(pkey, Constraint.EQ, "'" + value + "'");
}
/* 添加一个列 */
public void addColumn(String column,
String description) {
columns.add(column);
col_desc.put(column, description);
}
/* 添加一个子表,通过外键建立关系 */
public void addChildTable(DBTable table, String fkey) {
children.add(table);
addConstraint(this.pkey, Constraint.EQ,
table.name + "." + fkey);
}
/* 搜索当前表以及(递归地搜索)所有子表,
* 寻找指定的列,返回该列的描述 */
public String findColumnDescription(String column) {
String result = (String) col_desc.get(column);
if (result != null) { // 已经找到指定的列
return result;
} else {
// 在所有子表中搜索该列
Enumeration e = children.elements();
while (e.hasMoreElements()) {
DBTable child = (DBTable) e.nextElement();
result = child.findColumnDescription(column);
if (result != null) { // 已经找到!
return result;
}
}
return null; // 在所有表中都无法找到指定的列
}
}
/* 搜索当前表以及(递归地搜索)所有子表,
* 检查指定的列是否是一个主键 */
public boolean isPrimaryKey(String column) {
if (pkey.equals(column)) { // 已经找到指定的列
return true;
} else {
// 搜索所有子表
Enumeration e = children.elements();
while (e.hasMoreElements()) {
DBTable child = (DBTable) e.nextElement();
if (child.isPrimaryKey(column)) { // 已经找到!
return true;
}
}
return false;
}
}
}
单独的DBTable类其实没有什么实际用途,它只是一种抽象的描述,既没有建立底层的数据库连接,也没有任何SQL命令。为发挥DBTable类的作用,我们必须定义一个DBTable类的子类,在子类中利用DBTable类定义的方法访问数据库服务器。
下面就是DBTable类的子类SQLDBTable.java:
【Listing 2:SQLDBTable,扩展DBTable提供SQL和JDBC支持】
import java.sql.*;
import java.util.*;
public class SQLDBTable extends DBTable {
/* 构造函数 */
public SQLDBTable(String name, String pkey) {
super(name, pkey);
}
/* 生成一个SQL命令 */
public String generateSQL() {
/* 获得SQL命令中出现的所有表的一个清单 */
Vector tables = new Vector();
findTables(tables);
/* 获得所有列的一个清单 */
Vector columns = new Vector();
findColumns(columns);
/* 获得必须在WHERE子句中出现的所有约束
* 的一个清单
*/
Vector where = new Vector();
findConstraints(where);
/* 创建一个容纳SQL命令的StringBuffer */
StringBuffer sql = new StringBuffer("SELECT ");
sql.append(delimitedList(", ", columns.elements()));
sql.append(" FROM ");
sql.append(delimitedList(", ", tables.elements()));
if (where.size() > 0) {
sql.append(" WHERE ");
sql.append(delimitedList(" AND ", where.elements()));
}
return sql.toString();
}
/* 利用一个JDBC连接提取结果记录,
* 注意:调用者必须关闭结果集的
* Statement对象
*/
public ResultSet fetchRows(Connection conn)
throws Exception
{
String sql = generateSQL();
/* 创建Statement(可能抛出SQLExeception异常)*/
Statement stmt = conn.createStatement();
/* 返回结果集 */
return stmt.executeQuery(sql);
}
/* 这是一个从主键以外的各个列获取数据的简便
* 方法。它在下列情形下使用:当你不想让用户看到主键时。
*关于使用该方法的例子,请参见HtmlSelectListMaker.java。
*/
public Enumeration getDisplayData(ResultSet rs) throws SQLException
{
ResultSetMetaData rmd = rs.getMetaData();
Vector result = new Vector();
for(int i = 0; i < rmd.getColumnCount(); i++) {
String column = rmd.getColumnName(i + 1);
if (isPrimaryKey(column)) {
continue;
}
result.addElement( rs.getString(column) );
}
return result.elements();
}
/* 该方法生成带分界符的列表,仅供内部使用。
* 用来为SQL命令生成空格或逗号分隔的列表。
*/
private String delimitedList(String delim, Enumeration e)