目录 | 上一页 | 下一页 JDBCTM 指南:入门


9 自定义 SQL 类型

本章描述了 JDBC 2.0 API 所提供的对 SQL 结构化和 distinct 类型到 Java 类的映射方式进行自定义的支持。自定义机制最小程度地涉及了 JDBC API 的扩展。新功能是现有 getObject()setObject() 机制的扩展。

9.1 类型映射方式

使用 java.util.Map 的实例可以保留 SQL 自定义类型(结构化和 distinct 类型)和 Java 类之间的自定义映射方式。java.util.Map 接口是 JDK 1.2 中新增的接口,它取代了 java.util.Dictionary 接口。这样的对象被称为类型-映射对象。类型-映射对象实现了从自定义类型的 SQL 名称转换成 java.lang.Class 类型的对象的功能。类型-映射对象确定了一种类,从中可以构造对象来包含给定的 SQL 自定义类型的数据。

每个 JDBC Connection 都有与之相关联的类型-映射对象。类型-映射对象所含的类型映射方式可以在该连接上的操作中转化 SQL 自定义类型的数据。此外,我们还提供了获取以及设置连接的类型映射的方法。例如,

java.util.Map map = con.getTypeMap();
con.setTypeMap(map);


Connection.getTypeMap() 方法返回与连接相关联的类型-映射对象,而使用 Connection.setTypeMap() 则可设置新的类型映射方式。

映射机制非常灵活。如果 JDBC 应用程序没有显式地初始化某连接的类型映射方式,则该连接上的操作就采用在第 8 章中所述的缺省映射方式。如果将自定义映射方式插入到 SQL 类型 type-name 的类型-映射中,则该连接上的所有操作对 type-name 类型的值都将使用该自定义映射方式。最后,我们要指出的是,当调用某个 getXXX() setXXX() 方法,甚至可以显式地提供类型-映射对象来覆盖与 Connection 相关联的自定义或缺省映射方式。

9.2 Java 类约定

出现在自定义的类型-映射中的 Java 类必须实现一种新接口 — java.sql.SQLDataSQLData 接口所含的方法可以将 SQL 自定义类型的实例转换成 Java 类实例,反之亦然。例如,SQLData.readSQL() 方法读入数据值流并且创建 Java 对象,而 SQLData.writeSQL() 将来自 Java 对象的值序列写入流中。我们希望通常由了解数据库架构的工具来生成这些方法。

这种 SQL 和 Java 之间基于流的数据交换方法在概念上类似于 Java 对象序列化。数据将从 JDBC 驱动程序所提供的 SQL 数据流中读出或者写入。可以基于多种网络协议和数据格式来实现 SQL 数据流。可以基于任何逻辑数据表示来实现 SQL 数据流,在这种逻辑数据表示中,在对结构化类型进行“深度优先”遍历时可以从数据流中读出(或写入数据流)叶子 SQL 数据项(SQL 结构化类型的组成元素)。这就是说,SQL 结构化类型的属性在流中的出现顺序实际上就是属性在类型中的声明顺序,而且在下一个属性出现之前,每个(也许是结构化的)属性值在流中全部(其结构被递归地详细说明了)出现。对于采用了继承的 SQL 结构化类型的数据,属性在流中的出现顺序必须是其继承顺序。这就是说,父类型的属性必须出现在子类型的属性之前。如果采用了多重继承,则父类型的属性在流中的出现顺序应该是父类型在类型声明中的排列顺序。本协议不需要数据库服务器了解 Java。

9.3 SQL 数据流

本节描述了流接口,即 SQLInput 和 SQLOutput。它们支持对 SQL 到 Java 的类型映射方式进行自定义。

9.3.1 检索数据

当从数据库检索到 SQL 结构化和 distinct 类型的数据时,数据就“到达”了实现 SQLInput 接口的流。SQLInput 接口所含方法可以从流中顺序地读入各个数据值。下例说明了如何使用 SQLInput 流来为 SQLData 对象的域提供数值。SQLData 对象(例中的 this 对象)包含了三个持久的域:String sBlob blobEmployee emp

