概要
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とします。
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しています。
@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クラスで定義します。
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);
}
}
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をそのまま条件に指定しています。
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で扱います。
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は次の通りです。
<?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メソッドを実装します。
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));
}
}
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つずつ記載します。
<?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()などで値を取得する方法があります。
<?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の完全クラス名@列挙子
で表現します。
<?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値で未定義のときの値にします。
実行結果
[
{
"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インタフェースを実装すると自動的に変換されます。
おわりに
自動型変換・値変換を使って、より堅牢でメンテナンスしやすいアプリケーション構築の一助になれば幸いです(・ω・)
-
「フラグ」や「区分値」が指し示す内容にも「開発現場やベンダーの方言」があります。この記事では、「フラグ」は2値しか持たない、いわゆるbooleanのtrue/falseのみを示すカラム、「区分値」はboolean以外の目的で、取りうる値の範囲が固定化されているカラムです。 ↩
-
例えば、2018年現在でもあまり好ましくない 削除フラグや更新フラグあたりは、システムの新旧問わず存在するケース。0:有効なレコード、1:削除済み など ↩
-
レガシーシステムでありがちな、名前から内容が推測しにくい、わざとダサダサなカラム名にしています。statusとdisplayって何!?となるでしょうか。 ↩
-
今回は常に何らかの値を返すようにしていますが、ありえない値が指定されたらアプリケーション例外をスローするのもあるでしょう。 ↩
-
java.lang.Enum.valueOf メソッドが実行されます。 ↩