S-JIS[2009-02-23/2009-12-25] 変更履歴

JavaDB APIメモ

JavaDB(Apache Derby)でテーブル自身の情報(いわゆるメタデータ)を保持するのにそのまま使えそうなクラスのメモ。
および、それらを使ったJDBC使用例。
(ただし、非公開APIもあるみたい)


Derbyのソース

derby.jarにはJavaソースは付いていない。JDKのディレクトリー内にも無いような気がする。
ソースが欲しかったらApache Derbyのダウンロードページからダウンロードすることが出来る。

JDK1.6.0に付いているJavaDBの場合、derbyのバージョンは10.2.1.7。
ただ、10.2.1.7のソースは無いようなので、一番近そうな10.2.1.6で我慢。

db-derby-10.2.1.6-src.zipをダウンロードすれば良い。と言いたいところだが、なぜかファイルが壊れている…。
db-derby-10.2.1.6-src.tar.gzの方は大丈夫なので、そちらをダウンロードする。
解凍すると、Javaソース以外にも色々入っている。
このうち、db-derby-10.2.1.6-src/java/engineに入っているのがderby.jarのソースっぽい。
(Eclipseでは、derby.jarに対してこの部分をソースとして添付すればよい。自分はengine.zipを作ってそのファイルを使った)


テーブル名

テーブル名を保持するのは、TableNameクラス

import org.apache.derby.impl.sql.compile.TableName;

スキーマが「APP」で、「SAMPLE」という名前のテーブルは以下のようになる。

	TableName tableName = new TableName();
	tableName.init("APP", "SAMPLE");
	System.out.println(tableName.getTableName());
	System.out.println(tableName.getFullName());
↓
SAMPLE
APP.SAMPLE

カラム(項目)のデータ型

テーブルの項目の属性(データ型)を保持するのは、DataTypeDescriptorクラス

import org.apache.derby.iapi.types.DataTypeDescriptor;
import org.apache.derby.iapi.types.TypeId;
	// INTEGERの例
	DataTypeDescriptor intAttr = new DataTypeDescriptor(TypeId.INTEGER_ID, false);
	// CHAR(4)の例
	DataTypeDescriptor charAttr = new DataTypeDescriptor(TypeId.CHAR_ID, false, 4);
	// booleanの例
	DataTypeDescriptor boolAttr = new DataTypeDescriptor(TypeId.INTEGER_ID, false);
	// VARCHAR(1024)の例
	DataTypeDescriptor varcharAttr = new DataTypeDescriptor(TypeId.getBuiltInTypeId("VARCHAR"), false, 1024);

DataTypeDescriptorのコンストラクターの第2引数は、その項目がNULL可能かどうか(nullable)を渡す。
falseだとNULL不可、つまりNOT NULLであることを表す。

第3引数は、桁数。

