Hatena::ブログ(Diary)

脱サラ大学生のプログラム日記 RSSフィード

2016-09-26

Seasar Conference 2016 Finalに行ってきた

| 10:45

行ってきました!

あいにくの雨でしたが、久しぶりのConference。

気がつけばはてなも何年放置していたんだろう?

SEASAR PROJECTふりかえり

ひがさんがこれまでのプロジェクトを振り返って、当時どうおもって作っていたのかを話してくれました。

SeasarからS2Daoときて、失敗したといっていたS2JSFTeeda、、、

そしてS2Strutsを採用しようとしたプロジェクト用にもっと使いやすい仕組みとして作ったSAStrutsS2JDBC

Slim3の話も聞きたかったな

Teedaは失敗だった?

ひがさん的には失敗だったみたいです。ただJSFって規格に乗っかったのが原因かな?

個人的にはテンプレート部分がすごく好きです。

テンプレートのHTMLがブラウザでそのまま確認できて、組み込みやすいと思うんですよね。

ただ、JSFの影響がいろいろでてきている、、、

DOCTYPEがちょっと変だとエラーになったり、出力されるIDなどがJSF規格なので、JavaScriptと相性が悪かったりといろいろありました。

あとはSeasar全般でチュートリアルプログラムの中をみると使い方がわかるけれど、逆引きとかだとどうデータをいれていいのかわからなかったり、セッションとかの使い方がわかりにくかったりとかして、みんな同じところでハマっていた気がします。

とはいえ、テンプレートエンジンとしては好きです

Mayaaとかはあまり触ったことないですが、やっぱりTeedaの方が直感的かなって思います

ためにしJQueryでYAML形式かJSON形式のデータを読み込んでTeedaテンプレートをブラウザだけでどのように動くのか確認するもの作ってみましたが便利でした。

実際には使っていなくて、テストだけで終わりましたが、今度なにかで使えたらいいな

SEASARのフォークの仕方、LASTAFLUTE

まだ、フォークしていないです。ごめんなさい

そして、たぶんフォークしません

そういえばSeasarをビルドするのに昔にJava4をいれたなと思い出しました。

最近その環境消した気もします、、、

Java8化したときには結構いろいろ手をいれないといけないみたいですね。

SASTRUTS + S2JDBC から SKINNY FRAMEWORK への移行ガイド

Scalaもたのしそうですね

Javaやっていた人はGoかScalaに移動していった感じなのかな?

ここで次の予定があったので、離脱

久しぶりのカンファレンスは非常に楽しかったです!

2011-03-22

Teedaネスト対応 途中経過

| 00:17

コミッタにならさせていただきました!

これから頑張ります

いろいろ考えてはいるのですが、ダイナミックプロパティとかに対応しないとダメですね。完全に想定から抜けていました。

利用するための条件

デフォルトは利用しないにしたいと思っています。やっぱり後方互換性がなくなるのと、処理的に遅くなる可能性があるからです。大抵のプロジェクトは利用しても問題はでないとは思っています。

設定自体は teedaCustomize.dicon を利用したいかなと思っています。

ここはDoltengに入るファイルにも影響でますね

利用方法

まだ悩み中ですが_で区切った名前があったらメンバーのプロパティーを調べるのがいいかなと思っています。-とかの文字がいいんだけれど、それ使うと既存の処理にかむので手を入れる範囲が広がりそうですし、処理的に遅くなりそうです。

暫定影響範囲

  • 利用しない場合でも若干チェックロジックが遅くなる
  • 設定ファイル変更(teedaCustomize.dicon)
  • テスト追加
  • マニュアル追記(ここ結構重要)
  • ダイナミックプロパティ対応(getAaaBbbStyle形式?)
  • Doltengの初期設定ファイル変更
  • Doltengのマーカー対応(_が入ったものは一番先頭のプロパティーに関連付け)
  • Doltengの自動生成対応(_が入ったものは自動生成対象にしない?)

んー、まだ抜けているきがしますが走り始めてから考えます。ブランチで作業していたら、一回全部消してから組みなおしてもいいですしね!

2011-02-23

Teedaネスト対応

| 01:15

Teedaでネストしたプロパティー表示の実験

これで実験してから一年半ぐらい経過していますが、組んでみました。

	data3Dto = new Data3Dto();
	data3Dto.data5 = "DataText5";
	data3Dto.data6 = "DataText6";
	data3Dto.data2Dto = new Data2Dto();
	data3Dto.data2Dto.data3 = "DataText3";
	data3Dto.data2Dto.data4 = "DataText4";
	data3Dto.data2Dto.dataDto = new DataDto();
	data3Dto.data2Dto.dataDto.data1 = "DataText1";
	data3Dto.data2Dto.dataDto.data2 = "DataText2";

こんな感じのデータが

<span id="data3Dto_data5">data3Dto_data5</span><br />
<span id="data3Dto_data6">data3Dto_data6</span><br />
<span id="data3Dto_data2Dto_data3">data3Dto_data2Dto_data3</span><br />
<span id="data3Dto_data2Dto_data4">data3Dto_data2Dto_data4</span><br />
<span id="data3Dto_data2Dto_dataDto_data1">data3Dto_data2Dto_dataDto_data1</span><br />
<span id="data3Dto_data2Dto_dataDto_data2">data3Dto_data2Dto_dataDto_data2</span><br />
<span id="data3Dto_data2Dto_dataDto_data3">data3Dto_data2Dto_dataDto_data3</span><br />

こんな感じで利用できます。

最後のdata3Dto_data2Dto_dataDto_data3はプロパティーがないので、データが入らない状態ででます。標準だと例外エラーになるのでここのチェックが一番面倒だと思います。

以下パッチですが、

http://akira.info/labs/NestTeeda/teeda.patch

http://akira.info/labs/NestTeeda/teedaNest20110224.lzh

ここにパッチと、サンプルで動いているプロジェクト起きました。

Eclipseのパッチはなんかおかしい。。。ひとつだけ適応されないところがでてきています(涙)

もう少しテストを書く必要があるけれど、普通にデータ表示するだけだったらこれでいいのかな?

性能実験やっていないので、もう少し速度的な最適化はできるきがします。


Index: teeda-extension/src/test/java/org/seasar/teeda/extension/html/impl/TagProcessorAssembleImplTest.java
===================================================================
--- teeda-extension/src/test/java/org/seasar/teeda/extension/html/impl/TagProcessorAssembleImplTest.java	(revision 4307)
+++ teeda-extension/src/test/java/org/seasar/teeda/extension/html/impl/TagProcessorAssembleImplTest.java	(working copy)
@@ -450,6 +450,10 @@
             return false;
         }
 
+        public boolean hasNestProperty(String name) {
+            return false;
+        }
+
         public boolean isModified() {
             return false;
         }
Index: teeda-extension/src/test/java/org/seasar/teeda/extension/html/impl/page/AaaDto.java
===================================================================
--- teeda-extension/src/test/java/org/seasar/teeda/extension/html/impl/page/AaaDto.java	(revision 4307)
+++ teeda-extension/src/test/java/org/seasar/teeda/extension/html/impl/page/AaaDto.java	(working copy)
@@ -19,10 +19,11 @@
 
 /**
  * @author higa
- * 
+ *
  */
 
 public class AaaDto implements Serializable {
 
     public static final String COMPONENT = "instance=session";
+    public String aaaBbb;
 }
Index: teeda-extension/src/test/java/org/seasar/teeda/extension/html/impl/PageDescImplTest.java
===================================================================
--- teeda-extension/src/test/java/org/seasar/teeda/extension/html/impl/PageDescImplTest.java	(revision 4307)
+++ teeda-extension/src/test/java/org/seasar/teeda/extension/html/impl/PageDescImplTest.java	(working copy)
@@ -43,6 +43,15 @@
         assertFalse(pd.hasProperty(null));
     }
 
