Java
MyBatis
Lombok
springframework

MybatisのTypeHandlerでEnumを扱う

概要

O/Rマッピングフレークワークの Mybatis にて、Mybatisに備わっている自動型変換の機構:TypeHandlerを利用してEnumを扱う動的SQLの記述方法と、SpringMVCを使った応用も紹介します。

まとめた理由

これから新しく作る・すでに作成済みのシステムやアプリケーションが利用するデータベースには必ずあるかと思われる「フラグ」や「区分値」を扱うカラム1があり、その値の範囲はほぼ不変 2です。
これらの値が文字列型(CHARやVARCHAR)や数値型で扱われているケースも往々にしてありますので、せめてJavaコードの世界では文字列や数値をそのまま扱うことなく、さらには値の範囲が決まっているのであればenumで扱えないか…? が発端です。

記事の前提

本記事中は以下のバージョンで作成・動作確認をしています。

  • Java 8u152 (1.8.0_152)
  • Spring-Boot 1.5.9.RELEASE
  • mybatis-spring-boot-starter 1.3.1
    • mybatis 3.4.5
    • mybatis-spring 1.3.1
  • Lombok 1.16.18

ソースファイル一式

GitHubにて公開しています。

https://github.com/A-pZ/mybatis-spring-enum-sample

検索機能の実装

今回紹介するのは、とあるフラグや区分値を扱っているテーブルを検索した結果を返す機能です。

実装する内容

実装する内容をかんたんに説明すると、

  • データべースのフラグや区分値を表すenumを作成する
  • 作成したenum用の自動型変換クラス(TypeHandler)を作成する
  • TypeHandlerを登録する

です。

参照するテーブル

今回参照するテーブルの定義3です。

商品テーブル ( テーブル名:item )

カラム名 カラムの役割 制約など
id 数値 商品を一意に定義する主キー 自動採番される値
name 文字列 商品名 商品名の表示に使う
status 文字列 商品の表示区分 0:ステータスなし 1:一般公開 2:会員のみ
display 文字列 商品の表示・非表示 0:表示しない 1:表示する

今回フォーカスを当てるのは、statusで定義される3値の区分値、displayで扱うフラグです。

フラグや区分値を定義するenum

今回登場する2つのカラム status と display について、Java側でenumを作成します。statusは「商品の公開範囲を決めるもの」なのでItemPublishと名前を変えておきます。displayは表示する・しないを定める2値ですので、他のカラムでも利用できるよう、汎用的にTrueOrFalseとします。

ItemPublish.java
import java.util.Arrays;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 商品表示区分。
 */
@Getter
@AllArgsConstructor
public enum ItemPublish {
    NONE("0"), PUBLISH("1"), MEMBER_ONLY("2"), NO_VALUE("");

    private String value;

    public static ItemPublish getDisplayStatus(String value) {
        return Arrays.stream(values())
                .filter(v -> v.getValue().equals(value))
                .findFirst()
                .orElse(NO_VALUE);
    }
}

区分値がとりうる範囲外の値が指定されたときのために、NO_VALUEを定義4しています。

TrueOrFalse.java
@Getter
@AllArgsConstructor
public enum TrueOrFalse {
  TRUE("1"), FALSE("0"), NONE("");

  private String value;

  TrueOrFalse(String value) {
    this.value = value;
  }

  public static TrueOrFalse getTrueOrFalse(String input) {
    return Arrays.stream(values())
        .filter(v -> v.getValue().equals(input))
          .findFirst().orElse(NONE);
  }
}

真偽値(trueの場合1、falseなら0)を定義するenum。
0と1以外の値は想定外ですが、想定外の値はNONEとして定義します。

SpringとMybatisの実装

データベースに問い合わせを行う実装です。SpringMVCのRepositoryとServiceです。検索条件は ItemConditionクラス、検索結果の1レコードは Itemクラスで定義します。

ItemRepository.java
mport java.util.List;

import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;
import serendip.spring.sample.mybatis.model.Item;
import serendip.spring.sample.mybatis.model.ItemCondition;

/**
 * 商品検索リポジトリ。
 */
@Repository
@RequiredArgsConstructor
public class ItemRepository {
    private final SqlSession sqlSession;

    public List<Item> selectItems(ItemCondition condition) {
        return sqlSession.selectList("selectItems", condition);
    }
}
ItemService.java
import java.util.List;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import serendip.spring.sample.mybatis.model.Item;
import serendip.spring.sample.mybatis.model.ItemCondition;
import serendip.spring.sample.mybatis.repository.ItemRepository;

/**
 * 商品検索Service。
 */
@Service
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository repository;

    public List<Item> selectItems(ItemCondition condition) {
        return repository.selectItems(condition);
    }
}