this.str = sqlin.readString();
this.blob = sqlin.readBlob();
this.emp = (Employee)sqlin.readObject();


SQLInput.readString() 方法从流中读取 String 值。可以使用 SQLInput.readBlob() 方法来从流中检索 Blob 值。缺省情况下使用 SQL 定位符来实现 Blob 接口,因此调用 readBlob() 不会在 JDBC 客户机上具体实现 blob 的内容。可以使用 SQLInput.readObject() 方法来从流中返回对象引用。在示例中,所返回的 Object 被限定为 Employee

SQLInput 接口上定义了很多附加的 readXXX() 方法,可以读取每种 JDBC 类型的数据。可以调用 SQLInput.wasNull() 方法来检查 readXXX() 方法的返回值是否为空。

9.3.2 存储数据

当经由 setXXX() 方法将 SQLData 对象作为输入参数传递给 JDBC 时,JDBC 驱动程序就调用该对象的 SQLData.writeSql() 方法来获得对象内容的流表示。writeSQL() 方法将来自对象的数据作为 SQL 自定义类型的表示写入 SQLOutput 流中。通常由一些工具依据 SQL 类型定义来生成 writeSQL() 方法。下例说明了 SQLOutput 流对象的使用方法。

sqlout.writeString(this.str);
sqlout.writeBlob(this.blob);
sqlout.writeObject(this.emp);

下例说明了如何将 SQLData 对象的内容写入 SQLOutput 流中。SQLData 对象(即例中的 this 对象)包含了三个持久的域:即 String sBlob blobEmployee emp。每个域将被依次写入 SQLOutputsqlout 中。SQLOutput 接口所含的附加方法用于写入每种 JDBC 类型。

9.4 示例

9.4.1 SQL 结构化类型的示例

以下 SQL 示例定义了结构化类型 PERSON、FULLNAME 和 RESIDENCE。本例还定义了拥有 PERSON 和 RESIDENCE 类型的行的几个表,并且在每个表中都插入了一行,以便使行之间相互引用。最后,本例对表进行了查询。

CREATE TYPE RESIDENCE
(
	DOOR NUMERIC(6),
	STREET VARCHAR(100),
	CITY VARCHAR(50),
	OCCUPANT REF(PERSON)
);

CREATE TYPE FULLNAME
(
	FIRST VARCHAR(50),
	LAST VARCHAR(50)
);

CREATE TYPE PERSON
(
	NAME FULLNAME,
	HEIGHT NUMERIC,
	WEIGHT NUMERIC,
	HOME REF(RESIDENCE)
);


CREATE TABLE HOMES OF RESIDENCE (OID REF(RESIDENCE)
	VALUES ARE SYSTEM GENERATED);

CREATE TABLE PEOPLE OF PERSON (OID REF(PERSON)
	VALUES ARE SYSTEM GENERATED);

INSERT INTO PEOPLE (SURNAME, HEIGHT, WEIGHT) VALUES
(
	FULLNAME('DAFFY', 'DUCK'),
	4,
	58
);

INSERT INTO HOMES (DOOR, STREET, CITY, OCCUPANT) VALUES
(
	1234,
	'CARTOON LANE',
	'LOS ANGELES',
	(SELECT OID FROM PEOPLE P WHERE P.NAME.FIRST = 'DAFFY')
);

UPDATE PEOPLE SET HOME = (SELECT OID FROM HOMES H WHERE
H.OCCUPANT->NAME.FIRST = 'DAFFY') WHERE
FULLNAME.FIRST = 'DAFFY'

上例构造了三个结构化类型实例,类型 PERSON、 FULLNAME 和 RESIDENCE 分别对应其中一个。FULLNAME 属性内嵌在 PERSON 中。PERSON 和 RESIDENCE 实例作为表中的行来存储,并且经由 Ref 属性相互引用。

以下的 Java 类代表的是以上的 SQL 结构化类型。我们希望通常用 SQL 到 Java 的映射工具来生成这些类,映射工具从目录表中读取结构化类型的定义,而且根据工具使用者对基本域的名称映射方式和类型映射方式的自定义生成如下所示的 Java 类。

