目次 | 前の項目 | 次の項目 JDBCTM ガイド: 入門


9 SQL 型のカスタマイズ

この章では、SQL の構造化型および弁別型から Java クラスへのマッピング (対応付け) をカスタマイズするために、JDBC 2.0 API で用意されている機構について説明します。カスタマイズの機構は、JDBC API への最低限の拡張機構の追加だけで実現されています。具体的には、JDBC の以前のバージョンから存在する getObject() および setObject() の機構が拡張されているだけです。

9.1     型のマッピング

SQL のユーザ定義型 (構造化型と弁別型) と Java クラス間のカスタムマッピングは、java.util.Map のインスタンスにより保持されます。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 型の型マッピングにカスタムマッピングが挿入されている場合は、その接続で行われるすべての処理で、その SQL 型の値に対してこのカスタムマッピングが使用されます。なお、getXXX() メソッドと setXXX() メソッドの呼び出し時に型マッピングオブジェクトを明示的に指定することで、特定の Connection に対応するカスタムマッピングまたはデフォルトのマッピングをオーバーライドすることも可能です。

9.2     Java クラスに関する規約

カスタム型マッピングの中で使用する Java クラスでは、新しいインタフェース java.sql.SQLData を実装しなければなりません。SQLData インタフェースには、SQL のユーザ定義型のインスタンスを Java クラスのインスタンスに変換したり、その逆を行うメソッドが含まれています。たとえば、SQLData.readSQL() メソッドは、データ値のストリームを読み出して Java オブジェクトを構築し、反対に、SQLData.writeSQL() メソッドは、Java オブジェクトの値のシーケンスをストリームに書き込みます。あとでも説明しますが、通常、これらのメソッドは、データベーススキーマを理解するツールによって生成されます。

SQL と Java の間でのデータのやりとりにストリームを使用するというこの方法は、考え方としては Java のオブジェクト直列化に似ています。データの読み書きは、JDBC ドライバの提供する SQL データストリームを介して行われます。SQL データストリームは、さまざまなネットワークプロトコル上にさまざまなデータ形式のものが実装できます。SQL データストリームは、それを介して、構造化型の「深さ優先」の走査形式でリーフ SQL データ項目 (SQL の構造化型の構成要素) を読み出す (または書き込む) ことができる、任意の論理的データ表現上に実装できます。つまり、SQL の構造化型の属性は、型宣言部分で記述されている順序でストリームに現れ、それぞれの属性 (おそらくは構造化された属性) の値は、次の属性より前に完全に現れます (その構造を再帰的に走査)。継承を使用する SQL の構造化型のデータの場合は、属性は継承された順序でストリームに現れなければなりません。つまり、スーパータイプの属性がサブタイプの属性より前に現れなければなりません。多重継承を使用している場合は、スーパータイプは、型宣言部分で記述されている順序でストリームに現れるようにすべきです。このプロトコルでは、データベースサーバが Java に関する知識を持っている必要はありません。

9.3     SQL データのストリーム

この項では、SQL から Java への型マッピングをカスタマイズできる、SQLInput と SQLOutput の 2 つのストリームインタフェースについて説明します。

9.3.1 データの取得

SQL の構造化型および弁別型のデータがデータベースから取り出されると、データは SQLInput インタフェースを実装したストリームに「到達」します。SQLInput インタフェースには、ストリームから個々のデータ値を順次読み出すためのメソッドが含まれています。SQLInput ストリームを使用して、SQLData オブジェクトのフィールドに値を設定する方法を下の例に示します。SQLData オブジェクト (この例では this オブジェクト) に含まれる持続フィールドは、String sBlob blobEmployee emp の 3 つです。

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


SQLInput.readString() メソッドは、ストリームから String 値を読み出します。SQLInput.readBlob() メソッドを使用すると、ストリームから Blob 値を取り出すことができます。デフォルトでは、Blob インタフェースは SQL のロケータを使用して実装されています。したがって、readBlob() を呼び出しても、BLOB の内容は JDBC クライアント上には生成されません。また、SQLInput.readObject() メソッドを使用すると、ストリームからオブジェクト参照を取得できます。この例では、SQLInput.readObject() によって返された ObjectEmployee にナロー変換しています。

SQLInput インタフェースには、これ以外にも、JDBC のさまざまな型のデータを読み出すための readXXX() メソッドが多数定義されています。readXXX() メソッドから返された値が null でないかどうかは、SQLInput.wasNull() メソッドでチェックできます。

9.3.2 データの格納

SQLData オブジェクトを setXXX() メソッドで入力パラメータとして JDBC に引き渡すと、JDBC ドライバは受け取ったオブジェクトの SQLData.writeSql() メソッドを呼び出して、そのオブジェクトの内容のストリーム表現を取得します。writeSQL() メソッドは、オブジェクトのデータを、SQL のユーザ定義型の表現として SQLOutput ストリームに書き込みます。writeSQL() メソッドは、通常、ツールを使って SQL の型定義から生成します。SQLOutput ストリームオブジェクトの使い方を次に示します。

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