JavaDBでは項目のデータ型にBOOLEANは無いので、TypeIdはINTEGER_ID(INTEGER)やCHAR_ID(CHAR(1))で代用する。
その場合、trueが1や'1'、falseが0や'0'でテーブルのデータとして入る。
(BOOLEANは、将来サポートされるらしい?
 2009-03-22時点でCREATE TABLE文にBOOLEANを指定することは出来ないが、SYSSTATEMENTSテーブルの様に、BOOLEANになっている項目もある 。[2009-03-22]

VARCHARには何故かTypeIdの定数が割り当てられていない(privateになっている)ので、TypeId#getBuiltInTypeId()を使用してTypeIdを取得する。


DataTypeDescriptorには、データ型の文字列を取得するメソッドがある。

	System.out.print(charAttr.getSQLstring());
	System.out.print(charAttr.isNullable() ? "" : " NOT NULL");
↓
CHAR(4) NOT NULL

カラム名(項目名)

テーブルの項目名とデータ型のペアを保持するのは、GenericColumnDescriptorクラス

import org.apache.derby.impl.sql.GenericColumnDescriptor;
	public static final GenericColumnDescriptor SAMPLE_ID =
		new GenericColumnDescriptor("SAMPLE_ID", new DataTypeDescriptor(TypeId.INTEGER_ID, false));

getName()で項目名、getType()でデータ型を取得できる。

	System.out.print(SAMPLE_ID.getName());
	System.out.print(' ');
	System.out.print(SAMPLE_ID.getType().getSQLstring());
	System.out.print(SAMPLE_ID.getType().isNullable() ? "" : " NOT NULL");
↓
SAMPLE_ID INTEGER NOT NULL

テーブル作成

以上のクラスを使って、CREATE TABLE文を作成する事が出来る。

DerbyTable.java:

import java.sql.*;
import java.util.List;

import org.apache.derby.iapi.types.DataTypeDescriptor;
import org.apache.derby.impl.sql.GenericColumnDescriptor;
import org.apache.derby.impl.sql.compile.TableName;
public abstract class DerbyTable {

	public abstract TableName getTableName();

	public abstract List<GenericColumnDescriptor> getColumnList();

	public List<GenericColumnDescriptor> getPrimaryKey() {
		return null;
	}
	public boolean createTable(Connection conn) throws SQLException {
		String ddl = getDDL();
		Statement stat = conn.createStatement();
		try {
			return stat.execute(ddl);
		} finally {
			stat.close();
		}
	}

	public String getDDL() {
		StringBuilder sb = new StringBuilder(256);

		sb.append("create table ");
		sb.append(getTableName().getFullTableName());

		sb.append(" (\n\t");
		List<GenericColumnDescriptor> clist = getColumnList();
		for (int i = 0; i < clist.size(); i++) {
			if (i != 0) {
				sb.append(",\n\t");
			}
			GenericColumnDescriptor cd = clist.get(i);
			sb.append(cd.getName());
			sb.append(' ');
			DataTypeDescriptor type = cd.getType();
			sb.append(type.getSQLstring());
			if (!type.isNullable()) {
				sb.append(" not null");
			}
		}

		List<GenericColumnDescriptor> plist = getPrimaryKey();
		if (plist != null) {
			sb.append(",\nconstraint PK_");
			sb.append(getTableName().getTableName());
			sb.append(" primary key (");
			for (int i = 0; i < plist.size(); i++) {
				if (i != 0) {
					sb.append(", ");
				}
				GenericColumnDescriptor cd = plist.get(i);
				sb.append(cd.getName());
			}
			sb.append(")");
		}

		sb.append("\n)");
		return sb.toString();
	}
}

SampleTable.java:

import java.util.Arrays;
import java.util.List;

import org.apache.derby.iapi.types.DataTypeDescriptor;
import org.apache.derby.iapi.types.TypeId;
import org.apache.derby.impl.sql.GenericColumnDescriptor;
public class SampleTable extends DerbyTable {

	private static SampleTable INSTANCE;

	public static SampleTable getInstance() {
		if (INSTANCE == null) {
			INSTANCE = new SampleTable();
		}
		return INSTANCE;
	}
	private static final TableName TABLE_NAME = new TableName();
	static {
		TABLE_NAME.init("APP", "SAMPLE");
	}

	public static final GenericColumnDescriptor SAMPLE_ID = new GenericColumnDescriptor(
		"SAMPLE_ID",
		new DataTypeDescriptor(TypeId.INTEGER_ID, false));

	public static final GenericColumnDescriptor FLAG = new GenericColumnDescriptor(
		"FLAG",
		new DataTypeDescriptor(TypeId.INTEGER_ID, false));

	public static final GenericColumnDescriptor DATA = new GenericColumnDescriptor(
		"DATA",
		new DataTypeDescriptor(TypeId.getBuiltInTypeId("VARCHAR"), false, 64));

	private static final List<GenericColumnDescriptor> COLUMN_LIST = Arrays.asList(SAMPLE_ID, FLAG, DATA);
	private static final List<GenericColumnDescriptor> PKEY_LIST   = Arrays.asList(SAMPLE_ID);
	@Override
	public TableName getTableName() {
		return TABLE_NAME;
	}

	@Override
	public List<GenericColumnDescriptor> getColumnList() {
		return COLUMN_LIST;
	}

	@Override
	public List<GenericColumnDescriptor> getPrimaryKey() {
		return PKEY_LIST;
	}
}

データ値

項目のデータ値を保持するのは、DataValueDescriptorインターフェースを実装したクラス。[2009-02-24]
具体的にはSQLIntegerやSQLChar・SQLVarcharなど。これらはDataType抽象クラスから派生している。

例えばデータを保持するクラス(JavaBean)は、以下のように書くことが出来る。

SampleBean.java:

import org.apache.derby.iapi.error.StandardException;
import org.apache.derby.iapi.types.*;
public class SampleBean {

	protected SQLInteger sampleId = new SQLInteger();

	public void setSampleId(int n) {
		sampleId.setValue(n);
	}

	public int getSampleId() {
		return sampleId.getInt();
	}
	protected SQLBoolean flag = new SQLBoolean();

	public void setFlag(boolean b) {
		flag.setValue(b);
	}

	public boolean getFlag() {
		return flag.getBoolean();
	}
	protected SQLVarchar data = new SQLVarchar();

	public void setData(String s) {
		data.setValue(s);
	}

	public String getData() {
		try {
			return data.getString();
		} catch (StandardException e) {
			throw new RuntimeException(e);
		}
	}
}

setValue()メソッドは、色々な型の引数でオーバーロードされている。
値の取得はgetInt()・getBoolean()・getString()など、メソッド名に型の名前が付いている。
これらのメソッドは共通のDataValueDescriptorインターフェースで宣言され、DataTypeクラスで定義され、データ型に応じた具象クラスでオーバーライドされて実装されている。
データ型によっては変換できないものもあり、その場合はStandardExceptionの例外が発生する。


toString()メソッドで、それぞれの値の文字列表現を取得することが出来る。[2009-02-26]
ただし値がNULLの場合、「NULL」という文字列が返る(空型のnullでなく)。
(getString()ではnullが返る(文字列ではなく、正真正銘の空型))

データがNULLかどうか知りたい場合はisNull()を使う。
データにNULLをセットしたい場合はsetToNull()を使う。(実際にDBに入れるまで、NOT NULL制約とは無関係)

データがNULLの場合、例えばintを返すgetInt()は 0を返す。

setString(null)を実行すると、データはNULLになる。
setString("")を実行すると、データは空文字列になる(NULLではない)。


また、DataValueDescriptorは、データ型を表すDataTypeDescriptorからインスタンスを生成することも出来る。[2009-02-24]
これを使うと、例えば以下の様に書くことが出来る。

SampleBean.java

import org.apache.derby.iapi.error.StandardException;
import org.apache.derby.iapi.types.DataValueDescriptor;
public class SampleBean {

	protected DataValueDescriptor sampleId = SampleTable.SAMPLE_ID.getType().getNull();

	protected DataValueDescriptor flag = SampleTable.FLAG.getType().getNull();

	protected DataValueDescriptor data = SampleTable.DATA.getType().getNull();
〜
}

setValue()やgetInt()等は先の例と同様。
ただしDataValueDescriptorでは全てthrows StandardExceptionが宣言されているので、try〜catchが必要。


データ挿入

DataValueDescriptorには、setInto()という便利なメソッドが用意されている。[2009-02-24]
これは、PreparedStatementの「?」に実際の値を埋め込むもの。
これを使えば、実際の型が何であるかを意識せずにSQL文に値を埋め込むことが出来る。

以下、これを使ってinsert文を実行する例。

DerbyBean.java:

import org.apache.derby.iapi.types.DataValueDescriptor;
public interface DerbyBean {

	public DataValueDescriptor getDataValueDescriptor(int n);
}

SampleBean.java

public class SampleBean implements DerbyBean {
〜
	@Override
	public DataValueDescriptor getDataValueDescriptor(int n) {
		switch (n) {
		case 0:
			return sampleId;
		case 1:
			return flag;
		case 2:
			return data;
		default:
			throw new IllegalArgumentException("index=" + n);
		}
	}
}

DerbyTable.java

public abstract class DerbyTable {
〜
	public boolean insert(Connection conn, DerbyBean bean) throws SQLException {
		PreparedStatement stat = getInsertStatement(conn);
		try {
			List<GenericColumnDescriptor> clist = getColumnList();
			for (int i = 0; i < clist.size(); i++) {
				DataValueDescriptor value = bean.getDataValueDescriptor(i);
				value.setInto(stat, i + 1);
			}
			return stat.execute();
		} catch (StandardException e) {
			throw new SQLException(e);
		} finally {
			stat.close();
		}
	}

	public PreparedStatement getInsertStatement(Connection conn) throws SQLException {
		String sql = getInstertSQL();
		return conn.prepareStatement(sql);
	}

	public String getInstertSQL() {
		StringBuilder sb = new StringBuilder(256);

		sb.append("insert into ");
		sb.append(getTableName().getFullTableName());

		sb.append(" (\n\t");
		List<GenericColumnDescriptor> clist = getColumnList();
		for (int i = 0; i < clist.size(); i++) {
			if (i != 0) {
				sb.append(",\n\t");
			}
			GenericColumnDescriptor cd = clist.get(i);
			sb.append(cd.getName());
		}
		sb.append("\n) values (\n\t");
		for (int i = 0; i < clist.size(); i++) {
			if (i != 0) {
				sb.append(", ");
			}
			sb.append('?');
		}

		sb.append("\n)");
		return sb.toString();
	}
}

データ取得

SELECT結果(ResultSet)からDataValueDescriptorに値をセットするには、setValueFromResultSet()を使用する。[2009-02-25]

DerbyTable.java

public abstract class DerbyTable {
〜
	public abstract DerbyBean newBean();
	public List<DerbyBean> selectAll(Connection conn) throws SQLException {
		List<DerbyBean> ret = new ArrayList<DerbyBean>();
		List<GenericColumnDescriptor> clist = getColumnList();

		String sql = getSelectSQL();
		PreparedStatement stat = conn.prepareStatement(sql);
		try {
			ResultSet rs = stat.executeQuery();
			try {
				while (rs.next()) {
					DerbyBean bean = newBean();

					for (int i = 0; i < clist.size(); i++) {
						GenericColumnDescriptor c = clist.get(i);
						DataValueDescriptor v = bean.getDataValueDescriptor(i);
						try {
							v.setValueFromResultSet(rs, i + 1, c.getType().isNullable());
						} catch (StandardException e) {
							throw new SQLException(e);
						}
					}

					ret.add(bean);
				}
			} finally {
				rs.close();
			}
		} finally {
			stat.close();
		}

		return ret;
	}

	public String getSelectSQL() {
		StringBuilder sb = new StringBuilder(256);

		sb.append("select\n\t");

		List<GenericColumnDescriptor> clist = getColumnList();
		for (int i = 0; i < clist.size(); i++) {
			if (i != 0) {
				sb.append(",\n\t");
			}
			GenericColumnDescriptor c = clist.get(i);
			sb.append(c.getName());
		}

		sb.append("\nfrom ");
		sb.append(getTableName().getFullTableName());

		return sb.toString();
	}
}

SampleTable.java

public class SampleTable extends DerbyTable {
〜
	@Override
	public SampleBean newBean() {	←共変戻り値型
		return new SampleBean();
	}
}

SampleBean.java(ただ単にtoString()を入れただけ):

public class SampleBean implements DerbyBean {
〜
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder(128);

		sb.append(SampleTable.SAMPLE_ID.getName());
		sb.append("=");
		sb.append(sampleId);
		sb.append(", ");

		sb.append(SampleTable.FLAG.getName());
		sb.append("=");
		sb.append(flag);
		sb.append(", ");

		sb.append(SampleTable.DATA.getName());
		sb.append("=");
		sb.append(data);

		return sb.toString();
	}
}

