MyBatisでINSERTを実行したあと、entity.getId() が0やnullのまま返ってきて困ったことはありませんか。AUTO_INCREMENTやIDENTITYで採番するテーブルは、INSERT文を投げただけではJava側に値が戻ってきません。MyBatisに「採番された値を取ってきて」と明示的に伝える設定が必要なんですよね。

この記事では、アノテーション方式とXML方式それぞれでの useGeneratedKeys / keyProperty の書き方、採番方式を持たないDB向けの selectKey、そしてIDが取得できないときの原因の切り分け方を順に見ていきます。Mapperの基本的な実装は MyBatis導入ガイド や Mapper実装ガイド に譲り、ここではINSERT後のID取得だけに絞ります。

なぜIDが0やnullのまま返ってくるのか

AUTO_INCREMENT(MySQL)やIDENTITY(PostgreSQL/SQL Server)は、INSERT実行時にDBエンジンが採番する仕組みです。JDBCの Statement は getGeneratedKeys() を呼べば採番値を取得できますが、MyBatisはデフォルトではこの呼び出しをしません。

つまり、何も設定しないままだと次のようなコードを書いても id フィールドは初期値のままになります。

userMapper.insert(user);
System.out.println(user.getId()); // => 0 のまま

これはバグではなく「取得する設定を書いていないだけ」というのがほとんどのケースです。

アノテーション方式:@Optionsで有効にする

Mapperインタフェースのメソッドに @Options を付けるだけで、生成キーの取得を有効にできます。

public interface UserMapper {

