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のソースを見てみようと思う。
SQLのNOT INはなぜ遅い?
このあいだ聞かれたときにすぐに答えられなかったから書いとく。あとから考えたら当たり前のことだったんだけどね。
結論から言えば、全レコードをシーケンシャルアクセスしなければならないから遅い。
以下、それについて具体的な話を展開するけど、最初に断っておくと私はSQLもRDBMSも詳しくないので間違ったことを書いているかもしれない。もし、致命的な間違いなどに気づいた方はコメントしてくれると助かる。
NOT INについて考える
NOT INが遅いのは、RDBMSの内部動作を考えれば当たり前で。
SELECT * FROM Employee WHERE id NOT IN(2, 3);
というSQLは、Employeeテーブルからidが2でも3でもでないレコードを取得しようとしているが、これを達成するためには最初から最後のレコードまでアクセスしてノットイコールの比較を行わないと抽出できない。例外なく最後のレコードまでね。
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みたいなもので、レコードをシーケンシャルにアクセスする必要がないことを意味する。
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のレコードは登場しないはずだから。
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と同程度の速度です。
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版も用意
くらい。
すばらしいコードを掲示してくださった丸山さんに敬意を払い、このエントリーを終わらせていただきます。