注意:JDBC 2.0 没有提供访问 SQL 到 Java 映射工具所需的元数据的标准 API。提供这种类型的元数据将引入许多对 SQL3 类型模型错综复杂的依赖性,因此目前没有考虑这样做。

在下列的每一种类中,SQLData.readSQL() 方法读取属性的顺序就是属性在数据库中的相应结构化类型的定义中的出现顺序(即“行序,深度优先”顺序:读取下一个属性之前,递归地读取每个属性的完整结构)。同样,SQLData.writeSQL() 将数据写入流也是依据这种顺序。

public class Residence implements SQLData {
    public int door;
    public String street;
    public String city;
    public Ref occupant;

    private String sql_type;
public String getSQLTypeName() { return sql_type; }

    public void readSQL (SQLInput stream, String type)
	throws SQLException {
      sql_type = type;
      door = stream.readInt();
      street = stream.readString();
      city = stream.readString();
      occupant = stream.readRef();
}

    public void writeSQL (SQLOutput stream) throws SQLException {
stream.writeInt(door);
      stream.writeString(street);
      stream.writeString(city);
      stream.writeRef(occupant);
    }
}

public class Fullname implements SQLData {
    public String first;
    public String last;

    private String sql_type;
public String getSQLTypeName() { return sql_type; }

    public void readSQL (SQLInput stream, String type)
	throws SQLException {
      sql_type = type;
      first = stream.readString();
      last = stream.readString();
}

    public void writeSQL (SQLOutput stream) throws SQLException {
      stream.writeString(first);
      stream.writeString(last);
    }
}

public class Person implements SQLData {
    Fullname name;
    float height;
    float weight;
    Ref home;

    private String sql_type;
public String getSQLTypeName() { return sql_type; }

    public void readSQL (SQLInput stream, String type)
	throws SQLException {
      sql_type = type;
      name = (Fullname)stream.readObject();
      height = stream.readFloat();
      weight = stream.readFloat();
      home = stream.readRef();
    }

    public void writeSQL (SQLOutput stream)
    	throws SQLException {
      stream.writeObject(name);
      stream.writeFloat(height);
      stream.writeFloat(weight);
      stream.writeRef(home);
    }
}

以下方法使用这些类来具体实现 HOMES 和 PEOPLE 表(稍前定义的)中的数据:

import java.sql.*;
.
.
.

public void demo (Connection con) throws SQLException {

// 设置连接的映射
try {
java.util.Map map = con.getTypeMap();
map.put("S.RESIDENCE", Class.forName("Residence"));
map.put("S.FULLNAME", Class.forName("Fullname"));
map.put("S.PERSON", Class.forName("Person"));
}
catch (ClassNotFoundException ex) {}

PreparedStatement pstmt;
ResultSet rs;

pstmt = con.prepareStatement("SELECT OCCUPANT FROM HOMES");
rs = pstmt.executeQuery();
rs.next();
Ref ref = rs.getRef(1);

pstmt = con.prepareStatement(
"SELECT FULLNAME FROM PEOPLE WHERE OID = ?");
pstmt.setRef(1, ref);
rs = pstmt.executeQuery();
rs.next();
Fullname who = (Fullname)rs.getObject(1);

// 打印输出 "Daffy Duck"
System.out.println(who.first + " " + who.last);
}

9.4.2 在 Java 中镜像 SQL 继承

可以定义 SQL 结构化类型来构成继承层次。例如,不妨考虑继承自 PERSON 的 SQL 类型 STUDENT:

CREATE TYPE PERSON AS OBJECT (NAME VARCHAR(20), BIRTH DATE);

CREATE TYPE STUDENT AS OBJECT EXTENDS PERSON (GPA NUMERIC(4,2));

