S-JIS[2009-05-02] 変更履歴

ユーザー定義型

Oracleのユーザー定義型(TYPE)のメモ。
いわば構造体のようなデータを定義し、テーブルの項目の型として使える。


TYPE作成

create or replace type タイプ名 as object (
  属性名 型, …
)
SQL> create or replace type TYPE0 as object (
  2    key  numeric(4),
  3    data varchar2(100)
  4 );
  5 /

型が作成されました。

SQL> show err
エラーはありません。

JavaからアクセスできるTYPEを作る場合は、以下のようにオプションを追加する。

create or replace type TYPE1 as object
external name 'jp.hishidama.sample.jdbc.oracle.Type1' language java
using SQLData(
  KEY   numeric(4),
  DATA1 varchar2(100),
  DATA2 varchar2(200)
);
/

「using SQLData」は、その実体となるクラス(上記の例ではjp.hishidama.sample.jdbc.oracle.Type1)がjava.sql.SQLDataをimplementsしている事を示す。
他に「using Serializable」や「using ORAData」等がある。


テーブル作成・データ作成

作ったTYPEは、create tableで使うことが出来る。

create table TYPESAMPLE (
  DT1 type1
);

insert文は以下のように書く。

insert into TYPESAMPLE values( TYPE1(1234,'123ABCあいう','zzz') );

SQL*Plusでselectすると、以下の様に表示される。

SQL> select * from TYPESAMPLE;

DT1(KEY, DATA1, DATA2)
--------------------------------------------------------------------------------
TYPE1(1234, '123ABCあいう', 'zzz')

Javaからのアクセス

JavaJDBC)からユーザー定義型のデータを取得することが出来る。
この場合、java.sql.SQLDataを実装したクラスを普通に作っておき、実行時にそのクラスがCLASSPATHに入っていればよい。

src/jp/hishidama/sample/jdbc/oracle/Type1.java:

import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.sql.SQLOutput;
public class Type1 implements SQLData {

	protected int key;
	protected String data1;
	protected String data2;

	@Override
	public String getSQLTypeName() throws SQLException {
		return "TYPE1";
	}

	@Override
	public void readSQL(SQLInput stream, String typeName) throws SQLException {
		// TODO 自動生成されたメソッド・スタブ	→実装例
	}

	@Override
	public void writeSQL(SQLOutput stream) throws SQLException {
		// TODO 自動生成されたメソッド・スタブ
	}

	@Override
	public String toString() {
		return "key=" + key + ",data1=" + data1 + ",data2=" + data2;
	}
}

これを使う側は、特に何かの定義をする必要は無い。(import文とかも不要)
ResultSet#getObject()を使えば、その中で自動的に該当クラスがロードされ、インスタンスが作られる。
(実行時のクラスパスにそのクラスが含まれている必要はある)

		Class.forName("oracle.jdbc.driver.OracleDriver");
		Connection conn = DriverManager.getConnection("jdbc:oracle:oci:@ora92", "scott", "tiger");

		Statement stat = conn.createStatement();
		try {
			ResultSet rs = stat.executeQuery("select * from TYPESAMPLE");
			try {
				//メタデータを表示してみる
				ResultSetMetaData md = rs.getMetaData();
				for (int i = 1; i <= md.getColumnCount(); i++) {
					String tname = md.getColumnTypeName(i);
					String cname = md.getColumnClassName(i);
					System.out.printf("rmd[%d]\t%s\t%s%n", i, tname, cname);
				}

				//データを取得・表示
				while (rs.next()) {
					for (int i = 1; i <= md.getColumnCount(); i++) {
						Object dt = rs.getObject(i);	//データ取得
						System.out.println(dt.toString());
					}
				}
			} finally {
				rs.close();
			}
		} finally {
			stat.close();
		}

メタデータのTYPE_NAMEはOracle側のTYPE名、CLASS_NAMEは定義されているクラス名となる。

rmd[1]	SCOTT.TYPE1	jp.hishidama.sample.jdbc.oracle.Type1

SQLDataにはreadSQL()writeSQL()といったメソッドがあり、Oracle(DB)からJavaのクラスのフィールドへの値の設定(およびその逆)は自分で実装する必要がある。

	@Override
	public void readSQL(SQLInput stream, String typeName) throws SQLException {
		key   = stream.readInt();
		data1 = stream.readString();
		data2 = stream.readString();
	}

今回のサンプルで使ったデータ(key=1234、data1=「123ABCあいう」、data2=「zzz」)だと、以下のように表示された。(Oracle9iのojdbc14.jar)

