MyBatis + FreeMarkerを使用した環境でレコード追加時にAuto IncrementされたIDを取得する

f:id:excite-kazuki:20210513111813p:plain

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 MyBatisとFreeMarkerを使用した環境において、INSERT文を実行したときにAuto IncrementされたIDが取得できない問題とその解決策についてまとめました。

MyBatisとFreeMarkerを使用した環境

現在のチームでは、簡単なSELECT文やINSERT文であれば、MyBatis Generatorで生成したマッパーを使用してSQLを実行しています。 しかし、結合をする必要があるクエリーやその他複雑なクエリーは、FreeMarkerを使用するようにしています。 MyBatisとFreeMarkerに加えて、下記環境にて開発をしています。

詳細は下記記事にまとめています。 tech.excite.co.jp

問題点:MyBatis Generatorで自動生成生成したマッパーでINSERTを実行するとIDが取得できない

下記の3つをカラムに持つ書籍テーブルがあり、book_idはAuto IncrementされるIDであるとします。

  • 書籍ID(book_id)
  • タイトル(title)
  • 著者(author)

このとき、自動生成したマッパーを使用してレコードを追加するときは下記のように記述します。

@Repository
@RequiredArgsConstructor
public class BookRepositoryImpl {
    private final BookMapper mapper;
    
    public Long create(String title, String author) {
        final Book book = new Book();
        book.setTitle(title);
        book.setAuthor(author);
        return mapper.insertSelective(book);
    }
}

自動生成したマッパーのコードを見ても、intが返り値となっているため、なんだかIDが返ってきそうな感じがします。

@Generated(value="org.mybatis.generator.api.MyBatisGenerator")
default int insertSelective(Book record) {...}

しかし、insertSelectiveの返り値はINSERT文で影響を与えた件数が返るため、IDが返ってくるわけではありません。 そのため、なんらかの方法でIDを取得する必要があります。

解決策:OUTPUT句を使用する

SQL Serverでは、OUTPUT句を使用することで、INSERT / DELETE / UPDATE で追加 / 削除 / 更新された行のデータを取得することができます。 ここでは、INSERT文でレコードを追加したときのAuto IncrementされたIDを取得するために使用します。

docs.microsoft.com

まず、下記のようにftlファイルを記述します。

INSERT INTO book (
    title,
    author
)
OUTPUT
    inserted.book_id
VALUES (
    <@p name="title"/>,
    <@p name="author"/>
);

次に、Javaでは下記のようなカスタムマッパーを記述します。 このように記述することで、ftlファイルに記述したSQLを実行することができ、book_idを返り値として受け取ることができます。

@Mapper
public interface BookCustomMapper {
    @Lang(FreeMarkerLanguageDriver.class)
    @Select("insert_book_return_book_id.ftl")
    Long insert(
            @Param("title") String title,
            @Param("url") String author
    );
}

ここで、INSERT文を実行しているのに、@Select("insert_book_return_book_id.ftl")SELECT文のアノテーションを使用していることに注意する必要があります。 @Insertを使用してしまうと、OUTPUT句で指定したbook_idを取得することができません。

補足

現在のプロジェクトでは上記の解決策でAuto IncrementされたIDを取得しています。 上記解決策の他にIDを取得することができる方法についてまとめます

  1. 生成したマッパーに@SelectKey@Option("useGeneratedKeys = true") を付与する
  2. generatorConfig.xml<table> <generatedKey ... /> </table>を記述する
  3. INSERT文を実行した後に、SELECT SCOPE_IDENTITY()を実行する

1については、MyBatis Generatorで生成したマッパーにアノテーションを付与しなくてはならないため、 マッパーを再度自動生成したときにアノテーションが消えてしまうといった問題があります。

2については、INSERT文を実行したときにIDが欲しいと思ったタイミングで、generatorConfig.xmlXMLを記述し、その後自動生成を行うため手間がかかります。

3については、自分の環境で再現できなかったため断念しました。

まず、下記のようなカスタムマッパーを作成しました。 次に、生成したマッパーを使用してINSERT文を実行した後に、get_last_insert_id()を実行したところ、IDを取得することができなかったです。(nullになってしまいました) 本件についてわかる方がいたら教えていただけると嬉しいです!

@Mapper
public interface LastInsertIdCustomMapper {
    @Select("SELECT SCOPE_IDENTITY()")
    Long get_last_insert_id();
}

おわりに

MyBatisとFreeMarkerを使用した環境において、INSERT文を実行したときにAutoIncrementされたIDが取得できない問題とその解決策についてまとめました。 「IDの取得くらい簡単にできるだろう」と思っていましたが、想定していたよりも時間を使ってしまいました。 本記事を見ていただいた方のお役に立てれば幸いです。