SQLData オブジェクトの内容は、この例のようにして SQLOutput ストリームに書き込みます。SQLData オブジェクト (この例では this オブジェクト) には、String sBlob blobEmployee emp の 3 つの持続フィールドが含まれます。各フィールドは、順番に SQLOutput ストリームの sqlout に書き込まれます。SQLOutput インタフェースには、これ以外にも、JDBC のさまざまな型のデータを書き込むためのメソッドが定義されています。

9.4     さまざまな例

9.4.1 SQL の構造化型の例

次に示す SQL の例では、PERSON、FULLNAME、RESIDENCE の 3 つの構造化型を定義しています。構造化型の定義のあとは、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 のそれぞれの型に対して 1 つずつ、計 3 つの構造化型のインスタンスを生成しています。FULLNAME 属性は PERSON に組み込まれています。PERSON のインスタンスと RESIDENCE のインスタンスはテーブルの行として格納し、Ref 属性により、それぞれのインスタンスを相互に参照しています。

下に示すのは、上の SQL の構造化型に対応する Java クラスです。このようなクラスは、カタログテーブルから構造化型の定義を読み出す、SQL-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 {

  // setup mappings for the connection
  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);

  // prints "Daffy Duck"
  System.out.println(who.first + " " + who.last); 
}

9.4.2 Java での SQL の継承構造の表現

SQL の構造化型は、継承の階層を成すように定義することも可能です。たとえば、PERSON を継承した STUDENT という SQL 型を考えてみます。

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

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

下の Java クラスは、上の 2 つの SQL 型を表現したものです。Student クラスは、extends 節で 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 クラスをスーパークラスを使わずに宣言することも可能です。この場合は、SQL 型 STUDENT の継承属性を格納するフィールドと、STUDENT で宣言した属性を格納するフィールドを Student クラスに含めます。

9.4.3 SQL の弁別型を Java にマッピングする例

SQL の弁別型 MONEY と、それを表現した Java クラス Money を次に示します。


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

// Java definition
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     カスタマイズの概略

JDBC 2.0 API では、SQL の構造化型と弁別型を表す Java クラスを極めて柔軟にカスタマイズできます。まず、SQL の組み込み属性の型から Java のフィールドの型へのマッピングを制御できます。また、SQL の名前 (型と属性の名前) から Java の名前 (クラスとフィールドの名前) へのマッピングを制御できます。ドメイン固有の機能を実装したフィールドやメソッドを、SQL 型を表す Java クラスに追加することも可能です。さらに、SQL 型を表すクラスとしての JavaBeans を生成することもできます。

SQL の特定の型を、条件によって Java の異なるクラスにマッピングすることもできます。これには、条件によって異なるクラスのオブジェクトを構築し、返すように SQLData.readSQL() の実装をカスタマイズします。

同様に、SQL の特定の値を Java のオブジェクトのグラフにマッピングすることもできます。この場合もやはり、SQLData.readSQL() の実装をカスタマイズして、複数のオブジェクトを構築し、構築したオブジェクトのフィールドに SQL の属性を割り当てるようにします。

SQLData.readSQL() メソッドのカスタマイズによって、型マッピングオブジェクトを段階的に生成することも可能です。このような柔軟性により、アプリケーションの種類に応じて、SQL 型を適切にマッピングできるようになっています。

9.6     NULL データ

JDBC アプリケーションでは、JDBC の以前のバージョンから存在する getObject() および setObject() の機構を使用して、SQLData の値の取得と格納を行います。PreparedStatement.setObject() メソッドの第 2 パラメータ x が Java 値の null であるとき、対応する SQL 文は、その SQL 文の対応するパラメータの部分に SQL のリテラル NULL が指定されたかのように実行されます。

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

型の値が null ではない場合に、対応する引数式の型が、その SQL 文に問題なく引き渡せる Java の型でなければならないという規則はありません。Java の null には型の情報がありません。たとえば、AntiMatter クラスの null 変数は、対応する型マッピングオブジェクトが MATTER から AntiMatter への変換を許可していなくても、SQL 型 MATTER の値を要求する SQL 文に引数として渡すことができ、しかもエラーは発生しません。

9.7     まとめ

第 8 章第 9 章では、新しい種類の SQL 型をサポートする JDBC の拡張機構について説明しました。この拡張機構は、次のような特徴を持っています。



目次 | 前の項目 | 次の項目
jdbc@eng.sun.com または jdbc-business@eng.sun.com
Copyright © 1996, 1997 Sun Microsystems, Inc. All rights reserved.