+    public void testHasNestProperty() throws Exception {
+        PageDesc pd = createPageDesc(FooPage.class, "fooPage");
+        assertFalse(pd.hasNestProperty("aaa"));
+        assertFalse(pd.hasNestProperty("aaaDto"));
+        assertTrue(pd.hasNestProperty("aaaDto_aaaBbb"));
+        assertFalse(pd.hasNestProperty(null));
+    }
+
+
     public void testHasItemsProperty() throws Exception {
         PageDesc pd = createPageDesc(FooPage.class, "fooPage");
         assertTrue(pd.hasItemsProperty("cccItems"));
Index: teeda-extension/src/main/java/org/seasar/teeda/extension/html/impl/PageDescImpl.java
===================================================================
--- teeda-extension/src/main/java/org/seasar/teeda/extension/html/impl/PageDescImpl.java	(revision 4307)
+++ teeda-extension/src/main/java/org/seasar/teeda/extension/html/impl/PageDescImpl.java	(working copy)
@@ -17,6 +17,7 @@
 
 import java.io.File;
 import java.io.Serializable;
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -72,6 +73,8 @@
 
     private static final String[] EMPTY_SCOPES = new String[0];
 
+    private Class pageClass;
+
     public PageDescImpl(Class pageClass, String pageName) {
         this(pageClass, pageName, null);
     }
@@ -90,6 +93,8 @@
     }
 
     protected void setup(Class pageClass) {
+        this.pageClass = pageClass;
+
         BeanDesc beanDesc = BeanDescFactory.getBeanDesc(pageClass);
         for (int i = 0; i < beanDesc.getPropertyDescSize(); ++i) {
             PropertyDesc pd = beanDesc.getPropertyDesc(i);
@@ -172,6 +177,57 @@
         return propertyNames.contains(name);
     }
 
+    public boolean hasNestProperty(String name) {
+        if( name == null ){
+            return false;
+        }
+
+        BeanDesc beanDesc = BeanDescFactory.getBeanDesc(pageClass);
+        String[] items = name.split("_");
+
+        if( items.length < 2 ){
+            return false;
+        }
+
+        for (int i = 0; i < beanDesc.getPropertyDescSize(); ++i) {
+            PropertyDesc pd = beanDesc.getPropertyDesc(i);
+            String propertyName = pd.getPropertyName();
+            if( items[0].equals(propertyName) ){
+                String[] items2 = new String[items.length-1];
+                for( int j = 0 ; j < items.length-1; j++ ){
+                    items2[j] = items[j+1];
+                }
+                return hasNestPropertySub( items2, pd.getPropertyType() );
+            }
+
+        }
+
+        return false;
+    }
+
+    private boolean hasNestPropertySub(String[] items,Class propertyClass) {
+        Field propertyFields[] = propertyClass.getDeclaredFields();
+
+        for( int i = 0 ; i < propertyFields.length ; i++ ){
+            String fieldsName = propertyFields[i].getName();
+            if( items[0].equals(fieldsName) ){
+                if( items.length == 1 ){
+                    return true;
+                } else {
+                    String[] items2 = new String[items.length-1];
+                    for( int j = 0 ; j < items.length-1; j++ ){
+                        items2[j] = items[j+1];
+                    }
+                    Class fieldsClass = propertyFields[i].getType();
+
+                    return hasNestPropertySub( items2, fieldsClass );
+                }
+            }
+        }
+
+        return false;
+    }
+
     public boolean hasItemsProperty(String name) {
         return itemsPropertyNames.contains(name);
     }
Index: teeda-extension/src/main/java/org/seasar/teeda/extension/html/PageDesc.java
===================================================================
--- teeda-extension/src/main/java/org/seasar/teeda/extension/html/PageDesc.java	(revision 4307)
+++ teeda-extension/src/main/java/org/seasar/teeda/extension/html/PageDesc.java	(working copy)
@@ -24,6 +24,8 @@
 
     boolean hasProperty(String name);
 
+    boolean hasNestProperty(String name);
+
     /**
      * ForEach繧ТelectOneMenu縺ァ縺ョPage繧ッ繝ゥ繧ケ縺ォ謖√▽Collection縺後≠繧句?蜷医↓true繧定ソ斐☆.
      * @param name
Index: teeda-extension/src/main/java/org/seasar/teeda/extension/html/factory/OutputTextFactory.java
===================================================================
--- teeda-extension/src/main/java/org/seasar/teeda/extension/html/factory/OutputTextFactory.java	(revision 4307)
+++ teeda-extension/src/main/java/org/seasar/teeda/extension/html/factory/OutputTextFactory.java	(working copy)
@@ -67,6 +67,9 @@
         if (isLabel(id, elementNode)) {
             return true;
         }
+        if (isNest(id, pageDesc)) {
+            return true;
+        }
         return pageDesc.hasProperty(id);
     }
 
@@ -88,6 +91,11 @@
         if (pageDesc.hasProperty(id)) {
             properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                     pageDesc.getPageName(), id));
+        } else if( isNest(id, pageDesc) ){
+            String[] items = id.split("_");
+            String itemName = implode( items, "." );
+            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
+                    pageDesc.getPageName(), itemName));
         } else {
             final String key = toNormalizeId(id);
             TextNode firstTextNode = elementNode.getFirstTextNode();
@@ -96,6 +104,26 @@
         }
     }
 
+    private static String implode(Object[] array, String sep) {
+        if (array == null) {
+            return null;
+        } else if (array.length < 1) {
+            return "";
+        } else if (array.length < 2) {
+            return (array[0] != null) ? array[0].toString() : "";
+        }
+
+        StringBuffer buf = new StringBuffer(
+                (array[0] != null) ? array[0].toString() : "");
+
+        for (int i = 1; i < array.length; i++) {
+            buf.append(sep);
+            buf.append((array[i] != null) ? array[i].toString() : "");
+        }
+
+        return buf.toString();
+    }
+
     protected boolean isLabel(final String id, final ElementNode elementNode) {
         final String key = toNormalizeId(id);
         if (!TeedaExtensionConfiguration.getInstance().outputTextLabelUnderAnchorOnly) {
@@ -115,6 +143,20 @@
         }
     }
 
+    protected boolean isNest(final String id, PageDesc pageDesc) {
+        if( id == null ){
+            return false;
+        }
+
+        String[] items = id.split("_");
+
+        if( items.length < 2 ){
+            return false;
+        }
+
+        return pageDesc.hasNestProperty(id);
+    }
+
     protected String toNormalizeId(String id) {
         final int pos = id.lastIndexOf('-');
         if (pos >= 0) {

2009-08-24

Teedaでネストしたプロパティー表示の実験

| 21:20

概要

Teedaを利用していて、面倒だなって思うところでDtoなどの大量のデータを表示する際にページプロパティーに詰め替える作業があります。Teedaの次バージョンでは実装予定項目で入っていますが、まだまだ出そうにないので実験してみました。

目的

実用的に利用できる状態までの実験は行わず、実装方法の検証とどれぐらいの作業量がかかりそうかの検証を目的とします。

書式の規定

通常Teedaはテンプレートのidにプロパティー名を入れます。ネストした場合には「neko.itemName」とするのが妥当なのですが、"."(ピリオド)を利用するとJavaScriptなどでスタイルシートの"."(ピリオド)との区別が付かなくなります。JSFデフォルトの":"(コロン)と同じくあまり好ましくない書式になります。

idに使わない記号で、あまり違和感のない"_"(アンダーバー)を今回は利用したいと思います。Teedaの場合通常ローワー・キャメルケースで書くので"_"(アンダーバー)は使いませんよね?

ただし、JSFの実装理念的には"."(ピリオド)を使うのが正しいはずです!

<span id="nekoDto_nekoString">nekoStringDummy</span><br />
<span id="nekoDto_nekoInt">nekoIntDummy</span><br />

こんな感じで"_"(アンダーバー)がきたら、そのクラスのプロパティーを表示するようにしたいと思います。

準備

ソースのダウンロード

Teeda本体をコンパイルするのにSeasar2本体も必要なので同時にダインロードしましょう。

JDKの設定

Seasarプロダクトは基本的にJDK4かJDK5でコンパイルを行います。Tigerと付いているパッケージは5でそれ以外が4の事が多いようです。今回はJDK5でコンパイルを行いたいと思います。

また、JDK6だとコンパイルエラーがでるのでコンパイルを行うことができません!

JAVA_HOME=C:\Program Files\Java\jdk1.5.0_16

JDK5の指定を行います。またコンパイル等はコマンドプロンプト上のmaven2を利用して行いました。

パッケージのテスト
mvn package

を実行してパッケージが成功するか確かめましょう。

該当場所のソース検索

表示にかかわる場所はすべて「teeda-extension」以下にありますが、そのものずばりのソースの場所がわからないので、まずは「label」をキーワードにしてソースを眺めてみました。

LabelProviderMap.java

    public Object get(final Object pageName) {
        return new LabelProviderMap() {
            public Object get(final Object key) {
                final String label = LabelUtil.getLabelValue((String) key,
                        (String) pageName);
                if (!StringUtil.isEmpty(label) || suppressDecolate) {
                    return label;
                }
                return "??" + key + ExtensionConstants.LABEL_ATTRIBUTE_SUFFIX +
                        "??";
            }
        };
    }

ラベルの指定をミスると??で囲われて出力されるので、実際にはここでリストから取ってきているようです。ただ今回はあまり関係なさそうですね。

AbstractElementProcessorFactory.java

    protected String getLabelExpression(final String attributeValue,
            PageDesc pageDesc) {
        final String pageName = pageDesc.getPageName();
        final String labelName = attributeValue.substring(0, attributeValue
                .length() -
                ExtensionConstants.LABEL_ATTRIBUTE_SUFFIX.length());
        return "#{labelProvider." + pageName + "." + labelName + "}";
    }

ここで上のプロバイダーを利用してラベルの出力を行っているようです。#{}の形式ですのでJSFで出力しているんですね。

OutputTextFactory.java

    public boolean isMatch(ElementNode elementNode, PageDesc pageDesc,
            ActionDesc actionDesc) {
        final String tagName = elementNode.getTagName();
        if (TeedaExtensionConfiguration.getInstance().outputTextSpanOnly &&
                !tagName.equals(JsfConstants.SPAN_ELEM)) {
            return false;
        } else if (!acceptableElements.contains(tagName.toLowerCase())) {
            return false;
        }
        if (pageDesc == null) {
            return false;
        }
        final String id = elementNode.getId();
        if (id == null) {
            return false;
        }
        if (isLabel(id, elementNode)) {
            return true;
        }
        return pageDesc.hasProperty(id);
    }

isLabelという名前を発見。なんかここが怪しいですね!

    protected boolean isLabel(final String id, final ElementNode elementNode) {
        final String key = toNormalizeId(id);
        if (!TeedaExtensionConfiguration.getInstance().outputTextLabelUnderAnchorOnly) {
            return key.endsWith("Label");
        }

        final ElementNode parent = elementNode.getParent();
        if (parent == null) {
            return false;
        }
        final String tagName = parent.getTagName();
        if (key.endsWith("Label") &&
                tagName.equalsIgnoreCase(JsfConstants.ANCHOR_ELEM)) {
            return true;
        } else {
            return false;
        }
    }

えーっと、処理内容的には最後にLabelが付いた場合にtrueになっていますね。isMatchという名前を見る限りテキスト出力時にidが処理対象かを判断する場所と思われます!

ここを拡張してネストしたプロパティーを実装したいと思います。

実装の追加

ネストチェックロジック追加
    protected boolean isNest(final String id, PageDesc pageDesc) {
        String[] items = id.split("_");

        if( items.length != 2 ){
            return false;
        }

        return pageDesc.hasProperty(items[0]);
    }

isNestという名前でチェック関数の追加をします。内容的には"_"(アンダーバー)が1つ含まれている場合で、そのクラスがページプロパティーにある場合にtrueとなります。本当はネストした先の存在チェックとかも必要ですし、複数ネストした状況とかもあるのですが実験はここまで。。。

idチェックロジック修正
    public boolean isMatch(ElementNode elementNode, PageDesc pageDesc,
            ActionDesc actionDesc) {
        final String tagName = elementNode.getTagName();
        if (TeedaExtensionConfiguration.getInstance().outputTextSpanOnly &&
                !tagName.equals(JsfConstants.SPAN_ELEM)) {
            return false;
        } else if (!acceptableElements.contains(tagName.toLowerCase())) {
            return false;
        }
        if (pageDesc == null) {
            return false;
        }
        final String id = elementNode.getId();
        if (id == null) {
            return false;
        }
        if (isLabel(id, elementNode)) {
            return true;
        }
        if (isNest(id, pageDesc)) {
            return true;
        }
        return pageDesc.hasProperty(id);
    }

isNestをチェック関数に追加します。これでネストしたid形式の場合OutputTextで利用されるようになりました。

テストプロジェクトの準備

ここで一度テスト用のプロジェクトを作成して、実験してみたいと思います。

プロジェクト作成

Doltengを利用してTeedaプロジェクトを作成します。

Dto作成

NekoDto.java

package neko.web;

public class NekoDto {
    public int nekoInt;
    public String nekoString;
}

シンプルなDtoクラスを作成します。privateでもかまいませんがその場合にはSetterとGetterを作成してあげてください。

ページクラス作成

TestPage.java

package neko.web;

public class TestPage {

    public NekoDto nekoDto;

    public Class<?> initialize() {
        nekoDto = new NekoDto();
        nekoDto.nekoInt = 12345;
        nekoDto.nekoString = "neko1234";

        return null;
    }

    public Class<?> prerender() {
        return null;
    }
}

こちらもシンプルにDtoの作成と値設定のみを行います。

テンプレート作成

test.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:te="http://www.seasar.org/teeda/extension" lang="ja" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>nekoTest</title>
</head>
<body>

Teeda[nekoString]:<span id="nekoDto_nekoString">nekoStringDummy</span><br />
Teeda[nekoInt]:<span id="nekoDto_nekoInt">nekoIntDummy</span><br />

</body></html>

ネストの指定が入っている以外は通常のXHTMLで問題ありません。

自作Teedaパッケージへ入れ替え

新規作成の状態では最新のTeedaのパッケージが利用されるので、自分でパッケージしたファイルを利用するように変更します。

copy /y teeda-ajax\target\teeda-ajax-1.1.0-alpha1-SNAPSHOT.jar ..\neko\src\main\webapp\WEB-INF\lib
copy /y teeda-core\target\teeda-core-1.1.0-alpha1-SNAPSHOT.jar ..\neko\src\main\webapp\WEB-INF\lib
copy /y teeda-extension\target\teeda-extension-1.1.0-alpha1-SNAPSHOT.jar ..\neko\src\main\webapp\WEB-INF\lib

なんのひねりもないですが、自分でパッケージしたファイルをテスト用のプロジェクトにコピーして、元々あるパッケージを削除してコピーしたパッケージを利用するようにクラスパスを書き換えます。

実行

Teeda[nekoString]:??nekoDto_nekoSLabel??
Teeda[nekoInt]:??nekoDto_neLabel??

当たり前ですが、出力がおかしいです。OutputTextで出力部分の実装を行っていないですからね。ただlabelの処理系に飛んでいるってことで、認識までは動く動作していることが確認できました。

出力部分実装

ここはかなりいろいろ試行錯誤して、どこにいれればいいのか悩んだのですが、、、

OutputTextFactory.java

    protected void customizeProperties(Map properties, ElementNode elementNode,
            PageDesc pageDesc, ActionDesc actionDesc) {
        super
                .customizeProperties(properties, elementNode, pageDesc,
                        actionDesc);
        properties.put("tagName", elementNode.getTagName());

        if (pageDesc == null) {
            return;
        }
        final String id = elementNode.getId();
        if (pageDesc.hasProperty(id)) {
            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                    pageDesc.getPageName(), id));
        } else {
            final String key = toNormalizeId(id);
            TextNode firstTextNode = elementNode.getFirstTextNode();
            properties.put(JsfConstants.VALUE_ATTR, getLabelExpression(key,
                    pageDesc));
        }
    }

灯台下暗し・・・isLabelのすぐ上になった関数で処理していました!

処理的には「pageDesc.hasProperty(id)」でページプロパティーの場合にはidを出力して、それ以外はlabelを出力するってなっていますね。

    protected void customizeProperties(Map properties, ElementNode elementNode,
            PageDesc pageDesc, ActionDesc actionDesc) {
        super
                .customizeProperties(properties, elementNode, pageDesc,
                        actionDesc);
        properties.put("tagName", elementNode.getTagName());

        if (pageDesc == null) {
            return;
        }
        final String id = elementNode.getId();
        if (pageDesc.hasProperty(id)) {
            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                    pageDesc.getPageName(), id));
        } else if( isNest(id, pageDesc) ){
            String[] items = id.split("_");
            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                    pageDesc.getPageName(), items[0]+"."+items[1]));
        } else {
            final String key = toNormalizeId(id);
            TextNode firstTextNode = elementNode.getFirstTextNode();
            properties.put(JsfConstants.VALUE_ATTR, getLabelExpression(key,
                    pageDesc));
        }
    }

改造して、isNestの場合に「nekoDto_nekoSLabel」を「nekoDto.nekoSLabel」に置換して出力しています。

実行

  • パッケージ作成(mvn package)
  • jarファイルコピー(copy -y...)
  • Tomcat再起動

の手順を踏んでからページを再表示させます。

Teeda[nekoString]:neko1234
Teeda[nekoInt]:12345

見事表示されました!

仕組み

元々JSFではネストしたプロパティーを実は表示することができます。そのため案外改造箇所が少なく動いています。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:te="http://www.seasar.org/teeda/extension" xmlns:h="http://java.sun.com/jsf/html" lang="ja" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>nekoTest</title>
</head>
<body>

Teeda[nekoString]:<span id="nekoDto_nekoString">nekoStringDummy</span><br />
Teeda[nekoInt]:<span id="nekoDto_nekoInt">nekoIntDummy</span><br />

<br />

JSF[nekoString]:<h:outputText value="#{testPage.nekoDto.nekoString}"/><br />
JSF[nekoInt]:<h:outputText value="#{testPage.nekoDto.nekoInt}"/><br />

</body></html>

こんな感じで最初に「xmlns:h="http://java.sun.com/jsf/html"」を追加してJSFのタグを利用可能にして、h:outputTextを直接呼べば無改造のTeedaでもネストしたプロパティーへアクセスすることができます。

現状はネストしたidの場合ページプロパティーに存在していないと思い、labelの処理に飛んでいるので表示することができませんでした。

総括

簡単にできているように書いてありますが、予想以上に解析に時間かかっていたりします(笑) 結局6時間前後かかったよ。。。

outputTextは簡単にできているように見えますが、実運用するためにはもう少しきれいに組む必要があります。。。InputTextとか大量に対応しないといけない場所があるので、ちょっと面倒かも。ただ技術的難易度はそれほどない気がしますので、手間をかければ実装できそうです。

ただTeedaプロジェクトの人は現在T2に注力している感じなので、当分機能追加のバージョンアップはなさそうですね。個人的にはこのネストがあるとページクラスが非常にすっきりするので欲しい機能だったりします。

うーむ、自分で組むかが悩ましい。。。

2009-08-20

Teedaにおけるpom.xmlの設定

| 00:24

Teedaでプロジェクトを利用していると、通常Maven2でパッケージを作ると思いますが、その設定例を書き出してみます。基本的にはEclipse上でDoltengを利用して作成したものに手をいれています。あとちょっと昔のプロジェクトなのでバージョンとかは少し古いものです。

注意事項としては、小規模から中規模案件向けの設定になります。

build

ビルドに関してはpathなどはデフォルト設定のまま使っています。ここは妙に変えると面倒ですからね。会社規定のpath構造などをあって、デフォルトと違う場所に移動するとパッケージを作る際に特別な手順などが必要なのと、導入教育が面倒なのでデフォルトで使うのが良いと思います。(共通Path設定にするためのMavenだよね?)

コンパイラバージョンの指定
<plugin>
  <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
      <source>1.5</source>
      <target>1.5</target>
      <encoding>UTF-8</encoding>
    </configuration>
</plugin>

通常1.6の新機能って使うことないと思いますので、1.5にしておきます。この辺は環境依存しますが確実に1.6の使える環境であれば1.6を使った方が動作が早いような気もしますが、まだまだサーバーが1.5で動いていることが多いので。。。

javadocの設定
<plugin>
  <artifactId>maven-javadoc-plugin</artifactId>
  <configuration>
    <source>1.5</source>
    <minmemory>128m</minmemory>
    <maxmemory>512m</maxmemory>
    <encoding>UTF-8</encoding>
    <docencoding>UTF-8</docencoding>
    <charset>UTF-8</charset>
    <locales>ja</locales>
  </configuration>
</plugin>

個人的には使いませんが、とりあえず入れておきます。パッケージを作るだけであれば利用されないですしね。なんとなくDoxygen派です。でも基本的にはソースは頭で覚えるか実物見るのであまりこの手のドキュメントは使いません。

profiles

環境別のビルドファイルを作る設定を追加します。

<profiles>
  <profile>
    <id>development</id>
    <activation>
      <activeByDefault>true</activeByDefault>
    </activation>
    <build>
      <finalName>neko-development</finalName>
      <resources>
        <resource>
          <directory>src/main/resources</directory>
        </resource>
        <resource>
          <directory>src/main/java</directory>
        </resource>
      </resources>
    </build>
  </profile>
  <profile>
    <id>ita</id>
    ... 略 ...
  </profile>
  <profile>
    <id>itb</id>
    ... 略 ...
  </profile>
  <profile>
    <id>product</id>
    <build>
      <finalName>neko-product</finalName>
      <resources>
        <resource>
          <directory>src/product/resources</directory>
        </resource>
        <resource>
          <directory>src/main/resources</directory>
        </resource>
        <resource>
          <directory>src/main/java</directory>
        </resource>
      </resources>
    </build>
  </profile>
</profiles>

src/main/resources にデフォルト環境+開発環境

src/ita/resources に社内テストサーバー用設定(差分のみ)

src/itb/resources に本番テストサーバー用設定(差分のみ)

src/product/resources に本番サーバー用設定(差分のみ)

って使い分けをしています。中身は

env.txt環境別にitやproductを指定
jdbc.diconDBのIPやパスワードの設定
log4j.propertiesテストサーバーはdebugなど出力レベル調整
siteSetting.diconそのサーバー特有の設定

みたいなものを入れています。デフォルトでないのがsiteSetting.diconです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
  "http://www.seasar.org/dtd/components24.dtd">
<components>
  <component name="SiteSettingService" class="neko.service.SiteSettingService">
    <property name="csvBasePath">
      "C:/neko-data/csv/"
    </property>
    <property name="siteBaseURL">
      "http://127.0.0.1:8080/neko/"
    </property>
  </component>
</components>

上記のようにサーバーによって変わってくる部分を記述します。メールとかに埋め込むサイトのURLとか、管理者のメールアドレスとかDBに設定すればいいのですが、ちょっとしたものはここで設定しています。propertiesでもいいのですが、なんとなく(笑)

package neko.service;

public class SiteSettingService {
  public String csvBasePath;
  public String siteBaseURL;
}

受側のjavaファイルです。サンプルでは何もしていませんが、実際にはURLの合成などのちょっとした処理が入っていたりします。利用するときにはこのサービスを宣言すると自動的にDIされて設定したdiconファイルの内容が初期値に入っています。

pluginRepositories

リポジトリの設定ですが利用していません。個人的にはプロジェクトで利用するファイルはすべてSVNにコミットする方針を採っているので、インターネットやイントラからのダウンロードはしない方針です。

dependencies

すべてプロジェクト内部にファイルを持ちます。これによってMaven系のプラグインのダウンロードは発生しますが、個別のライブラリのダウンロードは必要なくなります。

    <dependencies>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-jsp_2.0_spec</artifactId>
            <version>2.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/geronimo-jsp_2.0_spec-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-servlet_2.4_spec</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/geronimo-servlet_2.4_spec-1.0.jar</systemPath>
        </dependency>
        ... 略 ...

パッケージの作り方

バッチファイルで以下のコマンドを実行しています。Eclipse上から実行するとEclipseの実行Pathからになるので、エクスプローラーかDOS窓上から実行しています。内容は本番テストサーバーと本番サーバーのパッケージになります。開発系も一緒にパッケージングしてもいいのですが、開発系はHudsonとかで自動デプロイされる環境がこのましいのと、別タイミングで作ることが多いのでこんな感じになっています。

mkdir package

call mvn clean package -P itb
copy /y target\*.war package

call mvn clean package -P product
copy /y target\*.war package

call mvn clean

全文

<?xml version="1.0" encoding="UTF-8"?><project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>neko</groupId>
    <artifactId>neko</artifactId>
    <packaging>war</packaging>
    <name>neko</name>
    <version>0.0.1</version>
    <build>
        <sourceDirectory>src/main/java</sourceDirectory>
        <outputDirectory>src/main/webapp/WEB-INF/classes</outputDirectory>
        <testOutputDirectory>target/test-classes</testOutputDirectory>
        <testSourceDirectory>src/test/java</testSourceDirectory>
        <testResources>
            <testResource>
                <directory>src/test/resources</directory>
            </testResource>
        </testResources>
        <defaultGoal>validate</defaultGoal>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-source-plugin</artifactId>
                <executions>
                    <execution>
                        <id>source-jar</id>
                        <phase>package</phase>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>build-helper-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>add-source</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>add-source</goal>
                        </goals>
                        <configuration>
                            <sources>
                                <source>src/main/webapp/view</source>
                            </sources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-javadoc-plugin</artifactId>
                <configuration>
                    <source>1.5</source>
                    <minmemory>128m</minmemory>
                    <maxmemory>512m</maxmemory>
                    <encoding>UTF-8</encoding>
                    <docencoding>UTF-8</docencoding>
                    <charset>UTF-8</charset>
                    <locales>ja</locales>
                </configuration>
            </plugin>
        </plugins>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-eclipse-plugin</artifactId>
                    <configuration>
                        <wtpversion>1.5</wtpversion>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
    
    <profiles>
       <profile>
         <id>development</id>
         <activation>
           <activeByDefault>true</activeByDefault>
         </activation>
         <build>
           <finalName>neko-development</finalName>
           <resources>
             <resource>
               <directory>src/main/resources</directory>
             </resource>
             <resource>
               <directory>src/main/java</directory>
             </resource>
           </resources>
         </build>
       </profile>
       <profile>
         <id>ita</id>
         <build>
           <finalName>neko-ita</finalName>
           <resources>
             <resource>
               <directory>src/ita/resources</directory>
             </resource>
             <resource>
               <directory>src/main/resources</directory>
             </resource>
             <resource>
               <directory>src/main/java</directory>
             </resource>
           </resources>
         </build>
       </profile>
       <profile>
         <id>itb</id>
         <build>
           <finalName>neko-itb</finalName>
           <resources>
             <resource>
               <directory>src/itb/resources</directory>
             </resource>
             <resource>
               <directory>src/main/resources</directory>
             </resource>
             <resource>
               <directory>src/main/java</directory>
             </resource>
           </resources>
         </build>
       </profile>
       <profile>
         <id>product</id>
         <build>
           <finalName>neko-product</finalName>
           <resources>
             <resource>
               <directory>src/product/resources</directory>
             </resource>
             <resource>
               <directory>src/main/resources</directory>
             </resource>
             <resource>
               <directory>src/main/java</directory>
             </resource>
           </resources>
         </build>
       </profile>
    </profiles>

    <dependencies>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-jsp_2.0_spec</artifactId>
            <version>2.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/geronimo-jsp_2.0_spec-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-servlet_2.4_spec</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/geronimo-servlet_2.4_spec-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/junit-3.8.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>junit-addons</groupId>
            <artifactId>junit-addons</artifactId>
            <version>1.4</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/junit-addons-1.4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>rhino</groupId>
            <artifactId>rhino</artifactId>
            <version>1.6r2</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/rhino-1.6r2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-webunit</artifactId>
            <version>0.2.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/teeda-webunit-0.2.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-xmlunit</artifactId>
            <version>0.1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/teeda-xmlunit-0.1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>xmlunit</groupId>
            <artifactId>xmlunit</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/xmlunit-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/aopalliance-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-collections-3.1.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-el</groupId>
            <artifactId>commons-el</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-el-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-fileupload-1.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-io-1.3.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-logging-1.1.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-jta_1.1_spec</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/geronimo-jta_1.1_spec-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.0.69</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/h2-1.0.69.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>jboss</groupId>
            <artifactId>javassist</artifactId>
            <version>3.4.ga</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/javassist-3.4.ga.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.1.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/jstl-1.1.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.13</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>ognl</groupId>
            <artifactId>ognl</artifactId>
            <version>2.6.9-patch-20070908</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/ognl-2.6.9-patch-20070908.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.0-FINAL</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/poi-3.0-FINAL.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.dao</groupId>
            <artifactId>s2-dao</artifactId>
            <version>1.0.49</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-dao-1.0.49.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.dao</groupId>
            <artifactId>s2-dao-tiger</artifactId>
            <version>1.0.49</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-dao-tiger-1.0.49.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.container</groupId>
            <artifactId>s2-extension</artifactId>
            <version>2.4.33</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-extension-2.4.33.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.container</groupId>
            <artifactId>s2-framework</artifactId>
            <version>2.4.33</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-framework-2.4.33.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.container</groupId>
            <artifactId>s2-tiger</artifactId>
            <version>2.4.33</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-tiger-2.4.33.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-ajax</artifactId>
            <version>1.0.13-sp4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/teeda-ajax-1.0.13-sp4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-core</artifactId>
            <version>1.0.13-sp4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/teeda-core-1.0.13-sp4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-extension</artifactId>
            <version>1.0.13-sp4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/teeda-extension-1.0.13-sp4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-tiger</artifactId>
            <version>1.0.13-sp4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/teeda-tiger-1.0.13-sp4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>xerces</groupId>
            <artifactId>xercesImpl</artifactId>
            <version>2.6.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/xercesImpl-2.6.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>xerces</groupId>
            <artifactId>xmlParserAPIs</artifactId>
            <version>2.6.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/xmlParserAPIs-2.6.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>net.arnx.jsonic</groupId>
            <artifactId>jsonic</artifactId>
            <version>1.0.3</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/jsonic-1.0.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.lowagie.text</groupId>
            <artifactId>iText</artifactId>
            <version>2.1.4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/iText-2.1.4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-lang-2.4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.chronos</groupId>
            <artifactId>s2chronos-core</artifactId>
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2chronos-core-1.0.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.chronos</groupId>
            <artifactId>s2chronos-extension</artifactId>
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2chronos-extension-1.0.0.jar</systemPath>
        </dependency>
    </dependencies>
</project>

総括

小規模から中規模程度のTeedaプロジェクトであればリポジトリよりはプロジェクトの内部にライブラリもった方が楽だと思います。SVNにはライブラリがコミットされているけれど、パッケージの際にさらに同じものがダウンロードとかされたりしてないでしょうか? JDBCとか公開リポジトリに置いてない物がありますしね。

2009-08-19

Teeda向けS2JDBC-GenでのDB構成管理

| 20:12

S2JDBC-Genの使い方としては、想定外の使い方をしています。他にも構成管理をする方法はありますので、よく検討してから参照してください。

基本概要

Teedaに限ったことではありませんが、S2JDBCを利用していないプロジェクト向けに、無理やりS2JDBC-Genを利用してDB構成管理をしてみる方法について記述しています。

そのため、S2JDBCを開発で利用している場合の想定手順と違う方法や、手法などを利用しています。

ERDツールとの差別化

構成管理だけであれば、ERDツールなりエクセルなりで管理が可能だと思います。ただしS2JDBC-Genではデータの管理も行うことができます。通常開発用の最低限のマスターデータが入ったもの。負荷テスト用に最大限のデータが入ったものなどを準備し、気軽に準備することができます。

もちろん開発用の共有で使えるデータベースを準備して、接続先を切り替えて使う方式でも対応できますが、S2JDBC-Genを使うとローカルのデータベースをコマンド一発で初期状態に戻したりできますし、確実に最新環境に入れ替えたりが可能ですのでより失敗することが少なく開発をすることが可能です。

準備

データベースのバックアップ

おそらく実験途中で何度かデータベースを壊します。復帰できるようにあらかじめデータベースのバックアップを行いましょう。

プロジェクト作成

Doltengを利用してSAStrutsS2JDBCの構成でプロジェクトを作成します。作成する名前はメインプロジェクトがnekoであれば、neko-dbなどわかりやすい名前が良いと思います。

ポイントとしては、なるべく最新のDoltengを利用して作成してください。Seasar本体のバージョンはなるべく新しいほうが安定していると思います。

DBの設定

resouecesの中にある「jdbc.dicon」と「s2jdbc.dicon」を編集して利用しているデータベースに接続できるようにします。必要があればJDBCなどをプロジェクトの中に追加する必要があります。

Teedaで利用されているS2Daoと違うところは、「s2jdbc.dicon」にて利用するデータエースの種類を選択することになります。ファイルの中身を見ればわかると思いますがdialectをデフォルトのH2から利用しているものに変更しましょう。

データベースへの設計反映思想

S2JDBC推奨Entity -> DDL作成S2JDBCで開発をするのであれば自然な流れ
個人的推奨DDL -> Entity作成新しく覚えることが最小

S2JDBCはEntityを作成して、それをデータベースに反映するサイクルを推奨しています。S2JDBCをベースに開発をしているのであれば上記の流れでもよいのですが、以下の場合にはこの流れを変えたほうがいいと思います。

  • ERDツールなどでDBを設計している
  • エクセルでDBを設計している
  • CREATE TABLE文を書かないと落ちつかない
  • S2JDBCのEntityの書き方を覚えるのが面倒

何をマスターとするかにかかわってきますが、S2JDBCはEntityをマスターとする考えになります。逆にERDツールを利用していると、ERDツールがマスターになりますので、推奨パターンでは面倒になります。あとは何気にエクセルで管理しているところもまだ多いですよね?

この流れの変化を嫌ってS2JDBC-Genを使っていない人って結構いるんじゃないでしょうか?

DDLからEntityへの私の推奨反映方法

新規作成したテーブルgen-entity、gen-ddlを実行
軽微なテーブル変更Entityを編集後、gen-ddlを実行
大規模なテーブル変更Entityを削除後gen-entity、gen-ddlを実行

もちろん、S2JDBC推奨のEntityを新規作成してもかまいません。軽微な変更でも毎回Entityを削除してからでもかまわないと思います。最終的にはgen-ddlが作成したDDLが、元のDDLと同一であるかが重要だと思います。

マッピングの不一致

実際のところgen-entity、gen-ddlをして作成されたDDLは当初のDDLと完全に一致するとは限りません。これは使っているデータ種などによるのですが、Javaの型とデータベースの型が1対1でマッピングできないので仕方がないと思います。

なので、出来上がったDDLを確認してからEntityの修正を行う必要があります。この作業があるためすでにある程度の数のテーブルがある場合にはかなりの手間になると思います。

プロジェクトの初期からの導入以外で、すでに稼動しているプロジェクトなどに適用するのはテスト工数などを考えると非常に大変になると思ってください。基本的に最初に導入を決めないと、途中からは危ないのでお勧めできません。

S2JDBC-Genの仕組み

プロジェクト直下にあるs2jdbc-gen-build.xmlがS2JDBC-Genの設定ファイルになります。内容はAntタスクであり、実行するタスクが記述されています。

S2JDBC-Genを拡張する場合には、準備されているタスクの設定を変更するか、新規のAntタスクを作成することになります。比較的簡単に拡張が可能ですので、いろいろと作ってみると楽しいと思います。

DBに日本語のコメントを追加する

以下、実際のS2JDBC-Genの設定になります。

当初なくって残念でしたが、追加された機能です。JavadocのAPIを利用してコメントに埋め込む形で対応を行っています。昔は自作タスクを作って対応していましたが現在は標準機能を利用したほうがスマートだと思います。

    <gen-entity
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      javafiledestdir="${javafiledestdir}"
      javafileencoding="${javafileencoding}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      applyDbCommentToJava="true"
    />

上記のように最後にapplyDbCommentToJavaを追加します。これでデータベースに設定されているコメントをEntity作成時に取り込むようになります。

    <gen-ddl
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      applyJavaCommentToDdl="true"
    />

EntityからDDL作成時にもコメントを利用するように設定します。gen-entityとgen-ddlで設定の名前が違うので注意してください。上記の設定ができたら、gen-entityを実行し作成されたEntityにコメントが振られていることを確認してください。

package neko.entity;

import java.io.Serializable;
import java.math.BigInteger;
import java.sql.Date;
import javax.annotation.Generated;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

/**
 * イベント履歴
 * 
 */
@Entity
@Generated(value = {"S2JDBC-Gen 2.4.39", "org.seasar.extension.jdbc.gen.internal.model.EntityModelFactoryImpl"}, date = "2009/08/18 23:58:19")
public class EventList implements Serializable {

    private static final long serialVersionUID = 1L;

    /** イベントの連番 */
    @Id
    @Column(precision = 22, nullable = false, unique = true)
    public BigInteger eventId;

    /** イベントの発生日時 */
    @Column(nullable = false, unique = false)
    public Date eventDate;

    /** イベントコメント */
    @Column(length = 4000, nullable = true, unique = false)
    public String eventComment;
}

たとえば上記のような形になります。

このファイルができたところで、gen-ddlを実行します。

com.sun.tools.javadoc.Docletが使用できません。JDKのtools.jarがクラスパスに通されていることを確認してください。

上記のエラーがgen-ddl時に出た場合にはJDKのtools.jarをクラスパスに追加しましょう。Eclipse上でJREなどを利用して開発を行っている場合にはtools.jarが入っていないのでエラーになります。

この場合には、お行儀が悪いですがどこかのJDKからtools.jarをコピーしてきて、プロジェクト直下のlibフォルダなどにいれてクラスパスに追加すると良いと思います。

そうするとdbフォルダ上にddlなどが作成されます。

create table EVENT_LIST (
    EVENT_ID number(22,0) not null,
    EVENT_DATE date not null,
    EVENT_COMMENT varchar2(4000),
    constraint FORM_EVENT_LIST_PK primary key(EVENT_ID)
);

comment on table EVENT_LIST is 'イベント履歴';
comment on column EVENT_LIST.EVENT_ID is 'イベントの連番';
comment on column EVENT_LIST.EVENT_DATE is 'イベントの発生日時';
comment on column EVENT_LIST.EVENT_COMMENT is 'イベントコメント';

Oracle上の場合ですが、上記のようなDDLが作成されました。この場合にはオリジナルのDDLと同一の構成なのでこのままで大丈夫ですが、違っていた場合には編集する必要があります。

たとえばOracleの場合CHARとVARCHAR2は両方Entity上ではStringになりますが、StringからはVARCHAR2で作成されるので非対称の変換になります。この場合には

    @Column(length = 8, nullable = false, unique = false)
    public String textData;

上記のようなカラムの場合には

    @Column(length = 8, nullable = false, unique = false, columnDefinition = "char(8)")
    public String textData;

このようにDBの宣言をそのものずばり書いてあげれば大丈夫です。(http://s2container.seasar.org/2.4/ja/s2jdbc_gen/entity_definition.html

シーケンスでのINSERT

S2JDBCはシーケンスの機能を備えていますが、データベースごとの機能の差を吸収するためだと思いますが、独自に実装しています。そのためS2JDBCを利用した開発の場合には問題がないのですが、S2Daoやツール上からInsertした場合などにシーケンスが自動的に更新されません。

たとえばOracleの場合にはシーケンスを作成して、Insertのトリガーとして処理を追加する必要があります。このようなデータベースに依存する実装方法はS2JDBC-Genではサポートしていません。

そこで自分でトリガーやシーケンスを作成する必要があります。Oracleの場合ですのでMySQLなどであれば違った手順になります。

    /** eventIdプロパティ */
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "EVENT_ID")
    @SequenceGenerator(name = "EVENT_ID",allocationSize=1)
    @Column(name = "EVENT_ID", precision = 22, nullable = false, unique = true)
    public BigInteger formEventId;

上記のようにSequenceGeneratorでEVENT_IDという名前で、増分1のシーケンスを作成し、GeneratedValueで設定します。この状態でgen-ddlを実行すると030-sequenceというフォルダが作成され、中にシーケンスを作成するSQLが入っています。

このままですとシーケンスができただけで、トリガーができていませんのでトリガーは自分で追加してあげる必要があります。任意の名前でフォルダを作成して、中にSQLファイルを入れることで自動的に実行してくれますので999-USERとします。

db
 +- 0000 初期構成
 +- 0001
   +- create
   +  +- 010-table
   +  +- 020-uniquekey
   +  +- 030-sequence
   +  +- 040-dump
   +  +- 050-foreignkey
   +  +- 999-user
   +- drop

上記のようなフォルダ構成にします。数値の場所はDBのリビジョン番号ですのでどんどん増えていきます。システムが自動作成したフォルダ以外は次のリビジョンを作成したときに自動的にコピーしてくれますので、最新のリビジョンに入れることで次のリビジョンから常に入った状態になります。

CREATE OR REPLACE TRIGGER "BI_EVENT_LIST" 
  before insert on "EVENT_LIST" 
  for each row 
begin 
    select "EVENT_LIST_EVENT_ID".nextval into :NEW.EVENT_ID from dual; 
end; 
/

上記のようなSQLをevent_list.sqlなどとして999_userに入れておきます。

その他の注意点

カラムの順番が変わる

gen-entityはプライマリーキーを一番上に移動させますので、飛んでいるカラムにプライマリーキーを設定している場合には注意してください。

プライマリーキーの数など

念のためプライマリーキー関連は確かめた方がよいと思います。

外部制約など

外部制約なども確かめた方がよいと思います。

viewについて

viewはS2JDBC-Genでは管理されません。

トリガーと同じように適当なフォルダを作成して、中にSQLを入れておきます。

DBのリビジョン管理について

標準的に利用しているとdbフォルダ上にgen-ddlを実行するたびにリビジョンが増えていきます。どうもファイルが増えていくのが個人的には好きになれないので、標準のリビジョン管理を回避しています。

まず最初にdbフォルダを削除して、gen-ddlを実行します。

[db]
+- [migreate]
    +- [0000]
    +- [0001]
    +- ddl-info.txt

すると上記のような構成になります。

このddl-info.txtが内部で管理してあるデータベースのリビジョン番号を記述したファイルです。このファイルをコピーしてddl-info_base.txtとします。

[db]
+- [migreate]
    +- [0000]
    +- [0001]
    +- ddl-info.txt
    +- ddl-info_base.txt

こうなります。

次にs2jdbc-gen-build.xmlのgen-ddlのタスクを書き換えます。

  <target name="gen-ddl">

    (略)

    
    <refresh projectName="neko-db"/>

    <delete dir="db/migrate/0000"/>
    <copy todir="db/migrate/0001">
      <fileset dir="db/migrate/0002"/>
    </copy>
    <delete dir="db/migrate/0002"/>
    <copy file="db/ddl-info_base.txt" tofile="db/ddl-info.txt" overwrite="true" />

gen-ddlの最後にdelete以下の文を追加します。

0000の削除DROPは別タスクで行うので削除します
0002を0001にコピー新規でできた0002の内容を0001に上書きします。※
0002の削除追加分を削除します
ddl-infoの書き戻し常にリビジョン1固定にします

こんな処理をして、常に0001しかないようにしています。ただしこのままだとテーブルを削除した場合にも0001上にファイルが残り続けることになるので、そこは手で消します!

データベースの初期化

リビジョン管理をしなくなった場合、いつのデータベースに対してmigrateをかけるのかがわからないので、存在しないテーブルを削除しようとしてエラーになったり(現在はエラーでも処理が続きます)、管理外のテーブルが消えていなかったりします。

特に実装上必要だけれど、管理化に入っていないテーブルなどが残っていると大変ですので、migrateをしてまっさらな状態から管理しているテーブルを作る手順を取っています。

ただし危険です!

Antタスク開発用にjar追加

ant-1.7.0.jarなどant開発用のjarをプロジェクトに追加します。

削除用タスク作成

Oracleの例ですが、以下のファイルを作成します。

/neko-db/src/main/java/org/seasar/extension/jdbc/gen/internal/command/CleanDatabaseCommand.java
/neko-db/src/main/java/org/seasar/extension/jdbc/gen/task/CleanDatabaseTask.java

CleanDatabaseという、すべてのテーブルなどを削除するタスクを作成します。

/neko-db/src/main/java/org/seasar/extension/jdbc/gen/task/CleanDatabaseTask.java

package org.seasar.extension.jdbc.gen.task;

import org.seasar.extension.jdbc.gen.command.Command;
import org.seasar.extension.jdbc.gen.internal.command.CleanDatabaseCommand;

/**
 * データベースからテーブルなどを削除する{@link Task}です。
 *
 * @see CleanDatabaseCommand
 */
public class CleanDatabaseTask extends GenerateEntityTask {
	/** コマンド */
	protected CleanDatabaseCommand command = new CleanDatabaseCommand();

	@Override
	protected Command getCommand() {
		return command;
	}
}

このファイルは登録だけなので、GenerateEntityTaskを継承してCleanDatabaseCommandを作成して終わりです。

/neko-db/src/main/java/org/seasar/extension/jdbc/gen/internal/command/CleanDatabaseCommand.java

package org.seasar.extension.jdbc.gen.internal.command;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.seasar.extension.jdbc.util.ConnectionUtil;
import org.seasar.extension.jdbc.util.DataSourceUtil;
import org.seasar.framework.exception.SQLRuntimeException;
import org.seasar.framework.util.PreparedStatementUtil;
import org.seasar.framework.util.ResultSetUtil;
import org.seasar.framework.util.StatementUtil;

/**
 * データベースのテーブルなどをすべて削除するコマンドです。
 */
public class CleanDatabaseCommand extends GenerateEntityCommand {
    @Override
    protected void doExecute() {
        dropTables();
        dropSequence();
        dropView();
        dropTrigger();
    }

    private void dropTables() {
        String execSql = " select" +
                            "   tbl.TABLE_NAME NAME" +
                            " from" +
                            "   USER_TABLES tbl";
        Connection conn = DataSourceUtil.getConnection(jdbcManager
                .getDataSource());
        try {
            logger.debug(execSql);
            PreparedStatement ps = ConnectionUtil.prepareStatement(conn,
                    execSql);
            try {
                ResultSet rs = PreparedStatementUtil.executeQuery(ps);
                try {
                    while (rs.next()) {
                        String name = rs.getString("NAME");

                        String execSql2 = "DROP TABLE \"" + name + "\" CASCADE CONSTRAINT";

                        try {
                            logger.debug(execSql2);
                            PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, execSql2);
                            try {
                                PreparedStatementUtil.executeQuery(ps2);
                            } finally {
                                StatementUtil.close(ps2);
                            }
                        } finally {
                        }
                    }
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                } finally {
                    ResultSetUtil.close(rs);
                }
            } finally {
                StatementUtil.close(ps);
            }
        } finally {
            ConnectionUtil.close(conn);
        }
    }

    private void dropSequence() {
        String execSql = " select" +
                            "   tbl.SEQUENCE_NAME NAME" +
                            " from" +
                            "   USER_SEQUENCES tbl";
        Connection conn = DataSourceUtil.getConnection(jdbcManager
                .getDataSource());
        try {
            logger.debug(execSql);
            PreparedStatement ps = ConnectionUtil.prepareStatement(conn,
                    execSql);
            try {
                ResultSet rs = PreparedStatementUtil.executeQuery(ps);
                try {
                    while (rs.next()) {
                        String name = rs.getString("NAME");

                        String execSql2 = "DROP SEQUENCE \"" + name + "\"";

                        try {
                            logger.debug(execSql2);
                            PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, execSql2);
                            try {
                                PreparedStatementUtil.executeQuery(ps2);
                            } finally {
                                StatementUtil.close(ps2);
                            }
                        } finally {
                        }
                    }
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                } finally {
                    ResultSetUtil.close(rs);
                }
            } finally {
                StatementUtil.close(ps);
            }
        } finally {
            ConnectionUtil.close(conn);
        }
    }

    /**
     * VIEW削除
     */
    private void dropView() {
        String execSql =  "select tbl.VIEW_NAME NAME" +
                            " from USER_VIEWS tbl";
        Connection conn = DataSourceUtil.getConnection(jdbcManager.getDataSource());
        try {
            logger.debug(execSql);
            PreparedStatement ps = ConnectionUtil.prepareStatement(conn,
                    execSql);
            try {
                ResultSet rs = PreparedStatementUtil.executeQuery(ps);
                try {
                    while (rs.next()) {
                        String name = rs.getString("NAME");

                        String execSql2 = "DROP VIEW \"" + name + "\"";

                        try {
                            logger.debug(execSql2);
                            PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, execSql2);
                            try {
                                PreparedStatementUtil.executeQuery(ps2);
                            } finally {
                                StatementUtil.close(ps2);
                            }
                        } finally {
                        }
                    }
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                } finally {
                    ResultSetUtil.close(rs);
                }
            } finally {
                StatementUtil.close(ps);
            }
        } finally {
            ConnectionUtil.close(conn);
        }
    }

    /**
     * TRIGGER 削除
     */
    private void dropTrigger() {
        String execSql =  "select  trigger_name  NAME from USER_TRIGGERS";

        Connection conn = DataSourceUtil.getConnection(jdbcManager.getDataSource());
        try {
            logger.debug(execSql);
            PreparedStatement ps = ConnectionUtil.prepareStatement(conn,
                    execSql);
            try {
                ResultSet rs = PreparedStatementUtil.executeQuery(ps);
                try {
                    while (rs.next()) {
                        String name = rs.getString("NAME");

                        String execSql2 = "DROP TRIGGER \"" + name + "\"";

                        try {
                            logger.debug(execSql2);
                            PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, execSql2);
                            try {
                                PreparedStatementUtil.executeQuery(ps2);
                            } finally {
                                StatementUtil.close(ps2);
                            }
                        } finally {
                        }
                    }
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                } finally {
                    ResultSetUtil.close(rs);
                }
            } finally {
                StatementUtil.close(ps);
            }
        } finally {
            ConnectionUtil.close(conn);
        }
    }
}

