S2JDBC-Gen でコメントの管理

S2JDBC-Gen は通常ではコメントの管理ができません。コメント大好きな私としては管理できるように改造してみました。

下準備

S2JDBC-Gen を最新のバージョンにします。Dolteng のバージョンがまだあがっていないので今週中に最新版の S2JDBC-Gen を使う場合には手で最新版に入れ替える必要があります。

S2JDBC-Gen は失敗するとDBを壊すので、DBのバックアップを事前に行いましょう。また最初は本物のプロジェクトではなく、DB管理だけのプロジェクトを新規作成したほうがいいと思います。

方針

s2jdbc-gen-0.9.2.jar は触らないで、外部から拡張します。
事前にコメントをsqlファイルに保存して、migrate しても常にコメントがついている状態にします。

S2JDBC-Gen の構造

Antの設定ファイルである s2jdbc-gen-build.xml の中にコマンドが登録されています。実際のコマンドは lib/s2jdbc-gen-0.9.2.jar の中にあります。

実際に登録されているクラスは jar を展開して s2jdbc-gen-task.properties の中に記述されています。

dump-data=org.seasar.extension.jdbc.gen.task.DumpDataTask
dump-db-meta=org.seasar.extension.jdbc.gen.task.DumpDbMetaTask
exec-sql=org.seasar.extension.jdbc.gen.task.ExecuteSqlTask
gen-condition=org.seasar.extension.jdbc.gen.task.GenerateConditionTask
gen-ddl=org.seasar.extension.jdbc.gen.task.GenerateDdlTask
gen-entity=org.seasar.extension.jdbc.gen.task.GenerateEntityTask
gen-names=org.seasar.extension.jdbc.gen.task.GenerateNamesTask
gen-service=org.seasar.extension.jdbc.gen.task.GenerateServiceTask
gen-test=org.seasar.extension.jdbc.gen.task.GenerateTestTask
load-data=org.seasar.extension.jdbc.gen.task.LoadDataTask
migrate=org.seasar.extension.jdbc.gen.task.MigrateTask

ベースになるコマンドを選択

今回はコメントのSQLを作成するのでgen-ddlをベースにしました。作ったあとからいうと少しこのクラスは大きいのでベースにはもう少しシンプルな方が良かったかも。。。

ファイルの作成

─src
  ├─main
  │  ├─java
  │  │  └─org
  │  │      └─seasar
  │  │          └─extension
  │  │              └─jdbc
  │  │                  └─gen
  │  │                      ├─internal
  │  │                      │  └─command
  │  │                      │          GenerateCommentCommand.java
  │  │                      │          
  │  │                      └─task
  │  │                              GenerateCommentTask.java

こんな感じの構造にしました。本当はsrc以下に置くのは良くないのでしょうが自動的にコンパイルするので。。。安定したら jar に固めて lib に突っ込むのがスマートですね。

内容

基本的には GenerateDdlTask などをコピーしてきて、クラス名などを調整します。Task はおそらく何も触らなくても大丈夫だと思います。GenerateCommentCommand の中で実際の処理をしていますので、こっちを触ります。

Command の構造

doInit() で初期化処理をします。主に各種クラスの作成になります。今回はここは触っていません。

doExecute() で実際の処理を行います。ここはまるまるすべて書き換えました。通常の gen-ddl だと新しいリビジョンに更新するためにコールバック登録などを行っていますのでそのまま使うとどんどんフォルダが増えていくので注意です(笑)

doDestroy() で後処理を行います。基本的にはなにもしていないみたいです。今回はantで一発実行なのでメモリーがたまっても終了すればきれいになりますしね!

doExecute の処理

// 書き出し
File dir = ddlVersionDirectoryTree.getCurrentVersionDirectory().getCreateDirectory().createChild( "050-user" ).asFile();
dir.mkdir();

File oFile = new File( dir, "comment.sql" );
FileOutputStream outFile = new FileOutputStream(oFile);
OutputStreamWriter out = new OutputStreamWriter(outFile, "UTF-8");

050-user ディレクトリに comment.sqlutf-8 で作成する準備をします。
S2JDBC-Genディレクトリの番号順に処理を行って行くので、最低でもcreate tableが完了している必要があります。

// データベースコネクション作成
Connection conn = DataSourceUtil.getConnection(jdbcManager.getDataSource());

