ActionでFormに対して処理をするときの私のコーディングスタイル

SAStrutsはActionクラスでActionFormを受け取るときは以下のように書く。

@ActionForm
@Resource
public FooForm fooBarForm;

ここで注意したいのが(SAStrutsユーザーなら誰でも知っているが)、ActionFormは名前でインジェクションされるということ。つまり、

@ActionForm
@Resource
public FooBarForm form;

じゃ、ダメだということ。この仕様はS2Containerの自動インジェクションの仕様をそのまま反映したものだと思うのだけれど、個人的には@ActionFormをつけてるんだから型から名前を推測してインジェクションしてくれてもいいのに・・・なんて思う(薄いラッパーであるための理由とかがあるんだろうか?)。

たかだか"fooBar"が余計に付くだけじゃないか!なんて思うかもしれないけど、Actionクラスの名前が長ければ必然と長くなるし、なによりActionクラスでActionFormに対して処理をするときにいちいち"fooBarForm.xxx"と書くのは可読性的にちょっと微妙な気がする。

そこで現状私は以下のようにコーディングしている。

@ActionForm
@Resources
public FooForm fooBarForm;

@Execute(validator = false)
public String bar() {
    final FooBarForm form = fooBarForm;
    ...
}

私は最終的に"form"を採用したけれど、プロジェクト内で統一が取れれば"f"とか一文字にしてしまっても良いかもしれない。

ともあれ、なぜ型でActionFormをインジェクションする仕様にしなかったのかは気になるところ。ちょっとMLを過去ログとか、SAStrutsのソースを見てみようと思う。

Hudson使用時にRangeError: Maximum recursion depth exceeded (form not submitted)がでた時の対処

たぶん、それはあなたがOperaを使用しているのが原因です。あなたがいかにOperaを愛しているかは知りませんが、ChromeFireFoxを使用することでこの問題を避けることが出来るでしょう。

一応情報共有のために書いておきました(笑)

SQLのNOT INはなぜ遅い?

このあいだ聞かれたときにすぐに答えられなかったから書いとく。あとから考えたら当たり前のことだったんだけどね。

結論から言えば、全レコードをシーケンシャルアクセスしなければならないから遅い。

以下、それについて具体的な話を展開するけど、最初に断っておくと私はSQLRDBMSも詳しくないので間違ったことを書いているかもしれない。もし、致命的な間違いなどに気づいた方はコメントしてくれると助かる。

NOT INについて考える

NOT INが遅いのは、RDBMSの内部動作を考えれば当たり前で。

SELECT * FROM Employee WHERE id NOT IN(2, 3);

というSQLは、Employeeテーブルからidが2でも3でもでないレコードを取得しようとしているが、これを達成するためには最初から最後のレコードまでアクセスしてノットイコールの比較を行わないと抽出できない。例外なく最後のレコードまでね。

Java擬似コードを書くと以下のようになる。

for (Employee record : employeeTable) {
    if (record.id != 2 && record.id != 3) {
        resultList.add(record);
    }
}

INについて考える

じゃあINの場合はどうなのか?

SELECT * FROM Employee WHERE id IN(2, 3);

この場合は、Employeeテーブルからidが2あるいは3のレコードを取得しようとしている。

これも最初から最後のレコードまでアクセスする必要がありそうに感じる。実際、特定の条件下では最後までアクセスする必要もある。でも、以下の2つの条件においてはそうはならない。

  • 対象カラムがユニークインデックス(主キーを含む)である場合
  • 対象カラムがユニークである場合

1つ目は2つ目の条件を含んでいる。では、それぞれについてみていこう。

対象カラムがユニークインデックス

ユニークインデックスのカラムはポインタによりアクセスすることが可能である。それはJavaでいうところのHashMapみたいなもので、レコードをシーケンシャルにアクセスする必要がないことを意味する。

Java擬似コードを書くと次のようになる。