ここではGenerateEntityCommandを参考にしてSQLを実行しています。詳細はS2JDBC-Genのソースを見て調べることになります。

内容的にはテーブルであれば、テーブルの一覧を取得してすべてのテーブルを削除しています。

タスクの登録

s2jdbc-gen-build.xmlにタスクを登録します。

  <taskdef resource="s2jdbc-gen-task.properties" classpathref="classpath"/>

  <taskdef name="clean-database" classname="org.seasar.extension.jdbc.gen.task.CleanDatabaseTask" classpathref="classpath"/>

s2jdbc-gen-task.propertiesの下ぐらいに追加します。

<target name="migrate">
    <clean-database
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      javafiledestdir="${javafiledestdir}"
      javafileencoding="${javafileencoding}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
    />

migrateタスクの一番先頭でこのタスクを呼び出します。これで最初にデータベースの内容をすべて消してから0001のSQLを順に実行していきます。

migrateの実行

既存のテーブルなどをすべてDROPしてからCREATE TABLEしていくはずです。完了した後にテーブルのデータなどが正しく入っていれば成功です。この際に当初の構造と同じかを確認して、違っていたらEntityの編集などを行い修正を行います。

Eclipse上のログなどを確認し、エラーが出ていないかも確認することが重要だと思います。

SCHEMA_INFOの削除

