S2CSVのEntityをS2JDBC-Genで作ってみる

どんどん自分でも方向性が見えなくなってきましたが、少し気になっているS2CSVを触ってみました!

毎度のことですが実験レベルなので、実運用にはそのままだと適さないと思いますのでご注意ください。毎回数時間で終わるように時間重視で検証していますので!(逃げ口上)

http://s2csv.sandbox.seasar.org/

構成

s2csvのサンプルなどをみてプロジェクトで利用できるようにします。サンプルはどうもWebインターフェースではなくmainがあるコマンド起動を前提としたもののようです。。。

たしかにバッチ系での用途もあると思うけれど、Webインターフェースからアップロードとかを想定しているので、Webインターフェースありのプロジェクトにしたいと思います。

サンプルの内部ではS2JDBCを利用しているのでSAStrutsで作ります。実はSAStrutsS2JDBC触るの初めて(笑)

サンプルを動作させる

サンプルやチュートリアルをみてS2CSVを組み込みます。mainで動かしている部分はindexにコピーしてきます。サービスなどは既存のプロジェクトと名前がかぶるのでDeptCsvServiceみたいな感じに最後にCsvをつけています。もろもろサンプルからファイルをコピーしてきます。

public class IndexAction {
    public DeptCsvService deptCsvService;
    
    //※ファイルが出力されます気をつけてください!
    public static String outPutFileName = "/temp/csv_out_dept"; // c:\tempに変更
    public static String outReadFileName = "/temp/csv_read_dept"; // c:\tempに変更
    public static String csvExt = ".csv";
    
    @Execute(validator = false)
    public String index() {
        try {
            this.out1(); //CSV出力処理1
            this.out2(); //CSV出力処理2
            this.in1(); //CSV取込処理1
            this.in2(); //CSV取込処理2
            this.in3(); //CSV取込処理3
            this.in4(); //CSV取込処理4
        } catch (Exception e) {
            System.out.println("例外発生!");
        }
        return "index.jsp";
    }

    public void out1() throws Exception {
        deptCsvService.outCsv1(outPutFileName + "1" + csvExt);
        System.out.println("[csv1]出力完了");
    }
    public void out2() throws Exception {
        deptCsvService.outCsv2(outPutFileName + "2" + csvExt);
        System.out.println("[csv2]出力完了");
    }
    public void in1() throws Exception {
        deptCsvService.inCsv1(outReadFileName + "1" + csvExt);
        System.out.println("[csv1]取込完了");
    }
    public void in2() throws Exception {
        deptCsvService.inCsv2(outReadFileName + "2" + csvExt);
        System.out.println("[csv2]取込完了");
    }
    public void in3() throws Exception {
        deptCsvService.inCsv3(outReadFileName + "3" + csvExt);
        System.out.println("[csv3]取込完了");
    }
    public void in4() throws Exception {
        deptCsvService.inCsv4(outReadFileName + "4" + csvExt);
        System.out.println("[csv4]取込完了");
    }
}

とりあえずサンプルほぼそのままです、アクセスするといろいろ動くはずです。ただしコマンドラインの起動ではないのでpathがプロジェクトのルートにCSVを置いても読めません。とりあえずc:\tempに置いてみましたので、pathを変更しています。

実行検証

c:\tempにcsv_out_dept1.csvcsv_out_dept2.csvが作成されていたら成功です。ここにくるまでにDBに接続できないって言われてみたら設定がH2じゃなくてOracleだったりといろいろありましたが、割愛させていただきます!

ファイル追加

org
└─seasar
    └─extension
        └─jdbc
            └─gen
                ├─internal
                │  ├─command
                │  │      GenerateCsvEntityCommand.java
                │  ├─generator
                │  │  └─tempaltes
                │  │      └─java
                │  │              csventity.ftl
                │  └─model
                │          CsvEntityModelFactoryImpl.java
                └─task
                        GenerateCsvEntityTask.java

上記のファイルを追加しました。

GenerateCsvEntityTask

タスクファイルです。GenerateEntityTaskをコピーして、ほぼそのまま使っています。継承した方がいいのでしょうが、継承した場合antで設定したパラメーターなどが渡されないみたいでしたので、そのままコピーして必要なところにCSVをつけてます。

GenerateCsvEntityCommand

コマンドファイルです。GenerateEntityCommandをコピーして、ほぼそのまま使っています。

以下変更点です

テンプレート名設定
    /** エンティティクラスのテンプレート名 */
    protected String entityTemplateFileName = "java/csventity.ftl";

よく見たらantからパラメーター渡せますね。。。今回は直書きでやってしまいました(汗)
パッケージ名はなぜかant経由で渡しています(汗)