if (employeeTable.containsKey(2)) resultList.add(employeeTable.get(2));
if (employeeTable.containsKey(3)) resultList.add(employeeTable.get(3));

見ただけで速いような気がするし、実際HashMapの実装がまずくなければ高速に動作する。幸いなことに殆のRDBMSはこれを高速にやってのける(と思っているのだが)。

対象カラムがユニーク

単なるユニーク制約の場合はポインタによりアクセスすることは不可能である。では、結局最後のレコードまでシーケンシャルアクセスしなければならないのか。そんなことはない。レコードをシーケンシャルアクセスする必要があるのは当然だが、同一の値がないことが保証されているので、INに指定された値にマッチするレコードが全て見つかった時点で処理を完了できるのだ。

先の例で言えば、idが2と3のレコードが見つかった時点で、それ以降のレコードにはアクセスする必要がないことになる。なぜなら、それ以降idが2あるいは3のレコードは登場しないはずだから。

Java擬似コードを書くと次のようになる。

boolean isFoundId2 = false;
boolean isFoundId3 = false;
for (Employee record : employeeTable) {
    if (!isFoundId2 && record.id == 2) {
        resultList.add(record);
        isFoundId2 = true;
    }
    else if (!isFoundId3 && record.id == 3) {
        resultList.add(record);
        isFoundId3 = true;
    }
    if (isFound2 && isFound3) break;
}

まとめ

NOT INはあんまり速くないです。でも、それは主キーやユニークのカラムを対象としたINに比べればというレベルの話で、最後のレコードまでシーケンシャルアクセスが必要な他のSQLと同程度の速度です。

補足

本当に速いか遅いかは実際に使用するRDBMSに依存します。本記事は、一般的に高速に処理することができるアルゴリズムを全てのRDBMSは実装しているであろうという希望的観測に基づいて書かれています。

SAStrutsで1件だけのメッセージをシンプルに追加する

みんな勝手にやってるような気がするけど一応書いてみる。

SAStrutsでリダイレクト後にメッセージを表示したい場合、通常以下のようにすると思う。

ActionMessages messages = new ActionMessages();
messages.add(ActionMessages.GROBAL_MESSAGE, new ActionMessage("key"));
ActionMessagesUtil.saveMessages(session, messages);

で、思うんだけどこういう場合にメッセージを表示するのって殆の場合が1件だけだと思うんだよね。それなのにActionMessagesを用意するのって面倒だなーと。

で、リファクタリングとして1件のメッセージをsaveするActionMessagesUtilEx#saveSingleMessage()を作ってみる。

public class ActionMessagesUtilEx {
    public static void saveSingleMessage(HttpSession session, String property, String key) {
        ActionMessages messages = new ActionMessages();
        messages.add(property, new ActionMessage(key));
        ActionMessagesUtil.saveMessages(session, messages);
    }
}

使う側はこうなる。

ActionMessagesUtilEx.saveSingleMessage(session, ActionMessages.GLOBAL_MESSAGE, "key");

シンプルで意図が分かりやすくなった。

staticインポートを使えばもっとスッキリする。

saveSingleMessage(session, GLOBAL_MESSAGE, "key");

比べてみれば、記述量も分かりやすさも一目瞭然(両方ともstaticインポート使用)

ActionMessages messages = new ActionMessages();
messages.add(GROBAL_MESSAGE, new ActionMessage("key"));
saveMessages(session, messages);

saveSingleMessage(session, GLOBAL_MESSAGE, "key");

補足等

  • saveErrors用のヘルパーメソッドを用意してもよいね
  • ActionMessagesUtilExという名前では依然としてsがついて複数形だけど、ActionMessagesUtilの特別版という意図を残したいのでこの名前。賛否両論?
  • saveSingleMessageじゃなくてsaveMessageでも十分意図は伝わるかも?

SimpleDateFormatで実在日チェックは出来ない