Driverを直接使った接続

通常、JDBCでのDB接続ではDriverManagerを使用して接続するが、直接Driverクラスを使って接続する事も出来る。[2009-04-23]
この方法の場合、クラスローダーを使ってderby.jarの場所を後から指定することが出来る。

		Connection conn = DriverManager.getConnection(url, info);
		Driver driver = new org.apache.derby.jdbc.EmbeddedDriver();
		Connection conn = driver.connect(url, info);
		URL[] lib = { new File("C:/Program Files/Sun/JavaDB/lib/derby.jar").toURI().toURL() };
		URLClassLoader loader = URLClassLoader.newInstance(lib);
		Class<Driver> cd = (Class<Driver>) loader.loadClass("org.apache.derby.jdbc.EmbeddedDriver");
		Driver driver = cd.newInstance();
		Connection conn = driver.connect(url, info);

ところが、JavaDBの場合、同一のDB(接続URL)に別インスタンスのクラスローダーで接続しようとすると、たとえ最初の接続を切断(close())していても、接続できない。
JavaDBのドライバー内でキャッシュか何かが残っているらしく、以下のエラーになる。

XJ040: データベース 'C:/temp/javadb/sample1' を始動できません。詳しくは、次の例外を参照してください。
XSDB6: Derby の別のインスタンスがすでにデータベース C:\temp\javadb\sample1 をブートしている可能性があります。