    @Insert("INSERT INTO users (name, email) VALUES (#{name}, #{email})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);
}

ポイントは、keyProperty に指定するのはDBのカラム名ではなく Java側のフィールド名 だという点です。"id" と書けば User#id に値が書き戻されます。ここをカラム名の "ID" にしてしまうミスは意外とよくあるので注意してください。

mybatis-spring-boot-starterを使っている場合も @Options の挙動自体は変わりません。Mapperインタフェースが @Mapper か @MapperScan でスキャン対象になっていれば、追加の設定なしでそのまま動作します。

XML Mapperを使うなら<insert>タグに属性を追加する

XML Mapperを使っている場合も考え方は同じです。

<insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="ID">
    INSERT INTO users (name, email)
    VALUES (#{name}, #{email})
</insert>

XML方式では keyColumn(DB側のカラム名)と keyProperty(Javaのフィールド名)の両方を指定できます。keyColumn を省略できるかどうかはJDBCドライバによって差があり、MySQLでは省略してもAUTO_INCREMENTカラムを自動で拾えることが多いですが、PostgreSQLなど他のDBでは明示したほうが安全です。カラム名とプロパティ名が大きく異なる場合も同様に、明示しておくことをおすすめします。

採番値はいつentityに反映されるのか

ここが意外と誤解されやすいところです。採番値は insert() メソッドの戻り値ではなく、 引数に渡したentityオブジェクト自体 が書き換えられる形で反映されます。

User user = new User("Taro", "[email protected]");
int affectedRows = userMapper.insert(user); // 戻り値は影響行数
Long generatedId = user.getId();            // ここに採番値が入っている

よくある勘違いとして、呼び出し側で別のentityインスタンスを作り直して参照してしまい、「IDが入らない」と誤解するケースがあります。参照しているのは必ず insert() に渡したオブジェクトそのものです。

useGeneratedKeysが使えないDBではselectKeyを使う

Oracleの旧来のシーケンス運用のように、JDBCドライバの getGeneratedKeys() に頼れない場合は <selectKey> で代替します。なお、Oracle 12c以降はIDENTITY列を使えばAUTO_INCREMENT相当の自動採番ができるため、その場合は通常の useGeneratedKeys / keyProperty だけで対応できます。ここで紹介する selectKey は、従来のシーケンス運用やIDENTITY列を使わない構成向けの代替手段と考えてください。useGeneratedKeys と selectKey は同じINSERTで併用するものではなく、DBの採番方式に応じてどちらか一方を選びます。

INSERT前にシーケンスから採番する場合は order="BEFORE" です。

<insert id="insert">
    <selectKey keyProperty="id" resultType="long" order="BEFORE">
        SELECT user_seq.NEXTVAL FROM dual
    </selectKey>
    INSERT INTO users (id, name, email)
    VALUES (#{id}, #{name}, #{email})
</insert>

逆にAUTO_INCREMENTのように「INSERT後でないと値が確定しない」仕組みでは order="AFTER" を使い、LAST_INSERT_ID() のような関数で取得します。

<insert id="insert">
    INSERT INTO users (name, email)
    VALUES (#{name}, #{email})
    <selectKey keyProperty="id" resultType="long" order="AFTER">
        SELECT LAST_INSERT_ID()
    </selectKey>
</insert>

採番のタイミングが「INSERT前か後か」で order を使い分ける、と覚えておけば迷いません。

IDが取れないときに疑うべきポイント

設定したのにまだ0やnullのままという場合は、次の順番で疑ってみてください。

  • keyProperty に指定した名前がentityのフィールド名と一致しているか(カラム名を書いていないか)
  • DB側のカラムに本当にAUTO_INCREMENT/IDENTITYが設定されているか
  • 使っているJDBCドライバが getGeneratedKeys() に対応しているか
  • keyColumn に指定したカラム名が、大文字小文字や引用符付き識別子(ダブルクオート)の有無で実際のテーブル定義とズレていないか
  • XML MapperとMapperインタフェースを併用している場合、@Options と <insert> の設定が重複・競合していないか

特に3つ目は見落としがちで、ドライバのバージョンを上げたら解決した、という事例も珍しくありません。設定を一通り見直してもまだ解決しない場合は、SQLとMapperの紐付けそのものがズレている可能性もあるので、BindingException対処ガイド も確認してみてください。

チェックリストを一通り確認すれば単発のINSERTはほぼ解決しますが、処理をバッチ化した途端に同じ設定がすり抜けていくことがあります。

バッチINSERT(foreach)での注意点

ここは「foreachで組み立てる1本のINSERT文」と「JDBCの本来のバッチ実行」を分けて考える必要があります。

<foreach> でVALUESを複数並べて1本のINSERT文として実行する場合、実体はSQL文1本の発行です。MySQL(Connector/J)では、AUTO_INCREMENT値が連続して確保されることを前提に、getGeneratedKeys() で各行分のキーをまとめて取得できることが公式ドキュメントで説明されています。keyProperty にリスト要素のプロパティ名を指定すれば、各行のオブジェクトに採番値が反映されます。

<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO users (name, email) VALUES
    <foreach collection="list" item="u" separator=",">
        (#{u.name}, #{u.email})
    </foreach>
</insert>

ここで確認したのはMySQL(Connector/J)の組み合わせです。PostgreSQLなど他のDB・ドライバで同じように動くとは限らないので、採用する前に自分の環境で必ず確認してください。

一方で ExecutorType.BATCH を使った本来のJDBCバッチ実行(addBatch() / executeBatch())では、生成キーの取得がドライバやDBによって期待どおりに動かないことがあります。環境依存の部分が大きいので、バッチモードで生成キーが必須の処理は事前に自分の環境で動作確認しておくのが安全です。確実性を優先するなら、1件ずつ insert() を呼ぶか selectKey に切り替える選択肢も検討してください。

複数カラムを採番する場合

複合キーなど、生成されるカラムが複数ある場合は keyProperty と keyColumn にカンマ区切りで並べて指定できます。

<insert id="insert" useGeneratedKeys="true"
        keyProperty="id,createdSeq" keyColumn="ID,CREATED_SEQ">
    ...
</insert>

ただ実務では単一の id カラムだけというケースが大半なので、ここまで必要になる場面は多くありません。

まとめ

MyBatisでINSERT後のIDを取るには、アノテーション方式の @Options(useGeneratedKeys, keyProperty) か、XML方式の <insert> の同名属性のどちらかを使います。プロジェクト内でMapperの書き方を統一しているなら、それに合わせて選べば十分です。

IDが0やnullのままという症状に当たったら、まず keyProperty の名前とDB側の採番設定を疑うのが近道です。それでも解決しなければ、ドライバの対応状況やバッチ実行の制限を順番に確認してみてください。INSERT以外のSELECT結果のマッピングで詰まった場合は、resultMapでの結合マッピング記事 も参考になります。