S-JIS[2009-03-28/2010-07-24] 変更履歴
JDBCのメタデータは、データベース(RDB)の(データでなく)定義自体を扱う。
メタデータを利用して、テーブルの項目一覧だとか属性だとかを取得することが出来る。
DatabaseMetaDataはConnectionから取得できる。
ResultSetMetaDataはResultSetから取得できる。
import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet;
Connection conn = 〜; DatabaseMetaData dmd = conn.getMetaData(); ResultSet rs = dmd.getColumns(null, "スキーマ名", "テーブル名", "%"); try { while (rs.next()) { String name = rs.getString("COLUMN_NAME"); String type = rs.getString("TYPE_NAME"); System.out.println(name + "\t" + type); } } finally { rs.close(); }
getColumns()には、取得したいスキーマ名やテーブル名や項目名を指定する(スキーマ名は、要するにログインしているユーザー名)。
nullあるいは「%」を指定すると、全データがマッチする。
「%」は要するにSQLのWHERE句のLIKEなので、「項目名の一部%」や1文字にマッチする「_」も使える。
getColumns()の戻り値はResultSet。つまり、SELECTした結果が返ってきているのと同じ。
どんな項目がSELECTされているのかは、getColumns()のJavadocに書かれている。
Connection conn = 〜; DatabaseMetaData dmd = conn.getMetaData(); ResultSet rs = dmd.getPrimaryKeys(null, "スキーマ名", "テーブル名"); try { while (rs.next()) { String kname = rs.getString("PK_NAME"); String cname = rs.getString("COLUMN_NAME"); short seq = rs.getShort("KEY_SEQ"); System.out.println(cname + "\t" + seq + "\t" + kname); } } finally { rs.close(); }
使い方はgetColumns()と同様。
getPrimaryKeys()のJavadocには「KEY_SEQ
short => 主キー内の連番」といった記述がある。KEY_SEQ項目の属性が「short」であると書いてあるので、ResultSet#getShort()を使って値をする。
レコードを一意に識別する項目を取得するメソッドも用意されている。
使い方はgetColumns()と同様。
Connection conn = 〜; DatabaseMetaData dmd = conn.getMetaData(); ResultSet rs = dmd.getBestRowIdentifier(null, "スキーマ名", "テーブル名", DatabaseMetaData.bestRowTransaction, true); try { while (rs.next()) { String cname = rs.getString("COLUMN_NAME"); short pseud = rs.getShort("PSEUDO_COLUMN"); short scope = rs.getShort("SCOPE"); System.out.println(Arrays.toString( new Object[] { cname, getPseudoString(pseud), getScopeString(scope) } )); } } finally { rs.close(); }
getBestRowIdentifier()がどういう項目を返すかは、たぶんDBMS(JDBC)の実装次第。
でも
たぶん、基本的にプライマリキーを返す。
プライマリキーが定義されていないテーブルの場合、ユニークキーがあればそれを返す。
それも無ければ、Oracleは何も返さない。JavaDBは全項目を返す。
全項目が返って来ている場合、WHERE句で全項目に値を指定したとしても、レコードが特定できるとは限らない。
public static String getScopeString(short scope) { switch (scope) { case DatabaseMetaData.bestRowTemporary: return "bestRowTemporary"; case DatabaseMetaData.bestRowTransaction: return "bestRowTransaction"; case DatabaseMetaData.bestRowSession: return "bestRowSession"; default: return Short.toString(scope); } }
getBestRowIdentifier()の引数scopeは、いつまで使用可能な項目を取得したいかを指定する。
Transactionはトランザクション、つまりコミットまたはロールバックするまで。
Sessionはセッションなので、Connectionがクローズされるまでだと思われる。
実務的にはTransactionを指定すればいいと思われる。
SELECTした結果(ResultSet)のSCOPEも、同じ値が返ってくる。
public static String getPseudoString(short pseudo) { switch (pseudo) { case DatabaseMetaData.bestRowUnknown: return "bestRowUnknown"; case DatabaseMetaData.bestRowNotPseudo: return "bestRowNotPseudo"; case DatabaseMetaData.bestRowPseudo: return "bestRowPseudo"; default: return Short.toString(pseudo); } }
Pseudoは、擬似項目を意味する。Oracleでいう擬似列ROWIDのこと。JavaDBにはROWIDに相当する仕組みは無い(ような)ので、Pseudoが返ってくることは無い。
NotPseudoは、擬似項目でない、つまり実際にテーブルに存在する項目。
DBMSがレコードを一意に特定する為に暗黙の項目を保持している(つまりOracleならROWID)場合、それが返ってくる可能性がある。
なお、ROWIDはトランザクション内でしか使えないようなので、引数scopeにTransactionを指定した場合しか返って来ない。
引数scope | スキーマ=SCOTT テーブル=EMP |
説明 |
---|---|---|
bestRowSession |
[EMPNO, bestRowNotPseudo, bestRowSession] |
1種類だけ返って来ている。 EMPNOの項目だけ指定すればレコードを特定できる。 |
bestRowTransaction |
[ROWID, bestRowPseudo, bestRowTransaction] [EMPNO, bestRowNotPseudo, bestRowSession] |
2種類返って来ている。 ROWIDかEMPNOのいずれかを指定すればレコードを特定できる。 ROWIDはトランザクションの間だけ有効で、EMPNOはセッション中有効。 |
つまり、1レコード目のSCOPEの値をまず取得し、2レコード目以降はSCOPEが一致している項目だけを使う、というようなロジックが要るのだろう。
DatabaseMetaData#getColumns()やResultSetMetaDataの各メソッドで項目の属性を取得することが出来るが、取得できる内容は少々異なる。[2009-04-10]
DatabaseMetaDataはテーブルの定義、つまり静的な情報を取得するが、ResultSetMetaDataはSELECT文の実行結果に対する情報を取得する。したがって、COUNT(*)とか演算を行った項目に対しても情報が得られる。
DatabaseMetaDataはConnectionから取得するが、ResultSetMetaDataはexecuteQuery()等の実行結果のResultSetから取得する。
SELECT結果が0件だとしても、属性はちゃんと取れる。
Statement stat = conn.createStatement(); try { ResultSet rs = stat.executeQuery("select * from EMP where 1=0"); try { ResultSetMetaData rmd = rs.getMetaData(); for (int i = 1; i <= rmd.getColumnCount(); i++) { System.out.printf("%s\t%s\t%s%n", rmd.getColumnName(i), rmd.getColumnTypeName(i), rmd.getColumnClassName(i) ); } } finally { rs.close(); } } finally { stat.close(); }
取得できる内容 | DatabaseMetaData dmd = 〜; ResultSet rs = dmd.getColumns(); |
ResultSetMetaData rmd = 〜; | 取得される値の例 | 概要 | |||||
---|---|---|---|---|---|---|---|---|---|
テーブル名 | rs.getString("TABLE_NAME") |
rmd. getTableName (i) |
EMP | EMP | EMP | テーブル名 ResultSetMetaDataの方は、Oracle9iでは空文字列が返ってくる。 |
|||
項目名 | rs.getString("COLUMN_NAME") |
rmd. getColumnName(i) |
EMPNO | ENAME | HIREDATE | 項目名 | |||
項目別名 | - |
rmd. getColumnLabel (i) |
EMPNO | ENAME | HIREDATE | select empno as enoの場合、「ENO」が返る。 | |||
DBの型名 | rs.getString("TYPE_NAME") |
rmd. getColumnTypeName(i) |
NUMBER | VARCHAR2 | DATE | DB固有表記の型名 | |||
表示桁数 | - |
rmd. getColumnDisplaySize (i) |
22 | 10 | 7 | 最大文字数? | |||
項目サイズ | rs.getInt("COLUMN_SIZE") |
rmd. getPrecision (i) |
4 | 10 | 7 | 型の桁数 | |||
小数点以下の桁数 | rs.getObject("DECIMAL_DIGITS") |
rmd. getScale (i) |
0 | null | 0 | null | 0 | 小数点以下の桁数 数値型の場合のみ意味がある。 rs.getInt()で取得した場合は、nullの代わりに0が返る。 |
|
Javaの型名 | - |
rmd. getColumnClassName(i) |
java.math. BigDecimal |
java.lang. String |
java.sql. Timestamp |
ResultSet#getObject()で取得する際に使われるJavaクラス名 | |||
NULL可否 | rs.getString("IS_NULLABLE") |
rmd.isNullable(i) |
NO | 0 | YES | 1 | YES | 1 | NULL値を許容するかどうか |
自動採番有無 | rs.getString("IS_AUTOINCREMENT") |
rmd.isAutoIncrement(i) |
NO | false | NO | false | NO | false | 自動採番する項目かどうか DatabaseMetaDataの方は、Oracle9iでは例外が発生する。 |
Oracleは自動採番はシーケンスを使うのであって項目の属性ではないから、IS_AUTOINCREMENTが使えないのだろうか。
でも発生する例外は「java.sql.SQLException: 列名が無効です」。つまり定義されていない名前を使った場合と同じなのがちょっと気になるが…。
ResultSetMetaDataが実行結果のメタデータを扱うのに対し、ParameterMetaDataはPreparedStatementの実行前のメタデータを扱う。[2009-10-25]
PreparedStatementのパラメーター「?」に値をセットする場合、setObject
(n,
obj)
を使えば、objに入っている型に応じて自動的に値がセットされる。
しかしobjがnullの場合、setObject
(n,
null)
では(DBの種類によっては)エラーになってしまう。
nullをセットしたい場合はsetNull(n,
sqlType)
またはsetObject(n,
null, sqlType)
を使うのが正解だが、いずれにしてもSQL型(java.sql.Typesのint値)が必要になる。
何の型なのか別途分かっているならそれを指定してやればいいが、ParameterMetaDataを使えば、そのパラメーターのSQL型を取得することが出来る。
つーか、それでSQL型が取得できるなら、内部でそれを使って自動的に変換してくれりゃいーのに(苦笑)
PreparedStatement stat = conn.prepareStatement(sql); try { for (int i = 0; i < valueList.size(); i++) { Object obj = valueList.get(i); if (obj != null) { stat.setObject(i + 1, obj); } else { ParameterMetaData pmd = stat.getParameterMetaData(); int type = pmd.getParameterType(i + 1); stat.setNull(i + 1, type); } } int r = stat.executeUpdate(); return r; } finally { try { stat.close(); } catch (SQLException e) { e.printStackTrace(); } }
(メタデータとは違うかもしれないが、)INSERT時に自動的にインクリメントして採番される項目値(JavaDBで言う識別子列属性)を、実際にINSERTした後で取得することが出来る。
String sql = "insert into TEST (KEY1, DATA1) values (default, 'abc')"; Statement stat = conn.createStatement(); stat.execute(sql, Statement.RETURN_GENERATED_KEYS); ResultSet rs = stat.getGeneratedKeys(); try { rs.next(); BigDecimal id = rs.getBigDecimal(1); System.out.println(id); } finally { rs.close(); }
Statement#execute()の第2引数に、自動生成値を返す指定を付けるのがポイント。
(項目名や項目番号を指定するオーバーロードもあるのだが、JavaDBでは実装されていないようだ。JDK1.6.0_0だからバージョンが古いのかな?)
execute()でStatement.RETURN_GENERATED_KEYSを指定していない場合、getGeneratedKeys()はnullを返す。
参考: kameidさんのapache derbyでのシリアルID値の設定方法
Oracleでシーケンスを使って採番した場合、返されるのは採番された値ではなく、ROWIDとなる。[2010-07-24]
String sql = "insert into EMP (EMPNO, ENAME) values (TEST_SEQ.nextval, 'abc')"; Statement stat = conn.createStatement(); stat.execute(sql, Statement.RETURN_GENERATED_KEYS); ResultSet rs = stat.getGeneratedKeys(); try { rs.next(); RowId id = rs.getRowId(1); System.out.println(id); } finally { rs.close(); }
Oracleでシーケンスの値を取得したい場合は、INSERT文のRETURNING句を使う。
String sql = "begin\n" + "insert into EMP (EMPNO, ENAME) values (TEST_SEQ.nextval, 'abc')\n" + "returning EMPNO into ?;\n" + "end;"; CallableStatement stat = conn.prepareCall(sql); stat.registerOutParameter(1, Types.INTEGER); stat.execute(); int id = stat.getInt(1); System.out.println(id);
→CallableStatementでのOracleのRETURNINGの使用方法