【Mybatis】TypeHandlerでValueObjectへ値をマッピングできるようにしてみる


タイトルの通り、TypeHandlerでValueObjectへマッピングできるようにしてみたので記事にしてみました。

Motivation

XMLにこんな感じでどのカラムをマッピングするか書いてたんですが、今後ValueObjectが増えるたびに定義しなきゃいけないのか...とツラミを感じていました。

↓こんな感じにすれば定義の共通化自体は可能なのですが、カラムの名前を都度気にしなきゃいけないのもバグりポイントなのでどうにかしたいなって思ってました。(調べればXMLの設定でもこの辺いい感じに解決できるのかもしれませんけど...)

  <resultMap id="ValueObject" type="org.test.hoge.ValueObject">
    <constructor>
      <idArg javaType="java.lang.String" column="value" />
    </constructor>
  </resultMap>
    <association property="value" resultMap="org.test.hoge.Dao.ValueObject" />

TypeHandlerでマッピングできるようにする

型がTypeHandlerでサポートされていれば、StringやIntなどのプリミティブな型同様何も気にせずselectできます。
リフレクション使ってるので嫌な人は見ない方がいいと思います。
あとkotlinで書いてます。(javaで書くとどうなるかも未検証...

まずはValueObjectのabstractなclassを作ります。(equalsとhashCodeは適当)

abstract class ValueObject<E>(open val value: E) {
    override fun equals(other: Any?): Boolean {
        return value == other
    }

    override fun hashCode(): Int {
        return value?.hashCode() ?: 0
    }
}

あとはtypehandlerを定義します。ValueObjectを継承したクラスのコンストラクタにStringなvalue一つだけのコンストラクタを含む前提です。

@MappedTypes(
    SubVO::class,
)
class StringValueObjectTypeHandler<E : ValueObject<String>>(val type: Class<E>) : BaseTypeHandler<E>() {
    override fun setNonNullParameter(ps: PreparedStatement?, i: Int, parameter: E, jdbcType: JdbcType?) {
        ps?.setString(i, parameter.value)
    }

    override fun getNullableResult(rs: ResultSet?, columnName: String?): E? {
        return rs?.getString(columnName)?.let { valueObject(it) }
    }

    override fun getNullableResult(rs: ResultSet?, columnIndex: Int): E? {
        return rs?.getString(columnIndex)?.let { valueObject(it) }
    }

    override fun getNullableResult(cs: CallableStatement?, columnIndex: Int): E? {
        return cs?.getString(columnIndex)?.let { valueObject(it) }
    }

    private fun valueObject(value: String): E {
        val c = type.kotlin.constructors.first {
            it.parameters.size == 1 && it.parameters.first().type == typeOf<String>()
        }
        return c.call(value)
    }
}

valueの型に応じてTypeHandlerを複数作るか、 it.parameters.first().type == typeOf<String>() の比較でジェネリクスを使えばいける気もする(試してない)