データベースのリビジョン管理用のテーブルが作成されますが、邪魔であれば999-userなどにDROPするSQLを作成して入れておきましょう。

/neko-db/db/migrate/0001/create/999-user/drop_chema_info.sql

DROP TABLE SCHEMA_INFO;

環境別データ作成

  <target name="dump-2">
    <dump-data
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      applyenvtoversion="${applyenvtoversion}"
      entitypackagename="${entitypackagename}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      applyEnvToVersion="true"
    />
    
    <refresh projectName="${projectname}"/>
  </target>

上記のようにdumpタスクを編集して、applyEnvToVersionを有効にすると

[db]
+-[migreate]
    +-[0000]
    +-[0001]
    +-[0001#ut]
       +-[create]
          +-[040-dump]

とデータのみ別のディレクトリに保存されます。同じ用にloadタスクを作ると通常と、#utで2つのデータを使い分けることができます。通常は無印で作業を行い、パフォーマンスチェック用にデータ件数が多い物を#utに置いておくことなどができます。この名前はenvで制御していますので、もっとたくさんの種類のデータを準備することもできます。

この場合にはまずはmigrateを実行して、テーブルなどを最新の状態にしてからload-2で別環境用のデータを利用する手順になります。

総括

データベースの構成管理は結構導入が大変です。ただし一度できてしまえば比較的横展開はしやすいと思います。また、メリットもたくさんあるので、ぜひ使って見ましょう。また、導入途中でS2JDBC-Genのプロジェクトがおかしくなったり、データベースが破壊されたりしますのでおかしいなと思ったらプロジェクトの作り直しや、データベースのリストアなどをして何度か壊す覚悟で作業を進めてください。

またDBFluteを利用している場合にはS2JDBC-Genではなく、DBFluteにも構成管理がありますのでそちらの利用をお勧めします。

2009-08-18

Teedaでの開発ポリシー

| 00:06

私が思っている小規模から中規模向けのTeeda開発ポリシーです。

大規模はそもそもSAStrutsを(あわわ)

前提の前提

オフィシャルサイトの『現場で役立つ実践Teeda』(http://teeda.seasar.org/ja/presentations.html)が標準的な開発環境をきれいに記述しているドキュメントになります。

全体的にここのプレゼンテーション資料は非常に質が高いので、すべて目を通しましょう!

前提

データベース周りの処理はDBAが担当、ロジックはロジック専用の人が担当、画面は画面専用の人が担当っていう階層わけした開発用ではありません。機能ごとでの分担を前提としています。

また、デザインパターンを意識しないでいます。そもそもデザインパターンは作っていて問題がでたから導入する流れが好ましいと思っているので、無駄に複雑なデザインパターンありきで実装をするのもどうかと思っています。なので、Strutsなどで昔からJavaで開発していた人には違和感があると思いますし、規模の大きな開発には向かないと思います。

設定をすれば変えられる物も極力デフォルト設定で使うようにしています。

レイヤーとコンポーネントの取捨

最小の状態から検討していきます。

ページクラスって必要ですか?

必要です。無いと表示できないですもんね。

アクションクラスって必要ですか?

んー、微妙ですが、無くても大丈夫かな?

画面ごとに作ったり、作らなかったりをするのであればすべて作らない方がすっきりすると思います。個人的には作ったことありません。すべてのページで大量の入力項目がある場合などはすっきりするので別けたほうがいいと思います。あとはsetter、getterがある場合。。。

サービスって必要ですか?

これは必要だと思います。デフォルトではService以下の処理はトランザクションがかかります。トランザクションを利用しない場合にはページクラスのみで処理できますが、複数のページから呼ばれる処理はサービスに定義した方がいいと思います。

ロジックって必要ですか?

よほど大掛かりなシステムでなければ必要ないと思っています。基準としてはサービスの中身がほぼ空っぽで、すべてロジックで処理を行っている場合などは別れている必要がないと思います。個人的にはサービス+ロジックをサービスでまかなっています。

Daoって必要ですか?

データベースを利用する上では必要ですね。

Entityって必要ですか?

データベースを利用する上では必要ですね。

Dtoって必要ですか?

forEachの処理があるので必要になってくると思います。

Dxoって必要ですか?

んー、個人的にはあまり使いたくない機能です。便利ですがEntityに少し項目が増えた程度であればそのままDaoにつっこんだり、データをループして変更点のみ更新するような処理を作ったりします。

Dxoで変換後に、自動変換できない部分があって手で詰め替えを行うのであれば、最初から専用の更新用メソッドでフィールドごとに処理がわかるようにした方がいいと思います。

開発思想など

モックって必要ですか?

個人的には嫌いな機能です。たしかに便利ですが、単体テスト終わっているけど結合テストしたらぼろぼろだっていうプロジェクトの原因の一つがモックだと思います。

たとえば新規登録時にログインIDが既存で使われていないかなどのチェックの場合、モックを作るより実際にDBに問い合わせた方が早いと思います。

個々は動いているように見えても、都合のよいデータをモックに与えて動いている場合も多いので、注意しましょう。。。

Teedaはページ駆動のフレームワークですので、外部システムとの連携など相手がある物以外は基本的には実際に動かして開発を進めるスタイルがいいと思います。

インターフェースって必要ですか?

設計上絶対に必要であれば使ってもいいと思います。デザインパターン的に必要だからって作るのであれば無駄なコードだと思います。

インターフェースと実態が1対1でしか存在しないケースの場合、ページクラスから特定のDaoのメソッドを呼び出すまでに、サービスのインターフェース、サービスの実態、ロジックのインターフェース、ロジックの実態、Daoのメソッドとソースを追加していくのはどうも好きになれません。

ただし、テストなどでのメリットもあるはずですので、階層が浅い場合には利用してもよいと思います。上記の場合には処理によりますが個人的にはページクラスからDaoを直接操作した方が直感的だと思います。(インターフェース関係ない。。。)

テストケースって必要ですか?

もちろん、無いよりあった方がいいです。ただしここに無駄に時間をかけているのはどうかなと思います。テストをするのであれば本当に必要なことをテストしましょう。

都合のいいデータばかりを使っているテストケースで、データのカバレッジも足りなくてまったく意味無いけれど、作業時間はかかっているものを見たことがあります。

テストケースは100%動くけれど、実際に触るとぼろぼろだったりするんですよね。。。一般的にJava系のエンジニアは実際に画面をさわるテスト工数が足りない気がします。前後の機能の流れまでの確認が抜けていて、自分の担当が動いていればOKって感覚がどうも好きになれません。

モンキーテストと呼ばれるフリーのテストをまったくしない場合もあったりして、テストケースを作るのはいいけれど、ケース漏れにはどう対応するのって思ったりします。

Webコンテンツ系の開発の場合には使い勝手や反応時間まで含めて、早めに実際に動かしていくのが重要だと思います。そもそも仕様と違うけれど、ものすごく作りこんだテストケースって負の遺産ですよね。。。

Seleniumのような外部からのテストも非常に有効だと思います。

ページクラスに処理を書いちゃだめ?

複数のページクラスで使う処理はサービスにまとめて、それ以外はページクラスで処理するようにしています。Windows系アプリの開発をやっていたかもしれませんが、その方が見通しがいいんですよね。

トランザクションが必要なものや、エディタ上で1ページに収まらないような複雑な処理はサービスに分離しています。

Javadocは書かないとだめ?

エンジニアのためのJavadoc再入門講座(ISBN:9784798119489)をお勧めします。

ただし、この本もどちらかというとモジュール単位での大規模開発向けドキュメントなので、Javadocに使っちゃだめと書いておけばエラーが起きても自分の責任じゃないよっていうスタンスなので、その辺はいいところだけ取り入れましょう!

とくにJavadocに関係ないソース部分のコメントは必要ないと書いてありますが、保守をしていく上では重要な場合もありますのでなるべく入れたほうがいいと思います。逆にメソッド名を単に日本語化したコメントは必要ないとあり、書くのであればどんな値を許容するのかなども書きましょう!

公開しない関数なども保守をしていく上では、必要なことも多いので書いた方がいいですね。ただし必要以上に書く必要はないと思います。過去にすべてのsetterとgetterにプロパティー名の日本語訳がそのまま入っているだけのJavadocコメントを見たことがありますが、あれは無駄ですね。。。

無駄なコメントを書くのであれば、必要なコメントを充実させましょう!

プロパティはpublicじゃだめ?

画面上にものすごくたくさんの入力項目などがあるとソースのほとんどがsetterとgetterになったりしますよね。setterとgetterでなにか必要な処理をしているのであれば仕方ないですが、単に自動生成できるものを書いているのであればpublicですっきりした方がいいと思います。

Checkstyleって必要?

使ってもいいと思いますが、使うのであればちゃんと使っているJavaのバージョンにあったものを利用してください。Java5を利用しているのに古い物を使っていて、総称型とかの警告は無視するとかはやめてください。

社内規約って必要?

すべてに絡んできますが、社内規約は最低限のものにするか、ちゃんと更新していきましょう!

とはいえ、社内規約ってものすごく規定するの面倒ですよね。そして変更も面倒なので古いまま残ってしまう。コードスタイルとかは社内規約じゃなくってEclipseの標準の書き方でもいいと思うんですよね。

単体テストってどこまで?

たとえば新規ユーザー登録の場合、画面から実際に登録してDBにデータが書き込まれて、あればメールが届くまでが私は単体だと思っています。

たぶん、一般的なJavaでいう単体テストじゃなくって結合テストになるのかな? ただTeedaであればメールはちょっと面倒だけれど、それ以外はすぐに組めるのでそこまで作った方がトータルでは早いと思うんですよね。

DBのスキーマー管理って必要?

絶対に必要だと思います。

ソリューションとしてはS2JDBC-Genを私はお勧めします。理由としては、Teedaは通常S2Daoを利用しているので、スキーマー管理のプロジェクトを別けることになるからです。

もちろんDBFluteを内部で利用しているのであれば、DBFluteがベストだと思います。ただしS2Daoの機能しか使っていなくってDBFluteでスキーマー管理になるとプロジェクト内に開発では利用しないファイルが増えることになりますので、ちょっと開発者が混乱しますよね。

なので、思い切って別プロジェクトに別けてEntityとかも二重管理にします。こうすることで本体のプロジェクトは余計なファイルが入らなくてすっきりします。S2JDBC-Genの使い方は去年の日記がありますが、ちょっと内容が古いので今度書き直したいと思います。

DBにはコメントを入れましょう!

絶対必要!

開発に利用しているツールによると思いますがA5:SQL(http://www.wind.sannet.ne.jp/m_matsu/developer/a5m2/)などコメントがわかるSQLエディタを利用している場合には非常に便利です。CSE(http://www.hi-ho.ne.jp/tsumiki/)も便利なのですが、コメントが見えないんですよね。

あとはDB定義書はDBのスキーマー情報から自動生成とかしているので、コメント入れないと日本語名でないんですよね(笑)

ER図って必要?

ケースバイケースですよね。私は見ません。DB定義書をみて頭の中でSELECTとかの検索がうまく連結するのかを考えます。私は脳みそ上のオンメモリで考えるので、100人月以上とかのでかいプロジェクトは苦手なんですよね(苦笑)

とはいえ、視覚化して頭に入れる人もいると思いますので、メンバーが必要であれば作りましょう。

プロトタイププロジェクトって必要?

私は要件定義の間から、プロトタイプ作ってデータ構造とかの検証をします。必要なデータ量より多目のデータを入れて、検索などのレスポンスが大丈夫かを検証したり、コアなデータ構造がきれいに処理できるかを検証します。

あとは、実際の実装メンバーが確定したときにプロトタイプのソースを見てもらって、こんなのを作るんでフレームワークの仕組みと、プロジェクトの概要を勉強してねと渡します。

多くの場合プロトタイプを作ったときに細かい改善点が見えますので、プロトタイプは捨てて新しいプロジェクトで組み始めたりします。インターフェースなども実際に触って検証しないとなかなか使い勝手がいいものはできないですからね。

DIの明示的指定って必要?

通常必要ありません。指定するとHot Deployが効かなくなります。

AOPって必要?

本当に理解していて、必要があれば利用してもよいと思います。同じぐらいの労力で他の解決方法があれば従来の方法の方をお勧めします。

そりゃないぞっていう実装

Dxoが複雑

Daoからロジック用のデータ構造で取得して、Dxoで画面用のデータ構造に変換。しかもロジック用と画面用で微妙にデータ構造が違う。。。

担当者が違っていて、別々に作っているとこうなるって実装ですね。しかもDxoで変換できない物があって失敗したものを詰め替えている。

ロジック部分はコード値のみを持っていて、画面部分は文字列しか持っていなくって更新時に文字列からコード値を逆変換するとかマジやめてほしいです。

ほぼ同じ構成のはずなのにカラム名が違う

A会員、B会員、C会員とあってほとんど同じカラム構成だけど少し違うテーブル設計。共通部分のはずのカラム名が微妙に違う。。。

設計者が違うのか?

Dxo(+手変換)を渡るうちに名前が変わるってのも厄介です!

モックでしか動かないコード

モックの中だけであとで必要になる値を設定しているんだけど。。。

モジュール単位で開発をしていて、動かない場合にはモックに手をいれて動かしちゃうとこうなりますよね。

総括

Teedaは非常にさくさく実装できるフレームワークだと思います。実際にDBと連動して画面を動かすってところまでがS2Daoの利点もあって、非常に手軽に動かすことができます。

まずは作って動かしましょう!

とはいえ、古のStrutsをメインで開発してきたメンバーが、古い開発規約を元に実装をするとものすごく面倒なものができあがります。サクサク感がまったくなく、ロジックをちょっと触るとTomcat再起動が必要になったりします。(とくにSeasar2.3などからSeasarを触っていたりするとね)

個人的にはJavaの古参プログラマよりは、他の言語から入ってきた人や、新人プログラマが過去のしがらみがなく使えば非常に便利だと思います。過去のしがらみがある環境の場合には、SAStrutsを普通のStrutsより楽に使えるフレームワークとして使った方がいいと思います。

あと、ここに書いてあることは結構偏っている意見なので、そのまま鵜呑みにしないでください(笑)

jflutejflute 2009/08/19 01:02 こんばんは

> あと、ここに書いてあることは結構偏っている意見なので、
> そのまま鵜呑みにしないでください(笑)
と、ありますが、とても良い記事だと思うので少しだけ発展させて下さい。
(自分もTeedaを使って二案件こなした経験があります)

> サービスって必要ですか?
自分がTeeda使ったときは、Pageクラスからトランザクションを
掛けて、サービスのようなもう一つのレイヤは無しでやりました。
Pageクラス自体がWebに依存しないPojoなので、実装上困ることは
ほとんどなく、逆に、入力値をサービスに渡すときにDTOに詰めたり
とかを考える必要もなく、実開発では非常にスムーズな
実装をディベロッパーに展開することができました。
複数の画面で共通の処理はロジックを作りました。
まあ、それがサービスとも言えるかもしれませんね...たんなる名前の問題で。

> Dxoって必要ですか?
自分は一回も採用しませんでした。ちょっとの時間を稼ぐために
目に見えにくいトラブルのリスクを抱えるのは意味がないと思って。
実際に使ってるプロジェクトでここでトラブってるのを見たことあります。

> モックって必要ですか?
「単体テスト終わっているけど結合テストしたらぼろぼろだ」
っていうのにとても共感します。
しっかり業務的に整合性の取れたテストデータを作って(DBAとか)、
それで一気通貫でJUnitテストを実装するのが良いと考えています。
そして、Pageクラスのinitialize()やdoXxx()だけをテスト書く
ようにしました。その他の細かいメソッドは割り切りで書きません。
テストを書くコストがあるので、費用対効果として一番効率が良い
だろうという判断でそうしました。品質は良かったですね。

> DBにはコメントを入れましょう!
同感です。苦労はしましたが、DBFluteでは
http://d.hatena.ne.jp/jflute/20081028/1225120253
こんな風にDBコメントを活用しています。

> ER図って必要?
個人的には主にコミュニケーションツールとして大事かなと思います。
複数人でER図を見ながら業務仕様の食い違いがないようにします。
ER図無いプロジェクトで言葉だけで言い合っていて、結局意思疎通
出来てなかったってのを何度も見たことがあります。

akiranekoakiraneko 2009/08/19 09:21 こんにちは!

>> サービスって必要ですか?
>自分がTeeda使ったときは、Pageクラスからトランザクションを
>複数の画面で共通の処理はロジックを作りました。

PHPとかのフレームワークの場合ロジックがなくてサービスって名前だったりするので、サービスで私はやっています(笑)
でもトランザクションかけちゃだめな処理って少ないと思いますしページでもいいですよね

>> Dxoって必要ですか?
>目に見えにくいトラブルのリスクを抱えるのは意味がないと思って。

黒魔術的なところがありますよね。。。

>> モックって必要ですか?
>しっかり業務的に整合性の取れたテストデータを作って(DBAとか)、
>それで一気通貫でJUnitテストを実装するのが良いと考えています。

コストメリットって大切ですよね
私は複数人でのソースコードチェックにわりと時間を割り振っています
新人が多いので、教育のために他の人のソースを読んだり、添削してあげたりとか必要なんで。。。

>> DBにはコメントを入れましょう!
>同感です。苦労はしましたが、DBFluteでは

あっ、いい感じですね!

DBFluteも使ってみたいんですが、導入コストを考えて少し悩んでいます
ある程度Teedaの経験者がいるプロジェクトだと使ってみたいのですが、毎回初心者ばかりだとどうもチャレンジできないでいます

>> ER図って必要?
>個人的には主にコミュニケーションツールとして大事かなと思います。

普通必要ですよね。。。
私+新人PGって構成で動くことが多いので、最初にコアな部分だけER図に起こして構成の説明ぐらいはしたりします。

更新されていないER図に意味はないので、更新できない書類は開発ドキュメントに含めないようにしています。

でもちゃんとメンバーとかで、話してどこまでちゃんと作るかは決めた方がいいですよね

jflutejflute 2009/08/19 09:54 > 更新されていないER図に意味はないので、更新できない書類は
> 開発ドキュメントに含めないようにしています。
なるほど、やり方次第ですね。
自分(や自分の周り)はDDL文の作成をERDツールから自動生成するようにしています。
なので、DB変更時にERDが更新されないで古くなるってことがないです。

> DBFluteも使ってみたいんですが、導入コストを考えて少し悩んでいます
http://dbflute.sandbox.seasar.org/contents/introduction/basic.html
ここの「全体最適化」にも書いてある通り、確かにアーキテクトの頑張りが必要です。
ただ、「+新人PG」という状況はある意味DBFluteがばっちりハマるケースです。
もし気が向いたらなんでも気軽に聞いて下さいね。

akiranekoakiraneko 2009/08/19 10:07 本当はERDツールとか使えればいいんですが、なかなか無料のだと使い勝手がいいのが少ないのと、もともとリレーショナルってよりデータストレージとしてDBを使っているので、GoogleのBigTable的な使い方になっています(苦笑)

外部制約もつけないので。。。

あとはJava言語自体はアプレットでJDK1.0と1.3の経験があったのですが、サーブレット未経験で、O/Rマッパー自体も初めてだったので(苦笑)

次は検討をしてみたいのですが、言語が変わりそうなんですよね。。。

jflutejflute 2009/08/19 10:50 > なかなか無料のだと使い勝手がいいのが少ない
ないですね。。。確かに。
自分はEAという2万くらいのツールを使っています。「UMLツール + ERDツール」という感じで
UMLも書けるので他の有償ツールに比べたらリーズナブルですね。

> もともとリレーショナルってよりデータストレージとしてDBを使っている
なるほど、まあしたらまたちょっと状況が違うシステムならしょうがないですね。

> 次は検討をしてみたいのですが、言語が変わりそうなんですよね。。。
ありがとうございます。一応、DBFluteはC#版(DBFlute.NET)もありますので。

yone098yone098 2009/08/19 17:31 補足ですがTeedaは2.4からですー

akiranekoakiraneko 2009/08/19 17:35 >jfluteさん

有料のソフトってFlashぐらい開発に必要じゃないとなかなか購入って難しいですよね
コスト考えるとエクセル使ってがんばるより安くなったりするんですが。。。

>yone098さん

ありゃ
すみません、こっそりTeedaをSeasarに書き換えてみました(笑)

2009-08-09

更新作業失敗

| 15:34

最新版のwarファイルに置き換えて、サーバー再デプロイっと。あっ、カラムが追加されているんだったとSQLを実行。

むむっ、追加したカラムにデータ入れても反映しない。。。

当たり前ですが、DBのスキーマ情報キャッシュとかしているので実行してから変えちゃだめなのよね。。。サーバー再起動したら反映しました。公開していないサーバーだからって手順書とか作らないで作業すると、工程前後してだめっすね。

2009-07-30

S2Chronosを使ってみた

| 23:06

S2Chronosは,Seasar2に対応したJavaオブジェクトスケジューリングフレームワークです.

というわけで、S2Chronosを利用してみました。

http://s2chronos.sandbox.seasar.org/ja/index.html

特徴

S2ChronosはSeasar2の機能がそのまま利用できるのでs2daoとかがDIされ、Webアプリを利用しているのと同じ感覚で組むことができます。

導入

オフィシャルサイト(http://s2chronos.sandbox.seasar.org/ja/install.html)をみて導入します。導入はおそらくそれほど問題ないと思います。

利用方法

@Task
@CronTrigger(expression = "0 */1 * * * ?")  // 1分ごとに実行
public class BasicTask {
}

個人的には一番利用するのがクーロン型のタスクだと思います。LinuxなどのCronとの一番の違いは秒単位での実行が可能になっています。*/15などの指定で15秒間隔での実行が可能です。ただし0秒、15秒、30秒と実行されていくのではなく、前回のタスクが完了してから15秒後となりますので、インターバルでの実行と考えた方がよいと思います。

設定ファイルの外だし

現状のままですとアノテーションで指定していますので、本番サーバーは1日一回実行するが、テストサーバーは毎時間実行するなどの制御ができません。

@Task
public class BasicTask {
    private String cronStr = ResourceUtil.getProperties("s2Chronos.properties").getProperty("BasicTask");
    private CCronTrigger trigger = new CCronTrigger(cronStr);

    public TaskTrigger getTrigger(){
    	return trigger;
    }

    public void doExecute() {
        // 実際の処理
    }
}

こんな感じでプロパティファイルから設定をロードしています。

利用しているダイナミックトリガーの実装について(http://s2chronos.sandbox.seasar.org/ja/userguide.html)を参考にして上記のような設定を行います。

BasicTask=*/15 * * * * ?

プロパティファイルには上記のように記述します。あとはmvnなどの機能を利用して、パッケージを作る際に組み込まれるリソースファイルを変更することで環境別の設定を行うことが可能です。

余談

毎時ぐらいの実行であればTeedaなどで実装したページをwgetで取得して実行していたりします。Taskでタイミングを管理するとぼーっと0分になるまで待っていたり、ブラウザで動かした場合と違う挙動をしたりすることがあるからです。

    public String prerender() {
        try {
            // 実際の処理
        } catch (Exception e) {
            //500(INTERNAL_SERVER_ERROR)発生
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            facesContext.responseComplete();
        }

        return null;
    }

wgetでも上記のようにエラー時には500とかのステータスを返せば、呼び出し側で制御をすることができます。JP1など外から制御するパターンは上記の組み合わせの方が相性が良い気がします。

HOT reloading利用時の注意点

Taskは便利なのですが、HOT reloadingで利用はお勧めできません。なぜならTeedaでページを開いている瞬間に同じクラスを利用しているTaskが動くとHOT reloadingが動き、Teedaで利用していたクラスが破棄されたりします。

これは防ぐことができません。また混在環境の場合にはTaskはログを頻繁に出力したりするので邪魔になります。。。

COOL Deplyにするか、クラスの明示的リロード(http://www.seasar.org/wiki/index.php?SeasarUpdateOperationLog#j39e75d1)を使うかのどちらかがいいと思います。

私は。。。プロジェクトを分離しました!

Taskの同時実行について

原因は特定できていませんが、複数のTaskが同時に動くとTaskが止まる場合があります。エラーは出なかったと思うのですが、以後一切Cronのタスクが実行されませんでした。

ポーリング系の常に短いタームで動いているタスクと、バッチ処理などで高負荷で長時間動くTaskがある場合には、同時に実行して大丈夫かを検証する必要があります。

S2Chronosが原因ではなくて、スレッドセーフじゃない処理がどこかに入っていたのかもしれません。

DBのセッションについて

S2Chronosではなく、S2Dao側の特徴になりますが、トランザクションを利用していない場合にはDBのコネクションが変わる可能性があります。

    dao.insert( data );
    // ここで他のアクセスがあると。。。
    int lastId = dao.lastInsertId();

上記のようにシーケンスを利用したInsertで、最後に登録したIDを取得しようとした場合、InsertとlastInsertIdの間で他の処理が走ると別のDBコネクションが利用される場合があります。

この辺はDBのコネクションプーリングの機能なので仕方ないのですが、意図しない番号やInsertしていないので番号無いなどのエラーが発生する可能性があります。

上記のような作業はServiceクラスを利用して、トランザクションがかかった状態で実行しましょう!

通常のWebアプリでも起こる問題ですが、DBへのポーリング処理などを行っていると非常に発生しやすく原因がわかりにくいバグになります。

感想

非常に便利です!

でもはまりました。。。

開発環境の場合にはTaskのログを出さなくする、taskScanIntervalTimeの値を大きくしてあまり実行されなくする、必要なファイルを環境にいれないようにして実行させない。。。など通常の開発に影響がでないようにしたほうが良いと思います。

お勧めはWebアプリとTaskを別プロジェクトに分離するってのですが、そうすると共通クラスとかが気軽に使えないってことですね。

ただし最初は普通に同居してみて問題があれば分離という方針がいい気がします。Taskのログがどんどん出力されて、見たいログが流されていくのが不便に感じるとは思いますが(苦笑)

ドキュメントを読む限り、もっと高度なことができると思いますが、どう組み合わせて使えばよいのかがわからないのと、タスク管理はJP1を利用していたので、今回は単純なCron設定のみで利用しました。もう少し細かい機能の使い方事例があると有効に使えそうですね!

細谷 淳一細谷 淳一 2010/09/08 14:15 お忙しいところ大変恐縮ですが、
1点、ご教授いただきたい事項がございます。
現材、以下の環境で開発を行っております。
JDK1.6、TOMCAT5.5、SAStruts
WEB経由でタスクのリスケジュールを行っているのですが、
env.txtの値をctからproductに変更したところ動作しなくなりました。
(具体的にはタスククラスのインスタンスが起動しない)
COOL DEPLOYではaddTaskメソッドが動作しないということなのでしょうか。
ソースコードは以下となります。

<schedule>
<tasks class="java.util.ArrayList">
<task name="SokushinMailTask" type="interval">
<start>2008/07/22 06:12:12</start>
<end>2008/07/23 17:12:12</end>
<cron>0 21 09 11 08 ? *</cron>
<time></time>
<interval>0</interval>
</task>
<task name="ReflectionTask" type="interval">
<start>2008/07/22 06:12:12</start>
<end>2008/07/23 17:12:12</end>
<cron>0 21 09 11 08 ? *</cron>
<time>2010/07/30 19:43:12</time>
<interval>1</interval>
</task>
</tasks>
</schedule>

public class TriggerFactory {
TaskTrigger trigger;
public TriggerFactory(String kind){
//時間範囲トリガ
try{
//Task設定XMLファイルの読み込み
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = builder.parse( ResourceUtil.getResourceAsFile("s2chronos.xml") );
//値取得の準備
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
String type = xpath.evaluate("/schedule/tasks/task[@name='"+kind+"']/@type", doc);
System.out.println("["+kind+":"+type+"]");
if(type.equals("interval")){//時間間隔を伴ったクーロン形式の指定
String start = xpath.evaluate("/schedule/tasks/task[@name='"+kind+"']/start", doc);
String end = xpath.evaluate("/schedule/tasks/task[@name='"+kind+"']/end", doc);
String cron = xpath.evaluate("/schedule/tasks/task[@name='"+kind+"']/cron", doc);
System.out.println("cron="+cron);
System.out.println("start="+start);
System.out.println("end="+end);
ScopedTimeTrigger trigger = new ScopedTimeTrigger(new CCronTrigger(cron));
trigger.setScopedStartTime(DateFormat.getInstance().parse(start));
trigger.setScopedEndTime(DateFormat.getInstance().parse(end));
this.trigger = trigger;
}else if(type.equals("cron")){//クーロン形式の指定
String cron = xpath.evaluate("/schedule/tasks/task[@name='"+kind+"']/cron", doc);
System.out.println("cron="+cron);
CCronTrigger trigger = new CCronTrigger(cron);
this.trigger = trigger;
}else if(type.equals("time")){//特定の時間を指定
String time = xpath.evaluate("/schedule/tasks/task[@name='"+kind+"']/time", doc);
System.out.println("time="+time);
CTimedTrigger trigger = new CTimedTrigger();
trigger.setStartTime(DateFormat.getInstance().parse(time));
this.trigger = trigger;
}
}catch(Exception e){
e.printStackTrace();
}
}
public TaskTrigger getTaskTrigger(){
return this.trigger;
}
}

@Root
public class Schedule {

@ElementList
List<Task> tasks;

public List<Task> getTasks() {
return tasks;
}

public void setTasks(List<Task> tasks) {
this.tasks = tasks;
}
}

@Root
public class Task {
@Attribute
String name;
@Attribute
String type;
@Element(required=false)
String start;
@Element(required=false)
String end;
@Element(required=false)
String cron;
@Element(required=false)
String time;
@Element(required=false)
int interval;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getStart() {
return start;
}
public void setStart(String start) {
this.start = start;
}
public String getEnd() {
return end;
}
public void setEnd(String end) {
this.end = end;
}
public String getCron() {
return cron;
}
public void setCron(String cron) {
this.cron = cron;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
public int getInterval() {
return interval;
}
public void setInterval(int interval) {
this.interval = interval;
}
}


public class ScheduleAction {
@Resource
public Scheduler scheduler;

@ActionForm
@Resource(name = "scheduleForm")
protected ScheduleForm form;

@Execute(validator=false)
public String index(){
//Task設定XMLファイルの読み込み
try{
Serializer serializer = new Persister();
File source = ResourceUtil.getResourceAsFile("s2chronos.xml");
Schedule schedule = serializer.read(Schedule.class, source);
List<Task> taskList = schedule.getTasks();
for( Task t : taskList){
if(t.getName().equals("ReflectionTask")){
form.type1 = t.getType();
form.time1 = t.getTime();
form.cron1 = t.getCron();
form.start1 = t.getStart();
form.end1 = t.getEnd();
form.interval1 = t.getInterval();
}else{
form.type2 = t.getType();
form.time2 = t.getTime();
form.cron2 = t.getCron();
form.start2 = t.getStart();
form.end2 = t.getEnd();
form.interval2 = t.getInterval();
}
}
}catch(Exception e){
e.printStackTrace();
}
return "index.jsp";
}
@Execute(validator=false)
public String reSchedule(){
try{
Serializer serializer = new Persister();
File source = ResourceUtil.getResourceAsFile("s2chronos.xml");
Schedule schedule = serializer.read(Schedule.class, source);
List<Task> taskList = schedule.getTasks();
for( Task t : taskList){
if(t.getName().equals("ReflectionTask")){
t.setType(form.type1);
t.setTime(form.time1);
t.setCron(form.cron1);
t.setStart(form.start1);
t.setEnd(form.end1);
t.setInterval(form.interval1 );
}else{
t.setType(form.type2);
t.setTime(form.time2);
t.setCron(form.cron2);
t.setStart(form.start2);
t.setEnd(form.end2);
t.setInterval(form.interval2);
}
}
serializer.write(schedule,source);
scheduler.removeTask(SokushinMailTask.class);
scheduler.removeTask(ReflectionTask.class);
System.out.println("スケジュールの削除");
scheduler.addTask(SokushinMailTask.class);
scheduler.addTask(ReflectionTask.class);
scheduler.start();
System.out.println("スケジュールの登録");
}catch(Exception e){
e.printStackTrace();
}
return "result.jsp";
}
}
@Task
public class ReflectionTask {
TaskTrigger trigger = (new TriggerFactory("ReflectionTask")).getTaskTrigger();
public TaskTrigger getTrigger(){
System.out.println("データ反映トリガ発行");
return trigger;
}
// 初期化処理
public void initialize() {
System.out.println("initialize");
}
// 開始処理
@NextTask("taskA")
public void start(){
System.out.println("start");
}
@JoinTask(JoinType.Wait)
public void doTaskB() {
System.out.println("TaskB");
}
@NextTask("taskB")
@JoinTask(JoinType.Wait)
public void doTaskA(){
System.out.println("TaskA");
}
// 終了処理
public void end(){
System.out.println("end");
}
// 破棄処理
public void destroy() {
logger.debug("destroy");
}
}
@Task
public class SokushinMailTask {
TaskTrigger trigger = (new TriggerFactory("SokushinMailTask")).getTaskTrigger();
public TaskTrigger getTrigger(){
System.out.println("促進メールトリガ発行");
return trigger;
}
public void doExecute() {
System.out.println("doExecute");
}
// 初期化処理
public void initialize() {
System.out.println("initialize");
}
// 開始処理
public void start(){
System.out.println("start");
}
// 終了処理
public void end(){
System.out.println("end");
}
// 破棄処理
public void destroy() {
System.out.println("destroy");
}
}

細谷 淳一細谷 淳一 2010/09/08 14:17 大変申し訳ございません。
コメント先を間違えておりました。
上記コメントについてはお手数おかけしますが削除いただければと存じます。

2009-07-29

Teedaのはまりどころ

| 22:30

いろいろとはまりやすいところのTIPSです。

Itemsの要素をJavaScriptで触る

きれいに組めば触れますが、基本的には最大要素分をあらかじめ作っておいて表示、非表示を制御した方が楽だと思います。個数に上限が無いようなデータの場合には追加するボタンでリロードが一番かな?

私はこんな回避でした。

  • 上限が少ない場合には上限分の個数を作る
  • 上限がない場合には現在数+20個などを作り一時保存するとまた増えるようにする

バリテーションエラーでデータが消える

JSF的にエラー時はページクラスの値は更新されないようです。たぶん汚れたデータってポリシーですね。

可能であれば画面側のJavaScriptで細かい入力制限を行い、Teeda側のバリテーションではエラーが起こらないように組んでおくのがよいと思います。

プレゼンテーション資料の「Teeda再考 - 使い方から拡張方法まで -」(http://teeda.seasar.org/ja/presentations.html)に別のバリデーションを使う方法が書いてあるので、こっちを使った方が幸せになれるかも。。。

本番環境だと特定のページが404

テンプレート名の先頭が大文字になっていませんか?

Windows上のTomcatだと動いちゃったりしますがApacheをはさむとエラーになったりします。

メモリ不足になる

COOL deployで動かすと多少改善します!(ちょっと違う)

基本的にメモリはたくさんたべる子なので、ヒープメモリ系の設定としましょう。

-Xmx1024m
-XX:PermSize=128m
-XX:MaxPermSize=256m

開発機など搭載メモリの関係(搭載メモリの25%がMax)で大きな数字が設定できない場合には。。。メモリエラーがでたらVMを再起動します。。。あとEclipse上のTomcatはヒープが少ないので、256mぐらいには増やしましょう!

普通に使っているとあまりメモリ不足にはならないかもしれませんが、HOT deployで全部のページをSeleniumとかで自動的に巡回するようにすると、メモリを消費していくのがわかりやすいと思います。(COOLでも少し余裕をみて設定した方がいいと思います)

データ構造がわからない

Listでの繰り返しとかデータ構造がいまいちわかりにくいですよね。その場合にはTeeda HTML Example(http://teeda.seasar.org/ja/downloads.html)を参考に!

ただし総称型とかでばりばり警告でまくりなので注意しましょう。

	public Class initialize() {
		return null;
	}

デフォルトで絶対にでるのがこのClassの警告ですが

	public Class<*> initialize() {
		return null;
	}

<*>でなんでも大丈夫にします!

総称型の意味ないのですが仕方ないです。。。ページクラスに共通親クラスがあるのであればそのクラス名を指定した方がスマートかな?

public List<Map<String,String>> statusItems;

private void makeList(){
	statusItems = new ArrayList<Map<String,String>>();
	Map<String,String> map;
	StatusList statusList[] = statusListDao.selectAll();
	for( int i = 0 ; i < statusList.length ; i++ ){
		map = new LinkedHashMap<String,String>();
		map.put("value", statusList[i].value);
		map.put("label", statusList[i].title);
		statusItems.add(map);
	}
}

List型などはちょっと面倒ですが、上記のように記述します。

これってTeeda関係ないですが、何を指定していいのかわかりにくくなりますので、Java1.5で組んでいるのであれば初期からちゃんと記述していきましょう!

CheckStyleとか使っていても内部はJava1.4向けだったりして、この警告は無視する設定とかはなるべくやめましょう。

どこみて開発すればいいの?

最初は「Seasar2によるスーパーアジャイルなWeb開発」の書籍を参考にしましょう!

ここに書いてあることで基本は大丈夫だと思います。ただしDoltengのバージョンとかが少し古いので、公式サイト(http://teeda.seasar.org/ja/setup.html)みてセットアップした方がいいかもしれません。

一番便利なのは逆引きリファレンス(http://teeda.seasar.org/ja/extension/reverse/index.html)です。

ここに書いてないことは基本使わない方がいいと思います!

コアな資料はプレゼンテーション(http://teeda.seasar.org/ja/presentations.html)にあります。ここの資料はかなり重要なので目を通しましょう!

ただし、最初は読んでもわからないことがあるので、ある程度理解できたときに読み直すと理解しやすいと思います。

Gridって?

リファレンス(http://teeda.seasar.org/ja/extension_component_reference.html#grid)に記述がありますが、逆引きにはないので、個人的には非推奨の機能です。

便利ですが、細かい調整はできませんのでそのまま使うか、カスタマイズする場合にはJavaScriptなどで同じ動作をするものを作成するか、Teeda Extensionの元のソースを改造するぐらいの気合が必要になります。

個人的には素のまま使うと決めても、あとからやっぱり改造したいと言われるので、最初から選択肢からはずした方が吉!

セッションって?

@Component(instance = InstanceType.SESSION)
public class SessionDto implements Serializable {
}

こんな感じのクラスを作って中身が保存されます。使う場合にはページクラスで宣言してDIしてもらってください。

public class IndexPage {
	public SessionDto sessionDto;
}

クラスの名前とかは何でも大丈夫だと思います。SerializableするのでserialVersionUIDは設定してあげてください。この辺はEclipseで警告でて、解決方法選ぶと作ってくれるはずです。

レイアウトが。。。

レイアウトも少しはまりやすい機能だと思います。

利用する場合には、事前に検証をして大丈夫だなって確認してから利用してみてください。

わからないことはどうすれば?

プレゼンテーション資料によると恵比寿にいくのが一番だと思いますが、なかなか難しいと思いますので、はてなで日記を書くのが一番だと思います。

最近はMLで問い合わせをするより2chで聞くほうが楽と思っている人が多いみたいですね。。。