コード値をEnumを使って安全に扱う
例えば会員情報の性別や血液型のように、データベースに定数項を格納する際にコード値(数値)を使う事が多いんだけど、設計書でコード値の対応付け(男性が1で女性が2など)を決めていたとしても、コーディング上も間違いやすく、入力画面を作る際にも対応付けが面倒臭かったりする。そこで、列挙型(Enum)を使用して間違いの起こりにくそうな方法を考える。
サンプルの概要
会員情報を画面から入力し、データベースに格納するような例を考える。今回は動作サンプルなんで、会員情報に含まれる情報は名前と血液型のみとする。
入力はWebでおこない、名前をテキストフィールドで、血液型をプルダウン(セレクトボックス)で入力する。Web側のフレームワークにはStruts 1.3 + S2Struts + Mayaaを使用する。
入力した値はデータベースの会員情報テーブルに格納する。名前はvarcharのカラム、血液型はコード値(0:A型 ~ 3:AB型)をsmallintのカラムに入れる。データベース側のフレームワークにはS2Dao 1.0.50 + PostgreSQL 8.3 (JDBCドライバ)を使用する。
前準備
- PostgreSQLで会員情報テーブルを作成
create table member (
member_id serial,
name varchar(32) not null,
blood_type smallint not null
);
- S2StrutsBlank V1.3.1をベースにしたプロジェクト(EnumSample)を作成し、mayaa関連とPostgreSQLのJDBCドライバのjarをWEB-INF/lib/に追加。
- Rhinoはrhino1_7R2.zipのrhino1_7R2/js.jaに入れ替えた方が良いかも
- j2ee.diconのPostgreSQLの部分を有効にし、接続先を変更。
- convension.diconのaddRootPackageNameを変更
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN" "http://www.seasar.org/dtd/components21.dtd"> <components> <component class="org.seasar.framework.convention.impl.NamingConventionImpl"> <initMethod name="addRootPackageName"> <arg>"sample.enumlove"</arg> </initMethod> </component> </components>
- web.xmlにMayaaServletのエントリを追加し、*.htmlをマッピング
- web.xmlのencodingfilterの文字コードを設定。今回はWindows-31Jに
S2DaoでEnumを使用する
これは簡単で、Dtoのプロパティの型に列挙型を指定するだけ。
DBのカラムが数値型の場合、Enumのordinalの値がコード値として使用される。
package sample.enumlove.model; import org.seasar.dao.annotation.tiger.Bean; import org.seasar.dao.annotation.tiger.Id; import org.seasar.dao.annotation.tiger.IdType; @Bean(table = "member") public class MemberDto { private Integer memberId; private String name; private BloodType bloodType; // ※ 自前で定義した列挙型を使用 public MemberDto() { } @Id(value = IdType.SEQUENCE, sequenceName = "member_member_id_seq") public void setMemberId(Integer memberId) { this.memberId = memberId; } public Integer getMemberId() { return memberId; } public void setBloodType(BloodType bloodType) { this.bloodType = bloodType; } public BloodType getBloodType() { return bloodType; } public void setName(String name) { this.name = name; } public String getName() { return name; } }
列挙型(今回は血液型を入れるBloodTypeというクラス)は下のような感じ。ラベル名が埋め込みなのが嫌ならResourceBundleとか使って下さい。
package sample.enumlove.model; /** 血液型の列挙型 バージョン1 */ public enum BloodType { A ("A型"), B ("B型"), O ("O型"), AB ("AB型"); /** ラベル名 */ private String label; BloodType(String label) { this.label = label; } public String getLabel() { return label; } }
これを読み書きするDaoを用意する。
package sample.enumlove.dao; import java.sql.SQLException; import java.util.List; import org.seasar.dao.annotation.tiger.Query; import org.seasar.dao.annotation.tiger.S2Dao; import sample.enumlove.model.MemberDto; @S2Dao(bean = MemberDto.class) public interface MemberDao { @Query("member_id=?") MemberDto findById(int memberId) throws SQLException; void insert(MemberDto member) throws SQLException; void update(MemberDto member) throws SQLException; void delete(MemberDto member) throws SQLException; List<MemberDto> findAll() throws SQLException; }
書き込み例:
public void testInsert() { MemberDto memberDto = new MemberDto(); memberDto.setName("Test Name"); memberDto.setBloodType(BloodType.B); // B型 try { memberDao.insert(memberDto); } catch (SQLException e) { e.printStackTrace(); } }
実行結果
sample=# select * from member; member_id | name | blood_type -----------+----------+------------ 1 | Test Name | 1 (1 row)
B型がコード値1で格納されている。
ordinalは0開始なので、コード値に0を避けたい場合は、先頭にダミーの要素を増やすなどする必要があるかも。
読み込み例:
public void testFind() { int memberId = 1; // 会員ID=1のレコードを取得 try { MemberDto memberDto = memberDao.findById(memberId); BloodType bloodType = memberDto.getBloodType(); if (bloodType == BloodType.B) { System.out.println("Type B!"); // B型 } } catch (SQLException e) { e.printStackTrace(); } }
S2StrutsでEnumを使用する(入力フォーム表示まで)
入力画面をS2StrutsとMayaaで作ってみる。
まずPOJOアクションフォーム。MemberDtoを拡張して作る。
package sample.enumlove.web.member; import org.seasar.struts.annotation.tiger.StrutsActionForm; import sample.enumlove.model.BloodType; import sample.enumlove.model.MemberDto; /** 会員情報入力用のForm バージョン1 */ @StrutsActionForm(name="memberForm") public class MemberForm extends MemberDto { // セレクトボックス用に血液型一覧を用意 public BloodType[] getBloodTypes() { return BloodType.values(); } }
一覧表示用の配列を出すメソッドをいちいちFormで定義するなら嫌なら、Constantsクラスでも作ってコンテナに放流しとくといいかも。
入力フォームを表示するアクション
package sample.enumlove.web.member; import org.seasar.struts.annotation.tiger.StrutsAction; import org.seasar.struts.annotation.tiger.StrutsActionForward; /** 入力画面を表示するアクション */ @StrutsAction(input=IndexAction.REGISTER, name = "memberForm") public class IndexAction { @StrutsActionForward public static final String REGISTER = "/pages/member/register.html"; // アクションメソッド public String execute() { return REGISTER; } }
BloodTypeに、値取り出し用のメソッドgetValue()を追加。(name()だとELでアクセスできないので)
package sample.enumlove.model; /** 血液型の列挙型 バージョン2 */ public enum BloodType { A ("A型"), B ("B型"), O ("O型"), AB ("AB型"); /** ラベル名 */ private String label; BloodType(String label) { this.label = label; } public String getLabel() { return label; } // [NEW!!] 値取り出し用のメソッドを追加 public String getValue() { return name(); } }
入力ページ(htmlとmayaaファイル)
WebContent/pages/register.html:
<html xmlns:m="http://mayaa.seasar.org" lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=Windows-31J"> <title>登録画面</title> </head> <body> <b>登録画面</b><br> <hr> <form m:id="mainForm"> 名前: <input m:id="name" type="text" size="10"><br> 血液型: <select m:id="bloodTypeSelector"> <option>A型</option> </select><br> <input m:id="submitButton" type="submit" value="登録する"> </form> </body> </html>
WebContent/pages/register.mayaa:
<?xml version="1.0" encoding="Shift_JIS"?> <m:mayaa xmlns:m="http://mayaa.seasar.org" xmlns:html="http://struts.apache.org/tags-html" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mayaa.seasar.org http://mayaa.seasar.org/schema/mayaa_core_1_0_0.xsd" noCache="true"> <html:form m:id="mainForm" action="/member/index.do" method="POST" /> <html:submit m:id="submitButton" value="登録する" /> <html:text m:id="name" size="10" name="memberForm" property="name"/> <html:select m:id="bloodTypeSelector" name="memberForm" property="bloodType"> <html:optionsCollection property="bloodTypes" value="value" label="label"/> </html:select> </m:mayaa>
html:selectにname="memberForm" property="bloodType"のように列挙型のプロパティ名を直接指定する。
この時点で]http://localhost:8080/EnumSample/member/index.do[にアクセスすれば、血液型のセレクトボックスに選択肢(「A型」~「AB型」)が表示される。仮にMemberFormのbloodTypeにBloodType.Bをセットしておけば、セレクトボックス表示時にはB型が選択される。
Strutsはプロパティが未知の型の場合、toString()で文字列化して使用する。列挙型の要素(例えばBloodType.B)のtoString()の値は名称(name()の戻り値である例えば"B")であるため、それがそのまま表示や選択項目の判定に使われる。
html:optionsCollectionのvalueにname()を指定したいところだけど、Property名のルール的に上手く行かないので、列挙型側(BloodTypeクラス)にgetValue()というメソッドを追加して"value"を指定するようにした。
このフォームをポストすると、変換エラーが発生する。文字列から列挙型の要素への変換が行えないためである。
S2StrutsでEnumを使用する(入力系)
入力時に列挙型の要素に変換するには、beanutilsのConvertUtilsにConverterを登録する。
まず使用する列挙型用のConverterを作成する。
package sample.enumlove.struts.converter; import org.apache.commons.beanutils.Converter; public class EnumConverter implements Converter { public Object convert(Class type, Object value) { return Enum.valueOf(type, value.toString()); } }
valueOf()を呼んでいるだけである。これをConvertUtils.register()を使って登録する。
登録する箇所はどこでも良いけど、クラスがDeployされるタイミングがシビアならFormでおこなうのが安全かも。
package sample.enumlove.web.member; import org.apache.commons.beanutils.ConvertUtils; import org.seasar.struts.annotation.tiger.StrutsActionForm; import sample.enumlove.model.BloodType; import sample.enumlove.model.MemberDto; import sample.enumlove.struts.converter.EnumConverter; /** 会員情報入力用のForm バージョン2 */ @StrutsActionForm(name="memberForm") public class MemberForm extends MemberDto { /* BloodType用のコンバータを登録 */ static { EnumConverter converter = new EnumConverter(); ConvertUtils.register(converter, BloodType.class); } public BloodType[] getBloodTypes() { return BloodType.values(); } }
ConvertUtilsへの登録は、プロパティに使用されるまさにそのクラスを指定する必要がある。(親クラスとかではダメ。) EnumConverterの方は他の列挙型にも使い回す事が出来る。
入力結果を表示する画面を作ってみる。
入力結果を受け取って表示するアクション
package sample.enumlove.web.member; import org.seasar.struts.annotation.tiger.StrutsAction; import org.seasar.struts.annotation.tiger.StrutsActionForward; /** 確認画面を表示するアクション */ @StrutsAction(input=ConfirmAction.CONFIRM, name = "memberForm") public class ConfirmAction { @StrutsActionForward public static final String CONFIRM = "/pages/member/confirm.html"; // アクションメソッド public String execute() { return CONFIRM; } }
確認ページ(htmlとmayaaファイル)
WebContent/pages/confirm.html:
<html xmlns:m="http://mayaa.seasar.org" lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=Windows-31J"> <title>登録確認画面</title> </head> <body> <b>登録確認画面</b><br/> <br/> <form m:id="mainForm"> 名前: <span m:id="name"></span><br> 血液型: <span m:id="bloodType"></span><br> <input m:id="hiddenName" type="hidden"> <input m:id="hiddenBloodType" type="hidden"> <input m:id="submitButton" type="submit" value="戻る"> </form> </body> </html>
WebContent/pages/confirm.mayaa:
<?xml version="1.0" encoding="Shift_JIS"?> <m:mayaa xmlns:m="http://mayaa.seasar.org" xmlns:html="http://struts.apache.org/tags-html" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mayaa.seasar.org http://mayaa.seasar.org/schema/mayaa_core_1_0_0.xsd" noCache="true"> <html:form m:id="mainForm" action="/member/index.do" method="POST" /> <html:submit m:id="submitButton" property="back" value="戻る" /> <html:hidden m:id="hiddenName" name="memberForm" property="name"/> <html:hidden m:id="hiddenBloodType" name="memberForm" property="bloodType"/> <m:write id="name" value="${memberForm.name}"/> <m:write id="bloodType" value="${memberForm.bloodType.label}"/> </m:mayaa>
文字列で表示する時は${memberForm.bloodType.label}の用にlabelプロパティを使用する。
hiddenで値を受け渡す時も、name="memberForm" property="bloodType"のように列挙型のプロパティ名を直接指定できる。
あとはregister.mayaaのアクションを/member/confirm.doに変更すれば、入力フォームと確認ページを交互に行き来できる。
S2JSFの場合
S2JSFの場合は、Converterの登録は必要なく、以下のようにそのまま列挙型のプロパティを指定できる。(但し、getValue()は実装している必要がある。)
register.html
... <!-- selectの場合 --> <select id="bloodType" m:value="#{memberForm.bloodType}" m:items="#{memberForm.bloodTypes}" m:itemLabel="label"> <option value="1">ここはダミーです</option> </select> <!-- hiddenの場合 --> <input type="hidden" m:value="#{memberForm.bloodType}" /> ...
これは、S2PropertyResolverがS2フレームワークのPropertyDescImplを呼んでおり、PropertyDescImplは未知の型にstaticなvalueOf(String)メソッドが定義されている場合、文字列から未知の型の処理にそのメソッドを使う為である。(EnumにはvalueOf(String)が定義されているので、そのまま使用される。)
列挙型ではなく、独自に定義したクラス(設定ファイルなどから値を読み込んで)を使用する場合でも、文字列を取るコンストラクタかpublic static なvalueOf(String)を定義すればページに貼付ける事が出来る。
環境
- J2SE 5.0以上
- Tomcat 6.0.18 (5.5以上なら)
- S2Struts V1.3.1同梱のjar(要らないものも入ってるかも)
- aopalliance-1.0.jar
- commons-beanutils-1.8.0-BETA.jar
- commons-chain-1.1.jar
- commons-digester-1.8.jar
- commons-fileupload-1.1.1.jar
- commons-io-1.1.jar
- commons-logging-1.1.jar
- commons-validator-1.3.1.jar
- geronimo-jta_1.1_spec-1.0.jar
- hsqldb-1.8.0.1.jar
- javassist-3.4.ga.jar
- jstl-1.1.2.jar
- junit-3.8.2.jar
- log4j-1.2.13.jar
- ognl-2.6.9-patch-20070908.jar
- oro-2.0.8.jar
- poi-3.0-FINAL.jar
- s2-dao-1.0.50.jar
- s2-dao-tiger-1.0.50.jar
- s2-extension-2.4.34.jar
- s2-framework-2.4.34.jar
- s2-struts-1.3.1.jar
- s2-struts-tiger-1.3.1.jar
- s2-tiger-2.4.34.jar
- standard.jar
- struts-core-1.3.8.jar
- struts-taglib-1.3.8.jar
- struts-tiles-1.3.8.jar
- Mayaa 1.1.26同梱のjar
- rhino1_7R2.zipのJava5用のjar
- js.jar
- PostgreSQL JDBCドライバ
- postgresql-8.3-604.jdbc3.jar