11
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

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"
    }
]

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

[
    {
        "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 メソッドが実行されます。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
11
Help us understand the problem. What are the problem?