スポンサーリンク

【MyBatis】【Annotation】まとめ

スポンサーリンク

公式でもAnnotationに関してはxmlを参照とのこともあり、検索をかけても求めている答えに辿り着かないことが多いのでまとめました。(時間を短縮するためにORMを導入したはずが、予想以上に時間がかかってしまった。やはり新しいライブラリはラーニングカーブがありますね。。)

ちなみにKotlinで記載していますが、Intellijを使用すれば、Build後にTools > Kotlin > Show Kotlin ByteCodesで表示した後にDecompileでJavaに変換できますので、Javaユーザーの方でも問題ない内容になっています。(そもそも今回の記載内容に殆ど違いはないので見れば分かると思います)

Quick Start

上記で簡単にmybatisを始められます

前提

MyBatis Version

3.5.10

Table

Syntaxが異なるのは実験するためです。

Table SQL

CREATE TYPE myDataType AS ENUM('A', 'B', 'C');

CREATE TABLE some_data(
       id serial UNIQUE,
       data_name VARCHAR(32) UNIQUE,
       flag bool,
       data_type myDataType,
       PRIMARY KEY (id, data_name)
);

CREATE TABLE partData(
    data_name VARCHAR(32) NOT NULL REFERENCES some_data(data_name),
    contents VARCHAR
);

Sample Record (some_data)

Sample Record(partData)

Model Class

SomeData.kt

data class SomeData(
     val id: Int,
     val dataName: String,
     val flag: Boolean,
     val dataType: DataType
 ){
     enum class DataType{
         A,B,C
     }
 }

PartData

data class PartData(
    val someDataName: String,
    val contents: String
)

@Select

基本

基本はこれだけでOKです。すごく便利ですね。(Results等を記載している記事もありますが、現在のVersionでは必要ありません。)

@Select("select * from some_data where id = #{id}")
fun getSomeData(id: Int): SomeData

@Select("select * from some_data")
fun getSomeDataList(): List<SomeData> 

なお、後述するargNameBasedConstructorAutoMappingをTrueにしていない場合、TableのColumnの順番とConstructorのfieldの順番が一致している必要があります。順番が誤っている場合、例えば以下のようなErrorが発生します。これはDefaultでは順番に応じてマッピングを実現しているためです

Cause: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column ‘data_name’ from result set. Cause: org.postgresql.util.PSQLException: boolean へのキャストはできません: “testDataName1”

応用

@Resultの前提

これ以降は@Resultの記載が必要です。ただ初めに理解する必要がある事項があります。マッピングは次の2段階で実現されているということです。(おそらくこれはAnnotationベースの制約で明らかに一つにまとめられそうなのですが・・)

  1. TableのColumn名とModelクラスのField名を紐づける(ModelクラスのField名あるいは@Paramで指定します。)
  2. TableのColumn名とModelクラスのInstanceのField名を紐づける(@Resultのcolumnとpropertyがこの関係に対応します)

実プロセスでいうと、1はConstructorの指定で、2は実際の値のセットです。そのため1のConstructorはInstance化さえできれば何でもいいということになります。(逆を言えばValidationの役割を果たしていると言えます)

field名でマッピングしてほしい場合 ①

実現方法は2つありますが、現状で推奨される方から記載します。

MyBatisのVersionが3.5.10以降かつJava8以降限定です。

次の2つのsettingをmybatis-config.xmlに追加します。

<configuration>
    <settings>
        <setting name="argNameBasedConstructorAutoMapping" value="true"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>

build.gradleにparameters optionを追加します。(Javaの場合はJavaベースの機能なのでjava -paramertersで同様の機能を導入してください)

compileKotlin {
    kotlinOptions.jvmTarget = '1.8'
    kotlinOptions{
        javaParameters = true
    }
}

compileTestKotlin {
    kotlinOptions.jvmTarget = '1.8'
    kotlinOptions{
        javaParameters = true
    }
}

argNameBasedConstructorAutoMappingは宣言順ではなく名前に基づいてマッピングすること、mapUnderscoreToCamelCaseはアンダースコア”_”を排除し、camelCaseで比較する機能です(mapUnderscoreToCamelCaseはDBとコードの命名規則がこれで関連づくことが多いと思いますが、プロジェクト次第です)。いずれも上記の@Resultの前提1に対応しています。

