テーブルおよびカタログ情報にアクセスする
データベースをコピーするには、データベースに関するカタログ情報を取得する必要があります。具体的には、データベースのテーブルのリストと、各テーブル内のすべての列のリストが必要になります。この情報に基づいて、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(""); }
最初に述べたように、このサンプルはデータベース間でデータをコピーする簡単なユーティリティですが、私はこれを、より大きなデータベースユーティリティを作成するための土台としてよく利用しています。このユーティリティには、もっと複雑なデータベースコピーアプリケーションを作成するために必要な多くの関数が含まれていることがわかるでしょう。