INSERTでレコードを登録できても、「データベースが採番したIDをどこから受け取るのか」が分からないと、後続の登録処理へ進めません。親レコードを登録して、そのIDを子レコードの外部キーへ設定したい場面では、採番結果をJava側で扱う必要があります。
MyBatisでは、useGeneratedKeys と keyProperty を設定し、INSERTへ渡したオブジェクトに生成されたIDを受け取る方法があります。この記事では、その書き方と後続処理への渡し方、IDが入らない場合の確認点を説明します。
MyBatisでINSERT後の自動採番IDを取得する仕組み
戻り値ではなく引数のオブジェクトでIDを受け取る
通常のINSERTを実行するMapperメソッドを int insertUser(User user) と定義した場合、その戻り値は更新件数です。採番されたIDではありません。
生成されたIDは、設定に従って引数の user に書き込まれます。呼び出し後に user.getId() を参照することで、後続処理に渡す値を取り出します。
int affectedRows = userMapper.insertUser(user);
Long generatedId = user.getId();affectedRows は登録件数、generatedId は採番結果です。メソッドの戻り値を long に変えても、戻り値が主キーになるわけではありません。
useGeneratedKeysとkeyPropertyの役割
useGeneratedKeys="true" は、JDBCの生成キー取得機能を利用するための設定です。keyProperty="id" は、取得した値をJavaオブジェクトの id プロパティへ設定する指定です。
ここで、採番そのものを行うのはデータベース側です。useGeneratedKeys を付けるだけで、テーブルの主キーが自動採番になるわけではありません。対象カラムには、利用するDBに合った自動採番の定義が必要です。
useGeneratedKeysで自動採番IDを取得する書き方
ここでは、users テーブルにユーザーを1件登録する例を使います。テーブルには、自動採番される id と、文字列を保存する name があるものとします。テーブル作成SQLはDBごとに異なるため省略します。
IDを保持するJavaクラスを用意する
登録内容と採番結果を保持する User クラスを用意します。IDには Long を使うと、値がまだ設定されていない状態を null で表せます。
package com.example.domain;
public class User {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}この例では、MyBatisが採番結果を書き込めるように setId(Long id) を用意しています。Java側の型は、DBの主キーの型や値の範囲に合わせて選びます。
Mapper XMLのinsertに取得設定を追加する
Mapperインターフェースには、User を引数に取る登録メソッドを定義します。最初の例では、引数は1つで、@Param は付けません。
package com.example.mapper;
import com.example.domain.User;
public interface UserMapper {
int insertUser(User user);
}対応するMapper XMLは次の形です。Mapperの登録やXMLの読み込み設定は済んでいる前提で、XML宣言とDOCTYPE宣言は省略しています。
<mapper namespace="com.example.mapper.UserMapper">
<insert id="insertUser"
parameterType="com.example.domain.User"
useGeneratedKeys="true"
keyProperty="id">
INSERT INTO users (name)
VALUES (#{name})
</insert>
</mapper>id はDB側で生成するため、この例のINSERT対象には含めていません。#{name} で登録内容を渡し、取得した主キーを keyProperty="id" で指定したプロパティへ受け取ります。生成キーを受け取るために resultType を追加する必要はありません。
DBやドライバーによっては、取得対象のカラムを指定する keyColumn も必要です。必要な場合は、たとえば keyColumn="id" のようにDB側のカラム名を指定します。Java側の受け取り先を表す keyProperty とは役割が異なります。
MapperインターフェースとXMLの対応や、INSERT文の基本を確認したい場合は、次の記事を参照してください。

Mapper呼び出し後にIDを参照する
呼び出し側では、新しいオブジェクトに登録内容を設定し、INSERT後に同じオブジェクトからIDを読み取ります。以下は userMapper を利用できるメソッド内の処理例です。
User user = new User();
user.setName("みかん");
int affectedRows = userMapper.insertUser(user);
if (affectedRows != 1) {
throw new IllegalStateException("登録件数が想定と異なります");
}
Long generatedId = user.getId();
if (generatedId == null) {
throw new IllegalStateException("生成されたIDを取得できませんでした");
}
// generatedIdを後続処理へ渡すここで例外を投げただけでは、すでに実行したINSERTが必ず取り消されるわけではありません。登録を取り消す必要がある場合は、次のトランザクション設計も合わせて行います。
取得したIDを後続の登録処理で使う流れ
親レコードのIDを子レコードへ設定する
ユーザー登録に続いてプロフィールを登録する場合は、users.id を取得し、子テーブル user_profiles の user_id に渡します。IDを受け取るために、ユーザー名などから登録直後のレコードを検索し直す必要はありません。
子レコードの登録には、次のMapperを使うものとします。user_profiles には user_id と display_name があり、それ以外の必須項目はない前提です。
package com.example.mapper;
import org.apache.ibatis.annotations.Param;
public interface UserProfileMapper {
int insertProfile(@Param("userId") Long userId,
@Param("displayName") String displayName);
}<mapper namespace="com.example.mapper.UserProfileMapper">
<insert id="insertProfile">
INSERT INTO user_profiles (user_id, display_name)
VALUES (#{userId}, #{displayName})
</insert>
</mapper>親のINSERT後に user.getId() を取得し、insertProfile の第1引数へ渡すことで、親子の関連を保存できます。
複数の登録処理を同じトランザクションにまとめる
ユーザーとプロフィールを一組として登録したい場合は、両方のINSERTを同じトランザクションに含めます。SpringとMyBatisを連携している構成なら、Serviceで処理全体をまとめる形が考えられます。
package com.example.service;
import com.example.domain.User;
import com.example.mapper.UserMapper;
import com.example.mapper.UserProfileMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserRegistrationService {
private final UserMapper userMapper;
private final UserProfileMapper profileMapper;
public UserRegistrationService(UserMapper userMapper,
UserProfileMapper profileMapper) {
this.userMapper = userMapper;
this.profileMapper = profileMapper;
}
@Transactional
public Long register(String name, String displayName) {
User user = new User();
user.setName(name);
int userRows = userMapper.insertUser(user);
if (userRows != 1 || user.getId() == null) {
throw new IllegalStateException("ユーザー登録結果を確認できません");
}
int profileRows = profileMapper.insertProfile(
user.getId(), displayName);
if (profileRows != 1) {
throw new IllegalStateException("プロフィール登録に失敗しました");
}
return user.getId();
}
}この例は、両Mapperが同じSpring管理のトランザクションへ参加し、外部のBeanからプロキシ経由で register を呼ぶ構成を前提とします。トランザクション管理が有効で、標準のロールバック設定であれば、例の IllegalStateException によって一連の登録がロールバック対象になります。
IDを取得できたことと、登録がコミットされたことは別です。トランザクション内では取得したIDを子の登録に使えますが、その後の失敗で登録が取り消される場合があります。ロールバックしても、Javaオブジェクトに設定されたIDが自動的に消えるわけではありません。
@Transactional の適用条件や、例外によるロールバックの違いは、次の記事で確認できます。

自動採番IDを取得できないときの確認ポイント
keyPropertyとJavaプロパティ名が一致しているか
keyProperty に指定するのはJava側のプロパティ名です。DBのカラム名が user_id でも、Java側が id なら、基本例の指定は keyProperty="id" です。
Java側を userId にしている場合は、keyProperty="userId" と対応させます。SELECT結果のマッピング設定とは別に、生成キーを書き込む先を確認してください。DB側のカラム指定が必要な場合は keyColumn を使います。
IDプロパティに値を設定できる構造になっているか
採番結果を受け取るには、対象オブジェクトのIDへ値を書き込める必要があります。まずは例のように、対応する型のフィールドとsetterを持つ構造で確認すると、受け取り先を追いやすくなります。
不変オブジェクトや record など、生成後に値を書き換えない設計では、この例と同じ方法で代入できるとは限りません。また、ID用の Long だけを引数へ渡しても、呼び出し元の変数そのものを採番結果に置き換えることはできません。
INSERTへ渡したものとは別のインスタンスを見ていないか、採番された値を保持できる型かも確認しましょう。
データベースとJDBCドライバーが採番キー取得に対応しているか
設定名が正しくても、DB・JDBCドライバー・採番方式の組み合わせによって、取得方法や必要な設定は異なります。利用中のバージョンに対応した公式情報と、小さな登録例による確認が必要です。
- 対象カラムに自動採番の定義があり、実際にDB側で値が生成されるか
- 利用中のJDBCドライバーが、そのINSERTの生成キー取得に対応しているか
keyColumnなど、取得対象の指定が必要か- バッチ実行などで、IDを参照した時点ではSQLがまだ実行されていない可能性がないか
原因を切り分ける際は、まず単一行の通常INSERTで、登録件数と引数オブジェクトのIDを確認します。複数行INSERTやバッチ実行では、生成キーと各オブジェクトの対応も別途確認してください。
複数引数や入れ子オブジェクトの指定が正しいか
@Param でオブジェクトに名前を付ける場合は、その名前を含めて受け取り先を指定します。たとえば、先ほどの登録メソッドを次の定義に置き換える場合です。
int insertUser(@Param("user") User user);@Param は org.apache.ibatis.annotations.Param を使用します。対応するXMLでは、keyProperty とSQL中の参照を次のように変更します。
<insert id="insertUser"
useGeneratedKeys="true"
keyProperty="user.id">
INSERT INTO users (name)
VALUES (#{user.name})
</insert>複数引数の場合も、@Param で付けた名前を使い、どの引数へIDを書き込むかを明示します。この形では、最初の例の parameterType="com.example.domain.User" は残さず、省略しています。
引数オブジェクトがさらに user を保持する構造なら、その階層に合わせたプロパティのパスが必要です。入れ子のオブジェクトを事前に生成しておき、SQLの値の参照先とIDの書き込み先が、同じ対象を指していることを確認してください。
selectKeyを検討するケース
INSERT前後に採番用SQLを実行する場合
JDBCの生成キー取得とは別に、採番値を得るSQLを明示的に実行する方法として selectKey があります。INSERTの前にシーケンスから値を取得する設計や、INSERTの後にDB固有の方法で生成値を取得する設計で検討します。
order="BEFORE" はINSERT前、order="AFTER" はINSERT後に selectKey のSQLを実行する指定です。keyProperty で値の格納先、resultType で取得する値のJava型を指定します。
BEFORE方式では、取得した値をIDプロパティへ設定し、その値をINSERT文に渡す構成になります。AFTER方式では、直前の登録に対応する値を取得できるSQLと、接続に関する条件を確認する必要があります。具体的なSQLは、利用するDBに合わせて選びます。
登録後に SELECT MAX(id) でIDを探す方法は避けてください。同時に別の登録が行われると、他のレコードのIDを取得する可能性があります。採番結果は、自分のINSERTに対応することが保証される方法で受け取ります。
useGeneratedKeysとの使い分けを確認する
DB側で生成される主キーをJDBC経由で取得できる構成なら、useGeneratedKeys が候補になります。一方、シーケンスを事前に取得するなど、採番用SQLをアプリ側で明示する設計なら、selectKey を検討します。
IDが入らないからといって、同じINSERTに両方を追加して解決しようとするのではなく、採番方式と取得経路を決めましょう。利用するDB、JDBCドライバー、MyBatisのバージョンに合った設定を確認し、取得したIDが登録行の主キーと一致することを実環境で検証します。
まとめ
MyBatisでINSERT後の自動採番IDを使うときは、引数として渡したオブジェクトに採番結果を受け取り、そのIDを後続処理へ渡す流れを押さえましょう。
useGeneratedKeys="true"でJDBCの生成キー取得を利用するkeyPropertyにJava側のIDプロパティを指定する- 通常のINSERTの戻り値は更新件数で、IDは引数オブジェクトから参照する
- 親子の登録を一組として扱う場合は、同じトランザクションにまとめる
- IDを取得できない場合は、プロパティの指定、書き込み可能な構造、DBとドライバーの対応を確認する
まずは単一レコードの登録で、採番された主キーが同じオブジェクトへ設定されることを確認します。そのうえで後続の登録処理へつなげると、IDの受け渡しとトランザクションの範囲を整理しやすくなります。


コメント