これは、別アプリケーションが接続している時のエラーとまったく同様(なので、区別がつかない)。
EclipseのプラグインであるDBViewer自作のJavaDBツールでこの状態になった)

こういう状態に陥ったら、接続していたアプリケーションを一旦終了させるしか無い。


結局この問題は、シャットダウンが行われていないのが原因だった。[2009-12-25]
コネクションのクローズ(切断)だけでは駄目で、シャットダウンをきちんと行えば、ドライバーのインスタンスを作り直して同一DBに再接続できる。


一応、アプリケーションを終了させなくても(シャットダウンしなくても)、DB側で何とかする方法も無くはないが。

接続URLが「C:/temp/javadb/sample1」の場合、そのディレクトリー直下にロックファイルが在る。

> cd C:\temp\javadb\sample1
C:\temp\javadb\sample1> dir /b *.lck
db.lck

C:\temp\javadb\sample1> del db.lck
C:\temp\javadb\sample1\db.lck
プロセスはファイルにアクセスできません。別のプロセスが使用中です。

Windowsのエクスプローラーから削除操作をすればUnlocker等のツールでプロセスのロックを解除できるので、ロックファイルを削除できる。
ただし、無理矢理なロック解除なので、その後も正しく使い続けられる保証は当然無いと思う。が、クラスローダーの所為でロックされている時は(少なくとも表面上は)大丈夫っぽい。


JavaDBへ戻る / Java目次へ戻る
メールの送信先:ひしだま