MybatisでのID自動採番とSQL操作について
IT技術
はじめに
わりとDBでINSERT時に自動採番したIDを、すぐ後の処理で使いたい時ってあるかなと思います。
その時の操作をMybatisを利用して対応していった時の話です。
実現したいもの
今回、ユーザ情報を登録したり、更新したり、閲覧したりした時のログをDBレコードとして残して、
ユーザ行動ログ一覧として表示したい、ということがありました。
SpringでのAPI開発で、言語はKotlin、DBはMySQLでの対応となります。
APIとして作るのは以下の3つとなります。
・新規登録用のAPI
・更新登録用のAPI
・表示用の一覧API
<ユーザ行動ログ一覧>(表示用の一覧API)
| No | 名前 | 行動 | 実行日時 | 
|---|---|---|---|
| 1 | ユーザA | email更新 | 2025/05/18 12:34:56 | 
| 2 | ユーザB | 新規登録 | 2025/05/15 18:00:10 | 
| 3 | ユーザA | 新規登録 | 2025/05/14 11:05:32 | 
※実行日時の降順表示
今回は新規登録用のAPIに注目して話していきたいと思います。
DBの構成と取得イメージ
DBの構成としては、以下の2つのテーブルを準備しました。
・usersテーブル(ユーザ情報のテーブル)
・user_logテーブル(ユーザ行動ログのテーブル)
usersテーブルには、名前やemailなどの一般的な情報として作成します。
これに対して、user_logテーブルには、
そのuserのidを持たせ、その時の行動をuser_processに入れていきます。
それぞれidは自動採番されるようにしています。
1【テーブル構成】
2CREATE TABLE users (
3  id BIGINT AUTO_INCREMENT PRIMARY KEY,
4  name VARCHAR(255) NOT NULL,
5  email VARCHAR(255) NOT NULL,
6  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
7);
8CREATE TABLE user_log (
9  id BIGINT AUTO_INCREMENT PRIMARY KEY,
10  user_id BIGINT NOT NULL, // 外部キー。usersテーブルのidと紐付く
11  user_process VARCHAR(255) NOT NULL,
12  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
13);この2つのテーブルを以下のようなSQLで取得すれば、
最初の一覧が表現出来るかなと。
1【取得SQL】
2SELECT
3  users.name,
4  user_log.user_process,
5  user_log.created_at
6FROM
7  users
8INNER JOIN
9  user_log
10ON 
11  users.id = user_log.user_id
12ORDER BY
13  user_log.created_at DESC新規登録の時のIDはどうやって取得するか
新規登録の時は、1つのAPI処理の中でusersテーブルに登録しつつ、user_logテーブルにも登録する処理を一度に行うとします。
ここで、新規登録時のusersテーブルのidを、どのようにuser_logテーブルのuser_idに渡すかを考えます。
usersテーブルのidは自動採番なのでINSERT処理内で作られます。
なのであらかじめidを決めて設定する、といったことは出来ない形となります。
以下に簡単なMapperの例を。
DB処理はMybatisのアノテーション利用してのアクセスです。
1【userMapper】
2@Insert(
3        """
4        INSERT INTO users(
5            name, 
6            email
7        ) VALUES (
8            #{name}, 
9            #{email}
10        )
11        """
12)
13fun insert(user: User): Int1【userLogMapper】
2@Insert(
3        """
4        INSERT INTO user_log (
5            user_id, 
6            user_process
7        ) VALUES (
8            #{userId}, 
9            #{userProcess}
10        )
11        """
12)
13fun insert(userLog: UserLog): Int1【Userのdataクラス】
2data class User(
3    val id: Long?,
4    val name: String?,
5    val email: String?
6)1【UserLogのdataクラス】
2data class UserLog(
3    val id: Long?,
4    val userId: Long?,
5    val userProcess: String?,
6)1【Serviceクラス】
2fun insertUser(){
3    // 入力ユーザ情報
4    val user = User(
5        id = null,
6        name = "ユーザA",
7        email = "aaa@bbb.com"
8    )
9
10   // userResult には登録件数(1)が返ってくる
11  val userResult = userMapper.Insert(user = user)
12
13   val user = UserLog(
14        id = null,
15        userId = ?, //← userのidをどこから取るべきか?
16        userProcess = "新規登録"
17    )
18
19  val userLogResult = userLogMapper.Insert(userLog = userLog)
20
21}まず考えられるのは、userのinsert後にselect文で取得する方法です。
MySQLだと「SELECT LAST_INSERT_ID」といったものがあります。
1@Select("SELECT LAST_INSERT_ID()")
2fun latestId(): Long?1【Serviceクラス】
2(中略)
3  val userResult = userMapper.Insert(user = user)
4
5   val latestId = userMapper.latestId()
6
7   val user = UserLog(
8        id = null,
9        userId = latestId, ← select取得してきた値
10        userProcess = "新規登録"
11    )
12
13  val userLogResult = userLogMapper.Insert(userLog = userLog)
14
15}ただ、これだとSQL増えて面倒だなぁと。
もう少しシンプルにしたい感じで。
Mybatisの@Optionsを使ってみる
同様の取得をMybatisの@Optionsアノテーションでも出来るのでそちらでも試してみました。
記述の仕方としてはMapperのメソッドに追記する感じです。
こちらのほうがシンプルで良さそうかなと。
1【userMapper】
2@Insert(
3        """
4        INSERT INTO users(
5            name, 
6            email
7        ) VALUES (
8            #{name}, 
9            #{email}
10        )
11        """
12)
13@Options(useGeneratedKeys = true, keyProperty = "id")
14fun insert(user: User): Int@OptionsアノテーションのuseGeneratedKeysを「true」にし、
keyPropertyに「id」を指定することによって、
idの自動採番後にdata class「User」の「id」に設定されます。
1【Serviceクラス】
2fun insertUser(){
3    // 入力ユーザ情報
4    val user = User(
5        id = null,
6        name = "ユーザA",
7        email = "aaa@bbb.com"
8    )
9
10  val userResult = userMapper.Insert(user = user)
11
12   val userLog = UserLog(
13        id = null,
14        userId = user.id, ← 自動採番されたidが入っている
15        userProcess = "新規登録"
16    )
17
18  val userLogResult = userLogMapper.Insert(userLog = userLog)
19
20}1【実行結果の例】※ idは10番まで採番済み
2// userMapper.Insert 実行前のuser
3User(id=null, name=ユーザA, email=aaa@bbb.com)
4// userMapper.Insert 実行後のuser
5User(id=11, name=ユーザA, email=aaa@bbb.com)この場合、data classのidがvalでも、idを設定してくれるようです。
valは基本的に値が変更出来ないイメージなので、ここはちょっと注意しないといけないかもしれないです。
Mybatisのforeachタグも併せて使ってみる
上記は1件のINSERTについてでしたが、複数件まとめてInsertする場合も考えてみます。
単純なのは、上記の「userMapper.Insert(user = user)」にループかける感じかなと思いますが、これだとSQL文を件数分発生させるので処理速度的にもあんまり良くないかなと思います。
ここで、Mybatisを利用してるのでタグを使って複数行INSERTを実行します。
1【userMapper】
2@Insert(
3    """
4    <script>
5        INSERT INTO users(
6            name, 
7            email
8        ) VALUES 
9        <foreach collection="users" item="user" separator=",">
10        (
11            #{user.name}, 
12            #{user.email}
13        )
14        </foreach>
15    </script>
16    """
17)
18@Options(useGeneratedKeys = true, keyProperty = "id")
19fun bulkInsert(users: List<User>): Int「collection」で引数のListを指定し、Listの中身を「item」で変数名"user"として設定し、「separator」を"," にします。
これによって、
1INSERT INTO users(name, email) 
2VALUES 
3(users[0]),
4(users[1]),
5・・・
6(users[n])といった複数行INSERTを実現しています。
肝心のidはちゃんと設定されているか、実行してみます。
前述の処理のuserMapper.InsertをuserMapper.bulkInsertに差し替えて、データを3件で試してみました。
1【実行結果の例】※ idは10番まで採番済み
2// userMapper.bulkInsert 実行前のusersList
3[
4User(id=null, name=テストユーザー11, email=test11@example.com), 
5User(id=null, name=テストユーザー12, email=test12@example.com), 
6User(id=null, name=テストユーザー13, email=test13@example.com)
7]
8// userMapper.bulkInsert 実行後のusersList
9[
10User(id=11, name=テストユーザー11, email=test11@example.com), 
11User(id=12, name=テストユーザー12, email=test12@example.com), 
12User(id=13, name=テストユーザー13, email=test13@example.com)
13]複数行の処理でも期待値どおりのidは設定されてました。
なので@Optionsを利用すればだいぶシンプルな記述で対応が出来そうでした。
これで、今回対応したかった新規登録APIについては、実現可能となります。
まとめ
Mybatisはアノテーションといくつかの設定するだけでidのマッピング等の操作をしてくれるので、今回みたいな自動採番のIDを取ってきたりするのには便利かなと思います。
今回はMySQLでの話でしたが、SQLによってはその言語の中で同じようなこと出来たりするものもあるかと思うので、必要に応じて取捨選択が出来ると良いかなと思います。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
元ファストフード店長代理のJava系ITエンジニア。 Webサイト系の開発や運用をいくらか経験し、 現在はAndroidアプリ開発を主に担当したり。 休みの日はゲームとか風景写真撮りに行ったりとかマラソンしたりとか。