検索条件のItemConditionです。先程定義したenumをそのまま条件に指定しています。

ItemCondition.java
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

/**
 * 商品の検索条件。
 */
@Builder
@ToString
@Getter
public class ItemCondition {
    private int id;
    private ItemPublish status;
    private TrueOrFalse display;
}

検索結果の1レコードを表すItemです。カラムstatusとdisplayの値をそれぞれ定義したenumで扱います。

Item.java
import lombok.Getter;
import lombok.Setter;

/**
 * 商品テーブル(Item)の1レコード。
 */
@Getter @Setter
public class Item {
    private Integer id;
    private String name;
    private ItemPublish status;
    private TrueOrFalse display;
}

SQLMappingを定義

今回実行するSQLは次の通りです。

sql-mappings.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="serendip.spring.sample.mybatis.repository.MemberRepository">
    <select id="selectItems"
        parameterType="serendip.spring.sample.mybatis.model.ItemCondition"
        resultType="serendip.spring.sample.mybatis.model.Item">
        SELECT
            id,
            name,
            status,
            display
        FROM
            item
    </select>
</mapper>

以上で実装クラスの準備が整いました。しかしこのまま実行したときMybatisがenumに対して自動的に変換を行うのですが、デフォルトで実行するメソッド5が取得できないため実行時例外がスローされます。

そこで、Mybatisでは特定のenumに対して型変換を実行するTypeHandlerを使います。

enum用のTypeHandlerを作成

TypeHandlerとは、MybatisがSQLMapping定義に含まれるJavaクラスとデータベース間の変換を行うものです。

実装済みのTypeHandler

実装済みのTypeHandlerは、Mybatisの org.apache.ibatis.typeパッケージにて提供されています。ここにJavaの型に対する変換ならびにデータベースのカラムで使う型に対するTypeHandlerを定義しています。例えばJavaのBigDecimalに対する型変換や、データベースのBLOBに対する型変換も提供されており、これらはJDBCのPretaredStatementにある機能を実行します。

TypeHandlerの拡張

MybatisのTypeHandlerは BaseTypeHandler<変換対象のクラス> で行います。SQLへ渡す値を設定する setNonNullParameter と、値を受け取る getNullableResult (メソッドは計3つ)の4メソッドを実装します。

ItemPublishTypeHandler.java
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import serendip.spring.sample.mybatis.model.ItemPublish;

/**
 * 商品公開ステータスのEnum用型変換クラス。
 */
public class ItemPublishTypeHandler extends BaseTypeHandler<ItemPublish> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, ItemPublish parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setString(i, parameter.getValue());
    }

    @Override
    public ItemPublish getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return ItemPublish.getDisplayStatus(rs.getString(columnName));
    }

    @Override
    public ItemPublish getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return ItemPublish.getDisplayStatus(rs.getString(columnIndex));
    }

    @Override
    public ItemPublish getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return ItemPublish.getDisplayStatus(cs.getString(columnIndex));
    }
}
TrueOrFalseTypeHandler.java
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import serendip.spring.sample.mybatis.model.TrueOrFalse;

/**
 * フラグを扱うEnum用の型変換クラス。
 */
public class TrueOrFalseTypeHandler extends BaseTypeHandler<TrueOrFalse> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, TrueOrFalse parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setString(i, parameter.getValue());
    }

    @Override
    public TrueOrFalse getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return TrueOrFalse.getTrueOrFalse(rs.getString(columnName));
    }

    @Override
    public TrueOrFalse getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return TrueOrFalse.getTrueOrFalse(rs.getString(columnIndex));
    }

    @Override
    public TrueOrFalse getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return TrueOrFalse.getTrueOrFalse(cs.getString(columnIndex));
    }
}

TypeHandlerの登録

作成したTypeHandlerはMybatisの設定ファイルに記載します。<typeHandlers>要素の中に<typeHandler>要素にて作成したTypeHandlerを1つずつ記載します。

mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeHandlers>
        <typeHandler handler="serendip.spring.sample.mybatis.typehandler.ItemPublishTypeHandler"/>
        <typeHandler handler="serendip.spring.sample.mybatis.typehandler.TrueOrFalseTypeHandler"/>
    </typeHandlers>
...
</configuration>

検索条件にも適用する

データベースからの検索結果だけでなく、TypeHandlerは引数(パラメータ)にも適用できます。

SQLMappingでenumを扱う記述

検索条件は前述した ItemCondition の TrueOrFalse を使用します。ごく簡潔に使う場合、enumのgetValue()などで値を取得する方法があります。

