以下の内容はhttps://aoe-tk.hatenablog.com/entry/2024/12/23/005425より取得しました。


Iceaxeを使ってTsurugiのSQLダンプを出力する

このエントリは Tsurugi Advent Calendar 2024 の22日目のエントリです。前日は septigram さんによる「 劔"Tsurugi"をZabbixで監視する 」でした。

以下の前回のエントリではTsurugiに用意されているダンプ、ロード方法を紹介しました。

aoe-tk.hatenablog.com

前回のエントリの最後に書いたのですが、他のRDBMSではSQL文によるダンプを行えるものがあります。 現時点でTsurugiにはそのような機能はありませんが、Iceaxeに用意されている機能をうまく活用することで、SQLファイルとしてデータをダンプすることができます。今回はその方法について案内します。

なお、今回説明するコードの全体はgistにアップしているので、そちらをご参照ください。

SQLファイルとしてTsurugi上のテーブルデータをダンプするコード · GitHub

このプログラムでは外部からDBのURL dbUrl 、テーブル名 tableName 、出力先ディレクトoutputDir を受け取ります。 まず、出力先ファイル名とそのパス、DB接続のための TsurugiConnector オブジェクト、データ取得SQLを準備します。

var fileName = "insert_" + tableName + ".sql";
var filePath = outputDir.resolve(fileName);
var connector = TsurugiConnector.of(dbUrl);
var sql = "select * from " + tableName;

続いてDBへのセッション確立とファイルオープンを try-with-resources 文のtry句の中で行います。

try (TsurugiSession session = connector.createSession();
        var writer = new PrintWriter(Files.newBufferedWriter(filePath, WRITE, CREATE, TRUNCATE_EXISTING))) {

以降は PrintWriter#println() メソッドを使って、出力先ファイルにSQLを書き込んで行くことになります。 ファイルの先頭にLTXでトランザクションを開始するSQLを記述します。

    writer.println("start long transaction write preserve " + tableName + ";");

Iceaxeの TsurugiSession#findMetadata() メソッド を使うとテーブルのメタデータ情報 TgTableMetadata を取得することができます。

    var metadata = session.findTableMetadata(tableName).orElseThrow();

このオブジェクトの getLowColumnList() メソッドでテーブルの全ての列のメタデータ情報 com.tsurugidb.sql.proto.SqlCommon.Column を取得できます。このオブジェクトに列の型情報や名称が含まれています。

    List<? extends SqlCommon.Column> columnList = metadata.getLowColumnList();
    var columnNameList = columnList.stream().map(SqlCommon.Column::getName).toList();

テーブル列のメタデータ情報が取得できたので、実際にデータを取得し、その結果を使ってinsert文を組み立てることにします。

    var tm = session.createTransactionManager(TgTxOption.ofRTX());
    var resultList = tm.executeAndGetList(sql);
    for (TsurugiResultEntity entity : resultList) {
        var builder = new StringBuilder("insert or replace into ");
        builder.append(tableName).append(" (").append(String.join(",", columnNameList)).append(") values (");
        var valuesString = columnList.stream().map(column -> getValueString(entity, column)).toList();
        builder.append(String.join(",", valuesString)).append(");");
        writer.println(builder);
    }

取得したレコードごとに insert or replace 文を組み立てます。

先に取得した列名称のリスト columnNameList を使って、 insert or replace into <テーブル名>value 句の間に記述する列名称を出力している点に留意してください。

        builder.append(tableName).append(" (").append(String.join(",", columnNameList)).append(") values (");

value 句の値の出力は次の部分で行っています。

        var valuesString = columnList.stream().map(column -> getValueString(entity, column)).toList();
        builder.append(String.join(",", valuesString)).append(");");

列情報のメタデータ columnList の情報を使って、列に対応した値を TsurugiResultEntity から取得して、値の文字列を出力しています。 具体的な処理は次の getValueString() メソッドの中で行っています。

private String getValueString(TsurugiResultEntity entity, SqlCommon.Column column) {
    var atomType = column.getAtomType();
    var columnName = column.getName();
    return switch (atomType) {
        case INT4 -> entity.findInt(columnName).map(value -> Integer.toString(value)).orElse("NULL");
        case INT8 -> entity.findLong(columnName).map(value -> Long.toString(value)).orElse("NULL");
        case FLOAT4 -> entity.findFloat(columnName).map(value -> Float.toString(value)).orElse("NULL");
        case FLOAT8 -> entity.findDouble(columnName).map(value -> Double.toString(value)).orElse("NULL");
        case DECIMAL -> entity.findDecimal(columnName).map(BigDecimal::toPlainString).orElse("NULL");
        case CHARACTER -> entity.findString(columnName).map(value -> "'" + value + "'").orElse("NULL");
        case DATE -> entity.findDate(columnName).map(value -> "date '" + value + "'").orElse("NULL");
        case TIME_POINT -> entity.findDateTime(columnName).map(value -> "timestamp '" + value + "'").orElse("NULL");
        case TIME_POINT_WITH_TIME_ZONE ->
                entity.findOffsetDateTime(columnName).map(value -> "timestamp with time zone '" + value + "'")
                        .orElse("NULL");
        default -> "NULL";
    };
}

SqlCommon.Column#getAtomType() メソッドで列の型情報を取得できます。 型情報を元に TsurugiResultEntity の適切なデータ取得呼び出しを行い、型に応じたリテラルを出力しているのが分かると思います *1

date 型や timestamp 型の場合は演算子を付与している点にも注意してください。また、TsurugiのSQLにおける日付、時刻リテラルはISO-8601のフォーマットに対応しているので、 toString() の結果をそのまま出力しても大丈夫です。

このようにして作成したプログラムで出力したSQLダンプは次のようになります (見やすいように適宜改行していますが、insert文は実際には一行です) 。

start long transaction write preserve dump_test;
insert or replace into dump_test (
  c_bigint,
  c_int,
  c_float,
  c_double,
  c_decimal,
  c_varchar,
  c_date,
  c_timestamp,
  c_timestamp_tz
)
values (
  100000000,
  100,
  100.001,
  100000.00001,
  20241010.111111,
  'test',
  date '2024-12-20',
  timestamp '2024-12-20T23:36:17.417184',
  timestamp with time zone '2024-12-20T14:37:35.324142Z'
);
commit;

出力したSQLファイルはそのままtgsqlを使って実行可能です。

$ tgsql -c tcp://localhost:12345 --script insert_dump_test.sql
[main] INFO com.tsurugidb.tgsql.core.TgsqlRunner - establishing connection: tcp://localhost:12345
[main] INFO com.tsurugidb.tgsql.core.TgsqlRunner - start processing script
[main] INFO com.tsurugidb.tgsql.core.executor.report.BasicReporter - transaction started. option=[
  type: LTX
  write_preserve: "dump_test"
]
[main] INFO com.tsurugidb.tgsql.core.executor.report.BasicReporter - (1 row merged)
[main] INFO com.tsurugidb.tgsql.core.executor.report.BasicReporter - transaction commit(DEFAULT) finished.
[main] INFO com.tsurugidb.tgsql.core.TgsqlRunner - script execution was successfully completed

一度プログラムとして作っておけば意外と色んな場面で使えると思います。テーブルではなく任意のSQLを渡せるようにしてもいいでしょう。是非参考にしてみてください。

*1:Javaに新しく入ったswitch式便利ですね




以上の内容はhttps://aoe-tk.hatenablog.com/entry/2024/12/23/005425より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14