key=1234,data1=0x31323341424382A082A282A4,data2=0x7A7A7A

どうも、“文字コードを十六進数で文字化した”文字列として返って来ているらしい(苦笑)
文字コードはShift_JISのようだが、これはDBがShift_JISだからだと思われる。

SQL> select dump(dt1) from TYPESAMPLE;

DUMP(DT1)
--------------------------------------------------------------------------------
Typ=121 Len=28: 132,1,254,0,0,0,28,3,194,13,35,12,49,50,51,65,66,67,130,160,130,162,130,164,3,122,122,122
					    ↑123    ↑ABC    ↑あいう		   ↑zzz

ちなみにOracle11gのojdbc6.jarだと、文字化けした(爆)

key=1234,data1=???,data2=???

という訳で、(文字化けしない方なら、)無理矢理文字コードから戻してやれば、期待した文字列に変換することは出来る。

	@Override
	public void readSQL(SQLInput stream, String typeName) throws SQLException {
		key   = stream.readInt();
		data1 = conv(stream.readString());
		data2 = conv(stream.readString());
	}

	protected String conv(String s) {
		int len = (s.length() - 2) / 2;
		byte[] r = new byte[len];
		for (int i = 0; i < len; i++) {
			int j = i * 2 + 2;
			byte b = Integer.decode("0x" + s.substring(j, j + 2)).byteValue();
			r[i] = b;
		}
		try {
			return new String(r, "MS932");
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
	}

…けど、なんだかなぁ(苦笑)


SQLInputにはreadNString()というメソッドもある。

	@Override
	public void readSQL(SQLInput stream, String typeName) throws SQLException {
		key   = stream.readInt();
		data1 = stream.readNString();
		data2 = stream.readNString();
	}

が、これはJDK1.6で追加されたものなので、ojdbc14.jar(JDK1.4用)では実装されていない(実行時エラーになる)。
ojdbc6.jarだと以下のように文字化けした(苦笑)

key=1234,data1=???,data2=???

(そもそもnvarchar用だから、varchar2には使えないのかも?
もしくは、DB自体はOracle9iだから、Oracle11gのクライアントから接続するには何か設定が必要とか?)


SQLInputのreadCharacterStream()というメソッドは、Unicode文字を返してくれるらしい。
ただしこれの戻り値はReaderなので、Stringへの変換はコーディングする必要がある。

	@Override
	public void readSQL(SQLInput stream, String typeName) throws SQLException {
		key   = stream.readInt();
		data1 = readReader(stream.readCharacterStream());
		data2 = readReader(stream.readCharacterStream());
	}

	protected String readReader(Reader r) throws SQLException {
		try {
			StringBuilder sb = new StringBuilder();
			for (;;) {
				int c = r.read();
				if (c < 0) break;
				sb.append((char) c);
			}
			return sb.toString();
		} catch (IOException e) {
			throw new SQLException(e);
		} finally {
			try {
				r.close();
			} catch (IOException e) {
				throw new SQLException(e);
			}
		}
	}

しかし実行してみたら、例外が発生した(Oracle9iのojdbc14.jarの場合)。

java.sql.SQLException: Non supported character set: oracle-character-set-832

ojdbc14.jarと同じ場所にあるnls_charset12.jarをクラスパスに追加してやると実行できるようになり、文字化けせずに表示された。

なお、Oracle11gのojdbc6.jarだと以下のような例外メッセージなので、追加すべきjarファイルが分かり易い。

java.sql.SQLException: サポートされないキャラクタ・セットです(orai18n.jarをクラスパスに追加してください): oracle-character-set-832

他に、SQLInputにはreadBinaryStream()というメソッドもある。
これはバイト列(InputStream)でデータを取得でき、自分で文字コード変換をすることになるので、Oracleの文字コード変換用jarファイルは不要になる。

	@Override
	public void readSQL(SQLInput stream, String typeName) throws SQLException {
		key   = stream.readInt();
		data1 = readStream(stream.readBinaryStream());
		data2 = readStream(stream.readBinaryStream());
	}

	protected String readStream(InputStream is) throws SQLException {
		try {
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			for (;;) {
				int d = is.read();
				if (d < 0) break;
				bos.write(d);
			}
			return new String(bos.toByteArray(), "MS932");
		} catch (IOException e) {
			throw new SQLException(e);
		} finally {
			try {
				is.close();
			} catch (IOException e) {
				throw new SQLException(e);
			}
		}
	}

SQLメモへ戻る / 情報照会へ戻る / Oracle目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま