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変換の仕組みを利用しましょう。