SomeData.kt を以下のように書き換えます。(flagとdataNameを入れ替えました。)

data class SomeData(
    val id: Int,
    val flag: Boolean,
    val dataName: String,
    val dataType: DataType
)

この場合のMapperの記載方法は以下になります。

@Select("select * from some_data where id = #{id}")
@Results(id = "someData", value = [
    Result(column = "id", property = "id"),
    Result(column = "flag", property = "flag"),
    Result(column = "data_name", property = "dataName"),
    Result(column = "data_type", property = "dataType"),
])
fun getSomeData(id: Int): SomeData

@Select("select * from some_data")
@ResultMap("someData")
fun getSomeDataList(): List<SomeData>

@ResultMapは別箇所の@Resultsを再利用するための機能です。@Resultsで指定したidで指定します。別ファイルに定義されている場合は完全修飾子で指定します。

field名でマッピングしてほしい場合 ②

TableのColumnの順番とConstructorのfieldの順番が一致している必要 までは必須です。

こちらは argNameBasedConstructorAutoMappingが使用できず、上記のような機能が使えないので@Paramを用いて指定します。DBとコードの命名規則は異なることが多いので多々ある場面だと思われます。

SomeData.ktに@Param(“db_column名”)を追加します。

data class SomeData(
    @Param("id") val id: Int,
    @Param("data_name") val dataName: String,
    @Param("flag") val flag: Boolean,
    @Param("data_type") val dataType: DataType
)

Mapperの記載方法は以下になります。 これは①の場合と同様です。Resultの書く順番は順不同です。下は敢えて変えて書いています。

@Select("select * from some_data where id = #{id}")
@Results(id = "someData", value = [
    Result(column = "id", property = "id"),
    Result(column = "flag", property = "flag"),
    Result(column = "data_name", property = "dataName"),
    Result(column = "data_type", property = "dataType"),
])
fun getSomeData(id: Int): SomeData

@Select("select * from some_data")
@ResultMap("someData")
fun getSomeDataList(): List<SomeData> 

@Insert

Java8以降ではそのままparamerter名を使用できるので、以下のように記載することでInsertできます。SQL Enumについてはcastすることで実現できます

@Insert("insert into some_data(id, data_name, flag, data_type) values (#{id}, #{dataName}, #{flag}, cast(#{dataType} as myDataType))")
@Options(keyProperty = "id", useGeneratedKeys = true)
fun insertSomeData(someData: SomeData): Boolean

@Options(keyProperty, useGeneratedKeys)を指定することで、DBによって自動採番されるserialに値を委譲することができます。もし@Optionsを記載しない場合は、SomeDataに与えた値がそのまま入ってしまうので注意してください。逆にいくらSomeDataにidを手動で割り当てても@Optionsがある限り自動で振られます。

BooleanはinsertSomeDataが成功したかどうかです。

複数SQLの実行

余談なのでかなり省略して記載します。

@Insertは下記のように複数のSQLを実行することも可能です。