// テーブルの一覧とコメントを取得
String selectSql = "SELECT tbl.table_name NAME , com.COMMENTS FROM user_tables tbl LEFT JOIN user_tab_comments com on (tbl.table_name = com.table_name ) ORDER BY NAME";
PreparedStatement ps = ConnectionUtil.prepareStatement(conn, selectSql);
ResultSet rs = PreparedStatementUtil.executeQuery(ps);
while (rs.next()) {
  String name = rs.getString("NAME");
  String comment = rs.getString("COMMENTS");
  String sql = "";

  if( comment == null || comment.equals("") ){
    // コメントが設定されていない場合コメントアウト、後で手で設定する!
    sql = "-- comment on table "+name+" is '';\n";
  } else {
    // コメントが設定されている場合
    sql = "comment on table "+name+" is '"+comment+"';\n";
  }

  // コメント付与SQL出力
  out.write(sql);

  // カラムの一覧とコメント取得
  String selectSql2 = " SELECT" +
                      "    T.column_name NAME," +
                      "    comments COMMENTS" +
                      " FROM" +
                      "    user_tab_columns T JOIN user_col_comments C ON ( T.table_name = C.table_name AND T.column_name = C.column_name )" +
                      " WHERE" +
                      "    T.table_name = UPPER( '" + name + "' )" +
                      " ORDER BY" +
                      "    T.column_id";
  PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, selectSql2);
  ResultSet rs2 = PreparedStatementUtil.executeQuery(ps2);
  while (rs2.next()) {
    String name2 = rs2.getString("NAME");
    String comment2 = rs2.getString("COMMENTS");
    String sql2 = "";

    if( comment2 == null || comment2.equals("") ){
      // コメントが設定されていない場合コメントアウト、後で手で設定する!
      sql2 = "-- comment on column "+name+"."+name2+" is '';\n";
    } else {
      // コメントが設定されている場合
      sql2 = "comment on column "+name+"."+name2+" is '"+comment2+"';\n";
    }

    // コメント付与SQL出力
    out.write(sql2);
  }

  // カラム用データ後片付け
  ResultSetUtil.close(rs2);
  StatementUtil.close(ps2);

  // テーブルの最後は改行を入れる
  out.write("\n");
}

// テーブル用データ後片付け
ResultSetUtil.close(rs);
StatementUtil.close(ps);
ConnectionUtil.close(conn);

// ファイルを閉じる
out.close();

実際には try とかあってたいそうなことになっていますが、読みやすいように編集してあります。

今回はオラクル用のコードですが、各DB用に書けば他のでも大丈夫です。ただこれは使い捨て用のコードなので中でDBの種類ごとにSQLを使い分けたりはしません。

これで実行するとcomment.sqlが吐き出されます。

ファイル例

comment on table ZIP is '郵便番号';
comment on column ZIP.NUM_SEQ is '郵便番号全体連番';
comment on column ZIP.CD_ZIP is '郵便番号';
comment on column ZIP.CD_PREF is '都道府県コード';
comment on column ZIP.NAM_PREF is '都道府県名';
comment on column ZIP.CD_TOWN is '市区町村コード';
comment on column ZIP.NAM_TOWN is '市区町村名';
comment on column ZIP.NAM_ST is '町域名';
comment on column ZIP.NAM_ST_KANA is '町域名カナ';

-- comment on table TEST is '';
-- comment on column TEST.ID is '';

こんな感じのSQLが吐き出されます。コメントアウトされている場所はコメントがついていないところなので、後で手でコメントを付与します。

すべてのテーブルについていないと意味ないですからね!

antへの組み込み

  <taskdef resource="s2jdbc-gen-task.properties" classpathref="classpath"/>
  <taskdef name="gen-comment" classname="org.seasar.extension.jdbc.gen.task.GenerateCommentTask" classpathref="classpath"/>

一行目が通常のコマンドを登録している場所です。その下で自分で作ったクラスを登録するコードを書きます。そんなに数は登録しないと思いますので必要数登録します。安定したところでjarに複数まとめて properties 化した方がスマートですね。

  <target name="gen-comment">
    <gen-comment
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
    />
  </target>

これはどこにおいても大丈夫かな。今回はなんとなく migrate の上におきましたけれど、実際には名前順で表示されるので、どこにおいても変わりません(笑)

ちなみにここだとコメントファイルだけを最新のディレクトリに吐き出すコマンドになります。この状態でテストを行い、実際には gen-ddl の中で動くようにします。

  <target name="gen-ddl">
    ...省略
    <gen-service
      ...省略
    />
    <gen-comment
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
    />
  </target>

gen-ddl の最後に追加すると ddl 作成の最後にコメントファイルも同時に作ってくれます
これでかなり実用的になりました。

ファイル生成例