モデルクラス登録
    /**
     * {@link EntityModelFactory}の実装を作成します。
     * 
     * @return {@link EntityModelFactory}の実装
     */
    protected EntityModelFactory createEntityModelFactory() {
        Class<?> superClass = enititySuperclassName != null ? ClassUtil
                .forName(enititySuperclassName) : null;

//      return factory.createEntityModelFactory(this, ClassUtil.concatName(
//              rootPackageName, entityPackageName), superClass, useAccessor,
//              showCatalogName, showSchemaName, showTableName, showColumnName,
//              showColumnDefinition, showJoinColumn, jdbcManager
//                      .getPersistenceConvention());

        return new CsvEntityModelFactoryImpl(ClassUtil.concatName(rootPackageName, entityPackageName),
            superClass,
            new AttributeModelFactoryImpl(showColumnName,
            showColumnDefinition, jdbcManager.getPersistenceConvention()),
            new AssociationModelFactoryImpl(showJoinColumn),
            new CompositeUniqueConstraintModelFactoryImpl(), useAccessor,
            showCatalogName, showSchemaName, showTableName);
    }

Entity作成時の各種設定をしているModelを作成しています。本当はコメントアウトされている場所のようにfactory経由で作るはずですが、factoryはすべてのコマンドに影響を与えるクラスなので、バイパスして直接対象クラスを作成しています。

コンテンツ作成関数編集
    /**
     * {@link GenerationContext}の実装を作成します。
     * 
     * @param model
     *            モデル
     * @param templateName
     *            テンプレート名
     * @return {@link GenerationContext}の実装
     */
    protected GenerationContext createGenerationContext(ClassModel model,
            String templateName) {
        File file = FileUtil.createJavaFile(javaFileDestDir, model
                .getPackageName(), model.getShortClassName()+"Csv");
        return factory.createGenerationContext(this, model, file, templateName,
                javaFileEncoding, overwrite);
    }

えーっと。。。クラス名の後にCsvを追加しています(汗)

CsvEntityModelFactoryImplの編集

EntityModelFactoryを継承して作っています。

    /**
     * インポート名を処理します。
     * 
     * @param model
     *            エンティティクラスのモデル
     * @param entityDesc
     *            エンティティ記述
     */
    protected void doImportName(EntityModel model, EntityDesc entityDesc) {
        classModelSupport.addImportName(model, CSVEntity.class);
        classModelSupport.addImportName(model, CSVColumn.class);
        if (superclass != null) {
            classModelSupport.addImportName(model, superclass);
        }
        for (AttributeModel attr : model.getAttributeModelList()) {
            if (attr.isId()) {
              classModelSupport.addImportName(model, CSVRequired.class);
            }
            classModelSupport.addImportName(model, attr.getAttributeClass());
        }
    }

テンプレートにつけるimport文を設定する関数です。CSVEntityとCSVColumnは絶対に生成するようにして、idがついている場所は必須にしています。最後のgetAttributeClass()でdateなどの変数の型をimportしています。

csventity.ftlの編集

entity.ftlをコピーして作りました。

<#include "/copyright.ftl">
<#if packageName??>
package ${packageName};
</#if>

<#list importNameSet as importName>
import ${importName};
</#list>

/**
 * ${shortClassName}Csvエンティティクラスです。
 * 
 * @author S2JDBC-Gen
 */
