MyBatisで大量データを扱う

こんにちは。ASKULのほかほかごはんです。最近は商品データに関するバッチ開発を担当しています。

バッチ開発では、社内外へデータを連係する際に大量のデータをDBから取得し、csvなどのファイルに加工する機会が多くあります。 本記事では効率的にQuery結果をハンドリングする方法として、 MyBatisの ResultHandler と Cursor を紹介します。

なお、弊社ではSpring Boot上でMyBatisを利用しています。 本記事のサンプルコードもその前提で紹介させていただきます 🙇‍♂️

ResultHandler

ResultHandlerは検索結果を1件単位で処理する仕組みです。 検索結果が大量になった場合でも1件単位のメモリ消費で済むためメモリ枯渇による異常終了や性能劣化が起こる可能性を抑えることができます。

fun writeAllItems(path: Path) {
    MyItemFileWriter(path).use { writer ->
        myItemMapper.findAll(
            resultHandler = { resultContext ->
                writer.write(resultContext.resultObject)
            }
        )
    }
}


@Mapper
interface MyItemMapper {
    @Select(
        """
        SELECT * FROM my_item
        """
    )
    @ResultType(MyItemDto::class)
    fun findAll(
        resultHandler: ResultHandler<MyItemDto>
    )
}

Cursor

CursorもResultHandlerと同じく大量データを効率的にハンドリングする仕組みです。 ResultHandlerよりも直感的にコードを記述できます。

個人的にはResultHandlerよりもCursorの方が好きです 😉

@Transactional(readOnly = true)
fun writeAllItems(path: Path) {
    MyItemFileWriter(path).use { writer ->
        val myItemCursor = myItemMapper.findAll()
        
        myItemCursor.use { cursor ->
            cursor.forEach {
                writer.write(it)
            }
        }
    }
}

@Mapper
interface MyItemMapper {
    @Select(
        """
        SELECT * FROM my_item
        """
    )
    fun findAll(): Cursor<MyItemDto>
}

Cursorの魅力はCollectionのコンテキストでコードを記述できるところです。 複数行をまとめて処理するなどのロジックを入れ込みやすくなります。

@Transactional(readOnly = true)
fun writeAllItems(path: Path) {
    MyItemFileWriter(path).use { writer ->
        val myItemCursor = myItemMapper.findAll()

        val tmpDtoList = mutableListOf<MyItemDto>()
        myItemCursor.use { cursor ->
            cursor.forEach {
                // 例えば100件ごとに出力
                tmpDtoList.add(it)
                if (tmpDtoList.size() >= 100) {
                    writer.writeAll(tmpDtoList)
                    tmpDtoList.clear()
                }
            }
        }

        if (tmpDtoList.isNotEmpty() writer.writeAll(tmpDtoList)
    }
}

iteratorを使っての処理も可能です。

@Transactional(readOnly = true)
fun writeAllItems(path: Path) {
    MyItemFileWriter(path).use { writer ->
        val myItemCursor = myItemMapper.findAll()
        myItemCursor.use { cursor ->
            val iterator = cursor.iterator()
            
            while (iterator.hasNext()) {
                val dto = iterator.next()
                // 例えば特定の行をskip
                if (dto.isFoo()) {
                    continue
                }
                
                writer.write(dto)
            }
        }
    }
}

まとめ

MyBatisで大量データを扱う際はResultHandlerかCursorを利用して省メモリかつ高速にデータハンドリングを行いましょう 🏎️

参考

mybatis.org

ASKUL Engineering BLOG

2021 © ASKUL Corporation. All rights reserved.