コード値を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ドライバ)を使用する。

前準備

create table member (
member_id serial,
name varchar(32) not null,
blood_type smallint not null
);

  • S2StrutsBlank V1.3.1をベースにしたプロジェクト(EnumSample)を作成し、mayaa関連とPostgreSQLJDBCドライバの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>

S2DaoEnumを使用する

これは簡単で、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();
        }
    }

S2StrutsEnumを使用する(入力フォーム表示まで)

入力画面をS2StrutsMayaaで作ってみる。
まず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"を指定するようにした。


このフォームをポストすると、変換エラーが発生する。文字列から列挙型の要素への変換が行えないためである。

S2StrutsEnumを使用する(入力系)

入力時に列挙型の要素に変換するには、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
    • commons-collections-3.1.jar
    • mayaa-1.1.26.jar
    • nekohtml-0.9.5.jar
    • xercesImpl-2.7.1.jar
  • rhino1_7R2.zipのJava5用のjar
    • js.jar
  • PostgreSQL JDBCドライバ