JavaDB(Apache Derby)でテーブル自身の情報(いわゆるメタデータ)を保持するのにそのまま使えそうなクラスのメモ。
および、それらを使ったJDBC使用例。
(ただし、非公開APIもあるみたい)
|
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文を作成する事が出来る。
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();
}
}
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)は、以下のように書くことが出来る。
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]
これを使うと、例えば以下の様に書くことが出来る。
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文を実行する例。
import org.apache.derby.iapi.types.DataValueDescriptor;
public interface DerbyBean {
public DataValueDescriptor getDataValueDescriptor(int n);
}
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);
}
}
}
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]
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();
}
}
public class SampleTable extends DerbyTable {
〜
@Override
public SampleBean newBean() { ←共変戻り値型
return new SampleBean();
}
}
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();
}
}
通常、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等のツールでプロセスのロックを解除できるので、ロックファイルを削除できる。
ただし、無理矢理なロック解除なので、その後も正しく使い続けられる保証は当然無いと思う。が、クラスローダーの所為でロックされている時は(少なくとも表面上は)大丈夫っぽい。