テーブルおよびカタログ情報にアクセスする
データベースをコピーするには、データベースに関するカタログ情報を取得する必要があります。具体的には、データベースのテーブルのリストと、各テーブル内のすべての列のリストが必要になります。この情報に基づいて、DataMoverはデータのコピーに必要なSQL文を作成します。
Databaseクラスには、データベースのすべてのテーブルのリストを返すlistTablesというメソッドがあります。JDBCでテーブルのリストを取得する方法はいくつかあります。多くのデータベースには、テーブルのリストを取得するためのストアドプロシージャやシステムテーブルが用意されています。しかし、ストアドプロシージャやシステムテーブルの名前はデータベースごとに異なるため、テーブルのリストを取得する場合はJavaクラスのDatabaseMetaDataを使うのが最も互換性の高い方法になります。このクラスは、テーブルの名前など、データベースに関する情報を提供します。
listTablesメソッドは、まずテーブルリストを格納するコレクションを作成します。
Collection<String> result = new ArrayList<String>();
次に、テーブルのリストを保持するResultSetを作成し、DatabaseMetaDataオブジェクトを取得します。
ResultSet rs = null; try { DatabaseMetaData dbm = connection.getMetaData();
DatabaseMetaDataクラスにはたくさんのプロパティとメソッドがありますが、この例ではテーブルを取得するだけなので、getTablesメソッドを使います。このメソッドは3つの引数をとります。catalog文字列とschemaPattern文字列(この例ではどちらもnull)、および取得するテーブルの種類を示す文字列配列です。次の例では、"TABLE"という1つの要素を含む文字列配列を作成し、この配列を3つ目の引数としてgetTablesメソッドを呼び出しています。
String types[] = { "TABLE" };
rs = dbm.getTables(null, null, "", types);
この場合のgetTablesメソッドは、すべてのテーブルのリストが含まれるResultSetを返します。後は、結果に対して繰り返し処理を行い、各テーブル名をresultコレクションに追加するだけです。
while (rs.next()) { String str = rs.getString("TABLE_NAME"); result.add(str); } } catch (SQLException e) { throw (new DatabaseException(e)); }
finallyブロックを用意して、ResultSetが正しく閉じられるようにします。
finally { if( rs!=null ) { try { rs.close(); } catch (SQLException e) { } } }
最後に、getTablesメソッドはテーブルのリストを返します。
return result;
テーブルのリストを取得した後は、それらを処理してテーブル内の列のリストを取得します。それにより、適切なCREATE TABLE文に加えて、現在のテーブルに適合するINSERT文とSELECT文も作成できるようになります。テーブルの列のリストを取得するために、DatabaseクラスにはlistColumnsメソッドが含まれています。このメソッドの処理はテーブルリストの取得と似ているので、詳しくは説明しません。
データベースカタログ情報(テーブルと列)を取得したら、必要なSQL文を生成することができます。
CREATE TABLE文を生成する
データベースカタログ情報からCREATE TABLE文を生成するルーチンは、さまざまな目的に利用できます。DatabaseクラスのgenerateCreateメソッドは、まずCREATE TABLE文を格納するためのStringBufferを作成します。
StringBuffer result = new StringBuffer();
次に、SELECT * FROM [table]という文を作成して実行し、得られた結果からテーブル構造のメタデータを取得します。
try { StringBuffer sql = new StringBuffer(); sql.append("SELECT * FROM "); sql.append(table); ResultSet rs = executeQuery(sql.toString()); ResultSetMetaData md = rs.getMetaData();
先にCREATE TABLEの部分を作成します。列は後から埋めることができます。
result.append("CREATE TABLE "); result.append(table); result.append(" ( ");
すべての列に対してループ処理を行い、列の名前をCREATE TABLE文に追加します。
for (int i = 1; i <= md.getColumnCount(); i++) {
CREATE TABLE文の列はコンマ区切りなので、最初の列を除き、各列名の後ろにコンマを追加する必要があります。
if (i != 1) result.append(','); result.append(md.getColumnName(i)); result.append(' ');
列の型を取得し、それを文の後ろに追加します。
String type = processType(md.getColumnTypeName(i), md.getPrecision(i)); result.append(type);
型の後に精度を指定する必要があります。精度が65535を超える場合は、BLOB(Binary Large Object)またはText型なので精度は不要です。そうでない場合は、精度と桁数を指定します。
if (md.getPrecision(i) < 65535) { result.append('('); result.append(md.getPrecision(i)); if (md.getScale(i) > 0) { result.append(','); result.append(md.getScale(i)); } result.append(") "); } else result.append(' ');
型が数値で、種類が符号なしの場合は、UNSIGNED句を指定します。
if (this.isNumeric(md.getColumnType(i))) { if (!md.isSigned(i)) result.append("UNSIGNED "); }
また、データ型がNULL値を受け付けるかどうかも指定します。
if (md.isNullable(i) == ResultSetMetaData.columnNoNulls) result.append("NOT NULL "); else result.append("NULL "); if (md.isAutoIncrement(i)) result.append(" auto_increment"); }
さらに、主キーを指定する必要があります。これはDatabaseMetaDataクラスを使って取得できます。主キーの列には、それを示す文字列を指定します。
DatabaseMetaData dbm = connection.getMetaData(); ResultSet primary = dbm.getPrimaryKeys( null, null, table); boolean first = true; while (primary.next()) { if (first) { first = false; result.append(','); result.append("PRIMARY KEY("); } else result.append(","); result.append(primary.getString ("COLUMN_NAME")); } if (!first) result.append(')');
終わりのかっこを付けてCREATE TABLE文は完成です。
result.append(" ); ");
}
エラーが発生した場合は、DataMoverがDatabaseExceptionをスローします。
catch (SQLException e) { throw (new DatabaseException(e)); }
最後に、generateCreateメソッドは完成したCREATE TABLE文を文字列として返します。
return result.toString();
CREATE TABLE文を実行する前に、既存のコピー先データベースのテーブルを削除する必要があります。さもないと、generateCreateメソッドはエラーをスローします。Databaseクラスには、指定されたテーブル名に基づいてDROP TABLE文を生成するgenerateDropメソッドが含まれています。データをコピーする
コピー先データベースにテーブルを作成したら、そこにデータをコピーします。それには、SELECT文とINSERT文を作成する必要があります。SELECT文はコピー元データベースからテーブルのデータを読み取ります。INSERT文はコピー先データベースにデータを書き込みます。
まず、SELECT文、INSERT文、およびINSERT文のVALUES句を格納するための3つのStringBufferのインスタンスを作成します。
StringBuffer selectSQL = new StringBuffer(); StringBuffer insertSQL = new StringBuffer(); StringBuffer values = new StringBuffer();
DataMoverは列を取得し、それぞれの状態を表示します。
Collection<String> columns =
source.getColumns(table);
System.out.println("Begin copy: " + table);
次に、SELECT文とINSERT文の先頭部分を作成します。
selectSQL.append("SELECT "); insertSQL.append("INSERT INTO "); insertSQL.append(table); insertSQL.append("(");
さらに、すべての列に対してループ処理を行い、SELECT文とINSERT文を作成します。最初の列名を除くすべての列名の後ろにコンマを挿入します。
boolean first = true; for (String column : columns) { if (!first) { selectSQL.append(","); insertSQL.append(","); values.append(","); } else first = false;
各文に列名を追加し、VALUES句に疑問符(?)を追加します。疑問符が必要なのは、パラメータ化されたSQLを使用しているからです。この部分を後から実際の値に置き換えます。文をこのように作成すると、データベースでINSERT文をプリコンパイルして時間を節約することができます。
selectSQL.append(column);
insertSQL.append(column);
values.append("?");
}
次に、FROM句とVALUES句を追加します。
selectSQL.append(" FROM "); selectSQL.append(table); insertSQL.append(") VALUES ("); insertSQL.append(values); insertSQL.append(")");
ここまででSELECT文とINSERT文が完成し、SELECT文を実行してレコードのリストを取得できます。
// now copy PreparedStatement statement = null; ResultSet rs = null; try { statement = target.prepareStatement( insertSQL.toString()); rs = source.executeQuery(selectSQL.toString());
サンプルでは、状態レポート用に行数をカウントしています。
int rows = 0;
すべてのレコードに対してループ処理を行い、INSERT文を実行します。次に進むごとに値を増分します。
while (rs.next())
{
rows++;
それぞれのINSERT文に、個々の列データをコピーします。
for (int i = 1; i <= columns.size(); i++) { statement.setString(i, rs.getString(i)); }
その後、INSERT文を実行します。
statement.execute(); }
最後に、状態情報を表示して終了します。
System.out.println("Copied " + rows + " rows."); System.out.println(""); }
最初に述べたように、このサンプルはデータベース間でデータをコピーする簡単なユーティリティですが、私はこれを、より大きなデータベースユーティリティを作成するための土台としてよく利用しています。このユーティリティには、もっと複雑なデータベースコピーアプリケーションを作成するために必要な多くの関数が含まれていることがわかるでしょう。