今まで社内のライブラリを使用していたから知らなかったけど、JavaのSimpleDateFormat.parse()で実在日チェックは出来ないのね。

フォーマットが正しくなければParseExceptionがスローされるからOKだと思いがちだけど、以下のようなコードでもParseExceptionはスローされない。

Date date;
DateFormat fmt = new SimpleDateFormat("yyyy/MM/dd");

date = fmt.parse("2010/01/32");
System.out.println(fmt.format(date)); // 2010/02/01

date = fmt.parse("2000/13/-10");
System.out.println(fmt.format(date)); // 2000/12/21

1/32も13/-10もアプリケーション的にはエラーにしたいケースだろうけど、きちんとパースされる。結果をみる限り、内部的に年月日が加減算されている模様。結局、入力された日付をチェックするなら実在日チェック用のメソッドを呼び出す必要がありそうだ。

実在日チェックのコード

ちょっとぐぐってみたら、実在日チェック処理コードは以下のページのものが非常にクールだった。
http://java-house.jp/ml/archive/j-h-b/017541.html

MLのページなのでもし消えてしまうともったいないと思い、自分でも似たようなクラスを書いてみた。

import java.util.Calendar;
import java.util.GregorianCalendar;

public class DateUtil {

    public static boolean isExistsDate(String yyyy, String mm, String dd) {

        try {
            int year = Integer.parseInt(yyyy);
            int month = Integer.parseInt(mm);
            int day = Integer.parseInt(dd);
            return isExistsDate(year, month, day);
        }
        catch (NumberFormatException e) {
            return false;
        }
    }

    public static boolean isExistsDate(int year, int month, int day) {

        Calendar gc = new GregorianCalendar(year, month - 1, day);

        if (gc.get(Calendar.YEAR) != year ||
            gc.get(Calendar.MONTH) != (month - 1) ||
            gc.get(Calendar.DAY_OF_MONTH) != day) {
            return false;
        }
        else {
            return true;
        }
    }
}

基本的な作りは同じで違いといえば

  • 日本の年月日表記なら、引数の月はそのまま(-1せずに)渡せる
  • 実際のアプリケーションで利用しやすいようにString版も用意

くらい。

すばらしいコードを掲示してくださった丸山さんに敬意を払い、このエントリーを終わらせていただきます。

サービス公開「みんなの日程表」

自宅サーバJava製のWebサービスを公開した。
みんなの日程表β

これは以前の私の幹事の経験から作成したもので、みんなの日程の都合をWebで共有できるというもの。幹事になると、みんなの日程の都合を聞いて回らなければならないわけだけど、必ずしも参加者が近くにいるわけではなく、メールや電話で一人ひとりの日程を聞き出さなければならないケースもある。

それは面倒じゃないか!!
ということでこのサービスを作った。

いわゆるスケジュール共有サービスの一種だけれど、特徴として

  • 登録不要
  • シンプル

というのが強みだと思っている。

すぐに試せるんで、よかったら皆さん使ってみてください。

使用している技術等

MySqlを使用時に、S2Unit4で自動的にロールバックが行われない

と思ったら、MySqlのテーブルタイプがMyISAMだったのがいけなかったらしい。

デフォルトだと、テーブル作成時にMyISAMがテーブルタイプとして適用けど、MyISAMトランザクションをサポートしていないらしい。他のテーブルタイプとしてInnoDBというのがあって、これはトランザクションをサポートしているので、これを使えばきちんとロールバックが行われる。

テーブル作成時にInnoDBを指定するやり方

CREATE TABLE xxx (
    ...
) ENGINE=InnoDB;

MyISAMInnoDBの違い

マイコミジャーナルの記事:MySQL、MyISAMとInnoDBを選ぶ方法

MyISAMの方が高速みたいだけど、データの整合性が重要な場合はInnoDBを選択したほうがよさそうだね。よほど性能に気を使わない限り、InnoDBを選択したほうが無難そうだけど、どうなんだろうね?