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