MyBatisでDB上のいわゆる業務コードを、Java上のenumに結び付ける
enumは非常に便利です。CやJavaやC#のプログラマであれば、使わない手はありません。
ここではJavaのenumが対象ですが、なぜenumが良いのかは簡単に言うと型安全だからです。詳しくは7ステップで理解するJavaでの列挙型/enum使用法 (1/3) - ITあたりを参照してください。
JavaのenumとDBの問題
この便利なenumですが、残念ながらDBに出し入れする時には一苦労します。DBにはenumを直接格納・取り出しできないため、何らかの変換処理が必要だからです。
自分の経験した残念なプロジェクトではenumを利用していませんでした。ではどうしたかというと、Stringで代用していました。DB上に部門コードのようなものがあって”00”, “01”, …, “40”というような感じに格納していました。このString(VARCHAR2)をDB上の結合などにも使うし、コード上で使いたいときはSQLで取ってきて”01”.equals(bumonCode)などとやっていました。
残念ながら上記のような仕組みのせいで問題だらけでした。部門コードが2桁で”01”とかなってるけど、比較する部分では”1”と比較していてバグが発生したり。メソッドの始めにString CONST_1000 = “00”; (他多数、CONST_1100くらいまで。。。)とかするもんだから、後のコードでCONST_1000.equals(bumonCode)などとなっていて、さっぱり意味が分からなかったり。部門コードではないCONST_1050と比較するCONST_1050.equals(bumonCode)なんてバグもありました。色々と最悪でした。ちなみに自分はプロジェクトのテストフェーズから参画しましたが、デスマってました。
DB上のコードとJavaのenumを結びつける方法
そんな訳で、できればenumを利用したい所です。(上記の問題の例はenum以前の問題という気もしますが)
ではどうすれば良いか、というとEnum と データベースの「コード値」の相互変換 - penultimate diaryなんかは、良い例を挙げています。こういう仕組みを利用することが大切だと思います。
ここでは上記とほぼ同じ仕組み、つまりenumとDBのコード変換の仕組みを用意し、ORマッパーに自動変換させる手法を使います。上記のURLはHibernateですが、こちらではMyBatis用のコードです。
MyBatisなら変換の仕組みはデフォルト装備
実はMyBatisには上記のURLのような仕組みがデフォルトで備わっています。これを活用しない手はありません。
MyBatis - MyBatis 3 | 設定を見ると分かりますが、EnumTypeHandlerとEnumOrdinalTypeHandlerというものがあります。EnumTypeHandlerはDB上の値が先ほどの部門コードではなく部門名であるような場合に利用します。EnumOrdinalTypeHandlerはDB上の値が先ほどの部門コードのような場合に利用します。
それではこれを使ったサンプルを作成します。
まずテーブルを作成します。GENDER列とDEPARTMENT_ID列が今回のenumの対象です。
CREATE TABLE EMPLOYEE
(
EMPLOYEE_ID BIGINT UNSIGNED,
NAME VARCHAR(50),
GENDER VARCHAR(10),
DEPARTMENT_ID INT UNSIGNED
);
次にJavaのコードを作成します。
Gender.java
package com.tsukaby.mybatis.bean;
public enum Gender {
/** 男性 */
Male,
/** 女性 */
Female
}
Department.java
package com.tsukaby.mybatis.bean;
public enum Department {
/** 総務部 */
GeneralAffairs,
/** 開発部 */
Development,
/** 営業部 */
Sales
}
Employee.java
package com.tsukaby.mybatis.bean;
public class Employee {
private Long employeeId;
private String name;
private Gender gender;
private Department department;
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
@Override
public String toString() {
return "Employee [employeeId=" + employeeId + ", name=" + name + ", gender=" + gender + ", department=" + department + "]";
}
}
EmployeeMapper.java
package com.tsukaby.mybatis.mapper;
import java.util.List;
import com.tsukaby.mybatis.bean.Department;
import com.tsukaby.mybatis.bean.Employee;
import com.tsukaby.mybatis.bean.Gender;
public interface EmployeeMapper {
int insert(Employee record);
List<Employee> selectByGender(Gender gender);
List<Employee> selectByDepartment(Department department);
}
mybatis-config.xmlのconfigurationタグ内に以下を追加します。
<typeHandlers>
<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.tsukaby.mybatis.bean.Department"/>
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler" javaType="com.tsukaby.mybatis.bean.Gender"/>
</typeHandlers>
最後にMain.javaです。
package com.tsukaby.mybatis;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import com.tsukaby.mybatis.bean.Department;
import com.tsukaby.mybatis.bean.Employee;
import com.tsukaby.mybatis.bean.Gender;
import com.tsukaby.mybatis.mapper.EmployeeMapper;
public class Main {
public static void main(String[] args) {
System.out.println("Start");
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
// 適当なデータを挿入し、取得する
insertSampleData(mapper);
printSampleData(mapper);
session.commit();
} finally {
session.close();
}
System.out.println("End");
}
private static void insertSampleData(EmployeeMapper mapper) {
Employee record = new Employee();
record.setEmployeeId(100L);
record.setName("Tarou");
record.setGender(Gender.Male);
record.setDepartment(Department.GeneralAffairs);
mapper.insert(record);
Employee record2 = new Employee();
record2.setEmployeeId(101L);
record2.setName("Hanako");
record2.setGender(Gender.Female);
record2.setDepartment(Department.Sales);
mapper.insert(record2);
}
private static void printSampleData(EmployeeMapper mapper) {
List<Employee> list;
// 男性の全レコードを取得し表示 (enumで指定)
list = mapper.selectByGender(Gender.Male);
System.out.println(list);
// Sales部の全レコードを取得し表示 (enumで指定)
list = mapper.selectByDepartment(Department.Sales);
System.out.println(list);
}
}
適当なレコードを挿入し、その後enumによって検索しています。enumを検索条件に指定できる点がポイントです。これによって型安全が実現できています。SELECTによって取得する時も同様で、enumで取れるため、Java側で安全に利用することができます。
これを実行すると、DB上には以下のような形式でデータが格納されます。
EMPLOYEE_ID | NAME | GENDER | DEPARTMENT_ID |
100 | Tarou | Male | 0 |
101 | Hanako | Female | 2 |
GENDERもDEPARTMENT_IDも同じenumですが、格納されている値の形式が異なります。これはGENDERにはEnumTypeHandlerを、DEMAPARTMENT_IDにはEnumOrdinalTypeHandlerを利用したためです。数値を利用することが多いと思うので、EnumOrdinalTypeHandlerを利用しましょう。DEPARTMENT_IDには0と2が入っていますが、これはenumであるDepartmentのordinalに依存しています。Tarouは総務部で総務部はDepartmentの最初の要素ですから0になります。Hanakoは営業部で営業部はDepartmentの3番目の要素ですから2になります。ordinal=indexと思っていいと思います。
コード体系変更の危険性とordinalの注意
実際のプロジェクトでこの仕組みは上手く動作すると思いますが、注意しなければならないシーンはやはりあります。
コードの体系を変えるというシーンはよくあるかと思います。例えば、上記の部門enumの要素は総務部と開発部と営業部でした。ここで顧客から「組織体系を改革します。部署も一新するため、コードを変更します。総務部=11, コンサルティング部=12, 営業部=13としてください。DB上のコードマスタとプログラムを改修してください。」というような要求があったとします。
残念ながら上記の仕組みはenumのordinalに依存していますから、enumに3つの部門要素を定義してそれぞれ11, 12, 13というようにはできません。やるとしたら、以下のような非常に見苦しいコードになることでしょう。
(コード体系変更された)Department.java
package com.tsukaby.mybatis.bean;
public enum Department {
/** 未使用 */
@Deprecated
Padding0,
/** 未使用 */
@Deprecated
Padding1,
/** 未使用 */
@Deprecated
Padding2,
/** 未使用 */
@Deprecated
Padding3,
/** 未使用 */
@Deprecated
Padding4,
/** 未使用 */
@Deprecated
Padding5,
/** 未使用 */
@Deprecated
Padding6,
/** 未使用 */
@Deprecated
Padding7,
/** 未使用 */
@Deprecated
Padding8,
/** 未使用 */
@Deprecated
Padding9,
/** 未使用 */
@Deprecated
Padding10,
/** 総務部 */
GeneralAffairs,
/** コンサルティング部 */
Consulting,
/** 営業部 */
Sales,
}
見苦しいですが、それでもintやStringよりかはenumを利用した方が良い気はしています。実際上記の要望を叶えるとDBの値変更、上記のenum変更、廃止された開発部(Development)固有のコードへの対応、新設されたコンサルティング部(Consulting)固有のコードへの対応、という感じになるでしょう。Padding0-10が嫌なだけで、それほど問題ではないかな・・・?
以上で終わりです。
enumが利用できるとプログラムの安全性が大きく高まります。MyBatisを利用する時は、ぜひ上記のようなenum変換の仕組みを利用しましょう。