sql-mappings.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="serendip.spring.sample.mybatis.repository.MemberRepository">
    <select id="selectItems"
        parameterType="serendip.spring.sample.mybatis.model.ItemCondition"
        resultType="serendip.spring.sample.mybatis.model.Item">
        SELECT
            id,
            name,
            status,
            display
        FROM
            item
        <where>
            <if test="display.getValue() != ''">
                 display = #{display.value}
            </if>
        </where>
    </select>
</mapper>

ただしこの方法は、せっかくenumだけで記述してきた内容を、動的SQL内部で文字列比較していますので、やや勿体ない感じがします。ですが、Mybatisは動的SQLの条件文をOGNLで記載できます。

使ってみましょう。

SQLMappingでenumを直接比較する

OGNLでenumを参照するには、以下のように @enumの完全クラス名@列挙子 で表現します。

sql-mappings.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="serendip.spring.sample.mybatis.repository.MemberRepository">
    <select id="selectItems"
        parameterType="serendip.spring.sample.mybatis.model.ItemCondition"
        resultType="serendip.spring.sample.mybatis.model.Item">
        SELECT
            id,
            name,
            status,
            display
        FROM
            item
        <where>
            <if test="display != @serendip.spring.sample.mybatis.model.TrueOrFalse@NONE">
                display = #{display.value}
            </if>
        </where>
    </select>
</mapper>

これで動的SQLの条件分岐にもenumを適用できました。

SpringMVCのRestControllerを経由する

今回紹介した内容をSpringのRestControllerから実行し、JSON形式のレスポンスを返すよう作成します。

Controllerの実装

import java.util.List;
import java.util.Optional;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import serendip.spring.sample.mybatis.model.Item;
import serendip.spring.sample.mybatis.model.ItemCondition;
import serendip.spring.sample.mybatis.model.ItemPublish;
import serendip.spring.sample.mybatis.model.TrueOrFalse;
import serendip.spring.sample.mybatis.service.ItemService;

/**
 * 商品検索RestController。
 */
@RestController
@RequestMapping("/items")
@RequiredArgsConstructor
public class ItemController {

    private final ItemService service;

    @GetMapping("")
    public List<Item> searchItems(@RequestParam Optional<String> publish, @RequestParam Optional<String>  display) {
        ItemCondition condition = ItemCondition.builder()
                .status(ItemPublish.getDisplayStatus(publish.orElse("")))
                .display(TrueOrFalse.getTrueOrFalse(display.orElse("")))
                .build();

        return service.selectItems(condition);
    }
}

検索条件を設定しない場合も想定して、もしパラメータが未定義だった場合は、それぞれのenum値で未定義のときの値にします。

実行結果

http://127.0.0.1:8080/items

[
    {
        "id": 1,
        "name": "木製椅子",
        "status": "NONE",
        "display": "TRUE"
    },
    {
        "id": 2,
        "name": "ガラステーブル",
        "status": "PUBLISH",
        "display": "TRUE"
    },
    {
        "id": 3,
        "name": "木製テーブル",
        "status": "MEMBER_ONLY",
        "display": "FALSE"
    }
]

http://127.0.0.1:8080/items?display=0

[
    {
        "id": 3,
        "name": "木製テーブル",
        "status": "MEMBER_ONLY",
        "display": "FALSE"
    }
]

http://127.0.0.1:8080/items?display=1

[
    {
        "id": 1,
        "name": "木製椅子",
        "status": "NONE",
        "display": "TRUE"
    },
    {
        "id": 2,
        "name": "ガラステーブル",
        "status": "PUBLISH",
        "display": "TRUE"
    }
]

今回のパラメータ値はデータベースの値をそのまま条件値に使いますが、他の値にすることもControllerでできるでしょう。
また、JSONのレスポンス値をさらに変換したい場合は、enumにjacksonのJsonSerializeインタフェースを実装すると自動的に変換されます。

おわりに

自動型変換・値変換を使って、より堅牢でメンテナンスしやすいアプリケーション構築の一助になれば幸いです(・ω・)



  1. 「フラグ」や「区分値」が指し示す内容にも「開発現場やベンダーの方言」があります。この記事では、「フラグ」は2値しか持たない、いわゆるbooleanのtrue/falseのみを示すカラム、「区分値」はboolean以外の目的で、取りうる値の範囲が固定化されているカラムです。 

  2. 例えば、2018年現在でもあまり好ましくない 削除フラグや更新フラグあたりは、システムの新旧問わず存在するケース。0:有効なレコード、1:削除済み など 

  3. レガシーシステムでありがちな、名前から内容が推測しにくい、わざとダサダサなカラム名にしています。statusとdisplayって何!?となるでしょうか。 

  4. 今回は常に何らかの値を返すようにしていますが、ありえない値が指定されたらアプリケーション例外をスローするのもあるでしょう。 

  5. java.lang.Enum.valueOf メソッドが実行されます。