├─db
│  │  ddl-info.txt
│  │  
│  └─migrate
│      ├─0000
│      │  └─drop
│      │      └─040-table
│      │              zip.sql
│      │              test.sql
│      │              
│      └─0001
│          ├─create
│          │  ├─010-table
│          │  │      zip.sql
│          │  │      test.sql
│          │  │      
│          │  ├─040-dump
│          │  │      zip.csv
│          │  │      test.csv
│          │  │      
│          │  └─050-user
│          │          comment.sql
│          │          
│          └─drop
│              └─040-table
│                      zip.sql
│                      test.sql

こんな感じになります。migrate 時には comment.sql も自動的に実行されますので特に意識する必要はありません。

実際のコード

    protected void doExecute() throws Throwable {
        File dir = ddlVersionDirectoryTree.getCurrentVersionDirectory().getCreateDirectory().createChild("050-user").asFile();
        dir.mkdir();

        File oFile = new File( dir, "comment.sql" );
        FileOutputStream outFile = new FileOutputStream(oFile);
        OutputStreamWriter out = new OutputStreamWriter(outFile, "UTF-8");

        String selectSql = "SELECT tbl.table_name NAME , com.COMMENTS FROM user_tables tbl LEFT JOIN user_tab_comments com on (tbl.table_name = com.table_name ) ORDER BY NAME";

        Connection conn = DataSourceUtil.getConnection(jdbcManager.getDataSource());
        try {
            logger.debug(selectSql);
            PreparedStatement ps = ConnectionUtil.prepareStatement(conn,
                    selectSql);
            try {
                ResultSet rs = PreparedStatementUtil.executeQuery(ps);
                try {
                    while (rs.next()) {
                        String name = rs.getString("NAME");
                        String comment = rs.getString("COMMENTS");
                        String sql = "";
                        if( comment == null || comment.equals("") ){
                            sql = "-- comment on table "+name+" is '';\n";
                        } else {
                            sql = "comment on table "+name+" is '"+comment+"';\n";
                        }
                        out.write(sql);

                        String selectSql2 = " SELECT" +
                                            "    T.column_name NAME," +
                                            "    comments COMMENTS" +
                                            " FROM" +
                                            "    user_tab_columns T JOIN user_col_comments C ON ( T.table_name = C.table_name AND T.column_name = C.column_name )" +
                                            " WHERE" +
                                            "    T.table_name = UPPER( '" + name + "' )" +
                                            " ORDER BY" +
                                            "    T.column_id";
                        try {
                            logger.debug(selectSql2);
                            PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, selectSql2);
                            try {
                                ResultSet rs2 = PreparedStatementUtil.executeQuery(ps2);
                                try {
                                    while (rs2.next()) {
                                        String name2 = rs2.getString("NAME");
                                        String comment2 = rs2.getString("COMMENTS");
                                        String sql2 = "";
                                        if( comment2 == null || comment2.equals("") ){
                                            sql2 = "-- comment on column "+name+"."+name2+" is '';\n";
                                        } else {
                                            sql2 = "comment on column "+name+"."+name2+" is '"+comment2+"';\n";
                                        }
                                        out.write(sql2);
                                    }
                                } catch (SQLException e) {
                                    throw new SQLRuntimeException(e);
                                } finally {
                                    ResultSetUtil.close(rs2);
                                }
                            } finally {
                                StatementUtil.close(ps2);
                            }
                        } finally {
                        }
                        out.write("\n");
                    }
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                } finally {
                    ResultSetUtil.close(rs);
                }
            } finally {
                StatementUtil.close(ps);
            }
        } finally {
            ConnectionUtil.close(conn);
        }

        out.close();
    }

たぶんこれで動くはず。。。中身みてわかると思いますが結構大変なことになっています(笑)

感想

antの設定ファイルを書くのも始めての状況だったので、結構面倒だったかな?
ただ結構きれいに拡張できそうなので、今後もプラグインみたいに気軽に追加できますね。エクセルに定義書吐き出しもJavaで作り直そうかな。。。

今はなぜかPHPでその辺のツール作ったので、PHPから定義書吐き出しています(笑)

あと DATA_1 などのカラムが DATA1 になっちゃうのよね〜
これは仕方ないか。。。name とかで指定すれば DATA_1 になるかな? でもたぶんカラム名を修正します(笑)

あとデータのdumpとloadは標準コマンドでも分離されていた方がいい気がしました。データだけ出力したり、元に戻したりって結構実開発ではあると思うんですよね〜。

中身を切り出してきてもいいけれど、本家で分離してそれだけで実行できるようにしてくれるとうれしいです!
すでに分離されていました! ドキュメント読めオレ! http://s2container.seasar.org/2.4/ja/s2jdbc_gen/task.html