また、useGeneratedKeysで生成された値を2つめのinsertで使用したい場合には、SELECT CURRVAL(pg_get_serial_sequence(‘tabel_name’,’column_name’)`で最新のSerial採番を取得することが可能です。

@Insert("insert into some_data (id, data_name) values (#{ data_name });" +         "insert into part2_data (id, data) values ((SELECT CURRVAL(pg_get_serial_sequence('trade','id'))), #{part2_data.data})") @Options(keyProperty = "id", useGeneratedKeys = true) fun insertTrade(someData: SomeData): Boolean

@Update

Java8以降ではそのままparamerter名を使用できるので、以下のように記載することでDeleteできます。

@Update("update some_data set data_name = #{dataName}, flag = #{flag}, data_type = cast(#{dataType} as myDataType) where id = #{id}")
fun updateSomeData(someData: SomeData): Boolean

@Update, @Insertについては自作でfield名を取得してSqlProviderを組み立てる汎用クラスを作成した方が使いまわしができてよさそうですね。

@Delete

Java8以降ではそのままparamerter名を使用できるので、以下のように記載することでDeleteできます。

@Delete("delete from some_data where id = #{id}")
fun deleteSomeData(id: Int): Boolean

BooleanはdeleteSomeDataが成功したかどうかです。

Tableを結合する@Results

@Selectで少しだけ出ましたが、高度な@Resultsの使い方は以下のようになります。高度といっても参照等がある場合に、その参照先のObjectでInstance化したい場合があると思うのでその手法です。

参照キーも残してそれぞれ取りたい

以下のようなそれぞれのTable情報を入れるためのWrapperClassを定義します。

data class WrapperData(
    val someData: SomeData?,
    val partData: PartData?
){
    @AutomapConstructor
    constructor(
        id: Int,
        dataName: String,
        flag: Boolean,
        dataType: SomeData.DataType
    ):this(null, null)
}

補足

アノテーションベースだとAutomappingがどうしても起動してしまうせいか(@Propertyで一時的に切れそうだが、使い方がよく分からない)、まずInstance化するためにTableのカラムと同名のproperty(mapUnderscoreToCamelCaseをONにしている場合はこの範囲でもOKです。)を持ったConstructorが必須です。Instance化された後に、@Resultの定義にもとづいてMapping(上書き)されるので与える値は適当で問題ありません。

これはKotlinのせいなのか・・?また調査します。

続き

Mapperクラスは以下のようになります。

@Select("select * from some_data where id = #{id}")
@Results(id = "someData")
fun getSomeData(id: Int): SomeData

@Select("select * from partData where some_data_name = #{name}")
@Results(id = "partData")
fun getPartData(name: String): PartData

@Select("select * from some_data where id = #{id}")
@Results(id = "WrapperData", value = [
        Result(column = "id", property = "someData", one = One(resultMap = "someDataList")),
        Result(column = "data_name", property = "partData", one = One(select = "mybatis.mapper.MyDataMapper.getPartData"))
])
fun getWrapperData(id: Int): WrapperData 

Resultの記載方法が以下です。

column : Table(今回はsome_dataのcolumn名)
property : 上書きしたいproperty (今回はWrapperDataのproperty. WrapperDataであることはメソッドの返り値から自動判定されます)
one : 1:1のマッピング方法を指定します。resultMapで指定しても、Mapperのメソッド名を指定しても構いません。ともに完全修飾子で記載すると別クラスでも参照可能です。
many:1:多のマッピング方法を指定します。

よって、今回は

Tableの”id”を受け取ったら、One(resultMap = “someDataList”)に”id”を引数として渡して得られた結果で、property “someData”を上書きしてね、という意味になります。

Many

クエリの結果、複数のRowが返され、それをCollectionとして保持したい場合にはManyで指定します。以下のようになります。partDataをListにしています。

data class WrapperData(
    val someData: SomeData?,
    val partData: List<PartData>?
){
    @AutomapConstructor
    constructor(
        id: Int,
        dataName: String,
        flag: Boolean,
        dataType: SomeData.DataType
    ):this(null, null)
}

    @Select("select * from partData where some_data_name = #{name}")
    @Results(id = "partData")
    fun getPartData(name: String): PartData

    @Select("select * from partData")
    @ResultMap("partData")
    fun getPartDataList(): List<PartData>

    @Select("select * from some_data where id = #{id}")
    @Results(id = "WrapperData", value = [
        Result(column = "id", property = "someData", one = One(resultMap = "someDataList")),
        Result(column = "data_name", property = "partData", many = Many(select = "mybatis.mapper.MyDataMapper.getPartDataList"))
    ])
    fun getWrapperData(id: Int): WrapperData

(ただ、これoneのままでもなぜか正常に動くんですよね。なんでだろう・・・。)

一つのクラスとして取り出したい

モデルクラスを以下のように定義します。

data class WrapperData2(
    val id: Int,
    val partData: PartData?,
    val flag: Boolean,
    val dataType: SomeData.DataType
) {
    @AutomapConstructor
    constructor(
        id: Int,
        dataName: String,
        flag: Boolean,
        dataType: SomeData.DataType
    ):this(
        id, null, flag, dataType
    )
}

この場合Mapperクラスは以下のようになります

@Select("select * from some_data where id = #{id}")
@Results(id = "WrapperData2", value = [
    Result(column = "data_name", property = "partData", one = One(select = "mybatis.mapper.MyDataMapper.getPartData")),
])
fun getWrapperData2(id: Int): WrapperData2

columnの他の値はdefaultのAutoMappingで自動的に指定してくれるため、記載する必要はありません。ただ、漏れかどうかが分かりにくいため、仕事ではすべて記載するのがいいと思います。

@ConstructorArgs

Constructorに渡して初期化処理が必要な場合があると思います。そういう場合は@ConstructorArgsを用いてParameterを渡します。(基本的に初期化処理が必要なければ、@Resultsで同等のことが実現できます。違いはインスタンス化のタイミングで、@ConstructorArgsを指定した場合は、その指定されたConstructorによりインスタンス化した後にマッピングはされませんが、@Resultsの場合はインスタンス化したあとにマッピングに基づいて上書きされる点です。)

上記のWrapperDataの場合、下記のように記載できます。

    @Select("select * from some_data where id = #{id}")
    @ConstructorArgs(
        Arg(column = "id", name = "someData", resultMap = "someData"),
        Arg(column = "data_name", name = "partData",
            select = "mybatis.mapper.MyDataMapper.getPartData"
//            resultMap = "partData" ← 同等のはずですが怪しい
        )
    )
    fun getWrapperData(id: Int): WrapperData

なお、私の環境では、resultMapで指定した場合正しく認識されない場合がありました。上記の場合、2項目目のArgはselectだと正しく動作しますが、resultMapだと期待通りの動作となりませんでした。同等のはずですが場合によっては微妙に違いがあるのかも・・。

Error集

java.lang.IllegalArgumentException: Result Maps collection does not contain value for {クラス名}

以下のように@ResultMap(“hoge”)でidを指定しているのに、その@Resultsが存在しない場合に発生するエラーです。

@Select("select * from some_data where id = #{id}")
fun getSomeData(id: Int): SomeData

@Select("select * from some_data")
@ResultMap("someData") ←someDataを定義していないのに使用するとErrorが出る
fun getSomeDataList(): List<SomeData>

解決策としては

1. @ResultMapを消去する

@Select("select * from some_data where id = #{id}")
fun getSomeData(id: Int): SomeData

@Select("select * from some_data")
fun getSomeDataList(): List<SomeData> 

2. @Resultsを定義する

@Select("select * from some_data where id = #{id}")
@Results(id = "someData")
fun getSomeData(id: Int): SomeData

@Select("select * from some_data")
@ResultMap("someData")
fun getSomeDataList(): List<SomeData>

のいずれかです。

Cause: org.apache.ibatis.executor.ExecutorException: Statement returned more than one row, where no more than one was expected.

データクラスは単一オブジェクトを想定しているのに、クエリの結果が複数(Collection)のためマッピングできていないことが原因です。

解決策としては、データクラスの対象オブジェクトをCollectionにする、あるいはクエリ結果が一つになるように修正することです。

Cause: org.apache.ibatis.executor.ExecutorException: Constructor auto-mapping of ‘{クラス名}’

AutoMappingで初めにインスタンス化する際にコンストラクタを見つけられていません。特に独自のクラスにマッピングする際は参照するTableのカラムと一致していないproperty名を設定してしまっている可能性が高いです。

解決策としては、参照するTableのカラム名と一致した名前のpropertyでConstructorを定義することです。このConstructorはインスタンス化するだけで後のマッピングで上書きされるので、マッピングの範囲内に限っては中身は気にする必要はありません。

org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property=’xxx’, mode=IN, javaType=int, jdbcType=null, numericScale=null, resultMapId=’null’, jdbcTypeName=’null’, expression=’null’}. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #2 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: org.postgresql.util.PSQLException: 列インデックスは範囲外です: 2 , 列の数: 1

どういう意味でこのエラー文が出ているのかは理解できませんでしたが、原因は文字列リテラル内でパラメータをマッピングしようとするとこういうエラーになります。パラメータを文字列として埋め込みたい場合は例えば以下のようにconcatで連結します。

@Insert(value = ["insert into mygeo (name, geo) values (#{name}, ST_GeomFromText(concat('POLYGON((',#{lon},' ', #{lat},'))'),4326))"]) //OK
@Insert(value = ["insert into mygeo (name, geo) values (#{name}, ST_GeomFromText('POLYGON((#{lon} #{lat}))',4326))"]) //NG

最後に

アノテーションベースがあるなら、アノテーションの方が楽だと思って使い始めましたが、まだxmlと完全に互換があるわけではなく、微妙に挙動も分かりにくい箇所があるのでネストされたObjectにマッピングが必要な場合は素直にxmlを使用したほうがいいように感じましたね。

タイトルとURLをコピーしました