以下的 Java 类型可以表示这些 SQL 类型的数据。类 Student 扩展了 Person,同时镜像 SQL 类型层次结构。子类的 SQLData.readSQL()SQLData.writeSQL() 方法将每一个调用和其父类中相应方法级联在一起,其目的是在读写子类属性之前先读/写父类属性。

   import java.sql.*;
   ...
   public class Person implements SQLData {
     public String name;
     public Date birth;

     private String sql_type;
public String getSQLTypeName() { return sql_type; }

     public void readSQL (SQLInput data, String type)
	throws SQLException {
       sql_type = type;
       name = data.readString();
       birth = data.readDate();
     }

     public void writeSQL (SQLOutput data)
     	throws SQLException {
       data.writeString(name);
       data.writeDate(birth);
     }
   }

   public class Student extends Person {
     public float GPA;

     private String sql_type;
public String getSQLTypeName() { return sql_type; }

     public void readSQL (SQLInput data, String type)
	throws SQLException {
sql_type = type;
       super.readSQL(data, type);
       GPA = data.readFloat();
     }

     public void writeSQL (SQLOutput data)
     throws SQLException {
       super.writeSQL(data);
       data.writeFloat(GPA);
     }
   }

Java 类层次结构并不需要镜像 SQL 继承层次。例如,可以在没有父类的情况下声明以上的 Student 类。这种情况下,Student 所含的域可能不仅保存有 STUDENT 自身所声明的属性,而且保存了 SQL 类型 STUDENT 的继承属性。

9.4.3 将 SQL distinct 类型映射到 Java 的示例

SQL distinct 类型 (MONEY) 及表示该类型的 Java 类 Money:


-- SQL 定义
CREATE TYPE MONEY AS NUMERIC(10,2);

// Java 定义
public class Money implements SQLData {

public java.math.BigDecimal value;

private String sql_type;
public String getSQLTypeName() { return sql_type; }

public void readSQL (SQLInput stream, String type)
	throws SQLException {
sql_type = type;
value = stream.readBigDecimal();
}

public void writeSQL (SQLOutput stream) throws SQLException {
stream.writeBigDecimal(value);
}
}

9.5 方法的通用性

用户在自定义 Java 类来表示 SQL 结构化类型和 distinct 类型方面具有相当大的灵活性。他们控制内嵌 SQL 属性类型到 Java 域类型的映射方式。而且还控制 SQL 名称(类型和属性的名称)到 Java 名称(类和域名称)的映射方式。用户不仅能增加实现特定于域功能的域和方法(增加到代表 SQL 类型的 Java 类中),而且还可以生成 JavaBeans 作为代表 SQL 类型的类。

用户甚至可以将单个 SQL 类型映射到不同的 Java 类上,这取决于不定的条件。为此,用户必须自定义 SQLData.readSQL() 的实现来在不同的条件下构造和返回不同类的对象。

同样,用户可以将单个 SQL 值映射到多个 Java 对象上,而这又是通过自定义 SQLData.readSQL() 的实现来构造多个对象并将 SQL 属性分配到这些对象的域中来完成的。

SQLData.readSQL() 方法的自定义可以递增地组装类型-映射对象,诸如此类。我们相信这些灵活性可以使 JDBC 的用户能为不同的应用程序映射相应的 SQL 类型。

9.6 NULL 数据

JDBC 应用程序使用 JDBC 现有的 getObject()setObject() 机制来检索和存储 SQLData 值。我们注意到:当 PreparedStatement.setObject() 方法的第二个参数 x 具有 Java 值 null 时,JDBC 执行 SQL 语句的方式就仿佛 SQL 字符 NULL 已经取代语句中的该参数:

  void setObject (int i, Object x) throws SQLException;

当参数 x 值为空时,并不强制要求相应的参数表达式是 SQL 可以接受的 Java 类型(如果其值为非空,则该 Java 类型可以成功地传给 SQL 语句)。Java null 不带任何类型信息。例如,可以将类 AntiMatter 的空 Java 变量作为参数传给 SQL 语句(该语句要求 SQL 类型 MATTER 的值)而不会导致错误,即使相应的类型-映射对象不允许将 MATTER 转换成 AntiMatter

9.7 总结

8 章和第 9 章介绍了支持新型 SQL 类型的 JDBC 扩展。扩展具有以下功能:



目录 | 上一页 | 下一页


jdbc@eng.sun.comjdbc-business@eng.sun.com

版权所有 © 1996, 1997 Sun Microsystems, Inc. 保留所有权利