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_IDNAMEGENDERDEPARTMENT_ID
100TarouMale0
101HanakoFemale2

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