@CSVEntity(header=false)
<#if catalogName?? || schemaName?? || tableName?? || compositeUniqueConstraintModelList?size gt 0>
@Table(<#if catalogName??>catalog = "${catalogName}"</#if><#if schemaName??><#if catalogName??>, </#if>schema = "${schemaName}"</#if><#if tableName??><#if catalogName?? || schemaName??>, </#if>name = "${tableName}"</#if><#if compositeUniqueConstraintModelList?size gt 0><#if catalogName?? || schemaName?? || tableName??>, </#if>uniqueConstraints = { <#list compositeUniqueConstraintModelList as uniqueConstraint>@UniqueConstraint(columnNames = { <#list uniqueConstraint.columnNameList as columnName>"${columnName}"<#if columnName_has_next>, </#if></#list> })<#if uniqueConstraint_has_next>, </#if></#list> }</#if>)
</#if>
public class ${shortClassName}Csv<#if shortSuperclassName??> extends ${shortSuperclassName}</#if> {
<#list attributeModelList as attr>

  <#if attr.unsupportedColumnType>
    /**
     * FIXME このプロパティに対応するカラムの型(${attr.columnTypeName})はサポート対象外です。
     */
  <#else>
    /** ${attr.name}プロパティ */
  </#if>
  <#if attr.id>
    @CSVRequired
  </#if>
    @CSVColumn(columnIndex=${attr_index})
    <#if useAccessor>private<#else>public</#if> ${attr.attributeClass.simpleName} ${attr.name};
</#list>
<#list associationModelList as asso>

    /** ${asso.name}関連プロパティ */
    @${asso.associationType.annotation.simpleName}<#if asso.mappedBy??>(mappedBy = "${asso.mappedBy}")</#if>
  <#if asso.joinColumnModel??>
    @JoinColumn(name = "${asso.joinColumnModel.name}", referencedColumnName = "${asso.joinColumnModel.referencedColumnName}")
  <#elseif asso.joinColumnsModel??>
    @JoinColumns( {
    <#list asso.joinColumnsModel.joinColumnModelList as joinColumnModel>
        @JoinColumn(name = "${joinColumnModel.name}", referencedColumnName = "${joinColumnModel.referencedColumnName}")<#if joinColumnModel_has_next>,<#else> })</#if>
    </#list>
  </#if>
    <#if useAccessor>private<#else>public</#if> ${asso.shortClassName} ${asso.name};
</#list>
<#if useAccessor>
  <#list attributeModelList as attr>

    /**
     * ${attr.name}を返します。
     * 
     * @param ${attr.name}
     */
    public ${attr.attributeClass.simpleName} <#if attr.attributeClass.getSimpleName()?matches("[bB]oolean")>is<#else>get</#if>${attr.name?cap_first}() {
        return ${attr.name};
    }

    /**
     * ${attr.name}を設定します。
     * 
     * @param ${attr.name}
     */
    public void set${attr.name?cap_first}(${attr.attributeClass.simpleName} ${attr.name}) {
        this.${attr.name} = ${attr.name};
    }
  </#list>
  <#list associationModelList as asso>

    /**
     * ${asso.name}を返します。
     * 
     * @param ${asso.name}
     */
    public ${asso.shortClassName} get${asso.name?cap_first}() {
        return ${asso.name};
    }

    /**
     * ${asso.name}を設定します。
     * 
     * @param ${asso.name}
     */
    public void set${asso.name?cap_first}(${asso.shortClassName} ${asso.name}) {
        this.${asso.name} = ${asso.name};
    }
  </#list>
</#if>
}

変更点は

  • クラス名にCsvを強制的につけています(汗)
  • @CSVEntity(header=false)をすべてのカラムに追加
  • @CSVColumn(columnIndex=${attr_index})でカラムインデックス追加
  • @CSVRequiredをidがついていたら追加

publicのとき以外や、細かい制約がついた場合などはテストしていませんので、テンプレートはもう少し検証が必要だと思います。

s2jdbc-gen-build.xmlの編集

  <property name="csventitypackagename" value="csv"/>

  <taskdef name="gen-csv-entity" classname="org.seasar.extension.jdbc.gen.task.GenerateCsvEntityTask" classpathref="classpath"/>

  <target name="gen-csv-entity">
    <gen-csv-entity
      rootpackagename="${rootpackagename}"
      entitypackagename="${csventitypackagename}"
      javafiledestdir="${javafiledestdir}"
      javafileencoding="${javafileencoding}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
    />
  </target>

登録するパッケージ名の設定、クラスの登録、実際のタスクの登録を行っています。
ここだけcsventitypackagenameを利用しています。。。

生成物

package jdbc.s2csv;

import org.seasar.s2csv.csv.annotation.column.CSVColumn;
import org.seasar.s2csv.csv.annotation.column.CSVRequired;
import org.seasar.s2csv.csv.annotation.entity.CSVEntity;

/**
 * DeptCsvエンティティクラスです。
 * 
 * @author S2JDBC-Gen
 */
@CSVEntity(header=false)
public class DeptCsv {

    /** idプロパティ */
    @CSVRequired
    @CSVColumn(columnIndex=0)
    public Long id;

    /** deptNoプロパティ */
    @CSVColumn(columnIndex=1)
    public Integer deptNo;

    /** deptNameプロパティ */
    @CSVColumn(columnIndex=2)
    public String deptName;

    /** locプロパティ */
    @CSVColumn(columnIndex=3)
    public String loc;

    /** versionNoプロパティ */
    @CSVColumn(columnIndex=4)
    public Integer versionNo;
}

こんな感じのファイルが生成されます。ちなみに実験のために標準的なEntityを比べるために違うパッケージに作成しています。

ちなみにサンプルのEntityは

package jdbc.csv;

import org.seasar.s2csv.csv.annotation.column.CSVColumn;
import org.seasar.s2csv.csv.annotation.column.CSVRequired;
import org.seasar.s2csv.csv.annotation.entity.CSVEntity;

@CSVEntity(header=false)
public class DeptCsv {

	@CSVColumn(columnIndex=0)
	public Long id;
	
	@CSVRequired
	@CSVColumn(columnIndex=1)
	public Integer deptNo;

	@CSVColumn(columnIndex=2)
	public String deptName;

	@CSVColumn(columnIndex=3)
	public String loc;

	@CSVColumn(columnIndex=4)
	public Integer versionNo;
}

こんな感じです。

感想

サンプルを動かすために初めてSAStrutsS2JDBCを触りました! なんとなく雰囲気はわかりましたが、実際に組んでみるのはひがさんの本を読んでからかな。。。

S2CSVは実際ほとんど理解していません。読み込みとかでSystem.out.printlnなどでデータを出力すればまだわかると思うのですが、実行するとデータベースからデータ取ってきて、CSVに書き出すってタイプの処理なので、直接目では確認しにくいのですよね〜。

Webインターフェースで、ページの次へを押すたびに読み込んだり状態を表示したり目に見えるようなサンプルだとうれしいなと感じました。

SAStrutsのサンプルから動かしていたので、タイムスタンプみてここまで4時間ぐらいもかかってしまった。。。と思ったら作業は3時間でこの文章書くのに1時間かかっているんだ(汗)